tasks.md 36 KB


description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"

Tasks: OMG(歐買尬/FunPoint)AIO 支付接入

Input: Design documents from /specs/016-omg-payment/

Prerequisites: plan.md, spec.md, research.md(D1–D13), data-model.md, contracts/api.md(A1–A5/B1–B7/§C), quickstart.md(均已就绪)

Tests: 未要求 TDD;验证以 OmgCheckMacValue.main 自测(官方示例值)+ quickstart.md 场景 A–E 端到端为准。

Organization: 按 user story 组织,每个 story 可独立实现与验证。MVP = US1 + US2(支付闭环)。

⚠️ 硬约束(research §0):OMG 全部代码独立新建,newebpay 依赖(不得 import com.ruoyi.app.utils.newebpay.*NewebPayEncryptUtilNewebPayNewebpayPayControllerPosStoreNewebpay*,不复用 pos_order_payment 表)。仅复用平台共享基础设施:PosOrder/IPosOrderServicePayPush/PushEventServiceOrderLogHelperIpnLogOrderPushBodyDtosendAcceptRiderPush/sendHdfkMessage 链路、InfoUser@Anonymous

Format: [ID] [P?] [Story] Description

  • [P]: 可并行(不同文件、无依赖)
  • [Story]: 归属 user story(US1~US5)
  • 描述含精确文件路径;前端 CRLF 文件用 Python 脚本编辑

Phase 1: Setup(基础设施)

Purpose: 数据库 DDL 与配置就位

  • T001 追加三表建表 DDL 到 updatesql/sql.mdpos_store_omgpos_order_omg_paymentpos_order_omg_refund,取自 data-model.md,标注 2026-07-29 OMG 支付接入;不直接执行)
  • T002 [P] 在 ruoyi-admin/src/main/resources/application.yml(及 application-dev.yml)新增 omg 配置段(见 research D8/contracts B1):base-url=https://payment-stage.funpoint.com.tw(正式 payment.funpoint.com.tw)、return-urlorder-result-urlpayment-info-urlclient-redirect-urlInvoiceMark=NEncryptType=1 在代码固定,不入配置

Phase 2: Foundational(阻塞前置,所有 story 依赖)

Purpose: 签名工具、HTTP 客户端、三实体、Mapper——所有 story 的公共地基

⚠️ CRITICAL: 本阶段完成前(尤其 OmgCheckMacValue 自测通过)不得开始任何 user story

  • T003 [P] 创建 ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgPayConfig.java(merchantId/hashKey/hashIv,构造器+getter,仿 NewebPayConfig 结构但独立包)
  • T004 创建 ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgCheckMacValue.java:静态 generate(Map params, hashKey, hashIv)verify(Map params, hashKey, hashIv)。算法(contracts §C4 / research D1):去 CheckMacValue → key 字母序 → k=v&... → 包夹 HashKey={k}&..&HashIV={iv}URLEncoder.encode(s,UTF_8) → 替换 %2d→- %5f→_ %2e→. %21→! %2a→* %28→( %29→) → 转小写 → SHA256 → hex 大写。含 main 自测:用官方示例 HashKey=5294y06JbISpM5x9/HashIV=v77hoKGq4kWxNNIS 复算应得 AA5842FDA7E55ACEB7118D6353E9822CA6D6FF09A0D1FC129A879DD5CAF93266
  • T005 [P] 创建 ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStoreOmg.java@TableName("pos_store_omg"),字段见 data-model.md §1)
  • T006 [P] 创建 ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgPayment.java@TableName("pos_order_omg_payment"),字段见 data-model.md §2)
  • T007 [P] 创建 ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgRefund.java@TableName("pos_order_omg_refund"),字段见 data-model.md §3)
  • T008 [P] 创建 ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreOmgMapper.java + ruoyi-system/src/main/resources/mapper/chanting/PosStoreOmgMapper.xml(基础 CRUD + selectStoreList/selectStoreDetail VO 查询,仿 PosStoreEzpayMapper)
  • T009 [P] 创建 ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgPaymentMapper.java + .../mapper/chanting/PosOrderOmgPaymentMapper.xml(insert、selectByTradeNo、selectByMerchantTradeNo、selectLatestByDdId、updateById)
  • T010 [P] 创建 ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgRefundMapper.java + .../mapper/chanting/PosOrderOmgRefundMapper.xml(insert、selectByPaymentId、selectByDdId)
  • T011 [P] 创建 ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgPay.java@Component,HTTP 客户端,httpclient4+fastjson2):端点常量 URL_AIO_CHECKOUT=/Cashier/AioCheckOut/V5URL_QUERY_TRADE_INFO=/Cashier/QueryTradeInfo/V5URL_DO_ACTION=/CreditDetail/DoActioncreateAioForm(baseUrl,cfg,params)→对全部参数生成 CheckMacValue 后返回 form 字段 Map(含 gatewayUrl);queryTrade(baseUrl,cfg,merchantTradeNo)→响应 k=v 解析为 Map;doAction(baseUrl,cfg,merchantTradeNo,tradeNo,action,totalAmount)→响应 Map;私有 postForm

CheckpointOmgCheckMacValue.main 自测通过、三表可建、实体/Mapper 就绪 → 可开始 user story


