ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot+Vue疗养院管理系统实战:从表结构设计到权限控制

Spring Boot+Vue疗养院管理系统实战:从表结构设计到权限控制

年初接手了一套疗养院的业务系统改造,对方机构还在用Excel表格维护老人档案、床位和收费记录,每个月月底核算账单要折腾好几天。后来我基于Spring Boot做了一套管理系统,后端用Spring Boot,前端配Vue,数据库选MySQL,整套源码跑通之后直接扔到对方服务器上,到现在用了三个多月,稳定性和效率都很能打。这篇文章把整个项目从功能拆分、表结构设计到运行踩坑的过程完整记录下来,适合准备做管理系统开发、或者正在折腾此类系统的同学直接参考。

先说明一点:这不是一个炫技型项目,没有微服务、没有分布式、没有复杂的算法设计。它就是一套典型的前后端分离信息管理系统,价值在于把业务梳理清楚、把代码写规矩、把坑提前填好。你拿到手之后,改改配置就能跑,跑起来之后照着业务继续往深了做也不费劲。

1. 为什么要单独搞一套疗养院系统,而不是直接套用通用后勤系统

1.1 疗养院场景和普通酒店、医院管理差在哪

不少第一次接触这个领域的人会有个误区:疗养院不就是"酒店住宿 + 简单医疗"吗?拿一套通用的公寓管理系统改一改不就行了?实际上手之后你会发现,完全不是一回事。

疗养院的日常管理核心是"人",而且是很特殊的一批人。他们不是短期住客,也不是普通门诊病人,而是长期驻留、需要持续观察健康状况、每天产生各类康养服务消耗的群体。这意味着系统的业务主线至少要覆盖三条:入住周期管理、健康照护过程管理、按日累计的费用核算。这三条线彼此交织,比如一位老人每次身体指标的记录都会影响护理等级,护理等级一变,每天的费用也跟着变。这种动态联动,通用系统很难处理。

普通公寓系统只关心"房间有没有人住、租金交没交";医院HIS系统关心的是"门诊挂号、医嘱、划价收费"。疗养院系统夹在中间,既要有房间和床位管理,又要有健康档案和照护记录,还要把长期住宿产生的费用按天算清楚、按月出账单。这种业务定位本身就决定了它需要一套独立的建模思路。

1.2 我按哪几条主线来拆需求

这套系统上手之前,我先和机构的一线工作人员聊了两次,把日常流程走了一遍,最后确定了四个核心模块组:接待入住与退住、健康档案与照护执行、护理排班与任务分配、费用计费与报表核销。外围再补充系统管理模块,用来管账号、角色和权限。

这四个模块组不是拍脑袋分的,它们分别对应机构里真实存在的岗位角色。前台接待管入住和访客,护士站管档案和照护计划,护工组管排班和任务执行,财务月底要跑账单。每个角色打开系统看到的页面、能操作的功能都不同,这就是后面权限设计的基本出发点。

1.3 功能边界要提前定死,否则开发会失控

做这类系统最怕需求蔓延。比如有人提出要加"在线问诊""家属视频探视""智能硬件心率接入",听起来很美好,但第一版做进去必然拖垮进度。我当时和对方明确约定:第一版只做人工信息录入与流程管理,不做硬件对接,不做实时消息推送,不做复杂图表分析。所有扩展功能留到二期的接口空位。

这个边界现在已经写进代码里了。比如健康指标记录表预留了一个source_type字段,将来如果接智能手环,数据来源标记成设备即可;费用表明细里预留了exception_flag,将来处理减免、折扣也不会改表结构。前期边界越清晰,后期改起来越舒服。

2. 技术栈选型:为什么这套组合最省心,模块和表是怎么设计的

2.1 三个核心组件选择的理由

后端用Spring Boot,这个没什么悬念,企业级管理系统的标配,生态成熟、招人容易、出了问题网上一搜一大把解决方案。我选的是Spring Boot 2.7.x版本,配合JDK 8,稳定优先。安全认证用Spring Security加JWT;持久层用MyBatis-Plus,因为单表CRUD它能自动生成大部分方法,我只需要手写那些复杂的统计SQL。

