Date: 2026-09-17 | Spec: spec.md
研究方式:直接阅读现有代码与 i18n 资源(无外部依赖需要调研)。所有决策均基于仓库内已验证的事实。
Decision: 新建 FlashDeliveryNotificationService(ruoyi-admin 的 com.ruoyi.app.flashdelivery.service 包),完整镜像 DeliveryOrderNotificationService 的模式:TransactionSynchronizationManager 的 afterCommit 回调 + AsyncManager.me().execute(TimerTask) 异步执行 + PayPush.userPushHandleLocal / qsPushHandleLocal 发送。
Rationale:
DeliveryOrderNotificationService(ruoyi-admin/.../order/DeliveryOrderNotificationService.java)是外送订单推送的现行实现,模式经过生产验证。PayPush.*HandleLocal 系列封装了 locale 解析、MessageUtils.message(...) 多语言渲染、PublisherEvent 入库(满足 FR-006/FR-008)与 cid 判空跳过。ruoyi-admin 符合 CLAUDE.md 模块边界(依赖外部 HTTP 的集成代码不进 ruoyi-system)。Alternatives considered:
FlashDeliveryApplicationService 里调 PayPush:把推送基础设施混进 1200+ 行的应用服务,且无法单测路由;放弃。@TransactionalEventListener:项目无此用法先例,与现有模式不一致;放弃。Decision: 全部复用现有 key(已验证 4 个 key × 6 个语言文件全部齐备,含 th_TH):
| 事件 | title key | content key | zh_CN 文案 |
|---|---|---|---|
| 骑手抢单 (→ACCEPTED) | no.message.push.message |
no.message.push.delivery.personnel.receiving.order |
骑手已接单 |
| 骑手取件 (→PICKED_UP) | no.message.push.message |
no.message.push.delivery.personnel.qspsz.order |
骑手配送中 |
| 骑手送达 (→DELIVERED) | no.message.push.message |
no.message.push.delivery.personnel.qsysd.order |
骑手已送达 |
| 订单取消 (→CANCELLED) | no.message.push.message |
no.message.push.order.cancelled |
订单已取消 |
Rationale:
PayPush.*HandleLocal 会在 content 后拼接 ,NO:{订单号}(满足 FR-007)。Alternatives considered: 新增 flash.delivery.push.* 专属 key(如"閃送騎手已取件"):文案可更精确区分闪送与外卖,但要新增 4×6=24 条翻译且收益有限;如后续需要区分再追加。
Decision: payload 用 OrderPushBodyDto.getJson(orderNo, status, -1, 3):ddId 字段承载闪送 orderNo(形如 FD + 24 位),state 承载状态枚举字符串(如 ACCEPTED),type=-1(非语音提醒),pushType=3 标识闪送订单(现有取值 1=外卖、2=充值)。
Rationale: 客户端已按 OrderPushBodyDto 的形状解析 payload 并按 pushType 路由;复用形状让客户端只需新增一个 pushType 分支。orderNo 是面向用户的订单标识(与外卖 ddId 同角色)。
Alternatives considered: 新建 FlashDeliveryPushBodyDto:字段更语义化但客户端要适配两种形状;放弃。
Decision:
FlashDeliveryOrder.userId(userPushHandleLocal)。order.riderId != null 时推骑手(qsPushHandleLocal),否则跳过。Rationale: 与 spec FR-001~FR-005 一一对应;riderId 判空即覆盖"未接单取消不推骑手"。
Decision: 通知服务注入 InfoUserMapper,用 selectById 取 cid。
Rationale: 闪送模块(FlashDeliveryApplicationService)现用 InfoUserMapper 而非 IInfoUserService,模块内保持一致。
Decision: 镜像 DeliveryOrderNotificationServiceTest 的手法:
spy 服务 + doNothing 打断包级可见的 send 方法(其内部是真实 HTTP 调用,不可单测),验证接收人判定与跳过逻辑;无事务同步时 runAfterCommit 同步执行,无需 Spring 上下文。FlashDeliveryNotificationService 注入,verify 各状态流转方法成功路径调用对应 notify、失败路径与排除事件(complete、cancelExpiredScheduledOrder、加小费)never() 调用。仓库已有 MockedStatic 先例(FlashDeliveryApplicationServiceTest)可参考构造方式。Rationale: 与项目现有测试风格完全一致,不引入新测试设施。
Decision: 不额外开发——PayPush.userPush/qsPush 内部先 pushEventService.PublisherEvent(...)(写 push_message,即 FR-008 的消息记录)再判 cid;cid 为空只记 error 日志并跳过云端推送。两个行为均由复用通道免费获得。
无 NEEDS CLARIFICATION 遗留;不新增接口端点。2026-09-18 追加的新单推送需要 1 个字段、2 个专属 i18n key、1 个独立任务与骑手候选查询,具体见 D8-D10。
is_display 同时控制列表可见性与通知资格Decision: 立即单创建为 true;预约单创建为 false。预约任务在半开时间窗 [scheduledPickupStartAt, scheduledPickupEndAt) 内执行带完整业务条件的 false -> true 更新,只有影响 1 行者发送通知。
Rationale: 若列表仅依赖时间表达式,而推送由分钟任务触发,骑手会先看到订单、最多一分钟后才收到通知。统一使用数据库门闩后,展示与通知资格由同一次原子写入开启;每秒调度把正常延迟控制在约一秒内,数据库条件更新承担多实例幂等。
Decision: 新建 FlashDeliveryAvailabilityTask.openScheduledOrders(),Quartz 表达式 0/1 * * * * ?,禁止同任务并发;不把逻辑放入既有 FlashDeliveryAutoCompleteTask。
Rationale: 开放展示属于可抢单生命周期,和送达自动完成、预约过期取消的职责及频率不同。独立 Bean 便于单独调整、监控和停用;批内单笔失败隔离,下一秒可重试尚未成功开放的订单。
Decision: 候选骑手须为在线骑手,支持 FLASH(空值按历史兼容)、车型匹配、处于配置半径内,并复用普通/加急闪送接单互斥规则;按距离选最近 20 人。外卖和闪送分别使用 no.message.push.new.food.order、no.message.push.new.flash.order。
Rationale: 推送对象与实际能抢单的人群一致,减少无效提醒;专属文案让骑手无需打开 App 即可区分业务类型。