Phase 3: User Story 1 - 发起 OMG 支付 (Priority: P1) 🎯 MVP part 1

Goal: C 端用户下单后调发起接口拿到 form 参数,前端 Form Post 跳转 OMG 收银台(新订单仅信用卡与 Apple Pay)

Independent Test: 对已开通 OMG 的门店下单 → 调 /pay/omg/create → 返回 gatewayUrl + 全参数(含 CheckMacValue) → 前端跳转 OMG 页出现支付方式;pos_order_omg_payment 落一行(pay_status=0)、pos_order.pay_type="2"

  • T012 [P] [US1] 创建 ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/StoreOmgCredentialDto.java(storeId/merchantId/hashKey/hashIv/enabledPayments,@Valid)与 ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/PosStoreOmgVo.java(列表/详情 VO)
  • T013 [US1] 实现 ruoyi-system/.../service/IPosStoreOmgService.java + impl/PosStoreOmgServiceImpl.java(纯 DB:getOrCreateByStoreIdgetEnabledConfig(storeId) 返回已开通且启用的凭证或 null、getEnabledByMerchantId(merchantId) 回调按商店代号反查凭证),仿 PosStoreEzpayServiceImpl
  • T014 [US1] 实现 ruoyi-system/.../service/IPosOrderOmgPaymentService.java + impl/PosOrderOmgPaymentServiceImpl.javacreatePayment(ddId,merchantTradeNo,storeId,merchantId,amount,choosePayment)getByTradeNogetByMerchantTradeNogetLatestByDdIdmarkSuccess(...payType,rtnCode,rtnMsg,authCode,payTime,callbackRaw)(幂等:tradeNo 已 pay_status=1 返回 0)、markFailmarkRefunded
  • T015 [US1] 在 ruoyi-system/.../domain/PosOrder.java 附近或新建常量类定义 PAY_TYPE_OMG="2"(OMG 在线支付),不改表结构
  • T016 [US1] 创建 ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java 实现 POST /pay/omg/create@Anonymous+@Auth,Header token):校验订单存在/未支付/属当前用户/金额>0 → getEnabledConfig(mdId)(无则 error("该门店暂不支持线上支付"))→ MerchantTradeNo="OMG"+清洗(ddId) → 组参(MerchantID/MerchantTradeNo/MerchantTradeDate/PaymentType=aio/TotalAmount/ReturnURL/ChoosePayment=Credit/UnionPay=2/EncryptType=1/ItemName/InvoiceMark=N)→ 生成 CheckMacValue 与 form 字段 → 落流水(pay_status=0) → 更新 pos_order.pay_type="2"/pay_url=gatewayUrl → 返回 form 字段(见 contracts/api.md B1)
  • T017 [US1] 前端 E:\QtwCode\foodie\foodie-store\ 新增「OMG 支付」入口:下单后调 /pay/omg/create,用返回字段构建隐藏 form 自动 submit 到 gatewayUrl;支付相关文案用 $t() 四语 i18n(zh/tw/en/vi,key 加到对应对象层级,有意义命名) ⏸️ 推迟:属客户侧流程(客户下单→Form Post 跳 OMG),foodie-store 是商家端无此入口,客户 uni-app 不在当前工作区,待其就位再做

Checkpoint: 发起支付链路通,可跳转 OMG 测试收银台(此时回调未接,订单状态待 US2 更新)


Phase 4: User Story 2 - 支付回调核销与履约触发 (Priority: P2) 🎯 MVP part 2

Goal: OMG ReturnURL 回调到达后,验签 CheckMacValue + 幂等 + 金额 + RtnCode/SimulatePaid 校验,更新订单 payStatus=1 并触发推送(含可接单骑手);/pay/omg/return 仅引导

Independent Test: US1 发起并跳转后用测试卡付款 → OMG 回调 /pay/omg/notify → 订单 payStatus=1、pos_order_omg_payment 写 trade_no/pay_type=Credit_CreditCard/rtn_code=1、用户/商家/骑手收到推送;重复回调不变;金额不符/伪造签名拒绝

  • T018 [US2] 在 OmgPayController.java 实现 POST /pay/omg/notify@Anonymous):collectForm 收参 → 将脱敏回调内容写入 IpnLog(type=omg)→ 由 MerchantIDgetEnabledByMerchantId → 无凭证记录并返回
  • T019 [US2] notify 验签:OmgCheckMacValue.verify(全部回调参, hashKey, hashIv),与回传 CheckMacValue 不符 → 拒绝并记录(绝不变更订单),仍回 1|OK
  • T020 [US2] notify 幂等+金额+状态校验:按 TradeNopos_order_omg_payment(已 pay_status=1 → 直接返回 1|OK);TradeAmt==订单 amount(不符→拒绝核销、告警);由 MerchantTradeNo 反查流水与订单(不存在→记录待查返回);判 RtnCode==1 且 SimulatePaid!=1(模拟支付 SimulatePaid=1 → 不发货、记录)
  • T021 [US2] notify 成功业务链路:markSuccess(pay_type=回覆 PaymentType 如 Credit_CreditCard、rtn_code、pay_time=PaymentDate、callback_raw)→ 原子更新 pos_order(state IN (0,1), pay_status=0→1) → orderLogHelper.logSync 记日志 → PayPush+pushEventService.PublisherEvent 推送用户/商家(参照 PosOrderController.sendHdfkMessage 货到付款同款 OrderPushBodyDto.getJson)→ 补全可接单骑手推送:在 OmgPayController 内独立实现(复用 PayPush.sendAcceptRiderPush / 货到付款链路调用方式,不复用 NewebPay 代码,见 CLAUDE.md「sendAcceptRiderPush 迁移」)→ 回应纯字符串 1|OK(注意:非 JSON,与蓝新不同)
  • T022 [US2] notify 失败处理(RtnCode!=1):markFail 写流水(pay_status=2)、订单保持未支付、记 rtn_code+rtn_msg(不硬编码错误码分支,见 contracts §C3)、回 1|OK
  • T023 [US2] 在 OmgPayController.java 实现 GET|POST /pay/omg/return@Anonymous):仅 302 重定向前端结果页(带 ddId),不改订单状态(以 notify 为准)
  • T024 [US2] 前端 E:\QtwCode\foodie\foodie-store\ 支付结果页:展示支付状态、轮询订单 payStatus(ReturnURL 不改状态,以轮询/回调后状态为准);文案四语 i18n ⏸️ 推迟:同 T017,支付结果页属客户侧