前端用Vue 2.6加Element UI。可能有朋友会问,现在Vue 3都用起来了,为什么还守着Vue 2?原因很实在:Element UI的表格、表单、弹窗组件非常成熟,网上现成的管理后台模板也多,改起来快。如果从零搭Vue 3加Element Plus,组件API变了、表单校验方式也变了,开发量会明显增加。对于"快速交付、稳定好用"为目标的系统,Vue 2完全够。这不是技术落后,是工程选型的现实考量。

数据库用MySQL 5.7,单机部署,日活几十人,数据量撑死几十万条,完全不担心性能。如果后面真有高并发诉求,加一层Redis缓存就够了,但现在完全没必要。

2.2 核心表设计的几个关键点

表结构是这个系统的灵魂,字段名可以改,但表关系错了后面全是坑。我总共设计了20多张表,按业务域可以分成四组:

  • 人员与组织:sys_user(登录账号)、sys_role(角色)、sys_user_role(账号角色关联)、resident(疗养人员档案)
  • 床位与空间:building(楼栋)、room(房间)、bed(床位)
  • 健康照护:health_record(健康指标记录)、nursing_plan(照护计划)、nursing_task(照护任务执行)、medication_record(用药记录)
  • 运营计费:visit_record(访客记录)、shift_record(排班记录)、charge_item(收费项目)、resident_account(在住账户)、charge_detail(每日计费明细)

有几个设计细节值得专门说。

resident表把人员信息和入住状态分开设计。人员基本表里存姓名、证件号、家属联系方式、既往病史;入住状态则用单独字段stay_status控制,值分别是0待入住、1在住、2已退住、3转院。这样设计的直接好处是,退住之后人员档案不会跟着消失,财务核对历史数据随时能查到"这个人当时住哪床、哪位护工负责"。

床位和房间不合成一张表。为什么?因为疗养院里一个房间常见两张床甚至三张床,但两张床的朝向、床位费、有无护栏这些细节可能不同。床是独立收费单位,房间只是空间归属。凡是涉及收费的地方,我全部落在bed这一层,从根上避免了"算一间房还是算一张床"的混乱。

费用相关的表设计我前后改了三版,最终采用"收费项目配置表 + 每日流水明细"的模式。charge_item定义每一类收费项的日单价,比如"一级护理60元/天""餐费30元/天""单人间床位费80元/天";系统每天晚上跑批,按疗养人员当前状态和所在床位自动生成当天的charge_detail记录。月底要出账单,直接按人汇总这个表的记录即可,每一条都有日期、项目、金额、生成来源,没有一笔是拍脑袋算出来的。

2.3 后端项目结构参考

拿到源码之后,你会发现包结构基本如下,建议二次开发也保持这个习惯:

com.example.convalescent ├── common // 通用类:统一返回、异常处理、常量 ├── config // 配置:跨域、安全、MyBatis-Plus分页 ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto // 前端交互对象 ├── vo // 视图返回对象 ├── util // 工具类 └── security // JWT相关

entity里的类和数据库表一一对应,dto和vo存在的意义是不要直接把数据库实体暴露给前端。比如查询入住列表时,前端需要显示"房间号、床位号、护理等级、负责人",但不需要id_card和medical_history这些敏感字段,所以我单独定义了一个ResidentListItemVO,只返回必要字段。这个小习惯能少掉非常多"返回了不该返回的数据"的安全问题。

3. 把核心流程串起来:入住、照护、排班、费用这套链路怎么走

3.1 入住登记到底要做几步

入住登记不是简单地点一个"新增人员"。在系统里,前台操作员的完整流程是这样的:先录入疗养人员的基本信息,包括姓名、证件、紧急联系人、过往病史;再选择楼栋、楼层、房间、床位,床位要多条件筛选——只有显示"空闲"状态的才能选;然后根据评估结果设置护理等级,这个等级会直接影响照护计划和后续计费。

整个登记过程在界面上看起来是一个多步骤表单,但后端实际上分了四步事务操作:插入人员档案、占用床位、初始化在住账户、生成首日费用记录。这四步必须放在同一个@Transactional里,任何一步失败都要整体回滚,否则很可能出现"人登记成功了但床还是空闲的"这种脏数据。

