1. 维护代码健壮

2. 更新项目结构文档
This commit is contained in:
技术老胡
2026-04-27 16:20:41 +08:00
parent 9a16a88640
commit 0e5de50337
198 changed files with 21038 additions and 702 deletions

72
doc/backend/README.md Normal file
View File

@@ -0,0 +1,72 @@
# 后端总说明
`mpay` 是支付中台后端服务,基于 Webman。命令默认在 `mpay/` 目录执行。
## 技术栈
- PHP `>=8.1`
- `workerman/webman-framework ^2.1`
- MySQL、Redis
- JWT、Webman validation/cache/event/redis/database
- OSS/COS SDK 用于对象存储
## 快速启动
```bash
composer install
Copy-Item .env.example .env
php webman start
```
Windows 开发环境如需启动自定义进程,可使用:
```bash
php windows.php
```
## 主要目录
```text
app/
command/ 命令与烟雾测试
common/ 基类、常量、工具、中间件、支付插件
http/ admin、mer、api 三类 HTTP 入口
model/ 数据模型
repository/ 数据访问
route/ 显式路由
service/ 业务服务
config/ Webman 与业务配置
public/ 静态资源与前端构建产物
support/ Webman 支撑代码
```
## 关键入口
- 路由:`config/route.php``app/route/admin.php``app/route/mer.php``app/route/api.php`
- 支付:`app/service/payment/order/PayOrderService.php`
- 退款:`app/service/payment/order/RefundService.php`
- 清算:`app/service/payment/settlement/SettlementService.php`
- 路由:`app/service/payment/runtime/PaymentRouteService.php`
- 插件:`app/service/payment/runtime/PaymentPluginManager.php`
- 商户:`app/service/merchant/MerchantService.php`
- 商户后台:`app/service/merchant/portal/MerchantPortalService.php`
- 文件:`app/service/file/FileRecordService.php`
## 常用命令
```bash
php webman start
php webman restart
php webman mpay:test --all
php webman epay:mapi
php webman system:config-sync
```
## 关联文档
- [后端路由](./routing.md)
- [后端服务层](./services.md)
- [后端命令](./commands.md)
- [文件资产](./files.md)
- [接口总说明](../api/README.md)
- [部署说明](../deployment/backend.md)

41
doc/backend/commands.md Normal file
View File

@@ -0,0 +1,41 @@
# 后端命令
命令默认在 `mpay/` 目录执行。
## Webman 启动
| 命令 | 作用 |
| --- | --- |
| `php webman start` | 启动后端 HTTP 服务 |
| `php webman restart` | 重启后端服务 |
| `php windows.php` | Windows 开发环境启动 Webman 与自定义进程 |
| `php start.php start` | 生产/Linux 常见启动入口 |
`payment-runtime` 是自定义进程,不是 Console 命令;它随 Webman 进程启动,负责通知重试、支付超时扫描和支付中订单主动查单。
## 业务命令
| 命令 | 作用 |
| --- | --- |
| `php webman mpay:test --all` | 支付、退款、清结算、余额、追踪链路烟雾测试 |
| `php webman epay:mapi` | ePay V1 `mapi.php` 兼容接口烟雾测试 |
| `php webman epay:v2-api` | ePay V2 创建、查询、关闭、退款、商户信息等核心 API 烟雾测试 |
| `php webman epay:mock-chain` | 自动写入 mock 配置并跑 ePay V1/V2 全链路 |
| `php webman epay:v2-bootstrap` | 生成开发联调用平台和商户 RSA 密钥 |
| `php webman payment:notify-retry` | 手动重试到期商户通知任务 |
| `php webman system:config-sync` | 将 `config/system_config.php` 默认配置同步到数据库 |
| `php webman test` | 基础命令注册检查 |
## 常用参数
- `mpay:test``--payment``--refund``--settlement``--balance``--trace``--all``--live`
- `epay:mapi``--live``--merchant-id``--merchant-no``--type``--money``--refund-trade-no`
- `epay:v2-api``--live``--merchant-id``--merchant-no``--merchant-private-key-file``--type``--method``--money`
- `epay:mock-chain``--only=v1|v2|all`
- `payment:notify-retry``--limit`
## 关联文档
- [后端总说明](./README.md)
- [支付运行时数据契约](./payment-runtime-contract.md)
- [ePay 兼容协议](../api/legacy/epay.md)

39
doc/backend/compat.md Normal file
View File

