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

262 lines
8.4 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.

# 支付插件模板说明
这份说明对应 `mpay/app/common/payment/TemplatePayment.php`
它的作用不是提供一个可直接上线的插件,而是提供一份可以复制、改名、接第三方网关的起点模板。
## 1. 这个模板解决什么问题
支付插件开发最容易踩坑的地方,不是第三方接口调用本身,而是这些基础约定容易不统一:
- 插件元信息怎么写
- 配置表单怎么定义
- `class_name` 怎么配置
- `pay()` 返回什么结构
- 回调怎么收口
- 哪些字段该放在插件配置里,哪些字段该放在订单入参里
`TemplatePayment` 把这些骨架先搭好,后面新增插件时可以直接复制,再替换为真实渠道逻辑。
## 2. 模板在系统里怎么被加载
插件并不是直接被业务代码 `new` 出来的,而是先经过插件注册表,再由工厂服务实例化。
```mermaid
flowchart LR
A[后台维护插件定义] --> B[ma_payment_plugin]
B --> C[ma_payment_plugin_conf]
C --> D[ma_payment_channel]
D --> E[PaymentPluginFactoryService]
E --> F[实例化插件类]
F --> G[init(配置)]
G --> H[pay / query / close / refund / notify]
```
关键点有两个:
- `ma_payment_plugin.class_name` 可以填短类名,也可以填完整类名
- 如果是短类名,工厂会自动补成 `app\\common\\payment\\{class_name}`
也就是说,`TemplatePayment` 这种类既可以直接写成 `TemplatePayment`,也可以写成完整命名空间。
## 3. 复制时要改哪些地方
复制这个模板后,优先改下面几块:
1. `paymentInfo.code`
2. `paymentInfo.name`
3. `paymentInfo.pay_types`
4. `paymentInfo.transfer_types`
5. `paymentInfo.config_schema`
6. `init()`
7. `pay()`
8. `query()`
9. `close()`
10. `refund()`
11. `notify()`
其中最重要的是:
- `paymentInfo` 决定后台怎么展示这个插件
- `config_schema` 决定后台怎么维护插件配置
- `init()` 决定插件怎么吃到 `ma_payment_plugin_conf.config` 里的运行时参数
- `pay()` 决定第三方下单时返回给系统什么支付参数
- `notify()` 决定第三方回调回来后怎么验签和归一化结果
## 4. 这个模板里的返回结构
### 4.1 `pay()` 的返回值
模板已经按项目当前口径返回了这几个字段:
- `pay_product`
- `pay_action`
- `pay_params`
- `chan_order_no`
- `chan_trade_no`
- `ext_json`
其中 `pay_params` 是给收银台前端或业务调用方用的,常见类型包括:
- `html`
- `qrcode`
- `jump`
- `h5`
- `jsapi`
- `urlscheme`
- `mini`
- `json`
- `error`
后端会在支付单拉起后立即校验这份返回值。校验失败会把支付单收口为失败态并抛出 `PaymentException`,所以新插件不要返回旧字段名或半结构化内容,必须直接返回标准结构。
不同 `pay_params.type` 的必要字段:
- `jump` / `web` / `h5``redirect_url``payurl``pay_url``mweb_url``url`
- `qrcode``qrcode_text``qrcode_data``qrcode_url``qrcode`
- `html``html``action`
- `jsapi``jsapi_params`,或 `order_str` / `order_string` 等拉起参数
- `urlscheme``urlscheme``redirect_url``order_str``order_string`
- `mini``path``scheme``urlscheme``trade_no``mini_params`
实际接第三方时,你只需要把 `pay_params` 换成真实可渲染的数据结构即可。`ext_json` 只能放插件私有的轻量补充信息,完整请求、响应和通知记录不要放在这里。
标准示例:
```php
[
'pay_product' => 'alipay',
'pay_action' => 'jump',
'pay_params' => [
'type' => 'jump',
'redirect_url' => 'https://...',
],
'chan_order_no' => '渠道订单号',
'chan_trade_no' => '渠道交易号,可选;未生成时返回空字符串',
'ext_json' => [],
]
```
### 4.2 `query()` 的返回值
主动查单依赖插件 `query()`。新插件建议直接返回下面的标准结构,便于定时维护进程统一推进订单状态:
```php
[
'success' => true,
'status' => 'success|failed|closed|pending',
'channel_order_no' => '渠道订单号',
'channel_trade_no' => '渠道交易号',
'channel_status' => '渠道原始状态',
'message' => '查询说明,可选',
'paid_at' => '2026-04-25 12:00:00',
'failed_at' => null,
'ext_json' => [],
]
```
`status=success` 会推进支付成功,`failed` 会推进失败,`closed` 会推进关闭。`pending``unknown` 或查询异常只记录轻量查单快照,不改变支付单终态。
### 4.3 `notify()` 的返回值
回调处理建议返回这些语义字段:
- `status`
- `message`
- `channel_order_no`
- `channel_trade_no`
- `channel_status`
- `paid_at`
- `failed_at`
- `fee_actual_amount`
- `ext_json`
如果是失败回调,也可以补充:
- `channel_error_code`
- `channel_error_msg`
后端回调链路会根据 `status` 统一推进支付单状态。完整回调原文和插件解析结果会保存到 `ma_pay_callback_log`,不要再塞进支付单 `ext_json`
标准示例:
```php
[
'status' => 'success',
'message' => 'TRADE_SUCCESS',
'channel_order_no' => '渠道订单号',
'channel_trade_no' => '渠道交易号',
'channel_status' => 'TRADE_SUCCESS',
'paid_at' => '2026-04-25 12:00:00',
'fee_actual_amount' => null,
'ext_json' => [],
]
```
### 4.4 订单扩展字段
业务单和支付单的 `ext_json` 使用分区结构:
- 顶层 `_protocol_version`:协议版本,方便后台查询。
- `merchant`:商户透传字段,如 `param``buyer`
- `payment`:本次支付载体字段,如 `method``auth_code``sub_openid`
- `presentation`:插件返回给收银台承接的支付参数快照。
- `plugin`:插件私有轻量信息,以及主动查单的 `active_query` 快照。
- `lifecycle`:关单、超时等生命周期原因。
详细契约见 [支付运行时数据契约](./payment-runtime-contract.md)。
## 5. 模板里的占位逻辑
`TemplatePayment` 里有意保留了一些占位实现:
- `query()`
- `close()`
- `refund()`
- `notify()`
这些方法现在会直接抛出 `PaymentException`,避免被误当成真实插件投入使用。
`pay()` 里也保留了示例结构:
- 默认支付形态可选 `html``qrcode``jump``jsapi`
- `buildAutoSubmitForm()` 只是表单跳转的通用模板
- `sign` 目前是 `TODO`
所以它更像“开发脚手架”,不是上线成品。
## 6. 推荐的开发步骤
建议按这个顺序做新插件:
1. 复制 `TemplatePayment.php`,改成新的类名
2. 修改 `paymentInfo`,让插件代码和后台展示名称唯一
3. 根据第三方网关要求补齐 `config_schema`
4.`init()` 里读取配置、初始化 SDK
5.`pay()` 里实现真实下单
6.`notify()` 里实现真实回调验签
7.`query()``close()``refund()` 按第三方能力补齐
8. 在后台创建 `ma_payment_plugin` 记录
9. 再创建对应的 `ma_payment_plugin_conf` 记录
10. 把通道的 `plugin_code``api_config_id` 绑定起来
## 7. 后台配置关系
从数据库角度看,通常会涉及三张表:
- `ma_payment_plugin`:插件注册表,保存 `code``name``class_name``config_schema``pay_types`
- `ma_payment_plugin_conf`:插件运行配置表,保存 `config`
- `ma_payment_channel`:支付通道表,保存 `plugin_code``api_config_id`
也就是说:
- `ma_payment_plugin` 负责“这个插件是什么”
- `ma_payment_plugin_conf` 负责“这个插件怎么运行”
- `ma_payment_channel` 负责“这个插件被哪个通道使用”
## 8. 适合复制的场景
这个模板特别适合下面几类插件:
- 表单跳转类支付
- 二维码类支付
- 链接跳转类支付
- 需要自定义回调验签的第三方网关
- 先接通主链路、后逐步补齐查单退款的渠道
如果是像支付宝这种已经有成熟 SDK 的渠道,也可以直接参考现有 `AlipayPayment` 的实现,再用模板做新的落地版本。
## 9. 使用建议
- 新插件先保证 `pay()``notify()` 跑通,再补 `query()``refund()``close()`
- 插件配置只放运行时必需信息,不要把订单级入参混进去
- `pay_types` 存的是支付方式编码,不是支付方式 ID
- 新插件类名和 `code` 一定要唯一,避免和已有插件冲突
- 真正上线前,必须把占位异常和 `TODO` 字段全部替换掉
## 10. 相关代码
- `mpay/app/common/payment/TemplatePayment.php`
- `mpay/app/service/payment/runtime/PaymentPluginFactoryService.php`
- `docs/db/payment-middle-ddl.sql`