news 2026/10/2 15:02:56

SpringBoot+Vue+MyBatis+MySQL健康管理系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue+MyBatis+MySQL健康管理系统实战

前几年做这类信息管理系统的项目特别多,很多刚入门的同学第一次接触企业级开发就是从一套“SpringBoot + Vue + MyBatis + MySQL”的前后端分离项目开始的。这个项目就是一套非常典型的例子:人员健康信息登记、每日打卡、出入审批、异常上报、统计分析,再加上管理后台和用户端,基本把业务系统的常见模块都覆盖了。今天我把它从业务设计、表结构、后端接口、前端页面,到最后的打包部署和踩坑记录完整过一遍,你能照着把它跑起来,也能把这个项目的思路迁移到其他任何管理类系统上。

这套技术栈看着老生常谈,但它恰好是前后端分离项目里最经典、也最稳的组合:SpringBoot负责把后端服务跑起来,MyBatis处理数据库访问,MySQL存数据,Vue做前端交互。你把这套流程走通一次,等于把JavaWeb开发的整条链路都摸了一遍,后面再去看若依这类快速开发框架,或者自己从零写一套系统,都不会一头雾水。

我不打算只讲业务怎么实现,因为这类系统最核心的价值是让你搞清楚几个问题:表到底怎么设计才合理?前端请求后端的时候鉴权是怎么做的?所谓前后端分离,分离的到底是什么?还有最后部署的时候为什么总有一堆奇奇怪怪的问题?这些问题我会根据我实际开发、调试的经验展开说,源码里没有体现的细节我也会补上。

1. 从需求出发,拆解这套管理系统的核心设计

1.1 业务闭环与角色划分

很多人拿到项目就开始建表写CRUD,这是大忌。写管理系统之前,必须先把业务闭环想清楚。这套疫情防控管理系统的核心流程其实可以抽象成一个闭环:人员信息录入 → 每日健康状态上报 → 出入申请与审核 → 登记留痕 → 异常数据告警 → 管理层查看统计。每一环都对应一组页面和一批接口,数据库表就是围绕这条链路设计的。

系统里至少会有三类角色:管理员、普通用户、审核人员。

  • 管理员:管人员名单、管角色权限、看统计报表。
  • 普通用户:填健康信息、发起出入申请、查看自己的历史记录。
  • 审核人员:审核出入申请、登记访客信息、处理异常上报。

这三类角色的操作范围差异很大,所以权限模型是这套系统的地基。一般会做成经典的RBAC模型:用户表、角色表、权限表,再加上用户角色关联表和角色权限关联表。有人觉得这种五张表的结构麻烦,但在真实项目里几乎都是这个套路,因为权限一变,硬编码在代码里就非常痛苦。

1.2 技术选型:为什么这样搭是合理的

技术选型不是越新越好,而是要看场景和团队熟悉度。SpringBoot之所以是首选,是因为它的自动配置能把项目启动成本压得很低,内嵌Tomcat让部署也简单,不用额外装容器。Vue在几年前还是最多人用的前端框架,组件化和数据响应式这两点,就足够支撑一个中小型管理后台了;配合Element UI或Element Plus,表格、表单、弹窗这些后台高频组件都能直接复用。

数据库这块,MySQL依然是中小型项目的稳妥选项,生态成熟、问题资料多。有人会纠结到底用MyBatis还是JPA,我个人的经验是:如果是报表统计、多表关联、复杂条件查询比较多的系统,MyBatis更合适,因为SQL是你自己写在XML里的,执行过程完全可控,出了问题也好排查。JPA在简单的单表CRUD里确实省事,但一旦牵涉到动态查询和性能优化,写起来反而不顺手。

至于为什么不直接拿若依这种现成的快速开发框架改,我的观点是:学习阶段一定要亲手从零搭一遍,哪怕丑一点、功能少一点,好过直接套一个看不懂的脚手架。看完我这个项目,你再去看若依,会发现很多东西都能对应上,那时候框架对你来说就不是黑盒了。

2. 数据库设计、MyBatis配置与后端核心实现

2.1 表结构设计:先把最重要的几张表拆清楚

