ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot健康管理系统毕设指南:功能闭环、数据库设计与部署踩坑全记录

SpringBoot健康管理系统毕设指南:功能闭环、数据库设计与部署踩坑全记录 做毕设选“基于SpringBoot的健康管理系统”的人很多但大多数都是套个框架就开始写增删改查最后做出来的东西既没有健康管理的业务闭环也体现不出SpringBoot的核心价值。最近后台陆续收到几个同学的提问问这个题目到底怎么设计才算完整、技术选型怎么定、表怎么建、鉴权和文件存储怎么接。这篇文章我把自己做这套系统的完整思路和落地过程整理出来包括功能边界梳理、数据库设计、核心模块编码、JWT鉴权、MinIO文件存储、Vue前端打包集成还有运行阶段踩过的坑。无论你是正在准备毕设还是想把SpringBoot的开发流程系统地捋一遍这篇都可以当作一个能直接参考的工程模板。1. 健康管理系统到底在做什么功能边界与技术选型的对应分析1.1 这个系统不是普通的CRUD四大业务闭环如果只看表面健康管理系统确实就是用户信息、指标数据、运动记录的增删改查。但真正设计的时候你会发现如果只按这种思路做系统会变成一个“录入工具”没有实用价值。健康管理系统的核心应该是形成一个闭环采集、建档、分析、干预。采集用户每天录入体重、血压、心率、血糖、血氧这些指标也可以考虑预留穿戴设备数据同步的接口。建档把用户的基础信息、既往病史、过敏史、家族病史、体检报告统一归档形成一份动态更新的健康档案。分析根据指标时间序列生成趋势图计算BMI、体重变化率判断是否存在异常区间给出简单的健康建议。干预系统根据规则给用户发送提醒比如“该测血压了”“体重连续一周上涨”“某项指标连续三次异常建议就医检查”。这四个环节落到系统里对应的就是用户模块、健康档案模块、指标管理模块、提醒与建议模块再加上管理员端的数据统计和用户管理。功能边界先划清楚后面的表设计和接口设计才不会乱。我在动手写代码之前先把每个模块的用户故事列了一遍比如“用户登录后能看到最近30天的血压趋势”“管理员可以按时间段查看所有用户的指标异常情况”然后再往表结构和接口上映射。这个过程不能省因为健康管理系统的数据链路比一般管理系统长跳着设计很容易漏表。1.2 为什么选SpringBoot而不是SSM或微服务说实话虽然现在面试题里全是微服务、分布式、Spring Cloud但一个单体管理系统用SpringBoot是最合理的。原因有三点。第一点很直接自动配置把样板配置砍掉一大截。传统SSM项目要手写web.xml、spring-mvc.xml、mybatis-config.xml还要配一堆bean很多配置跟业务没有半毛钱关系纯粹是浪费时间。SpringBoot用starter机制把依赖和默认配置打包好引入依赖就能跑。我实际搭骨架的时间大约缩短了三分之二。第二点是内置Tomcat让部署变简单。毕设项目通常要拿到别的机器上演示传统SSM要装Tomcat、配端口、部署war包SpringBoot直接java -jar跑一个可执行jar配合多环境配置文件演示环境五分钟就能起。第三点是生态成熟找资料容易。SpringBoot相关的博客、组件、面试题太多了遇到问题基本都搜得到方案。这个理由听起来不太“技术”但做项目的时候确实重要——可维护性不只包括代码质量还包括有没有人替你踩过坑。微服务框架当然不是不行但一个健康管理系统连单机性能瓶颈都没碰到过强行拆微服务只会增加服务发现、配置中心、链路追踪这些跟业务无关的复杂度。技术选型要服务于真实需求而不是为技术炫技服务。1.3 完整技术栈清单与版本匹配我的最终选型是这样的层次技术选型版本/说明后端框架SpringBoot2.7.xJDK 1.8ORMMyBatis-Plus3.5.x配合BaseMapper和分页插件数据库MySQL8.0缓存Redis验证码存储、token黑名单文件存储MinIO体检报告、头像等对象存储鉴权JWT HandlerInterceptorjjwt 0.9.1前端Vue 3 ViteElement Plus ECharts构建部署Maven前端dist并入后端static选SpringBoot 2.7.x而不是3.x是因为3.x要求JDK 17而且包名从javax改成jakartaMyBatis-Plus等配套组件当时还有兼容问题。如果你的JDK已经是17及以上用SpringBoot 3.x没问题但要注意引入MyBatis-Plus专用starter网上搜代码时也要注意版本差异。这个决策在第7章会详细展开这里先记住一个原则毕设类的项目尽量选稳定且资料多的版本组合不要盲目追新。2. 数据库设计推演从需求到表结构的完整思路2.1 核心业务表与字段级设计我最终建的几张核心表是用户表、健康档案表、健康指标记录表、运动记录表、饮食记录表、提醒规则表、提醒记录表、体检报告表。这里挑几张重点表讲字段设计。用户表字段id、username、passwordbcrypt加密后的密文、nickname、phone、email、avatar、status0禁用 1正常、create_time、update_time、deleted逻辑删除、version乐观锁版本号。健康档案表字段id、user_id唯一索引、gender、birthday、height、weight、blood_type、allergy_history、medical_history、family_history、smoking是否吸烟、drinking是否饮酒、exercise_frequency、create_time、update_time。健康指标记录表字段id、user_id、record_time测量时间注意不是创建时间、systolic_pressure、diastolic_pressure、heart_rate、blood_sugar、blood_oxygen、temperature、bmi、remark、create_time。这几张表看起来常规但有三个细节值得单独说。第一指标记录表的record_time一定要单独设计。很多人喜欢直接用create_time代替测量时间但用户可能补录昨天的数据create_time就不是真实测量时间趋势图就会出错。我在接口设计时还允许前端传一个测量时间参数后端不做“只允许今天”的限制这样业务上更灵活。第二身高体重分开存用DECIMAL(5,1)而不是FLOAT。BMI不要在录入时就算好存进去应该存身高体重展示时再算。如果存了BMI用户后面改了身高历史BMI全是错的。这条规则对所有衍生指标都适用能算出来的字段不要落库。第三健康档案表加smoking、drinking这种行为字段是因为健康评估逻辑经常用到这些因子。一开始不建好后面做健康建议模块又要改表而alter table在数据有量之后成本就不一样了。2.2 表关系与约束设计的关键决策用户表与健康档案表是一对一user_id加了唯一索引。很多人把健康信息直接堆在用户表里这样表会越来越宽而且下次体检之后想保留历史也没有地方放。更合理的做法是健康档案只存“当前状态”需要保留历史轨迹时再加一张健康档案快照表每次体检后往快照表里写一条记录。用户表与指标记录表是一对多。指标记录表对user_id和record_time建联合索引因为查询基本上都是“某个用户某段时间的数据”没有这个索引数据量到几万条以后查询就会明显变慢。我在第7章会给出一个实际案例补了这个索引之后接口耗时从120ms降到20ms。提醒规则表和提醒记录表也简单说明。规则表存的是业务规则比如“每天8点提醒测血压”“体重连续7天上涨提醒”记录表存的是实际发送记录。规则表不要写死SQL条件把条件字段化比如min_value、max_value、duration_days、remind_time之后改规则只需要改数据库记录不用改代码。这里涉及一个常见的设计决策提醒规则到底是写死在代码里还是放数据库我的建议是放数据库。健康管理领域的规则后续大概率要调整比如正常血压范围标准变了如果写死在Java代码里就要改代码重新部署放数据库只需要update一条记录。很多系统后期难维护就是因为在“这个配置到底放哪一层”的问题上太随意。2.3 几个容易被忽视的字段设计细节第一个是逻辑删除字段deleted。SpringBootMyBatis-Plus里用TableLogic注解就能自动处理查询时自动加deleted0条件。但加了逻辑删除之后username的唯一索引冲突问题就来了用户删了之后再注册同名用户会失败。解决办法是删除时把username改成带时间戳的值或者用复合唯一索引把deleted包含进去。我在项目里选择了前者因为处理逻辑更直观。第二个是乐观锁字段version。健康档案表这种可能被多个页面同时编辑的数据建议加version字段配合MyBatis-Plus的Version实现乐观锁。虽然毕设场景下并发不大但能说清楚这个设计意图在答辩时是加分项。第三个是create_time和update_time不要手动填。MySQL里直接设置DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMPMyBatis-Plus也有MetaObjectHandler自动填充两条路都可以但不要在Java代码里手动set时间——总会漏总会不一致。我做项目时统一用数据库默认值MyBatis-Plus实体里对应字段标TableField(fill FieldFill.INSERT)两步保证一致。3. 项目骨架搭建与自动装配原理的应用3.1 分层架构与目录职责划分项目我按controller-service-mapper三层来分但每层包内的类职责划分得更细一点。controller只做参数接收、参数简单校验、调用service、返回统一结果不写业务逻辑。service是业务逻辑核心层事务控制也加在这里比如“新增健康档案的同时写入一条归档日志”这种跨表操作必须放在同一个事务方法里。mapper对应MyBatis-Plus的BaseMapper接口复杂查询用XML或Select注解。dto是接收前端参数的请求对象用于参数校验。vo是返回给前端的数据对象把实体里不想暴露的字段比如密码过滤掉。entity对应数据库表。config放配置类比如MyBatis-Plus分页插件、拦截器注册、跨域配置。common放通用工具类、统一返回对象、全局异常处理、常量类。这个目录本身不稀奇但很多人因为赶进度把代码全堆在service里一个service类几百行后面加功能时非常痛苦。分层不是做给别人看的是给自己降低后续维护成本的。特别是健康管理这种功能模块很多的系统每层职责清晰之后新增一个统计接口基本不用动别的模块的代码。提一下包命名package命名上我用com.example.health.controller这种形式业务模块用包内再分包的方式区分auth、health、report、remind。实际开发中按业务分包比按技术分包更好扩展。按技术分包会产生com.example.health.controller里有几十个类挤在一起的情况但按业务分包后每个业务包内自成体系改健康档案模块不会碰到提醒模块的文件。3.2 统一响应、全局异常与自定义业务异常前后端分离项目接口返回格式必须统一。我定义了一个Result类字段包含code、message、data。成功时code为200业务失败时code为500未登录时code为401。前端axios拦截器拿到code之后统一处理不用每个接口单独判断。配合统一响应的是一套全局异常处理。用RestControllerAdvice可以拦截所有controller抛出的异常核心是区分两类异常一类是业务异常比如“档案不存在”“指标数据超出合理范围”这类异常要在service层主动抛出由全局异常处理器转成对应的提示信息。业务异常不要打印一堆堆栈前端看到的应该是干净的业务消息。另一类是系统异常比如数据库连接失败、空指针这类异常要记录完整堆栈到日志然后返回给前端一个通用提示比如“系统繁忙请稍后重试”不要把异常细节暴露给前端。我自定义了一个BusinessException类带错误码和错误消息service里直接throw new BusinessException(ErrorCode.NOT_FOUND, 健康档案不存在)。全局异常处理器里用ExceptionHandler(BusinessException.class)单独处理再用ExceptionHandler(Exception.class)兜底所有未知异常。这个机制看起来简单但实际联调阶段价值非常大前端最烦的就是后端返回的报错内容每次都不一样有的还带Java堆栈。统一之后前后端联调效率提升明显。3.3 自动装配原理从配置生效到排查思路SpringBoot的自动装配原理是高频面试题也是理解“为什么加一个依赖就多了一堆功能”的关键。核心机制是SpringBootApplication组合注解它里包含SpringBootConfiguration、EnableAutoConfiguration和ComponentScan。最关键的是EnableAutoConfiguration它通过Import导入AutoConfigurationImportSelector类这个类会扫描所有jar包里的META-INF/spring.factories文件SpringBoot 2.7及以前或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件2.7之后的推荐方式拿到所有自动配置类的全限定名然后逐个尝试加载配合ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty这些条件注解按条件决定哪些自动配置真正生效。举一个实际例子项目引入spring-boot-starter-web之后为什么直接就能支持HTTP接口因为自动配置类里有一个DispatcherServletAutoConfiguration它看到classpath里有DispatcherServlet类就会自动装配前端控制器但如果你自己定义了DispatcherServletConditionalOnMissingBean就不会再重复装配。理解这套机制对排查问题非常有用。遇到“配置没生效”的情况第一反应应该是去检查条件是否满足而不是反复重启。启动类上加debugtrue启动时会打印所有自动配置的生效/失效报告排查“为什么MinIO的配置没生效”“为什么连接池选了HikariCP而不是Druid”这类问题一目了然。有一次我发现配置文件里的Redis地址一直没生效后来看自动配置报告才意识到自定义的RedisTemplate覆盖了默认配置导致ConditionalOnMissingBean判断为“已有bean不再创建”这不是配置没读到而是条件判断的结果。4. 核心业务模块实现健康档案、指标趋势与图表接口4.1 健康档案的新建与更新策略健康档案模块第一个要解决的问题是“用户可能没有档案”。注册成功之后并不会自动生成档案所以接口设计时要区分两种情况档案存在则更新不存在则创建。我用的是service层先按userId查档案存在就走updateById不存在就insert。但这里有个并发隐患两个请求同时发现档案不存在可能插入两条数据。解决方案是用user_id唯一索引兜底插入时如果重复就catch DuplicateKeyException转成更新。档案更新时要注意一点不要把前端传来的空字符串直接覆盖数据库里的旧值。用户可能只想改体重但前端把整个表单都提交过来了某个字段值为空。我的做法是空字符串和null一律不更新用UpdateWrapper只set非空字段或者在前端做必填校验把必填字段都校验完再提交。这个处理逻辑在service层写一个固定的“非空更新”工具方法其他模块也能复用。前面提过BMI、体脂率这些衍生指标不要存库。我在VO层动态计算返回给前端的对象里带一个bmi字段但表里不落库。同理健康建议这种按规则算出来的内容也不要为了省事直接写进数据库——规则变化后历史数据会很尴尬。4.2 指标录入、参数校验与自定义校验注解指标录入接口接收的数据达到8个指标字段以上如果不做校验用户的血压值可能填成-30血糖填成500。前端的校验可以被绕过所以后端必须校验。我用的是JSR-303规范的校验注解NotNull、DecimalMin、DecimalMax这些直接加到dto字段上在controller参数上加Validated就能自动触发。但这套标准注解有个麻烦不同指标字段的合理范围差异很大而且标准注解的参数是写死的。比如收缩压范围90-139舒张压60-89血糖3.9-6.1每个字段都要写一遍规则。所以我自定义了一个HealthRange注解支持配置min和max内部用ConstraintValidator实现校验逻辑可复用。核心代码如下Target({ElementType.FIELD, ElementType.PARAMETER}) Retention(RetentionPolicy.RUNTIME) Constraint(validatedBy HealthRangeValidator.class) public interface HealthRange { String message() default 指标数据超出合理范围; Class?[] groups() default {}; Class? extends Payload[] payload() default {}; int min() default Integer.MIN_VALUE; int max() default Integer.MAX_VALUE; }public class HealthRangeValidator implements ConstraintValidatorHealthRange, BigDecimal { private int min; private int max; Override public void initialize(HealthRange constraintAnnotation) { this.min constraintAnnotation.min(); this.max constraintAnnotation.max(); } Override public boolean isValid(BigDecimal value, ConstraintValidatorContext context) { if (value null) { return true; } return value.compareTo(BigDecimal.valueOf(min)) 0 value.compareTo(BigDecimal.valueOf(max)) 0; } }dto里就可以这么用NotNull(message 收缩压不能为空) HealthRange(min 80, max 200, message 收缩压需在80-200之间) private BigDecimal systolicPressure;这里有个小坑要提醒自定义校验器遇到null时直接返回true因为是否必填交给NotNull判断两个注解职责分离。如果自定义注解里把null判定为false那用户不填字段时报的就不是“不能为空”而是“范围不合法”错误信息就错位了。4.3 趋势图数据接口SQL聚合与VO设计健康管理系统最核心的展示功能是趋势图。前端用ECharts画折线图接口需要返回类似这样的数据{ dates: [2025-05-01, 2025-05-02, 2025-05-03], systolic: [118, 121, 117], diastolic: [76, 78, 75], heartRate: [72, 70, 74] }这个接口的SQL写法是重点。按天聚合血压平均值的查询如下SELECT DATE_FORMAT(record_time, %Y-%m-%d) AS record_date, AVG(systolic_pressure) AS avg_systolic, AVG(diastolic_pressure) AS avg_diastolic, AVG(heart_rate) AS avg_heart_rate FROM health_indicator_record WHERE user_id #{userId} AND record_time BETWEEN #{startTime} AND #{endTime} AND deleted 0 GROUP BY record_date ORDER BY record_date ASC有一个经验别在Java代码里做循环查数据库。比如前端要显示最近30天的趋势最差的做法是循环30次查询。正确做法是上面这一条SQL直接拿到全部数据SQL层面的分组聚合非常快。如果数据量大后续还可以按月份预聚合但在毕设规模下分组查询完全够用。vo类的设计也简单TrendVO里有List dates、List systolic等字段。mapper返回ListMapString,Object再组装成VO或者用Select注解直接映射到自定义的Outcome对象两种方式都可以。这里我踩过一个小坑用int接收AVG的结果小数点被截断血压平均值变成了118而不是118.4。AVG返回的精度类型要对应统一用BigDecimal接收。这个坑在金额、体重字段上也很常见写SQL聚合查询时千万注意接收类型。趋势接口之后可以跟联动一下前端拿到最近30天的指标数据后除了画图还能判断有没有连续异常天数。比如“连续3天收缩压超过140”前端展示一个警告条后端提醒模块再根据这个条件发提醒。业务上的分析逻辑不做在图表接口里而是放到独立的健康评估服务里保持接口单一职责。5. 鉴权与对象存储集成JWT方案和MinIO接入实录5.1 为什么我用JWT拦截器而不是Spring Security很多毕设同学上来就用Spring Security结果搞不明白过滤器链和配置类浪费时间。Spring Security本身很强大但它的设计目标是企业级安全框架大量配置和组件对付管理员加普通用户两种角色的系统有点杀鸡用牛刀。我的方案是JWT加HandlerInterceptor用户登录成功后后端生成JWT token返回给前端前端把token存在localStorage里每次请求在请求头带上Authorization: Bearer 。后端写一个拦截器拦截需要认证的接口解析token、校验过期、取出用户id和角色塞进ThreadLocalcontroller里随时可取当前登录用户。这套方案的好处代码量小每个环节都看得懂出问题能快速定位答辩的时候也能把JWT原理和代码讲清楚比背Spring Security配置更有说服力。它的不足是token无法在服务端强制失效、权限粒度控制也不如Spring Security细致但两个角色的系统完全够用。如果后续要扩展精细权限可以基于拦截器引入RBAC表结构用自定义注解RequireRole控制接口权限。5.2 JWT拦截器与用户上下文实现细节JWT依赖我用的是jjwt库0.9.1版本配JDK1.8比较稳新版API变化比较大。生成token的核心代码String token Jwts.builder() .setSubject(userId.toString()) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() expireTime)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();拦截器里解析tokenString header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); try { Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); Long userId Long.parseLong(claims.getSubject()); UserContextHolder.set(userId, (String) claims.get(role)); return true; } catch (ExpiredJwtException e) { throw new BusinessException(ErrorCode.UNAUTHORIZED, 登录已过期请重新登录); } catch (JwtException e) { throw new BusinessException(ErrorCode.UNAUTHORIZED, token无效); } } throw new BusinessException(ErrorCode.UNAUTHORIZED, 未登录);注册拦截器时一定要设置放行路径。登录接口、注册接口、静态资源、图片预览等都要排除registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/file/preview/**);这里有一个容易犯的错拦截器里校验失败后直接return false前端会收到空响应而不是JSON。因为不是controller抛出的异常全局异常处理器也接不到。我在项目里改成拦截器内直接throw BusinessExceptionSpring MVC环境下拦截器里抛出的异常同样会被RestControllerAdvice捕获返回格式就和业务异常一致了。UserContextHolder我实现了一个简单的ThreadLocal封装。请求处理完必须在afterCompletion里remove否则线程池复用时会出现用户信息串号。这个问题只在并发量上来后出现但排查起来非常隐蔽。5.3 MinIO接入SpringBoot的完整步骤与踩坑文件存储是健康管理系统的刚需模块体检报告要上传PDF、图片。把文件直接传到服务器磁盘上省事但不安全也不方便扩展这个项目我选了MinIO。为什么是MinIO因为它兼容S3 API、部署简单本地一条命令就能起还提供Web管理界面适合学习和毕设演示。如果学校要求公网可访问也可以换对象存储服务商API用法基本兼容。接入步骤第一步本地起MinIO服务minio server /data --address :9000 --console-address :9001第二步SpringBoot工程里引入MinIO Java SDK配置MinioClientBean public MinioClient minioClient() { return MinioClient.builder() .endpoint(minioProperties.getEndpoint()) .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey()) .build(); }第三步写上传代码核心是Bucket名称、对象名称、ContentType几个参数public String upload(MultipartFile file, String objectName) { boolean found minioClient.bucketExists( BucketExistsArgs.builder().bucket(bucketName).build()); if (!found) { minioClient.makeBucket( MakeBucketArgs.builder().bucket(bucketName).build()); } minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return previewUrl / bucketName / objectName; }踩坑记录里最典型的有三个。第一个是预览地址403。默认情况下MinIO的bucket是私有权限直接拼出来的URL访问不了。两种解决方式一是上传后调用getPresignedObjectUrl生成带签名的临时URL适合体检报告这种隐私文件二是如果就是要公开访问图片在MinIO控制台里设置bucket的Access Policy为public。健康管理系统的体检报告我建议用带签名URL用户头像这类公开资源用public策略。第二个是端口问题。启动地址是127.0.0.1:9000上传之后生成的URL如果写成localhost预览时部分浏览器会拦截。最好把endpoint统一配置成服务器IP或域名避免文件上传后访问不了。第三个是SDK版本兼容。MinIO Java SDK 8.x的putObject参数和7.x差别很大网上搜到的代码版本对不上编译报错的时候先看异常信息指定的是哪个方法再对照pom里的版本。6. Vue前端与SpringBoot部署整合打包、路由与静态资源6.1 开发环境下的代理与跨域配置前后端分离开发的时候前端跑在5173端口Vite默认后端跑在8080跨域绕不开。我的处理方案是开发环境不开启后端CORS直接用Vite的proxy代理。server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求 /api/xxx 会被Vite开发服务器转发到后端8080浏览器看起来是同一个源不存在跨域问题。后端就不需要写CorsFilter了。生产部署时情况不同。如果前后端分开部署就需要在后端加跨域配置。如果前端打包后由SpringBoot直接托管则同源不需要跨域配置。我实际项目里是方案B所以后端一直没写CORS相关代码。6.2 Vue打包产物集成进SpringBoot的两种方案方案A前端独立部署。Vue打包的dist文件丢到Nginx后端jar包单独跑用Nginx做反向代理。这个方案适合生产环境但毕设通常一台机器演示运维成本略高。方案B把Vue打包后的dist目录复制到SpringBoot的src/main/resources/static下SpringBoot直接托管静态资源。这样打出的jar包自带页面启动后浏览器访问8080就能看到完整系统演示非常方便。方案B的具体操作分两步。第一步前端构建npm run build构建产物在dist目录。第二步把dist目录里的文件全部复制到后端的resources/static目录然后重新打包后端。我为了省事写了一个脚本构建完前端自动复制到后端再执行mvn package一条命令出jarcd frontend npm run build rm -rf ../backend/src/main/resources/static/* cp -r dist/* ../backend/src/main/resources/static/ cd ../backend mvn package -DskipTests这里有个注意点SpringBoot默认静态资源路径之一就是classpath:/static/你要把dist目录里的内容直接放在static根目录下也就是static/index.html能直接访问。千万别把dist文件夹本身拷进去变成static/dist/index.html访问路径会多一层容易出问题。6.3 history路由刷新404的三种解决方式Vue Router默认是hash模式URL里有#号刷新不会404但不好看。如果改成history模式URL干净了但直接访问 /health 这样的路径刷新时后端找不到对应controller会返回404。解决办法有三种按推荐顺序第一种如果项目用的是hash模式就干脆别改成history了。毕设演示时URL多个#号没有任何影响最省事。第二种坚持history模式的话后端加一个ViewController把前端路由转发到index.htmlController public class ViewController { RequestMapping(value {/health/**, /statistics/**, /profile/**}) public String forward() { return forward:/index.html; } }注意这个配置只针对刷新场景而且要确认不会误伤带后缀的静态资源请求。第三种在Nginx里配置try_files适合生产部署location / { try_files $uri $uri/ /index.html; }我在实际项目中用的第一种路由不算复杂hash模式完全够用少踩一个坑。7. 运行调优与踩坑汇总版本、索引、静态资源拦截7.1 SpringBoot版本太高引发的连锁问题这个坑最近特别普遍很多人用Spring Initializr直接创建SpringBoot 3.3、3.4项目JDK选17结果后面全是兼容问题。第一个是包名迁移。SpringBoot 3.x把javax.改成jakarta.网上抄的旧代码基本都要改import。第二个是组件兼容MyBatis-Plus要引入专门的starter部分旧插件可能不兼容。第三个是starter的默认配置有变化一些配置项改了名字。我的建议是除非JDK已经是17且能熟练处理这些问题否则SpringBoot 2.7.x加JDK 8是毕设项目的稳妥组合。如果确实用了3.x排查问题时优先看官方迁移文档不要搜旧文章照抄。顺手提一句SpringBoot 2.7支持在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中声明自动配置类。如果答辩时想演示自动装配原理可以自己写一个小工具starter用这个文件的方式比传统spring.factories更清晰。7.2 数据库慢查询与索引优化系统跑了一段时间指标记录表数据量上来之后趋势接口开始变慢。打开MySQL慢查询日志耗时最长的SQL基本都是按user_id和record_time查询的。检查发现联合索引没建上后来补了索引ALTER TABLE health_indicator_record ADD INDEX idx_user_time (user_id, record_time);补完之后同样接口的耗时从120ms降到20ms左右。还有一个容易被忽略的坑DATE_FORMAT(record_time, %Y-%m-%d)这种写法会破坏索引。因为DATE_FORMAT是函数MySQL没法直接用索引。如果要按天查询查询条件尽量写成record_time ? AND record_time ?的范围查询SQL里聚合时可以用函数但WHERE条件里的字段不要套函数。这个细节面试也会问实现的时候顺手做对就好。7.3 拦截器放行顺序导致样式丢失这个坑在前端文件首次放到static目录后很常见。用户登录页所有CSS、JS都404页面白板。排查下来发现是因为拦截器addPathPatterns(/**)拦截了一切请求包括静态资源请求token校验失败直接抛异常导致样式等静态资源全部加载失败。解决办法是把静态资源路径加进excludePathPatterns.excludePathPatterns(/static/**, /favicon.ico, /index.html, /assets/**, /js/**, /css/**)如果用了Vue打包产物静态资源名称会带hash路径前缀是assets所以assets/**必须放行。这里也提醒一句拦截器的addPathPatterns不要写成/**把所有东西都拦住尽量用/api/**这类前缀控制业务接口避免静态资源受影响。7.4 多环境配置与日志的实用建议最后讲两个能让项目维护轻松一点的习惯。第一个是多环境配置。application.yml放公共配置application-dev.yml和application-prod.yml分别放不同环境的数据库、Redis地址。打包时用--spring.profiles.activeprod切换外置参数还可以用环境变量覆盖。我演示时数据库在本地部署在服务器代码完全不用改只改启动参数。第二个是日志分段。用logback-spring.xml可以按天生成日志文件appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/health-system.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/health-system.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy /appender排查问题最大的帮手就是日志。service层关键操作至少打一条info日志异常路径打error日志带完整堆栈这样上线之后复现问题、定位问题才有依据。我见过不少项目代码写完了日志一条没有出了故障全靠猜这种状态在项目交付阶段非常被动。最后说点个人心得。这套系统的技术栈没有用到任何前沿的东西但把SpringBoot的配置机制、拦截器、自动装配、对象存储、静态资源托管这些基础能力完整地串了一遍。做完你会发现很多以前在面试题里背过的概念——“自动装配原理是什么”“JWT和Session有什么区别”“前后端分离怎么部署”——都成了自己亲手验证过的经验。如果你也在做类似的毕设或练手项目建议先把功能边界和表结构设计想清楚再动手写代码而不是一边写一边改。先把地基打稳后面SpringBoot带给你的开发效率才会真正发挥出来。
返回列表