简介这套面向养老服务的东软颐养社区系统采用Java技术开发可作为Java学习者、初级开发者及养老系统设计人员的实战案例。压缩包内共163个文件包含35个Java源码文件、105个编译后的字节码文件同时附有依赖库、界面图片、说明文档等整体大小3.25MB文件类型分配清晰便于对照学习。目前已有8310人学习下载。通过源码可以梳理数据访问对象、实体模型、列表管理等分层设计思路并结合界面开发、数据库访问、对象关系映射、多线程处理和安全控制等Java核心技术了解健康管理、社交互动等养老功能模块的落地实现。附带的字节码及工程配置信息也能帮助反推运行机制适合用于系统化练习企业级Java开发。1. 颐养社区Java版解压就能跑的养老机构管理平台某养老社区的管理员每天要处理的事无非几件帮新入住的老人登记档案、分配床位、记录血压血糖、给护工派服务工单、月底对账。我拿到这份 java 版颐养社区源码时第一反应是“又一个 CRUD 后台”但翻完代码后发现它把养老机构运营里最有业务味的流程都做了进去工单状态流转、健康档案查询、入住退住管理、费用记录。整套项目是标准的 Spring Boot 单体架构解压后配好数据库就能直接启动。对想正经跑通 Spring Boot 全流程的新手或者要在内部管理系统上快速做二次开发的开发者来说它都是一个能直接落地的底座不是那种只教你写 hello world 的 demo。2. 技术架构拆解Spring Boot MyBatis MySQL 的三层设计2.1 为什么是 Spring Boot MyBatis 这套组合拿到项目先看 pom.xml技术选型很经典Spring Boot 2.x 做基础框架ORM 用的是 MyBatis 而不是 Spring Data JPA。这个选择比较符合国内后台管理系统的开发习惯颐养社区里大量涉及多表关联查询比如按护工维度汇总工单、按月份统计收费用 MyBatis 写 XML 可以明确控制每一条 SQL而不是让框架自动生成难优化的语句。Spring Boot 负责把整个项目组装起来内嵌 Tomcat 解决了部署环境问题MyBatis 负责数据访问层把 entity 和数据库表之间的映射关系交给你自己掌控MySQL 存业务数据。安全部分用了 JWT 做无状态鉴权配合 Spring MVC 的拦截器保护接口前端没有上前后端分离那套用的是服务端渲染页面模板加 jQuery 的 AJAX 请求这样在低配置服务器上也能流畅运行部署时只打一个 jar 包就行。2.2 后端目录结构与包职责压缩包解压后的结构是标准的 Maven 布局我把核心部分标出来yiyang-community/ ├── pom.xml ├── sql/ │ ├── init.sql # 建库建表脚本 │ └── data.sql # 初始化演示数据 └── src/main/ ├── java/com/yiyang/ │ ├── YiyangApplication.java # Spring Boot 启动类 │ ├── config/ # 拦截器、CORS、WebMvc 配置 │ ├── controller/ # 接口层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis 数据访问接口 │ ├── entity/ # 数据库实体类 │ └── util/ # JWT、MD5 等工具类 └── resources/ ├── application.yml # 数据源、端口、MyBatis 配置 ├── mapper/*.xml # SQL 映射文件 └── templates/ # 前端页面登录、老人列表、工单列表等各层职责很清晰controller 只接收参数、调用 service、封装返回值service 写业务规则判断比如工单状态能不能流转、老人能不能退住mapper 接口和 XML 一一对应负责 SQL 执行entity 里每个类对应一张核心表。这种分层方式的好处是二次开发新模块时照着这个目录再复制一份改就行。2.3 数据模型核心业务表怎么关联整个项目围绕“老人”这个核心主体展开表结构设计不复杂但覆盖了养老社区的主要业务面我把核心表整理了一下表名职责关键关联sys_user后台登录账号区分管理员与护工角色elder_info老人档案关联床位、关联工单bed_info床位信息标记占用状态service_order服务工单关联老人与护工health_record健康记录关联老人activity_info社区活动参与老人记录fee_record费用记录关联老人与收费项目sys_menu菜单权限控制页面可见性关联关系里最关键的一条是 elder_info 和 service_order老人入住时生成一条 status 为“在住”的档案记录护工的服务工单通过 elder_id 关联到老人。健康记录和费用记录都以 elder_id 为外键查询时用 JOIN 把老人姓名带出来。这种设计对单体管理系统够用也方便后续扩展比如要给老人加亲属联系人单独建表关联 elder_id 就行。2.4 前后端约定与统一返回结构前端页面放在 resources/templates 下页面和 controller 之间通过 AJAX 交互。接口统一返回 Result 结构避免每个接口返回格式不一致导致前端到处兼容Data public class Result { private Integer code; // 200 成功500 业务失败401 未授权 private String message; // 提示信息 private Object data; // 业务数据可为 List 或 Map }前端拿到 Result 后先判断 code 再渲染数据登录页面保存 token 到本地存储之后每个请求在 header 里带 Authorization 字段。这个约定贯穿整个项目二次开发时新增接口也按这个格式返回前端页面可以直接复用已有的 AJAX 封装方法。3. 核心模块实现从登录鉴权到服务工单的完整链路3.1 登录鉴权JWT 签发与拦截器校验登录接口是整个系统的入口逻辑比较直接根据用户名查数据库比对密码通过后签发 JWT 返回给前端。项目源码里的 LoginController 核心代码如下RestController RequestMapping(/api/auth) public class LoginController { Autowired private SysUserService userService; Autowired private JwtUtil jwtUtil; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { SysUser user userService.findByUsername(dto.getUsername()); // 演示项目密码用 MD5 存储正式项目建议换成 BCrypt if (user ! null user.getPassword() .equals(DigestUtils.md5Hex(dto.getPassword()))) { String token jwtUtil.createToken( user.getId(), user.getUsername(), user.getRole()); return Result.success(token); } return Result.error(用户名或密码错误); } }这里牵扯到一个选择题为什么用 MD5 而不是 BCrypt源码里写的是 MD5因为演示项目的 data.sql 里预置的密码都是 MD5 密文你直接改密文格式会让初始化数据失效。实战里如果要把这个系统拿到正式环境第一件事就是把密码改为 BCrypt 加密再加一次盐。有了 token 之后需要拦截器统一校验每次请求的合法性。JwtInterceptor 实现了 Spring MVC 的 HandlerInterceptor 接口Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); if (JwtUtil.verify(token)) { return true; } } response.setStatus(401); return false; } }拦截器通过 WebMvcConfigurer 注册并且要排除登录接口和静态资源路径否则会出现“还没登录就被拦下来”的尴尬情况Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /static/**, /templates/**); }这里最容易忽略的就是 OPTIONS 预检请求放行如果你给这个项目加上跨域配置后忘了处理预检前端浏览器会报 CORS 错误但后端日志里看不到任何异常排查起来很玄学。3.2 老人档案入住登记怎么落库老人档案是养老社区的基础数据入住登记接口做的事情不复杂但涉及一个业务判断床位状态检查。新增老人时前端传进来姓名、身份证、床位号、紧急联系人后端要先确认这个床位没有被占用再插入 elder_info 表。Service public class ElderService { Autowired private ElderMapper elderMapper; Autowired private BedMapper bedMapper; Transactional public void checkIn(ElderInfo elder) { BedInfo bed bedMapper.findById(elder.getBedNo()); if (bed null) { throw new BizException(床位不存在); } if (1.equals(bed.getStatus())) { throw new BizException(该床位已占用请重新选择); } elder.setStatus(1); // 1 在住 elderMapper.insertElder(elder); bedMapper.updateStatus(elder.getBedNo(), 1); // 床位标记占用 } }注意 Transactional 注解这里涉及两张表的写操作插入老人档案和更新床位状态必须保证事务一致性否则会出现“人住进来了但床位还是空”的脏数据。我在改这个模块时就翻过一次车当时只写了 elderMapper.insertElder忘记更新床位状态管理员在床位页面看到的永远是可分配状态。对应的 mapper XML 片段如下插入时字段写详细一点方便二次开发时看清楚到底落了哪些列insert idinsertElder parameterTypecom.yiyang.entity.ElderInfo INSERT INTO elder_info ( name, id_card, gender, age, phone, emergency_contact, bed_no, status ) VALUES ( #{name}, #{idCard}, #{gender}, #{age}, #{phone}, #{emergencyContact}, #{bedNo}, #{status} ) /insert前端页面上入住登记的表单字段和这段 XML 的列名一一对应新增模块时照这个模板改不容易漏字段。还有一个实际细节身份证号在数据库里建议加唯一索引防止同一个老人重复入住。3.3 服务工单状态机约束派单与完成工单模块是这套系统里最能体现业务逻辑的地方它不是一个简单的增删改查而是有明确状态流转规则的待派单 - 进行中 - 已完成。ServiceOrderService 里写得比较清楚Service public class ServiceOrderService { Autowired private ServiceOrderMapper orderMapper; public void createOrder(ServiceOrder order) { // 新工单默认状态为待派单 order.setStatus(待派单); order.setCreateTime(new Date()); orderMapper.insert(order); } public void assign(Long orderId, Long workerId) { ServiceOrder order orderMapper.findById(orderId); if (order null) { throw new BizException(工单不存在); } // 状态机校验只有待派单的工单才能派单 if (!待派单.equals(order.getStatus())) { throw new BizException(当前状态不允许派单); } order.setWorkerId(workerId); order.setStatus(进行中); orderMapper.update(order); } public void complete(Long orderId) { ServiceOrder order orderMapper.findById(orderId); if (!进行中.equals(order.getStatus())) { throw new BizException(只有进行中的工单才能完成); } order.setStatus(已完成); order.setEndTime(new Date()); orderMapper.update(order); } }业务规则就在这里体现出来了派单之前必须先创建工单工单一旦进入“进行中”就不能重复派单只有“进行中”状态才能点完成完成后不允许再改回进行中。这种状态机写法比散落的 if 判断更安全也方便你后续加“异常关闭”或“转派”状态时照着这个模式扩展。3.4 健康档案时间范围查询与分页健康记录是老人档案的延伸模块护工每次给老人测完血压血糖后录入一条记录。列表查询支持按时间和老人筛选项目里直接在 mapper 接口上用注解写 SQLMapper public interface HealthRecordMapper { Select(script SELECT * FROM health_record WHERE elder_id #{elderId} if teststartDate ! nullAND record_date gt; #{startDate}/if if testendDate ! nullAND record_date lt; #{endDate}/if ORDER BY record_date DESC /script) ListHealthRecord queryByCondition(Param(elderId) Long elderId, Param(startDate) String startDate, Param(endDate) String endDate); }这个写法有个关键点用了 MyBatis 动态 SQL 的 script 标签和 XML 里写 效果一样,但可以直接写在注解里适合单表简单查询。注意大于小于号必须转义成 和 不然解析 SQL 时会报 XML 语法错误这是新手最容易踩的坑之一。4. 本地运行指南从装环境到页面验证的 5 个步骤4.1 准备 JDK8、Maven 与 MySQL先把最基础的环境装齐。项目是基于 Spring Boot 2.x 开发的JDK 1.8 就能编译运行我建议装 JDK 8 而不是 JDK 11避免出现依赖兼容问题。Maven 用 3.6.0 以上的版本直接到官网下载解压后配置环境变量即可。MySQL 建议 5.7 或 8.0两个版本跑这套系统都没问题但数据库驱动和连接串写法上有差别。项目里用的是 MySQL 8 的驱动如果你是 MySQL 5.7连接串里要加上 useSSLfalse不然启动时会报 SSL 警告。这些细节在下面的配置环节会说到。检查环境是否就绪命令行执行三条命令java -version mvn -version mysql --version三条命令都能输出版本号说明环境没问题。如果 mvn 命令找不到检查 MAVEN_HOME 环境变量有没有配置以及 Path 里有没有加 bin 目录。4.2 执行 SQL 脚本初始化数据库进入 sql 目录先建库再导数据。我一般不用 navicat 或 idea 的数据库工具点按钮直接用命令行执行脚本这样能确保脚本执行的顺序受控mysql -uroot -p123456 sql/init.sql mysql -uroot -p123456 sql/data.sqlinit.sql 里包含 CREATE DATABASE 和 CREATE TABLE 语句data.sql 里的内容是演示数据预置了管理员账号、床位、老人档案和几条服务工单。执行完后检查一下USE yiyang_community; SHOW TABLES; SELECT * FROM sys_user;sys_user 表里应该有一条 admin 账号记录密码字段是 MD5 密文。如果查询出来是空表说明 data.sql 执行失败大概率是密码里的特殊字符在命令行里被转义了可以用mysql -uroot -p交互方式进入后再 source 导入。4.3 修改数据源与端口配置打开 src/main/resources/application.yml重点改数据源部分spring: datasource: url: jdbc:mysql://localhost:3306/yiyang_community?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mvc: static-path-pattern: /static/** server: port: 8080 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.yiyang.entityusername 和 password 一定改成你自己本机的 MySQL 账号密码。url 里的 serverTimezoneAsia/Shanghai 不建议删MySQL 8 默认时区是 UTC不指定会让日期字段差 8 小时查询结果跟实际时间对不上。如果你本机 MySQL 是 5.7可以去掉 driver-class-name 那行让系统自动加载驱动。4.4 以打包或开发模式启动环境就绪后有两种启动方式。开发调试时推荐用 Maven 内置的插件直接跑mvn spring-boot:run这种方式的好处是修改代码后 spring-boot-devtools 会自动重启适合边改边看效果。缺点是控制台日志输出比较乱出现报错时不容易定位。我一般先用第二种方式确认能正常启动再切回开发模式改代码mvn clean package -DskipTests java -jar target/yiyang-community-1.0.jar打包启动后看到类似Started YiyangApplication in 5.2 seconds的日志说明启动成功了。这里有个小坑如果你的机器装了 8080 端口被占用启动会失败改 server.port 为 8081 再试。4.5 登录后台验证功能启动成功后浏览器访问 http://localhost:8080/ 会跳转到登录页。输入 data.sql 里预置的账号 admin密码是 123456如果你没有改过 data.sql 的话。登录成功后进入后台主页左侧菜单应该能看到老人档案、服务工单、健康记录、费用管理。验证三个关键链路先到老人档案页面新增一位老人选一个未被占用的床位保存后去床位页面确认状态变成已占用再到服务工单页面给这位老人创建一张工单状态是“待派单”点派单选一位护工状态变成“进行中”最后给这位老人录入一条血压记录在健康档案页面按时间查询能查到。这三条链路走通说明项目的基础功能是正常的可以开始做二次开发了。5. 常见问题与避坑跑通项目时踩过的 5 个坑5.1 数据库连接失败时区与驱动版本现象启动时报Access denied for user rootlocalhost或Communications link failure无法连接数据库。原因分两种一种是用户名密码和 application.yml 里不一致纯配置问题另一种是 MySQL 8 的驱动要求连接串必须带上时区参数不带 serverTimezone 时驱动默认用 UTC连接可能超时失败。解决先确认密码用命令行能连上再把 url 改成完整格式jdbc:mysql://localhost:3306/yiyang_community?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。如果密码里有特殊字符比如 符号连接串里要改写为 %40 编码。5.2 登录后主页 404静态资源路径被拦截现象登录接口返回成功token 也拿到了但跳转主页后页面样式全丢或者接口请求被 401 拦截。原因JWT 拦截器的拦截范围是 /api/**但有些静态资源或者页面请求路径也在 /api 下比如 favicon.ico 和部分模板引用被拦截器挡住后返回 401前端渲染就乱了。解决在 WebMvcConfigurer 的 excludePathPatterns 里显式放行静态资源我建议把 /static/、/templates/、/favicon.ico 都加进去。改完重启项目再试页面样式能正常加载。5.3 JWT 过期后接口返回 401现象系统用了一段时间后用户反馈“明明登录着突然操作失败”后端日志里一堆 401。原因项目里 JWT 设置了有效期默认可能是 2 小时或 24 小时前端 token 过期后没有做静默刷新所有接口请求都被拦截器挡下。解决最简单的方案是在前端 AJAX 封装里捕获 401 状态码跳回登录页重新登录。我一般还会在后端加一个刷新 token 的接口处理长时操作场景但演示项目里暂时不够用先保证 401 时提示明确“登录已过期请重新登录”。后续可以做双 token 机制这个放在二次开发章讲。5.4 中文乱码连接串缺失 encoding现象页面上老人姓名显示成问号或者录入中文后数据库查出来是乱码。原因数据库连接串里没有 characterEncodingUTF-8 参数MySQL 默认字符集可能是 latin1插入中文时转码失败。解决application.yml 的 url 末尾一定要加useUnicodetruecharacterEncodingutf8同时确认 init.sql 建表时指定了DEFAULT CHARSETutf8mb4。如果已经建好表可以用 ALTER TABLE 调整字符集但最稳妥的还是重建库用对的编码初始化。5.5 Mapper XML 找不到target 目录缓存现象启动时不报错但一调用某个接口就报Invalid bound statement (not found)明明 mapper XML 文件存在。原因IDEA 或 Eclipse 编译时没有把 resources/mapper 下的 XML 文件复制到 target 目录MyBatis 扫描 mapper-locations 时找不到对应 XML。解决检查 pom.xml 里是否把 XML 文件排除掉了。我遇到翻车时直接执行mvn clean packageclean 会清空 target 目录重新编译通常能解决。如果还不行检查 application.yml 的 mapper-locations 路径是否写成classpath:mapper/*.xml注意 classpath 后面不用加斜杠。6. 二次开发实战给颐养社区新增体检报告模块6.1 新增业务表与实体类拿这套系统做二次开发最适合的练手场景是加一个体检报告模块。老人档案里只有健康记录但健康记录是零散的血压血糖数据养老社区通常还要求保存年度体检报告的整体结论所以我们在健康记录之外单独建一张表CREATE TABLE health_report ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_id BIGINT NOT NULL COMMENT 老人ID, report_date DATE NOT NULL COMMENT 体检日期, height VARCHAR(20) COMMENT 身高cm, weight VARCHAR(20) COMMENT 体重kg, blood_pressure VARCHAR(30) COMMENT 血压, doctor_comment VARCHAR(500) COMMENT 医师建议, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) DEFAULT CHARSETutf8mb4;实体类照抄 ElderInfo 的写法字段类型和表结构一一对应注意 Long 对应 BIGINT、String 对应 VARCHAR、Date 对应 DATE。生成完实体类后复制一份 HealthRecordMapper改成 HealthReportMapper这样可以保证新的模块和原有模块风格完全统一别人看代码时也容易找到对应关系。6.2 编写 Mapper XML 与 Service新增查询接口按老人 ID 和时间范围查询体检历史。我直接参考 HealthRecord 的 XML 写法保持动态 SQL 风格一致select idqueryReportsByElderId resultTypeHealthReport SELECT * FROM health_report WHERE elder_id #{elderId} if teststartDate ! null and startDate ! AND report_date gt; #{startDate} /if if testendDate ! null and endDate ! AND report_date lt; #{endDate} /if ORDER BY report_date DESC /selectService 里写一个方法查询后把老人姓名补充进去方便前端列表直接展示public ListHealthReportVO listReports(Long elderId, String startDate, String endDate) { ListHealthReport reports reportMapper.queryReportsByElderId(elderId, startDate, endDate); return reports.stream().map(report - { HealthReportVO vo new HealthReportVO(); BeanUtils.copyProperties(report, vo); ElderInfo elder elderMapper.findById(report.getElderId()); vo.setElderName(elder null ? 未知 : elder.getName()); return vo; }).collect(Collectors.toList()); }Controller 沿用 Result 封装前端页面复制 health_record/list.html 改成相应模板菜单表 sys_menu 里插入一条记录整个模块就能出现在左侧导航栏了。这个过程本质上就是“照着已有模块复制一份替换业务字段”比从零搭一个 Spring Boot 项目效率高太多。6.3 验证改造结果并形成习惯模块加完验证方式还是走那三条链路录入一条体检报告列表里能查到且老人姓名显示正常按日期筛选范围外的数据不出现修改一条记录后重新查询字段内容已更新。同时打开控制台确认接口响应时间没有明显增加。这套系统主要价值在于业务流程完整、结构清晰你能顺着它的骨架把一个新需求快速变成可运行的页面和接口。但它的密码加密、JWT 过期策略都还停留在演示水平真要上生产环境第一个改动应该是密码加密方案和操作日志表。从那以后我每次拿到新项目不管多着急都会先做三件事跑初始化脚本、检查连接串编码、登一次后台把主流程点一遍。这三件事不做完我心里就没底。这套颐养社区 Java 版你可以按这个流程拆一遍应该能省下不少碰壁的冤枉时间希望帮到你。本文还有配套的精品资源点击获取