我不管接手什么项目,只要涉及信息管理,第一件事永远是设计表结构。表设计不合理,后面接口写得再漂亮也白搭。这个项目里最重要的几张表我直接给出参考SQL,字段就是按业务闭环来定义的。

-- 用户表 CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `id_card` varchar(18) DEFAULT NULL COMMENT '身份证号', `avatar` varchar(255) DEFAULT NULL COMMENT '头像文件地址', `status` tinyint(4) DEFAULT 1 COMMENT '1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 健康打卡记录表 CREATE TABLE `health_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `temperature` decimal(4,2) DEFAULT NULL COMMENT '体温', `health_status` tinyint(4) DEFAULT 1 COMMENT '1正常 2异常', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `record_date` date NOT NULL COMMENT '打卡日期', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_date` (`user_id`, `record_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日健康打卡记录'; -- 出入登记表 CREATE TABLE `access_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) DEFAULT NULL, `visitor_name` varchar(50) DEFAULT NULL COMMENT '访客姓名', `visitor_phone` varchar(20) DEFAULT NULL, `direction` tinyint(4) DEFAULT 1 COMMENT '1进入 2离开', `temperature` decimal(4,2) DEFAULT NULL, `audit_status` tinyint(4) DEFAULT 0 COMMENT '0待审核 1通过 2驳回', `apply_time` datetime DEFAULT NULL, `audit_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出入登记表';

有几个设计细节值得多说一句。第一,所有时间字段统一用datetime而不是timestamp,datetime没有2038年问题和时区换算的坑,对这类管理系统来说足够了。第二,状态类字段用tinyint不用char或varchar,查询效率高,写代码时用常量类或者枚举去对应,不要在代码里散落裸数字。第三,索引不要乱加,像health_record表里user_id和record_date组合查询频率很高,就建联合索引;字段值区分度低的列比如status,单独加索引反而可能拖慢写入。

用户表里的密码字段长度我给了100,因为一般用BCrypt加密,加密结果接近60个字符,长度不够存储时你就等着上线当天翻车吧。id_card字段虽然是身份证号,但注意不要数据库里密文存储,除非你愿意每次查询都解密;实际项目里更常见的做法是展示时脱敏,比如中间几位打星号,然后接口返回的时候由后端统一处理。

2.2 MyBatis的关键配置:TypeHandler、缓存与动态SQL

MyBatis在这个系统里的作用就是让SQL保持可读、可控。我特别不建议用mybatis-plus一键生成的CRUD来做核心业务,看起来快,但你会丧失对SQL的执行细节的掌控。

先说说TypeHandler。很多教程里一句话带过,但实际开发里非常实用。比如user表的id_card字段需要加密存储,你可以在插入和查询时用自定义TypeHandler自动完成加解密,Java代码里反复调用加解密工具类这种低级重复就可以省掉了。自定义TypeHandler的核心是继承BaseTypeHandler,重写setNonNullParameter和getNullableResult几个方法。

@MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(String.class) public class EncryptTypeHandler extends BaseTypeHandler<String> { private static final String KEY = "your-secret-key"; @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, encrypt(parameter)); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return decrypt(rs.getString(columnName)); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return decrypt(rs.getString(columnIndex)); } // 这里省略encrypt/decrypt的具体实现,可以用Hutool的AES工具类 }

然后是缓存。MyBatis自带一级缓存和二级缓存,很多人图省事直接开二级缓存,但我要强调一下:在这个项目里,二级缓存默认就好。原因很简单,二级缓存是Mapper级别的,查询结果会把对象放在JVM堆内存里,如果多个节点部署,缓存数据会出现不一致。而且这系统里有大量用户状态、出入审核这类高频更新数据,缓存命中率低,还白白占内存。如果将来并发量真的大到需要缓存,优先考虑Redis做分布式缓存,把逻辑控制在Service层,而不是依赖MyBatis缓存。

写Mapper XML时尽量用好动态SQL。比如出入记录查询,查询条件有方向、状态、时间范围,每一种组合都要支持,直接用<where>加<if>标签解决,SQL拼接时注意最后一个条件不能多出and,<where>标签会自动去掉前缀的and或or。另外,大批量插入健康打卡记录时,一条一条insert效率很低,用<foreach>标签拼接批量insert,一次插入几百条性能提升非常明显,但注意SQL的长度限制,控制在500条一批比较合适。

