接到这个老年人健康检查就诊就医管理系统的需求时,项目背景其实并不复杂——一家社区养老服务机构的负责人找到我,说他们现在还在用纸质表格记录老人的体检数据和就诊信息,每次上级来检查都要翻半天档案柜,医生快退休了录入速度慢,家属想了解老人情况只能电话问护士。他们要的也很明确:把老人的健康档案、年度体检、日常就诊、慢病随访这几条线串起来,能查、能统计、能提醒。我最终定的技术方案是 thinkphp + vue 前后端分离,后端跑在 PHP 环境上,前端用 Vue 3 做单页应用,中间用 JSON 接口通信。
这篇东西我打算从需求梳理、数据库设计、接口约定到前端页面实现的完整过程都写一遍,重点讲那些不写进文档、但实际开发里绕不过去的细节。如果你正打算做类似的管理系统,或者是养老机构、社区服务中心的信息员,可以把这篇当作一个可参考的落地案例。后端部分用到 thinkphp 的模型、验证器、中间件,前端部分用到 vue 路由、状态管理、组件通信和 ECharts 图表,整体代码量不大,但业务流程涉及的角色和状态转换有好几处需要谨慎处理。
1. 接这个项目之前,我先搞清楚了几件事
1.1 需求方真正想要什么
很多开发拿到需求第一反应是“不就是一套CRUD吗”,但老年人健康管理系统跟普通的企业管理系统有一个很不一样的地方:数据要长期沉淀,还要跨角色流转。体检记录是一年一年累积的,就诊记录是每一次都关联到具体医生的,随访计划是按周期生成的。需求方口头上说“做个档案管理”,实际上他们想要的是三件事:
第一,把老人的基础信息统一管起来。包括姓名、性别、生日、身份证号、联系电话、紧急联系人、血型、过敏史、既往病史、家庭住址、所属社区等。第二,把体检数据沉淀成可对比的历史趋势。比如今年和去年相比,老人的血压是升高了还是稳定了,这个必须能快速查出来。第三,把就诊流程数字化。老人生病来机构里看病,要能预约、签到、分诊、写病历、开处方,而不是靠一张纸在各科室之间转。
搞清楚这三件事之后,技术方案就不是拍脑袋定的,而是被业务倒逼出来的。
1.2 项目角色与权限边界
这个系统里角色划分我一开始就定了五类,权限边界越早明确,后面的接口设计和菜单配置就越省事:
- 超级管理员:负责系统配置、账号管理、数据备份,不直接参与业务操作。
- 机构管理员:一般是养老院院长或者社区服务中心主任,能看全部统计报表,能审核异常数据。
- 医生:主要操作就诊模块,写病历、开处方、查看老人历史健康档案和体检趋势。
- 护士/体检人员:负责录入体检数据、执行随访计划、更新老人日常健康状态。
- 老人家属:通过手机端(H5页面)查看健康档案、体检报告,提交预约申请。
这五类角色在界面上的体现就是左侧菜单不同,后端接口也要做权限校验。我用的是 thinkphp 的中间件配合 JWT,在请求头带上 token,后端解析出用户身份和角色,再按角色白名单判断当前接口是否允许访问。
1.3 功能清单确定过程
跟需求方开了三次会,最终确定下来八个功能模块:系统登录与账号管理、老人档案管理、体检记录管理、就诊就医管理(预约、分诊、病历、处方)、慢病随访管理、统计报表、消息通知、系统设置。
这里有个容易被忽略的细节:需求方最开始还想要“在线支付功能”,说老人看病可以扫码付费。我建议第一版先不做,因为涉及到对公账户、支付渠道资质、退款流程等一堆非技术问题,核心的医疗流程先跑通比在线支付重要得多。第一版先做到“收费记录”,也就是把费用明细和实收金额记下来,后面再考虑接支付渠道。
2. 技术选型:thinkphp + vue 到底怎么搭
2.1 后端为什么选 thinkphp
说实话,这个项目用 Java Spring Boot 或者 Go 也能做,但最终选了 thinkphp,是综合考虑了团队能力和部署环境的结果。
第一,部署目标环境是机构自己的一台服务器,装的是 Windows Server,带宝塔面板,PHP 5.6 和 MySQL 5.7 都是现成的。如果用 Java,还得装 JDK、配置 Tomcat 或者打 Docker 包,对机构的信息员来说维护成本偏高。第二,thinkphp 在国内生态成熟,官方文档中文资料全,遇到问题搜一下就有答案,团队的熟手能直接上手写业务,不需要花时间搭框架。第三,对于这个系统的数据量来说,PHP 完全够用。一个养老机构撑死几千位老人,一年体检记录几万条,MySQL 加上正确的索引,查询毫无压力。
我用的是 thinkphp 6 的完整版,开启了多应用模式,也就是app/api、app/admin分开,api 下放移动端接口,admin 下放管理后台接口。这样职责清晰,后面就算要再做一个家属端小程序,也可以共用一个 api 应用,只是增加新的控制器和方法。
2.2 前端为什么选 vue 3 + element-plus + vite
前端我直接用 Vue 3 + Element Plus + Vite 这套组合。这里不选 Vue 2 的原因很简单:Vue 3 的组合式 API 写起来逻辑复用性更好,而且 Vite 开发时启动速度比 webpack 快很多,对开发者体验提升明显。Element Plus 是 Element UI 的 Vue 3 版本,表格、表单、弹窗、日期选择器这些后台常用组件都有现成的,样式也统一。
项目脚手架用npm create vue@latest生成,选上了 Vue Router 和 Pinia。Vue Router 负责页面路由,因为管理后台是多页面菜单的结构,路由配置要跟菜单联动;Pinia 用来存登录状态、用户信息和全局的权限标记。开发时用 vue devtools 插件调试组件状态和路由跳转,特别方便排查那种“页面跳转了但数据没更新”的问题。
2.3 前后端分离架构与目录设计
整体架构就是标准的浏览器加载静态页面,页面里的数据全部通过 AJAX 请求后端接口获取。前端的 Vue 项目打包之后生成静态文件,放到 Nginx 的站点目录下,后端 thinkphp 项目放在另一个目录,通过反向代理把/api/前缀的请求转发给 PHP 服务。
前端的目录我习惯按功能分模块,src/api/专门放接口请求函数,src/views/放页面组件,src/router/放路由配置,src/store/放 Pinia 状态。后端的目录按 thinkphp 多应用规范来,控制器放app/admin/controller/,模型放app/common/model/,验证器放对应的 controller 目录下。
关键的一点是接口地址的规划。我约定所有接口都以/api/开头,比如/api/senior/list、/api/healthcheck/add、/api/appointment/create,这样前后端联调的时候,Nginx 只需要把/api/转发到index.php,其他路径全部交给 Vue 的 history 路由处理,互不干扰。
3. 数据库设计:把业务拆成一张张表
3.1 老人健康档案表的设计
健康档案是整个系统的基础,绝大多数业务都围绕老人档案展开。我建的表叫senior_member,核心字段如下:
CREATE TABLE `senior_member` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '姓名', `gender` tinyint(1) NOT NULL DEFAULT '1' COMMENT '性别 1男 2女', `birthday` date NOT NULL COMMENT '出生日期', `id_card` varchar(18) NOT NULL COMMENT '身份证号', `phone` varchar(20) NOT NULL COMMENT '联系电话', `emergency_contact` varchar(50) DEFAULT NULL COMMENT '紧急联系人', `emergency_phone` varchar(20) DEFAULT NULL COMMENT '紧急联系电话', `blood_type` varchar(10) DEFAULT NULL COMMENT '血型', `allergy_history` text COMMENT '过敏史', `chronic_disease` text COMMENT '既往病史/慢病', `address` varchar(255) DEFAULT NULL COMMENT '家庭住址', `community_id` int(11) DEFAULT NULL COMMENT '所属社区', `nursing_level` tinyint(1) DEFAULT '1' COMMENT '护理等级 1自理 2半自理 3全护理', `status` tinyint(1) DEFAULT '1' COMMENT '状态 1正常 2已退住', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`), KEY `idx_id_card` (`id_card`), KEY `idx_community` (`community_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='老人健康档案表';这里有一个经验之谈:身份证号一定要加索引,因为录入体检数据时经常要用身份证号快速匹配档案,而且身份证号是唯一标识,相比姓名来说不会出现重名歧义。姓名虽然也加了索引,但机构里老人重名的情况不少,身份证号才是更可靠的关联字段。
年龄不要直接存进数据库,而是根据birthday动态计算。原因很简单:年龄会变化,如果存了固定值,一年后所有档案的年龄都错了。我在后端写了一个calcAge方法,根据出生日期和当前日期计算实足年龄,列表接口返回的时候自动带上age字段,前端直接展示。
3.2 体检记录与随访计划表
体检记录表health_check是典型的“主表+明细”结构。主表记录体检时间、体检类型、体检机构,明细字段则根据机构实际使用的体检项目来确定。常见的有身高、体重、血压(收缩压/舒张压)、心率、空腹血糖、血常规指标、肝功能指标、视力、腰围等。
CREATE TABLE `health_check` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `member_id` int(11) NOT NULL COMMENT '老人档案ID', `check_date` date NOT NULL COMMENT '体检日期', `check_type` varchar(20) DEFAULT 'annual' COMMENT '体检类型 annual年度 admiss入院 review复查', `height` decimal(5,1) DEFAULT NULL COMMENT '身高cm', `weight` decimal(5,1) DEFAULT NULL COMMENT '体重kg', `blood_pressure_high` int(4) DEFAULT NULL COMMENT '收缩压mmHg', `blood_pressure_low` int(4) DEFAULT NULL COMMENT '舒张压mmHg', `heart_rate` int(4) DEFAULT NULL COMMENT '心率次/分', `blood_sugar` decimal(4,1) DEFAULT NULL COMMENT '空腹血糖mmol/L', `total_cholesterol` decimal(4,1) DEFAULT NULL COMMENT '总胆固醇mmol/L', `vision_left` decimal(3,1) DEFAULT NULL COMMENT '左眼视力', `vision_right` decimal(3,1) DEFAULT NULL COMMENT '右眼视力', `doctor_comment` text COMMENT '医生结论', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_member_date` (`member_id`, `check_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='体检记录表';随访计划表follow_up_plan存的是慢病老人的随访安排。慢病随访逻辑是机构里一个很实际的需求:高血压老人一个季度要随访一次,糖尿病老人一个月要测一次空腹血糖。这个表里记录计划的到期时间、计划内容、执行时间、执行护士、随访结果,列表页按“未完成、已完成”筛选,再把临期计划推送到消息通知里。
3.3 就诊、处方与预约表
就诊流程相关的表有三张:预约表appointment、就诊记录表diagnosis_record、处方明细表prescription。
预约表存储老人预约某个医生某个时间的请求,状态字段是关键,我用状态机来管理。老人提交预约后状态是0(待确认),医生确认后变成1(已预约),到院签到后变成2(就诊中),看诊结束变成3(已完成),中途取消变成4(已取消)。这个状态机在前后端都做了约束,避免出现流程倒流。
就诊记录表是核心,每次老人看病都会生成一条记录,关联老人ID、接诊医生ID、就诊日期、主诉症状、诊断结果、处理意见、费用金额。这里要注意一点:诊断结果是医疗文本,用text类型存储,不要限制长度,因为有些慢病老人的既往病史很长。
处方表关联就诊记录ID,一行存一种药品:药品名称、用法用量、频次、天数、数量、药品单价、总价。
多说一句,这类系统不太适合把所有数据都塞进一张大宽表。数据一旦多了,改一个字段要造成全表锁,而且业务上每张表的修改人、修改时间都不一样。拆开之后,体检、就诊、随访各管各的,后期维护、统计、导出都很方便。
4. 核心功能模块的实操实现
4.1 健康档案管理:从列表到表单的细节
档案管理的页面看起来简单,实际处理时有几个隐藏的坑。
列表页我用 Element Plus 的el-table展示,包括姓名、年龄、性别、联系电话、社区、护理等级、最近体检时间、最近就诊时间、状态。操作栏里有编辑、查看详情、删除按钮。注意每个表格字段我都加了show-overflow-tooltip,因为身份证号和住址这类长文本如果不加这个属性,表格会被撑得很丑。
新增和编辑表单都要做两层校验:前端用 Element Plus 的rules做必填和格式校验,后端再用 thinkphp 的验证器做同样的校验。身份证号需要做18位校验,电话做11位手机号校验,出生日期不能晚于今天。后端验证器的好处是防止有人绕开前端直接调用接口传脏数据。
这里分享一个我踩过的坑:身份证号里可能会有X,客户端提交时如果用了input type="number"会把X过滤掉。直接在模板上就要用type="text"配maxlength="18",而不是 number。
新增、编辑、删除接口在 thinkphp 里就是标准的模型增删改查。清单列表返回给前端的数据,除了模型字段外,我还会附带上age字段和最近一次体检信息的简要摘要。这样列表页可以显示出“最近体检:2024-10-15”,不用前端再挨个调体检接口。
4.2 体检数据录入与指标校验
体检录入是护士每天都要用的功能,页面做成了按老人档案搜索之后加载一个表单,表单字段对应体检明细的各个指标。护士填完点击保存,后端逻辑里做了几个关键的处理:
第一个是重复体检校验。同一个老人在同一天内不允许重复录入年度体检数据,如果录了两次,后端先查询当天是否已有记录,有的话直接返回“该老人今天已经有体检记录”。这是业务上防止误操作的必要校验。
第二个是指标范围校验。血压、血糖这类数据是有正常范围的,血压过低或过高都意味着身体异常。我在验证器里定义了规则:
protected $rule = [ 'blood_pressure_high' => 'between:60,260', 'blood_pressure_low' => 'between:40,160', 'blood_sugar' => 'between:1.0,35.0', 'heart_rate' => 'between:20,250' ];超出范围就给出提示,但这里注意不要直接拒绝保存。原因很简单:如果老人真的血压到260了,护士录入时反而要能录进去,因为这是真实病情,拒录等于掩耳盗铃。更好的做法是:正常值范围内的记录直接保存,超出范围时前端弹出一个确认框提示“血压值异常,是否确认保存”,护士确认后照样入库。这套逻辑的设计是基于真实使用场景的,而不是理想化的校验。
第三个是趋势对比。体检录入完成后,前端在详情页用 ECharts 画折线图,把最近几次体检的血压、血糖、体重数据连成趋势线。老人家属最关心的就是“指标是上升还是下降”,折线图比任何文字描述都直观。
4.3 就诊就医流程的完整实现
就诊流程是这个系统里状态转换最复杂的部分,我按“预约 → 签到 → 分诊 → 诊断 → 处方 → 结算记录”的顺序来实现。
预约功能支持两种入口:护士代为预约,或者家属在小程序端提交预约申请。预约信息包含老人档案ID、预约医生、预约日期、时间段。医生端有一个待确认列表,确认后方可纳入当天的门诊排班。
签到环节我用的最简单的方式:诊所前台查到预约记录后点击“确认到达”,状态从“已预约”变为“就诊中”。这个操作的目的是区分“预约了但没来”和“已经在诊室候诊”两类人群,方便医生查看当前有哪些老人在等着。
分诊其实就是给就诊记录打个标签,比如内科、外科、慢病复诊、取药。我是在签到之后弹一个分诊选择框,选完进入医生的接诊工作台。医生的接诊工作台左边是排队列表,右边是当前老人的完整资料卡片和病历填写区。病历填写区包含三个文本框:主诉、体格检查与诊断、处理意见,另外还要填本次费用金额。
处方部分做了联动:医生在病历区写完诊断后,可以动态添加多条处方明细,每条明细包括药品名、用法用量、频次、天数、数量。前端动态增删表单行的功能用的是 Vue 的v-for循环渲染,添加一行就向一个数组 push 一个空对象,修改时用行索引绑定。保存时前端把病历信息和处方明细放在一个请求里提交,后端用事务处理,病历主表和处方明细表要么全部写入成功,要么全部回滚,保证数据一致性。
Db::startTrans(); try { $recordId = Db::name('diagnosis_record')->insertGetId($recordData); foreach ($prescriptions as $item) { $item['record_id'] = $recordId; Db::name('prescription')->insert($item); } Db::commit(); } catch (\Exception $e) { Db::rollback(); return jsonError('保存失败:' . $e->getMessage()); }之前我遇到过一种情况:病历保存成功,处方保存失败,结果老人药都拿了但系统里没有处方记录,对账时非常麻烦。所以这里事务是必须的,只要写过一次线上系统的人都能理解这个痛。
4.4 统计报表与可视化看板
管理后台统计页面用的是 ECharts 的折线图、柱状图和饼图。最常见的三种报表是:
月度体检人数趋势图:按月份统计体检人次,看出哪些月份是体检高峰。社区疾病分布饼图:把慢病老人的疾病类型按人数统计,比如高血压多少个、糖尿病多少个、冠心病多少个。性别年龄分布柱状图:按年龄段分桶统计人数,比如60-69、70-79、80-89、90岁以上。
后端统计接口的做法是写原生 SQL 查询,用 thinkphp 的Query查询构造器配合group和count。比如统计每月体检人数的代码:
$monthList = Db::name('health_check') ->field("DATE_FORMAT(check_date, '%Y-%m') as month, COUNT(*) as cnt") ->where('check_date', '>=', date('Y-m-d', strtotime('-12 months'))) ->group('month') ->order('month', 'asc') ->select() ->toArray();前端用 ECharts 按月份排序渲染。这里要特别留意:SQL 查出来的月份是字符串,排序要按 YYYY-MM 的字典序排,不要直接按月份数字排,否则会出现 2025-01 排在 2024-12 前面的问题。另外接口返回空数据时,前端要自己补齐缺失的月份,图表上的横轴才能连续显示,不然会出现“中间缺一个月”的现象。
统计页还有一个实用的功能:异常指标预警列表。把最近一次体检中血压高于140/90、空腹血糖高于6.1的老人自动捞出来,按“需要关注”排序,机构管理员可以一键给家属发送通知。这个功能需求方一开始没想到,是我在做趋势对比时发现“只画图不处理”价值有限,主动加上去的,实际使用时护士们说这个列表比图表还有用。
4.5 便民功能:视频宣教、地图导诊、消息通知
这个系统不光是机构内部用,家属端也要能访问。我做了几个便民功能,也都跟技术选型相关。
健康宣教视频:机构会定期上传一些老年人防摔倒、慢病饮食指导的视频。视频文件如果放在服务器上直接用<video>标签播放,带宽压力大,而且大视频加载慢。机构内网环境我用的是 HTML5 的video标签直接播放 mp4,如果后续有公网部署需求,建议转成 m3u8 分片流,前端用 hls.js 库动态加载播放。
地图导诊:家属端展示老人就诊的具体位置和医院导航入口,我引入的是腾讯地图 JavaScript SDK,通过地理坐标显示机构位置,点击“导航”跳到地图App。这里有个细节,开发者密钥建议放到后端环境变量里,不要直接写在前端源码中,否则会被爬走。
消息通知:机构内部的消息直接通过站内信实现,给家属推送重要消息时,用的是企业微信群机器人 Webhook。机构其实已经建了家属群,机器人往群里推消息不需要额外开发App,只需要配一个 Webhook 地址,后端用file_get_contents或者 curl 发 POST 请求就行。这个方案成本低,运维也简单。
5. 前后端联调与部署运维
5.1 跨域问题与请求鉴权
前后端分离后,第一件要解决的事就是跨域。开发时 Vite 启动在 5173 端口,后端接口跑在 8000 端口,浏览器会拦截跨域请求。我的做法是在 Vite 配置文件里设置开发代理:
server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } }这样前端开发时请求/api/xxx会被 Vite 代理转发到后端,浏览器看到的只有同源的/api/xxx,没有跨域问题。
生产环境则是 Nginx 配置反向代理,把/api/转发给后端,同样不需要后端处理 CORS。但如果你没有用代理而是直接让前端请求后端域名,那后端就必须处理跨域。我在 thinkphp 里写了一个全局中间件,设置允许的跨域头部,代码如下:
public function handle($request, \Closure $next) { $origin = $request->header('Origin', '*'); header('Access-Control-Allow-Origin: ' . $origin); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With'); if ($request->method() === 'OPTIONS') { return response('', 204); } return $next($request); }鉴权我用的是 JWT。用户登录时后端生成 token,前端把 token 存在 localStorage 里,每次请求通过 axios 拦截器加到请求头。遇到 token 过期时,axios 响应拦截器统一跳转到登录页并清除本地登录状态。
5.2 文件上传配置与体检报告管理
系统里有两类文件上传:一是体检报告 PDF 或图片,二是老人证件的照片。前端用的是 Element Plus 的el-upload组件,后端在 thinkphp 里用上传文件类来处理。
前端上传时要设置action为后端接收地址,同时通过 Headers 带上 token。上传成功后的响应格式要走统一的 JSON 格式,否则el-upload会把非预期格式解析为失败。
后端要改两个 PHP 配置项,这两个项经常被新手忽略:
upload_max_filesize = 50M post_max_size = 50M实测下来,很多“上传文件过大”的问题不是 Java 管也不是 Vue 管,而是 PHP 默认upload_max_filesize=2M卡住了。另外,体检报告经常是好几个文件,前端用el-upload的multiple属性开启多文件上传,后端循环处理文件数组即可。
上传的文件路径不要直接存相对路径,我建议在配置里定义一个不包含文件名的绝对路径常量,数据库里只保存相对于站点根目录的路径。这样以后迁服务器,只要把整个项目目录拷过去,文件路径不会出问题。
5.3 前端路由模式与 Nginx 配置
Vue 路由默认在开发时用 hash 模式,地址栏带#。但管理后台这种系统,用 hash 模式刷新页面没问题,就是地址不太好看。为了体验,我用了 history 模式,让路由更干净。
history 模式有一个绕不开的坑:用户直接访问某个子路由(比如/senior/list)时,Nginx 找不到这个文件,返回404。解决办法是在 Nginx 配置中做回退:
location / { try_files $uri $uri/ /index.html; }这句配置的意思是:如果请求的文件或目录不存在,就回退到index.html,由 Vue Router 接管路由解析。这个配置不写,history 模式一上线就会一堆404,写完之后就正常了。
thinkphp 的部署目录我单独指到public/目录,其他目录不允许被 Web 服务器直接访问。入口文件是public/index.php,Nginx 的 fastcgi 配置把.php请求交给 PHP-FPM 处理。这里还有一个容易出问题的点:如果用 thinkphp 多应用模式,URL 里的admin应用路径也要能被正确解析,伪静态规则要配好。
5.4 安全加固与日常维护
做医疗健康相关的系统,数据保密是底线,我在上线前专门做了四件事:
第一,数据库账号用最小权限的独立账号,不直接用 root。第二,管理后台的登录密码必须走加密存储,我用 thinkphp 自带的password_hash,登录验证时用password_verify。第三,所有涉及老人信息的接口返回前做字段脱敏,手机号中间四位打星号,身份证号保留前三位和后四位,只有医生端接口才返回完整信息。第四,thinkphp 的debug模式在生产环境要关闭,防止报错页面泄露服务器路径信息。
日常运维上,我建议机构每星期手动备份一次数据库,备份文件放到项目目录之外的位置。对于高并发需求,这个系统其实没有太大压力,但数据不能丢,老人一年的体检记录是全年的心血。
另外,机构值班室如果有大屏展示需求,可以把 vue 项目打包后用 Electron 封装成一个桌面客户端,直接开机自启全屏打开统计看板,比开浏览器再输地址要方便得多。这也算给后续扩展留了一条路。
6. 常见问题速查与实测经验
6.1 问题速查表
开发过程中实际遇到的问题我整理成了一张表,按出现的频率排序,方便后来人直接对照排查。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 前端请求接口报404 | Vite代理配置没生效或后端路由伪静态没配 | 检查vite.config.js的 proxy 路径,检查 Nginx location 规则和 thinkphp 伪静态 |
| 跨域报错 | 生产环境没配反向代理且后端 CORS 中间件缺失 | 后端加全局 CORS 中间件,OPTIONS预检直接返回204 |
| 上传文件失败或超时 | PHP 的upload_max_filesize和post_max_size太小 | 修改 php.ini 对应配置,重启 PHP-FPM |
| 前端登录后刷新页面白屏 | history 模式没有配置try_files回退 | Nginx 增加try_files $uri $uri/ /index.html; |
| 列表页分页数据不对 | 后端以此为page参数,前端传的页码字段不一致 | 统一接口参数名,前后端约定字段名为page和limit |
| 保存病历后处方丢失 | 没用事务,主表插入成功但明细表失败 | 用 thinkphp 事务将病历表和处方表插入包在同一事务中 |
| 统计图表某个月份消失 | SQL 结果里没有该月数据,前端未补零 | 前端按最近12个月生成月份数组,循环补齐数据 |
| 身份证号输入框无法输入X | 用了type="number"输入框 | 改为type="text"配maxlength="18" |
| 上传文件后前端拿不到返回地址 | 后端返回数据格式与前端el-upload期望不一致 | 统一返回{code, msg, data: {url}}格式,前端on-success里解析 |
| 视图按钮点击无反应 | Vue 路由跳转时写错了路径,或者组件没注册 | 用 vue devtools 查看路由匹配,检查component引入路径 |
这里再补充一个 Vue 组合式 API 的开发习惯:列表页的加载状态、分页参数、查询条件这三样用reactive包裹成一个对象,请求函数独立到src/api/模块中。这样每个页面都是同样的套路,团队协作时接手成本低,出 bug 也容易定位。
6.2 实际运行中的几点体会
项目上线至今稳定运行了小半年,我自己复盘下来有几个体会想分享。
先说数据库设计的部分:当初在体检记录表里预留了很多指标字段,实际使用中发现机构每年的体检项目并不完全固定,有的年份加了骨密度,有的年份加了尿酸。遇到这种需求,我是在指标字段之外加了一个可扩展字段extra_data,用 JSON 格式存自定义指标,前端动态渲染卡片。后期扩展确实比改表结构省事,但要控制好复杂度,简单场景用固定字段还是最稳的。
再说权限控制的粒度:最初我做了非常细的角色权限,后来发现机构的实际分工没那么复杂,角色权限太细反而导致医生和护士互相看不到数据,增加了沟通成本。后来调整成了“医生能看全部健康数据,护士只能看和操作体检与随访模块,管理员只看统计”,这个粒度在实际使用中是最顺手的。
最后是关于家属端的建议:我最初想给家属做个独立的小程序,后来评估工作量后先用 H5 页面实现了核心功能,因为机构给家属发个链接就能看,不需要扫码注册小程序。如果后续家属使用量大,再考虑用同一套接口封装成小程序或 App。技术选型不一定要追逐最新最重,满足业务场景才是第一位的。