这一步我在源码里写得很清楚,针对它做了完整的单元测试,因为它是整个系统中出错概率最高的入口。无风险的小技巧是:入住完成后立即在主页面刷新床位状态,让前台人员养成确认的习惯。

3.2 健康档案与照护计划的联动逻辑

疗养人员的健康管理是这个系统里"人情味"最重的模块。每天护士会录入体温、血压、血糖、精神状态这几个核心指标,数据进入health_record表。系统不会自动做诊断判断,但会在连续指标异常时给出页面级别的提示,方便护士关注。

照护计划表nursing_plan则记录每位在住人员的护理要求,比如"每2小时翻身一次""三餐后测血糖""每周二次康复训练"。这些计划项不会孤零零存在,它们会生成到对应护工的每日任务清单里,护工完成任务后逐项勾选。管理者在报表页面能看到"今天的护理任务完成率"是多少。

这个模块设计上有一个容易忽略的细节:所有健康记录都只做追加,不做修改。业务上不存在"把昨天的血压从130改成120"这种场景,如果录错了,正确做法是重新录入一条并将旧记录标记为作废。这样虽然会导致表数据量偏大,但每一份健康数据都可追溯,真出现纠纷时,这是一份说得清楚的证据链。

3.3 排班模块:比想象中更简单,也比想象中更实用

排班功能很多管理系统里都有,但疗养院的排班有两个特点:一是班次稳定,基本就是白班、夜班、休息三态循环;二是排班结果直接关联到护工的任务分配范围。我没有做复杂的自动排班算法,就做了一个简单实用的"按人设置周模板,一键生成下周排班表"的功能。

排班生成后,系统会在每天凌晨根据值班人员名单,把当前在住疗养人员按房间楼栋分配到对应的责任护工名下。这样一来,护工登录系统看到的任务列表天然就是"今天该我管的那几个人",不用自己满屏找。这个设计逻辑简单,但实际使用反馈非常好,因为一线人员最怕花时间找"到底谁负责谁"。

3.4 每日计费跑批和退住结算的算法解释

计费是财务最关心的部分,也是代码里注释最多的地方。系统里维护了一个定时任务,每天凌晨2点执行,逻辑分三步:

  1. 查出所有stay_status = 1的在住人员;
  2. 对每个人,根据当前床位所属的收费项、护理等级对应的护理费、固定餐费,各生成一条charge_detail;
  3. 累计到对应resident_account的当月应缴金额字段中。

退住结算的核心算法是按天确认边界。假设某位老人6月15日上午10点办理退住,系统中退住日期记录为6月15日,最后一天的费用仍然收取,因为床位当天无法再安排其他人入住。如果退住时间在上午8点前且机构有明确的半日规则,则通过配置项settle_type切换为半日计费。这种细节在需求讨论阶段就要聊明白,写进代码就是charge_detail里每条记录都带上精确的生成时间和业务日期。

月底对账时,财务只需要打印月度费用汇总报表,按人展示"上月结余、本月新增费用、已收款、应收余额"四个数字。再也不需要对着Excel手工拉公式了。

4. 权限设计:不同角色看到的东西不能一样,这一点必须较真

4.1 四种角色和对应的数据范围

这套系统的账号一共分四类:系统管理员、医生/护士、护工、前台/财务。如果再细分,还有只读性质的领导查看看板,但第一版没做,统一合进管理员角色。

角色不同,不仅操作按钮不一样,连数据范围都完全不同。这块我用了最简单的方案:sys_role表里有一个data_scope字段,取值为ALL(全部数据)、DEPARTMENT(本部门数据)、SELF(仅自己相关数据)。比如医生角色配DEPARTMENT,只能看到自己负责的疗养区;护工配SELF,任务列表只显示分配给自己的那部分。查询的时候,MyBatis-Plus的拦截器会根据当前登录用户的data_scope自动拼接SQL条件。

这么做比纯靠前端隐藏按钮靠谱得多。前端隐藏只是体验层面的保护,真正的数据隔离必须落在SQL层面。否则我只要绕过页面直接调接口,就能看到别人的健康档案,这对医疗数据来说是不可接受的安全事故。

4.2 JWT登录态:令牌怎么发、怎么验、怎么续期