Checkpoint: US1+US2 构成完整 MVP——下单→付款→订单已支付→推送,可端到端验证(quickstart 场景 B、C)


Phase 5: User Story 5 - 门店 OMG 凭证开通管理 (Priority: P2)

Goal: 运营在平台后台录入/验证/启停门店 OMG 凭证(独立于蓝新凭证表)

Independent Test: 后台为门店录入测试凭证 → 调 QueryTradeInfo/V5 验证通过 → 状态已开通 → 该门店可发起支付;停用后不可

  • T025 [US5] 创建 ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreOmgController.java/system/storeOmg@PreAuthorize("chanting:storeOmg:*")):list/{storeId}/apply/{storeId}/saveCredentials/toggleEnable/{storeId}/reset/{storeId}/enabledPayments/{storeId},仿 PosStoreEzpayController 模式
  • T026 [US5] saveCredentials 凭证验证:Controller 调 OmgPay.queryTrade(fictitious MerchantTradeNo) 探测;响应提示金钥/商店错误 → 视为无效拒绝、状态不变、记 last_verify_result;否则 enableWithCredentials(omg_status=2/is_enabled=1)
  • T027 [US5] 扩展 PosStoreOmgServiceImplapply(0→1)、enableWithCredentialstoggleEnableresetsetEnabledPaymentsrecordVerifyResult(仿 PosStoreEzpayServiceImpl 状态机)
  • T028 [US5] 平台后台 E:\QtwCode\foodie\foodie-admin-vue\ 新增门店 OMG 凭证管理页(列表分页/详情/录入凭证/启停/重置/设置方式),文案四语 i18n ✅ 实现为「门店支付配置」Tab 页(OMG Tab 可用 + LINE Pay 占位),克隆 storeNewebpay;权限 chanting:storeOmg:* 不变;菜单 component=mendian/storePayment/index(见 sql.md)
  • T029 [US5] 菜单与权限:新增菜单项 + 权限标识 chanting:storeOmg:list/query/apply/saveCredentials/toggleEnablesys_menu 插入 SQL 写入 updatesql/sql.md(仿 storeEzpay 菜单,不直接执行)

Checkpoint: 凭证可全流程管理,US1 发起支付有真实凭证来源(不必手插数据)


Phase 6: User Story 3 - 非即时支付(已撤销)

Goal: 不实现 ATM、超商或其他非即时支付;当前全是开发测试数据,没有历史交易兼容范围。

Independent Test: 新支付表单不包含 PaymentInfoURLClientRedirectURL 或延期缴费期限参数,且控制器不暴露延期支付回调/返回接口。

  • T030–T032 [US3] 原延期支付任务撤销;相关入口不进入新 omgpay 实现。

Checkpoint: 当前范围只有信用卡和 Apple Pay。


Phase 7: User Story 4 - 订单取消与退款 (Priority: P3)

Goal: 订单取消时,信用卡/Apple Pay 订单调 OMG DoAction 退款并同步状态

Independent Test: 对一笔 OMG 已支付信用卡订单发起取消 → 调 DoAction(Action=R) → pos_order_omg_refund 记录、pos_order_omg_payment.pay_status=3pos_order.pay_status=2;失败→记 rtn_msg 可重试(quickstart 场景 E)

  • T033 [US4] 实现 ruoyi-system/.../service/IPosOrderOmgRefundService.java + implrecord(paymentId,ddId,tradeNo,action,amount,rtnCode,rtnMsg,callbackRaw)listByPaymentlistByDdId
  • T034 实现 POST /pay/omg/refund:查唯一有效的已支付尝试 → DoAction(Action=R, TotalAmount=amount) → 写退款流水;成功后同步支付尝试和订单退款状态。
  • T035 [US4](可选/增强)状态化 Action:下单时 NeedExtraPaidInfo=Ygwsr,退款前查 CreditDetail/QueryTrade/V2(需盘合后台 CreditCheckCode) 决定 N(已授權)/E(要關帳)/R(已關帳);MVP 可先统一 R,失败再分支
  • T036 [US4] 接入订单取消链路:PosOrderShOprateController(商家取消) / UserOrderController(用户取消) 取消 OMG 已支付订单时触发退款;相关提示文案四语 i18n

