☰
企业级夕阳红公寓管理系统:从数据库设计到部署实战笔记
2026/10/21 22:27:58 网站建设 项目流程

做这类管理系统,最常见的误区是拿到需求就开始建表,把表建完了才发现业务流程根本走不通。这里要聊的“企业级夕阳红公寓管理系统”,本质上是一套给养老机构用的内部管理后台,核心就管三件事:人、床、钱。人是指老人、护理员和家属,床是指房间床位,钱是指床位费、护理费、餐费这些日常收支。技术选型上用了SpringBoot + Vue + MyBatis + MySQL这套非常主流的组合,后端SpringBoot处理接口,前端Vue做页面,MyBatis负责数据库访问,MySQL做持久化存储,整体结构清晰,改造空间也大。

这个项目我前后跟了完整的一轮,从需求梳理、数据库设计、前后端联调,到部署上线、现场解决各种数据问题,都踩过不少坑。所以这篇文章不是简单介绍功能,而是想把系统背后那些真实的业务逻辑、表结构设计思路、并发处理细节、联调时容易翻车的地方都摊开说清楚。不管你是准备拿这套源码做毕业设计参考,还是想在公司里落地一套类似的中后台系统,都可以对照这份实现笔记少走很多弯路。

1. 项目概述:先搞清楚我们到底在解决什么问题

1.1 养老公寓管理的真实痛点

很多小型养老公寓的信息化程度并不高,接手这个项目时,对方还在用Excel表格加纸质台账管老人信息。各楼层护理员交班靠手写记录,月底财务对餐费、护理费要翻一大摞单据,家属打电话问老人近况,前台得跑到档案柜里翻纸质资料。这类场景看着不起眼,真正处理起来非常耗时。

痛点主要集中在几个地方:一是老人档案信息分散,入院登记表、健康记录、用药清单、家属联系方式各记各的;二是床位资源靠人工记忆,哪些床位空着、哪些老人要换房,没有统一状态;三是费用计算规则不统一,护理等级不同价格不同,餐费按天计算还涉及请假外出退餐;四是领导想看经营数据时,Excel汇总耗时且容易出错。

这套系统的目标就是把这些散落的流程收拢到一个后台里,让前台接待、护理员、财务、管理员各角色都有自己的操作入口。老人档案、床位状态、健康记录、费用账单全都在一个数据库里统一管理,从入住登记到每月生成账单,再到退住结算,形成一条完整的数据链条。

1.2 为什么偏偏选这套技术栈

技术选型的时候并没有追求热门新技术,核心考虑的是稳定、好招人、好维护。后端用SpringBoot,因为它生态成熟,内置Tomcat,配置简单,一个jar包就能跑起来,非常适合这类业务密集型的中后台系统。MyBatis比JPA更容易控制SQL,因为养老公寓系统里大量涉及多表关联查询、动态条件筛选,比如按姓名、身份证、入住状态组合查询老人列表,这类需求用MyBatis动态SQL非常直观。

前端用Vue,是因为页面交互以表格、表单、弹窗为主,Vue的组件化开发让这些常见交互可以高度复用。实际项目里我们用的Vue 2 + Element UI,这套组合在中文社区资料极其丰富,遇到问题基本搜得到解决方案。当然,如果是全新项目也可以直接用Vue 3 + Element Plus,但底层思路差别不大。

MySQL作为存储层足够扛住这类系统的压力,养老公寓的并发并不高,同时在线操作人数可能就几十人,没必要上Oracle或PostgreSQL。真要扩容,后面把数据库层拆开做读写分离也有余地。整个架构里还引入了Redis做token缓存和部分字典缓存,但核心业务数据全部落在MySQL里,保证数据可靠。

2. 功能模块拆解:养老服务机构的日常运营链路

2.1 老人档案与入住管理:从登记到换房再到退住

