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。
[ID] [P?] [Story] DescriptionPurpose: 数据库 DDL 与配置就位
updatesql/sql.md(pos_store_omg、pos_order_omg_payment、pos_order_omg_refund,取自 data-model.md,标注 2026-07-29 OMG 支付接入;不直接执行)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 在代码固定,不入配置Purpose: 签名工具、HTTP 客户端、三实体、Mapper——所有 story 的公共地基
⚠️ CRITICAL: 本阶段完成前(尤其 OmgCheckMacValue 自测通过)不得开始任何 user story
ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgPayConfig.java(merchantId/hashKey/hashIv,构造器+getter,仿 NewebPayConfig 结构但独立包)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 复算应得 AA5842FDA7E55ACEB7118D6353E9822CA6D6FF09A0D1FC129A879DD5CAF93266ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStoreOmg.java(@TableName("pos_store_omg"),字段见 data-model.md §1)ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgPayment.java(@TableName("pos_order_omg_payment"),字段见 data-model.md §2)ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgRefund.java(@TableName("pos_order_omg_refund"),字段见 data-model.md §3)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)ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgPaymentMapper.java + .../mapper/chanting/PosOrderOmgPaymentMapper.xml(insert、selectByTradeNo、selectByMerchantTradeNo、selectLatestByDdId、updateById)ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgRefundMapper.java + .../mapper/chanting/PosOrderOmgRefundMapper.xml(insert、selectByPaymentId、selectByDdId)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;私有 postFormCheckpoint:OmgCheckMacValue.main 自测通过、三表可建、实体/Mapper 就绪 → 可开始 user story
Goal: C 端用户下单后调发起接口拿到 form 参数,前端 Form Post 跳转 OMG 收银台(ChoosePayment=ALL)
Independent Test: 对已开通 OMG 的门店下单 → 调 /pay/omg/create → 返回 gatewayUrl + 全参数(含 CheckMacValue) → 前端跳转 OMG 页出现支付方式;pos_order_omg_payment 落一行(pay_status=0)、pos_order.pay_type="7"
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)ruoyi-system/.../service/IPosStoreOmgService.java + impl/PosStoreOmgServiceImpl.java(纯 DB:getOrCreateByStoreId、getEnabledConfig(storeId) 返回已开通且启用的凭证或 null、getEnabledByMerchantId(merchantId) 回调按商店代号反查凭证),仿 PosStoreEzpayServiceImplruoyi-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、markRefundedruoyi-system/.../domain/PosOrder.java 附近或新建常量类定义 PAY_TYPE_OMG="7"(OMG 在线支付),不改表结构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=yyyy/MM/dd HH:mm:ss/PaymentType=aio/TotalAmount=amount/ReturnURL/ChoosePayment=ALL/EncryptType=1/ItemName/InvoiceMark=N)→ OmgPay.createAioForm 生成 CheckMacValue 与 form 字段 → createPayment 落流水(pay_status=0) → 更新 pos_order.pay_type="7"/pay_url=gatewayUrl → 返回 form 字段(见 contracts/api.md B1)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 更新)
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、用户/商家/骑手收到推送;重复回调不变;金额不符/伪造签名拒绝
OmgPayController.java 实现 POST /pay/omg/notify(@Anonymous):collectForm 收参 → 记 IpnLog → 由 MerchantID 调 getEnabledByMerchantId → 无凭证记录并返回OmgCheckMacValue.verify(全部回调参, hashKey, hashIv),与回传 CheckMacValue 不符 → 拒绝并记录(绝不变更订单),仍回 1|OKTradeNo 查 pos_order_omg_payment(已 pay_status=1 → 直接返回 1|OK);TradeAmt==订单 amount(不符→拒绝核销、告警);由 MerchantTradeNo 反查流水与订单(不存在→记录待查返回);判 RtnCode==1 且 SimulatePaid!=1(模拟支付 SimulatePaid=1 → 不发货、记录)markSuccess(pay_type=回覆 PaymentType 如 Credit_CreditCard、rtn_code、pay_time=PaymentDate、callback_raw)→ 更新 pos_order(state=0, payStatus=1) → orderLogHelper.logSync 记日志 → PayPush+pushEventService.PublisherEvent 推送用户/商家(参照 PosOrderController.sendHdfkMessage 货到付款同款 OrderPushBodyDto.getJson)→ 补全可接单骑手推送:在 OmgPayController 内独立实现(复用 PayPush.sendAcceptRiderPush / 货到付款链路调用方式,不复用 NewebPay 代码,见 CLAUDE.md「sendAcceptRiderPush 迁移」)→ 回应纯字符串 1|OK(注意:非 JSON,与蓝新不同)RtnCode!=1):markFail 写流水(pay_status=2)、订单保持未支付、记 rtn_code+rtn_msg(不硬编码错误码分支,见 contracts §C3)、回 1|OKOmgPayController.java 实现 GET|POST /pay/omg/return(@Anonymous):仅 302 重定向前端结果页(带 ddId),不改订单状态(以 notify 为准)E:\QtwCode\foodie\foodie-store\ 支付结果页:展示支付状态、轮询订单 payStatus(ReturnURL 不改状态,以轮询/回调后状态为准);文案四语 i18n ⏸️ 推迟:同 T017,支付结果页属客户侧Checkpoint: US1+US2 构成完整 MVP——下单→付款→订单已支付→推送,可端到端验证(quickstart 场景 B、C)
Goal: 运营在平台后台录入/验证/启停门店 OMG 凭证(独立于蓝新凭证表)
Independent Test: 后台为门店录入测试凭证 → 调 QueryTradeInfo/V5 验证通过 → 状态已开通 → 该门店可发起支付;停用后不可
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 模式saveCredentials 凭证验证:Controller 调 OmgPay.queryTrade(fictitious MerchantTradeNo) 探测;响应提示金钥/商店错误 → 视为无效拒绝、状态不变、记 last_verify_result;否则 enableWithCredentials(omg_status=2/is_enabled=1)PosStoreOmgServiceImpl:apply(0→1)、enableWithCredentials、toggleEnable、reset、setEnabledPayments、recordVerifyResult(仿 PosStoreEzpayServiceImpl 状态机)E:\QtwCode\foodie\foodie-admin-vue\ 新增门店 OMG 凭证管理页(列表分页/详情/录入凭证/启停/重置/设置方式),文案四语 i18n ✅ 实现为「门店支付配置」Tab 页(OMG Tab 可用 + LINE Pay 占位),克隆 storeNewebpay;权限 chanting:storeOmg:* 不变;菜单 component=mendian/storePayment/index(见 sql.md)chanting:storeOmg:list/query/apply/saveCredentials/toggleEnable,sys_menu 插入 SQL 写入 updatesql/sql.md(仿 storeEzpay 菜单,不直接执行)Checkpoint: 凭证可全流程管理,US1 发起支付有真实凭证来源(不必手插数据)
Goal: ChoosePayment=ALL 下消费者选 ATM/超商时,平台展示 OMG 虚帐/缴费码,消费者期限内付款后回调核销
Independent Test: 选 ATM/超商 → OMG 回调 /pay/omg/paymentInfo 带虚帐/缴费码 → 平台展示 + 期限 → 消费者付款 → /pay/omg/notify(RtnCode=1) → 订单核销(quickstart 场景 D)
OmgPayController.java 实现 POST /pay/omg/paymentInfo(@Anonymous):OmgCheckMacValue.verify → 解析 BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate → 落 pos_order_omg_payment.callback_raw(并可用专用字段/pay_url 存取号码供前端读)→ 回 1|OKGET /pay/omg/paymentInfo/{orderid}(或复用订单查询)返回虚帐/缴费码 + 期限;前端 E:\QtwCode\foodie\foodie-store\ ATM/超商结果页展示 + 倒计时;文案四语 i18n/pay/omg/notify(RtnCode=1)→ 复用 US2 的 markSuccess + 推送链路(pay_type=ATM_*/CVS_*)→ 订单已支付Checkpoint: ATM/超商 全链路(取号→展示→付款→核销)
Goal: 订单取消时,信用卡订单调 OMG DoAction 退款并同步状态;ATM/超商记录待人工
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)
ruoyi-system/.../service/IPosOrderOmgRefundService.java + impl(record(paymentId,ddId,tradeNo,action,amount,rtnCode,rtnMsg,callbackRaw)、listByPayment、listByDdId)OmgPayController.java 实现退款(POST /pay/omg/refund?orderid= 或由取消链路调用):查 OMG 已支付订单 → 信用卡(pay_type=Credit_CreditCard) → OmgPay.doAction(Action=R, TotalAmount=amount) → 写 pos_order_omg_refund → RtnCode==1 则 markRefunded(pay_status=3) + pos_order.pay_status=2;ATM_*/CVS_*/BarcodeATM_* 无 API → 记录待人工处理NeedExtraPaidInfo=Y 取 gwsr,退款前查 CreditDetail/QueryTrade/V2(需盘合后台 CreditCheckCode) 决定 N(已授權)/E(要關帳)/R(已關帳);MVP 可先统一 R,失败再分支PosOrderShOprateController(商家取消) / UserOrderController(用户取消) 取消 OMG 已支付订单时触发退款;相关提示文案四语 i18nCheckpoint: 信用卡退款闭环(stage DoAction 不可用,退款须正式小额或 mock 验证)
Purpose: 多 story 共享的收尾与护栏
/pay/omg/notify、/pay/omg/paymentInfo、/pay/omg/return 确为 @Anonymous 且仅这些匿名;HashKey/HashIV 不出现在任何前端响应/日志明文;CheckMacValue 验签为强制前置;金额校验与 SimulatePaid 守卫不可绕过com.ruoyi.app.utils.omg 及 OmgPayController/PosStoreOmg*/PosOrderOmg* 全部 OMG 新建类,确认无任何 newebpay import(必须为 0,违反硬约束)specs/016-omg-payment/quickstart.md 全部场景 A–E 端到端验证(stage + 测试卡 + 内网穿透),记录结果(退款场景 E 因 stage 不可用需正式小额/mock)updatesql/sql.md 中 SQL 齐全(三建表 + OMG 菜单权限)OmgCheckMacValue 自测必须先过)# 以下任务文件互不冲突,可并行:
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) 含被他人依赖的签名契约,建议优先完成并跑通自测
OmgCheckMacValue 自测必须先过)updatesql/sql.md,不直接执行newebpay 依赖(T039 复核);Apple Pay 归在 Credit 下,回覆 PaymentType=Credit_CreditCard