@@ -0,0 +1,39 @@
# ePay 兼容层
后端同时保留 ePay V1 旧入口和 ePay V2 新接口。兼容层只负责协议适配,不作为后台管理或商户后台的新能力入口。
## 当前实现
| 协议 | 控制器 | 服务 |
| --- | --- | --- |
| ePay V1 | `app/http/api/controller/epay/EpayV1Controller.php` | `app/service/payment/epay/EpayV1ProtocolService.php` |
| ePay V2 | `app/http/api/controller/epay/EpayV2Controller.php` | `app/service/payment/epay/EpayV2ProtocolService.php` |
签名实现:
- `Md5Signer`
- `RsaSigner`
- `EpaySignerManager`
## V1 路由
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `ANY` | `/submit.php` | 页面跳转支付 |
| `POST` | `/mapi.php` | 接口支付 |
| `ANY` | `/api.php` | 旧版标准 API |
## V2 路由
| 分组 | 路径 |
| --- | --- |
| 支付 | `/api/pay/submit``/api/pay/create``/api/pay/query``/api/pay/refund``/api/pay/refundquery``/api/pay/close``/api/pay/{payNo}/callback` |
| 商户 | `/api/merchant/info``/api/merchant/orders` |
| 转账 | `/api/transfer/submit``/api/transfer/query``/api/transfer/balance` |
## 关联文档
- [接口总说明](../api/README.md)
- [收银台与开放接口](../api/cashier.md)
- [ePay 兼容协议](../api/legacy/epay.md)
- [后端路由](./routing.md)

46
doc/backend/files.md Normal file
View File

@@ -0,0 +1,46 @@
# 文件资产
文件资产由后端记录元数据,实际文件交给本地、远程 URL、OSS 或 COS 驱动处理。
## 当前入口
- API 前缀:`/adminapi/file-asset`
- 控制器:`FileRecordController`
- 模型:`FileRecord`
- 数据表:`ma_file_asset`
## 接口
| 方法 | 路径 | 作用 |
| --- | --- | --- |
| `GET` | `/file-asset/options` | 文件选项 |
| `GET` | `/file-asset` | 文件列表 |
| `POST` | `/file-asset/upload` | 上传文件 |
| `POST` | `/file-asset/import-remote` | 导入远程文件 |
| `GET` | `/file-asset/{id}/preview` | 文件预览 |
| `GET` | `/file-asset/{id}/download` | 文件下载 |
| `GET` | `/file-asset/{id}` | 文件详情 |
| `DELETE` | `/file-asset/{id}` | 删除文件 |
## 服务与驱动
- `FileRecordService`
- `FileRecordQueryService`
- `FileRecordCommandService`
- `StorageConfigService`
- `StorageManager`
- `LocalStorageDriver`
- `RemoteUrlStorageDriver`
- `OssStorageDriver`
- `CosStorageDriver`
## 字段口径
- `object_key`:站点相对路径或对象存储 key。
- `url`:公开文件的完整访问地址,私有文件可以为空。
- `preview_url`:后台预览使用,不作为长期业务访问地址。
- `previewable`:列表返回给前端判断是否允许在线预览。
## 表单上传
系统配置和插件配置中的上传字段仍使用 `type: "upload"`。需要走项目定制选择器时,在 `props.fileUpload` 中声明 `selectorType``scene``isLocal``isPublic``getKey` 等配置。前端说明见 [管理后台前端](../frontend/admin.md)。

View File

@@ -0,0 +1,261 @@
# 支付插件模板说明
这份说明对应 `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`

View File

@@ -0,0 +1,243 @@
# 支付运行时数据契约
本文档约定支付链路里订单扩展字段、插件入参和插件返回值的结构。当前项目仍处于开发阶段,不保留旧平铺结构兼容。
## 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 队列,可以只替换监听器内部实现,把通知派发入队,而不用改订单生命周期服务。

47
doc/backend/routing.md Normal file
View File