Checkpoint: 信用卡退款闭环(stage DoAction 不可用,退款须正式小额或 mock 验证)


Phase 8: Polish & Cross-Cutting

Purpose: 多 story 共享的收尾与护栏

  • T037 [P] 四语 i18n 统一校验:foodie-store 与 foodie-admin-vue 的 zh.js/tw.js/en.js/vi.js 中 OMG 支付相关 key 四文件齐全、命名有意义、位于正确对象层级(用 i18n-consistency-checker 或人工核)
  • T038 [P] 安全复核:/pay/omg/notify/pay/omg/result 为外部返回入口;HashKey/HashIV 不出现在前端响应或日志;CheckMacValue 验签、金额校验与 SimulatePaid 守卫不可绕过
  • T039 [P] 零蓝新依赖复核:grep com.ruoyi.app.utils.omgOmgPayController/PosStoreOmg*/PosOrderOmg* 全部 OMG 新建类,确认无任何 newebpay import(必须为 0,违反硬约束)
  • T040 运行 specs/016-omg-payment/quickstart.md 全部场景 A–E 端到端验证(stage + 测试卡 + 内网穿透),记录结果(退款场景 E 因 stage 不可用需正式小额/mock)
  • T041 文档一致性:确认 spec.md/plan.md/tasks.md 描述一致;updatesql/sql.md 中 SQL 齐全(三建表 + OMG 菜单权限)

Phase 9: User Story 6 - OMG 漏单补单(回调可靠性)(Priority: P2)

Goal: 异步回调丢失/延迟不丢单。方案A 被动补单接口(前端结果页轮询仍未支付时主动查 OMG 真实状态补单)+ 方案B 定时兜底(用户付完即关 App 且回调丢失的极端场景)。

Source: callback-reconcile.md(方案A/B 设计)

Independent Test:

  • 方案A:测试卡付款成功但模拟不触发回调 → 订单 payStatus=0 → 调 POST /pay/omg/query → 查 OMG TradeStatus=1 → 补单 markSuccess + 订单 payStatus=1;回调+补单并发不重复(trade_no 幂等)
  • 方案B:构造 pay_status=0 流水(create_time 在窗口内)→ 等定时任务触发 → 自动补单;窗口外(>7天)/已取消订单不扫

  • [x] T042 [P] [US6] contracts/api.md:§B5 POST /pay/omg/query 由「可选/未实现」补全为正式契约(入参/鉴权/返回/TradeStatus 分支/幂等/自愈);新增 §B8 定时补单(@Scheduled+Redisson)说明

  • [x] T043 [US6] 抽取共享补单核销逻辑(OmgPayController):applyPaidResult(...)(notify 成功分支改调此,消除重复)+ public reconcileByQuery(ddId, source)(queryTrade→金额校验→applyPaidResult/markFail,含幂等与跨事务中断自愈),供 /query 与定时任务复用

  • [x] T044 [US6] 方案A:OmgPayController 新增 POST /pay/omg/query@Anonymous @Auth,校验登录+订单本人+payType=2+未支付未取消)→ 调 reconcileByQuery → 返回 {payStatus:0/1/2, reconciled};补单写 orderLogHelper 日志

  • [x] T045 [US6] 方案B 数据层:PosOrderOmgPaymentMapperselectLeakOrderDdIds(windowStart, graceCutoff, batchSize)(每订单取最新一笔 pay_status=0,INNER JOIN pos_order 排除 state=4)+ XML;IPosOrderOmgPaymentServicescanLeakOrders(...)

  • [x] T046 [US6] 方案B 任务:新建 ruoyi-admin/.../app/task/OmgReconcileTask.java@Scheduled fixedDelay 默认3分钟 + Redisson 分布式锁 lock:omg:reconcile,多实例单节点执行)→ 扫漏单逐笔调 reconcileByQuery(ddId,"scheduled")RuoYiApplication@EnableScheduling

  • [x] T047 [P] [US6] 配置:application.ymlomg.reconcile.*(fixed-delay-ms 默认180000 / window-hours 默认168=7天覆盖延期 ExpireDate / grace-minutes 默认2 / batch-size / lock-wait-seconds / lock-lease-seconds)

  • [x] T048 [US6] DDL:updatesql/sql.md 追加 pos_order_omg_payment 扫描索引 idx_pay_status_create(pay_status, create_time)(窗口扫描用,不直接执行)

  • [ ] T049 [US6] 方案A/B 测试:模拟回调丢失→/query 补单、回调+补单并发不重复、定时任务扫描补单(依赖测试环境 stage+测试卡,随 T040 端到端阶段做)

  • [ ] T050 [US6] 前端配合(frontend-integration.md §4.3):客户 uni-app 结果页轮询 >30s 仍 payStatus=0 时改调 /query —— ⏸️ 推迟:客户 uni-app 不在当前工作区(同 T017/T024)