登录接口的逻辑比较常规:核对用户名密码,校验通过后生成一个JWT令牌返回前端。这个令牌里我封了三个信息:userId、userName、roleKey。前端拿到后存到localStorage,每次请求在Authorization头里带上。

服务端的校验链路是:自定义的JwtAuthenticationFilter拦截所有/api/**请求,解析请求头里的令牌;如果令牌有效,就把用户信息放进SecurityContext;如果无效或过期,直接返回401。然后用Spring Security的@PreAuthorize("hasRole('ADMIN')")在Controller方法上做接口级权限控制,比如删除床位这个操作只允许管理员调用。

令牌有效期我设置的是8小时,实际用下来勉强够一天。系统里没有做自动续期,思路是:前端在每次请求收到401后,跳回登录页重新登录。对于内部管理系统来说这个体验可以接受。如果想更顺滑,可以在后端加一个刷新令牌接口,但为了控制复杂度,第一版我没加。

4.3 参数校验和防注入:服务器要当"不信任的边界"

所有前端传来的数据,后端一律默认不可信。实体类上加了JSR 303校验注解,比如姓名不能为空、手机号必须符合格式、金额必须大于零,Controller层通过@Validated统一触发校验。

SQL注入方面,MyBatis-Plus的QueryWrapper本身用#{}预编译,风险低。真正的隐患出在用字符串拼接SQL的统计报表里。我在代码审查时专门定了一条规矩:任何动态排序字段,必须走白名单校验。比如报表允许按"姓名、入住日期、费用"排序,前端传来的排序关键字如果不在白名单里,直接拒绝执行。这一条能挡住大多数人为构造的注入尝试。

5. 拿到源码之后的运行全流程:每一步都给你标清楚

5.1 环境版本对齐是第一道门槛

想"直接运行",最怕的就是版本不对齐。这套系统的后端用的是:

  • JDK 1.8
  • Maven 3.6+
  • Spring Boot 2.7.14
  • MySQL 5.7+

前端用的是:

  • Node.js 14.18+
  • npm 6.14+
  • Vue 2.6.14
  • Element UI 2.15+

如果你本机装的是JDK 17,直接跑Spring Boot 2.7大概率会报Unsupported class file major version,这不是项目代码的问题,是JDK版本太高。解决方式很简单:要么把JDK切回1.8,要么把pom.xml里的Spring Boot版本升级到2.7以上并同步调整依赖。我建议保持JDK 8,因为整套配置都是按这个版本调试好的,何必给自己找麻烦。

5.2 初始化数据库:不是简单执行一个SQL文件那么简单

源码里带了一个sql/init.sql文件,里面包含建库命令、全部建表语句和初始数据。执行之前要注意两点。

第一,字符集统一用utf8mb4,否则带生僻字的人名、特殊字符可能存不进去。建库语句默认是:

CREATE DATABASE `convalescent_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

第二,初始数据里预置了一个管理员账号和几个演示用的角色、菜单。项目完成后第一件事就是改掉这个默认密码。很多系统出安全问题都是从默认口令开始的,这是老生常谈的教训。

5.3 后端配置文件和启动步骤

把数据库初始化好之后,打开后端的application.yml,把数据源改成你自己的地址、账号、密码:

server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/convalescent_db?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

然后在项目根目录执行:

mvn clean package -DskipTests java -jar target/convalescent-admin.jar

看到Started Application in XX seconds就说明后端起来了。建议启动后先访问http://localhost:8080/api/auth/login,用管理员账号测试一下登录接口能否正常返回Token,这一步能快速确认后端和数据库连通性。

5.4 前端启动和代理联调

前端的src/utils/request.js里封装了Axios实例,基础URL指向一个代理地址,开发环境下由Vue CLI的代理转发到后端:

baseURL: '/api'

vue.config.js里的代理配置大概是这样:

devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这里有个非常容易踩的坑:后端context-path已经设置了/api,前端代理指向后端时如果再用/api前缀,就要让代理把路径原样转发,不要再重写。如果代理配置里多了一个pathRewrite: {'^/api': ''},请求到了后端就会变成/api/api/xxx,直接404。这个问题能坑掉一半的新手。

前端启动命令:

npm install npm run serve

如果npm install特别慢,把镜像切到国内源再装,一般一分钟内能装完。

浏览器打开http://localhost:8081,输入初始账号密码登录即可。

5.5 如果本地跑不起来,先从这三个方向定位

我见过很多人卡在启动环节,实际九成是这三个原因:

端口被占用。后端8080前端8081都可能有其他程序占用,尤其前端调试工具经常占用8080。后端改端口改application.yml里的server.port,前端改端口改vue.config.js里的port。前端代理里的target地址记得同步修改。

MySQL时区不匹配。连接串里不写serverTimezone=Asia/Shanghai,有时候会报The server time zone value错误。解决方案就是连接串带上这个参数,同时确保系统时间正常。

Maven依赖下载不下来。公司内网环境或者网络受限时,依赖可能拉取失败。在maven settings.xml里配置阿里云镜像即可解决。

6. 二开扩展方向与踩过的坑复盘

6.1 第一张要做好的成本账:不是所有功能都要做进系统

这套系统交付后,对方机构提了很多后续需求,但在评审时我都做了排序。真正值得做的几个扩展点我列一下,排在后面的直接砍掉。

首推做的是统计看板。管理层每天要看的无非就那几个数字:当前在住人数、今日新增入住、退住人数、床位使用率、本月应收费用。实现上不需要引入任何报表引擎,后端写一个聚合查询接口,前端用几个统计卡片加折线图就能搞定。这是投入产出比最高的一项。

第二是做消息提醒。当疗养人员的费用余额低于阈值或者健康指标连续异常时,通过短信或微信服务号通知家属。这个需要引入第三方消息服务,功能本身不复杂,但涉及费用和资质,第二期再考虑。

第三才是硬件对接。智能手环、血压计的数据接入,听着很高端,但实际部署要考虑设备成本、网络连接稳定性、数据准确率。在机构还没有完成基础设备升级前,上了也是摆设。

6.2 实际运行中遇到过的三个真问题

这套系统上线过程中,最有价值的收获反而来自几个意料之外的bug。

第一个是定时任务重复执行。最初用Spring自带的@Scheduled做每日计费跑批,后来因为某次手动触发定时器,同一天生成了两遍计费明细。修复方案是引一条执行记录表,每次跑批前先检查当天是否已经成功执行过,成功则直接跳过。这个"幂等检查"在财务系统里迟早会遇到,越早做好越好。

第二个是导出Excel中文乱码。报表导出我用的阿里开源的EasyExcel,在本地一切正常,部署到服务器后导出的文件名和表头出现乱码。排查了半天,发现是Linux服务器的默认编码不是UTF-8。解决方案是在代码里显式指定导出文件名的编码,而不是依赖服务器环境变量。这个坑希望你不要遇到,遇到了至少知道往这个方向查。

第三个是逻辑删除和唯一索引冲突。系统对账户、人员记录做逻辑删除后,为了保留历史痕迹,同一身份证号再次入住时插入了新的档案记录,但身份证号字段上有个全局唯一索引,导致新记录插入失败。解决方案是唯一索引改成联合唯一:id_card + deleted_flag。这个在数据库设计初期就要想清楚,否则上线后改库非常被动。

6.3 关于源码学习的最后一点建议

如果你拿到的这套源码是用来学习的,我的建议是不要从Controller开始读,而是从数据库表结构和service层开始。表结构承载了业务数据模型,service层承载了业务流程判断,Controller不过是薄薄的接口转发层。把这两层读懂,整个系统的脉络就通了。

如果是为了快速交付项目,那就先跑起来,再对着功能清单逐个验证,然后把报表和权限这两块吃透。这两块是最能体现系统价值的地方,也是最容易在答辩或验收时被深入追问的部分。

这套系统不是什么前沿架构,但它是我一手搭起来、真实跑过几个月的作品。对我个人来说,最大的收获不是写了几千行代码,而是明白了"一个管理系统能不能真正落地,不在于用了多新的技术,而在于业务逻辑有没有掰扯明白"。后续我还在持续迭代这套系统,下一阶段会优先补上数据大屏和家属端小程序。如果你也在搞类似的系统,欢迎多交流,踩过的坑已经帮你踩完了,剩下的路会好走很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进