☰
基于ThinkPHP+Vue的老年人健康检查就诊管理系统开发实战
2026/10/2 15:05:47 网站建设 项目流程

在接“老年人健康检查就诊就医管理系统”这类项目之前,我手头其实已经堆了好几个thinkphp+vue的前后端分离项目。但真正把健康体检、门诊就诊、慢病随访这几个业务流程揉进一套系统里,还是第一次。做完之后回头看,这个项目的难点不在于thinkphp怎么写接口,也不在于vue怎么画页面,而在于怎么把医疗场景里那些琐碎、敏感、偶尔还互相冲突的需求,转化成一套医生愿意用、家属看得懂、老人不抵触的系统。

这篇文章不打算写成标准文档,而是按我实际开发时的思路来拆:为什么选中thinkphp+vue这套组合、业务模块怎么切、后端接口怎么设计、前端页面怎么组织、以及最后联调部署时踩过的那些坑。如果你正打算做类似的医疗管理系统,或者想参考thinkphp+vue做前后端分离项目,这篇应该能帮你省下不少试错时间。

1. 为什么是thinkphp+vue:这套组合到底解决了什么问题

先说实话,医疗类管理系统用thinkphp+vue,在技术圈里不算“高级”。但项目能落地、能维护、能让客户满意,靠的是选型跟业务匹配。我接这个项目时,客户方的要求很明确:系统要跑在他们自己的服务器上,预算有限,开发周期三个月左右,使用人群包括医生、护士、老人家属、还有老人本人(虽然老人大多只看看报告)。这些约束直接决定了技术选型。

1.1 thinkphp在后端的定位:快速搭建业务逻辑的骨架

thinkphp在国内PHP圈子里占有率一直很高,原因就一个字:快。它的Model层、验证器、中间件、路由这些机制,能让我把大部分精力放在业务逻辑上,而不是重复造轮子。特别是对于健康检查这种业务——体检项目的增删改查、就诊记录的关联查询、报告文件的存储路径管理——thinkphp的Db类和模型关联用起来非常顺手。

我之前也犹豫过要不要用Java SpringBoot,但后来算了一笔账:这个项目的并发量峰值也就几十个人同时在线,数据库读写压力很小,PHP完全扛得住。而且服务器是客户自己买的低配云主机,PHP环境部署比Java简单太多。thinkphp自带的多应用模式,还能把后台管理和前台查询拆成两个应用,互不干扰。

1.2 vue在前端的价值:体检报告这种数据密集页面必须组件化

老年人健康管理系统里,最核心的页面就是体检报告展示和健康趋势图表。这类页面有一个共同特点:数据字段多、展示形式杂、状态切换频繁。如果还用传统的服务端模板渲染,每次筛选条件变化都得刷新整个页面,体验很差。

vue的价值在于组件化。我可以把“体检项目列表”做成一个组件,把“血压血糖趋势图”做成另一个组件,把“就诊记录时间线”再做成一个组件。页面之间通过路由切换,数据通过接口获取,前端只负责渲染和交互。而且vue对新手友好,就算后续客户要求改样式,找个会vue的人也能快速上手。

1.3 前后端分离对这个项目的实际意义

有一点我必须说清楚:前后端分离不是为了炫技,而是为了应对这个项目的真实需求。比如家属可能用手机浏览器查看老人的体检报告,医生在电脑端录入就诊信息,管理员在后台维护系统参数。如果前后端不分离,意味着每个终端都要写一套服务端渲染逻辑;分离之后,后端只需要提供一套JSON接口,前端可以做电脑端网页、手机端H5,甚至以后打包成小程序都能共用。

不过分离也带来了两个必须解决的问题:跨域和鉴权。后面我会详细讲thinkphp和vue之间怎么处理跨域,以及怎么用token机制保证接口安全。

2. 业务模块的重新划分:体检、就诊、用药提醒谁先谁后

拿到需求时,客户给的原话是“要一套能管体检、管看病、管吃药的系统”。听起来简单,但仔细一捋,这三个“管”背后的业务流程完全不同。如果一开始就把所有功能揉在一起开发,后期必然返工。我实际做的第一件事,是把业务拆成四个独立模块,每个模块内部再细分子功能。

2.1 老人健康档案:所有业务的根,必须优先建好

