做养老公寓管理系统,和你平时做的那些通用后台真不太一样。拆这套"SpringBoot+Vue+MyBatis+MySQL"完整源码之前,我先给一个判断:这类项目真正的难点从来不在某个技术点,而在于"业务状态怎么建模、健康数据怎么串联、护理任务怎么闭环"。这套企业级大健康养老公寓管理系统,把老人档案、入住退住、床位分配、护理计划、健康体征、费用结算、后勤报修、经营报表全部串在一条线上,适合正在做医疗健康类项目、想转行智慧养老方向、或者机构内部想用系统替代Excel表格的实施人员和开发者参考。我把它从部署到核心模块完整复现了一遍,下面把设计思路、关键代码和踩过的坑都摊开讲。
1. 项目整体设计与技术选型拆解
1.1 为什么是SpringBoot+Vue+MyBatis+MySQL这套组合
选这套技术栈,第一感觉是"稳"。养老公寓管理系统属于典型的传统企业级业务系统:并发量不会爆炸,但流程链路长、状态多、报表杂、权限细。SpringBoot负责快速搭建服务和统一配置,Vue负责做前后端分离的界面,MyBatis写SQL足够灵活,MySQL在中小型机构的数据量下完全够用,运维成本也低。这套组合在医疗、政务、传统行业里沉淀了很多年,资料多,招人也容易,后续换人维护不会太痛苦。
有人说既然要快,为什么不直接上MyBatis-Plus,甚至用JPA?我的看法是:这个项目的核心痛点是"复杂条件查询、动态报表、多表联查"。MyBatis手写SQL在这种场景下掌控力最强,SQL性能出问题可以直接定位,不会像JPA那样在N+1和复杂统计上绕圈子。MyBatis-Plus确实能省掉单表CRUD的代码,但如果源码把MP的BaseMapper写到底,后续业务查询逻辑全拼LambdaQueryWrapper,SQL的可读性和维护性反而差。所以这个项目选择原生MyBatis,是符合企业级应用"重业务、重维护"特点的合理决定。
前端选Vue,不是因为React不好,而是因为国内中小型后台管理系统的生态里,Vue+组件库的成熟度、中后台模板的丰富度确实最高。项目里大量列表、表单、弹窗、树形菜单,用配套组件能在一两周内搭出完整管理台,对于养老公寓这种预算有限、上线周期紧的项目,这是实打实的优势。
1.2 大健康养老场景到底"大"在哪里
"大健康"三个字,决定了这套系统不能只做"老人登记+床位管理+收钱"这类酒店式管理。真正的智慧养老机构里,系统还要承担健康监测、慢病随访、护理执行、家属互动这些任务。它不像酒店管理系统那样"入住即结束",而更像"预约挂号+护理执行+物资进销存"的合体。
具体来说,大健康场景有几个特征:
- 数据从"录入"变成"采集"。老人生命体征可能来自智能手环、血压计、血糖仪,系统需要定义好数据接入和处理路径。
- 角色特别多。院长要看经营数据,护士长要排班和分配任务,护理员要接单和执行,财务要出账,家属要远程看老人的健康和费用情况。
- 流程特别长。入院要评估、签约、分配房间、制定护理计划;住进来之后每天有护理任务、健康记录;退住还要结清费用、交接物品。流程任何一个环节断了,线下运营就会乱。
- 数据要能"追溯"。老人跌倒、异常体征、用药反馈这类事件,系统里必须有留痕机制,方便机构复盘和家长安心。
我前阵子帮一家机构做调研,发现他们还在用Excel加微信群管理老人信息,护理员交接班靠嘴说。不是他们不想用系统,而是市面上的产品要么太重,要么根本不贴合机构流程。这套源码的价值,正是把"健康数据"和"照护流程"这两条主线刻进了数据模型,而不是只做一个表面光鲜的增删改查界面。
1.3 总体模块划分与功能地图
整个系统我建议按七个模块来理解,每个模块对应机构里一个具体的业务职能:
| 模块 | 核心功能 | 涉及核心表 |
|---|---|---|
| 系统管理 | 用户、角色、权限、菜单、操作日志 | sys_user、sys_role、sys_menu |
| 入住管理 | 咨询登记、入住评估、签约、退住办理 | elder、check_in_info、assessment |
| 床位管理 | 楼栋/楼层/房间/床位的层次与状态流转 | building、room、bed |
| 护理管理 | 护理等级、护理计划、任务生成与执行 | care_level、care_plan、care_task |
| 健康管理 | 体征记录、体检、慢病随访、异常预警 | health_record、physical_exam |
| 费用管理 | 按月度计费、收费、退款、对账 | charge_bill、charge_item、payment_log |
| 报表中心 | 入住率、护理工作量、营收统计 | 通过聚合SQL生成 |
模块之间不是孤立的:老人入住之后,床位状态要变,护理计划要生成,费用账单要从入住日期开始计费;老人体检查出异常,系统要能生成预警并通知护士。理解了这条数据流,后面看代码才会觉得顺。
2. 核心功能模块与数据模型设计
2.1 老人档案与健康数据怎么关联最合理
老人档案表是整个系统的地基。字段量会很大,我从这个项目中抽出几个关键点:档案编号、姓名、性别、生日、身份证号、家属联系方式、健康等级、入住状态、评估状态。身份证号虽然正常都会存,但我强烈建议在实体层做加密处理,数据库里不要存明文,页面展示要脱敏,健康数据属于敏感数据,这个底线必须守住。
CREATE TABLE elder ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_no VARCHAR(32) NOT NULL COMMENT '档案编号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT COMMENT '1男 2女', birthday DATE, id_card VARCHAR(18), phone VARCHAR(20), emergency_contact VARCHAR(50), emergency_phone VARCHAR(20), health_level TINYINT COMMENT '1自理 2半自理 3全护理 4特护', assessment_status TINYINT COMMENT '评估状态', status TINYINT COMMENT '1待入住 2在住 3已退住', create_time DATETIME, update_time DATETIME ) COMMENT = '老人档案';健康记录不要直接塞在档案表里。老人住一个月,每天测两次血压血糖,就是60条记录,塞在同一张表会导致档案行爆炸,查询也会变慢。正确做法是单独建一张高频写入的健康记录表,通过elder_id关联:
CREATE TABLE health_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_id BIGINT NOT NULL, record_date DATE NOT NULL, systolic INT COMMENT '收缩压', diastolic INT COMMENT '舒张压', heart_rate INT, blood_sugar DECIMAL(5,2), temperature DECIMAL(4,2), abnormal_flag TINYINT COMMENT '0正常 1异常', record_time DATETIME );这里有个设计心得:体征数据表要保留"异常标记"字段。判断是否异常可以在后端做规则计算,但标记一定要落库,因为护理端需要按异常记录快速筛选,报表端还要统计"本月异常事件数",到时候再临时算会很痛苦。
2.2 入住退住与床位状态机
床位管理如果不设计状态机,很容易变成"显示一张房间表"这么简单。实际运营里,一张床会有这些状态:空置、预订、入住、维修、临时隔离。状态是会被流转的,而且部分流转必须由前一个操作触发。
正常流程是这样的:家属来参观咨询,系统里先建一条咨询记录;老人确定入住,做健康评估和护理等级评定;评估通过后,系统根据性别、护理等级、楼层偏好分配床位,床位从"空置"变成"预订";老人正式办理入住,床位变成"入住";老人出院/退住,结清费用后床位回到"空置";如果床垫、护栏损坏,床位要变成"维修",修好再转回"空置"。
这套状态机落到代码里,我的建议是不用复杂的状态机框架,直接在service层写清楚状态流转合法性判断,比如"入住状态的老人在未退住前,不允许把床位转成维修"。逻辑直观,新人接手也看得懂。多花一分钟设计状态,后面省掉大量"数据对不上"的排查时间。
2.3 护理任务闭环是系统的灵魂
护理管理模块决定了这套系统是不是真的能落地。很多系统把护理做成一个"记录表单",护理员做完勾一下就算完事,这其实是不够的。真正的闭环是:
- 护理主任为每位老人制定护理计划,比如每天两次血压测量、三餐后服药提醒、定时翻身、康复训练;
- 系统按计划每天自动生成护理任务,派发到对应护理员的待办池;
- 护理员领单、执行、上报完成,遇到异常情况(老人拒绝服药、体温偏高)直接标记异常;
- 护士长看到异常任务,进行复测和处理,处理结果留痕。
任务表的核心设计我摘出来,你看看就明白为什么能闭环:
CREATE TABLE care_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_id BIGINT NOT NULL, plan_id BIGINT COMMENT '护理计划ID', task_date DATE NOT NULL, task_type VARCHAR(32) COMMENT '翻身/喂药/体征测量/洗澡/康复训练', assignee_id BIGINT COMMENT '护理员ID', status TINYINT COMMENT '0待领取 1已领取 2执行中 3已完成 4异常 5已取消', start_time DATETIME, finish_time DATETIME, abnormal_remark VARCHAR(500), remark VARCHAR(500) );在做护理任务分页列表时,一定要按status和时间段建好索引,否则机构住了两三百位老人之后,每天产生的任务量加查询会明显拖慢。还有一个实操经验:任务生成用定时任务批量跑,每天凌晨生成当天计划,不要等护理员打卡后再生成,否则早班护工根本来不及看。
2.4 费用结算与账务完整性
养老公寓收费和普通酒店不一样,不是"住一晚收一晚",而是按月计费,包含床位费、护理费、餐费、水电费、药品费等多个科目。系统需要支持月度账单自动生成,退住时按天折算退款,家属可以查到每一项明细。
费用表设计有两个方向:一张主账单表加一张明细表,主表管汇总金额和支付状态,子表管科目。我强烈建议不要只做一张"费用流水表",因为一个账单多次部分支付、冲正、退款,单表会乱。
CREATE TABLE charge_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_id BIGINT NOT NULL, bill_month VARCHAR(7) COMMENT '账单月份', total_amount DECIMAL(12,2), discount_amount DECIMAL(12,2), payable_amount DECIMAL(12,2), paid_amount DECIMAL(12,2), status TINYINT COMMENT '0未出账 1已出账 2部分支付 3已结清 4已退款', create_by BIGINT, create_time DATETIME ); CREATE TABLE charge_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_id BIGINT NOT NULL, item_type VARCHAR(32) COMMENT '床位费/护理费/餐费', amount DECIMAL(12,2), start_date DATE, end_date DATE, remark VARCHAR(200) );用DECIMAL而不是DOUBLE存储金额,这个属于财务常识了,但每做一个新项目都会有人踩。DOUBLE有精度问题,算完对不上账很麻烦。费用计算建议统一放到后端service层做,前端只做展示,避免不同浏览器算出来的结果不一致。
3. 关键实现:项目搭建与核心代码解析
3.1 项目初始化与工程结构
拿到这套源码,先不要急着启动,先看目录结构。后端是标准的Maven单模块工程,但包结构分得很清楚,我个人建议你保持这个分层,别把代码全塞在controller里:
health-elder-system/ src/main/java/com/example/elder/ common/ -- 统一返回结果、全局异常、工具类 config/ -- 跨域、拦截器、WebMvc配置 controller/ -- 接口层 service/ -- 业务层 mapper/ -- MyBatis Mapper接口 entity/ -- 数据库实体 dto/ -- 入参/出参对象 src/main/resources/ mapper/ -- MyBatis XML application.yml frontend/ -- Vue工程为什么业务层不能省略?养老公寓的业务规则分散在多个环节,如果在controller里直接调用mapper,退住的时候要先改床位状态、再结算费用、再更新档案状态,事务边界根本控制不住。有service层,事务注解才能覆盖一整条业务链,后续也好加单元测试。
3.2 数据库连接与核心配置
启动之前,重点看application.yml。这里有几个配置项简直是踩坑重灾区,我直接给一份能稳定跑生产环境的配置:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/elder_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 servlet: multipart: max-file-size: 20MB max-request-size: 50MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.elder.entity configuration: map-underscore-to-camel-case: true jwt: secret: your-secret-key-change-me expire-hours: 24- serverTimezone=Asia/Shanghai必须加上,否则Java 8的LocalDateTime和MySQL的DATETIME会差8个小时,老人体征记录时间全错位,排查起来极其隐蔽。
- allowPublicKeyRetrieval=true是MySQL 8.0连接时常见的报错来源,连不上数据库先想到这个参数。
- map-underscore-to-camel-case: true是MyBatis的救命配置,开启后数据库的create_time才能自动映射成实体里的createTime,否则查出来全是null。
连接池我建议用HikariCP,SpringBoot默认就是它,性能和稳定性都很好。如果是给机构内部用,Druid的监控页面有一定价值,但会增加配置复杂度,看团队习惯。
3.3 Mapper层的高频查询与动态SQL写法
老人的列表查询是这个系统里最典型的需求:按姓名模糊搜、按档案编号搜、按护理等级筛选、按入住状态筛选,还得分页。用MyBatis的动态SQL,一个标签就能解决"查全部还是查某项"的拼接问题,代码可读性比在Java里拼SQL强太多。
<select id="selectElderPage" resultType="com.example.elder.entity.Elder"> SELECT id, elder_no, name, gender, birthday, phone, health_level, status, create_time FROM elder <where> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR elder_no LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="healthLevel != null"> AND health_level = #{healthLevel} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY id DESC </select>注意LIKE别写成 '%${keyword}%',那会直接引入SQL注入风险,用CONCAT拼参数就是安全写法。排序字段尽量用id或create_time,用索引字段排序能避免大数据量下内存排序。
健康数据批量插入也值得单独提。手环设备一旦接入,每五分钟就会上报一批数据,逐条insert基本不可用,批量插入必须安排上:
<insert id="batchInsertHealthRecord"> INSERT INTO health_record (elder_id, record_date, systolic, diastolic, heart_rate, blood_sugar, temperature, record_time) VALUES <foreach collection="list" item="item" separator=","> (#{item.elderId}, #{item.recordDate}, #{item.systolic}, #{item.diastolic}, #{item.heartRate}, #{item.bloodSugar}, #{item.temperature}, NOW()) </foreach> </insert>实测下来,一次插200条比循环200次单条插入快一个数量级。注意数据库的max_allowed_packet要调大一点,默认值太小可能导致批量插入报错。
3.4 Vue前端实现与接口联调要点
前端部分,先把axios封装搞好。我见过太多项目没做统一封装,每个页面各写各的请求,后端一改错误码格式,前端就要改几十个文件。基础封装长这样:
import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { config.headers['Authorization'] = 'Bearer ' + (localStorage.getItem('token') || '') return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { alert(res.msg) return Promise.reject(new Error(res.msg)) } return res.data }, error => { if (error.response && error.response.status === 401) { location.href = '/login' } return Promise.reject(error) } ) export default service统一处理token、统一处理业务错误码、遇到401自动跳登录页。这套逻辑几乎是中后台系统标配,新建页面时直接import就好。
老人列表页是最常见的页面形态,用组件库的表格加分页组件。这里有个细节:数据库里存的是状态码数字,页面上要显示成"在住/已退住"这种中文,别在模板里堆一堆三元表达式,定义一个映射对象会干净很多。
<template> <div> <el-form inline> <el-input v-model="query.keyword" placeholder="姓名/档案编号" clearable /> <el-select v-model="query.healthLevel" placeholder="护理等级" clearable> <el-option label="自理" :value="1" /> <el-option label="半自理" :value="2" /> <el-option label="全护理" :value="3" /> <el-option label="特护" :value="4" /> </el-select> <el-button type="primary" @click="loadData">查询</el-button> </el-form> <el-table :data="list" border stripe> <el-table-column prop="name" label="姓名" /> <el-table-column label="护理等级"> <template #default="{ row }">{{ levelMap[row.healthLevel] }}</template> </el-table-column> <el-table-column label="状态"> <template #default="{ row }">{{ statusMap[row.status] }}</template> </el-table-column> <el-table-column label="操作"> <template #default="{ row }"> <el-button size="small" type="primary" @click="edit(row)">编辑</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" layout="total, prev, pager, next" @current-change="loadData" /> </div> </template> <script setup> import { reactive, ref, onMounted } from 'vue' import { getElderPage } from '@/api/elder' const levelMap = { 1: '自理', 2: '半自理', 3: '全护理', 4: '特护' } const statusMap = { 1: '待入住', 2: '在住', 3: '已退住' } const query = reactive({ page: 1, size: 10, keyword: '', healthLevel: null }) const list = ref([]) const total = ref(0) const loadData = async () => { const data = await getElderPage(query) list.value = data.records total.value = data.total } onMounted(loadData) </script>联调时最容易出问题的就是时间字段。后端返回LocalDateTime,前端拿到的是"2024-08-15T10:30:00"这种带T的字符串,直接展示很难看。前端用dayjs格式化,或者后端在DTO里加JsonFormat注解,两种方案选一种,统一标准就行。我建议在前后端接口文档里直接约定好,省得每个字段都加注解。
3.5 JWT鉴权与权限控制
系统里有院长、护士长、护理员、财务、家属多种角色,权限控制绕不开。这个项目采用JWT方案:登录成功生成token,前端保存,每次请求带上Authorization头,后端用拦截器统一校验。
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } try { // 解析并校验token,将用户信息放入ThreadLocal或RequestAttribute Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { response.setStatus(401); return false; } } }对于养老公寓这种规模,JWT加拦截器就够用了。不需要把Spring Security全家桶引进来,配置复杂、学习成本高,项目周期紧的话得不偿失。如果后面要对接第三方统一认证,再考虑升级。
4. 实际操作中的常见问题与排查技巧实录
4.1 数据库连接和编码问题速查
我把部署阶段最常见的数据库问题整理成了一张表,照着排查基本能解决80%的启动失败:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 连接报Public Key Retrieval is not allowed | MySQL 8.0加密规则变化 | jdbcUrl加allowPublicKeyRetrieval=true |
| 数据库中文乱码 | 建库没用utf8mb4 | 建库用utf8mb4,连接串加characterEncoding=utf8 |
| 时间差8小时 | 时区未指定 | 连接串外加serverTimezone=Asia/Shanghai |
| 中文查询不到结果 | 排序规则不是utf8mb4_unicode_ci | 表和字段统一utf8mb4_unicode_ci |
| 连接数耗尽 | 连接池太小或连接泄漏 | 调大maximum-pool-size,排查未关闭的连接 |
建库时直接这样写,省得后面改:
CREATE DATABASE elder_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;4.2 MyBatis高频报错与SQL性能问题
开发阶段最常见的报错就是Invalid bound statement (not found)。出现这个先查两处:第一,Mapper接口的namespace是否和XML文件里的namespace一致;第二,application.yml里的mapper-locations路径是不是把XML扫进去了。还有一个容易被忽略的点:XML文件的SQL写错了,但项目启动时不报错,运行时才爆,所以写完SQL一定要先跑一次测试方法,别等到前端联调才发现。
老人列表查询要特别注意N+1问题。比如查询一批老人之后,页面又要显示每位老人的最近一条健康记录,如果循环里每条数据单独查一次,两三百位老人就会产生几百条SQL。解决办法是先批量查询最近记录,再用Map按elderId分组,在Java内存里做关联,SQL数量从N+1降到2。
4.3 跨域与前端接口联调问题
前后端分离开发时,跨域问题几乎必现。前端端口是5173,后端是8080,直接请求会被浏览器拦截。我的习惯是开发环境用Vite的proxy代理,生产环境用Nginx反向代理,都不需要后端开CORS。因为开CORS会有安全面扩大,而且一旦前后端域名调整,又要改后端。
// vite.config.js export default { server: { proxy: { '/api': { target: 'http://127.0.0.1:8080', changeOrigin: true } } } }生产环境Nginx配置后面会写。要注意的是,如果接口请求方式用了DELETE或PUT,有些旧版Nginx默认配置会拦截,需要显式放行对应method,或者统一改成POST。
4.4 部署打包的几个经典坑
前端用history路由模式打包后,刷新页面会出现404。因为浏览器请求的是真实路径,但服务器只有index.html,Nginx必须配置try_files回退。
server { listen 80; server_name your-domain.com; root /opt/elder/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意location /api/的proxy_pass如果写成http://127.0.0.1:8080,不带后面的/api/,请求会被转发到http://127.0.0.1:8080/api/...(因为原URI带了/api前缀,Nginx会加上),所以上面写法是标准的,既保留了前缀又能正确转发。
另一个坑是jar包运行后中文乱码,启动命令加上-Dfile.encoding=utf-8,或者确保服务器locale是UTF-8。数据库备份也要提前安排,crontab每天凌晨备份一次,不然出问题连恢复的余地都没有。
5. 部署上线与后续扩展
5.1 一套能落地的生产部署方案
小规模机构可以不用上Kubernetes,直接用Docker Compose编排三个容器:MySQL、后端、前端。下面这份配置是完整可跑的,数据目录做了持久化,重启不会丢数据:
version: '3' services: mysql: image: mysql:8.0 container_name: elder-mysql environment: MYSQL_ROOT_PASSWORD: changeit MYSQL_DATABASE: elder_db ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci backend: image: elder-backend:latest container_name: elder-backend ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/elder_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai depends_on: - mysql frontend: image: elder-frontend:latest container_name: elder-frontend ports: - "80:80" depends_on: - backend volumes: mysql-data:生产环境要注意:数据库密码不要在Compose文件里写死,建议用环境变量或Docker secret。后端镜像构建时,记得把日志目录挂载出来,方便排查问题。前端镜像用Nginx作为基础镜像,把dist目录拷进去,再带上上面的Nginx配置。
5.2 系统后续扩展的三个现实方向
第一,家属端小程序。老人家属最关心的是老人今天吃了什么、血压正不正常、最近账单花了多少钱。小程序端不需要做全功能,只做档案查看、健康日报、费用查询、留言沟通,就能显著提升机构服务口碑。后端接口按现有模块扩展即可,重要的是把健康报告汇总接口做好。
第二,物联网设备接入。血压计、血糖仪、智能手环通过蓝牙或网关把数据传到后端,设备数据的格式千奇百怪,建议在健康管理模块前面加一层设备适配层,统一转换成health_record的记录格式,不要在每个设备厂家的接口里写业务。
第三,数据大屏。养老机构的院长办公室通常要放一块屏,实时展示入住率、护理任务完成率、异常事件数量、当月营收。这些数据的聚合SQL在报表中心里已经有了,前端可以用开源的大屏组件搭建,接口复用现有报表模块。
最后说点实际的个人体会
做完这套系统,我最大的感触是:养老公寓系统真正考验人的不是SpringBoot、Vue或者MySQL的技术深度,而是你有没有把业务想透。床位状态流转、护理任务闭环、费用按月折算,这些环节在需求文档里只是一句话,落地时全是细节。
如果你准备拿这套源码去给机构做二次开发或自己学习,我建议先花时间把数据库脚本完整看一遍,搞懂每张表之间的关系,再动代码。等业务模型在心里有了地图,代码跑起来就是水到渠成的事。开发过程中,多看日志、多用分页查询、多检查时区和编码,比研究炫技写法有用得多。
真正上线之后,你会发现有太多计划外的事情要处理:打印体检报告、生成出院小结、给家属群发通知、统计护理员月度绩效。这套系统的价值恰恰在于它把核心的数据链路和状态流转搭扎实了,后续扩展功能时,你会发现怎么加都不乱。这就是一个架构合理的企业级系统该有的样子。