Feature Branch: 020-omg-payment-rebuild
Created: 2026-08-13
Status: Approved for planning and implementation
Input: 以 OMG 全方位金流 AIO 官方技术文件 V1.5.3(2026-07)为唯一外部事实来源,从零重建创建支付订单;现有 OMG 支付代码、旧支付流水和 specs/016-omg-payment 均不作为需求或设计依据。第一阶段只完成测试环境首次创建并进入 OMG 收银台,其余能力后续逐项重建。
MerchantID / HashKey / HashIV。CheckMacValue。ReturnURL 付款结果通知的接收、验签、核销和订单状态变更。OrderResultURL、PaymentInfoURL、ClientRedirectURL、ClientBackURL。OmgPayController、旧 OMG 工具类、旧支付流水表和 specs/016-omg-payment 均不可信,不得复制其行为或用其解释官方文档。pos_store_omg 及其现有门店凭证存储、录入、启停和查询实现。该表只保存每个门店独立的 MerchantID / HashKey / HashIV 等凭证信息。PosOrder.payType = "2" 表示选择 OMG 支付。MerchantID / HashKey / HashIV,不使用平台统一凭证。PlatformID。ChoosePayment 固定为 ALL;具体显示渠道以该门店在 OMG 后台实际开通的能力为准。/pay/omg/*;创建入口为 POST /pay/omg/create,后续可信回调仍预定为 POST /pay/omg/notify。/legacy/* 或任何其他旧 OMG 接口。omgpay 包目录;新代码不得引用旧支付 Controller、旧签名器、旧表单工具或旧支付流水服务。pos_order_omg_attempt。已登录用户为自己的单门店餐饮订单选择 OMG 后,调用创建接口并取得由服务端签名的表单。客户端在当前页面 POST 该表单,进入对应门店的 OMG 测试收银台,并看到 ALL 下该门店已开通的付款方式。
Why this priority: 这是本阶段唯一交付的用户价值,也是后续回调、查询和退款的前置能力。
Independent Test: 为测试门店配置有效测试凭证,创建一笔合法未支付订单,调用接口并提交响应表单,确认浏览器进入官方测试端点且收银台显示正确订单金额及可用渠道。
Acceptance Scenarios:
payType="2"、未取消、未付款、金额为正的单门店订单,且门店 OMG 凭证已启用,When 用户首次调用创建接口,Then 系统创建唯一支付尝试并返回可提交的 OMG 表单。gatewayUrl POST 全部 formFields,Then 浏览器进入 OMG 测试收银台,不通过 iframe 或新窗口加载。ChoosePayment=ALL,Then 收银台按 OMG 与门店配置展示可用渠道。客户创建表单后没有立即付款,再次点击支付时,系统既不重复提交旧 MerchantTradeNo,也不贸然生成新的 MerchantTradeNo。
Why this priority: ATM、CVS、BarcodeATM 可能在较长期限内仍可付款;多个有效入口可能造成重复付款。
Independent Test: 对同一订单顺序或并发调用创建接口,数据库始终只有一条未结束尝试,后续请求得到 PAYMENT_ATTEMPT_EXISTS,且不会返回第二份可提交表单。
Acceptance Scenarios:
CREATED 尝试,When 用户再次创建,Then 返回 PAYMENT_ATTEMPT_EXISTS,不生成新 MerchantTradeNo,也不重放旧表单。系统只为订单本人、合法业务状态、合法金额且门店凭证可用的测试订单创建支付尝试,并确保门店密钥不出现在响应或日志中。
Why this priority: 创建错误门店、错误金额或泄露密钥会形成直接资金风险。
Independent Test: 分别使用无效 token、他人订单、终态订单、异常金额、错误支付类型、无凭证门店和非测试网关配置调用接口,均被拒绝且不写入尝试表。
Acceptance Scenarios:
payType 不是 "2",When 调用创建接口,Then 系统返回国际化业务错误且不创建尝试。HashKey、HashIV;完整 CheckMacValue 与签名表单只存在于订单本人获准取得的创建成功响应,不出现在错误响应或日志中。MerchantTradeNo 的字符时,系统使用独立生成的英数字编号,不直接拼接或截断业务订单号。MerchantTradeNo 发生唯一索引冲突时,系统可在同一创建事务中重新生成;达到受控次数仍失败时整笔创建回滚。CREATED,不得宣称 OMG 已建立或未付款。TradeDesc、ItemName 不接受客户端文本,必须由服务端生成,无 HTML 标签或未经允许的特殊符号,并满足官方长度限制。ReturnURL 必须是服务端受控的 HTTPS URL,路径固定指向新的 /pay/omg/notify;客户端不得覆盖。/pay/omg/notify 处理器是预期行为;测试付款通知不会被旧回调接收或改变订单状态。com.ruoyi.app.omgpay 下的 Controller、请求/响应 DTO、创建服务、表单生成器、签名器和配置类型;这些新类 MUST NOT 引用旧 OmgPayController、旧 OmgPay、旧 OmgCheckMacValue 或旧 OMG 支付流水服务。com.ruoyi.system.omgpay 下的支付尝试 Entity、Mapper 和 Service;新支付尝试 MUST 使用 pos_order_omg_attempt,不得读取或写入旧 OMG 支付流水表。pos_store_omg 门店凭证查询实现,且这是唯一允许复用的旧 OMG 实现;新创建流程 MUST 按订单门店读取该门店已启用的 MerchantID / HashKey / HashIV。POST /pay/omg/create,使用 @RequestHeader String token 和显式 @RequestBody DTO;Controller 入参不得使用 Map,DTO 不使用 Bean Validation 注解。orderId;金额、门店、用户、支付类型、说明文字、网关地址、回调地址和支付渠道均必须由服务端决定。PosOrder.payType="2";任何校验失败 MUST NOT 创建支付尝试。TotalAmount,不得接受或信任客户端金额。https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5;第一阶段不得配置或回退到正式环境。MerchantTradeNo;不得从业务订单号直接派生可冲突或超长的编号。PAYMENT_ATTEMPT_EXISTS,不得返回旧表单、重复提交旧编号或创建新编号。CREATED 尝试。MerchantID、MerchantTradeNo、MerchantTradeDate、PaymentType=aio、TotalAmount、TradeDesc、ItemName、ReturnURL、ChoosePayment=ALL、EncryptType=1、InvoiceMark=N、NeedExtraPaidInfo=Y、ExpireDate=1、StoreExpireDate=30、BarcodeATMExpireDate=1 和 CheckMacValue。PlatformID、PaymentInfoURL、OrderResultURL、ClientRedirectURL、ClientBackURL、Language、分期、定期定额、记忆卡号或银联专用参数。MerchantTradeDate MUST 以 Asia/Taipei 时区格式化为 yyyy/MM/dd HH:mm:ss。TradeDesc 和 ItemName MUST 由服务端生成,禁止 HTML,符合 OMG 字符及长度限制;ItemName 不得超过中文 60 字或英数字 120 字的官方显示限制,字段总长度不得超过官方 String(200) 限制。ReturnURL MUST 是受控 HTTPS 地址并固定以 /pay/omg/notify 结尾;第一阶段只把它作为 OMG 必填值,不实现该路径的处理器。CheckMacValue MUST 严格按 OMG 官方规则生成:排除 CheckMacValue 本身,将其余全部实际发送字段按官方字母顺序排序,以 & 串接,前置 HashKey=...&、后置 &HashIV=...,执行符合官方 .NET 表的 URL 编码并转小写,使用 SHA-256,最后输出大写十六进制。CheckMacValue 自身外,创建请求实际发送的全部字段 MUST 参加签名,包括 NeedExtraPaidInfo=Y 和三个期限字段;不得挑选所谓核心字段计算。CheckMacValue 外,OMG 实际返回的全部字段均参加验签;启用 NeedExtraPaidInfo=Y 后,全部额外回传字段及空值字段也必须进入验签集合。本阶段只固化该约束,不实现回调。{status, gatewayUrl, formFields};status 固定为 CREATED,gatewayUrl 为测试 AioCheckOut 端点,formFields 含实际需要 POST 的全部字段但不含 gatewayUrl。application/x-www-form-urlencoded 表单 POST 全部 formFields 到 gatewayUrl;不得使用 iframe,不得另开新窗口,不得把响应转换为 GET 查询链接。HashKey、HashIV、完整 CheckMacValue 或整份签名表单;支付尝试只保存本地可确认事实和创建快照。HashKey、HashIV。完整 CheckMacValue 和签名表单只允许出现在通过归属及业务校验的创建成功响应中,不得出现在错误响应或日志中。日志可记录本地支付尝试 ID、脱敏后的 MerchantTradeNo、业务订单号和阶段结果。vi/zh/tw/en 支持来源。OmgPayController MUST 取消 Spring Controller 身份且所有旧接口不可访问;不得保留 legacy 路径。pos_store_omg 凭证表不属于该禁用范围。updatesql/sql.md,实现过程不得执行数据库变更。omgpay 代码 MUST 包含标准且必要的注释:公开类型说明职责和安全边界;协议字段、检查码编码、事务与数据库唯一约束等非显然逻辑说明“为什么”;不为显然的赋值、访问器或框架样板添加重复注释。注释不得包含真实凭证、完整签名原文或可用测试秘密。MerchantTradeNo;业务拒绝记录稳定业务错误码及已有的安全上下文;非预期异常记录相同安全上下文并保留服务端异常堆栈。不得以拼接整份 DTO、凭证对象或表单对象的方式记录日志。POST /pay/omg/create
Content-Type: application/json
token: <login-token>
{
"orderId": "991786433092835"
}
{
"status": "CREATED",
"gatewayUrl": "https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5",
"formFields": {
"MerchantID": "1000031",
"MerchantTradeNo": "OMGR8K3P7W2M9C4X6A1B",
"MerchantTradeDate": "2026/08/13 15:30:23",
"PaymentType": "aio",
"TotalAmount": "100",
"TradeDesc": "Food order 991786433092835",
"ItemName": "Order 991786433092835",
"ReturnURL": "https://example.test/pay/omg/notify",
"ChoosePayment": "ALL",
"EncryptType": "1",
"InvoiceMark": "N",
"NeedExtraPaidInfo": "Y",
"ExpireDate": "1",
"StoreExpireDate": "30",
"BarcodeATMExpireDate": "1",
"CheckMacValue": "<64 uppercase hexadecimal characters>"
}
}
HashKey 和 HashIV 永远不属于响应。外层继续使用项目现有 AjaxResult 成功/失败封装;上例只定义 data 契约。
PAYMENT_ATTEMPT_EXISTS。formFields 或新的 MerchantTradeNo。OmgPaymentAttempt / pos_order_omg_attempt表示本次重建产生的一次不可覆盖的 OMG 支付尝试。第一阶段字段如下:
| Field | Meaning | Constraint |
|---|---|---|
id |
本地支付尝试主键 | 自增主键 |
dd_id |
pos_order.dd_id 业务订单号 |
非空 |
merchant_trade_no |
本次 OMG 特店交易编号 | 非空、全局唯一、≤20 位英数字 |
store_id |
创建时订单门店 | 非空 |
merchant_id |
创建时门店 MerchantID 快照 | 非空、≤10 位 |
amount |
创建时订单整数 TWD 金额快照 | 非空、>0 |
attempt_status |
本地事实状态 | 0=CREATED |
active_dd_id |
单活跃约束生成列 | attempt_status=0 时为 dd_id,否则为 NULL |
create_time |
本地创建时间 | 非空 |
update_time |
本地更新时间 | 非空 |
约束:
UNIQUE (merchant_trade_no);UNIQUE (active_dd_id),利用 MySQL 唯一索引允许多个 NULL 的语义,为后续终态释放活跃键;CREATED 只表示本地已生成并持久化表单所需事实,不能解释为 OMG 已接收、已建立订单或未付款;PosStoreOmg / pos_store_omg可信门店凭证来源。本阶段不修改其表结构、录入流程或启停流程。新创建服务只读取与订单 storeId 对应、已启用的 MerchantID / HashKey / HashIV。
PAYMENT_ATTEMPT_EXISTS 是安全终止,不是系统异常,也不能自动删除或覆盖已有行。ReturnURL 必须由服务端配置并严格校验,不接受客户端 URL,防止开放重定向或向非 OMG 主机泄露签名表单。HashKey / HashIV 只在服务端签名过程中短暂使用;不得复制到新支付尝试表。/pay/omg/create 只能由新 omgpay Controller 映射;/pay/omg/notify 在下一阶段前必须没有旧处理器。INFO,可预期的业务拒绝使用 WARN,非预期且需要调查的异常使用 ERROR 并携带异常对象;单元测试可依赖稳定业务错误码,不依赖自然语言日志文本。MerchantTradeNo 只显示足以关联记录的首尾片段;如果应用已有 trace/request ID,则沿用现有上下文,不自行生成新的支付追踪体系。CheckMacValue 验证完整 SHA-256 签名链路;不得用旧实现测试或其他金流向量代替官方依据。CheckMacValue 字段都会改变签名。MerchantTradeNo 仅含英数字、长度不超过 20、重复冲突不会覆盖旧行。yyyy/MM/dd HH:mm:ss 格式。payType="2"、金额和门店凭证校验。ReturnURL 被拒绝。CREATED 尝试。HashKey 或 HashIV;完整 CheckMacValue 与签名表单只出现在创建成功响应,不出现在错误响应或日志中。pos_store_omg 查询保持可用。所有新增生产方法必须遵循测试先行:先运行定向测试并观察其因缺少新行为而失败,再写最小实现使其通过。
C:\Users\qmj\.jdks\graalvm-jdk-21.0.7 设置当前命令的 JAVA_HOME 和 PATH。omgpay 测试及受旧链路下线影响的订单回归测试。ruoyi-admin 及其依赖模块的 JDK 21 Maven 构建。git diff,确认未改动旧凭证能力、未重写无关文件、未引入编码或换行噪音。POST /pay/omg/create。formFields POST 到返回的测试 gatewayUrl。ALL 可用渠道显示正确。CheckMacValue 完全一致。CheckMacValue 字段均由测试证明参与签名,额外字段与空值字段不会被签名器丢弃。MerchantTradeNo 产生率为 0。/pay/omg/create 只映射到新 Controller。HashKey、HashIV 泄露数为 0;错误响应和应用日志中的完整 CheckMacValue 或完整签名表单泄露数为 0。omgpay 的公开类型、签名编码、事务和数据库并发边界均有必要注释,显然样板代码上的重复注释数为 0。pos_order.dd_id 是客户端使用的业务订单号,PosOrder.mdId 能唯一定位单门店订单的门店。PosOrder.amount 是当前订单应支付的整数新台币金额。pos_store_omg 的既有启用凭证查询能按 storeId 返回正确门店凭证。CREATED 尝试,在可信查询阶段完成前,需要人工准备新业务订单才能再次测试创建;系统不会自动清理或换号。