☰
SpringBoot乡村养老服务管理系统:从设计到部署全解析
2026/10/22 11:56:14 网站建设 项目流程

简介:这份毕业设计论文以SpringBoot为核心,围绕乡村养老服务管理系统展开,面向计算机类本科生、研究生及相关开发者。系统覆盖医疗保健、文娱活动、健康档案、老人信息、生活照料订单、土地承包、志愿者与管理员等模块,采用B/S模式与Java语言,配以Mysql数据库,论文从研究背景、目的意义、技术选型、系统分析到编码实现均有完整阐述,适合作为毕业设计选题论证或同类项目开发的参考范本。压缩包内仅含1个docx文档,整体大小4.48MB,文件结构紧凑,便于直接阅读或修改。该资源目前已有75人学习使用,具备一定参考热度。文档价值体现在完整呈现了系统化开发流程,既包含摘要、关键词、目录等论文常规要素,又涉及医疗保健管理、活动报名、健康档案维护等多模块的详细设计,可帮助读者快速理解乡村养老业务的信息化需求,也可为撰写开题报告、需求分析文档或设计说明书提供直接支撑,并结合代码实现思路提升实践能力。

1. 毕设选这个题,值不值:SpringBoot乡村养老服务管理系统到底在做什么

每年毕业季,总有一批人被“乡村养老”类题目吸引,觉得它接地气、好过题、有社会意义,但也有一批人做到一半开始头疼:业务边界太大,功能像无底洞,最后拿出来的系统既不像“乡村”,也不像“养老”。如果你手里正握着“SpringBoot乡村养老服务管理系统”这个题目,先别急着找代码,先想清楚它到底要交付什么。我的理解是:这是一个面向乡镇/村级养老服务机构(或民政代办点)的日常运营管理平台,核心是把老人档案、服务工单、健康记录、补贴发放、床位管理这些线下台账搬到线上,让管理员能查、能派、能统计,让家属能看到老人在机构里的状态。它不是一个社区团购App,也不是大而全的智慧城市平台,而是“机构内部管理 + 家属知情”的小型业务系统。适合的人有三类:一是想要一个工作量适中、能讲清楚业务闭环的毕设选手;二是想用这个方向参加比赛或积累项目的开发者;三是真的想给某个乡村养老服务站做信息化改造的从业者。这篇文章把我这些年做类似系统时的方案、参数和坑一次说透。

2. 先把业务和架构立住:乡村养老场景下的需求拆分与技术选型

2.1 乡村养老和城市养老管理系统,需求差在哪

很多人一上来就照搬城市养老系统的模块:健康监测大屏、在线问诊、智能穿戴接入。放到乡村场景里,大概率会翻车。城市养老机构人员密集、网络基础设施好、管理人员操作水平相对高;而乡村养老服务站点通常规模小,可能就几十张床位,管理员多是村里的工作人员,甚至要同时兼会计和护工。他们最需要的不是炫酷的物联网大屏,而是“今天谁该测血压了、谁的服务工单还没完成、上个月补贴发到哪一步了”这些台账问题。

我一般会建议把需求拆成四个核心场景。第一个是档案管理:老人基本信息、家属联系方式、入住时间、护理等级、既往病史,这是地基。第二个是服务工单:从“老人需要助浴”到“护工接单、完成、确认”,形成闭环。第三个是健康与提醒:定期体检记录、慢病随访提醒、异常指标预警。第四个是补贴与费用:政府补贴发放记录、个人缴费记录、月度统计报表。这四块做完,系统已经能回答“谁在住、谁在服务、健康状况如何、花了多少钱”这四个运营核心问题。

城市系统常见的在线缴费、上门服务预约、志愿者积分等功能,在乡村站点未必跑得通,因为支付习惯、网络条件、管理员精力都有限。毕设阶段更聪明的做法是:把核心闭环做扎实,把加分项做成“可扩展接口”,而不是把所有功能塞进来。比如志愿者管理可以先不做,但预留volunteer表和对应的API,答辩时告诉评委“这部分因为时间原因只做了设计”,比硬凑一个半成品强得多。

2.2 从单体到模块:SpringBoot + MyBatis Plus + Vue 的经典分工

技术选型上,SpringBoot + MyBatis Plus + Vue这套组合几乎是我见过最多的毕设和技术演示配置。理由很实际:SpringBoot让配置收敛,MyBatis Plus能省掉大量单表CRUD的XML,Vue + Element Plus做后台管理界面效率高。数据库用MySQL 8,缓存能用Redis就给公告或Token加一层,不能用就老老实实用JWT本地校验。这套组合不是性能最优解,但是“一个人在一个月内能写完并且能讲清楚”的最优解。

项目结构我习惯按业务模块分包,而不是按技术层分包。具体来说,controller、service、mapper这些技术层目录都保留,但业务包名按模块划分,比如elder、serviceOrder、healthRecord、subsidy。这样做的目的是:答辩时讲项目结构,直接说“我按业务域分了四个模块”,比说“controller层、service层、dao层”听起来更像是做过需求的开发者。另一个好处是后期加功能时不需要在几十个controller文件里找。

理论上经典的分层是Controller→Service→Mapper,但实际开发中我给乡村站点写系统时,会特意在Service层加一层“业务防呆”。比如删除一个老人档案时,如果该老人有未完成的工单或未结算的费用,应该拒绝删除并提示原因。这类逻辑放在Controller里会导致接口臃肿,放在Service里是正路。MyBatis Plus的ServiceImpl已经提供了removeById,但我会重写它,加校验逻辑。这段代码不算复杂,却是答辩中的亮点。

2.3 建表先想清楚:老人档案、工单、床位与补贴的四张核心表

表结构设计是整个项目成败的隐形关键。很多同学在建表时只想着“字段够用”,结果写到统计报表时发现缺状态字段、缺记录时间的区分,只能回头改表结构。下面是我用过的最省心的四张核心表设计思路,表名和字段名可以按你自己的想法调整,但几个关键字段建议保留。

老人档案表elder_info,我一般放这些字段:

字段名类型说明
idbigint主键
namevarchar(32)老人姓名
id_cardvarchar(18)身份证号,做唯一索引
gendertinyint性别:0未知,1男,2女
birthdaydate出生日期
level_of_caretinyint护理等级:1自理,2半自理,3不能自理
bed_idbigint关联床位表
statustinyint状态:1在住,2离院,3档案已注销
create_timedatetime创建时间
update_timedatetime更新时间

注意:status字段不是可选项,没有它,所有统计口径都会乱。比如要查“当前在住老人数量”,直接WHERE status = 1,不需要在where里再拼一堆入住时间和离院时间的条件。

服务工单表service_order是业务闭环的核心,字段设计要能回答“谁派的单、派给谁、干了什么、干完没有、老人认不认”:

字段名类型说明
idbigint主键
elder_idbigint老人ID
service_typetinyint服务类型:1助浴,2助餐,3保洁,4陪诊,5其他
worker_idbigint护工/服务人员ID
assign_timedatetime派单时间
finish_timedatetime完成时间,未完成则为null
statustinyint1待接单,2进行中,3已完成,4已取消
ratingtinyint老人或家属评价:1-5,可为null
remarkvarchar(255)备注

这里有个必须提前决定的点:一个工单是一次性任务还是周期性任务。如果是“每周三助浴”这种周期服务,我建议单独建一张service_plan表,工单表只存实际执行记录。别把周期信息塞进工单表,否则统计“本周应完成服务次数”时SQL会很难写。

床位表和补贴记录表相对简单。床位表bed_info至少要有room_no、bed_no、type(普通床/护理床)、status(0空闲,1占用,2维修)。补贴表subsidy_record要有elder_id、subsidy_month(格式如2025-01)、amount(用decimal(10,2))、status(1待发放,2已发放,3已退回)。amount必须用decimal,不能用float或double,这点后文会细讲。

3. 从空目录到能跑的最小系统:搭建工程与核心接口实现

3.1 用 Spring Initializr 生成工程,依赖选到够用为止

不用从零手写pom.xml。用Spring Initializr(无论网页版还是IDE内置的)生成一个SpringBoot项目,是最快的方式。但依赖别一股脑全选,毕设阶段选多了反而麻烦。我习惯的基础依赖是:Spring Web、MySQL Driver、MyBatis Framework(Spring Initializr里叫这个)、Lombok。如果后面要写接口文档,再加Spring Boot Starter Validation和springdoc-openapi,但这不是必需品。