2.3 登录鉴权与权限控制:JWT加拦截器是标准打法

你说前后端分离项目,分离的最核心点之一就是前端拿不到Session,因为前后端不在同一个Web容器里,传统基于Session的登录态管理在这个架构里会非常别扭。所以现在这类系统基本都走JWT方案。

JWT的流程很简单:用户登录成功后,后端生成一个token返回给前端,前端存到localStorage或pinia里,每次请求通过Authorization请求头带过去,后端拦截器校验token,解析出用户信息再放行。关键在于token过期时间的设计,太短了用户频繁重新登录,体验差;太长了有安全风险。我一般习惯access token有效期2小时,用户操作时前端拦截401自动跳登录页。

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals("OPTIONS")) { return true; // 放行预检请求 } String token = request.getHeader("Authorization"); // 校验token,解析成功则把userId放入ThreadLocal Long userId = JwtUtil.parseToken(token.replace("Bearer ", "")); UserContext.setUserId(userId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); // 一定要清理,否则线程复用会串号 } }

有一个坑我必须重点提,就是ThreadLocal用完一定要在afterCompletion里清理。Tomcat的线程池是复用的,这条线程处理完你的请求之后不会被销毁,下一个请求可能继续用同一根线程。你要是不清理ThreadLocal,轻则下个用户拿到上个用户的信息,重则出现“用户串号”这种严重事故。这样的问题在测试阶段很难发现,因为并发不够、线程池不紧张的时候复用的概率低,一上线就炸。

权限控制方面,我建议用一个自定义注解加AOP来做,比如@RequireRole("admin")标注在管理员接口上,切面里从UserContext取出当前用户角色再判断,比在Service方法内部写一堆if判断要清爽得多。也不用把权限粒度做成按钮级那么细,对这类管理系统来说,菜单级和接口级权限已经够用了。

3. 前端Vue的工程化落地与页面实现

3.1 环境准备与项目初始化

前端部分虽然不用你造轮子,但Vue环境配置里值得花点篇幅说。我见过太多同学卡在Node版本上。如果你用的是Vue3加Vite,Node版本需要16以上,但也不是越高越好,Node 17以上在某些老项目中会因为OpenSSL版本变化报错,提示error:0308010C:digital envelope routines::unsupported,解决办法是降级Node或加启动参数,这在我做项目时出现过。稳妥起见,直接用Node 16.20.2或Node 18的LTS版本,能省掉很多莫名其妙的坑。

初始化项目我用的是Vite:

npm create vite@latest health-ui -- --template vue cd health-ui npm install

然后再装项目依赖:vue-router做路由、pinia做状态管理、axios发请求、element-plus做UI组件库。我见过很多人项目一上来就装一堆UI插件和工具库,我不建议这样做,用到什么装什么,项目依赖越少,后续升级和排错越容易。

3.2 Axios封装与路由权限设计:前端安全的第一道关

前端工程化一定要提的,是请求的统一封装。所有请求都走同一个axios实例,在请求拦截器里追加token,在响应拦截器里统一处理业务状态码和HTTP状态码,这样就不用每个页面都写一遍错误处理逻辑。

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 15000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res.data } if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('登录已过期')) } ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg || '请求失败')) }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request

路由权限这块,初学者可以直接用静态路由加路由守卫的方式:在meta里标注需要的角色,beforeEach里判断当前用户角色能不能访问。管理员和普通用户的菜单是在前端写死的,或者登录后根据角色动态渲染。如果想做得更接近企业级,可以设计成动态路由:管理员登录后拿到权限码列表,通过router.addRoute方法把对应的路由动态加进去。动态路由看起来高级,但有一个很头疼的问题:刷新页面时动态路由会丢失,必须在路由守卫里同步做一次“从后端重新拉取权限并恢复路由”的操作。我在项目里做这一步的时候花了不少时间排查,最终方案是把用户权限信息存到pinia里,并且初始化一个标志位,如果刷新后发现标志位为false,就先调接口恢复路由,再放行导航。

3.3 核心页面功能拆解:打卡、出入审批、统计看板

