医院里最容易被忽视、但又最耽误事的,往往是设备报修这件“小事”。一台心电监护仪坏了,临床科室打电话找设备科,设备科再电话联系工程师,工程师来了发现缺配件,又得回去申请……信息全靠口头传递,流程全靠人肉追踪,最后谁也不知道工单卡在哪个环节。我这次做的这套基于微信小程序的医院医疗设备报修管理系统,就是用Thinkphp和Laravel分别实现了后端接口,前端跑在微信小程序上,把报修、派单、维修、验收整个闭环搬到线上。这篇文章会把我在这个项目里的技术选型、数据库设计、小程序端的关键实现、接口联调以及踩过的坑完整梳理一遍,给正在做类似系统的人一个可以照着落地的参考。
1. 项目背景:医院设备报修为什么值得单独做一个系统
1.1 报修流程的痛点在哪里
在动手写代码之前,我专门去医院设备科蹲了半天,把他们的报修流程摸了一遍。以前的方式基本是这样的:科室护士发现设备异常,手写一张报修单送到设备科,或者干脆打电话口述;设备科接电话后,记在一个Excel表里,然后电话联系维修工程师;工程师到场维修,修好了在纸上签字,修不好再走申请外修流程。整个过程的问题非常典型,纸质单据容易丢失,Excel表记录不规范,设备科没法实时掌握维修进度,临床科室也没法查询“我的设备到底修到哪一步了”。
这些痛点的本质是信息不对称。科室、设备科、工程师三方各自掌握一部分信息,但没有一个统一的信息流把它们串起来。系统要解决的,就是让每一台设备的报修状态变成一个可查询、可跟踪、可统计的数字化工单,而不是一条条躺在微信聊天记录里的语音和截图。
1.2 为什么选微信小程序而不是App或H5
医院这个场景有个特点,使用者覆盖护士、医生、设备科管理员、维修工程师,年龄跨度大,手机操作水平参差不齐。如果让所有人装一个App,推广成本和适配成本都太高;如果做H5,又存在入口太深、消息触达能力弱的问题。微信小程序天然解决了这两个矛盾:用户不需要安装,微信里扫一扫或者搜一搜就能打开;同时微信自带的订阅消息能力,可以用来通知“工单已派发”“维修完成待验收”。
另外,医院内网WiFi覆盖情况通常一般,小程序对弱网环境的容忍度比H5要好一些,加载完页面框架后,大部分数据交互都是轻量的接口请求,体验上接近原生。最核心的一点,小程序的登录体系可以直接复用微信授权,免去了手机号验证码注册那套流程,对年纪稍大的使用者更友好。
1.3 系统面向的角色与权限划分
整个系统的用户角色,我一开始设计的时候就分成四类:报修人(临床科室人员)、维修工程师、设备科管理员、系统超级管理员。报修人负责提交报修、查看进度、确认修复;工程师负责接单、填写维修记录、标记完成;设备科管理员负责审核、派单、统计报表;超级管理员负责设备档案、用户账号和系统配置。
权限这块,我前后端都做了控制,后端接口用中间件校验角色,小程序端也根据不同角色动态渲染菜单和操作按钮。后面我会细说接口鉴权怎么设计,这里先记住一个原则,前端隐藏按钮只是体验优化,真正的权限边界必须落在后端接口上,否则随便一个人拿着接口地址就能越权操作,这在医院系统里是绝对不允许的。
2. 框架之争:Thinkphp与Laravel如何选
2.1 两个框架的差异对比
这个项目我实现了两版后端,一版用Thinkphp 8,一版用Laravel 10,目的就是对比一下两个框架在这种业务场景下的真实表现。很多人纠结选型,其实只要搞清楚它们的核心差异,选起来并不难。
Thinkphp的优势是轻量、上手快、中文文档全,特别适合中小型团队和快速交付。它内置的模型关联、验证器、中间件这些核心功能都有,但生态相对封闭,第三方扩展的丰富度和社区活跃度不如Laravel。Laravel的优势在于设计理念先进,服务容器、门面模式、Eloquent ORM、事件系统这些架构层面的东西,让代码的组织方式非常规范,适合中大型项目和长期维护。
性能方面,在实际压测中Thinkphp的裸接口响应略快一点,但这个差距在医院的业务量级下基本可以忽略。医院设备报修系统不是高并发场景,一天撑死几百个工单,真正的瓶颈在业务逻辑的清晰度和后续扩展的便利性,而非框架的执行效率。
2.2 我的选择与理由
我最终把这一版交付的代码跑在Thinkphp上,但这不代表Laravel不行。选择Thinkphp的原因很实际:这套系统后续大概率要交给医院信息科或第三方维护团队二次开发,Thinkphp的中文生态和简单的目录结构对后来者更友好。如果你是个人开发者,或者团队里有熟悉Laravel的人,我反而建议用Laravel,因为它的迁移机制、队列、事件系统在这个项目里都能派上大用场。
我的建议是,做这类管理系统,选框架看三个维度:团队熟悉度、部署环境限制、后续维护力量。医院项目往往卡在服务器环境上,比如PHP版本、扩展组件、伪静态配置这些,两个框架对环境的要求不完全一样,选之前先用phpinfo()探一下服务器,别等代码写完了才发现扩展装不上。
2.3 框架基础配置与项目骨架
以我实际跑的Thinkphp 8版本为例,项目结构我是这样组织的:app/api是接口模块,按设备、工单、用户、消息分组;app/common放通用的模型、服务类和工具函数;数据库迁移文件单独建了一个目录。初始化阶段,我建议先把跨域配置、异常处理、日志写入这三件事做好,否则后面联调小程序的时候,光是跨域和报错定位就能耗掉不少时间。
// 跨域中间件示例,Thinkphp中注册为全局中间件 public function handle(Request $request, Closure $next) { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); if ($request->method() == 'OPTIONS') { return response('', 204); } return $next($request); }小程序端请求都在webview容器里发起,不存在浏览器同源策略问题,但开发时经常用浏览器直接调试接口,跨域配置不做好,调试效率会低很多。另外,异常处理一定要统一,我封装了一个响应类,所有接口返回固定格式:code、message、data,无论成功还是失败都是这个结构,小程序端解析逻辑就非常简单。
3. 数据建模与报修流程设计
3.1 设备档案与科室维度
设备是系统的核心对象,设备档案字段怎么设计,直接决定后面报修单的内容和统计维度。我把设备档案表设计成这些核心字段:设备编码、设备名称、设备型号、生产厂家、购入日期、保修截止日期、所在科室ID、设备状态、设备分类。设备编码我建议用自动生成的有规则编码,比如科室缩写加流水号,不要用自增ID作为对外展示的编码,因为自增ID会暴露系统数据量,也容易被遍历抓取。
科室维度一定要单独建表,而且要预留上级科室字段。医院的组织架构往往不是扁平的,有内科部、外科部这种大科,下面还分呼吸内科、消化内科。设备统计如果只做到二级科室,后期的报表会很难看。我在设计科室表时,用parent_id做自关联,支持树形结构,这样不管是按大科统计还是按具体科室统计,一条SQL都能搞定。
3.2 报修工单状态机怎么设计
工单是整个系统里流转最复杂的对象,我把它设计成这个状态流转过程:待审核 -> 已派单 -> 维修中 -> 待验收 -> 已完成,同时还有两个终点状态:已驳回、已关闭。报修人提交后,工单进入待审核;设备科管理员审核通过并指派工程师后,进入已派单;工程师点击开始维修,进入维修中;维修完成后提交维修结论,进入待验收;报修人确认设备恢复正常,工单变为已完成。
很多开发者在做状态流转时会不加约束,直接给状态字段塞一个值,这样时间一长数据就会乱掉。我在后端定义了一个常量类,把允许的状态迁移写成映射表,比如:只有待审核状态可以变成已派单或已驳回,只有维修中状态可以变成待验收,只有待验收状态可以变成已完成。每次状态变更都走同一个transition方法,非法跳转直接抛异常。这个设计让业务逻辑变得非常清晰,排查问题也容易。
3.3 报修单编号生成与并发处理
报修单编号看起来是个小事,实际上要做好不容易。如果直接用数据库自增ID,用户会看到一串连续数字,既不好记也不安全。我采用的方案是:日期加当天流水号,比如BX20240520001。这个编号需要保证并发情况下不重复,我用了一个独立的自增计数表,每次生成编号时先插入一条记录拿到自增ID,再拼接成业务编号,同时把当天的日期作为分组的key,每天从1开始重新计数。
这个方案虽然多了一次数据库写入,但在这个业务量级下完全够用,而且不会出现Redis不可用导致编号生成失败的风险。医院项目有时候会碰上断电、服务重启这种物理故障,纯Redis方案在极端情况下可能丢失计数,用数据库自增表反而更可靠。
CREATE TABLE repair_sequence ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, seq_date DATE NOT NULL, seq_no INT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_date (seq_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;生成编号的逻辑是:先查当天有没有记录,没有就插入一条seq_no为1的新记录;有就把seq_no加1更新,然后拼上日期和补齐零的流水号返回。因为seq_date有唯一索引,并发时只有一个请求能插入成功,其他请求会走更新分支,配合事务基本不会撞号。
3.4 消息触达设计
报修系统里最容易被低估的模块是消息通知。医院场景下,没有人会一直盯着小程序等状态变化,所以系统必须主动把状态变更推给相关人。小程序官方有订阅消息功能,但限制很严,一次性订阅只能推一次,长期订阅只对特定类目开放。我的做法是:核心的状态变更,比如派单成功、维修完成,用订阅消息推送;日常的通知,比如审核驳回、备注补充,通过短信或企业微信机器人提醒。
订阅消息在小程序端需要引导用户点击授权按钮,不能静默授权,我就在每次报修提交成功后弹出授权面板,提示“开启维修进度通知”,用户点了确认,后端就拿到一次模板推送机会。实测下来,订阅消息在微信端的触达率比公众号模板消息高不少,值得做。后端发订阅消息的代码,两个框架都封装了统一的服务类,传入模板ID、接收者openid、页面路径和表单数据即可。
4. 微信小程序端的关键实现
4.1 登录与手机号获取
微信小程序的登录链路是固定的:wx.login拿到code,然后传给后端,后端拿着code调微信的接口换openid和session_key。这里有一个坑,code是五分钟有效且只能用一次的,换完就废,所以前端一定要把code传好之后立刻请求,不能中途做其他操作。
医院系统通常需要绑定手机号,方便后续短信通知和账号关联。以前可以直接用获取手机号按钮组件一键拿手机号,但微信官方在2023年之后收紧了权限,现在必须企业认证的小程序才能开通这个能力,个人开发者拿不到接口权限。我采用的方案是分两步走:有权限的小程序用手机号快速填写组件,没有权限的就用手动输入手机号加短信验证码的备选方案,两种方式并存。
用手机号快速填写组件时,前端拿到的是加密手机号数据,需要传给后端解密。后端要在session_key有效的窗口期内用aes解密才有结果。session_key在微信服务器上保存时间是有限的,通常建议调用完就解,不要存库。
4.2 报修表单与图片上传
报修表单是整个小程序端交互最重的页面。我按医院实际使用习惯,把表单设计成:设备选择(扫一扫设备二维码或手动输入)、故障描述、故障图片、紧急程度、预约上门时间。故障图片这里支持多图上传,前端先调用wx.chooseMedia选图,再把每一张图片通过wx.uploadFile传给后端,后端保存到OSS或本地,返回URL。
图片上传我强烈建议加一个压缩环节,因为医院里拍的设备故障照片,相机原图动辄几兆,如果直接传原图,WIFI信号不好的情况下很容易超时失败。我在前端用canvas把图片压缩到最长边1280px,质量设置为80%,实测一张两三兆的照片能压到两百多KB,上传速度明显提升。压缩代码要放在chooseMedia之后、uploadFile之前,不能用官方自带的上传接口直接传原图。
故障描述我其实不建议完全开放自由输入,因为医护人员打字时间有限。我给了一些常见故障类型的快捷标签,比如“开机无反应”“显示异常”“无法充电”“按键失灵”,点击标签自动填入,也可以手动补充细节。这样既缩短了填报时间,又方便后端做故障类型统计。
4.3 工单跟踪与状态刷新
工单列表页我用的是下拉刷新加触底加载的方式,分页参数用page和pageSize,后端返回total和list。工单详情页的状态展示,我做成了一条可视化的时间轴:发起报修、审核派单、工程师接单、维修完成、验收通过,每个节点对应记录时间。状态数据从后端的工单日志表读取,每一次状态变更都落一条日志,这样用户能看到完整的时间线。
状态刷新这里有个技巧,不需要做成WebSocket实时推送。医院场景对实时性要求没那么高,用户在小程序里停留时间短,我在onShow生命周期里主动重新拉一次详情,配合下拉刷新就足够。如果做WebSocket推送,反而要考虑断线重连、token过期这些问题,得不偿失。
4.4 顶部导航栏高度与适配
小程序开发中一个经常被问到的点,就是顶部导航栏高度适配。自定义导航栏时,状态栏高度和胶囊按钮位置不同机型上不一样,不能写死。我封装了一个获取导航栏信息的工具函数:状态栏高度用wx.getWindowInfo()拿,胶囊按钮位置用wx.getMenuButtonBoundingClientRect()拿,然后计算出导航栏内容区的高度和padding。
const info = wx.getWindowInfo(); const menu = wx.getMenuButtonBoundingClientRect(); const navHeight = (menu.top - info.statusBarHeight) * 2 + menu.height;这段代码计算出来的navHeight,就是自定义导航栏的总高度,页面内容区域用这个值做padding-top,就能保证自定义按钮不会顶到状态栏或遮挡胶囊按钮。这个适配代码我直接放在app.js的全局方法里,每个页面都能调用,不用重复写。
5. 接口设计、权限校验与联调
5.1 接口规范与错误码设计
接口是否规范,直接决定前后端联调的效率。我在这套系统里把所有接口统一为REST风格,资源名用名词复数:/api/devices、/api/repairs、/api/users。请求方法对应动作:GET查详情或列表、POST创建、PUT更新、DELETE删除。每个接口返回固定JSON结构,code为0表示成功,非0表示业务异常,message给用户看,data放业务数据。
错误码一定要统一规划,不能想到哪写到哪。我按模块分段:1000开头是通用错误,2000开头是设备相关,3000开头是工单相关,4000开头是用户与权限相关,5000开头是文件上传相关。这样小程序端收到错误码后,既可以直接展示后端传的message,也可以针对特定错误码做定制处理。
{ "code": 0, "message": "success", "data": { "repairId": 10086, "status": "pending_review" } }后端封装好这个统一响应结构之后,控制器里的代码就非常干净。我写了一个BaseController,里面放success($data)和error($message, $code)两个方法,所有子控制器继承后直接调用,避免每个接口都手动拼数组。
5.2 登录态鉴权方案
因为微信小程序没有Cookie机制,登录态必须靠token维护。我在用户登录成功之后,签发一个token,小程序端放在请求头Authorization字段里携带。token的有效期我设置为7天,用户每天打开小程序时会自动调用刷新接口,续期7天,这样低频使用也不会频繁掉登录。
后端中间件的鉴权逻辑是:读取Authorization头,从Redis中取token对应的用户ID。如果取不到就返回401,小程序端收到401后统一跳转登录页。Redis的key设计为token:用户ID,value存用户信息JSON,过期时间7天。退出登录时删除Redis key,token立刻失效。
权限校验这里,我用了中间件加注解的组合。分类角色校验不是写在一个中间件里判断所有接口,而是针对不同接口分组注册不同中间件,比如AdminMiddleware只允许管理员访问,RepairMiddleware允许报修人和管理员访问。这样代码清晰,以后加新接口也不会忘记权限控制。
5.3 联调阶段的一些坑
联调时最大的坑是小程序开发者工具里的“不校验合法域名”开关。开发阶段可以勾选关闭,但真机预览和发布正式版时,后端域名必须在小程序后台配置为request合法域名,而且必须是HTTPS且备案过的域名。医院项目尤其要注意,域名备案信息必须和医院主体一致,否则审核过不了。
另一个坑是请求超时时间。默认小程序的request超时时间是60秒,但部分云服务商在HTTPS握手阶段耗时较长,特别是第一次请求。我的后端在nginx层做了优化:开启HTTP/2、配置SSL会话缓存、调整keepalive参数。实测首请求响应时间从两秒多降到了五百毫秒以内,体验提升非常明显。
还有就是POST请求的Content-Type问题。小程序默认的POST请求头是application/json,后端接收参数时如果用了传统的方式接收表单格式,就会取不到值。Thinkphp里我用json接收,Laravel里直接request()->all()就能同时处理两种格式,这些细节在联调前就要和后端确认清楚。
6. 常见问题与排查技巧实录
6.1 图片上传失败如何处理
图片上传失败是我在测试阶段遇到最多的问题。现象是wx.uploadFile偶尔返回fail,尤其在医院现场网络环境差的时候。排查后原因有两个:一是图片没压缩导致文件过大,二是上传接口没有设置合理的超时时间。
解决办法是:前端压缩后限制单张图片不超过500KB,同时监听上传进度,超过30秒主动终止重试;后端接口把php的upload_max_filesize和post_max_size调大,nginx的client_max_body_size也要改,两端都要放开限制。还有一个非常容易被忽略的点,云存储的临时URL过期时间要设长一点,否则用户隔天再查看工单详情时,图片已经无法访问了。
6.2 微信登录code失效
code失效是另一个高频问题。我遇到过用户小程序切后台超过五分钟再切回来,此时code可能已经过期;也遇到过同一段逻辑里重复调用wx.login,第二次拿到的code覆盖了第一次,导致第一次的code作废。
我在前端做了一个约束:wx.login只在App启动时调用一次,登录接口如果失败,不是马上重新login,而是先判断失败原因。如果是网络错误就重试,如果是code无效就重新调用wx.login。后端也做了一层保护,用code换openid失败时记日志,方便定位频率和数据。
6.3 工单状态与列表不同步
工单详情和列表状态不一致,这个问题的根源是列表页只拉取了一次数据,之后状态变化没有同步更新。我处理的方式是:列表页下拉刷新时重新请求全部数据,详情页的每次状态变更操作成功之后,把最新的工单对象返回给前端,前端直接覆盖本地数据,而不只是返回一个操作成功的空壳。
另外,状态变更时间我统一以后端服务器时间为准,不信任手机本地时间。原因是有用户的手机时间不标准,如果前端用new Date()生成时间戳,就会出现操作时间在未来的诡异情况。后端在状态变更日志里用数据库的CURRENT_TIMESTAMP。
6.4 小程序包体积超限
小程序上传时有主包2MB、总包20MB的限制。我开发中期的包体曾经超过主包限制,排查下来大部分体积来自UI组件库和图片资源。图片资源很好处理,全部切到OSS;UI组件库我用的是按需引入模式,只引入项目用到的组件,不整库引入。
这里分享一个排查技巧:开发者工具里的详情面板可以实时查看包体大小构成,包括每个目录占用的空间,非常直观。代码写完后,一定记得把console.log全部清掉,因为一些开发调试信息会被编译进代码包,虽然单个不大,但积少成多。
写在最后的经验
这套系统从需求调研到上线测试,我前后迭代了三轮,最深的体会是:技术选型永远不是这类管理系统的难点,真正的难点在于把医院实际业务里的那些模糊地带,转译成清晰的数据模型和状态流转规则。框架用Thinkphp还是Laravel都只是实现手段,如果把状态机、权限体系、消息通知这些业务骨架搭稳了,换框架其实只是工作量问题,不是风险问题。如果你也在做类似的系统,我建议先把流程跑通再优化性能,先保证工单能正常流转、状态不乱跳、消息能及时触达,再去抠接口响应时间和小程序体验。这套顺序走下来,项目基本不会失控。
最后再分享一个小技巧:我在验收阶段把所有状态流转的全路径写成了一个自动化脚本,依次模拟报修人提交、管理员派单、工程师接单、维修完成、报修人验收,每一步都验证数据库字段和接口返回值。这个脚本帮我发现了三个隐藏的状态错乱问题,建议你也试试,比手工点界面验证靠谱得多。