Files
mpay_v2_webman/doc/backend/payment-runtime-contract.md
技术老胡 0e5de50337 1. 维护代码健壮
2. 更新项目结构文档
2026-04-27 16:20:41 +08:00

244 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 支付运行时数据契约
本文档约定支付链路里订单扩展字段、插件入参和插件返回值的结构。当前项目仍处于开发阶段,不保留旧平铺结构兼容。
## 1. `ext_json` 职责
`ext_json` 只保存单据恢复、页面承接、插件运行所需的轻量扩展信息。通知、回调、重试、原始报文不进入 `ext_json`
专门表职责如下:
- `ma_pay_callback_log`:保存每一次渠道回调原始参数、请求摘要、验签状态、处理状态、插件解析结果。
- `ma_notify_task`:按 `event_type + ref_no` 保存商户通知内容、通知状态、重试次数、最后响应。
- `ma_channel_notify_log`:保存渠道通知或查单类日志。
- `ma_pay_order`:保存支付尝试当前状态、渠道单号、回调状态、错误码和页面承接快照。
## 2. `ma_biz_order.ext_json`
业务单只保存稳定业务上下文。同一商户订单号复用时会校验这些字段,支付载体参数不得放在业务单里。
```php
[
'_protocol_version' => 'v1', // 顶层强语义字段,方便后台查询和排障
'merchant' => [
'param' => '商户透传参数',
'buyer' => '商户侧买家标识,可选',
],
]
```
## 3. `ma_pay_order.ext_json`
支付单是一笔支付尝试的快照,可以保存本次支付载体和页面承接信息。
```php
[
'_protocol_version' => 'v2',
'merchant' => [
'param' => '商户透传参数',
'buyer' => '商户侧买家标识,可选',
],
'payment' => [
'method' => 'web|jump|jsapi|app|scan|applet',
'auth_code' => '条码/付款码支付时的付款码',
'sub_openid' => 'JSAPI 支付所需 openid',
'sub_appid' => '服务商模式子应用 ID',
],
'presentation' => [
'params_type' => 'jump|qrcode|html|jsapi|urlscheme|json|error',
'product' => 'alipay|wxpay|unionpay|...',
'action' => '插件动作名',
'params_snapshot' => [
'type' => 'qrcode',
'qrcode_url' => 'https://...',
],
],
'plugin' => [
'pay_result' => [],
'close_result' => [],
'active_query' => [
'queried_at' => '2026-04-25 12:00:00',
'status' => 'success|failed|closed|pending|unknown|error',
'raw_status' => '渠道原始状态',
'channel_status' => '渠道状态码',
'message' => '查单说明或异常信息',
'success' => true,
'channel_order_no' => '渠道订单号',
'channel_trade_no' => '渠道交易号',
'query_count' => 1,
],
],
'lifecycle' => [
'close_reason' => '关闭原因',
'timeout_reason' => '超时原因',
],
]
```
## 4. 插件 `pay()` 入参
系统调用插件下单时会传入结构化 `extra`。插件读取商户透传、支付载体或协议字段时,从对应分区取值。
```php
[
'pay_no' => 'PAY...',
'biz_no' => 'BIZ...',
'channel_request_no' => 'REQ...',
'merchant_id' => 1,
'merchant_no' => 'M...',
'pay_type_code' => 'alipay',
'amount' => 100,
'subject' => '订单标题',
'callback_url' => 'https://platform/api/pay/PAY.../callback',
'notify_url' => 'https://merchant/notify',
'return_url' => 'https://merchant/return',
'client_ip' => '127.0.0.1',
'_env' => 'pc',
'extra' => [
'_protocol_version' => 'v2',
'merchant' => [],
'payment' => [],
],
]
```
## 5. 插件 `pay()` 返回值
插件下单必须返回系统可承接的标准结构。后端会在 `PayOrderChannelDispatchService` 中严格校验字段和 `pay_params.type` 所需载荷,校验通过后才会写入 `ma_pay_order.ext_json.presentation`
```php
[
'pay_product' => 'alipay',
'pay_action' => 'jump',
'pay_params' => [
'type' => 'jump',
'redirect_url' => 'https://...',
],
'chan_order_no' => '渠道订单号',
'chan_trade_no' => '渠道交易号,可选;未生成时返回空字符串',
'ext_json' => [
// 插件私有轻量信息,可选
],
]
```
`pay_params.type` 决定收银台如何承接:
- `jump` / `web` / `h5`:必须提供 `redirect_url``payurl``pay_url``mweb_url``url`
- `qrcode`:必须提供 `qrcode_text``qrcode_data``qrcode_url``qrcode`
- `html` / `form`:必须提供 `html``action``form` 会归一为 `html`
- `jsapi` / `urlscheme` / `mini`:必须提供对应拉起参数、跳转参数或小程序参数。
- `pos` / `transfer`:展示结构化参数,适合收银设备或转账类场景。
- `json`:直接展示结构化参数,由业务端继续处理。
- `error`:展示插件返回的错误信息。
`pay_params.type` 的兼容别名只在平台内部归一化使用,插件文档和新插件代码应直接返回标准值。常见别名包括:`scan|qr|code -> qrcode``redirect|url -> jump``wap -> h5``form -> html``app -> urlscheme``applet|wxplugin -> mini`
## 6. 插件 `query()` 返回值
主动查单由 `PaymentRuntimeProcess` 定时触发,只处理 `status=支付中` 且已超过最小等待时间的支付单。插件 `query()` 应尽量返回标准字段:
```php
[
'success' => true,
'status' => 'success|failed|closed|pending',
'channel_order_no' => '渠道订单号',
'channel_trade_no' => '渠道交易号',
'channel_status' => 'TRADE_SUCCESS',
'message' => '渠道说明,可选',
'paid_at' => '2026-04-25 12:00:00',
'failed_at' => null,
'ext_json' => [
// 插件私有轻量补充信息,可选
],
]
```
处理规则:
- `success`:推进支付单成功,并发出 `payment.pay_order.succeeded` 事件。
- `failed`:推进支付单失败。
- `closed`:推进支付单关闭。
- `pending` / `unknown` / 查单异常:不推进终态,只把轻量快照写入 `ma_pay_order.ext_json.plugin.active_query`
主动查单快照只保存状态、说明、渠道单号、时间和次数,不保存完整上游响应;完整回调仍以 `ma_pay_callback_log` 为准。
## 7. 插件 `notify()` 返回值
插件回调只负责验签和归一化结果。完整回调原文和该返回值会写入 `ma_pay_callback_log`,不会再回灌支付单 `ext_json`
```php
[
'status' => 'success|failed|pending',
'message' => '渠道状态说明',
'channel_order_no' => '渠道订单号',
'channel_trade_no' => '渠道交易号',
'channel_status' => 'TRADE_SUCCESS',
'channel_error_code' => '',
'channel_error_msg' => '',
'paid_at' => '2026-04-25 12:00:00',
'failed_at' => null,
'fee_actual_amount' => null,
'ext_json' => [
// 插件私有轻量补充信息,可选
],
]
```
`status=pending` 只记录回调日志,不推进支付单终态。`success``failed` 会推进支付单状态,并按需要创建商户通知任务。
## 8. 商户通知任务事件模型
`ma_notify_task` 不再以 `pay_no` 作为唯一业务键。通知任务统一按事件建模:
```php
[
'event_type' => 'PAY_SUCCESS|REFUND_SUCCESS|SETTLEMENT_SUCCESS',
'ref_no' => '事件引用单号,例如 pay_no/refund_no/settle_no',
'biz_no' => '业务单号',
'pay_no' => '支付单号,支付相关事件保留',
'notify_data' => [
// 发送给商户的已签名参数
],
]
```
这样同一笔支付后续可以同时存在支付成功、退款成功、清算完成等多类通知,不会因为复用 `pay_no` 被唯一键挡住。
## 9. 回调日志留痕规则
渠道回调日志按“每次请求一条”写入,不做 `pay_no + callback_type` 覆盖或复用。
`request_hash` 是原始请求载荷的 SHA-256 摘要,用来在后台识别重复通知;重复通知是否推进业务状态,由支付单生命周期幂等控制。
## 10. 支付运行时维护
运行时维护使用 Webman 自定义进程 `payment-runtime`,对应 `app/process/PaymentRuntimeProcess.php`。进程只负责定时调度,具体业务由 `PaymentRuntimeMaintenanceService` 完成。
当前维护任务:
- 商户通知重试:扫描到期 `ma_notify_task` 并重新派发。
- 支付单超时:扫描已过期且未终态的支付单,推进为超时并释放冻结手续费。
- 主动查单:扫描支付中订单,调用插件 `query()` 补偿异步通知丢失或延迟。
当前阶段采用自定义进程直接执行,代码路径更短、部署依赖更少。后续如果商户通知量明显增大,建议引入 Redis 队列:支付成功监听器只负责投递队列,队列消费者负责 HTTP 通知派发和重试。
可在管理后台“支付配置”中维护以下系统配置:
- `pay_runtime_enabled`:运行时维护总开关。
- `pay_notify_retry_scan_interval_seconds` / `pay_notify_retry_batch_size`:通知重试扫描间隔和批量。
- `pay_order_timeout_scan_interval_seconds` / `pay_order_timeout_batch_size`:超时订单扫描间隔和批量。
- `pay_active_query_enabled` / `pay_active_query_interval_seconds` / `pay_active_query_min_age_seconds` / `pay_active_query_batch_size`:主动查单开关、间隔、等待时间和批量。
系统配置保存后会触发 `system.config.changed` 事件刷新运行时缓存,维护进程下一轮心跳读取新值。
## 11. 支付域事件
支付生命周期服务只负责状态推进和资金动作。支付单首次进入成功态后,会发送事件:
```php
PaymentEventConstant::PAY_ORDER_SUCCEEDED // payment.pay_order.succeeded
```
当前监听器 `PayOrderSucceededListener` 负责创建并派发商户支付成功通知。后续如果引入 Redis 队列,可以只替换监听器内部实现,把通知派发入队,而不用改订单生命周期服务。