
简介这份源码资源面向计算机专业学生、Java后端与微信小程序开发者提供一套完整的陪诊服务预约平台实现方案可用于课程设计、毕业设计或二次开发练手。项目以Java构建后端服务前端采用微信小程序技术覆盖预约陪诊、住院陪护、待办取药、待办挂号、检查结果领取等医疗事务场景并实现服务展示、时间选择、预约下单、历史订单查看与预约状态跟踪等核心模块。压缩包共518个文件约9.42MB其中JavaScript文件119个负责小程序逻辑wxss与wxml文件分别承担样式与页面结构JSON文件用于全局配置Java源码77个处理后端业务另含少量图片、SQL脚本、yml配置及安装使用手册文档目录划分清晰。目前已有192人学习下载。读者可从中获取完整的前后端代码结构、数据库脚本与部署说明便于快速理解小程序与Java服务端的协作方式并在此基础上进行功能扩展与调试排错。1. 陪诊预约平台为什么值得用小程序重做一遍陪诊这个行业这两年需求涨得很快老人独自就医、异地患者不熟悉流程、孕妇带娃看病分身乏术都是真实存在的场景。但真正落地时很多团队卡在同一个地方用户不愿意装 App。一个低频但刚需的服务让用户为了约一次陪诊去下载几十兆的安装包转化率低得可怜。微信小程序刚好补上这个缺口——扫码即用、微信登录免注册、模板消息能推送订单状态对陪诊这种「用完即走、但需要及时通知」的业务几乎是量身定做。这套「基于微信小程序的 Java 陪诊服务预约平台」讲的就是把陪诊业务拆成用户端小程序、陪诊师端小程序和 Java 后台三块用 Spring Boot 做服务端、MySQL 存业务数据、微信登录打通身份体系。它解决的核心问题是让患者三分钟内完成一次陪诊下单让后台能派单、结算、管理陪诊师资质。适合正在做本地生活服务、医疗辅助类小程序的后端和全栈开发者也适合拿它当毕业设计或接私活的落地骨架。下面从数据模型一路讲到派单和避坑能抄的部分我都给到可运行的代码。2. 陪诊业务的数据模型与接口分层怎么定陪诊预约看着简单本质是「服务商品 时间资源 人员调度」三件事叠在一起。如果一上来就写 Controller后面改需求会非常痛苦。我一般先把领域模型定死再往上搭接口这样派单、改期、退款这些逻辑才有地方落。2.1 五张核心表撑起整个预约链路陪诊平台最小可用的表结构是五张用户表、陪诊师表、服务项目表、订单表、订单状态流水表。前四张是业务实体第五张是后悔药——订单每次状态变更都记一条出纠纷时能还原全过程。-- 用户表小程序端用户openid 是微信身份唯一标识 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信 openid登录唯一键, nickname VARCHAR(64) DEFAULT , phone VARCHAR(20) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 陪诊师表资质、接单状态、服务城市 CREATE TABLE escort ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, phone VARCHAR(20) NOT NULL, city VARCHAR(32) NOT NULL COMMENT 服务城市派单按此过滤, cert_no VARCHAR(64) DEFAULT COMMENT 健康证/培训证编号, status TINYINT DEFAULT 0 COMMENT 0待审核 1可接单 2休息 3封禁, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 服务项目表半天陪诊、全天陪诊、代取报告等 CREATE TABLE service_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT 单位元, duration_hour INT NOT NULL COMMENT 服务时长用于排期冲突判断, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表核心业务表 CREATE TABLE order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务单号对外展示, user_id BIGINT NOT NULL, escort_id BIGINT DEFAULT NULL COMMENT 派单后写入, item_id BIGINT NOT NULL, hospital VARCHAR(128) NOT NULL, appoint_time DATETIME NOT NULL COMMENT 预约就诊时间, amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付 1待派单 2已接单 3服务中 4已完成 5已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_escort_time (escort_id, appoint_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单状态流水每次变更插一条用于追溯 CREATE TABLE order_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, from_status TINYINT, to_status TINYINT NOT NULL, operator VARCHAR(32) COMMENT user/escort/admin/system, remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有几个点值得说。order_no单独做业务单号而不是直接暴露自增 id是为了防止用户从单号推算出平台订单量也方便对账。escort表里的city是派单的第一道过滤条件跨城派单在陪诊场景里基本没意义。order表的联合索引idx_escort_time是给「同一陪诊师同一时间段是否已被占用」这个查询用的没有它排期冲突检查在订单量上来后会全表扫。2.2 接口按端分层别把小程序和管理后台混在一起小程序端和后台管理端的接口我一般物理上分两个 Controller 包路径前缀区分开/api/wx/**给小程序/api/admin/**给后台。原因是鉴权方式完全不同——小程序走微信登录换取的 token后台走账号密码登录混在一起拦截器会写得很乱。RestController RequestMapping(/api/wx/order) public class WxOrderController { Autowired private OrderService orderService; // 创建订单小程序端提交预约 PostMapping(/create) public ResultOrderVO create(RequestBody Valid OrderCreateDTO dto, RequestHeader(X-Token) String token) { Long userId tokenService.getUserId(token); // 从 token 解析用户 return Result.ok(orderService.createOrder(userId, dto)); } // 查询我的订单列表带分页 GetMapping(/list) public ResultPageResultOrderVO list(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, RequestHeader(X-Token) String token) { Long userId tokenService.getUserId(token); return Result.ok(orderService.pageByUser(userId, page, size)); } }OrderCreateDTO里必须做参数校验appointTime不能早于当前时间、itemId必须存在且上架、hospital不能为空。这些用NotNull、Future注解加一个全局异常处理器就能兜住别等到 Service 里再手动 if 判断代码会脏得很快。分页参数page和size一定要设上限size超过 50 直接截断否则有人传size100000就能把你的库拖垮。2.3 微信登录换 token 的最小闭环小程序端不存密码登录流程是小程序调wx.login拿 code传给后端后端拿 code 去微信服务器换 openid 和 session_key再自己签发一个 token 返回给小程序。这个 token 后续放在请求头里。Service public class WxAuthService { Value(${wx.appid}) private String appid; Value(${wx.secret}) private String secret; Autowired private RestTemplate restTemplate; Autowired private UserMapper userMapper; Autowired private TokenService tokenService; public String login(String code) { // 用 code 换 openid注意这个接口有频率限制 String url https://api.weixin.qq.com/sns/jscode2session ?appid appid secret secret js_code code grant_typeauthorization_code; JSONObject resp restTemplate.getForObject(url, JSONObject.class); String openid resp.getString(openid); if (openid null) { throw new BizException(微信登录失败: resp.getString(errmsg)); } // 查库没有就注册 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); userMapper.insert(user); } return tokenService.issue(user.getId()); } }appid和secret必须放配置文件或环境变量绝对不能硬编码进代码提交到仓库这是血泪经验。jscode2session接口的 code 只能用一次且五分钟内有效所以前端拿到 code 要立刻发请求别缓存。token 我一般用 JWT 签发有效期设 7 天小程序端存storage里过期了重新走登录。3. 派单、排期冲突与订单状态机怎么落地数据模型定好之后真正容易翻车的是业务逻辑一个陪诊师同一时间不能被派两单订单状态不能乱跳取消和退款要能对得上。这一章讲这三块的实现。3.1 派单的两种模式与冲突检查派单分两种用户下单时直接选陪诊师指定派单或者下单后由后台/系统分配自动派单。指定派单实现简单自动派单要考虑的因素多——城市匹配、陪诊师当前排期、评分、接单量均衡。不管哪种核心都是排期冲突检查。判断逻辑是同一陪诊师在[appoint_time, appoint_time duration_hour]这个区间内是否已有未取消的订单。public boolean hasConflict(Long escortId, LocalDateTime start, int durationHour) { LocalDateTime end start.plusHours(durationHour); // 查询该陪诊师在时间窗内、状态非取消的订单数量 int count orderMapper.countConflict(escortId, start, end); return count 0; }对应的 SQL 用区间重叠判断别用BETWEEN因为BETWEEN处理不了「新订单开始时间早于已有订单、结束时间晚于已有订单」这种包含关系SELECT COUNT(*) FROM order WHERE escort_id #{escortId} AND status IN (1,2,3) -- 待派单/已接单/服务中取消和完成的不算 AND appoint_time #{end} AND DATE_ADD(appoint_time, INTERVAL duration_hour HOUR) #{start}这里有个坑duration_hour存在service_item表里不在order表。要么在订单创建时把时长冗余进order要么 SQL 里 join 一下。我倾向冗余因为订单一旦创建服务时长就不该再随商品配置变化冗余字段反而更准确。自动派单的分配策略简单版就是按城市筛出可接单的陪诊师按当前进行中订单数升序排取第一个没冲突的。这个逻辑放在 Service 里加个Transactional避免并发下两个订单同时派给一个人。3.2 订单状态机用枚举管住非法流转订单状态乱跳是这类系统最常见的 bug。用户取消了订单后台还能点「开始服务」订单没支付陪诊师就能接单。解决办法是把状态流转规则写进枚举任何变更都先校验。public enum OrderStatus { WAIT_PAY(0, 待支付), WAIT_DISPATCH(1, 待派单), ACCEPTED(2, 已接单), SERVING(3, 服务中), FINISHED(4, 已完成), CANCELED(5, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } // 定义允许的流转key 是当前状态value 是可达状态集合 private static final MapOrderStatus, SetOrderStatus FLOW new HashMap(); static { FLOW.put(WAIT_PAY, EnumSet.of(WAIT_DISPATCH, CANCELED)); FLOW.put(WAIT_DISPATCH, EnumSet.of(ACCEPTED, CANCELED)); FLOW.put(ACCEPTED, EnumSet.of(SERVING, CANCELED)); FLOW.put(SERVING, EnumSet.of(FINISHED)); FLOW.put(FINISHED, EnumSet.noneOf(OrderStatus.class)); FLOW.put(CANCELED, EnumSet.noneOf(OrderStatus.class)); } public boolean canTransferTo(OrderStatus target) { return FLOW.getOrDefault(this, Collections.emptySet()).contains(target); } }变更状态时统一走一个方法先查当前状态校验canTransferTo通过后更新并写order_log。这样即使前端传了非法状态后端也能拦住。FINISHED和CANCELED是终态不允许再流转这是业务底线。3.3 取消与退款的边界处理取消订单要分阶段处理待支付阶段取消直接改状态即可已支付但未派单要触发退款已接单后取消可能涉及陪诊师补偿规则因平台而异。我一般把取消逻辑抽成一个CancelPolicy按订单当前状态决定是否退款、退多少。public void cancel(Long orderId, String reason) { Order order orderMapper.selectById(orderId); OrderStatus current OrderStatus.of(order.getStatus()); if (!current.canTransferTo(OrderStatus.CANCELED)) { throw new BizException(当前状态不可取消); } // 已支付的需要退款 if (order.getPayTime() ! null) { refundService.refund(order.getOrderNo(), order.getAmount()); } orderMapper.updateStatus(orderId, OrderStatus.CANCELED.getCode()); orderLogMapper.insert(orderId, current.getCode(), OrderStatus.CANCELED.getCode(), user, reason); }退款接口一定要做幂等用order_no作为退款单号微信支付那边同一单号重复请求会返回相同结果不会重复退。这个幂等键是防止用户狂点取消按钮导致重复退款的关键。4. 小程序端预约流程与页面加载的实战细节后端接口通了小程序端还有一堆细节决定体验。这一章讲预约表单、列表分页加载、以及消息通知这三块最常被问到的实现。4.1 预约表单时间选择与医院输入预约页的核心是时间选择和医院填写。时间用picker组件的modemultiSelector把日期和时段拆成两列避免用户选到过去的时间。医院输入用input加联想但联想数据源别每次都请求后端本地缓存一份热门医院列表输入时先匹配本地匹配不到再请求。// pages/order/create.js Page({ data: { dateRange: [], // 未来 7 天 timeSlots: [08:00-12:00, 13:00-17:00, 18:00-21:00], multiIndex: [0, 0], hospital: }, onLoad() { // 生成未来 7 天日期避免用户选到过去 const days []; for (let i 0; i 7; i) { const d new Date(Date.now() i * 86400000); days.push(${d.getMonth() 1}月${d.getDate()}日); } this.setData({ dateRange: days }); }, onTimeChange(e) { const [di, ti] e.detail.value; this.setData({ multiIndex: [di, ti] }); }, submit() { const { dateRange, timeSlots, multiIndex, hospital } this.data; if (!hospital.trim()) { wx.showToast({ title: 请填写就诊医院, icon: none }); return; } // 组装 appointTime 传给后端格式 yyyy-MM-dd HH:mm:ss wx.request({ url: ${app.globalData.baseUrl}/api/wx/order/create, method: POST, header: { X-Token: wx.getStorageSync(token) }, data: { /* ... */ }, success: (res) { if (res.data.code 0) { wx.redirectTo({ url: /pages/order/detail?id${res.data.data.id} }); } } }); } });时间格式一定要前后端约定死我吃过亏前端传2024/6/1后端JsonFormat配的是yyyy-MM-dd直接解析失败。统一用yyyy-MM-dd HH:mm:ss在 DTO 上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)。4.2 订单列表分页加载与上拉触底订单列表用onReachBottom做上拉加载更多这是小程序列表的标准做法。关键是维护好page、hasMore两个状态加载中要加锁防止重复请求。Page({ data: { list: [], page: 1, hasMore: true, loading: false }, onShow() { this.refresh(); }, refresh() { this.setData({ list: [], page: 1, hasMore: true }); this.loadMore(); }, loadMore() { if (!this.data.hasMore || this.data.loading) return; this.setData({ loading: true }); wx.request({ url: ${app.globalData.baseUrl}/api/wx/order/list, data: { page: this.data.page, size: 10 }, header: { X-Token: wx.getStorageSync(token) }, success: (res) { const rows res.data.data.rows; this.setData({ list: this.data.list.concat(rows), page: this.data.page 1, hasMore: rows.length 10, // 返回不足一页说明到底了 loading: false }); }, fail: () this.setData({ loading: false }) }); }, onReachBottom() { this.loadMore(); } });hasMore的判断用「返回条数是否等于 pageSize」比让后端返回 total 再算更省事代价是最后一页刚好满页时会多请求一次空列表可以接受。loading锁必须有否则用户快速上拉会并发多个请求列表出现重复数据。4.3 订单状态变更的消息通知陪诊订单的关键节点——派单成功、陪诊师出发、服务完成——都需要通知用户。小程序有订阅消息但需要用户主动授权且一次性订阅只能推一条。我的做法是下单时引导用户授权订阅消息同时在订单详情页做状态轮询兜底。// 下单成功后请求订阅授权 wx.requestSubscribeMessage({ tmplIds: [派单通知模板ID, 完成通知模板ID], success: (res) { /* 授权结果 */ } });后端在状态变更时调微信的subscribeMessage.send接口推送。注意模板 ID 要在小程序后台申请且推送内容有格式要求字段名和顺序都不能错。轮询兜底就是订单详情页每 30 秒拉一次状态用户停留在页面时能及时看到变化。5. 部署上线前必须排查的五个坑代码写完能跑和能上线是两回事。这一章是我在类似项目里踩过的坑按「现象 → 原因 → 解决」列出来照着排查能省不少时间。5.1 小程序请求域名未配置导致真机全部失败现象开发者工具里接口正常真机预览所有请求都失败报「不在以下 request 合法域名列表中」。原因微信小程序对网络请求域名有白名单限制开发者工具可以勾选「不校验合法域名」但真机不行。解决登录小程序后台在「开发管理 → 开发设置 → 服务器域名」里把后端域名加到request合法域名。域名必须备案且是 HTTPS端口只能是 443。测试阶段可以用内网穿透工具临时给个 HTTPS 域名但上线前必须换成正式域名。5.2 微信登录 code 复用导致登录偶发失败现象用户登录偶尔失败日志里jscode2session返回errcode: 40163code 已被使用。原因前端在某些场景下重复提交了同一个 code比如用户快速点了两次登录按钮或者页面onLoad和按钮点击都触发了登录。解决前端登录按钮加防抖登录中禁用按钮后端对同一个 code 的失败做一次重试意义不大因为 code 确实只能用一次正确做法是前端保证不重复提交。另外wx.login每次调用都会生成新 code所以重试时应该重新调wx.login而不是复用旧 code。5.3 订单并发下单导致陪诊师被重复派单现象两个用户几乎同时下单系统把同一个陪诊师派给了两单时间还重叠。原因冲突检查是「先查再写」两个请求都查到没冲突然后都写入典型的竞态。解决在order表对escort_id appoint_time加唯一约束不现实时间不完全相同正确做法是在派单方法上加分布式锁锁的 key 用escort_id或者用数据库的悲观锁SELECT ... FOR UPDATE锁住陪诊师行。订单量不大时synchronized加在单机 Service 方法上也够用但多实例部署必须用 Redis 锁。5.4 金额用 double 计算出现分位误差现象订单金额偶尔出现99.99999999这种值对账时差几分钱。原因Java 的double和float是浮点数不能精确表示小数0.1 0.2 ! 0.3是经典问题。解决所有金额字段用BigDecimal数据库用DECIMAL(10,2)。BigDecimal比较用compareTo而不是equals因为equals会比较精度。构造BigDecimal用字符串构造器new BigDecimal(99.99)别用new BigDecimal(99.99)后者会带入浮点误差。5.5 订单状态更新没加乐观锁导致覆盖现象用户取消订单的同时后台点了派单最终状态变成「已接单」取消丢了。原因两个操作都读了订单各自改状态写回后写的覆盖了先写的。解决order表加version字段更新时带版本号UPDATE order SET status?, versionversion1 WHERE id? AND version?影响行数为 0 说明被别人改过抛异常让前端重试。这是乐观锁的标准用法比悲观锁轻量适合订单这种读多写少的场景。6. 用状态流水做订单全链路追溯的进阶技巧前面提到order_log表很多人建了但没用好只当个日志堆着。其实这张表用好了能解决陪诊平台最头疼的纠纷问题。我一般会在订单详情页给用户和后台都展示一条时间轴数据就来自order_log每次状态变更的operator和remark都记清楚谁在什么时候做了什么一目了然。具体做法是封装一个统一的changeStatus方法所有状态变更必须走它禁止任何地方直接update order set status。方法内部做三件事校验流转合法性、更新订单、写流水。这样流水和订单状态永远一致不会出现订单改了流水没记的情况。Transactional public void changeStatus(Long orderId, OrderStatus target, String operator, String remark) { Order order orderMapper.selectById(orderId); OrderStatus current OrderStatus.of(order.getStatus()); if (!current.canTransferTo(target)) { throw new BizException(非法状态流转: current - target); } // 乐观锁更新version 不匹配会抛异常 int affected orderMapper.updateStatusWithVersion( orderId, target.getCode(), order.getVersion()); if (affected 0) { throw new BizException(订单已被其他操作修改请刷新重试); } orderLogMapper.insert(orderId, current.getCode(), target.getCode(), operator, remark); }验证这套机制是否可靠有个简单办法写个对账脚本遍历所有订单把order_log按时间排序模拟重放一遍状态流转看最终状态是否和order表一致。不一致的订单就是有地方绕过了changeStatus揪出来改掉。这个脚本我每次上线前都跑一遍比人工点页面靠谱得多。还有个细节order_log的remark字段别只存「取消」这种词要存具体原因比如「用户主动取消临时有事」。陪诊纠纷里用户说「我没同意取消」后台说「系统显示取消了」这时候流水里的operator和remark就是唯一的证据。我吃过一次亏早期版本remark存的是空字符串纠纷时两边各执一词最后只能赔钱了事。从那以后凡是状态变更remark必填前端取消订单必须让用户选或填原因。最后说个习惯这套平台的源码结构我建议按controller / service / mapper / entity / dto分包别按业务模块分包。陪诊业务模块边界其实不清晰订单、派单、结算互相调用按业务分包会导致大量跨包循环依赖。按层分包虽然老套但在这个体量的项目里最省心。希望帮到你。本文还有配套的精品资源点击获取