尧图精选

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

🕒 发布时间:2026/10/2 15:03:02 📁 来源:尧图网络
前几年做这类信息管理系统的项目特别多很多刚入门的同学第一次接触企业级开发就是从一套“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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT出入登记表;有几个设计细节值得多说一句。第一所有时间字段统一用datetime而不是timestampdatetime没有2038年问题和时区换算的坑对这类管理系统来说足够了。第二状态类字段用tinyint不用char或varchar查询效率高写代码时用常量类或者枚举去对应不要在代码里散落裸数字。第三索引不要乱加像health_record表里user_id和record_date组合查询频率很高就建联合索引字段值区分度低的列比如status单独加索引反而可能拖慢写入。用户表里的密码字段长度我给了100因为一般用BCrypt加密加密结果接近60个字符长度不够存储时你就等着上线当天翻车吧。id_card字段虽然是身份证号但注意不要数据库里密文存储除非你愿意每次查询都解密实际项目里更常见的做法是展示时脱敏比如中间几位打星号然后接口返回的时候由后端统一处理。2.2 MyBatis的关键配置TypeHandler、缓存与动态SQLMyBatis在这个系统里的作用就是让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 BaseTypeHandlerString { 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拼接时注意最后一个条件不能多出andwhere标签会自动去掉前缀的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加ViteNode版本需要16以上但也不是越高越好Node 17以上在某些老项目中会因为OpenSSL版本变化报错提示error:0308010C:digital envelope routines::unsupported解决办法是降级Node或加启动参数这在我做项目时出现过。稳妥起见直接用Node 16.20.2或Node 18的LTS版本能省掉很多莫名其妙的坑。初始化项目我用的是Vitenpm create vitelatest 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_status0这个条件如果更新影响行数为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_USERminioadmin \ -e MINIO_ROOT_PASSWORDminioadmin \ -e MINIO_DEFAULT_BUCKETShealth-file \ minio/minio server /data --console-address :90019000是API端口9001是Web控制台端口。然后后端引入依赖写一个配置类读取endpoint、accessKey、secretKey提供一个MinioService封装上传和生成访问URL的方法。minio: endpoint: http://127.0.0.1:9000 access-key: minioadmin secret-key: minioadmin bucket: health-filepublic 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.activeprod有一个坑我必须单独说如果用Maven打包时没有把src/main/resources里的XML文件打包进去运行时大概率会报Invalid bound statement (not found)这是因为MyBatis的Mapper XML默认不会被打进jar的classes目录下。解决办法是在pom.xml的build节点中显式声明把mapper目录下的xml文件作为resources资源打包进去。build resources resource directorysrc/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/xxxVite帮你转发到后端的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_filesNginx会先看有没有对应文件没有就统一返回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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai这个参数不加默认时区是UTC你在页面上看到的新增记录时间会比实际时间差8小时。useSSLfalse这是应对MySQL 8的一个经典报错——它默认要求SSL连接如果没有正确配置SSL证书会报Communications link failure或SSL connection error。本地开发直接禁掉SSL最省事生产环境要走内网链路的话也可以不用真需要加密传输就单独配SSL证书。还有allowPublicKeyRetrievaltrueMySQL 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加useSSLfalseallowPublicKeyRetrievaltrue页面能打开但所有接口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没设置时区加serverTimezoneAsia/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反向代理这些概念就不再是模糊的面经知识了。如果你也想照着搭一遍卡在某一环节的时候对照我上面写的排查表一条条过大概率不用花太多时间就能跑起来。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →