
做“社区老年人健康管理系统”这个题目坦白说是一个很典型的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.xORMMyBatis-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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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拦截器就要写一堆分支判断。我习惯定义一个ResultT作为所有接口的统一壳子Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT 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 PageHealthRecord pageByElder(Long elderId, Integer pageNum, Integer pageSize) { PageHealthRecord page new Page(pageNum, pageSize); LambdaQueryWrapperHealthRecord wrapper new LambdaQueryWrapper(); wrapper.eq(HealthRecord::getElderId, elderId) .orderByDesc(HealthRecord::getCheckDate); return healthRecordMapper.selectPage(page, wrapper); } }趋势统计是社区健康系统比较有亮点的功能。后端没必要写复杂SQL把该老人的全部体检记录查出来在Java层按月份分组求平均血压即可Override public ListMapString, Object trendChart(Long elderId, int months) { LocalDate start LocalDate.now().minusMonths(months); ListHealthRecord records healthRecordMapper.selectList( new LambdaQueryWrapperHealthRecord() .eq(HealthRecord::getElderId, elderId) .ge(HealthRecord::getCheckDate, start) .orderByAsc(HealthRecord::getCheckDate)); MapString, ListHealthRecord grouped records.stream() .collect(Collectors.groupingBy(r - r.getCheckDate().format(DateTimeFormatter.ofPattern(yyyy-MM)))); ListMapString, 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); MapString, 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() { ListMedicationReminder reminders reminderMapper.selectList( new LambdaQueryWrapperMedicationReminder() .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 ListString 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/ADMINPOST/api/elder新建档案DOCTORPUT/api/elder/{id}修改档案DOCTORGET/api/elder/{id}档案详情FAMILY/DOCTORPOST/api/health-record新增体检记录DOCTORGET/api/health-record/trend/{elderId}血压/血糖趋势DOCTOR/FAMILYGET/api/medication/today查询今日用药提醒FAMILY/DOCTORGET/api/statistics/community社区健康统计汇总ADMIN分页参数统一叫pageNum和pageSize排序字段由后端固定不接受前端传入的排序参数。这样做的目的是避免前端传orderByid 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在拦截器里统一加tokenservice.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模式createWebHashHistoryURL会带一个#不太好看但刷新绝对没问题。要是坚持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 --fromfrontend /app/frontend/dist ./src/main/resources/static RUN mvn clean package -DskipTests # 运行阶段 FROM openjdk:11-jre-slim ENV TZAsia/Shanghai WORKDIR /app COPY --frombackend /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-pluginmysql_native_password app: build: . depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/community_health?useSSLfalseserverTimezoneAsia/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 3Lombok也要升到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;因为请求先过NginxNginx这层就拦截掉了。我排查过几次类似问题每次都是后端改了配置但忘了前端Nginx的限制所以这里特别提醒一句链路上一旦有代理层上传大小限制的坑往往会藏在你最容易忽略的中间层。5.4 定时任务执行时间差8小时有人会发现用药提醒定时任务定的是8点实际执行却是16点或者数据库里存的时间比真实时间晚8小时。这个问题的根源是时区不是代码逻辑。排查路径有三处第一MySQL连接URL必须带serverTimezoneAsia/Shanghai第二运行容器的时区要设置成TZAsia/ShanghaiDockerfile里那行ENV TZAsia/Shanghai就是这个用途第三服务器系统时区如果是UTCnew Date()取到的时间会跟着偏。三处都设置一致时间相关的问题基本能消除。这个坑之所以阴险是因为本地开发环境一切正常一上服务器就跑偏。5.5 常见问题速查表现象可能原因解决办法前端刷新404Vue history路由未配置fallback改用hash模式或添加ViewController转发token失效后接口报500拦截器没处理ExpiredJwtException拦截器catch异常统一返回401数据库连接不上密码不对或MySQL未启动检查URL、用户名、密码Docker环境用服务名替代localhost分页total为0缺少分页插件配置PaginationInnerInterceptor定时任务重复执行多实例部署无任务锁加数据库处理标记或分布式锁中文乱码字符集不一致连接URL加characterEncodingutf8容器编码配置做这个项目最大的收获不是学会了某个框架的API而是建立了一种做“业务系统”的完整思维方式。社区老年人健康管理听起来只是CRUD但真正动手才发现数据的组织方式、权限的边界划分、定时任务的可靠性、部署环境的一致性每一层都有值得琢磨的细节。我个人的建议是先把MVP跑通再逐步叠加体检趋势和用药提醒这些进阶功能表结构设计时多想一步“这个字段未来要不要参与统计分析”可以省掉后期很多重构的功夫。后续如果想让这个项目更有竞争力方向很明确对接体检一体机自动采集数据、用药提醒接入短信网关、还是生成社区健康周报都是比单纯做增删改查有价值得多的扩展点。希望这篇拆解能帮你把系统的业务脉络理清楚动手实现时少走我踩过的那些弯路。