不管是体检记录、就诊记录还是用药提醒,最终都要挂到具体的老人身上。所以健康档案是第一个要做的模块。这里的“档案”不仅仅是姓名、年龄、身份证号这些基础字段,还包括:

  • 既往病史(高血压、糖尿病、冠心病等,用多选标签存储)
  • 过敏史(特别注意药物过敏,要支持文本说明)
  • 紧急联系人(至少两个人,方便突发情况联系)
  • 常用药物清单(后续用药提醒模块直接从这里读取)

数据库设计上,我单独建了一张elderly_info表,主键是老人ID,所有业务表都通过elderly_id关联。这样查一个老人的完整健康记录时,只需要按这个ID做关联查询,效率很高。

2.2 健康检查模块:不只是记一个结果,要管完整流程

体检这个业务,从登记到出报告,中间要经过预约、检查、录入结果、审核、家属查看五个环节。开发时我把它拆成了两张大表:

  • health_check:存体检单的总体信息,比如体检日期、体检类型(年度体检/入住院体检)、体检状态(待检查/检查中/已完成/已审核)、主检医生ID。
  • check_item_result:存具体每一项的检查结果,比如血常规的白细胞计数、尿常规的尿蛋白、心电图的心率。每条结果关联health_check_id,同时记录检查项目名称、结果数值、参考范围、异常标记。

这里有个细节要注意:同一项检查结果,对于不同年龄段的老人,参考范围其实是不一样的。所以我在表里加了一个ref_range字段,直接存储本次检查实际使用的参考范围,避免以后参考标准变了导致历史数据无法解释。

2.3 就诊就医模块:挂号、看诊、开药的状态流转

就诊模块的核心不是“记录一次看病”,而是管理“一次就诊从开始到结束的状态”。我设计的状态流转是这样的:

  • 挂号(状态:待就诊)
  • 医生叫号(状态:就诊中)
  • 医生完成诊断并开药(状态:已完成)
  • 如果医生建议复查(状态:待复诊)

这个状态流转用数据库字段visit_status表示,用整型数字存储(1待就诊,2就诊中,3已完成,4待复诊),比字符串更省空间也更方便比较。前端vue页面根据这个状态值渲染不同的按钮和提示。

开药这一步单独拆了一张prescription表,关联就诊ID和药品ID,同时记录用法用量和用药天数。因为老年人往往同时患多种慢性病,吃多种药,开药记录必须精确到“每天几次、每次几片”。

2.4 用药提醒与家属通知:这个功能看着小,实际最受欢迎

客户最初的需求说明里并没有用药提醒,是我在梳理业务流程时主动加的。原因很简单:老年人忘服药是常态,尤其子女不在身边的独居老人。我设计了一个简单的提醒逻辑——根据prescription表里的用法和天数,自动生成每天几条用药提醒记录,存到medication_reminder表里。

提醒方式不搞花哨,就两种:系统站内信和短信接口。站内信免费,但老人不一定看;短信要钱,但能确保提醒触达家属。我默认设置短信只发给紧急联系人,老人本人的登录账号只发站内信。这个设计后来客户非常认可,因为对子女来说,“我爸今天按时吃药了没”比任何体检报告都有感知。

2.5 数据表关系:理解这几张主表,整个项目就通了一半

用markdown表格把这几个核心表的关系列一下,方便后面开发时对照:

数据表核心字段关联关系
elderly_infoelderly_id, name, age, history, allergy主表,被所有业务表关联
health_checkcheck_id, elderly_id, check_date, main_doctor_id, status一对多关联check_item_result
check_item_resultresult_id, check_id, item_name, result_value, ref_range, is_abnormal多对一关联health_check
visit_recordvisit_id, elderly_id, doctor_id, visit_date, visit_status关联prescription
prescriptionprescription_id, visit_id, drug_id, usage_dosage, days多对一关联visit_record
medication_reminderreminder_id, elderly_id, reminder_time, status关联elderly_info

这六张表是核心骨架,再加上管理员表、医生表、药品字典表、通知记录表,整个系统的数据模型就完整了。

3. thinkphp后端:接口设计顺序与鉴权机制

后端开发这部分,我按“先搭基础能力,再填业务接口”的顺序来做。基础能力指的是统一的返回格式、异常处理、跨域配置和用户鉴权;业务接口再按前面提到的四个模块一个一个填。

3.1 接口返回格式统一:前端联调省一半时间

前后端分离项目最怕接口格式不统一。我定的标准返回格式是:

{ "code": 200, "message": "success", "data": { "list": [], "total": 0, "page": 1, "limit": 10 } }

