---
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.*`、`NewebPayEncryptUtil`、`NewebPay`、`NewebpayPayController`、`PosStoreNewebpay*`，不复用 `pos_order_payment` 表）。仅复用平台共享基础设施：`PosOrder`/`IPosOrderService`、`PayPush`/`PushEventService`、`OrderLogHelper`、`IpnLog`、`OrderPushBodyDto`、`sendAcceptRiderPush`/`sendHdfkMessage` 链路、`InfoUser`、`@Anonymous`。

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

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

---

## Phase 1: Setup（基础设施）

**Purpose**: 数据库 DDL 与配置就位

- [x] T001 追加三表建表 DDL 到 `updatesql/sql.md`（`pos_store_omg`、`pos_order_omg_payment`、`pos_order_omg_refund`，取自 data-model.md，标注 2026-07-29 OMG 支付接入；不直接执行）
- [x] 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-url`、`order-result-url`、`payment-info-url`、`client-redirect-url`；`InvoiceMark=N`、`EncryptType=1` 在代码固定，不入配置

---

## Phase 2: Foundational（阻塞前置，所有 story 依赖）

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

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

- [x] T003 [P] 创建 `ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgPayConfig.java`（merchantId/hashKey/hashIv，构造器+getter，仿 `NewebPayConfig` 结构但独立包）
- [x] 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`
- [x] T005 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStoreOmg.java`（`@TableName("pos_store_omg")`，字段见 data-model.md §1）
- [x] T006 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgPayment.java`（`@TableName("pos_order_omg_payment")`，字段见 data-model.md §2）
- [x] T007 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgRefund.java`（`@TableName("pos_order_omg_refund")`，字段见 data-model.md §3）
- [x] 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）
- [x] T009 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgPaymentMapper.java` + `.../mapper/chanting/PosOrderOmgPaymentMapper.xml`（insert、selectByTradeNo、selectByMerchantTradeNo、selectLatestByDdId、updateById）
- [x] T010 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgRefundMapper.java` + `.../mapper/chanting/PosOrderOmgRefundMapper.xml`（insert、selectByPaymentId、selectByDdId）
- [x] T011 [P] 创建 `ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgPay.java`（`@Component`，HTTP 客户端，httpclient4+fastjson2）：端点常量 `URL_AIO_CHECKOUT=/Cashier/AioCheckOut/V5`、`URL_QUERY_TRADE_INFO=/Cashier/QueryTradeInfo/V5`、`URL_DO_ACTION=/CreditDetail/DoAction`；`createAioForm(baseUrl,cfg,params)`→对全部参数生成 CheckMacValue 后返回 form 字段 Map（含 gatewayUrl）；`queryTrade(baseUrl,cfg,merchantTradeNo)`→响应 k=v 解析为 Map；`doAction(baseUrl,cfg,merchantTradeNo,tradeNo,action,totalAmount)`→响应 Map；私有 `postForm`

**Checkpoint**：`OmgCheckMacValue.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"`

- [x] 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）
- [x] T013 [US1] 实现 `ruoyi-system/.../service/IPosStoreOmgService.java` + `impl/PosStoreOmgServiceImpl.java`（纯 DB：`getOrCreateByStoreId`、`getEnabledConfig(storeId)` 返回已开通且启用的凭证或 null、`getEnabledByMerchantId(merchantId)` 回调按商店代号反查凭证），仿 PosStoreEzpayServiceImpl
- [x] T014 [US1] 实现 `ruoyi-system/.../service/IPosOrderOmgPaymentService.java` + `impl/PosOrderOmgPaymentServiceImpl.java`：`createPayment(ddId,merchantTradeNo,storeId,merchantId,amount,choosePayment)`、`getByTradeNo`、`getByMerchantTradeNo`、`getLatestByDdId`、`markSuccess(...payType,rtnCode,rtnMsg,authCode,payTime,callbackRaw)`(幂等:tradeNo 已 pay_status=1 返回 0)、`markFail`、`markRefunded`
- [x] T015 [US1] 在 `ruoyi-system/.../domain/PosOrder.java` 附近或新建常量类定义 `PAY_TYPE_OMG="2"`（OMG 在线支付），不改表结构
- [x] 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、用户/商家/骑手收到推送；重复回调不变；金额不符/伪造签名拒绝

