ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序远程在线诊疗系统实战:从挂号到开方的全链路技术解析

微信小程序远程在线诊疗系统实战:从挂号到开方的全链路技术解析 简介这份资源面向计算机相关专业的毕业设计学生与Java初学者提供一套基于微信小程序的远程在线诊疗系统完整项目。系统划分管理员、医生、用户三种角色覆盖用户管理、科室信息、预约挂号、取消预约、在线问诊与回复、处方信息、患者信息、通知公告、医院介绍及留言板等模块用户端可完成预约问诊与收藏操作。后台采用Java SSM框架开发MySQL作为本地数据库小程序端基于微信开发者工具实现界面清晰、操作简单、功能齐全适合作为课程设计或毕业设计参考。压缩包共1417个文件约39.85MB包含151个java源码、208个js脚本、153个vue组件、110个wxss与108个wxml小程序页面文件以及png、svg、jpg等图片素材和sql数据库脚本、bat启动脚本、properties配置等结构完整便于二次开发。目前已有56人学习配套提供全套开源源码、数据库、开题报告、论文、PPT与使用说明可帮助读者快速理解系统架构、掌握SSM与小程序联调思路并直接用于毕设答辩准备。1. 远程在线诊疗系统从挂号到开方微信小程序这条链路怎么跑通去年帮一个做社区医疗的朋友看他们刚上线的小程序用户端能挂号、能选医生但医生那边点「接诊」之后页面直接白屏。排查了半天发现是 WebSocket 连接在真机调试时被掐了开发者工具里一切正常。这个坑让我意识到远程在线诊疗系统看着就是「小程序 Java 后端」的常规组合但真正落地时问诊状态同步、处方数据流转、视频通话信令这几条链路每一条都有独立的坑。这套系统要解决的核心问题是患者在小程序端完成注册、选科室、选医生、预约时段医生在后台接诊后发起图文或视频问诊问诊结束开处方、写病历管理员管科室、医生、排班和订单。技术栈上前端是微信小程序原生开发或 uni-app后端是 Spring Boot MyBatis-Plus数据库用 MySQL缓存用 Redis 存会话和排班锁实时通信走 WebSocket。适合正在做 Java 毕业设计、课程设计或者想快速搭一套在线问诊 MVP 的开发者。下面按「数据模型怎么设计 → 后端接口怎么写 → 小程序端怎么调 → 实时问诊怎么通 → 坑在哪」的顺序拆开讲。2. 数据库表设计与问诊状态机先想清楚再动手2.1 核心表结构七张表撑起整个问诊流程远程诊疗系统的数据模型不复杂但表之间的关系要想清楚。我一般会先画状态流转图再倒推表结构。核心表包括用户表患者和医生共用一张用 role 字段区分、医生表扩展信息如职称、科室、简介、科室表、排班表、预约/问诊订单表、问诊消息表、处方表。-- 用户表患者和医生共用role 区分身份 CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, openid VARCHAR(64) DEFAULT NULL COMMENT 微信openid患者端必填, username VARCHAR(50) NOT NULL COMMENT 登录名医生端用, password VARCHAR(128) DEFAULT NULL COMMENT BCrypt加密医生端用, real_name VARCHAR(30) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT 0患者 1医生 2管理员, status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 问诊订单表核心状态机载体 CREATE TABLE consult_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, patient_id BIGINT NOT NULL, doctor_id BIGINT NOT NULL, dept_id BIGINT NOT NULL, schedule_id BIGINT DEFAULT NULL COMMENT 排班ID, type TINYINT NOT NULL DEFAULT 1 COMMENT 1图文 2视频, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1待接诊 2问诊中 3已完成 4已取消 5已退款, symptom_desc TEXT COMMENT 症状描述, amount DECIMAL(10,2) DEFAULT 0.00, pay_time DATETIME DEFAULT NULL, accept_time DATETIME DEFAULT NULL COMMENT 医生接诊时间, finish_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_patient (patient_id), KEY idx_doctor_status (doctor_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;问诊消息表用 consult_id 关联订单存消息类型文本/图片/语音、发送方、内容、发送时间。处方表关联订单和医生存处方详情 JSON 和审核状态。这里有个设计决策值得说订单状态用数字枚举而不是字符串。原因是状态流转判断在代码里高频出现数字比较比字符串快而且前端传参不容易出错。但代价是可读性差所以我在实体类里用常量类做映射别在代码里裸写 0、1、2。2.2 状态机设计问诊订单的六种状态怎么流转问诊订单的状态流转是整个系统的骨架。我见过不少毕设项目把状态判断散落在各个 Service 方法里结果改一个状态要翻五六个文件。正确做法是抽一个状态机工具类把合法流转路径集中管理。public class ConsultStateMachine { // 定义合法流转key当前状态value允许的下一状态集合 private static final MapInteger, SetInteger TRANSITIONS new HashMap(); static { // 0待支付 - 1待接诊 / 4已取消 TRANSITIONS.put(0, Set.of(1, 4)); // 1待接诊 - 2问诊中 / 4已取消 / 5已退款 TRANSITIONS.put(1, Set.of(2, 4, 5)); // 2问诊中 - 3已完成 TRANSITIONS.put(2, Set.of(3)); // 3已完成、4已取消、5已退款 为终态 TRANSITIONS.put(3, Set.of()); TRANSITIONS.put(4, Set.of()); TRANSITIONS.put(5, Set.of()); } public static void checkTransition(int from, int to) { SetInteger allowed TRANSITIONS.getOrDefault(from, Set.of()); if (!allowed.contains(to)) { throw new BizException(非法状态流转: from - to); } } }这个类的逻辑很直白每次要改订单状态前先调checkTransition校验。参数说明——from是数据库里当前状态to是目标状态。如果不在允许集合里直接抛业务异常避免出现「已完成订单又被接诊」这种脏数据。提示状态机校验要放在 Service 层事务内和更新语句在同一个事务里否则并发下仍可能脏写。2.3 排班与号源用 Redis 锁防止超卖医生排班表存的是「某医生某天上午/下午有多少个号」患者预约时扣减号源。如果只用 MySQL 的update schedule set remain remain - 1 where remain 0高并发下虽然不会超卖但容易出现大量失败请求堆积。我一般会在 Redis 里做一层预扣减。public boolean grabSchedule(Long scheduleId, Long patientId) { String lockKey schedule:lock: scheduleId; String stockKey schedule:stock: scheduleId; // 尝试获取分布式锁防止同一排班并发扣减 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, patientId.toString(), 10, TimeUnit.SECONDS); if (Boolean.FALSE.equals(locked)) { throw new BizException(当前预约人数较多请稍后重试); } try { Long remain redisTemplate.opsForValue().decrement(stockKey); if (remain null || remain 0) { // 回滚Redis库存 redisTemplate.opsForValue().increment(stockKey); throw new BizException(该时段号源已约满); } // 异步落库实际项目可用MQ削峰 scheduleMapper.decrementRemain(scheduleId); return true; } finally { redisTemplate.delete(lockKey); } }逻辑说明先用setIfAbsent抢锁拿到锁的请求才能扣库存。decrement是原子操作返回扣减后的值小于 0 说明超卖立刻回滚。参数上锁过期时间设 10 秒是经验值——太短可能业务没跑完锁就释放太长故障时恢复慢。库存 key 的初始值在排班创建时写入 Redis和数据库 remain 字段保持一致。注意Redis 和 MySQL 的数据一致性靠「先扣 Redis 再落库 定时对账」保证别指望强一致问诊预约场景最终一致就够了。3. Spring Boot 后端接口从登录到开方的完整链路3.1 微信登录code 换 openid 的正确姿势小程序端调wx.login()拿到临时 code传给后端后端拿 code appid secret 去微信接口换 openid 和 session_key。这一步是远程诊疗系统的入口做错了后面全白搭。PostMapping(/wx/login) public ResultLoginVO wxLogin(RequestBody WxLoginDTO dto) { // 1. 用code换取openid String url String.format( https://api.weixin.qq.com/sns/jscode2session?appid%ssecret%sjs_code%sgrant_typeauthorization_code, appId, appSecret, dto.getCode()); String resp restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(resp); String openid json.getString(openid); if (openid null) { throw new BizException(微信登录失败: json.getString(errmsg)); } // 2. 查库没有则注册 SysUser user userMapper.selectByOpenid(openid); if (user null) { user new SysUser(); user.setOpenid(openid); user.setRole(0); user.setStatus(1); userMapper.insert(user); } // 3. 签发JWT前端存storage String token jwtUtil.generateToken(user.getId(), user.getRole()); LoginVO vo new LoginVO(); vo.setToken(token); vo.setUserId(user.getId()); vo.setRole(user.getRole()); return Result.ok(vo); }逻辑说明第一步的 URL 拼接里appid 和 secret 从配置文件读别硬编码。第二步查库时用 openid 做唯一索引避免重复注册。第三步签发的 JWT 里放 userId 和 role前端每次请求带在 header 里后端用拦截器校验。参数说明code只能用一次有效期 5 分钟前端拿到后要立刻传给后端别缓存。session_key这个项目里用不到不做加密数据解密的话可以不存。3.2 问诊订单接口创建、支付回调、接诊订单创建接口接收患者选的医生、排班、问诊类型和症状描述生成订单号状态置为 0 待支付。支付回调接口是微信支付异步通知的入口验签后把状态改成 1 待接诊。接诊接口是医生端调的把状态从 1 改成 2同时记录 accept_time。Transactional(rollbackFor Exception.class) public void acceptOrder(Long orderId, Long doctorId) { ConsultOrder order orderMapper.selectById(orderId); if (order null) { throw new BizException(订单不存在); } if (!order.getDoctorId().equals(doctorId)) { throw new BizException(无权接诊该订单); } // 状态机校验只能从1待接诊 - 2问诊中 ConsultStateMachine.checkTransition(order.getStatus(), 2); order.setStatus(2); order.setAcceptTime(new Date()); orderMapper.updateById(order); // 给患者推送接诊通知小程序订阅消息 wxNotifyService.sendAcceptNotify(order); }逻辑说明先查订单校验归属再用状态机校验流转合法性最后更新状态并推送通知。Transactional保证更新和通知在同一个事务里——虽然通知失败不该回滚订单但这里为了简单先放一起实际项目可以把通知改成异步。参数说明orderId和doctorId都从 JWT 里取别信前端传的 doctorId否则医生 A 能接医生 B 的单。3.3 处方与病历开方接口的数据结构处方接口是医生问诊结束后调的接收订单 ID、诊断结论、处方明细药品名、规格、用法用量、数量。处方明细用 JSON 存因为药品数量不固定单独建表反而增加关联查询成本。PostMapping(/prescription/create) public ResultVoid createPrescription(RequestBody Valid PrescriptionDTO dto) { ConsultOrder order orderMapper.selectById(dto.getOrderId()); // 只有问诊中的订单能开方 if (order.getStatus() ! 2) { throw new BizException(当前订单状态不允许开方); } Prescription pres new Prescription(); pres.setOrderId(dto.getOrderId()); pres.setDoctorId(order.getDoctorId()); pres.setPatientId(order.getPatientId()); pres.setDiagnosis(dto.getDiagnosis()); // 处方明细序列化为JSON存储 pres.setDetailJson(JSON.toJSONString(dto.getItems())); pres.setStatus(0); // 0待审核 1已通过 2已驳回 pres.setCreateTime(new Date()); prescriptionMapper.insert(pres); // 订单流转到已完成 order.setStatus(3); order.setFinishTime(new Date()); orderMapper.updateById(order); return Result.ok(); }逻辑说明开方前校验订单状态必须是 2 问诊中开方后订单自动流转到 3 已完成。处方明细用 JSON 存查询时反序列化即可。参数上items是 List每个元素包含 drugName、spec、usage、quantity 四个字段。提示处方审核状态单独管理别和订单状态混在一起否则审核驳回时订单状态回退会很麻烦。4. 小程序端页面路由、请求封装与实时消息4.1 请求封装统一处理 token 和错误码小程序端每个页面都要调后端接口如果每个请求都写一遍 header 和错误处理代码会非常臃肿。我一般会封装一个 request 工具统一加 token、统一处理 401 和业务错误码。// utils/request.js const BASE_URL https://your-domain.com/api; function request(options) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer token : }, success(res) { // HTTP层成功 if (res.statusCode 200) { const body res.data; if (body.code 200) { resolve(body.data); } else if (body.code 401) { // token过期跳登录 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(body); } else { wx.showToast({ title: body.msg || 请求失败, icon: none }); reject(body); } } else { wx.showToast({ title: 网络异常, icon: none }); reject(res); } }, fail(err) { wx.showToast({ title: 网络连接失败, icon: none }); reject(err); } }); }); } module.exports { request };逻辑说明request返回 Promise调用方用await或.then拿数据。token 从 storage 读每次请求自动带上。401 时清 token 并跳登录页。业务错误码非 200 时统一 toast 提示。参数说明BASE_URL换成自己的域名小程序要求 HTTPS 且域名要在后台配置白名单。options.url是相对路径比如/consult/order/create。4.2 问诊聊天页WebSocket 连接与消息渲染图文问诊的核心是聊天页用 WebSocket 做实时消息推送。小程序端用wx.connectSocket建立连接收到消息后追加到消息列表并滚动到底部。// pages/consult/chat.js Page({ data: { messages: [], orderId: null, socketTask: null }, onLoad(options) { this.setData({ orderId: options.orderId }); this.initSocket(); this.loadHistory(); }, initSocket() { const token wx.getStorageSync(token); const task wx.connectSocket({ url: wss://your-domain.com/ws/consult?token${token}orderId${this.data.orderId} }); task.onMessage((res) { const msg JSON.parse(res.data); // 追加消息并滚动到底部 this.setData({ messages: [...this.data.messages, msg] }); this.scrollToBottom(); }); task.onClose(() { // 断线重连3秒后重试 setTimeout(() this.initSocket(), 3000); }); this.setData({ socketTask: task }); }, sendText(e) { const content e.detail.value; if (!content.trim()) return; this.data.socketTask.send({ data: JSON.stringify({ type: text, content }) }); e.detail.value ; }, scrollToBottom() { wx.createSelectorQuery() .select(#msg-list) .boundingClientRect() .exec(() { wx.pageScrollTo({ scrollTop: 99999, duration: 100 }); }); } });逻辑说明onLoad时建立 WebSocket 连接并加载历史消息。onMessage收到新消息后追加到数组然后滚动到底部。onClose里做断线重连3 秒后重试。发送消息时直接通过 socketTask 发 JSON。参数说明url里的 token 用于后端鉴权orderId 用于后端路由消息到正确的会话。wss协议要求域名有 SSL 证书。注意小程序切后台后 WebSocket 会被系统断开回到前台要重新连接并拉取离线消息否则会丢消息。4.3 视频问诊信令交换与 TRTC 集成视频问诊比图文复杂需要集成实时音视频 SDK。常见做法是用腾讯云 TRTC 或声网小程序端调trtc-room组件。流程是医生发起视频 → 后端生成房间号 → 双方进入房间 → 交换信令。// 医生端发起视频 async function startVideoConsult(orderId) { // 1. 后端生成房间号和用户签名 const { roomId, userSig } await request({ url: /consult/video/start, method: POST, data: { orderId } }); // 2. 跳转到视频页带上房间参数 wx.navigateTo({ url: /pages/video/video?roomId${roomId}userSig${userSig}roledoctor }); }逻辑说明房间号由后端生成一般用订单 ID 加随机数userSig 是 TRTC 的鉴权签名由后端用密钥计算。前端拿到后跳转到视频页视频页里用trtc-room组件加入房间。参数说明roomId是数字类型userSig是字符串有效期一般 24 小时。role用于区分医生和患者控制摄像头和麦克风权限。提示视频问诊对网络要求高建议在进入房间前做网络检测弱网时提示用户切换图文问诊。5. 避坑与排查那些让我加班到凌晨的问题5.1 坑一WebSocket 在真机调试时连不上现象开发者工具里 WebSocket 一切正常真机预览时连接立刻断开控制台报connectSocket:fail。原因小程序的 WebSocket 要求wss协议且域名必须在微信公众平台的「socket 合法域名」里配置。开发者工具可以勾选「不校验合法域名」但真机不行。解决在微信公众平台后台配置 socket 合法域名必须是 HTTPS/WSS 且已备案。本地开发时可以用内网穿透工具生成临时域名但要注意工具本身的安全合规性。5.2 坑二订单状态并发更新导致脏数据现象患者和医生同时操作同一订单比如患者取消的同时医生接诊结果订单状态变成「已取消但已接诊」。原因状态机校验和更新不在同一个原子操作里两个请求都通过了校验然后先后更新。解决用乐观锁。在订单表加 version 字段更新时带上 version 条件。UPDATE consult_order SET status 2, accept_time NOW(), version version 1 WHERE id ? AND status 1 AND version ?如果影响行数为 0说明状态已被其他请求改变抛异常让前端刷新。5.3 坑三微信支付回调重复通知现象微信支付回调接口被调了两次订单被重复处理患者看到两条接诊通知。原因微信支付回调在未收到成功响应时会重试如果接口处理慢或返回非成功格式就会重复通知。解决回调接口做幂等。用订单号做唯一键处理前先查是否已处理过。// 幂等校验已处理过的回调直接返回成功 if (payRecordMapper.existsByOrderNo(orderNo)) { return SUCCESS; } // 处理业务逻辑... payRecordMapper.insert(new PayRecord(orderNo)); return SUCCESS;5.4 坑四小程序端图片上传后显示裂图现象患者上传症状图片后端存了路径但小程序端显示裂图。原因小程序image组件的 src 如果是相对路径或本地路径真机上无法访问。必须用完整的 HTTPS URL。解决后端返回图片时拼完整域名或者前端在渲染时拼接 BASE_URL。另外图片域名也要在小程序的「downloadFile 合法域名」里配置。5.5 坑五医生端登录后看不到待接诊订单现象医生登录成功但订单列表为空数据库里明明有待接诊订单。原因查询条件里 status 传的是字符串1数据库字段是 intMySQL 隐式转换在某些版本下不走索引或者查询条件写成了status 1导致类型不匹配。解决统一参数类型前端传数字后端 DTO 用 Integer 接收。另外检查 SQL 的 where 条件确保doctor_id和status都传了。6. 进阶技巧用状态机日志和接口幂等把系统做稳前面把主链路跑通了但要让系统真正能扛住用还得补两个东西状态流转日志和接口幂等。这两个是我踩了无数次坑之后养成的习惯看起来是额外工作但省下的排查时间远超投入。状态流转日志每次订单状态变更都往consult_status_log表插一条记录存订单 ID、原状态、新状态、操作人、操作时间、备注。表结构很简单但排查问题时价值巨大。比如患者投诉「我的订单怎么突然取消了」查日志就能看到是患者自己点的取消还是系统超时取消还是管理员操作的。public void changeStatus(Long orderId, int toStatus, Long operatorId, String remark) { ConsultOrder order orderMapper.selectById(orderId); ConsultStateMachine.checkTransition(order.getStatus(), toStatus); // 记录流转日志 StatusLog log new StatusLog(); log.setOrderId(orderId); log.setFromStatus(order.getStatus()); log.setToStatus(toStatus); log.setOperatorId(operatorId); log.setRemark(remark); log.setCreateTime(new Date()); statusLogMapper.insert(log); // 更新订单 order.setStatus(toStatus); orderMapper.updateById(order); }这个方法的参数说明orderId订单 IDtoStatus目标状态operatorId操作人患者/医生/系统传 0remark备注。所有状态变更都走这一个入口别在业务代码里直接setStatus。接口幂等除了支付回调创建订单、开处方、取消订单这些接口都要做幂等。最简单的方式是用 Redis 存请求指纹key 是「用户 ID 接口名 业务参数哈希」value 是请求 ID过期时间 5 分钟。同一个请求重复提交时如果 key 已存在就直接返回上次的结果。public Result? idempotent(String bizKey, SupplierResult? action) { String redisKey idem: bizKey; Boolean success redisTemplate.opsForValue() .setIfAbsent(redisKey, 1, 5, TimeUnit.MINUTES); if (Boolean.FALSE.equals(success)) { throw new BizException(请勿重复提交); } try { return action.get(); } catch (Exception e) { // 业务失败时删除key允许重试 redisTemplate.delete(redisKey); throw e; } }调用时把业务逻辑包在 lambda 里bizKey用「userId:createOrder:参数哈希」拼。注意业务失败时要删 key否则用户改完参数重试会被拦住。验证方法状态机日志可以直接查表验证幂等可以用 JMeter 或 Postman 并发调同一个接口看是否只有一个成功。我一般会在本地用ab命令压 100 个并发确认幂等生效。最后说个血泪教训别在业务代码里裸写状态数字。我接手过一个项目代码里到处是if (status 2)后来加了一个新状态改漏了一处导致订单卡在中间态。从那以后我强制自己用常量类和状态机虽然多写几行代码但改需求时心里有底。希望帮到你。本文还有配套的精品资源点击获取
返回列表