@@ -0,0 +1,47 @@
# 后端路由
后端只使用显式路由。`config/route.php` 加载三份路由文件后关闭默认路由。
## 路由文件
| 文件 | 覆盖范围 |
| --- | --- |
| `app/route/admin.php` | `/admin` 页面入口、`/adminapi` 管理后台 API |
| `app/route/mer.php` | `/mer` 页面入口、`/merapi` 商户后台 API |
| `app/route/api.php` | 收银台页面、收银台 API、ePay V1/V2 与开放 API |
## 当前入口
| 前缀 | 说明 | 中间件 |
| --- | --- | --- |
| `/admin` | 管理后台静态页面入口 | 无业务鉴权 |
| `/adminapi` | 管理后台接口 | `Cors`,保护接口再走 `AdminAuthMiddleware` |
| `/mer` | 商户后台静态页面入口 | 无业务鉴权 |
| `/merapi` | 商户后台接口 | `Cors`,保护接口再走 `MerchantAuthMiddleware` |
| `/cashier` | 收银台入口页 | 无业务鉴权 |
| `/payment` | 支付页、中转页、结果页 | 无业务鉴权 |
| `/api/cashier` | 收银台上下文、确认支付、支付单详情 | `Cors` |
| `/api/pay` | ePay V2 支付、查询、退款、关闭、通道回调 | `Cors` |
| `/api/merchant` | ePay V2 商户信息与订单查询 | `Cors` |
| `/api/transfer` | ePay V2 转账提交、查询、余额 | `Cors` |
| `/submit.php``/mapi.php``/api.php` | ePay V1 兼容入口 | `Cors` |
## 流转
```mermaid
flowchart LR
Req[HTTP 请求] --> Route[显式路由]
Route --> Middleware[中间件]
Middleware --> Controller[控制器]
Controller --> Validator[参数校验]
Validator --> Service[服务层]
Service --> Repository[仓库层]
Repository --> Model[模型层]
Model --> DB[(MySQL)]
```
## 维护要求
- 新增接口先改 `app/route/*`,再补对应 `docs/api/*`
- 页面兜底路由只返回前端入口,不承载业务逻辑。
- 业务规则放服务层;路由文件只做 URL 到控制器方法的绑定。

51
doc/backend/services.md Normal file
View File

@@ -0,0 +1,51 @@
# 后端服务层
服务层承载业务规则,控制器只负责入口、参数和响应包装。
## 目录职责
| 目录 | 主要职责 |
| --- | --- |
| `account/funds``account/ledger` | 商户账户、余额和资金流水 |
| `bootstrap` | 启动期初始化 |
| `file``file/storage` | 文件资产、上传、导入、预览、下载和存储驱动 |
| `merchant` | 商户主体、总览、分组、策略、API 凭证 |
| `merchant/auth` | 商户后台登录认证 |
| `merchant/portal` | 商户后台资料、通道、路由预览、订单资金查询 |
| `ops/log``ops/stat` | 通道通知、支付回调、商户通知任务和通道日统计 |
| `payment/cashier` | 收银台上下文、确认支付、支付单展示 |
| `payment/config` | 支付方式、插件、插件配置、通道、轮询组、绑定 |
| `payment/epay` | ePay V1/V2 协议、MD5/RSA 签名 |
| `payment/order` | 支付单、退款单、费用、派单、回调、生命周期 |
| `payment/runtime` | 路由解析、插件装配、商户通知、运行时维护 |
| `payment/settlement` | 清算单和清算生命周期 |
| `payment/trace` | 交易追踪与报表 |
| `payment/transfer` | 转账能力 |
| `system/access``system/config``system/user` | 管理员认证、系统配置、管理员用户 |
## 命名规则
- `*Service`:对外门面。
- `*QueryService`:查询与展示拼装。
- `*CommandService`:写入、修改、删除。
- `*LifecycleService`:状态流转。
- `*CallbackService`:第三方回调。
- `*SyncService`:同步、扫描、刷新。
- `*SupportService`:业务辅助能力。
## 关键服务
- 支付:`PayOrderService``PayOrderLifecycleService``PayOrderChannelDispatchService`
- 退款:`RefundService``RefundCreationService``RefundLifecycleService`
- 清算:`SettlementService``SettlementLifecycleService`
- 路由与插件:`PaymentRouteService``PaymentRouteResolverService``PaymentPluginManager`
- 收银台:`CashierService`
- 通知:`NotifyService``MerchantNotifyDispatcherService`
- 商户:`MerchantService``MerchantPortalService``MerchantApiCredentialService`
- 文件:`FileRecordService``StorageManager`
## 维护要求
- 查询和写入不要混在一个越来越胖的类里。
- 回调、路由解析、插件装配、通知重试要保持独立职责。
- `SupportService` 只放有业务含义的复用逻辑,不做基础工具的空转发。