- [x] T018 [US2] 在 `OmgPayController.java` 实现 `POST /pay/omg/notify`（`@Anonymous`）：collectForm 收参 → 将脱敏回调内容写入 IpnLog（`type=omg`）→ 由 `MerchantID` 调 `getEnabledByMerchantId` → 无凭证记录并返回
- [x] T019 [US2] notify 验签：`OmgCheckMacValue.verify(全部回调参, hashKey, hashIv)`，与回传 `CheckMacValue` 不符 → 拒绝并记录（绝不变更订单），仍回 `1|OK`
- [x] T020 [US2] notify 幂等+金额+状态校验：按 `TradeNo` 查 `pos_order_omg_payment`（已 pay_status=1 → 直接返回 `1|OK`）；`TradeAmt==订单 amount`（不符→拒绝核销、告警）；由 `MerchantTradeNo` 反查流水与订单（不存在→记录待查返回）；判 `RtnCode==1 且 SimulatePaid!=1`（模拟支付 `SimulatePaid=1` → 不发货、记录）
- [x] 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，与蓝新不同）
- [x] T022 [US2] notify 失败处理（`RtnCode!=1`）：`markFail` 写流水(pay_status=2)、订单保持未支付、记 `rtn_code`+`rtn_msg`（不硬编码错误码分支，见 contracts §C3）、回 `1|OK`
- [x] 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` 验证通过 → 状态已开通 → 该门店可发起支付；停用后不可

- [x] 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` 模式
- [x] T026 [US5] `saveCredentials` 凭证验证：Controller 调 `OmgPay.queryTrade(fictitious MerchantTradeNo)` 探测；响应提示金钥/商店错误 → 视为无效拒绝、状态不变、记 `last_verify_result`；否则 `enableWithCredentials`(omg_status=2/is_enabled=1)
- [x] T027 [US5] 扩展 `PosStoreOmgServiceImpl`：`apply`(0→1)、`enableWithCredentials`、`toggleEnable`、`reset`、`setEnabledPayments`、`recordVerifyResult`（仿 PosStoreEzpayServiceImpl 状态机）
- [x] T028 [US5] 平台后台 `E:\QtwCode\foodie\foodie-admin-vue\` 新增门店 OMG 凭证管理页（列表分页/详情/录入凭证/启停/重置/设置方式），文案四语 i18n  ✅ 实现为「门店支付配置」Tab 页(OMG Tab 可用 + LINE Pay 占位)，克隆 storeNewebpay；权限 chanting:storeOmg:* 不变；菜单 component=mendian/storePayment/index（见 sql.md）
- [x] T029 [US5] 菜单与权限：新增菜单项 + 权限标识 `chanting:storeOmg:list/query/apply/saveCredentials/toggleEnable`，`sys_menu` 插入 SQL 写入 `updatesql/sql.md`（仿 storeEzpay 菜单，不直接执行）

**Checkpoint**: 凭证可全流程管理，US1 发起支付有真实凭证来源（不必手插数据）

---

## Phase 6: User Story 3 - 非即时支付（已撤销）

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

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

- [x] 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=3`、`pos_order.pay_status=2`；失败→记 `rtn_msg` 可重试（quickstart 场景 E）

