简介:医院挂号预约系统源码包是一套基于SpringBoot与Vue构建的现代化医疗服务平台实例,面向医疗信息化开发者与Java/Vue初学者,覆盖患者在线挂号、医生排班、科室查询、预约取消、费用支付、报告查询及消息推送等全流程,旨在减少就医排队、提升医疗资源利用效率。包内共1076个文件,压缩后约24.35MB,核心类型包括Java后端服务、Vue组件页面、JavaScript逻辑、JSON配置、PNG/SVG界面素材,以及微信小程序相关的wxml/wxss文件,说明源码同时覆盖Web端与移动端入口,目录结构完整,便于按模块检索与复用。目前已有49人学习下载。源码包还提供install/run/build批处理脚本,可快速完成安装、运行与构建;多个样式备份与IDE工程配置文件则便于对照学习和迁移开发,可直接运行或作为二次开发基础,尤其适合学习SpringBoot+Vue前后端整合、小程序联调及医疗业务建模的读者。系统内置数据分析与可视化能力,可帮助医生识别就诊高峰时段、管理者优化排班计划,进一步提升医疗服务价值。
1. 这个压缩包解决的是就医流程里最痛的一段链路
早上 8 点放号,候诊大厅排成长队,窗口挂号员一边点鼠标一边被催,系统偶尔还会卡到白屏——这个场景对做过医疗信息化的人来说并不陌生。标题里的“医院挂号预约系统”是一个典型的 SpringBoot + Vue 全栈项目,把患者在线挂号、医生排班管理、科室查询、预约取消、费用支付、报告查询和消息推送串成了闭环。它要回答的核心问题是:怎么让号源分配从“窗口抢”变成“在线约”,让医院管理员看得清号源和订单状态,同时真正减少现场排队。适合两类人:一类是中小医院或诊所的信息科,想用最短路径把预约流程数字化;另一类是正在做课设或外包项目、需要一份可扩展参考实现的开发者。下面按“模块边界 → 本地跑通 → 核心流程参数 → 踩坑记录 → 最后一公里优化”的顺序拆透。
2. 先看清SpringBoot+Vue的服务端与前端边界:模块拆分与数据流
2.1 为什么挂号系统卡在“并发集中”而不是功能多少
在线挂号系统表面上功能很多:科室查询、排班列表、预约、支付、报告查询、消息推送,但真正决定系统能不能用的只有两个时刻:放号那一刻(比如每天 8:00)和支付回调那一刻。放号时几十个患者同时提交预约,如果后端只是简单update余号字段,就会出现超卖;支付回调时如果不对幂等做处理,订单状态就会在“已支付”和“已取消”之间来回跳。
所以模块拆分的第一原则不是按页面拆,而是按“有状态资源”拆。医生排班是号源池,预约订单是状态机,支付记录是对账依据,这三块必须独立成模块,才能把并发控制和状态流转的代码集中在同一批类里,避免散落在各个 Controller。反过来,科室查询、报告列表这类只读业务可以适当放松,把它们放进同一个查询模块,用缓存扛住读取压力。
这套系统也不要一上来就拆微服务。单体内聚模块足够支撑几百个并发预约,拆成微服务后,预约服务和支付服务之间的同步调用会引入分布式事务,复杂度翻倍。压缩包里的方案应该以 Spring Boot 单体为主,模块边界通过 Maven module 或 packages 目录划分,团队协作时只要约定好 Service 层接口,不互相侵入对方的 Mapper 即可。
2.2 后端模块边界:从患者端到管理端的七个服务
用这个标题来搭建后端时,我一般会把 Spring Boot 工程拆成下面七个模块,既贴合业务,又方便把权限和事务边界划清楚:
| 模块 | 核心实体 | 主要接口 | 说明 |
|---|---|---|---|
| 用户认证 | user, patient | 登录、注册、JWT刷新 | 患者和管理员共用一套账号体系,用 role 区分权限 |
| 科室管理 | department | 科室列表、分页查询 | 静态数据,缓存到 Redis |
| 医生排班 | doctor_schedule | 按日期查排班、生成号源 | 号源池核心,库存扣减在这里 |
| 预约订单 | appointment_order | 提交预约、取消、订单状态查询 | 状态机所在模块 |
| 支付服务 | payment_record | 发起支付、回调处理、退款 | 对接支付平台的壳,支持模拟回调 |
| 报告查询 | patient_report | 报告列表、PDF 预览 | 与检验检查系统对接的预留接口 |
| 消息推送 | message | 站内信、WebSocket/SSE 推送 | 推送预约结果、报告提醒 |
模块之间通过 Service 调用,不在多个 Controller 里重复写订单状态判断逻辑。这样做有一个直接好处:当医院现有系统要接入时,只需要替换“医生排班”模块的数据来源,其他模块基本不动。比如某医院使用旧 HIS 的排班表格,我可以在scheduleService里加一个 Adapter,把 HIS 的排班数据转换成doctor_schedule记录,患者端无感知。
数据库层面还要留意“患者主数据”。同样一个患者,可能在预约挂号表里叫patient,在报告表里叫patient_info,两边的手机号不一致就会出现“挂了号却查不到报告”的问题。所以我在设计时会让所有业务表都引用patient_id,患者姓名和手机号只在user/patient主表里维护,业务表只存冗余的patient_name快照,避免每次关联查询。
2.3 前端路由与权限:Vue侧不是单纯页面堆砌
Vue 侧如果只按页面堆组件,后面一定会栽在权限控制上。常见做法是路由表分成三份:公共路由(登录页、科室查询)、患者路由(预约、支付、报告、我的预约)、管理路由(排班管理、号源监控、订单管理)。在路由守卫里读本地存的 JWT 和 role,用户没有对应权限就重定向到 403 页。
另一个容易被忽略的是 Axios 请求拦截器:要在请求头带上Authorization: Bearer token,在响应拦截器里统一处理 401 过期跳转。和后端接口约定保持一致,我一般会让后端返回统一的Result<T>包装体,结构包含 code、message、data,前端在响应拦截器里只认这个结构。这样“科室查询”“报告查询”这类只读接口和“提交预约”“支付”这类写接口可以共用同一套错误处理逻辑,不需要每个页面重复弹提示。
前端开发期的跨域问题,最好在 Vite 配置里解决,而不是每次后端都去允许跨域来源。vite.config.js里配一个 proxy:/api代理到http://localhost:8080,这样浏览器看到的是同源请求,后端也不需要在 CORS 配置里暴露所有来源。上线后由 Nginx 做反向代理,前端构建产物放在dist,后端接口走/api,一套规则贯穿开发和生产。
组件库我建议用 Element Plus 这类成套组件,表格、表单、日期选择器直接用现成的,把精力留给排班日历这种定制组件。排班日历的难点在于“日期 + 上下午 + 号源余量”三层结构,纯 Element 的el-calendar需要二次封装,我会用v-model绑定选中日期,再用插槽渲染每个日期的号源摘要,点击日期后拉取doctor_schedule列表。
3. 把系统跑起来的最小路径:初始化、建库、联调
3.1 环境清单与版本选择:JDK、MySQL、Node 的匹配边界
本地跑这套方案,环境版本不需要追新,但要注意匹配边界。我建议的搭配是 JDK 8 或 11、Spring Boot 2.7.x、MySQL 8.0、Redis 6.x(可选)、Node 16 或 18。JDK 17 对 Spring Boot 2.7 也能跑,但部分旧版 Lombok 会有兼容问题,所以别盲目上新。前端用 Vue 3 + Vite,Node 版本高于 16 即可,低于 14 会直接报依赖版本错误。
如果是为了快速验证,MySQL 和 Redis 可以用 Docker 起,但要注意:容器内的 MySQL 默认case_sensitive_table_names=0,在 Linux 下表名大小写不敏感,Windows 上则敏感,这是“表找不到”一类问题的源头。踩过一次坑后,我建表时统一小写表名,字段也统一小写下划线,避免操作系统差异带来的玄学问题。
Redis 在这套系统里承担两个职责:一是缓存科室、医生这类读多写少的数据;二是做预约锁和支付回调锁。如果本地不想装 Redis,可以在配置里把缓存降级为 Caffeine,但预约锁必须要有独立实现,不能直接省掉。否则第 5 章的并发问题会原样复现。
3.2 数据库初始化:表结构里的三个关键设计
解压后先找sql/init.sql或类似初始化脚本。下面三张表决定了预约流程能不能跑通。第一张是doctor_schedule,字段中包含doctor_id、work_date、start_time、end_time、total_slots、remain_slots、version。其中remain_slots是余号,version是乐观锁版本号。第二张是appointment_order,状态字段用status:0 待支付、1 已支付、2 已就诊、3 已取消、4 已过期。第三张是payment_record,用payment_no和order_no两个唯一键来保证回调幂等。
CREATE TABLE doctor_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, work_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, total_slots INT NOT NULL DEFAULT 1, remain_slots INT NOT NULL DEFAULT 1, version INT NOT NULL DEFAULT 0, UNIQUE KEY uk_doctor_time (doctor_id, work_date, start_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE appointment_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, patient_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0, pay_amount DECIMAL(10,2) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, KEY idx_patient_status (patient_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:doctor_schedule用(doctor_id, work_date, start_time)唯一键防止同一医生同一时间段生成两条排班;appointment_order的order_no用唯一键,是为了在重复提交订单时让数据库层兜底。remain_slots没有直接用total_slots - 预约数计算,而是单独维护,方便做UPDATE ... SET remain_slots = remain_slots - 1 WHERE remain_slots > 0的原子扣减。这是防超卖的核心,第 5 章会展开说。
消息推送表我也建议一起建上。message表至少包含id、patient_id、message_type、content、is_read、created_at。无论前端用 WebSocket 还是 SSE,业务消息都要先落库,前端断线重连后靠拉取接口补齐未读消息,这是“实时推送 + 拉取补偿”双通道的基础。
3.3 启动后端与前端:两个命令和一个联调开关
后端是标准的 Maven 工程,第一次启动先把依赖拉全:
# 后端启动 mvn clean install -DskipTests mvn spring-boot:run -Dspring-boot.run.profiles=dev参数说明:-DskipTests跳过测试是为了本地快速验证,如果你改了业务代码建议把单测跑一遍;-Dspring-boot.run.profiles=dev表示加载application-dev.yml,里面配置数据库连接和 Redis 地址。实际执行时如果端口被占用,会看到Port 8080 was already in use,这时要么改server.port,要么把占用端口的进程杀掉。
前端是 Vite 工程:
# 前端启动 npm install npm run dev启动完默认起在 5173 端口。此时浏览器访问http://localhost:5173,如果登录接口返回跨域错误,优先在vite.config.js里配置 proxy 解决,而不是改后端 CORS。联调时最容易翻车的是接口前缀不一致:后端 Controller 类上写@RequestMapping("/api/patient"),前端 axios 的 baseURL 却写成/api,请求路径变成/api/api/patient。我统一约定:axios baseURL 不写路径,只在具体请求里写/api/...,配合 proxy 不容易出歧义。
如果登录页能加载,但点登录后 5 秒没响应,先按这个顺序排查:看浏览器 F12 的 Network,接口返回 401 说明 JWT 没签发或 token 没带上;返回 500 就翻后端日志,九成是表名或字段名对不上。这里给一个本地验证的完整路径:先注册一个患者账号,用该账号登录拿 token,调科室查询接口,选一个科室查医生排班,再对一个可预约时段提交预约。把这条链路跑通,说明前后端联调没有结构性问题,剩下的只是业务参数调优。
4. 核心流程的参数设计:排班、预约、支付怎么才算可用
4.1 医生排班:号源池与时间段滑动的实现思路
排班模块是整套系统的“库存中心”。常见做法不是让医生手动添加 50 个号,而是只维护一个时间段,由代码生成号源池。比如某个医生上午 8:00-12:00,每个号源 30 分钟,总号数就是 8 个,每个号源默认total_slots=1。如果医院希望一个时段覆盖更多患者,把slotMinutes调小,或者让total_slots放大,比如普通门诊每 10 分钟一个号,上午 4 小时就是 24 个号。
public void generateSchedule(DoctorScheduleRequest req) { LocalTime cursor = req.getStartTime(); while (cursor.isBefore(req.getEndTime())) { DoctorSchedule schedule = new DoctorSchedule(); schedule.setDoctorId(req.getDoctorId()); schedule.setWorkDate(req.getWorkDate()); schedule.setStartTime(cursor); schedule.setEndTime(cursor.plusMinutes(req.getSlotMinutes())); schedule.setTotalSlots(req.getSlotsPerPeriod()); schedule.setRemainSlots(req.getSlotsPerPeriod()); scheduleService.save(schedule); cursor = cursor.plusMinutes(req.getSlotMinutes()); } }逻辑说明:cursor从开诊时间开始,每步加slotMinutes,到endTime之前停止;endTime本身不作为开诊时间,否则 12:00 会被多生成一个号源。参数设置上,getSlotMinutes一般填 10、15 或 30,getSlotsPerPeriod默认 1,即一个时间段一个号。生成之前要先删除该医生在work_date当天已有的排班,否则唯一键会报错,报错信息容易让人误以为是数据库异常。
排班发布后还会遇到“医生停诊”的场景。最简单的处理是给排班表加一个status字段,0 表示正常,1 表示停诊。患者端查询排班时只展示status=0的记录;已预约该医生的患者,在停诊操作里批量发送站内信并引导改约。这里要注意:停诊只释放尚未预约的号源,已预约订单要单独走“改约”流程,不能直接取消,否则就变成强制退号了。
4.2 预约与取消:状态机、并发锁与超时释放
预约接口是整个系统并发压力最大的地方。我推荐两层控制:第一层是 Redis 分布式锁,锁的 key 是schedule:lock:{scheduleId},防止同一号源段被并发扣减;第二层是数据库乐观锁,UPDATE doctor_schedule SET remain_slots = remain_slots - 1, version = version + 1 WHERE id = ? AND remain_slots > 0。两层都要有,因为只用 Redis,锁过期或 Redis 宕机时会超卖;只用数据库乐观锁,高并发下大量请求打到数据库,日志里全是更新失败重试。
public AppointmentOrder createOrder(CreateOrderRequest req) { String lockKey = "schedule:lock:" + req.getScheduleId(); boolean locked = redisLock.tryLock(lockKey, 3, TimeUnit.SECONDS); if (!locked) { throw new BizException("当前预约人数较多,请稍后重试"); } try { int updated = scheduleMapper.deductSlot(req.getScheduleId()); if (updated == 0) { throw new BizException("号源已约满"); } AppointmentOrder order = new AppointmentOrder(); order.setOrderNo(generateOrderNo()); order.setPatientId(req.getPatientId()); order.setScheduleId(req.getScheduleId()); order.setStatus(0); order.setPayAmount(req.getAmount()); orderMapper.insert(order); return order; } finally { redisLock.unlock(lockKey); } }参数说明:tryLock的第一个参数 3 秒是获取锁的等待时间,不是锁自动过期时间;第三个参数才是锁的持有时间。如果业务执行时间超过锁持有时间,锁会被自动释放导致并发问题,所以锁内代码不要包含外部 HTTP 调用,只做数据库操作。deductSlot是 Mapper 里带条件更新的 SQL,返回影响记录数,0 表示没扣到号源,直接抛“号源已约满”。订单状态设为 0 表示待支付,支付完成后改成 1。
预约订单的状态机可以概括为一张迁移表:
| 当前状态 | 允许动作 | 目标状态 | 说明 |
|---|---|---|---|
| 待支付 | 支付成功 | 已支付 | 支付回调触发 |
| 待支付 | 取消 / 超时 | 已取消 / 已过期 | 释放号源 |
| 已支付 | 取消(限时段) | 已取消 | 触发退款 |
| 已支付 | 签到就诊 | 已就诊 | 就诊台登记 |
| 已取消 / 已过期 | 无 | 终态 | 不可再修改 |
取消预约的规则要明确:已就诊不能取消;已支付状态取消要触发退款流程;待支付状态取消要立刻释放号源。超时释放建议用定时任务扫描“创建超过 30 分钟仍为待支付”的订单,把状态改成 4 并把remain_slots加回。注意修改订单状态和释放号源必须在同一个事务里,先改订单再回补号源,否则会出现订单已取消但号源没加的半截状态。
4.3 费用支付:回调幂等与对账字段
支付模块不需要真的对接支付机构也能验证流程,但回调处理代码一定要按真实场景写。患者在前端点支付后,后端先创建payment_record,状态为 0,再跳转支付页面。模拟支付时直接调用“支付成功回调”接口即可。回调里有两个必须的幂等设计:payment_no唯一索引防止同一笔支付重复入账;支付金额与订单金额不一致时直接拒绝。
@Transactional public void handlePayCallback(PayCallbackReq req) { PaymentRecord record = paymentMapper.selectByPaymentNo(req.getPaymentNo()); if (record == null || !"SUCCESS".equals(req.getPayStatus())) { return; } if (record.getStatus() != 0) { return; // 已处理过,直接返回 } AppointmentOrder order = orderMapper.selectForUpdate(req.getOrderNo()); if (order == null || order.getPayAmount().compareTo(record.getAmount()) != 0) { return; } record.setStatus(1); paymentMapper.updateById(record); order.setStatus(1); orderMapper.updateById(order); messageService.pushPaySuccess(order); }逻辑说明:selectByPaymentNo先查本地支付记录,没有就忽略;record.getStatus() != 0表示这条支付流水已经处理过,直接幂等返回。selectForUpdate是数据库行锁,防止两个回调线程同时把同一订单从待支付改成已支付。金额比较用compareTo而不用equals,因为BigDecimal.equals会把1.0和1.00判为不相等,这是支付回调里最典型的细节坑。
对账字段建议在payment_record里加channel、pay_time、callback_data。callback_data存原始回调 JSON,排查问题时直接看原始报文,不用再翻三方日志。如果要支持退款,再加refund_amount和refund_status,避免多次退款超过订单金额。这里强烈提醒:消息推送不要放在handlePayCallback事务里,推送失败会导致支付状态回滚,把推送改成事务提交后的异步事件,或者用ApplicationEvent解耦。
5. 落地避坑:预约并发、时间边界与支付回调的常见问题
5.1 余号扣减被超卖:为什么乐观锁也防不住
现象:放号瞬间,同一号源段有 10 个患者同时提交,最后只有 8 个成功,但日志里一条更新失败记录都没有;更严重的是remain_slots已经变成 0,后端预约接口还返回成功。
原因:最常见的是deductSlot的 SQL 没带remain_slots > 0条件,直接写成UPDATE doctor_schedule SET remain_slots = remain_slots - 1, version = version + 1 WHERE id = ?。这样即使余号为 0 也能更新成功,从 0 扣到 -1。另一个原因是前端先调“查询余号”接口,后端把余号缓存一段时间再返回,页面显示有号但实际已经被抢完。查询接口和扣减接口之间没有做关联,余号只是个展示数据。
解决:扣减 SQL 必须写成WHERE id = ? AND remain_slots > 0,并且用updated影响行数判断是否扣号成功。查询余号接口只做展示,不要拿它的结果做库存判断。如果用了 Redis 预扣库存,要在事务提交后才把缓存余号减掉,否则事务回滚后缓存和数据库不一致。我自己习惯的做法是:数据库字段remain_slots是唯一权威,Redis 只用于展示和锁,不做预减。
5.2 患者取消预约后号源没回到池子
现象:患者在“我的预约”里取消一个已支付订单,退款也成功了,但第二天这个时间段依然约不了,remain_slots没有增加。
原因:取消的 Service 方法里,更新订单状态和释放号源不在同一个事务里。常见写法是先orderMapper.updateById,再scheduleMapper.releaseSlot,释放号源时抛了异常,订单状态已提交,号源却被回滚。另一种情况是取消时没校验订单状态,把“已就诊”的订单也当成可取消对象,号源释放逻辑走了,但实际医疗行为已经发生,号源回收后会造成重复约。
解决:在取消方法上加@Transactional(rollbackFor = Exception.class),并且先执行号源释放,再更新订单状态。释放号源的 SQL 要带remain_slots < total_slots条件,防止重复取消时余号被放大。另外角色权限要分开:患者只能取消自己的订单,导诊台可以取消指定订单,但取消理由要记录到cancel_reason字段,方便后续统计爽约率。
5.3 支付回调重复通知导致订单状态漂移
现象:支付平台回调重试了 3 次,第一次成功把订单改为已支付,第二次又把状态改成已取消,患者端显示订单被取消,钱却已经扣了。
原因:回调处理逻辑里没有幂等判断。第一次回调把状态从 0 改成 1,第二次回调进来时没判断当前状态已经是 1,而是直接走了取消分支。更隐蔽的是“查询订单”和“更新订单”写了两个事务,第二次回调读到的是旧状态,然后拿着旧状态去更新。
解决:回调处理必须遵循“先查支付流水状态,再决定是否处理”的幂等模式;payment_record.status非 0 一律直接返回。如果要对账,可以在回调入口加 Redis 锁pay:callback:{orderNo},过期时间设 30 秒,确保同一订单的回调串行执行。不要在同一事务里做“支付状态更新 + 消息推送”,推送失败会导致支付状态回滚,推送应该发到 MQ 或 Spring 事件里异步消费。
5.4 跨天排班用字符串存时间导致的查询错乱
现象:查询某医生一周排班时,周五早上的排班出现在周四的数据里;导出报表时“上午”“下午”排序错乱。
原因:设计表时图省事把start_time和end_time设成 VARCHAR,存"8:00"、"08:30"。MySQL 按字典序排序,"8:00"会排在"12:00"后面,而且"8:00"和"08:00"是两个值,前端传的格式一旦不是后端存储格式,就查不到记录。跨天排班还会出现end_time是次日凌晨 1:00,直接用 TIME 类型也表达不了“次日”语义。
解决:后端实体类用LocalTime,数据库用TIME,JDBC 驱动自动映射,避免字符串比较。跨天排班要单独加end_date字段,或者把排班拆成两段:当天到 23:59,次日 0:00 到 1:00 单独一条记录。这样查询“某天排班”直接用work_date = ?,不用做跨天拼接。时间相关的条件查询也统一用LocalDate入参,避免前端传字符串导致时区偏差。
5.5 消息推送连不上:WebSocket被网关断掉
现象:患者把手机锁屏一段时间后再打开,报告提醒迟迟不出现,刷新后才能看到;部署在 Nginx 反代后面,WebSocket 时不时断开重连。
原因:前端用的 WebSocket 连接在移动端空闲时被系统回收,后端没有做断线重连和离线消息补偿;Nginx 默认proxy_read_timeout60 秒,WebSocket 超过 60 秒没有数据交互就被切断,前端又没有自动重连机制,消息自然推不过来。
解决:推送方案建议优先使用 SSE(Server-Sent Events),底层是 HTTP,Nginx 不用配置升级协议,断线后前端用EventSource自动重连。如果坚持 WebSocket,Nginx 的proxy_read_timeout和proxy_send_timeout要调到 3600 秒,前端监听close事件做指数退避重连。关键业务消息(预约成功、支付成功、报告已出)不能只靠实时推送,必须同时落message表,前端拉取未读消息走普通 HTTP 接口,“实时推送 + 拉取补偿”才是可靠组合。
6. 最后一公里:报告查询与消息推送的优化技巧
报告查询最影响体验的不是接口数量,而是“出报告那一刻的确定性”。患者关心的是“报告出了没有”,而不是“报告查询接口好不好看”。我会在预约订单旁边放一个report_ready状态,由后台的模拟 LIS 接口在生成报告后回调更新,患者端在“我的预约”里就能看到“报告已出,可查看”的角标,而不是非得进报告页才能发现。
具体实现上,报告 PDF 用接口直接返回二进制流,前端用 Blob 接收并交给预览组件,不要为了省事把 PDF 转成 Base64 塞进 JSON,否则 3MB 的报告会让首屏直接卡死,移动端内存也被吃满。下载报告时在响应头里带Content-Disposition: attachment; filename*=UTF-8''report.pdf,中文文件名就能正常显示,避免浏览器把空格解析成+号。
消息推送的另一个实用技巧是“合并未读提醒”。一个患者上午可能收到 3 条推送:预约成功、支付成功、排队提醒,如果三条都实时推,通知栏会被刷屏。我的做法是类型相同的消息合并为一条聚合通知,点击后进入消息列表看明细;未读数放在右上角角标,由拉取接口返回,而不是由每一条推送去修改角标。这样既保留实时性,又不会让用户觉得系统在轰炸。
最后说一个自己踩过的教训:早期这套平台把支付回调和消息推送放在同一个事务里,回调一到,推送服务没起,整个事务回滚,患者钱扣了订单却停在待支付。后来才把推送改成事务提交后的异步事件。如果你在做类似系统,记住一条原则:和钱相关的状态变更要实时同步,和通知相关的动作全部异步化。希望帮到你。
本文还有配套的精品资源,点击获取