在thinkphp里,我封装了一个ApiResponse类,所有控制器统一调用它的静态方法返回。这样做的好处是,前端axios拦截器只需要判断code === 200就正常处理,否则统一弹错误提示,不需要每个接口单独写判断逻辑。

3.2 跨域配置:thinkphp端只要这几行

前后端分离必然碰到跨域。我在thinkphp的中间件里加了跨域处理,核心代码如下:

public function handle(Request $request, \Closure $next) { $origin = $request->header('Origin', '*'); $allowOrigin = [ 'http://localhost:8080', 'http://127.0.0.1:8080', 'https://admin.yourdomain.com', ]; if (in_array($origin, $allowOrigin)) { 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: Content-Type, Authorization, X-Requested-With'); } if ($request->method() == 'OPTIONS') { return response('', 204); } return $next($request); }

注意这里的$allowOrigin数组,实际生产环境我建议改成从数据库或配置文件中读取,不要写死在代码里。另外OPTIONS预检请求必须直接返回204,不然浏览器跨域请求会卡在预检阶段,前端console里报错你会一脸懵。

3.3 基于jwt的登录鉴权:医生、家属、管理员三种角色

这个系统的用户分三类:管理员(系统配置)、医生(录入就诊信息)、家属(查看老人档案)。我在登录接口里用thinkphp的JWT扩展生成token,token里存入user_id和role字段,后续所有请求在header里带Authorization: Bearer <token>。

关键点是中间件里做角色权限校验。我写了一个CheckRole中间件:

public function handle(Request $request, \Closure $next, $roles) { $auth = $request->user; // 通过JWT解析出来的用户信息 $allowed = explode(',', $roles); if (!in_array($auth['role'], $allowed)) { return json(['code' => 403, 'message' => '无权访问'], 403); } return $next($request); }

路由定义时直接声明角色的访问范围,比如:

Route::group('elderly', function () { Route::get('info/:id', 'ElderlyController/getElderlyInfo'); Route::post('add', 'ElderlyController/addElderly'); })->middleware('CheckRole:admin,doctor,family');

这样既保证了接口安全,又避免了在每个控制器里重复写权限判断代码。

3.4 健康检查接口:一次体检记录的写入与更新

体检报告的数据录入是后端开发里最繁琐的部分,因为一张体检单对应几十项结果。我的设计思路是:用事务保证主表和子表的数据一致性,先插入health_check主记录,拿到自增ID后再循环插入check_item_result子记录。

核心代码片段:

public function submitCheckResult(Request $request) { $checkId = $request->post('check_id'); $items = $request->post('items'); // 二维数组:[{item_name, result_value, ref_range, is_abnormal}] Db::startTrans(); try { foreach ($items as $item) { Db::name('check_item_result')->insert([ 'check_id' => $checkId, 'item_name' => $item['item_name'], 'result_value' => $item['result_value'], 'ref_range' => $item['ref_range'], 'is_abnormal' => $item['is_abnormal'], ]); } // 更新主表状态 Db::name('health_check')->where('check_id', $checkId)->update([ 'status' => 3, // 已完成 'check_time' => time(), ]); Db::commit(); return ApiResponse::success(); } catch (\Exception $e) { Db::rollback(); return ApiResponse::error('保存失败:' . $e->getMessage()); } }

这里有个实际经验:体检项目虽然多,但不同体检类型的项目列表是相对固定的。我在前端体检类型管理里维护了一套“体检套餐模板”,录报告时直接从模板带出项目列表,医生只需要填数值,不需要手工逐行添加项目。这个设计极大提高了录入效率,也是客户后来最满意的点之一。

3.5 就诊状态流转接口:用数据库事务保证不“卡单”

就诊状态流转看着简单,但实际开发时踩过一个坑:如果两个操作同时提交,比如医生一边点“完成就诊”,一边又开了新药,就会造成状态不一致。解决办法是在更新就诊状态时加条件判断,只有当前状态等于期望状态时才能更新:

$result = Db::name('visit_record') ->where('visit_id', $visitId) ->where('visit_status', 1) // 只有待就诊状态才能改成就诊中 ->update(['visit_status' => 2]); if (!$result) { return ApiResponse::error('当前状态不可变更,请刷新后重试'); }

这种乐观锁的思路在状态机流转里非常实用,不用锁数据库表,也避免了并发问题。

4. vue前端:路由、权限、核心页面一次讲透

前端这部分的工作量不比后端小。vue要做的事情包括:路由搭建、登录状态管理、页面组件开发、接口调用封装、以及图表的展示。我按照“基础设施—核心页面—细节优化”来组织。

4.1 前端项目结构:别把所有组件都堆在views里

我用的是vue3 + vue-router + pinia的组合。项目结构按模块分目录,而不是按文件类型分:

src/ ├── api/ // 接口请求封装,一个模块一个文件 ├── views/ │ ├── check/ // 体检模块页面 │ ├── visit/ // 就诊模块页面 │ ├── elderly/ // 老人档案页面 │ └── dashboard/ // 首页仪表盘 ├── router/ │ ├── index.js │ └── routes.js ├── components/ // 通用组件 └── utils/ ├── request.js └── auth.js

这种按业务模块划分的方式,好处是后续新增功能时,开发和维护人员能快速定位代码。很多前端项目后期变成“屎山”,往往就是目录结构混乱导致的。

4.2 路由守卫与动态菜单:不同角色看到不同页面

由于用户角色不同,前端不能把所有菜单都显示出来。我用的方案是:登录成功后,前端根据用户角色去请求后端的/user/menus接口,拿到该角色可见的路由列表,然后通过router.addRoute()动态添加路由。

路由守卫的关键逻辑:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (!token && to.path !== '/login') { next('/login'); return; } if (token && !store.userInfo) { // 还没获取用户信息,先拉取用户信息和菜单 store.fetchUserInfo().then(() => { const routes = store.generateRoutes(); routes.forEach(route => router.addRoute(route)); next({ ...to, replace: true }); }); return; } next(); });

这里有个细节要提醒:动态添加路由后,如果直接next(),第一次会白屏,因为目标路由还没注册。我在next里传入{...to, replace: true},强制重新导航一次,路由匹配就正常了。

4.3 体检报告页面:表格、异常项高亮、参考范围对照

体检报告是前端最重要的页面。我的实现思路是:用el-table展示所有体检项目,每一行包括项目名称、结果、参考范围、异常标记。异常项的重点标记是必不可少的,因为医生和家属不可能逐行比对参考范围。

判断异常的逻辑放在后端返回数据时就算好,前端只根据is_abnormal字段渲染样式:

const rowClassName = ({ row }) => { if (row.is_abnormal) return 'abnormal-row'; return ''; };

同时加了一个筛选功能,家属可以只勾选“只看异常项”,这样几十项检查结果里,有问题的项目一眼就能挑出来。这个交互看起来简单,但在实际使用中,很多老人家属根本看不懂正常值范围,异常高亮帮他们省了大事。

4.4 健康趋势图表:用echarts展示血压血糖的长期变化

体检不是一次性的,需要看趋势。我引入了echarts画老人的血压和血糖趋势折线图。实现思路是:前端调用接口/health-check/trend?elderly_id=xxx&item_name=血压,后端返回该老人最近10次体检中血压的收缩压和舒张压数值。

echarts部分的核心配置:

const option = { title: { text: '血压变化趋势' }, tooltip: { trigger: 'axis' }, legend: { data: ['收缩压', '舒张压'] }, xAxis: { type: 'category', data: dates }, yAxis: { type: 'value', min: 60, max: 180 }, series: [ { name: '收缩压', type: 'line', data: systolicList, smooth: true }, { name: '舒张压', type: 'line', data: diastolicList, smooth: true }, ], };

为了让图表更直观,我加了两条警戒线,一条是正常范围上限,一条是下限。这样家属一眼就能看出老人的血压是否长期偏高。这个功能开发难度不高,但客户反馈说“终于能看懂我爸的血压到底稳不稳了”。

4.5 就诊流程页:状态管理的根本在于操作按钮的禁用逻辑

就诊流程页的用户是医生。页面按就诊状态显示不同的操作区:待就诊时显示“开始叫号”按钮,就诊中显示“完成诊断并开药”按钮,完成之后只能查看详情不能修改。

这个逻辑我在前端用computed根据visitStatus计算按钮的可点击状态:

const canStart = computed(() => props.visitStatus === 1); const canFinish = computed(() => props.visitStatus === 2 && !props.isFinished);

再配合后端状态校验,双重保障防止误操作。这种状态管理模式,对于任何多步骤业务流程页面都适用。

4.6 token过期与拦截器:前端必须处理的边界问题

axios请求封装里,我统一做了三件事:

  • 请求拦截器:给每个请求header带上Authorization token
  • 响应拦截器:判断HTTP状态码,401跳转登录页
  • 业务码判断:code === 401时清除本地token并跳转登录页

实际开发中,最常见的问题是token过期后,多个接口同时返回401,导致前端连续跳转多次登录页。我的处理方式是在拦截器里加一个isRedirecting标志位:

let isRedirecting = false; service.interceptors.response.use( (response) => { if (response.data.code === 401) { if (!isRedirecting) { isRedirecting = true; router.push('/login'); } return Promise.reject(new Error('登录已过期')); } return response.data; }, (error) => { if (error.response?.status === 401 && !isRedirecting) { isRedirecting = true; router.push('/login'); } return Promise.reject(error); } );

别小看这个细节,不加的话,用户token过期时会看到页面疯狂跳转,体验极差。

5. 联调部署阶段的实战经验:从本地跑通到服务器上线

项目开发到联调阶段,才算真正开始考验人。这一部分内容本地开发时不容易遇到,但上线了全是问题。

5.1 本地环境与服务器环境的差异处理

本地开发时,thinkphp跑在php think run启动的开发服务器上,vue跑在vite或webpack的dev server上,两者通过跨域请求沟通。但到生产环境部署时,有两个方案:

  • 方案A:npm run build生成dist静态文件,然后配置nginx把接口请求/api反向代理到thinkphp的PHP服务
  • 方案B:vue和thinkphp完全分离,放在不同域名,继续用跨域

我实际推荐方案A。原因很简单:虽然开发时是前后端分离,但生产环境把前端和后端放在同一个域下,不仅不需要处理跨域,还能减少一次网络往返。nginx配置大致如下:

location /api/ { proxy_pass http://127.0.0.1:9000/; # thinkphp的fastcgi监听地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /health/ { proxy_pass http://127.0.0.1:9000/health/; } location / { root /var/www/html/dist/; index index.html; try_files $uri $uri/ /index.html; }

需要注意:thinkphp的路由规则里如果定义了/health前缀的接口,nginx里必须单独配置反向代理,不能把/下的所有请求都指向dist目录,否则页面能打开、接口全404。

5.2 体检报告附件存储:路径必须存相对路径

体检报告有时需要上传体检报告pdf或检查图片。我在thinkphp里用think\facade\Filesystem处理上传,但存储路径必须值得特别小心。

我踩过的一个坑是:本地开发时,文件上传到public/uploads/然后通过绝对路径访问,一切正常;但部署到服务器后,项目放在/var/www/html目录下,文件明明存在,页面却加载不到图片。排查后发现是路径拼接问题。

最后我的统一方案是:数据库里存相对路径/uploads/2024/05/xxxx.jpg,前端拼接时带上服务器的域名前缀。这样不管本地还是生产,只要域名配置对,图片就能正常显示。

5.3 老年用户的使用习惯:界面设计要“少一步”

这个系统虽然主要使用者是医生和家属,但老人本人也会通过登录账号查看自己的体检报告。针对老人群体,我做了两个设计:

第一,所有列表页的内容字体默认不小于16px,重要数据(血压、血糖数值)用24px以上大字号突出显示。第二,操作按钮尽量少而明确,比如“预约体检”按钮只有一个,“查看报告”也只有一个,不做多级下拉菜单。

这一点如果客户没有明确要求,很多开发会忽略。但站在实际场景想,老人的视力和操作精度不如年轻人,界面设计必须做减法。

5.4 数据备份与隐私:老年人健康信息不是普通数据

最后必须强调,健康档案属于高度敏感的个人隐私信息。即使项目体量不大,我也做了三个基本防护:

  • 数据库定期自动备份到异地存储,防止服务器磁盘损坏导致数据丢失
  • 所有涉及健康数据的接口都要求登录后访问,并且后端有操作日志记录谁在什么时间查看了哪些老人的档案
  • 前端页面在非登录状态下不缓存任何健康数据,避免浏览器缓存或历史记录泄露

这个系统上线后,我收到了不少正向反馈,尤其是用药提醒和血压趋势图这两个功能,家属群里讨论得很热烈。回头再想,技术本身没什么特别的,thinkphp和vue都是“老朋友”了,真正值钱的是对业务流程的理解和对细节的较真。一个管理系统能不能真正用起来,拼的往往就是这些细节功夫。

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

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

立即咨询