Checkpoint: 回调丢失不丢单;方案A 用户侧自愈、方案B 离线兜底,A+B 覆盖全部漏单场景


Phase 10: 账本重构(追加式 S)+ 问题 A/B 收尾(Priority: P1)

Goal: 修复问题 A(每次 create 堆积流水,连点 9 行)+ 问题 B 余波(reconcile 漏单/节流),落地追加式 S(双轴 is_active ⊥ pay_status,物理保留历史行接迟到 notify + 审计)。逻辑上每订单 1 条活跃未付行(is_active=1),换号=新增行+旧行 is_active→0(pay_status 不动),mapper 刻意不提供 updateMerchantTradeNo。

Source: payment-attempt-lifecycle.md(§6 S 方案 + §8 落地顺序 + §0 现状 + §9 待拍板)

Prerequisites: ✅ bugB 验签已修(1a424c3 CASE_INSENSITIVE_ORDER,queryTrade 可靠);✅ parseKvResponse 尾随& 放宽(7a8e248);⏳ collation 3 表 CONVERT 待执行(sql.md 2026-08-12 节)。

Independent Test: 同订单重复 create 只保留并复用一条有效支付尝试;查询、回调和自动补偿均定位该尝试;同门店多订单查询不触发 OMG 限流。

  • T051 [P] 前置:parseKvResponse 重复键放宽(ruoyi-admin/.../utils/omg/OmgPay.java):重复键 last-wins 不抛(用同一份 Map 重算 CheckMacValue 验签,避 kill-shot ⑥ 解析放宽与验签打架);仅整段无 = 或缺 CheckMacValue 才抛;OmgPayTest 补重复键用例。S 依赖 queryTrade 做换号/补单安全网,重复键仍抛会让安全网失效。
  • T052 collation 对齐执行确认:执行 updatesql/sql.md 2026-08-12 节(pos_order_omg_payment/pos_order_omg_refund/pos_store_omg CONVERT 到 utf8mb4_unicode_ci);执行后 selectLeakOrderDdIds JOIN 不再报 Illegal mix of collations。
  • T053 reconcile 扫描改全量未付(PosOrderOmgPaymentMapper.xml + Service):selectLeakOrderDdIdsEXISTS(任意 pay_status=0 行) 去重 ddId(不再 MAX(id) 取一条);新增 listUnpaidByDdId(全量 pay_status=0,is_active 不限);selectLatestPaidByDdIdIN(1,3,4)(修退款中/已退返 null);新增 selectLatestRefundableByDdId(pay_status IN(1,3,4))、existsPaidByDdId
  • T054 reconcileByQuery 两段式(OmgPayController.reconcileByQuery):① 先 selectLatestPaidByDdId(pay_status=1 且 order.payStatus=0 → handlePaymentSuccess 自愈,L774 不能变死代码);② 否则 listUnpaidByDdId 遍历全量未付行 queryTrade:TradeStatus=1 applyPaidResult break / 0 no-op / 10200095 且 trade_no IS NULL markFail / 10200095 且 trade_no 非空 no-op。
  • T055 节流闸(防 OMG 403)(✅ OmgQueryThrottle.java + Redis,commit db043b4 + fc46fed):per-MerchantID 令牌桶 omg:qt:token:{merchantId}(1token/3s,burst1)acquire 失败 skip 本行本轮;per-ddId TTL(omg:reconcile:dd:{ddId} 180s / omg:query:dd:{ddId} 60s)。reconcileByQuery 与 /query 接入。⚠ 原"闸1 首查延迟 40min"已移除(fc46fed)——是对 OMG 节流的误读(403 是频率触发,非"下单 N 分钟内不能查"),会让已付客户 notify 丢失时等 40min;防 403 靠这两道频率闸,"刚创建未付"由扫描窗口 2min 宽限保护。
  • T056 DDL 追加式 S 字段(写 updatesql/sql.md,不直接执行):pos_order_omg_payment + expire_date DATETIME NULL(仅 paymentInfo 回调写,create 不猜)+ is_active TINYINT NOT NULL DEFAULT 1 + 生成列 active_dd_id=IF(is_active=1 AND pay_status=0,dd_id,NULL) VIRTUAL + UNIQUE uk_omg_active_dd(active_dd_id)(需 MySQL 5.7.6+,生产版本待确认;不够则退化 idx_omg_dd_active(dd_id,is_active,pay_status)+app 锁+CAS);历史回填(每 ddId 最新 pay_status=0 置 is_active=1,其余 0;终态行 0;绝不回填 expire_date)。
  • T057 实体 + mapper(PosOrderOmgPayment.java + Mapper/XML):+ isActive/expireDate 字段(activeDdId 生成列只读可不映射);加 selectActiveForReuse(ddId,freshMin)(WHERE dd_id AND is_active=1 AND pay_status=0 AND trade_no IS NULL AND create_time>=NOW()-INTERVAL freshMin MINUTE ORDER BY id DESC LIMIT 1)、markActiveHistorical(ddId)(CAS UPDATE is_active=0 WHERE dd_id AND is_active=1 AND pay_status=0,不动 pay_status)、markPaymentInfoIfOpen 扩 expire_date 解析;刻意不提供 updateMerchantTradeNo/updateTradeNo(从能力杜绝换号丢钱)。
  • T058 create 复用预检 ✅(commit 000f47f;实际:PaymentCreateGuardService afterCompletion 锁 + isSynchronizationActive 自愈,共享 OMG/LINE Pay)(OmgPayController.create,保留 @Transactional):Redisson RLock lock:omg:create:{ddId} tryLock(等 3s),锁释放在 TransactionSynchronizationManager.registerSynchronization 的 afterCommit(仿 WalletService.returnPoints,禁 try/finally unlock——否则 TX 提交前泄锁致重复活跃行);selectActiveForReuse 命中复用 MTN(form 用当前时刻重算 CheckMacValue);未命中同 TX:markActiveHistorical(旧 is_active→0)+ createPayment 新行(is_active=1, expire_date=NULL);DuplicateKeyException 重试 3 次(uk_omg_active_dd DB 兜底)。
  • T059 读侧 getLatestByDdId 迁移 ✅(OmgPayController 4 处 db043b4 + OrderLifecycleService 3 处 b9a0514;buildContext canReconcileOmg 重构为 hasOmgActivity):getPaymentInfo/refundOrderOutcome/L569 早退/markRefunding 并发/confirmManualRefundOutcome/reconcileByQuery(T054)/OrderLifecycleService.buildContext/validateOmgReconcile/validateOmgRefundselectLatestRefundableByDdId(pay_status IN(1,3,4))或 listPaidByDdId;修"订单未支付"早退为 listPaidByDdId 为空才 FAILED。
  • T060 砍掉 退款遍历(2026-08-12 用户拍板:双付不现实——用户不会对同一订单付两笔,实测 bugA 9 行也只 1 笔 paid。退款维持退单条已付行即可,无需遍历多笔)(refundOrderOutcome):改 listPaidByDdId 遍历所有 pay_status=1 且 trade_no 非空行;每行先查 pos_order_omg_refund 是否 action=R 且 rtnCode=1 → 跳过;否则 markRefundingIfPaid(1→4 CAS)→ DoAction(Action=R) 逐笔退(每笔独立 tradeNo);ATM/超商无退款 API 走 manual pending;孤立已付(双付第二笔)写 ipn_log(type=omg_orphan_paid) + 运营告警。
  • T061 砍掉 历史 9 行迁移(2026-08-12 用户:不管历史数据。新读路径对历史行仍兼容——可退款行优先)(orders 如 991786433092835 id=18~26):迁移前对每行 queryTrade 确认(已可靠);TradeStatus=1 的行无论本地 pay_status 一律保留并回填 pay_status=1(如 id=26 OMGFE0494FB7AD1474A8);仅 TradeStatus=10200095 或(0 且过 ExpireDate 且非信用卡)才允许清理;优先 is_active 软标记而非物理删除(lifecycle §9 Q10)。
  • T062 端到端验证(stage):create 重复点击复用单一有效尝试、query/notify/自动补偿完成核销、两道节流闸不触发 403;历史数据与延期支付不纳入验证。
  • T063 统一 OMG 支付类型为 2OmgPayControllerOrderLifecycleService、订单创建逻辑、实体/DTO 注释、016 规格与测试夹具统一使用 payType=2paymentMethod=2 不再写成货到付款 collectPayment=1;增加常量一致性回归测试。不迁移历史数据,不处理其他 OMG 评审问题。
  • T064 修复堂食订单支付核销OmgPayController.handlePaymentSuccess 的订单 CAS 从仅允许 state=0 改为允许 state IN (0,1),并继续要求 pay_status=0;回调和主动补单复用同一路径,配送中、已完成、已取消状态不核销;增加 CAS 条件回归测试。
  • T065 取消后迟到付款由自动补偿扫描并复用信用卡退款服务,保持幂等且不触发履约。
  • T066 支付尝试生命周期收敛为单一有效尝试;开发测试阶段不迁移历史数据、不兼容延期支付。
  • T067 P1 修正 OMG 查询节流公平性(随 T066 设计):只有实际准备发送 QueryTradeInfo 时才同时获取 ddId 与 MerchantID 额度,任一额度失败不得留下无效 ddId 占位;扫描不得长期固定由相同首批订单抢占门店 token,应增加公平轮转/next_query_at;将 3 秒 MerchantID 限制标注为实测配置而非当前公开官方契约,并补多订单同门店无饥饿测试。

