做“社区老年人健康管理系统”这个题目,坦白说是一个很典型的Spring Boot落地项目。不管你是拿它当毕业设计,还是社区真要做一套信息化管理后台,这套系统的业务边界都非常清晰:老人档案、体检记录、慢病跟踪、用药提醒、社区统计。把这几块用Spring Boot串起来,既能覆盖完整的业务闭环,又能把增删改查、权限控制、定时任务、数据可视化这些常用技术点全部练到手。这篇文章我会把这个系统的设计思路、数据库建模、后端核心实现、前端配合、打包部署和排查经验完整拆开,适合正在做类似课题的学生,也适合刚接触Spring Boot想找一个完整项目的开发者参考。
1. 项目整体设计与技术选型拆解
1.1 这个系统到底要解决什么问题
社区老年健康管理本质上是一个“数据驱动”的场景。传统模式下,老人的健康档案散落在纸面上,体检数据填完就归档,慢病随访全靠电话和微信群,社区医生很难快速回答“辖区内哪个老人血压连续偏高”“谁三个月没来体检了”这类问题。
这套系统就是把这些信息全部线上化,围绕一个老人建立电子健康档案,持续沉淀体检记录,并对慢病和用药情况做周期性跟踪。系统化的价值不在单点功能,而是把“建档—体检—监测—提醒—干预”串成一条完整的链路。比如医生打开后台就能看到高风险老人的列表,家属能在手机上接收用药提醒,管理员能按社区维度统计健康指标。理解了这条业务链,后面的表设计和接口设计才能有方向感。
1.2 为什么选择Spring Boot做底座
这个题目把Spring Boot放在前面不是偶然的。Spring Boot最大的价值是“自动装配”,它通过@EnableAutoConfiguration机制把Web容器、数据源、JSON序列化、日志框架这些基础配置全部按约定初始化好,开发者不需要再维护一堆繁琐的XML配置,写一个@SpringBootApplication就能启动整个项目。对社区健康管理这类业务型系统来说,90%的精力应该花在业务逻辑上,而不是环境配置上。
技术栈配合上,我建议这样选:
| 技术 | 选型建议 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 稳定、教程多、兼容性好,后面细说为什么不要一上来用3.x |
| ORM | MyBatis-Plus | 单表CRUD几乎不用写SQL,分页和自动填充都能直接用 |
| 数据库 | MySQL 8.0 | 成熟稳定,生态资料丰富 |
| 前端 | Vue 3 + Element Plus + ECharts | 后台管理界面开发快,图表组件齐全 |
| 认证 | JWT | 前后端分离下最常用的无状态登录方案 |
这里特别提醒一句版本问题。Spring Boot 3.x从2022年底开始成为主流,但3.x把javax.*包迁移到了jakarta.*,很多老教程的import javax.servlet直接编译失败。如果你的目标是快速做出一个稳定可演示的系统,Spring Boot 2.7.18搭配JDK 8或11是最稳的选择,网上资料也最多,遇到问题一搜就有答案。
1.3 系统角色与核心模块划分
系统涉及三类角色,权限的边界跟着业务走:
- 老人/家属:查看本人或绑定老人的健康档案、体检记录、接收用药提醒。这类账号权限最窄,只允许访问和自身相关的数据。
- 社区医护人员:负责档案的创建和审核、体检记录录入、慢病随访记录、查看辖区统计视图。
- 系统管理员:管理用户账号、角色权限分配、日志查看、基础数据维护。
核心模块我按业务域拆成五块:
- 统一认证与权限控制:登录、JWT签发与校验、角色鉴权。
- 健康档案管理:老人基础信息、紧急联系人、过敏史、既往病史的维护。
- 体检记录管理:身高体重、血压血糖、血脂等指标记录,以及历史趋势分析。
- 慢病随访管理:高血压、糖尿病等重点人群的随访计划与记录。
- 用药提醒与社区统计:按人产生提醒任务,按社区维度聚合健康数据。
这种划分方式的好处是,每个模块都能独立开发和测试,同时通过elder_id这一条主线关联起来,符合实际业务中“一个老人一条健康轨迹”的认知。
2. 数据库设计:健康数据的核心表结构
2.1 表关系设计的核心原则
社区健康管理系统的数据关系比较清楚,核心原则就一句话:以老人档案表为主表,其他业务表都通过elder_id关联,形成一主多从的结构。用户体系单独维护,通过角色字段区分登录身份。不要试图把健康档案和账号信息混在一张表里,因为一个老人的档案可能由家属账号共享查看,混表会非常难做权限控制。
每张业务表都建议带create_time、update_time和deleted这三个字段。前两个用于数据溯源和排序,deleted用于逻辑删除。健康数据存在合规风险,物理删除一旦误操作很难恢复,所以一律走逻辑删除。
2.2 核心表的字段定义与建表SQL
老人档案表是整套系统的地基,字段既要覆盖身份信息,也要包含健康管理必需的维度:
CREATE TABLE `elder_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `elder_no` varchar(32) NOT NULL COMMENT '档案编号,如 ELD20250101001', `name` varchar(32) NOT NULL COMMENT '老人姓名', `gender` tinyint NOT NULL COMMENT '性别 0-女 1-男', `birth_date` date NOT NULL COMMENT '出生日期', `id_card` varchar(18) NOT NULL COMMENT '身份证号', `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `address` varchar(255) DEFAULT NULL COMMENT '居住地址', `blood_type` varchar(4) DEFAULT NULL COMMENT '血型:A/B/AB/O/UNKNOWN', `allergy_history` varchar(500) DEFAULT NULL COMMENT '过敏史', `emergency_contact` varchar(32) DEFAULT NULL COMMENT '紧急联系人', `emergency_phone` varchar(20) DEFAULT NULL COMMENT '紧急联系电话', `status` tinyint NOT NULL DEFAULT '1' COMMENT '健康状态 1-良好 2-慢病管理 3-重点关注', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted` tinyint NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `uk_id_card` (`id_card`), KEY `idx_elder_no` (`elder_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='老年人健康档案表';体检记录表是后续做趋势分析的数据来源,也是表格最多的表。核心体检指标建议拆成独立字段,便于SQL聚合和图表展示:
CREATE TABLE `health_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `elder_id` bigint NOT NULL COMMENT '关联老人档案ID', `check_date` date NOT NULL COMMENT '体检日期', `height` decimal(5,1) DEFAULT NULL COMMENT '身高(cm)', `weight` decimal(5,1) DEFAULT NULL COMMENT '体重(kg)', `bmi` decimal(4,1) DEFAULT NULL COMMENT 'BMI指数', `heart_rate` int DEFAULT NULL COMMENT '静息心率(次/分)', `blood_pressure_high` int DEFAULT NULL COMMENT '收缩压(mmHg)', `blood_pressure_low` int DEFAULT NULL COMMENT '舒张压(mmHg)', `blood_sugar` decimal(4,1) DEFAULT NULL COMMENT '空腹血糖(mmol/L)', `cholesterol` decimal(4,1) DEFAULT NULL COMMENT '总胆固醇(mmol/L)', `doctor_remark` varchar(500) DEFAULT NULL COMMENT '医生备注', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_elder_id` (`elder_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='体检记录表';用药提醒表服务于定时任务,字段需要支持“每天固定时间提醒”和“周期内提醒”两种模式:
CREATE TABLE `medication_reminder` ( `id` bigint NOT NULL AUTO_INCREMENT, `elder_id` bigint NOT NULL COMMENT '关联老人档案ID', `medication_name` varchar(100) NOT NULL COMMENT '药品名称', `dosage` varchar(50) DEFAULT NULL COMMENT '每次用量', `remind_time` time NOT NULL COMMENT '提醒时间', `start_date` date NOT NULL COMMENT '开始日期', `end_date` date DEFAULT NULL COMMENT '结束日期,空表示长期', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1-启用 0-停用', `contact_phone` varchar(20) DEFAULT NULL COMMENT '接收提醒的联系方式', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_elder_id` (`elder_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用药提醒表';用户表相对简单,只做账号、角色和状态控制:
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL, `password` varchar(64) NOT NULL COMMENT '明文密码加盐后散列存储', `real_name` varchar(32) NOT NULL COMMENT '真实姓名', `role` varchar(16) NOT NULL COMMENT 'ADMIN-管理员 DOCTOR-医护 FAMILY-家属', `status` tinyint NOT NULL DEFAULT '1', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';2.3 表字段设计容易踩的几个坑
第一,血压不要存成120/80这样的字符串。我见过不少项目为了省事把血压写成一个字段,结果后面做趋势分析时完全没法用SQL排序和区间统计。拆成blood_pressure_high和blood_pressure_low两个整数,聚合查询非常顺畅。第二,体检的数值字段统一用decimal并明确单位,单位写死在代码枚举里,不要在字段里存kg或cm,否则数据录入时五花八门,统计时只能干瞪眼。第三,身份证号这类敏感字段在接口返回值中要做脱敏,接口响应只返回前3后4,完整号码仅后台导出时才允许查看,且要在日志层面禁止打印。第四,年龄不要直接存,要由birth_date动态计算。因为档案里的年龄会逐年过期,存了反而要定时更新,动态计算一劳永逸。第五,所有关联查询都走elder_id索引,不要用name或id_card做业务关联主键,身份证会变更,老人重名也常见,用数据库自增代理主键最省心。
3. 后端关键业务实现与代码解析
3.1 统一返回结构与全局异常处理
前后端分离项目里,接口返回结构如果不统一,前端axios拦截器就要写一堆分支判断。我习惯定义一个Result<T>作为所有接口的统一壳子:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } }配合一个@RestControllerAdvice全局异常处理类,把参数校验异常、业务异常、未知异常统一转成Result格式。这样Controller里的代码会非常干净,只管业务逻辑,不用到处try-catch。异常处理还有一个额外收获:定时任务里抛出的业务异常可以在全局日志里统一记录,排查线上问题时特别有用。
3.2 JWT登录与角色鉴权的落地
登录接口的流程很标准:根据用户名查出用户,校验密码,通过后生成JWT返回给前端。我用的工具是jjwt,生成token时把用户ID和角色放进claims,过期时间设为24小时。
public class JwtUtil { private static final String SECRET = "your-256-bit-secret-key-please-change-in-prod"; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }鉴权用拦截器实现。定义一个AuthInterceptor,在preHandle里取Header中的Authorization,解析token,成功则把用户信息放入ThreadLocal,失败直接返回401。然后注册拦截器时排除登录接口:
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login"); }拦截器只负责“你有没有登录”,角色权限需要在接口层面单独控制。最简单的方式是在Controller方法上加自定义注解@RequireRole("DOCTOR"),用另一个AOP切面去做校验。这样写的好处是权限逻辑和业务逻辑完全解耦,新增接口时加一行注解就能控制访问粒度。社区健康系统的数据特别敏感,角色控制往细了做是很有必要的,家属接口不允许查询非绑定老人的数据,至少要从源头拦截掉。
3.3 体检记录分页查询与趋势统计Service实现
体检记录的分页查询用MyBatis-Plus非常顺手。关键在于条件构造器的使用,按老人ID过滤、按体检日期倒序排列,同时用前端传来的页码参数分页:
@Service public class HealthRecordServiceImpl implements HealthRecordService { @Autowired private HealthRecordMapper healthRecordMapper; @Override public Page<HealthRecord> pageByElder(Long elderId, Integer pageNum, Integer pageSize) { Page<HealthRecord> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<HealthRecord> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(HealthRecord::getElderId, elderId) .orderByDesc(HealthRecord::getCheckDate); return healthRecordMapper.selectPage(page, wrapper); } }趋势统计是社区健康系统比较有亮点的功能。后端没必要写复杂SQL,把该老人的全部体检记录查出来,在Java层按月份分组求平均血压即可:
@Override public List<Map<String, Object>> trendChart(Long elderId, int months) { LocalDate start = LocalDate.now().minusMonths(months); List<HealthRecord> records = healthRecordMapper.selectList( new LambdaQueryWrapper<HealthRecord>() .eq(HealthRecord::getElderId, elderId) .ge(HealthRecord::getCheckDate, start) .orderByAsc(HealthRecord::getCheckDate)); Map<String, List<HealthRecord>> grouped = records.stream() .collect(Collectors.groupingBy(r -> r.getCheckDate().format(DateTimeFormatter.ofPattern("yyyy-MM")))); List<Map<String, Object>> result = new ArrayList<>(); grouped.forEach((month, list) -> { double avgHigh = list.stream() .mapToInt(HealthRecord::getBloodPressureHigh).average().orElse(0); double avgLow = list.stream() .mapToInt(HealthRecord::getBloodPressureLow).average().orElse(0); Map<String, Object> point = new HashMap<>(); point.put("month", month); point.put("avgHigh", Math.round(avgHigh * 10) / 10.0); point.put("avgLow", Math.round(avgLow * 10) / 10.0); result.add(point); }); return result; }这种写法的好处是逻辑直白、方便调试。等数据量真正大到需要优化时,再改成SQL按月份分组聚合也不迟。做毕业设计或中小型社区项目,这种规模的数据量完全够用。
3.4 定时任务:用药提醒的自动生成
用药提醒是这套系统里最有“智能感”的功能,实现起来依赖Spring Boot自带的@Scheduled。在启动类上加@EnableScheduling,然后定义一个任务类,每天早上8点扫描当天的用药提醒并推送:
@Component public class MedicationReminderTask { @Autowired private MedicationReminderMapper reminderMapper; @Scheduled(cron = "0 0 8 * * ?") public void sendDailyReminder() { List<MedicationReminder> reminders = reminderMapper.selectList( new LambdaQueryWrapper<MedicationReminder>() .eq(MedicationReminder::getStatus, 1) .le(MedicationReminder::getStartDate, LocalDate.now()) .and(w -> w.isNull(MedicationReminder::getEndDate) .or().ge(MedicationReminder::getEndDate, LocalDate.now()))); for (MedicationReminder r : reminders) { // 这里对接短信或微信模板消息,本地演示就打日志 log.info("提醒老人 {} 服用 {},用量 {}", r.getElderId(), r.getMedicationName(), r.getDosage()); } } }cron表达式0 0 8 * * ?表示每天8点执行。这里有个从实践中来的坑:如果系统要部署多个实例做负载均衡,定时任务会在每个节点都执行一遍,提醒就会重复发送。线上环境加一个分布式锁组件,或者在提醒记录表加一个processed_date字段,每个任务执行前先查一下今天是否已经处理过。单机部署做毕设的话,用数据库字段去重就够了,不需要引入额外的中间件。
3.5 Spring Boot自动装配原理与自定义自动配置
很多初学者一直在用Spring Boot却不知道它为什么“零配置”。这里把原理讲清楚:@SpringBootApplication是一个组合注解,等于@SpringBootConfiguration加@EnableAutoConfiguration加@ComponentScan。关键在于@EnableAutoConfiguration会通过AutoConfiguration.imports文件加载所有依赖jar包里的自动配置类,再配合@ConditionalOnClass、@ConditionalOnMissingBean这些条件注解,按需装配组件。比如你引入了spring-boot-starter-web,自动配置类就会在classpath下存在Servlet类时,帮你自动配置DispatcherServlet和Tomcat。
理解这个机制后,你可以自己写一个针对健康数据脱敏的自动配置。比如定义一个DataMaskProperties类:
@ConfigurationProperties(prefix = "health.mask") public class DataMaskProperties { private List<String> fields = new ArrayList<>(); // getter/setter }再写一个自动配置类,条件上判断health.mask.enabled是否为true,如果是就装配一个JSON序列化器,把身份证号这类字段在输出时自动脱敏:
@AutoConfiguration @ConditionalOnProperty(prefix = "health.mask", name = "enabled", havingValue = "true") @EnableConfigurationProperties(DataMaskProperties.class) public class DataMaskAutoConfiguration { @Bean @ConditionalOnMissingBean public DataMaskSerializer dataMaskSerializer() { return new DataMaskSerializer(); } }最后在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里加上这个配置类的全限定名。这个实践的价值在于,它让你从“用框架”跨到“造框架”的层面。真正理解了自动装配,以后再遇到乱码、日期格式等奇怪问题时,你就知道该去哪个自动配置类里找答案了。
4. 前端配合、接口规范与打包部署
4.1 接口路径设计与前后端联调约定
前后端分离开发时,接口路径必须在一开始就定好,不然联调阶段改来改去非常痛苦。我习惯统一用/api前缀,所有接口返回Result结构。这套系统的主要接口可以按下面的表来约定:
| 方法 | 路径 | 功能 | 角色 |
|---|---|---|---|
| POST | /api/auth/login | 登录,返回token | 匿名 |
| GET | /api/elder/page | 分页查询老人档案 | DOCTOR/ADMIN |
| POST | /api/elder | 新建档案 | DOCTOR |
| PUT | /api/elder/{id} | 修改档案 | DOCTOR |
| GET | /api/elder/{id} | 档案详情 | FAMILY/DOCTOR |
| POST | /api/health-record | 新增体检记录 | DOCTOR |
| GET | /api/health-record/trend/{elderId} | 血压/血糖趋势 | DOCTOR/FAMILY |
| GET | /api/medication/today | 查询今日用药提醒 | FAMILY/DOCTOR |
| GET | /api/statistics/community | 社区健康统计汇总 | ADMIN |
分页参数统一叫pageNum和pageSize,排序字段由后端固定,不接受前端传入的排序参数。这样做的目的是避免前端传orderBy=id desc这类字符串给后端拼接SQL带来注入风险。整体风格上偏RESTful但不过度设计,像“修改档案状态”这类动作就用PUT加资源路径,不搞复杂的动词接口。
4.2 Vue前端项目结构与页面要点
前端我用Vue 3加Element Plus,页面结构按业务模块组织:src/api放接口封装,src/views/elder放档案管理页面,src/views/healthRecord放体检记录页面,src/views/statistics放统计图表页面。axios请求封装成request.js,在拦截器里统一加token:
service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res; }, error => { if (error.response && error.response.status === 401) { router.push('/login'); } return Promise.reject(error); } );体检趋势图用ECharts实现,后端接口返回[{month, avgHigh, avgLow}]结构,前端直接映射成两条折线。需要注意的一点是,baseURL不要写死成localhost:8080,用环境变量VUE_APP_BASE_URL区分开发和生产,否则打包部署后一换服务器地址就要重新编译前端。
4.3 把Vue打包进Spring Boot实现单Jar启动
“把前端打包放进Spring Boot”这个需求在部署阶段特别常见,很多小项目不想维护两个独立服务,希望一个jar包全搞定。操作路径是这样的:前端在根目录执行npm run build,生成dist目录;然后把这个目录下的内容复制到后端src/main/resources/static;再用Maven打成jar包。
pom.xml里需要确保静态资源被打进jar,默认Maven插件会把resources目录打进去,一般不用额外配置。如果构建时发现static目录没进去,检查一下是否在maven-resources-plugin里加了不正确的排除规则。
这里有一个容易踩的坑:Vue默认使用history路由模式,打包后刷新某个子路由页面会返回404。因为Spring Boot的静态资源映射找不到对应的前端路由。最省事的方案是把Vue路由改成hash模式(createWebHashHistory),URL会带一个#,不太好看但刷新绝对没问题。要是坚持history模式,后端需要配置一个fallback把未知路径转发到index.html,实现起来也不复杂:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/{path:[^\\.]*}").setViewName("forward:/index.html"); } }我自己的实际体验是毕设演示场景用hash模式足够了,省一个配置类少一个出错点。打包完成后,java -jar community-health.jar一条命令启动,浏览器打开8080端口就能用全套前后端功能。
4.4 宝塔面板+Docker部署的完整流程
如果用宝塔面板部署,Docker是首选方式。先写一个多阶段构建的Dockerfile,把前端构建和后端打包放在一个文件里,避免维护两套镜像:
# 前端构建阶段 FROM node:16-alpine AS frontend WORKDIR /app/frontend COPY frontend/ . RUN npm install && npm run build # 后端构建阶段 FROM maven:3.8-openjdk-11 AS backend WORKDIR /app/backend COPY backend/pom.xml . RUN mvn dependency:go-offline COPY backend/src ./src COPY --from=frontend /app/frontend/dist ./src/main/resources/static RUN mvn clean package -DskipTests # 运行阶段 FROM openjdk:11-jre-slim ENV TZ=Asia/Shanghai WORKDIR /app COPY --from=backend /app/backend/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]然后用docker-compose.yml把应用和MySQL编排在一起:
version: '3' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: community_health ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql command: --default-authentication-plugin=mysql_native_password app: build: . depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/community_health?useSSL=false&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root123 ports: - "8080:8080"这里有个关键点:在Docker环境下,数据库连接地址不能再写localhost,要写服务名mysql,因为容器之间是通过Docker内部网络通信的。宝塔面板上的操作步骤很固定:安装Docker管理器,在/www/wwwroot/下建一个项目目录,上传Dockerfile和docker-compose.yml,在目录里执行docker-compose up -d,然后到安全组里放行8080端口。如果数据库表还没建,可以加一个MySQL初始化脚本目录挂载到/docker-entrypoint-initdb.d/,MySQL容器首次启动时会自动执行,非常省事。
5. 常见问题与排查技巧实录
5.1 Spring Boot版本太高导致的“一地鸡毛”
做这套系统时,我见过太多人栽在版本上。如果你用Spring Boot 3.0以上版本,javax.servlet全部变成了jakarta.servlet,网上很多老代码复制过来直接爆红。而且MyBatis-Plus必须要用3.5.3以上版本才兼容Spring Boot 3,Lombok也要升到1.18.30以上才能配合JDK 17。与其在这些兼容性问题上耗时间,不如直接用Spring Boot 2.7.18加JDK 8或11,开发效率和稳定性都更高。如果非要用新版本,记住一个排查思路:看启动报错的第一行是ClassNotFoundException还是NoSuchMethodError,前者大概率是jar包没引入或名字变了,后者大概率是版本冲突。执行mvn dependency:tree查看依赖树,定位冲突来源,再在pom.xml里显式声明指定版本。
5.2 MyBatis-Plus分页失效和自动填充不生效
分页查询返回的数据不分页,total一直是0,这是MyBatis-Plus新手最常遇到的问题。原因是没配置分页插件:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }创建时间和更新时间的自动填充也有同样的问题。如果你在实体上加了@TableField(fill = FieldFill.INSERT),必须实现MetaObjectHandler接口并在insertFill方法里赋值,否则注解形同虚设:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }这两个配置属于“你说它难吧也不难,但不配就是不行”的典型。建议在项目一创建时就先把这两个配置类写上,别等测试时再补。
5.3 上传体检报告图片报文件过大
社区健康系统一般会给体检报告加图片附件功能,Spring Boot默认单文件上传上限是1MB。上传稍大的体检单照片就会抛MaxUploadSizeExceededException。解决方法是改application.yml配置:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB如果部署时在前面挂了Nginx,还需要在Nginx配置里加client_max_body_size 20m;,因为请求先过Nginx,Nginx这层就拦截掉了。我排查过几次类似问题,每次都是后端改了配置但忘了前端Nginx的限制,所以这里特别提醒一句:链路上一旦有代理层,上传大小限制的坑往往会藏在你最容易忽略的中间层。
5.4 定时任务执行时间差8小时
有人会发现用药提醒定时任务定的是8点,实际执行却是16点,或者数据库里存的时间比真实时间晚8小时。这个问题的根源是时区,不是代码逻辑。排查路径有三处:第一,MySQL连接URL必须带serverTimezone=Asia/Shanghai;第二,运行容器的时区要设置成TZ=Asia/Shanghai,Dockerfile里那行ENV TZ=Asia/Shanghai就是这个用途;第三,服务器系统时区如果是UTC,new Date()取到的时间会跟着偏。三处都设置一致,时间相关的问题基本能消除。这个坑之所以阴险,是因为本地开发环境一切正常,一上服务器就跑偏。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 前端刷新404 | Vue history路由未配置fallback | 改用hash模式或添加ViewController转发 |
| token失效后接口报500 | 拦截器没处理ExpiredJwtException | 拦截器catch异常统一返回401 |
| 数据库连接不上 | 密码不对或MySQL未启动 | 检查URL、用户名、密码;Docker环境用服务名替代localhost |
| 分页total为0 | 缺少分页插件 | 配置PaginationInnerInterceptor |
| 定时任务重复执行 | 多实例部署无任务锁 | 加数据库处理标记或分布式锁 |
| 中文乱码 | 字符集不一致 | 连接URL加characterEncoding=utf8,容器编码配置 |
做这个项目最大的收获不是学会了某个框架的API,而是建立了一种做“业务系统”的完整思维方式。社区老年人健康管理听起来只是CRUD,但真正动手才发现,数据的组织方式、权限的边界划分、定时任务的可靠性、部署环境的一致性,每一层都有值得琢磨的细节。我个人的建议是:先把MVP跑通,再逐步叠加体检趋势和用药提醒这些进阶功能;表结构设计时多想一步“这个字段未来要不要参与统计分析”,可以省掉后期很多重构的功夫。后续如果想让这个项目更有竞争力,方向很明确:对接体检一体机自动采集数据、用药提醒接入短信网关、还是生成社区健康周报,都是比单纯做增删改查有价值得多的扩展点。希望这篇拆解能帮你把系统的业务脉络理清楚,动手实现时少走我踩过的那些弯路。