老人管理是整个系统的地基。一个老人从入院开始,就要维护一套完整档案:基本信息、身份证号、紧急联系人、家属关系、既往病史、过敏药物、护理等级。身份证号必须做唯一约束,避免同一个老人被重复登记。之前线上就遇到过一个情况:家属第一次来登记时填的信息不全,第二天又带着老人来补登记,结果生成了两条档案,后来靠身份证号做去重才清理干净。

入住流程要走一个明确的状态流转:待入住、在住、已退住。老人登记档案后,分配床位时才把状态改为在住;办理退住后状态改为已退住,床位同时释放。换房操作比较特殊,要把原床位释放、新床位占用,同时保留一条换房记录,方便后续追溯。这种状态管理如果只靠开发人员脑子记,后期很容易乱,所以我们把状态字段枚举固定死,后端校验,前端下拉框控制,不允许直接手工改数据库。

2.2 护理排班与健康记录:信息化的核心价值

护理排班是照顾公寓日常运转的重要模块。以前排班用纸质表,护理员换班后经常忘记同步,导致老人按时吃药、洗澡这些事项没人盯。系统里做成排班表,按日期、楼层、班次三个维度维护,护理员登录后能看到自己当天负责的老人列表和待办事项。

健康记录则负责日常体温、血压、血糖等指标录入,以及用药提醒。护理员每天给老人测完指标后直接在手机或平板上录入,系统根据预设阈值判断是否异常。比如收缩压超过160就自动打标,提醒护士复核。这些数据积累下来之后,还能生成周期健康报表,给家属展示老人的变化趋势。

实际开发中要注意,健康记录的录入频率很高,一天可能产生几十条数据,查询时一定要按老人ID和日期建立联合索引。另外用药提醒功能不能只靠系统推送,因为并不是所有护理员都随时盯着电脑,我们额外做了一个简单的待办清单,登录后第一眼就能看到今天哪些老人需要特殊照顾。

2.3 餐饮与物资:看似简单却容易出乱的地方

餐饮模块最容易被低估,但通常会成为上线后反馈最多的功能。养老公寓的餐费一般按月预收,老人请假外出或临时不在公寓吃饭,需要做退餐登记,月底结算时扣减相应餐费。如果靠人工记录,漏记一笔就是一笔糊涂账。

系统里设计了每周菜谱、老人报餐状态、每日就餐统计这三个环节。前台可以在系统里为某个老人设置“外出退餐”,后台自动记录未就餐天数;食堂可以根据当天统计人数决定备餐量。月底生成账单时,餐费自动扣除退餐金额,财务只要核对一遍即可。

物资库存管理相对简单,主要是米面油、日用品这类消耗品的入库、出库、存量预警。库存低于阈值时自动提醒采购。这里有一个小教训:库存字段一定要用decimal而不是float,否则累计多次入库出库后会出现小数点精度误差,对账时候非常头疼。

2.4 收费与结算:财务对账的最后一公里

收费模块直接关系到收入,也是最需要谨慎设计的地方。系统支持三种费用类型:一次性费用(如押金、入住费)、周期费用(如床位费、护理费、餐费)、临时费用(如加急洗衣费)。月度账单按月生成,后台定时任务每个月1号自动根据老人的在住天数和收费标准生成账单。

收费记录表里保存每一笔缴费流水,支持预缴费模式。老人入住时一般会交一笔押金,用于抵扣最后一个月费用或临时杂费。退住结算时,系统自动计算应退金额:押金 + 预缴余额 - 未缴费用。如果出现负数,表示还需要补缴。这个计算逻辑写完后一定要反复验证,特别是跨月入住、中途换房、护理等级调整这三类情况叠加时,容易出错。

财务人员最在意的对账功能,我们把账单流水和缴费流水单独分表,支持按月份、支付方式、状态筛选,导出Excel对账单。实际使用下来,导出功能是财务高频使用功能,开发时不要忽略。

3. 数据库设计:一张张表把业务逻辑钉死

3.1 核心表结构规划

数据库设计的好坏,基本决定了这个系统能撑到多大复杂度。我建议核心表这样划分:

表名作用关键字段
elder_info老人档案id, name, id_card, phone, guardian_name, health_status, nursing_level
sys_user系统用户id, username, password, real_name, role_id, status
sys_role角色id, role_name, role_key
bed_info床位信息id, room_no, bed_no, floor_no, status
check_in入住登记id, elder_id, bed_id, check_in_date, check_out_date, status
health_record健康记录id, elder_id, record_date, temperature, blood_pressure, remark, abnormal_flag
nurse_schedule护理排班id, user_id, elder_id, schedule_date, shift_type
menu_plan菜谱id, meal_date, meal_type, dish_names
fee_bill费用账单id, elder_id, bill_month, total_amount, status
fee_item账单明细id, bill_id, fee_type, amount, start_date, end_date, remark
payment_record缴费流水id, elder_id, bill_id, pay_amount, pay_method, pay_time, operator
refund_record退费记录id, elder_id, refund_amount, reason, operator, create_time

这几张表基本覆盖了养老公寓的核心业务。elder_info是主表,check_in负责处理老人和床位的关系,fee_bill和fee_item是一对多关系,每张账单包含多个费用明细项。payment_record负责记录每笔钱的实际到账情况,让财务能区分“应收”和“实收”。

3.2 关键关系、索引与注意事项

表之间存在几组重要关系。最核心的一张关联表是check_in,因为老人和床位之间不是简单的一对一关系,同一个老人在不同时间可能住过不同床位,所以不能直接在elder_info里加bed_id字段,必须通过入住记录来关联。

索引设计上,身份证号elder_info.id_card要建唯一索引;health_record要建(elder_id, record_date)联合索引,因为查询某位老人某段时间的健康记录是高频操作;fee_bill要建(elder_id, bill_month)联合索引,方便按月查账单;payment_record的pay_time字段要建索引,财务导出流水时经常按时间范围筛选。

字段类型上有几个细节务必注意:金额一律用decimal(10,2),不要用float;日期时间用datetime,状态用tinyint并且加注释;文本字段该用varchar就varchar,不要图省事全用text,否则索引建不上。另外所有业务表都要保留create_time和update_time,排查数据问题时会帮上大忙。

3.3 一条多表查询SQL的实战示范

项目里最常查的统计类需求,就是按月汇总每个老人的总费用。这里给出一个经过优化的查询示例:

SELECT e.id AS elder_id, e.name AS elder_name, e.id_card, f.bill_month, SUM(fi.amount) AS total_amount, f.status FROM elder_info e LEFT JOIN fee_bill f ON e.id = f.elder_id LEFT JOIN fee_item fi ON f.id = fi.bill_id WHERE f.bill_month = #{billMonth} AND e.deleted = 0 GROUP BY e.id, f.id ORDER BY total_amount DESC

为什么用LEFT JOIN而不是INNER JOIN?因为有些老人当月没有费用明细,但财务希望仍然能看到基本信息,所以老人表作为主表左连接费用数据。分组字段一定要包含e.id和f.id,否则不同账单月份的数据会串行聚合。这种SQL在MyBatis里写的时候,直接用@Select注解或XML文件都可以,核心是要控制好关联表的范围,避免全表扫描。

4. 后端实战:SpringBoot与MyBatis的配合

4.1 分层结构与统一响应体

后端结构严格遵循经典分层:Controller接收请求、Service处理业务、Mapper操作数据库。另外单独划分了entity(数据库实体)、dto(入参对象)、vo(出参对象)。很多初学者喜欢直接用Map接收参数,项目不大时还行,一旦字段多了,维护成本直线上升。我们规定所有接口入参必须有dto,出参必须有vo,虽然写起来麻烦一点,但后续改需求时能清楚看到影响范围。

统一响应体是这个项目的标配:

public class Result<T> { private Integer code; private String message; private T data; // getter/setter }

所有接口都返回这个结构,前端axios里统一判断code。同时用@RestControllerAdvice做全局异常处理,业务异常抛自定义RuntimeException,框架异常统一拦截。这样前端拿到的永远是结构一致的响应,而不是乱七八糟的堆栈信息。

4.2 用户权限与接口安全

权限设计分角色处理:管理员、前台接待、护理员、财务。管理员拥有全部权限,前台能操作老人入住和退住,护理员只能看老人信息和录健康记录,财务只能查看账单和缴费流水。后端使用Spring Security + JWT 做认证授权,登录成功后签发token,后续请求在请求头中携带token,拦截器校验token并解析出用户角色。

角色校验我推荐用注解方式,比如自定义@RequirePermission("elder:add"),在拦截器里统一检查。比在每个方法里手工判断更干净。密码存储使用BCrypt加密,不允许明文落库,即使数据库泄露也不能直接看到密码。项目里还要注意接口防重复提交,比如提交入住单时如果网络抖动导致前端重复请求,后端要抬起幂等校验,保证不会创建两条入住记录。

4.3 MyBatis动态SQL和多表关联

MyBatis在这个项目里的优势就是动态SQL。老人列表查询条件非常多,姓名模糊搜索、身份证精确搜索、入住状态筛选、护理等级筛选,组合方式有十几种。用XML里的<where>标签配合<if>标签,可以非常优雅地处理:

<select id="listElderByCondition" resultType="com.demo.vo.ElderVO"> SELECT e.*, b.room_no, b.bed_no FROM elder_info e LEFT JOIN check_in ci ON e.id = ci.elder_id AND ci.status = '1' LEFT JOIN bed_info b ON ci.bed_id = b.id <where> e.deleted = 0 <if test="name != null and name != ''"> AND e.name LIKE CONCAT('%', #{name}, '%') </if> <if test="idCard != null and idCard != ''"> AND e.id_card = #{idCard} </if> <if test="nursingLevel != null"> AND e.nursing_level = #{nursingLevel} </if> </where> </select>

这里有个小细节:LEFT JOIN check_in的时候,只关联status='1'的当前入住记录,这样不会把历史入住记录全部带出来。字符串拼接时要防止SQL注入,所以使用#{}而不是${}。所有模糊搜索都要包一层CONCAT函数,而不是直接拼字符串。

批量插入健康记录时使用foreach:

<insert id="batchInsertHealthRecord"> INSERT INTO health_record (elder_id, record_date, temperature, blood_pressure, remark) VALUES <foreach collection="list" item="item" separator=","> (#{item.elderId}, #{item.recordDate}, #{item.temperature}, #{item.bloodPressure}, #{item.remark}) </foreach> </insert>

批量插入可以极大减少数据库交互次数,尤其护理员每天录入多条健康数据时,一次提交十条数据比循环十条快得多。

4.4 事务与并发:别让床位被重复分配

养老公寓的床位资源是稀缺资源,并发分配时一定不能超卖。比如两个前台同时给两个老人办入住,刚好都选中了同一个空床位,如果不加控制,两个老人都会显示入住成功,但床位只有一张。

我们使用数据库行锁解决:分配床位时,先执行一条SELECT * FROM bed_info WHERE id = #{bedId} AND status = 0 FOR UPDATE,把这一行锁住,然后执行更新将状态改为1。在Spring事务中,这条FOR UPDATE锁会一直持有到事务提交。这样第二个事务只能等待第一个事务完成,再查的时候床位状态已经变为占用,会提示“床位已被占用”。

事务方法上使用@Transactional(rollbackFor = Exception.class),注意回滚条件必须包含RuntimeException和普通Exception,否则事务可能不回滚。另外不要在一个事务里做太多远程调用,比如发送短信通知,否则事务时间过长会导致数据库连接被占满,影响整体性能。

5. 前端实战:Vue项目落地与前后端联调

5.1 页面划分与组件复用

前端按照业务模块划分页面,主要包括登录页、首页工作台、老人管理、床位管理、护理排班、健康记录、餐饮管理、费用管理、系统管理。首页工作台展示待办事项,比如今天需要提醒用药的老人数量、空床位数、待审核退住申请等。不同角色登录后看到的首页卡板不一样。

为了减少重复代码,把常见的搜索表单、分页表格、弹窗表单都抽成了公共组件。比如老人列表用el-table渲染,搜索区有姓名、身份证、护理等级筛选,分页器统一封装。只要在页面里传入对应的列配置和请求方法,就可以直接复用。实际开发中,组件化能节省的时间非常可观,特别是后期加字段时,只需要改配置项。

5.2 axios封装与权限路由

axios请求封装是前端的基建工程。我们封装了request.js,统一设置baseURL为/api,请求拦截器从localStorage取token放到Authorization头里,响应拦截器统一处理返回码。code不为200时统一弹错误提示,401时跳转到登录页,网络错误时提示“网络连接失败”。

权限路由通过Vue Router的beforeEach守卫实现。每次路由跳转前检查token和用户角色,没有token直接去登录页;有token但访问了无权限页面,提示403无权限。菜单根据后端返回的角色权限动态生成,方式是从后端获取菜单树,用v-for渲染侧边栏。这样不同角色登录后看到的菜单项不一样,财务不会看到护理排班按钮。

5.3 联调中的几个大坑

前后端联调阶段最容易出的问题,一是跨域,二是字段名上下划线不一致。跨域处理最简单的方式是在SpringBoot里配置CorsFilter,允许前端开发服务器的地址。也可以在前端脚手架配置proxy代理,把/api开头的请求转发到8080端口,生产环境用Nginx反代。

字段命名问题最典型:MySQL里习惯用下划线命名,比如elder_id,Java里习惯驼峰,比如elderId。MyBatis配置文件里开启map-underscore-to-camel-case属性后,查询结果会自动映射。但如果查询SQL里写了别名,有时候还是会映射不上,这时就要手动在resultMap里配清楚。

还有一个坑是日期字段。后端返回的LocalDateTime序列化后是一串数组,而不是标准字符串。必须在SpringBoot里配置Jackson的日期格式化,或者统一用String接收日期参数。前端展示日期时再用dayjs格式化,否则表格里出现一大串数字非常难看。

6. 部署上线:从本地跑起来到服务器能访问

6.1 环境准备与工程配置

本地开发需要准备的基础环境:JDK 1.8或11、Maven 3.6+、Node 14+、MySQL 5.7以上版本。前端使用npm install安装依赖,后端使用mvn clean install编译。需要注意的是Node版本过新可能导致部分依赖安装失败,建议使用LTS版本。

数据库初始化时,把建表SQL和初始数据(管理员账号、角色菜单等)一起导入。项目中的application.yml要区分多套环境,比如dev、prod。开发环境数据库地址指向本地,生产环境通过环境变量或者启动参数覆盖:

spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: ${DB_USER} password: ${DB_PASSWORD}

使用环境变量的方式非常关键,直接写死在代码里,换环境又要改配置。用变量注入后,生产环境只需在系统环境变量里配置好即可。

6.2 打包构建与Nginx部署

后端打包命令是mvn clean package -DskipTests,生成一个可执行的jar包,使用nohup java -jar app.jar --spring.profiles.active=prod启动。前端执行npm run build生成dist目录,将dist目录上传到服务器,Nginx配置如下:

server { listen 80; server_name your-domain.com; location / { root /var/www/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

前端因为是history路由模式,必须配置try_files,刷新页面时不会出现404。api接口反向代理到本地8080端口,这样前端请求/api/login就会自动转发到后端,不需要处理跨域。

6.3 上线后的运维问题速查

上线后最常遇到的问题就是内存不足和时区问题。如果服务器内存只有2G,启动Jar包时一定要设置JVM参数,比如-Xms512m -Xmx1024m,否则频繁Full GC导致接口响应极慢。MySQL连接串里的serverTimezone务必设置为Asia/Shanghai,否则日期时间差8小时。

另外建议开启SpringBoot的actuator,用/actuator/health做健康检查,配合定时任务检测服务状态。日志输出用logback按天滚动,保留最近30天即可。上线初期不要急着把所有权限全放开,先只建一个管理员账号,后续再慢慢创建其他角色。

7. 常见问题与排查技巧实录

7.1 慢查询与接口超时

项目上线后,某个接口在数据量变大后突然变慢了,比如从老人列表进入详情页面,要展示健康记录、费用账单、入住记录等多个数据,前端并行发了五六个请求,其中一个特别慢。

排查方式分三步。第一步看是不是只慢在某几条数据上,如果固定老人ID才慢,多半是数据问题而不是SQL问题。第二步用MySQL的EXPLAIN分析SQL执行计划,发现健康记录查询没有走索引,因为之前只用单字段elder_id做了索引,而查询条件里有日期范围,于是改成(elder_id, record_date)联合索引,速度立刻上来。第三步检查后端日志,看是不是有N+1查询问题,比如遍历老人列表时每行再查询一次费用,这种情况必须改为批量查询后用Map内存组装。

7.2 数据错乱与唯一键冲突

身份证号唯一约束是一个硬底线,但由于历史数据里已经存在重复登记记录,上线后第一次同步数据时就报了Duplicate Entry错误。解决办法是先写一个清洗脚本,把重复数据合并,保留最新一次入住记录对应的档案,其余标记为删除。之后再在代码里通过身份证号查询是否存在,存在则提示“该老人已登记,请通过档案编号查询”。

床位状态发生错乱的情况也遇到过。原因是用update语句直接修改床位的状态,而没有走事务和锁。比如退住操作释放床位时,误把0和1的状态写反,导致空床显示已占用。后来规定所有状态变更都必须通过Service方法操作,不允许直接在数据库里手工改数据,并在床位上增加了版本号字段,乐观锁防止并发更新覆盖。

7.3 备份、迁移与多环境配置

数据备份是养老系统最容易疏忽的一环。每天凌晨使用mysqldump进行全量备份,备份文件保留7天,转移到一个独立磁盘或对象存储里。命令参考:

mysqldump -u root -p --single-transaction --routines --triggers app_db > backup_$(date +%Y%m%d).sql

--single-transaction参数很重要,备份时不会锁表,不影响白天业务。恢复时用source导入即可。多环境配置上遵循一个原则:配置与代码分离。数据库账号密码、Redis密码、短信密钥等都不允许写在仓库里,统一走环境变量。这样即使代码包泄露也不会连累生产数据库。

7.4 本地开发那点烦心事

本地启动项目时最烦的是端口被占用。SpringBoot默认端口8080,如果被其他服务占用,启动直接报错。解决办法是在application.yml里改端口,或者用server.port=8081启动指定。前端跑起来后,请求后端接口如果总是报跨域错误,优先检查代理配置是否生效,而不是一上来就改后端CorsFilter。

热部署建议装spring-boot-devtools,改完代码自动重启,能省不少时间。但要注意,这个依赖在生产环境一定要排除掉,否则运行时会拖慢启动速度。前端调试时,最好mock一份假的登录接口,这样后端还没好时前端可以先开发页面,联调时再切换到真实接口,能极大缩短等待时间。

写在后面的一点个人体会

这套系统真正做完一轮,我的体会是:技术难点其实没那么多,最花时间的是把公寓的业务规则想清楚,再把规则翻译成表结构和状态机。比如护理等级调整后费用怎么变,跨月入住按天折算又怎么算,这种边角需求反而最容易出bug。建议做之前先花一个下午把业务流程图画出来,和实际运营人员过一遍,再动手建表,会让你少走非常多弯路。如果让我再做一次,我还是会选SpringBoot + Vue + MyBatis这套组合,不是因为它最炫,而是它足够稳,出现问题你能很快定位、修复、上线。

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

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

立即咨询