Checkpoint: 问题 A 收敛(连点单活跃 T058)、问题 B 闭环(reconcile 全量补单 T054 + 两道频率节流闸 T055)、追加式 S 落地(is_active 双轴 + mapper 杜绝换号 + 读侧迁移 T059)。T060 退款遍历砍掉(双付不现实,退款维持退单条已付行)、T061 历史 9 行迁移砍掉(不管历史数据)。T062 stage 端到端待测试环境。

待 stage 确认(不阻塞):同 MTN 重复 POST、生产 MySQL 版本、节流精确参数、/query 轮询频率。


Phase 13: 新实现自动补偿(Priority: P1)

  • T076 在 pos_order_omg_attempt 增加自动查询调度字段、到期索引、Mapper 条件扫描及原子预留。
  • T077 在新 omgpay 目录增加定时任务、全局分布式锁、有限批次、重试间隔和单轮时间预算。
  • T078 自动任务复用可信查询验签与付款状态机,不复制回调或状态更新逻辑。
  • T079 增加锁竞争、预留竞争、单条异常继续、自动查询支付成功补单及 Mapper/SQL 契约测试源码。
  • T080 全部 OMG 功能调整结束后统一运行 Maven 编译、定向测试和 stage 联调;本批按用户要求暂不执行。

Checkpoint:自动补偿代码已实现;SQL 尚未手动执行,编译、测试及 stage 联调统一留到全部 OMG 功能完成后执行。