登录页是门面,一般会加一个图形验证码,避免接口被脚本刷爆。图形验证码在后端生成,返回给前端一张图片和一个缓存在Redis里的codeKey,用户提交时把codeKey和验证码一起传回去校验。这个逻辑在SpringBoot里通常用Hutool的CaptchaUtil就能实现,几分钟搞定,但它能挡住低级别的自动化攻击,值得加上。

首页的统计看板是我觉得这个项目最出效果的部分。它要展示今日打卡人数、异常人数、待审核申请、近七日趋势这四类信息。后端一次性返回一个聚合DTO,前端用ECharts画趋势图和饼图。这种页面做出来很有成就感,但你要明白它的性能关键在后端SQL——趋势数据不能循环查询数据库,应该用一条GROUP BY语句按日期聚合,然后按日期补零。比如这样:

SELECT record_date, COUNT(*) AS cnt FROM health_record WHERE record_date >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY record_date

查询出来的结果如果某天没有记录,那这天的数据是不存在的,前端要预处理补零,否则图表上就少一根柱子,看起来像漏数据一样。

健康打卡页面本身不复杂,一个表单提交到后端,后端做唯一性校验:一个用户一天只能打卡一次,重复提交要提示。这个校验不能只靠前端按钮置灰,后端必须查一下当天是否已有记录,用联合索引(user_id, record_date)加一条select存在性判断就可以。出入审批页面则是典型的列表加操作按钮模式:表格展示待审记录,管理员点通过或驳回,通过后系统自动生成一条进出记录,驳回要填写原因。需要注意审批动作的并发问题,同一张订单或记录被两个人同时审批,所以后端update语句里最好带上audit_status=0这个条件,如果更新影响行数为0,说明已经被处理过,直接提示“该记录已处理”。

4. 文件上传与对象存储:MinIO集成避坑

4.1 为什么文件上传不能直接存本地磁盘

用户头像、身份证明材料、异常上报时的截图,这些文件如果直接存到服务器本地目录,开发阶段看起来没问题,但部署时就有隐患:第一,前后端分离项目的前端静态资源、后端文件目录如果都在同一台机器上还可以,一旦将来部署到多台服务器,用户上传的文件在A服务器,请求被负载均衡转发到B服务器就找不到了。第二,本地磁盘挂载、备份、迁移都麻烦。

常见的正规做法是上传到对象存储。企业项目很多用阿里云OSS或腾讯云COS,但个人练手项目用MinIO就够了。MinIO是开源软件,兼容亚马逊S3接口,可以部署在自己的机器上,而且官方对SpringBoot有比较友好的SDK支持。我把MinIO集成到这个项目里,你会发现代码量很小,但节省了非常大的沟通成本——前后端之间只约定URL,不关心文件存到哪台机器上。

4.2 SpringBoot集成MinIO的完整过程

第一步是部署MinIO服务端,这里可以用Docker一条命令完成:

docker run -d --name minio \ -p 9000:9000 -p 9001:9001 \ -e MINIO_ROOT_USER=minioadmin \ -e MINIO_ROOT_PASSWORD=minioadmin \ -e MINIO_DEFAULT_BUCKETS=health-file \ minio/minio server /data --console-address ":9001"

9000是API端口,9001是Web控制台端口。然后后端引入依赖,写一个配置类读取endpoint、accessKey、secretKey,提供一个MinioService封装上传和生成访问URL的方法。

