Date: 2026-08-13 Payment-method decision updated: 2026-08-18 Decision source: OMG AIO 官方技术文件 V1.5.3 与已批准的 spec.md
本次已完整检查 OMG AIO 文档首页、介接流程、测试设置、7 个 API 页面与 8 个参考页面,共 18 页。创建支付协议只采用官方页面,不以现有 OMG 代码或 specs/016-omg-payment 解释协议。
结论:
POST application/x-www-form-urlencoded 到 https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5。ChoosePayment 是单值字段,不能传 Credit,ApplePay 之类组合;Credit 与 ApplePay 是两个独立取值。MerchantTradeNo 必须唯一、不可重复使用、最多 20 个 ASCII 英数字。PaymentType=aio、EncryptType=1、InvoiceMark=N、NeedExtraPaidInfo=Y。IgnorePayment 仅在 ChoosePayment=ALL 时生效,官方公开可用值不含 AFTEE;测试环境又没有商户后台渠道开关,因此 ALL 无法满足“严格不显示 AFTEE”的需求。CheckMacValue 自身外,实际发送的每个字段都参加检查码计算。CheckMacValue 排除。ReturnURL 发送最终付款结果;特店验证并正确处理后必须回复纯文本 1|OK,否则回复 0|ErrorMessage,OMG 会重试通知。RtnCode=1 是付款成功,其余代码均为异常;错误代码持续新增,因此保存原始 RtnCode/RtnMsg,不建立固定失败码枚举。SimulatePaid=1 官方表示模拟付款。项目测试阶段经业务批准仍把它同步为已付款,但必须保留标记便于识别。NeedExtraPaidInfo=Y 增加的所有回传字段都参加 CheckMacValue,包括官方示例中的空值字段。RtnCode=2 与 CVS/BarcodeATM 的 10100073 是独立 PaymentInfoURL 取号通知,不属于本期 /pay/omg/notify 最终付款结果;当前创建表单没有发送 PaymentInfoURL。HashKey/HashIV 快照,避免门店一行凭证后来被覆盖导致在途交易无法验证。ipn_log,完整请求也直接输出应用日志;数据库密钥不进入任何日志。TimeStamp 三分钟有效期只是查询请求的防重放规则,不是创建表单或支付尝试的有效期,不能据此自动换新交易号。决定实现新的 com.ruoyi.app.omgpay.OmgCheckMacSigner,不引用旧 OMG 签名器。
算法顺序:
null 字段值和调用方预先传入的 CheckMacValue。"" 的字段,不做过滤。key=value&key=value 串接。HashKey=<key>&,后置 &HashIV=<iv>。自动化测试使用官方附录向量:期望检查码为 AA5842FDA7E55ACEB7118D6353E9822CA6D6FF09A0D1FC129A879DD5CAF93266。官方公开范例键值只用于测试向量,不使用任何门店真实凭证。
Decision: App 先选择 CREDIT 或 APPLE_PAY,后端白名单映射为 ChoosePayment=Credit + UnionPay=2 或 ChoosePayment=ApplePay;不发送 ChoosePayment=ALL 或 IgnorePayment。
Rationale: 用户要求测试环境只保留信用卡和 Apple Pay,但测试环境没有商户后台渠道开关。官方 IgnorePayment 无法排除 AFTEE,而一个请求也不能同时指定 Credit 与 ApplePay。因此必须在 App 自有界面先选择渠道,再由服务端生成单渠道签名表单;这样无需依赖第三方页面配置,也不会让客户端控制 OMG 原始参数。Apple Pay 是否能完成仍取决于门店开通状态和当前设备环境。
Rejected:
ALL + IgnorePayment:只能隐藏 ATM、CVS、BarcodeATM,无法隐藏 AFTEE。ChoosePayment=Credit:能隐藏其他渠道,但会同时失去独立 Apple Pay 入口。Decision: 复用现有 IPosStoreOmgService#getEnabledCredential(Long storeId) 与 pos_store_omg。
Rationale: 用户明确确认这部分只负责每个门店独立记录和读取 MerchantID / HashKey / HashIV,属于可信实现。新创建流程只读取,不修改凭证表、录入、验证或启停逻辑。
Rejected:
当前真实下单代码对单门店订单使用 PosOrder.ddId == PosOrder.parentDdId;多门店父单的每个子订单使用带三位序号的 ddId,而 parentDdId 保持原父单号。
Decision: 创建请求的 orderId 必须精确命中一条 pos_order.dd_id,且满足:
parent_dd_id = dd_id;md_id 非空;user_id。这会拒绝父单没有对应 pos_order 行的请求,也会拒绝多门店子订单。金额只取该行 amount。
Decision: 使用数据库行锁与新表唯一键,不复用旧 OMG 的三分钟窗口或旧支付流水服务。
事务顺序:
SELECT 目标 pos_order 行并 FOR UPDATE。payType="2"、payStatus=0、金额和门店。pos_order_omg_attempt.active_dd_id = ddId。PAYMENT_ATTEMPT_EXISTS,不生成新交易号、不重放旧表单。CREATED 尝试并提交事务。同一订单的并发请求会阻塞在订单行锁上;后到请求在前一事务提交后读取到活跃尝试,因此不会生成第二个交易号。UNIQUE(active_dd_id) 是其他写入路径绕过行锁时的最终防线,UNIQUE(merchant_trade_no) 防止全局交易号碰撞。
Rejected:
MerchantTradeDate 与重复提交语义没有官方保证。新表 pos_order_omg_attempt 记录本地可确认事实:业务订单、交易号、门店、MerchantID/密钥快照、金额、状态、网关结果和时间。
状态为 0=CREATED, 1=PAID, 2=FAILED, 3=SUPERSEDED。CREATED 只表示本地表单事实已生成并持久化;PAID 是不可逆资金事实;FAILED 可被后续成功通知升级;SUPERSEDED 释放同订单的其他活动入口。表内保存创建时的 HashKey/HashIV 快照,但不保存 CheckMacValue 或完整创建表单。
omgpay,只配置受控 HTTPS return-url。return-url 必须无 user-info、query 和 fragment,路径必须精确为 /pay/omg/notify。/pay/omg/notify 由新 omgpay Controller 注册,只处理最终付款结果;不处理 PaymentInfoURL 取号通知。POST /pay/omg/create 使用 @RequestHeader String token 和显式 @RequestBody OmgCreatePaymentRequest,DTO 只含 orderId 与受控 paymentMethod。retry 使用相同渠道字段;query/refund 仍只含 orderId。
成功返回 AjaxResult.success(data),其中 data 为:
{
"status": "CREATED",
"gatewayUrl": "https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5",
"formFields": {}
}
业务失败返回 AjaxResult.error(message, {"status":"<stable-code>"})。订单不存在和越权使用同一外部状态 ORDER_NOT_AVAILABLE,避免泄露订单存在性;重复尝试使用 PAYMENT_ATTEMPT_EXISTS。
FOR UPDATE、生成列唯一键和重复键恢复说明原因。旧 OmgPayController、旧回调 DTO、旧退款 DTO、旧补单任务、旧支付/退款实体与 Mapper/Service 不再保留运行时入口。订单取消和管理端移除旧 OMG 退款/补单调用,OrderLifecycleService 移除旧表依赖。pos_store_omg 与其凭证管理代码保持不变。
这项删除只负责阻止旧代码在旧表删除后被调用,不为回调、退款、补单或状态核销提供替代实现;这些能力必须在后续独立规格中重新设计。