Phase 14: OMG 后端返回页与 App 唤醒(Priority: P1)

  • T081 创建表单加入 OrderResultURL,不生成 ClientBackURL;返回页固定使用项目 App scheme 与支付结果页面。
  • T082 新建 Client POST 验签服务,复用完整原始表单 DTO;未知额外字段与空值全部参与 CheckMacValue,验签页不写订单状态。
  • T083 新建 /pay/omg/result 控制器和 no-store/CSP HTML 渲染器,自动跳转 pages/OrderList/paySuccess/paySuccess?ddId={订单ddId} 并保留手动按钮;不实现 /pay/omg/back
  • T084 将支付结果 Client POST 的完整内容写入现有 ipn_log 并记录可复制调试日志,HashKey/HashIV 不进入日志或页面。
  • T085 增加表单字段、固定 App scheme/path、完整验签、额外字段篡改、单一返回路由、HTML 头和原始请求缓存测试源码。
  • T086 固定返回 com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId={订单ddId};App 页面打开后调用 /pay/omg/query
  • T087 全部 OMG 功能调整结束后统一运行 Maven 编译、定向测试和 stage 联调;本批按用户要求暂不执行。

Checkpoint:后端支付结果返回页固定唤醒 App 的 pages/OrderList/paySuccess/paySuccess,参数名为 ddId


Phase 15: 支付方式收敛为信用卡与 Apple Pay(Priority: P0)

  • T088 将新支付表单固定为 ChoosePayment=CreditUnionPay=2,并保证两个字段都参加 CheckMacValue 计算。
  • T089 从新支付表单移除 ExpireDateStoreExpireDateBarcodeATMExpireDateClientRedirectURL 及对应无用配置;删除延期支付客户端返回路由,不保留历史兼容。
  • T090 更新表单与返回路由测试源码,精确断言只生成信用卡/Apple Pay 所需字段且不暴露延期支付返回接口。
  • T091 全部 OMG 功能调整结束后统一运行 Maven 编译、定向测试和 stage 收银台验证;本批按用户要求暂不执行。

Checkpoint:代码层已限制订单只进入信用卡(含 Apple Pay)收银台并隐藏银联;开发测试阶段不保留延期支付兼容入口。


Phase 16: iOS App 唤起诊断(Priority: P0)

  • T092 为中转页诊断契约增加回归测试,覆盖统一日志前缀、自动/手动唤起、页面生命周期、异常监听和敏感字段不暴露。
  • T093 在 OmgPaymentReturnPageRenderer 增加脱敏浏览器诊断日志和分阶段唤起观察,不改变支付状态或 App Scheme 契约。
  • T094 在 frontend-integration.md 增加 uni-app App 生命周期、Scheme/newintent、当前路由、支付 WebView 事件日志代码及日志收集步骤。
  • T095 使用 JDK 21 运行返回页定向测试和 OMG 回归测试;iOS 真机日志采集由 App 接入人员部署后执行。

Checkpoint:单次真机复现可同时取得 [OMG-RETURN] 浏览器日志与 [OMG-APP] App 日志,并据此判断失败发生在页面脚本、WebView Scheme 交接、iOS App 唤起还是 App 路由阶段。


Phase 17: iOS uni-app Bridge 回跳修复(Priority: P0)

  • T096 增加返回页回归测试,覆盖自托管 SDK、固定 App 内路由、UniAppJSBridgeReady、App 环境检测、uni.webView.redirectTo、Scheme 兜底和 CSP 同源脚本许可。
  • T097 在 OmgPaymentReturnPageRenderer 实现 Bridge 优先回跳;Bridge 不可用、非 App 环境、环境检测异常或调用后未发生交接时回退现有固定 Scheme,手动按钮保持可重试。
  • T098 将官方 uni.webview.1.5.8.js 固定打包到 /static/omg/,CSP 只允许同源脚本;不引入运行时第三方 CDN,不开放动态跳转目标。
  • T099 将 [OMG-RETURN] 载荷改为 JSON 字符串并补充 Bridge/兜底事件,同步 spec.mdplan.mdcontracts/api.mdfrontend-integration.md
  • T100 使用 JDK 21 运行返回页定向测试、OMG 回归测试和 ruoyi-admin 模块构建,并校验内联脚本语法及本地 SDK 与官方 1.5.8 源码一致。
  • T101 部署后分别进行 iOS/Android 真机 OMG 信用卡支付回跳验收,确认结果页进入后继续调用 /pay/omg/query