- [x] T033 [US4] 实现 `ruoyi-system/.../service/IPosOrderOmgRefundService.java` + `impl`（`record(paymentId,ddId,tradeNo,action,amount,rtnCode,rtnMsg,callbackRaw)`、`listByPayment`、`listByDdId`）
- [x] T034 实现 `POST /pay/omg/refund`：查唯一有效的已支付尝试 → `DoAction(Action=R, TotalAmount=amount)` → 写退款流水；成功后同步支付尝试和订单退款状态。
- [x] T035 [US4]（可选/增强）状态化 Action：下单时 `NeedExtraPaidInfo=Y` 取 `gwsr`，退款前查 `CreditDetail/QueryTrade/V2`(需盘合后台 `CreditCheckCode`) 决定 `N(已授權)/E(要關帳)/R(已關帳)`；MVP 可先统一 `R`，失败再分支
- [x] 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 或人工核）
- [x] T038 [P] 安全复核：`/pay/omg/notify`、`/pay/omg/result` 为外部返回入口；HashKey/HashIV 不出现在前端响应或日志；CheckMacValue 验签、金额校验与 `SimulatePaid` 守卫不可绕过
- [x] T039 [P] **零蓝新依赖复核**：grep `com.ruoyi.app.utils.omg` 及 `OmgPayController`/`PosStoreOmg*`/`PosOrderOmg*` 全部 OMG 新建类，确认无任何 `newebpay` import（必须为 0，违反硬约束）
- [ ] T040 运行 `specs/016-omg-payment/quickstart.md` 全部场景 A–E 端到端验证（stage + 测试卡 + 内网穿透），记录结果（退款场景 E 因 stage 不可用需正式小额/mock）
- [x] 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](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 数据层：`PosOrderOmgPaymentMapper` 加 `selectLeakOrderDdIds(windowStart, graceCutoff, batchSize)`（每订单取最新一笔 `pay_status=0`，INNER JOIN pos_order 排除 state=4）+ XML；`IPosOrderOmgPaymentService` 加 `scanLeakOrders(...)`
- [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.yml` 加 `omg.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](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 限流。

- [x] T051 [P] **前置:parseKvResponse 重复键放宽**(`ruoyi-admin/.../utils/omg/OmgPay.java`):重复键 last-wins 不抛(用同一份 Map 重算 CheckMacValue 验签,避 kill-shot ⑥ 解析放宽与验签打架);仅整段无 `=` 或缺 CheckMacValue 才抛;`OmgPayTest` 补重复键用例。S 依赖 queryTrade 做换号/补单安全网,重复键仍抛会让安全网失效。
- [x] 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。
- [x] T053 **reconcile 扫描改全量未付**(`PosOrderOmgPaymentMapper.xml` + Service):`selectLeakOrderDdIds` 改 `EXISTS(任意 pay_status=0 行)` 去重 ddId(不再 MAX(id) 取一条);新增 `listUnpaidByDdId`(全量 pay_status=0,is_active 不限);`selectLatestPaidByDdId` 扩 `IN(1,3,4)`(修退款中/已退返 null);新增 `selectLatestRefundableByDdId`(pay_status IN(1,3,4))、`existsPaidByDdId`。
- [x] 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。
- [x] 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 宽限保护。
- [x] 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**)。
- [x] 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**(从能力杜绝换号丢钱)。
- [x] 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 兜底)。
- [x] T059 **读侧 getLatestByDdId 迁移** ✅(OmgPayController 4 处 db043b4 + OrderLifecycleService 3 处 b9a0514;buildContext canReconcileOmg 重构为 hasOmgActivity):`getPaymentInfo`/`refundOrderOutcome`/L569 早退/`markRefunding` 并发/`confirmManualRefundOutcome`/`reconcileByQuery`(T054)/`OrderLifecycleService.buildContext`/`validateOmgReconcile`/`validateOmgRefund` → `selectLatestRefundableByDdId`(pay_status IN(1,3,4))或 `listPaidByDdId`;修"订单未支付"早退为 listPaidByDdId 为空才 FAILED。
- [x] 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)` + 运营告警。
- [x] 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；历史数据与延期支付不纳入验证。
- [x] T063 **统一 OMG 支付类型为 2**：`OmgPayController`、`OrderLifecycleService`、订单创建逻辑、实体/DTO 注释、016 规格与测试夹具统一使用 `payType=2`；`paymentMethod=2` 不再写成货到付款 `collectPayment=1`；增加常量一致性回归测试。不迁移历史数据，不处理其他 OMG 评审问题。
- [x] T064 **修复堂食订单支付核销**：`OmgPayController.handlePaymentSuccess` 的订单 CAS 从仅允许 `state=0` 改为允许 `state IN (0,1)`，并继续要求 `pay_status=0`；回调和主动补单复用同一路径，配送中、已完成、已取消状态不核销；增加 CAS 条件回归测试。
- [x] T065 取消后迟到付款由自动补偿扫描并复用信用卡退款服务，保持幂等且不触发履约。
- [x] 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）

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

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

---

## Phase 14: OMG 后端返回页与 App 唤醒（Priority: P1）

- [x] T081 创建表单加入 `OrderResultURL`，不生成 `ClientBackURL`；返回页固定使用项目 App scheme 与支付结果页面。
- [x] T082 新建 Client POST 验签服务，复用完整原始表单 DTO；未知额外字段与空值全部参与 CheckMacValue，验签页不写订单状态。
- [x] T083 新建 `/pay/omg/result` 控制器和 no-store/CSP HTML 渲染器，自动跳转 `pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}` 并保留手动按钮；不实现 `/pay/omg/back`。
- [x] T084 将支付结果 Client POST 的完整内容写入现有 `ipn_log` 并记录可复制调试日志，HashKey/HashIV 不进入日志或页面。
- [x] T085 增加表单字段、固定 App scheme/path、完整验签、额外字段篡改、单一返回路由、HTML 头和原始请求缓存测试源码。
- [x] 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）

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

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

---

## Phase 16: iOS App 唤起诊断（Priority: P0）

- [x] T092 为中转页诊断契约增加回归测试，覆盖统一日志前缀、自动/手动唤起、页面生命周期、异常监听和敏感字段不暴露。
- [x] T093 在 `OmgPaymentReturnPageRenderer` 增加脱敏浏览器诊断日志和分阶段唤起观察，不改变支付状态或 App Scheme 契约。
- [x] T094 在 `frontend-integration.md` 增加 uni-app App 生命周期、Scheme/newintent、当前路由、支付 WebView 事件日志代码及日志收集步骤。
- [x] 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）

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

---

## Phase 18: Bridge SDK 匿名访问修复（Priority: P0）

- [x] T102 根据真机日志和线上 HTTP 响应确认原 `/static/omg/uni.webview.1.5.8.js` 返回 401 JSON，导致 SDK 未执行及 `bridge_ready_timeout`。
- [x] T103 先增加失败测试，再由 `OmgPaymentReturnController` 提供匿名只读 `GET /pay/omg/bridge.js`，返回正确 JavaScript Content-Type、no-store 与已打包 SDK。
- [x] 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 story**（`OmgCheckMacValue` 自测必须先过）
- **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

```bash
# 以下任务文件互不冲突，可并行：
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 待主链路稳定后接力

---

- [x] T104 根据 iOS 真机 `bridge_redirect_exception: ReferenceError` 追踪官方 SDK 派发链路，确认当前 App 运行时缺少 SDK 依赖的启动 WebView `UniPlusBridge`。
- [x] 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`