生成完项目后,我会手动加两个依赖:一个是MyBatis Plus的依赖(因为Initializr自带的MyBatis Framework是原生MyBatis,没有PLus能力),另一个是JWT相关的java-jwt或jjwt。注意版本兼容:MyBatis Plus 3.5.x对SpringBoot 2.x支持很稳,对SpringBoot 3.x需要使用mybatis-plus-spring-boot3-starter,这个版本坑在很多帖子里都出现过。

具体操作步骤:

  1. 访问Spring Initializr页面。
  2. Project选择Maven,Language选择Java,Packaging选择Jar,Java版本选8或11(取决于你本机环境,SpringBoot 2.x配Java 8最省心)。
  3. 依赖里勾选:Spring Web、MySQL Driver、MyBatis Framework、Lombok。
  4. 点击Generate下载压缩包,解压后用IDEA打开。
  5. 在pom.xml中加入MyBatis Plus和JWT依赖。

加入依赖后的关键部分:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>

这里的3.5.3.1是我用得比较多的一个版本,如果你用的是SpringBoot 3.x,需要换成mybatis-plus-spring-boot3-starter,否则启动时会报Failed to auto configure这类错误。JWT的jjwt版本选0.11.x是因为它的API比0.9.x更清晰,SecretKey支持更长的密钥,安全性好一些。

把依赖加好后,别忘了在application.yml里配置数据源和MyBatis Plus的日志。数据源配置是最容易复制错的地方,我一般这样写:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/rural_elder?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto

serverTimezone=Asia/Shanghai很重要,少了它,日期类型在JDBC驱动8.x下会报时区错误。map-underscore-to-camel-case开启后,id_card这种字段能自动映射到idCard,但前提是实体类属性名遵循驼峰命名。id-type: auto代表数据库主键自增,如果你建表时设了主键自增但没配这个,插入数据时会变成默认的ASSIGN_ID雪花算法ID,主键类型对不上会直接报错。

启动项目前,先确认MySQL里建好库,然后在Application类上加上@MapperScan注解,或者给每个Mapper接口加@Mapper注解。我更推荐@MapperScan("你的包名.mapper"),因为不用每个Mapper都写注解,也统一。启动时看到Tomcat started on port(s): 8080就说明工程通了。

3.2 写一个“老人档案分页查询”接口,打通前后端

很多同学喜欢一上来就做登录注册,但我建议先做一个不依赖权限的查询接口,把“数据库→后端→前端”这条链路跑通,再做权限。如果链路没通就开始写登录,出了问题都不知道是前端渲染问题还是后端接口问题。

老人档案分页查询是最典型的入门接口,它包含了实体类、Mapper、Service、Controller四件套。完整代码逻辑如下:

实体类ElderInfo:

@Data @TableName("elder_info") public class ElderInfo { @TableId(type = IdType.AUTO) private Long id; private String name; private String idCard; private Integer gender; private LocalDate birthday; private Integer levelOfCare; private Long bedId; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }

Mapper接口:

@Mapper public interface ElderInfoMapper extends BaseMapper<ElderInfo> { }

Service类:

@Service public class ElderInfoService extends ServiceImpl<ElderInfoMapper, ElderInfo> { public Page<ElderInfo> pageElder(int pageNum, int pageSize, String keyword, Integer status) { LambdaQueryWrapper<ElderInfo> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), ElderInfo::getName, keyword) .eq(status != null, ElderInfo::getStatus, status) .orderByDesc(ElderInfo::getCreateTime); Page<ElderInfo> page = new Page<>(pageNum, pageSize); return this.page(page, wrapper); } }

Controller:

@RestController @RequestMapping("/api/elder") public class ElderInfoController { @Autowired private ElderInfoService elderInfoService; @GetMapping("/page") public Result<Page<ElderInfo>> page(@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) Integer status) { return Result.success(elderInfoService.pageElder(pageNum, pageSize, keyword, status)); } }