Phase 18: Bridge SDK 匿名访问修复(Priority: P0)

  • T102 根据真机日志和线上 HTTP 响应确认原 /static/omg/uni.webview.1.5.8.js 返回 401 JSON,导致 SDK 未执行及 bridge_ready_timeout
  • T103 先增加失败测试,再由 OmgPaymentReturnController 提供匿名只读 GET /pay/omg/bridge.js,返回正确 JavaScript Content-Type、no-store 与已打包 SDK。
  • T104 返回页切换到专用 Bridge 地址并同步 spec、plan、contracts、前端接入文档;JDK 21 定向测试通过。

Checkpoint:App 内 OMG 返回页优先走 uni-app 内部路由,Android和外部浏览器仍保留固定 Scheme;支付状态继续只由 /pay/omg/query 确认。


Dependencies & Execution Order

Phase 依赖

  • Phase 1 Setup: 无依赖,立即开始
  • Phase 2 Foundational: 依赖 Phase 1 —— 阻塞所有 user storyOmgCheckMacValue 自测必须先过)
  • Phase 3 US1: 依赖 Phase 2
  • Phase 4 US2: 依赖 US1(回调处理依赖发起产生的流水与订单关联)
  • Phase 5 US5(凭证管理): 依赖 Phase 2(凭证表/Mapper);可与 US1/US2 并行(US1 测试可临时手插凭证数据)
  • Phase 6 US3(非即时支付): 已撤销,不进入当前交付范围
  • Phase 7 US4(退款): 依赖 US1+US2(依赖已支付流水与 trade_no)
  • Phase 8 Polish: 依赖所有欲交付的 story 完成

User Story 独立性

  • US1: 依赖 Foundational,无跨 story 依赖
  • US2: 依赖 US1(闭环下半段)
  • US3: 已撤销
  • US4: 依赖 US1+US2
  • US5: 依赖 Foundational,可与 US1/US2 并行

并行机会

  • Phase 2 的 T003/T005/T006/T007/T008/T009/T010/T011 互不冲突,可全部并行(T004 CheckMacValue 优先跑通自测)
  • US5(Phase 5)可与 US1/US2(Phase 3/4)并行(不同文件、不同端)
  • Phase 8 的 T037/T038/T039 可并行

Parallel Example: Foundational

# 以下任务文件互不冲突,可并行:
Task: "OmgPayConfig in ruoyi-admin/.../utils/omg/OmgPayConfig.java"
Task: "PosStoreOmg in ruoyi-system/.../domain/PosStoreOmg.java"
Task: "PosOrderOmgPayment in ruoyi-system/.../domain/PosOrderOmgPayment.java"
Task: "PosOrderOmgRefund in ruoyi-system/.../domain/PosOrderOmgRefund.java"
Task: "PosStoreOmgMapper + XML"
Task: "PosOrderOmgPaymentMapper + XML"
Task: "PosOrderOmgRefundMapper + XML"
Task: "OmgPay HTTP 客户端骨架 in ruoyi-admin/.../utils/omg/OmgPay.java"
# OmgCheckMacValue(T004) 含被他人依赖的签名契约,建议优先完成并跑通自测

Implementation Strategy

MVP First(US1 + US2)

  1. Phase 1 Setup + Phase 2 Foundational(OmgCheckMacValue 自测必须先过
  2. Phase 3 US1(发起支付)+ Phase 4 US2(回调处理)→ 端到端验证(quickstart 场景 B、C)
  3. STOP and VALIDATE: 下单→信用卡付款→订单已支付→推送(含可接单骑手),全链路通
  4. 此时已是最小可用产品

增量交付

  1. MVP(US1+US2)→ 验证 → 可上测试环境
  2. + US5(凭证管理)→ 去除手插数据、运营自助
  3. + US4(退款)→ 售后资金闭环
  4. Polish(i18n/安全/零蓝新复核/全场景验证)→ 可转正式

并行策略

  • Foundational 完成后:开发者 A 做 US1+US2(主链路),开发者 B 做 US5(凭证管理前后端),互不阻塞
  • US3/US4 待主链路稳定后接力

  • T104 根据 iOS 真机 bridge_redirect_exception: ReferenceError 追踪官方 SDK 派发链路,确认当前 App 运行时缺少 SDK 依赖的启动 WebView UniPlusBridge
  • T105 在返回页增加 iOS 父 WebView evalJS -> uni.redirectTo 路径并保留 Android 官方 Bridge 与固定 Scheme 兜底,同时为异常日志补充脱敏 message。

Notes

  • [P] 任务 = 不同文件、无依赖;同文件多改动须串行
  • 前端 CRLF 文件(foodie-store/foodie-admin-vue)用 Python 脚本编辑,避免 Edit 匹配失败
  • 所有数据库变更写 updatesql/sql.md,不直接执行
  • 每个任务或逻辑组完成后提交(按用户指示);在 test 分支开发,不新建分支
  • HashKey/HashIV 为敏感凭证:仅后端持有,日志脱敏,不下发前端
  • 硬约束:全程零 newebpay 依赖(T039 复核);Apple Pay 归在 Credit 下,回覆 PaymentType=Credit_CreditCard