minio: endpoint: http://127.0.0.1:9000 access-key: minioadmin secret-key: minioadmin bucket: health-file
public String upload(MultipartFile file, String objectName) throws Exception { // 检查bucket是否存在,不存在则创建 boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); if (!found) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); } minioClient.putObject( PutObjectArgs.builder() .bucket(bucket) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); return getObjectUrl(objectName); }

很多人在这一步栽跟头的地方是:MinIO的endpoint写的是http://localhost:9000,但前端部署在Nginx上,浏览器访问的回来的是localhost,根本打不开图片。解决办法是访问URL不要后端拼好返回,前端只要拿到objectKey,由前端或Nginx统一拼完整地址;或者后端返回的URL改成自定义域名或者服务器的内网地址。如果只是为了练手,直接让MinIO的bucket权限设为public并返回http://服务器IP:9000/bucket/objectKey也是可以的,生产环境再套一层签名URL或CDN。

另外SpringBoot默认的单次上传文件大小是1MB,超过就会报MaxUploadSizeExceededException。这个坑在本地开发时不太容易碰到,因为你传的头像可能都很小,一旦传健康证明截图这种几MB的图片就会触发。解决办法是写一个MultipartConfigElement的Bean,把maxFileSize和maxRequestSize调大,建议文件上限10MB,请求上限10MB。

@Bean public MultipartConfigElement multipartConfigElement() { MultipartConfigFactory factory = new MultipartConfigFactory(); factory.setMaxFileSize(DataSize.ofMegabytes(10)); factory.setMaxRequestSize(DataSize.ofMegabytes(10)); return factory.createMultipartConfig(); }

5. 构建打包与前后端分离部署教程

5.1 后端Maven打包:这一步踩过的坑比代码多

到部署环节,问题开始集中爆发。后端项目在IDEA里跑得好好的,一打包部署就各种报错,我最早也是这样。先把后端项目用Maven打成一个可执行jar包:

mvn clean package -DskipTests

生产环境不建议跳过测试跳过得太随意,这里跳过是因为测试环境不一定会配数据库和MinIO,跑测试反而容易失败。打完包后在target目录下会生成一个xxx.jar,通过java -jar xxx.jar就能启动。但如果你项目里用了SpringBoot的profile,不同环境配置不一样,启动时加参数指定:

java -jar health-system.jar --spring.profiles.active=prod

有一个坑我必须单独说:如果用Maven打包时没有把src/main/resources里的XML文件打包进去,运行时大概率会报Invalid bound statement (not found),这是因为MyBatis的Mapper XML默认不会被打进jar的classes目录下。解决办法是在pom.xml的build节点中显式声明把mapper目录下的xml文件作为resources资源打包进去。

<build> <resources> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.xml</include> <include>**/*.yml</include> </includes> </resource> </resources> </build>

有些同学习惯用war包部署到外部Tomcat,把SpringBoot项目改成war其实是可以的,步骤是修改pom的packaging为war,启动类继承SpringBootServletInitializer并重写configure方法。但我的建议是:既然用了SpringBoot,就直接用jar加内置Tomcat跑,外部Tomcat反而多一层配置和兼容性问题。前后端分离项目的部署架构应该是:SpringBoot容器自己跑,Nginx负责托管前端静态文件并做反向代理。

5.2 前端打包、Nginx配置与跨域

前端打包之前,需要检查Vite的base和接口代理配置。很多人打完包之后页面打开是空白,控制台还报404,多半就是base没配置对。Vite默认的base是/,如果你的项目部署在域名根路径上,那没问题;如果你的话要部署在http://ip:80/health-ui这种子路径下,就必须把base改成/health-ui/,否则所有JS和CSS资源都是绝对路径,全部404。

// vite.config.js export default defineConfig({ base: '/', plugins: [vue()], server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

开发环境的跨域通过Vite的proxy解决,前端请求/api/xxx,Vite帮你转发到后端的8080端口,浏览器角度看是同源的,所以不会出现跨域问题。打包部署时的跨域由Nginx统一处理,这是目前最标准的做法。在nginx.conf里配置一个反向代理:

server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # 关键:前端路由history模式必须加这行 } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

try_files $uri $uri/ /index.html;这行,刚才我说是关键的。因为前端用的vue-router是history模式,访问/dashboard这个路径时服务器上并没有真实的dashboard目录和文件,没有这行配置就会404。有了try_files,Nginx会先看有没有对应文件,没有就统一返回index.html,由前端路由接管解析。我想很多同学部署完后一刷新页面就白屏,十有八九就是这个配置漏了。

location /api/里的proxy_pass也是有讲究的。我看到很多教程直接写proxy_pass http://127.0.0.1:8080;,不带最后那个斜杠。这里斜杠的有无会直接影响转发路径:带斜杠是/api/xxx的/api前缀会被替换为空,转发到后端是/xxx;不带斜杠是完整路径转发,后端也要有/api前缀。两种写法都可以,但你必须保证Nginx的替换规则和后端接口的RequestMapping保持一致,否则你会在登录的时候卡半天,一直报404。

如果把前端文件直接放到SpringBoot的resources/static目录下,让SpringBoot同时提供前端页面和后端接口,这就不叫前后端分离了,虽然能运行,但完全失去了分离架构的灵活性。所以建议老老实实用Nginx托管前端。

5.3 MySQL安装配置与常见连接问题

MySQL的安装本身不算难,但我见过的坑主要集中在版本和连接参数上。如果是新环境,直接用MySQL 8.0以上版本,JDBC驱动要用com.mysql.cj.jdbc.Driver,老版本的com.mysql.jdbc.Driver在新驱动里已经废弃了。数据库连接URL建议写成:

spring: datasource: url: jdbc:mysql://127.0.0.1:3306/health_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

serverTimezone=Asia/Shanghai这个参数不加,默认时区是UTC,你在页面上看到的新增记录时间会比实际时间差8小时。useSSL=false,这是应对MySQL 8的一个经典报错——它默认要求SSL连接,如果没有正确配置SSL证书,会报Communications link failure或SSL connection error。本地开发直接禁掉SSL最省事,生产环境要走内网链路的话也可以不用,真需要加密传输就单独配SSL证书。还有allowPublicKeyRetrieval=true,MySQL 8的caching_sha2_password认证插件下,如果不用SSL,会报Public Key Retrieval is not allowed,这个是必加的。

6. 踩坑实录与排查技巧汇总

6.1 高频问题速查表

做这个项目的过程中,我整理了下面这些高频问题,全部都是实际报错现场。给正在跑源码的同学一份速查,能少掉一半头发。

报错现象根本原因解决方案
启动报Failed to configure a DataSource没有配置数据源或配置没生效检查application.yml里spring.datasource配置,确认数据库已创建
请求报Invalid bound statement (not found)Mapper XML没有打包进classespom.xml的resources节点显式声明include**/*.xml
登录接口报SSL connection errorMySQL 8默认要求SSLURL加useSSL=false&allowPublicKeyRetrieval=true
页面能打开但所有接口404Nginx转发路径里proxy_pass斜杠写错确认location前缀和目标URL的替换规则一致
刷新后路由404history模式没配try_filesnginx location / 加try_files $uri $uri/ /index.html;
上传文件报MaxUploadSizeExceededExceptionSpring Boot默认限制1MB自定义MultipartConfigElement调大文件上限
前端跨域报CORS错误开发环境没有配置代理,或Nginx未做转发开发环境用Vite proxy,生产环境用Nginx反向代理
日期字段比北京时间少8小时JDBC连接URL没设置时区加serverTimezone=Asia/Shanghai
多处请求拿到同一个用户的数据ThreadLocal没做清理拦截器afterCompletion里调用clear()

6.2 定位问题的思路:从日志到代码

我调试这套系统时有个习惯:遇到问题先看后端日志,不要一上来就翻前端代码。前端逻辑链路短,后端日志往往直接告诉你异常点和堆栈信息。SpringBoot的日志默认级别是INFO,但排查问题时建议临时把MyBatis的日志级别调成DEBUG,在application.yml里加上:

logging: level: com.example.health.mapper: debug

这样控制台会打印出每条SQL语句和传入的参数,你会发现很多“接口结果不对”的问题根源其实都在SQL上:多表关联写错了、条件传参为null导致没过滤、映射字段对不上导致返回为null。尤其是后者,MyBatis在开启mapUnderscoreToCamelCase之前,数据库的real_name字段和Java的realName属性对不上,返回的对象一直null。解决办法是在配置里设置:

mybatis: configuration: map-underscore-to-camel-case: true

这个配置在MyBatis里属于开箱即用的典型,但如果你没用对,排查起来会极其痛苦——因为你打印DTO对象时发现所有字段都是null,代码看起来又没问题,这种“幻觉型Bug”最容易让人怀疑人生。

6.3 关于并发与上线的一些个人习惯

最后分享几个我做了大量这类项目后沉淀下来的习惯,不针对这个项目本身,但都很实用。

第一个,Controller层尽量只做参数接收和结果封装,不写业务逻辑。所有业务逻辑下沉到Service层,Service层方法命名讲清楚做什么,比如submitHealthRecord、auditAccessLog。这样接口可复用性高,也方便加事务注解。健康打卡、出入审批这些动作,在Service方法上加上@Transactional,保证数据库操作要么全部成功要么全部回滚。比如出入审批通过时,你既要更新审批状态,又要插入一条通行记录,这两步少一步数据就不一致了。

第二个,数据库连接池的参数一定要调。SpringBoot默认的HikariCP配置可以跑起来,但对这类系统来说,maximum-pool-size默认10可能低了一点,我一般根据项目并发情况改成20,并设置connection-timeout为30000毫秒、idle-timeout为600000毫秒。这个调优没有标准答案,但总好过默认配置裸奔,特别是你部署后还有别的服务共用这台机器的时候。

第三个,上线前把前端打包后的dist目录和后端工作目录做一次完整备份,把数据库导出一份丢在同目录下。出了任何问题,总能恢复到上一个能跑的版本,这是我在正式环境里最有效的兜底方案,没有之一。

这套系统虽然是从业务出发的练手项目,但你把它做完、跑通、部署一遍之后,SpringBoot自动配置、MyBatis核心、Vue组件通信、Nginx反向代理这些概念就不再是模糊的面经知识了。如果你也想照着搭一遍,卡在某一环节的时候,对照我上面写的排查表一条条过,大概率不用花太多时间就能跑起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 15:02:51

Excel Range.Value数组:VBA与VB.NET的差异与避坑指南

做Excel二次开发的人&#xff0c;十有八九都写过或者抄过这行代码&#xff1a;arr Range("A1:C10").Value。在VBA里这行代码贼好用&#xff0c;一次把10行3列的数据打包进内存&#xff0c;循环、查找、统计都快得飞起。可是当你把同样的思路搬到VB.NET&#xff0c;打…

作者头像 李华
网站建设 2026/10/2 15:02:33

基于Spring Boot的南京特色美食小吃商城系统开发实战

每年三四月份&#xff0c;总有一批计算机专业的同学对着毕业设计选题发愁。Java方向绕不开Spring Boot&#xff0c;商城系统又是永远的“经典款”&#xff0c;但纯做电商又容易被老师一句“没有新意”打回来。这个题目——“基于Spring Boot框架的南京特色美食小吃商城系统”&a…

作者头像 李华
网站建设 2026/10/2 15:02:13

SpringBoot+Vue商城系统实战:从数据库建模到订单库存与部署全复盘

这个项目算是我这两年做过最完整的一个个人研发项目了。名字叫米家商城&#xff0c;其实是从零搭出来的前后端分离商城系统——前台用户商城加后台管理系统一套全通&#xff0c;后端SpringBoot MyBatis MySQL&#xff0c;前端Vue全家桶。源码量不算小&#xff0c;业务复杂度也…

作者头像 李华
网站建设 2026/10/2 15:02:08

输电线路红外过热检测数据集:VOC/YOLO双格式2253张实战解析

简介&#xff1a;面向电力设备巡检、计算机视觉目标检测研究者的输电线路红外过热检测数据集&#xff0c;针对输电线路运行中红外过热隐患难以快速定位的问题&#xff0c;提供精细标注数据&#xff0c;可直接用于目标检测模型的训练、验证与对比研究&#xff0c;也适合作为高校…

作者头像 李华
网站建设 2026/10/2 15:02:04

MindSpore字节码虚拟机:动态张量计算的实时编译内核

1. 这不是又一个“虚拟机JIT”的老故事&#xff0c;而是张量计算范式迁移的底层支点 你打开VSCode&#xff0c;选中MindSpore内核&#xff0c;写完一段 nn.Cell 定义&#xff0c;点击运行——几毫秒后&#xff0c;GPU上已开始执行融合后的算子流水线。你没手动写CUDA核&…

作者头像 李华
网站建设 2026/10/2 15:01:14

GitHub周榜精选:0920~0926效率工具与AI编程项目深度解析

1. 从“0920&#xff5e;0926”这个时间切片说起&#xff1a;为什么周榜比月榜更有参考价值 每周固定刷 GitHub Trending 的人应该都有个体会&#xff1a;月榜上的项目往往是“已经火了很久的老面孔”&#xff0c;而周榜才是真正能看出“这周圈子里在聊什么”的温度计。0920&am…

作者头像 李华