这段代码里,LambdaQueryWrapper是MyBatis Plus的条件构造器,like方法第一个参数StringUtils.hasText(keyword)是条件判断,只有keyword有值时才追加模糊查询条件,这是避免空条件乱拼SQL的常用写法。.eq(status != null, ...)同理。Page对象来自MyBatis Plus的分页插件,但注意:光引入依赖还不够,必须配置分页插件,否则page方法只会查询全部再内存分页(后文踩坑部分会详细说)。

Controller里用Result<T>统一包装返回体,我习惯定义成:

public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.message = "success"; r.data = data; return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.code = 500; r.message = msg; return r; } }

统一包装的好处是前端处理逻辑简单:先判断code,再取data。Page对象返回的字段包括records、total、current、size,前端分页组件可以直接用。

3.3 登录与权限:JWT + 拦截器的最简落地

乡村养老系统的角色通常就三类:管理员(机构负责人)、护工(服务人员)、家属(老人子女)。不需要复杂的RBAC权限模型,用一张user表加一个role字段就能应付。JWT方案比Session更适合前后端分离,也更像真实项目。

我的做法是:登录接口只接收手机号和密码,校验通过后生成一个有效期7天的token,token里带上userId和role。前端拿到token后存在localStorage里,每次请求放到请求头Authorization: Bearer <token>。后端写一个拦截器,拦截/api/**下的所有请求(登录接口除外),解析token并把用户信息放入ThreadLocal。这样后续Controller里随时能拿到当前用户是谁。

登录接口核心代码:

@Override public String login(String phone, String password) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getPhone, phone); User user = this.getOne(wrapper); if (user == null || !user.getPassword().equals(DigestUtils.md5DigestAsHex(password.getBytes()))) { throw new RuntimeException("手机号或密码错误"); } if (user.getStatus() == 0) { throw new RuntimeException("账号已被禁用"); } return JwtUtil.createToken(user.getId(), user.getRole()); }

这里密码用DigestUtils.md5DigestAsHex做MD5加密,只是毕设级别的做法。真实生产至少要加盐或使用BCrypt,但毕设里只要不是明文存储,答辩基本说得过去。JWT生成工具类大概是这样:

public class JwtUtil { private static final String SECRET = "rural-elder-secret-key-please-change-1234567890"; private static final long EXPIRE_DAYS = 7; public static String createToken(Long userId, Integer role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_DAYS * 24 * 3600 * 1000)) .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS256) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes())) .parseClaimsJws(token) .getBody(); } }

注意SECRET长度至少32字节,否则hmacShaKeyFor会报WeakKeyException。这是我第一次用jjwt时踩到的坑:密钥设置成了"secret",启动时没问题,一调用就报错。拦截器里解析token时,如果token过期或签名不对,会抛异常,需要拦截异常并返回401。建议在HandlerInterceptor的preHandle里用try-catch包住解析逻辑。

3.4 定时任务:补贴发放提醒与健康数据异常扫描

养老系统里定时任务很出彩,因为它是“系统主动干活”的体现。两个经典场景:每月25号提醒管理员下月补贴要开始造册;每天早上9点扫描昨天录入的健康数据,对血压超过阈值的老人生成关注提醒。用Spring自带的@Scheduled就能实现,不引入Quartz。

示例代码如下:

@Component public class SubsidyRemindTask { @Autowired private SubsidyRecordService subsidyRecordService; @Autowired private ElderInfoService elderInfoService; @Scheduled(cron = "0 0 9 25 * ?") public void remindNextMonthSubsidy() { List<ElderInfo> elders = elderInfoService.listActiveElders(); // 给管理员发送站内信或短信,具体实现可以走消息表 System.out.println("发现" + elders.size() + "位在住老人,下月补贴需造册"); } }

cron表达式里,0 0 9 25 * ?代表每月25号上午9点执行。如果想让任务可配置,可以把cron表达式写到application.yml里,用@Scheduled(cron = "${task.subsidy-remind-cron}")方式读取。这个习惯让项目交付后不用改代码就能调任务时间。

4. 前端管理台与移动端怎么配合:Vue 管理台 + 小程序家属端的取舍

4.1 管理台页面结构:从总览到服务工单的五个核心页面

管理台用Vue 2 + Element UI还是Vue 3 + Element Plus?我推荐Vue 3 + Element Plus,因为新项目和毕设趋势都在这边,踩坑资料也足够多。管理台的页面不要贪多,五个页面足够撑起整个系统:

  • 首页仪表盘:展示在住老人数量、今日待完成工单、本月补贴发放进度、近7天健康异常次数的图表。
  • 老人档案页:分页列表、新增编辑、详情抽屉、护理等级变更。
  • 服务工单页:工单列表、派单、接单、完成、取消,按状态筛选。
  • 健康记录页:每次体检或测量数据的新增、查询、趋势图。
  • 补贴管理页:按月生成补贴名单、标记发放、导出Excel。

技术实现上,Vue Router配置五个路由,Axios封装请求,每个页面对应一个API模块。以工单派单为例,前端逻辑是:从老年列表选择老人,从下拉框选择服务类型,从员工下拉框选择护工,提交到POST /api/order。后端Controller接收后,校验老人状态、护工是否在岗,然后插入工单记录。同时向护工的账号发送一条站内消息(存在message表里),护工登录后能看到待办。这个闭环做出来,比单纯CRUD的评分高不少。

4.2 家属端只做三件事:看档案、收提醒、发申请

家属端我建议做成微信小程序或者H5,不要做成第二个管理台。家属的需求很简单:不知道老人在机构里吃得好不好、护工有没有按时服务、有没有健康异常。所以家属端核心就三个功能:

  1. 查看老人基本信息与护理记录:能在列表里看到最近10条健康记录和服务记录。
  2. 接收主动提醒:后端定时任务扫描到异常指标后,向家属手机号推送短信或微信模板消息。毕设阶段做不了真实推送,可以用站内信模拟,前端有空就轮询/api/family/message/unread。
  3. 提交探访申请或服务申请:家属想给老人加点额外服务,填个申请表,管理员后台能看到并审批。

如果时间有限,家属端可以先用自适应的H5页面实现,放到管理台的同一个域名下,通过/h5路径访问。这样省去了小程序的注册和审核流程,前端代码也能复用一部分Vue组件。但如果你所在学校要求必须是“移动端”,那还是老老实实做小程序。小程序开发中接口请求的域名必须是HTTPS,且要在后台配置合法域名,本地调试用不校验合法域名选项。这个坑每年都有同学踩。

4.3 接口联调时的跨域与 token 携带问题

前后端分离开发中,最烦人的就是跨域。前端跑在http://localhost:8081,后端跑在http://localhost:8080,不同端口就是跨域。SpringBoot解决跨域最简单的方式是加一个配置类:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意:这里使用了allowedOriginPatterns而不是allowedOrigins("*"),因为allowCredentials(true)时allowedOrigins("*")会冲突并报错,这是SpringBoot 2.4以后的改动。如果前后端正式部署时通过Nginx代理到同一域名,跨域配置可以删掉,但开发阶段留着能省很多事。

token携带问题通常是前端忘记在axios拦截器里设置请求头。我常写的axios封装是:

service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }, error => Promise.reject(error));

后端拦截器在解析Authorization头时,记得用request.getHeader("Authorization"),然后截掉Bearer前缀。很多新手直接取整个头部字符串去解析,导致签名校验永远失败。另外,如果用了Spring Security,需要放行OPTIONS请求,否则跨域预检请求会被拦截。我这里没有引入Security,因为毕设项目用拦截器足够,省掉Security的过滤器链能把问题范围缩小。

5. SpringBoot 乡村养老系统踩坑记录:5 个典型问题与排查思路

5.1 时间字段在 JSON 返回时突然多出 8 小时

现象:数据库里create_time存的是2025-01-01 12:00:00,但接口返回的JSON里变成了2025-01-01 20:00:00。

原因:MySQL驱动连接串里的serverTimezone=Asia/Shanghai只解决了JDBC驱动读取时的时区问题,但SpringBoot的Jackson序列化LocalDateTime时使用的是服务器默认时区。如果你本机时区不是东八区(比如某些云服务器默认时区是UTC),Jackson会把数据库读出的LocalDateTime当成UTC时间再按东八区格式化,就多了8小时。

解决:在application.yml里显式指定Jackson时区:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

同时建议所有日期字段使用LocalDateTime或LocalDate,不要用Date。Date类本身就带时区信息,在不同服务器上表现不一致。改完之后重启服务,看返回的时间是否与数据库一致。

5.2 分页总数正确但列表数据串了——MyBatis Plus 的分页插件没配

现象:调用page方法返回的total是对的,但当前页的records总是全部数据,或者第一页和第二页数据一样。

原因:MyBatis Plus物理分页依赖分页插件PaginationInnerInterceptor,如果只引入了mybatis-plus-boot-starter而没有配置分页拦截器,page查询会退化为全表查询,然后在内存里用Page对象截取数据。当查询条件复杂或多表join时,内存分页结果会错乱。

解决:在配置类里添加MyBatis Plus的拦截器:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

注意DbType.MYSQL要和你实际数据库一致,否则生成的limit方言不对。配置完成后,同一个分页接口返回的SQL会自动加上LIMIT ?,日志里能看到。如果用的是MyBatis Plus 3.5.x,这个配置类是必需的。

5.3 文件上传到本地路径,重启后图片全丢

现象:上传的老人照片或身份证附件,开发时存在D://uploads/下,页面能正常访问,但项目重启后图片全部404,或者换一台电脑部署就什么都没有。

原因:本地文件系统不是稳定存储。SpringBoot的静态资源路径默认在classpath:/static/,你把文件写到项目外的绝对路径后,既不在资源目录里,也无法随项目打包。重启后路径没变,但如果是相对路径写到了临时目录,临时目录被系统清理,文件就消失了。

解决:毕设阶段正确做法是:文件存储路径配置到application.yml,并用自定义的映射方式对外提供访问。比如:

file: upload-dir: ./upload/

然后写一个配置类,把/upload/**映射到本地目录:

@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadDir); } }

文件上传接口保存时使用Paths.get(uploadDir, fileName),而不是写死绝对路径。这样项目在本地和服务器上都能跑,只要把upload目录一起备份即可。答辩时如果评委问“图片存在哪里”,你可以说“存在应用目录下的upload文件夹,通过虚拟路径映射访问”,这是一个明确的存储方案,不是临时方案。如果后续部署到Linux,把upload-dir改成/var/rural-elder/upload/,并确认目录有写权限。

5.4 定时任务在集群部署时被重复执行

现象:系统部署了两台服务器,每月25号的补贴提醒任务同时触发两次,家属收到两条重复短信;或者健康扫描任务在同一批数据上处理了两遍。

原因:@Scheduled是进程内调度,每台机器的Spring容器都会执行同一个方法。毕设大多单机部署,但如果评委问“你考虑了高可用吗”,或者你自己为了演示在本地起了两个实例,就会暴露这个问题。

解决:最简单的方法是加一张任务执行记录表,在任务开始时插入一条“任务名+执行时间+唯一索引”的记录,只有插入成功的实例才能继续执行。伪代码:

@Scheduled(cron = "0 0 9 25 * ?") public void remindSubsidy() { String taskName = "subsidyRemind"; String execMonth = LocalDate.now().withDayOfMonth(25).toString(); try { taskLogService.save(taskName, execMonth); } catch (DuplicateKeyException e) { log.warn("任务已被其他实例执行,跳过"); return; } // 实际任务逻辑 }

这里task_log表给(task_name, exec_month)加唯一约束,后插入的实例会抛出重复键异常,从而跳过。这个方法不需要引入分布式锁中间件,逻辑透明,答辩时容易讲清楚。当然,真实生产会用Redis的SETNX或ShedLock,但毕设里把“唯一约束防重”说清楚已经能证明你考虑过并发问题。

5.5 补贴金额用 double 计算,对账差了一分钱

现象:算月度补贴总额时,两条记录分别是200.30元和100.60元,数据库amount字段是decimal(10,2),但Java实体里定义成了double,页面显示总金额是300.9,再乘个税率或加个手续费,对账就差了0.01甚至0.02。

原因:二进制浮点数无法精确表示0.1、0.2这类十进制小数,double运算会产生误差。数据库的decimal精确,但Java的double不精确,二者一旦在代码里参与运算,误差就会传播。

解决:实体类所有金额字段一律使用BigDecimal,数据库字段保持decimal(10,2)。计算时使用BigDecimal的add、multiply方法,不要用+、*。另外注意BigDecimal构造时优先使用字符串构造:new BigDecimal("200.30"),不要用new BigDecimal(200.30),后者本身就不精确。MyBatis Plus处理BigDecimal没有任何问题,JSON序列化时前端也能正常显示成数字。

我在某个模拟项目里写过一个补贴统计方法,最初用了double,后来改成BigDecimal,代码变化不大,但对账结果从“基本对不上”变成“分毫不差”。这个坑非常适合写进毕设论文的“问题与解决”章节,属于能体现工程素养的细节。

6. 让毕设从“能跑”到“能答辩”:验证数据、压测与演示技巧

论文和工作汇报有一个共同点:系统不是做出来就完了,而是要被别人“看见”并理解。很多同学系统功能很全,但答辩时用几张静态页面截图,或者现场操作时数据是空的、点哪里都提示“暂无数据”,这就把优势全丢了。我习惯的做法有三步。

第一步,准备一套完整的演示数据。不要用“张三”“李四”这种占位数据,而要模拟一个真实乡村养老服务站的运转状态。比如在某村里,有30位在住老人,年龄分布在65到89岁,护理等级分布为自理10人、半自理15人、不能自理5人。服务工单有今天未完成的、已完成的和被取消的;补贴记录有上月已发放和本月待发放的;健康记录里有几条血压偏高的异常数据。数据量不用很大,但状态要覆盖全。这样演示时,每一个列表都有内容,统计图表有曲线,筛选功能有结果。准备数据的SQL脚本要写进项目里,放在doc/data.sql中,别人拿到项目后能一键导入。

第二步,做一次简单的接口压测并保留报告。不用专业的压测平台,就用JMeter或Postman的Collection Runner,对分页查询、登录、工单提交三个核心接口各发500次请求,记录响应时间和错误率。哪怕你的系统只能扛住每秒50个请求,在“乡村站点”这个场景下也是够用的。答辩时可以主动说:“我对核心接口做了500次循环测试,平均响应时间120ms,错误率0%。”这个数据比“系统性能良好”这种描述有说服力得多。如果测试中发现某个接口特别慢,优先检查有没有合适的索引。比如service_order表里频繁按elder_id和status筛选,那我就在这两个字段上建联合索引:

ALTER TABLE service_order ADD INDEX idx_elder_status (elder_id, status);

写进论文的“系统优化”部分也是加分项。

第三步,把演示流程变成一条“故事线”。不要从登录页开始逐个点菜单,而是模拟一个场景:某个家属打电话来问“我父亲今天血压高不高”,管理员打开系统,在老人档案里找到这位老人,点开健康记录,看到今早测量的血压值偏高,于是点开服务工单,查看今天有没有陪诊工单,发现还没有,立即创建一个陪诊工单并派给护工,同时系统自动给家属发了一条提醒消息。在这个流程中,你依次展示了档案管理、健康记录、工单闭环、消息提醒四个模块,而且所有操作之间都有逻辑关系,评委听着不困,也容易理解你的系统设计价值。

最后,我自己的一个习惯是:把手写脑子里常见问题的答案写在一个文档里。比如“为什么用JWT而不用Session?”“MyBatis Plus和MyBatis有什么区别?”“如果老人数量变成1万人,你这个系统哪里会先撑不住?”这些问题不准备,现场容易卡壳。特别是“会不会先撑不住”这种问题,其实考察的是你是否知道瓶颈在哪。你可以回答:“单表查询在1万数据量下没问题,但如果并发访问升高,我会先给热点表加索引,再考虑引入Redis缓存老人档案热点数据和工单状态。”这个回答就把系统的扩展边界说清楚了。

这个方向说到底不是做什么高科技,而是把线下的纸质台账变成线上的协作工具。认真把闭环做完,把数据造像,把几个坑讲明白,你的SpringBoot乡村养老服务管理系统就能从“能跑”变成“能拿得出手”。希望这篇笔记能帮你把这个题目做得踏实,也希望你在答辩时,能把自己写过的每一行代码背后的选择讲清楚。真的,把自己做的东西讲明白,比堆功能重要得多,这是我一直以来的教训——希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询