资讯动态

SpringBoot+Vue+MyBatis+MySQL打造智能推荐卫生健康管理系统

发布时间:2026/10/20 6:41:59 来源:尧图企业网站定制
做某社区健康服务中心的信息化改造时对方一口气提了十几个需求健康档案要电子化、体检报告要能结构化存档、慢病随访不能总靠打电话、健康宣教内容要能按人群推送……而且所有这些功能最好能集中在一个后台里完成。当时我选了SpringBoot Vue MyBatis MySQL这套经典组合来开发前后端分离从空数据库开始搭起一点一点把系统填满。整套做下来我对企业级智能推荐卫生健康管理系统这类项目有了不少实在的体会界面好不好看是一回事数据能不能串起来、提醒能不能自动跑、推荐依据靠不靠谱才是真正决定系统能不能用起来的关键。这篇文章就把我从这套系统开发里沉淀下来的东西完整梳理一遍包括技术选型逻辑、智能推荐模块的落地方式、数据库设计思路、以及上线前后踩过的坑。适合正在做类似管理系统开发的同学、准备做卫生健康方向项目的开发者还有想参考完整项目结构的团队参考。1. 一套系统背后的真实需求从健康档案孤岛说起1.1 旧工作方式下的痛点清单在动手写代码之前我花了不少时间把原来的工作流程摸了一遍。某社区健康服务中心过往的管理方式基本是靠纸质档案和Excel表格撑着。居民来建档登记表手填一份体检结果打印出来往档案袋里一塞下次来复查医生要翻半天才能找到。随访就更原始了护士按月份人工翻名单找到谁该复查了再打电话通知。健康宣教则是统一印传单不管老人小孩、不管有没有慢性病全发同一份。这些问题的本质其实是一条链上断了四个环节档案数据孤岛个人信息、体检记录、随访记录、宣教记录分属不同表格甚至不同纸质文件无法关联查询。异常指标发现被动体检报告单靠人工判读指标异常了没有自动提醒容易漏掉高风险人群。随访计划全靠记忆慢病管理要求定期追踪血压、血糖等指标但谁该随访、哪天该随访完全没有系统化记录。宣教内容无差别投放所有人收到同样内容缺少针对性和个性化居民打开率很低。这些痛点在中小型健康管理机构里特别典型。如果你也是给医院、体检中心、企业医务室这类场景做管理系统几乎都会遇到同样的问题。1.2 系统边界划定哪些做、哪些不做项目一开始最容易犯的错就是把需求无限扩大。我们当时也讨论过要不要做在线问诊、药品库存管理等模块后来全部砍掉了。最后确定这套系统只做五条核心业务线健康档案管理居民基础信息、既往史、家族史、过敏史、生活习惯的录入和查询。体检管理体检预约、体检记录登记、指标结构化存储、异常指标自动标记。慢病随访管理高血压、糖尿病等重点慢病随访计划的制定、提醒、结果记录。健康宣教内容库文章、视频等宣教素材的分级分类管理以及面向用户的定向推送。智能推荐引擎基于用户画像和健康指标自动推送宣教内容、生成随访建议、推荐体检项目。边界划定之后开发方向就清晰了很多。这也是我做这类系统的一个经验宁可把一个环节做透也不要每个环节都只做个半成品。2. 技术选型复盘SpringBoot Vue MyBatis MySQL为什么够用2.1 后端框架为什么用SpringBoot而不是其他方案这套系统的后端我选的SpringBoot 2.7.x理由很简单生态成熟、上手成本低、招人好招。SpringBoot的自动配置机制让项目不用纠结复杂的XML配置内嵌Tomcat也让部署变得非常省事打一个jar包丢到服务器上就能跑。启动类就是标准入口配合application.yml做环境区分开发、测试、生产三套配置互不干扰spring: profiles: active: dev datasource: url: jdbc:mysql://localhost:3306/health_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver有个点容易被忽略数据库连接URL务必带上serverTimezoneAsia/Shanghai。比版本升级到8.x之后不带时区参数会有8小时偏移的问题这个后面在踩坑章节会细讲。SpringBoot把定时任务、缓存、消息推送这些模块都做了starter封装我用EnableScheduling加了随访提醒的定时扫描用Spring Data Redis做了热点用户画像的缓存整体开发效率比早期SSH那套架构高太多了。2.2 MyBatis复杂SQL可控性优先的选择选型的时候也犹豫过Spring Data JPA它是挺方便实体关系映射直接、开发速度快但等报表查询多了之后JPA生成SQL的不可控性会变成隐患。健康管理系统里很多查询是跨表关联加条件动态拼接的比如按年龄区间血压异常随访状态筛选某社区的高危人群这种SQL交给JPA去推导生成出来的东西又长又难优化。MyBatis在这里的优势是SQL完全掌握在开发者手里。我习惯用XML来管理复杂查询简单CRUD用注解就够。比如筛选高风险人群SQL可以写成select idselectHighRiskUsers resultTypemap SELECT u.real_name, u.gender, u.birth_date, r.blood_pressure, r.blood_sugar, f.last_follow_up_time FROM health_user u LEFT JOIN health_record r ON u.id r.user_id LEFT JOIN follow_up_record f ON u.id f.user_id where if testminAge ! null AND TIMESTAMPDIFF(YEAR, u.birth_date, CURDATE()) gt; #{minAge} /if if testbpHigh ! null and bpHigh AND r.blood_pressure gt; 140 /if /where /selectwhere加if的动态SQL组合是MyBatis处理多条件筛选最顺手的方式既不会因为条件缺失导致全表扫描也不用在Java层手动拼SQL字符串。2.3 前端与数据库版本选择前端我用了Vue 2.7加Element UI。当时有考虑直接上Vue 3但如果团队成员都是第一次接触这套技术栈Vue 2的教程和踩坑案例明显更多排查问题快。选Element UI是因为它的表格、表单、树形控件、日期选择器都能直接覆盖管理后台的常见交互场景不需要自己造轮子。数据库用的是MySQL 8.0InnoDB引擎字符集统一utf8mb4。为什么要指定utf8mb4因为有些健康宣教文案里可能包含生僻字或者特殊符号旧版的utf8字符集存不了4字节字符会报incorrect string value错误。8.0这边默认就是utf8mb4但建库时建议还是显式声明一下比较稳妥。这套组合不追求花哨但很稳健。我曾经见过有人为了显示技术含量硬把几百人的内部管理系统做成微服务加容器编排结果光基础设施排查就耗掉一半时间。对于这种体量的项目单体应用加前后端分离就是性价比最高的方案。3. 智能推荐模块从规则引擎到内容匹配的渐进实现3.1 智能推荐在这个系统里的真正含义标题里的智能推荐容易让人联想到电商那套基于海量用户行为的推荐算法但健康管理系统的情况完全不同。健康数据的核心特征是稀疏和敏感一个用户一年可能就只有一两次体检记录行为日志更是少得可怜传统的协同过滤算法在这种数据密度下很难产出有效结果。所以我在设计推荐模块时定了个原则先让规则跑起来再用内容匹配做增强最后用热门兜底。这套渐进式方案不需要搭建复杂的算法平台单靠后端代码加SQL就可以落地但实际运行效果放在这个垂直场景里足够实用了。具体来说系统里跑三类推荐任务体检套餐推荐按年龄段、性别、既往病史、家族史输出下一年度建议体检项目。随访计划推荐根据最近一次体检指标判定是否需要进入慢病随访流程。宣教内容推荐根据用户画像和健康风险标签推送针对性的健康文章或视频。3.2 基于规则的推荐体检指标驱动随访计划规则推荐是整套系统的地基。思路很简单把医生的临床建议转译成可配置的规则系统根据体检指标自动判断风险等级和随访建议。例如血压规则可以这样配置收缩压mmHg风险等级推荐动作90-139正常常规随访每12个月一次140-159偏高进入重点关注名单每3个月随访160以上高危立即通知签约医生每1个月随访这些规则我放在一张recommend_rule表里字段包括规则名称、触发条件JSON、推荐结果JSON、优先级、状态。触发条件用JSON存的原因是规则经常需要调整每次改规则就发布一个新版本到生产环境不太现实做成配置化之后运营同学在后台改一改就能生效。实际判断的时候后端从体检记录里取出最新的指标数据逐条比对规则public void evaluateFollowUpPlan(Long userId) { HealthExam latestExam examMapper.selectLatestByUserId(userId); if (latestExam null) { return; } ListRecommendRule rules ruleMapper.selectEnabledRules(); for (RecommendRule rule : rules) { if (rule.matches(latestExam)) { followUpService.generatePlan(userId, rule.getResult()); break; } } }对于高血压这类指标值相对稳定的情况这套规则引擎已经能覆盖绝大多数业务需求而且逻辑透明、可解释性强居民或医生问起来都能说清楚这个建议是怎么得出来的。3.3 基于内容的推荐健康宣教文章匹配宣教内容推荐这部分我采用了基于内容的过滤方式。每篇文章上传时都会打上标签比如高血压、糖尿病、老年人、孕妇、营养、运动。每个用户档案里也有对应的人群属性和健康标签。推荐的核心逻辑就是标签匹配加权重计算。用户标签从两个维度来一是基础档案里填写的年龄、性别、疾病史二是最近一次体检的异常指标。文章标签和用户标签做向量内积得分高的优先推送。这两个维度放在一起的效果是一个60岁的高血压患者登录系统首页推送的文章排序里高血压饮食指导会比老年人春季养生排在更靠前的位置因为前者的标签匹配了两个维度后者只匹配了年龄。系统推荐逻辑不算复杂但比我预想的效果要好。我后来统计过推送文章的点击率基于标签匹配的内容打开率比之前统一推送的传单式内容要高出不少。3.4 冷启动与数据稀疏的处理策略冷启动问题在这里同样存在新用户没有任何体检记录和行为历史时推荐系统是无从下手的。当时处理方案是分级处理新用户注册完成且未体检推送通用健康科普文章热门内容排行兜底。新用户已建档未体检按年龄、性别推荐基础体检套餐。用户完成首次体检触发规则引擎生成随访计划并推送相关的个性化内容。这套逻辑加了两层保障所有推荐位都设有默认内容池即使规则匹配不到也不会出现空白位所有推荐结果都会记录用户是否浏览或点击作为后续人工调整标签的参考依据。实际运行下来系统的推荐结果始终有一份内容打底没有出现过推荐位空着的尴尬场景。4. 核心功能拆分与数据库设计一张表一张表说清楚4.1 功能模块全景系统后台的角色分为系统管理员、健康管理师、医生和普通用户四类。界面侧分两个端管理端Web后台和用户端供居民查看报告和随访计划。功能模块大致可以拆成下面几个用户管理用户注册、审核、角色分配、账号启停。档案管理个人信息、既往史、家族史、过敏史、生活习惯的多维录入与编辑。体检管理体检批次创建、指标项自定义、报告上传、异常自动标识。随访管理计划创建、提醒推送、随访结果记录、逾期统计。宣教库内容分类、标签维护、上下架管理。推荐引擎管理规则配置、推荐效果查看。统计报表人口结构、疾病分布、随访完成率等维度的统计图表。每一条业务线都是独立的模块化设计模块之间只通过接口交互不直接操作对方的表。这样后期做二次开发时改动一个模块不会影响其他模块。4.2 核心表结构与设计思路数据库整体按业务域分成了五个逻辑组。这里把对系统最关键的几张表列一下表名用途核心字段health_user用户基础信息id, real_name, gender, birth_date, phone, id_card_nouser_profile用户健康档案扩展user_id, height, weight, smoke_status, drink_status, allergy_historyhealth_exam体检记录主表id, user_id, exam_date, exam_org, conclusionexam_item_detail体检指标明细表exam_id, item_code, item_name, item_value, reference_range, statusfollow_up_plan随访计划表id, user_id, plan_start_date, plan_end_date, frequency, statusfollow_up_record随访结果记录表plan_id, follow_date, result_content, next_follow_datecontent_article宣教内容表id, title, content, category_id, tags, cover_urlrecommend_rule推荐规则表id, rule_name, condition_json, result_json, priority, status体检指标单独拆一张明细表是这套设计的核心决策。如果只用一行JSON把体检结果存进主表查询某个指标异常的人数时会非常痛苦——MySQL对JSON字段的检索性能远不如结构化行存储。拆成明细表之后用item_code加item_value做筛选一个普通索引就能搞定SELECT COUNT(DISTINCT user_id) FROM exam_item_detail WHERE item_code GLU AND CAST(item_value AS DECIMAL) 7.0;档案和体检分离的设计也很重要。health_user只存稳定不变的基础信息user_profile存身高体重这类会变化的数据每次体检更新的是指标表而不是覆盖档案这样既能保留历史轨迹又能避免并发写入时的锁冲突。4.3 索引与事务边界索引设计遵循的原则是查询频率高的字段建索引区分度低的字段不建。实际建了下面几组索引health_userphone唯一索引、id_card_no唯一索引。exam_item_detail(exam_id, item_code)联合索引查询一次体检的全部指标走这个索引(item_code, item_value)联合索引支撑按指标值筛选人群。follow_up_plan(plan_start_date, status)联合索引定时任务扫描即将到期的计划时直接命中索引不会拖慢主业务。事务边界上我坚持一个业务用例一个事务的原则。例如创建随访计划并推送提醒这是一个完整的操作单元就在Service层加Transactional但单纯读取推荐内容这类跨多张表的查询不加事务、走读库减轻数据库压力。5. 关键业务实现细节这些地方决定了系统能不能用5.1 登录鉴权与多角色权限控制系统的角色权限是这个项目里最不能凑合的部分。健康档案属于敏感信息不是随便一个登录用户就能看一眼的。我采用了JWT加Spring Security的组合方案。用户登录成功后后端签发一个包含用户ID和角色列表的JWT过期时间设置为8小时。前端每次请求在请求头带上Authorization: Bearer token后端根据JWT里解析出的角色判断是否放行。权限控制粒度上我做了两层。接口层用PreAuthorize注解控制粗粒度权限PreAuthorize(hasRole(HEALTH_MANAGER)) PostMapping(/follow-up/plan) public Result createFollowUpPlan(RequestBody FollowUpPlanVO vo) { return followUpService.createPlan(vo); }数据层用AOP拦一层细粒度权限核心是隔离用户之间的数据。比如医生只能查看自己签约管理的居民档案普通用户只能查看自己的报告。访问其他用户的数据时返回无权限提示而不是直接暴露数据。JWT有一个典型坑需要提前说签发之后无法主动失效。用户改了密码或者被管理员禁用旧token在过期之前仍然有效。我加了一个简单的解决方案用户表里存一个token_version字段JWT生成时带上这个版本号每次改密码或禁用账号就自增版本校验时对比版本号不一致就拒绝。5.2 体检报告指标的结构化解析信息化改造过程中最繁琐的工作就是把一堆格式五花八门的体检报告转成结构化数据。不同机构的报告单项目名称、单位、参考范围都不统一血糖可能写作空腹血糖也可能写作GLU。我的处理方案分两条路并行机构提供的电子报告是JSON或Excel格式写一个适配器接口不同数据格式实现各自的解析逻辑最终统一转成ExamItemDetailVO。纸质报告扫描或PDF先由录入员在后台选择标准指标项手工或批量导入数值等数据量积累够了再引入OCR识别做辅助录入。指标统一之后用一个标准字典表来映射别名。比如字典表里定义标准编码GLU别名可以是血糖空腹血糖GLUGlucose解析时统一映射为GLU。后续的异常判断、规则匹配都基于标准编码操作这样就不会出现一份报告里血糖异常而另一份报告对应字段查不出来的问题。这件事给我最大的体会是业务系统的硬骨头往往不在架构设计上而在于脏数据的清洗和标准化。提前设计好字典表能省掉后面大量麻烦。5.3 随访提醒的定时任务设计随访提醒如果全靠人工发起没多久就会漏。我做了两种自动化机制配合使用。第一种是系统内提醒。每天凌晨两点定时任务扫描follow_up_plan表找出未来三天内到期或者已经逾期未完成的计划批量生成待办提醒任务推送到管理端的消息中心。业务上称这为明日随访清单健康管理师上班第一件事就是打开这个清单处理。第二种是短信通知。对临近随访日期尚未完成的用户系统会发送短信模板提醒。短信发送通过一个抽象接口封装对接了某短信服务商但接口保留了扩展能力以后换服务商只需要替换实现类即可。定时任务本身用Spring的Scheduled注解加cron表达式。这里有个细节值得写一下生产环境里定时任务一定要加分布式锁否则部署多实例时同一个任务会被执行多次。我当时用数据库的行锁做了一个轻量锁方案——在任务记录表里插入一个唯一键的任务执行记录执行完毕删除重复执行会插入失败直接跳过。5.4 健康档案的前后端联动展示档案页面是整个系统里最看重交互体验的部分因为健康管理师每天都在这个页面工作。前端我用Vue的组件化方式把档案拆成基本信息、体检记录、随访记录、推荐建议四个Tab页切换时按需请求对应数据不一次性把全量数据加载进来。后端接口在设计上做了两个优化列表页只返回摘要字段详情按下拉刷新后再请求减少首屏传输体积。档案查询接口做了简单的Redis缓存用户的基础信息读取频繁但更新频率低缓存半小时的过期时间足够用。需要特别注意前后端字段类型的一致性。比如日期类型后端返回yyyy-MM-dd字符串前端不要随意转成时间戳再格式化容易在时区上产生误差。我后来统一约定所有接口的日期字段都返回字符串不做二次转换。6. 部署与调优踩坑记录我从上线过程里学到的几件事6.1 跨域与代理配置前后端分离项目最最常见的第一个坎就是跨域。开发时Vue默认跑在8080端口后端跑在8081端口直接用axios请求后端接口必然报跨域错误。我在后端写了全局跨域配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }生产环境则用Nginx做反向代理同一域名下通过/api前缀转发到后端服务这样用户浏览器始终只访问同一个域名不存在跨域问题也顺便把静态资源的缓存一起处理了。如果你同时配了跨域和Nginx代理记得让后端只保留其中一种方式否则可能出现重复响应头导致的奇怪问题。6.2 数据库时区与连接池参数这个坑是第一次启动时就踩到的。MySQL 8.0默认时区是UTC而我们的业务时区是东八区结果时间字段全部差了8小时。排查过程倒是直截了当先看数据库连接串发现没有配置serverTimezone补上serverTimezoneAsia/Shanghai之后时间就对了。连接池用的Druid参数上我最看重两个。initialSize设为5避免冷启动第一个请求等连接初始化太慢maxActive设为50防止并发高峰时连接不够用。还要打开validationQuery为SELECT 1定期检测连接有效性避免数据库重启后应用还拿着断掉的连接。这些参数配好之后长连接稳定不少不会出现运行几天就莫名报超时的情况。6.3 MyBatis动态SQL的性能隐患SQL性能问题是我在数据量上来之后才开始重视的。有一次某个列表页越用越慢排查发现是动态SQL中一个if条件拼出来的查询没走索引。具体原因是left join时驱动顺序和预期不符MySQL没有选中小表作为驱动表导致大表被反复扫描。优化的做法是改写查询把高基数字段筛选条件放进子查询里提前把数据量降下来再关联其他表SELECT u.real_name, r.item_code, r.item_value FROM health_user u INNER JOIN ( SELECT user_id, item_code, item_value FROM exam_item_detail WHERE item_code GLU AND CAST(item_value AS DECIMAL) 7.0 ) r ON u.id r.user_id WHERE u.status 1;另外写SQL时要注意隐式类型转换的问题。item_value如果存储的是字符串和数字比较时MySQL会做全表扫描根本用不上索引。解决办法是字段定义时确定好类型数值型指标就存成DECIMAL不要所有指标都塞进VARCHAR里。6.4 常见报错速查表上线前后整理一下遇到过的典型报错对其他接手的人会有帮助报错现象根因解决方案时间字段差8小时未配置serverTimezone连接串加serverTimezoneAsia/Shanghai存生僻字报incorrect string value库或表字符集不是utf8mb4库、表、字段统一改utf8mb4前端访问接口提示CORS error跨域配置缺失或Nginx重复代理后端或Nginx只保留一种跨域方案Druid报Connection is not available连接池被占满或连接泄漏排查未释放连接调大maxActiveJWT过期后接口仍然返回数据服务端没有主动失效机制接入token_version字段对比版本7. 源码组织方式与二次开发拓展建议7.1 目录结构与编码约定这套源码的目录结构经过两次重构最终稳定下来。后端按功能模块分包而不是按技术层分包这样做的直接好处是找类方便改一个功能时涉及的所有文件都在同一个包下面com.health.system ├── common // 通用工具、返回结构、异常处理 ├── config // 配置类 ├── controller // 控制层 ├── service // 接口及实现 ├── mapper // MyBatis接口 ├── entity // 数据库实体 ├── dto // 数据传输对象 └── module ├── auth // 登录认证模块 ├── user // 用户档案模块 ├── exam // 体检模块 ├── followup // 随访模块 ├── content // 宣教模块 └── recommend // 推荐模块编码约定上所有接口统一返回ResultT结构包含code、message、data三个字段。前端的axios响应拦截器统一处理code非200的情况避免每个页面都写一遍错误弹窗。这套约定看着朴素但多团队协作时能少吵很多架。7.2 想加新功能时可以怎么扩展如果基于这套系统做二次开发我建议优先考虑以下几个方向多租户改造当前系统按单机构设计如果想做成SaaS服务需要给核心表增加tenant_id字段并在SQL层做自动过滤。数据大屏管理端的统计报表已经积累了人群分布、疾病谱、随访完成率等数据可以增加一个大屏页面用图标库做可视化展示。对接设备数据社区中心如果有血压计、体脂秤等设备可以增加设备接口让检测数据自动写入档案减少人工录入。移动端适配现有用户端是响应式网页后续可以进一步封装成小程序服务居民群体更方便。我自己在实际部署这套系统的过程中最大的收获是明白了智能推荐在垂直行业里的落地姿态——不一定是复杂的模型调参而是把领域的业务规则理解透用工程手段固化下来让规则和内容推荐互相配合。这套逻辑放在健康管理场景里成立在其他需要千人千面的业务场景里同样适用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