diff --git a/help/docs/支付驱动二开指南.md b/help/docs/支付驱动二开指南.md new file mode 100644 index 000000000..7b2369274 --- /dev/null +++ b/help/docs/支付驱动二开指南.md @@ -0,0 +1,567 @@ +# CRMEB v8 支付驱动二开指南 + +> 面向需要在 CRMEB v8 中新增或改造第三方支付通道的研发同学。 +> 本文以当前仓库代码为准,说明支付驱动的定位、清单、接口契约、配置存储、回调/退款链路与验收清单。 +> 精简操作条目亦可对照 `backend/driver/pay/DEVELOPMENT.md`;机制总览见 `.crmeb/skills/crmeb-dev/references/02-驱动机制/driver-mechanism.md`。 + +--- + +## 1. 为什么要做「支付驱动」二开 + +电商系统里,「下单 → 收款 → 回调入账 → 退款」是固定业务,真正会变的是**跟哪家支付服务商说话**。CRMEB 把这部分差异抽成 **支付驱动(`backend/driver/pay/`)**: + +| 不变(业务层) | 变化(驱动层) | +|---|---| +| 统一订单状态机、支付单、退款单 | 下单请求字段、签名算法、证书 | +| `PayServices` 发起支付 / 退款 / 回调路由 | 各端支付形态(JSAPI / H5 / NATIVE / APP) | +| `OrderPaySuccess` 等业务事件 | 异步通知验签与响应格式 | +| 管理端「支付方式」配置页 | 商户号、密钥、证书等配置项 | + +因此:接入银联、汇付、易宝、自建聚合通道等,**正确姿势是新增一个支付驱动包**,而不是改订单中台或在业务 Service 里硬编码渠道枚举。 + +--- + +## 2. 架构定位:驱动在哪一层 + +``` +modules/(业务扩展) + ↓ +app/(统一管理中台:PayServices、PayChannelServices、订单支付) + ↓ +crmeb/(核心门面:Pay / PayInterface / BasePay / DriverRegistry) + ↓ +driver/pay/{DriverName}/(第三方协议与 SDK) +``` + +硬约束(违反即架构错误): + +1. **依赖单向**:`modules → app → crmeb → driver`。驱动包**禁止**访问数据库、禁止调用 `app/` / `modules/` 业务 Service。 +2. **零侵入二开**:新驱动只落在 `backend/driver/pay/{YourDriver}/`,禁止改 `app/`、`crmeb/`、核心前端、安装 SQL、composer 依赖清单(除非走正式评审的框架能力变更)。 +3. **配置注入**:商户凭据由上层在实例化门面时注入,驱动只在 `initialize(array $config)` 或通过既有 Service/SDK 读取上层已装好的配置,**禁止**把密钥写进 `plugin.json` 常量。 + +判定口诀: + +- 「怎么和第三方说话」→ `driver/pay/` +- 「付完款业务上怎么办」→ 订单中台 / 模块事件(如 `OrderPaySuccess`) + +--- + +## 3. 支付与通用驱动字典的差异(必读) + +多数驱动类型(短信、上传、邮件等)元数据进统一表 `eb_driver`,由 `DriverRegistry` 读取。 + +**支付是例外**:支付方式独立走渠道字典: + +| 能力 | 存储 / 组件 | +|---|---| +| 驱动实现与清单 | `backend/driver/pay/*/plugin.json` + `Driver.php` | +| 渠道字典 | `eb_pay_channel`(`channel_key` 唯一,含 `driver`、`config_fields`) | +| 各端支付形态 | `eb_pay_channel_platform` | +| 后台管理 | `app/services/driver/pay/PayChannelServices` 等 | +| 运行时编排 | `app/services/pay/PayServices` + 门面 `crmeb\services\pay\Pay` | + +排查时若在 `eb_driver` 里找支付驱动记录,会查不到——这是设计如此,不是安装失败。 + +余额支付(`yue`)、线下支付(`offline`)等是**系统固有渠道**,不是支付驱动插件;第三方在线渠道由驱动扫描结果动态纳入,业务侧不应再硬编码「微信/支付宝」枚举。 + +--- + +## 4. 现有支付驱动一览 + +| 目录 | `driver_key` | `channel_key` | 平台 | 说明 | +|---|---|---|---|---| +| `WechatPay` | `wechat_pay` | `weixin` | wechat | 微信支付 APIv2 | +| `V3WechatPay` | `v3_wechat_pay` | `weixin_v3` | wechat | 微信支付 APIv3(含商家转账等扩展能力) | +| `AliPay` | `ali_pay` | `alipay` | alipay | 支付宝(EasySDK) | +| `AllinPay` | `allin_pay` | `allinpay` | allinpay | 通联支付 | + +推荐对照实现: + +- **标准最小实现**:`backend/driver/pay/AliPay/` +- **多端映射 / 服务商 / 转账**:`backend/driver/pay/V3WechatPay/` + +--- + +## 5. 驱动包标准结构 + +``` +backend/driver/pay/{DriverName}/ +├── Driver.php # 必须:继承 BasePay,实现 PayInterface +├── plugin.json # 必须:双层清单(发现、安装、后台表单) +├── XxxSdk.php # 可选:协议/SDK 适配(大逻辑建议拆出 Driver) +├── Client.php # 可选:HTTP 客户端封装 +├── Installer.php # 可选:安装期动作(绝大多数支付驱动用通用 ManifestDriverInstaller 即可) +└── extend/ # 可选:大体积第三方 SDK 隔离目录 +``` + +命名约定: + +- 目录名使用大驼峰,如 `UnionPay`、`HuifuPay` +- 命名空间:`driver\pay\{DriverName}` +- 实现类默认名:`Driver`(与 `plugin.json` 中 `class_name` 一致,缺省即为 `Driver`) +- 门面解析:`Pay` 对传入的 `driver_key` 做 `Studly` 后优先加载 `driver\pay\{Studly}\Driver` + 例如 `ali_pay` → `AliPay\Driver`,`v3_wechat_pay` → `V3WechatPay\Driver` + +--- + +## 6. 从零接入:完整步骤 + +### 6.1 创建目录 + +```bash +mkdir -p backend/driver/pay/YourPay +``` + +### 6.2 编写 `plugin.json` + +支付驱动的清单是**双层结构**: + +- **顶层**:插件生命周期元信息(安装、版本、依赖) +- **`driver` 子对象**:支付专用描述(渠道、端映射、配置表单等),注册/扫描侧**全量透传** + +最小可用示例(请按实际渠道改名): + +```json +{ + "key": "your_pay", + "name": "某某支付", + "description": "某某支付驱动", + "version": "1.0.0", + "author": "YourTeam", + "type": "driver", + "path": "driver/pay/YourPay", + "default_install": false, + "require": { + "crmeb": "^8.0", + "php": "^8.0" + }, + "driver": { + "name": "某某支付", + "channel_key": "yourpay", + "driver_key": "your_pay", + "platform": "yourpay", + "icon": "icon-xxx", + "sort": 90, + "capabilities": ["pay", "refund", "notify"], + "required_credentials": {}, + "pay_type_map": { + "h5": "H5", + "app": "APP", + "pc": "NATIVE" + }, + "pay_status_type": { + "default": "yourpay_pay", + "h5": "yourpay_h5_pay", + "pc": "yourpay_pc_pay" + }, + "supported_platforms": { + "h5": "H5浏览器", + "app": "APP", + "pc": "PC端" + }, + "config_fields": [ + { + "field": "your_mch_id", + "label": "商户号", + "type": "text", + "required": true, + "group": "商户", + "tip": "支付平台分配的商户号" + }, + { + "field": "your_api_key", + "label": "API密钥", + "type": "password", + "required": true, + "group": "商户" + }, + { + "field": "your_cert", + "label": "商户证书", + "type": "upload", + "group": "证书" + } + ], + "help_doc": { + "title": "某某支付使用说明", + "steps": [ + "在支付平台注册商户并完成进件", + "获取商户号与 API 密钥", + "配置异步通知地址为:站点域名 + /api/pay/notify/{channel_key}", + "将凭据填入上方配置并开启对应客户端平台" + ], + "link": "https://example.com/docs" + } + } +} +``` + +#### 顶层字段 + +| 字段 | 必填 | 说明 | +|---|---|---| +| `key` | 是 | 与 `driver.driver_key` **必须一致** | +| `name` / `description` / `version` / `author` | 是 | 展示与版本管理 | +| `type` | 是 | 固定 `"driver"` | +| `path` | 是 | 相对 backend 的路径,如 `driver/pay/YourPay` | +| `require` | 建议 | `crmeb` / `php` 版本约束 | +| `default_install` | 否 | `true` 时随系统捆绑安装;需单独申请密钥的通道建议 `false`,后台按需安装 | + +#### `driver` 子对象(支付扩展) + +| 字段 | 必填 | 说明 | +|---|---|---| +| `name` | 是 | 后台显示名 | +| `channel_key` | 是 | 业务侧支付方式标识(回调 URL 的 `:type`、退款路由都认它) | +| `driver_key` | 是 | 驱动唯一标识,与顶层 `key` 一致 | +| `platform` | 是 | 支付平台归类,如 `wechat` / `alipay` / `allinpay` | +| `icon` | 否 | 管理端图标类名 | +| `capabilities` | 否 | 能力声明:`pay` / `refund` / `notify` / `merchant_pay` 等 | +| `required_credentials` | 否 | 各客户端发起支付前需要的前端凭证,如小程序/公众号要 `openid` | +| `pay_type_map` | 否 | 客户端平台 → 供应商支付类型(如 `wechat_mini` → `JSAPI`) | +| `pay_status_type` | 否 | 前端拿到支付结果后的跳转/调起形态;可为字符串或按端区分的对象 | +| `default_options` | 否 | 发起支付时自动并入的配置开关字段名列表 | +| `supported_platforms` | 否 | 支持的客户端及中文名;后台可按端开关 | +| `config_fields` | 否 | 动态表单定义(见下节) | +| `help_doc` | 否 | 配置页帮助文案 | + +`config_fields` 常用属性: + +| 属性 | 说明 | +|---|---| +| `field` | 配置键名(最终写入 `config_fields[].value`) | +| `label` | 表单项标题 | +| `type` | `text` / `password` / `textarea` / `upload` / `radio` 等 | +| `group` | 分组标题 | +| `required` | 是否必填 | +| `default` | 默认值 | +| `options` | `radio` 等选项列表 `{value,label}` | +| `show_if` | 条件显示 `{field, value}` | +| `tip` | 表单项说明 | + +### 6.3 实现 `Driver.php` + +必须继承 `crmeb\services\pay\BasePay` 并实现 `crmeb\services\pay\PayInterface`: + +```php +make(Pay::class, [$driverKey, $configValues]) + → BaseManager 解析 driver\pay\{Studly}\Driver + → Driver::create(...) + → 返回给前端的调起参数(JSAPI prepay、H5 URL、扫码码等) +``` + +`PayServices` 会把已扫描到的支付驱动动态当成「第三方在线渠道」;**新增驱动后调用方不必再改一处渠道枚举**。 + +### 7.2 退款 + +``` +售后/后台退款 + → PayServices::refund($channelKey, $orderId, $refundData, ...) + → resolveDriverName($channelKey) + → Pay 门面 → Driver::refund() + → (可选)Driver::queryRefund() 轮询或对账 +``` + +注意:退款必须走**原支付渠道**对应驱动;微信等场景会校验支付上下文中的 `channel_key` 与退款渠道一致。 + +### 7.3 异步回调 + +路由(用户端 API): + +```text +ANY /api/pay/notify/:type +``` + +`:type` 语义上对应 **`channel_key`**(历史别名例外见下)。 + +``` +PayController::notify($type) + → PayServices::handleNotify($type) + 1. resolveChannelKeyByType($type) + 2. resolveDriverName($channelKey) + 3. getChannelConfigValues($channelKey) + 4. Pay 门面 → Driver::handleNotify($type) +``` + +已知别名映射(写新驱动时尽量让 notify URL 直接使用 `channel_key`,少加特殊分支): + +| URL 中的 type | 解析后的 channel_key | +|---|---| +| 与渠道表一致的 key | 原样 | +| `v3wechat` | `weixin_v3`(兼容历史 notify_url) | +| `allin*` 前缀 | `allinpay` | + +商户平台应配置: + +```text +{站点域名}/api/pay/notify/{你的 channel_key} +``` + +例如支付宝现网使用 `/api/pay/notify/alipay`。 + +--- + +## 8. 回调实现规范(安全红线) + +支付回调是驱动最容易出错、也最危险的环节。强制要求: + +1. **验签**:校验通知来自支付平台,失败返回 `fail` / 非 2xx,让对方重试。 +2. **幂等**:同一订单重复通知不得重复发货、入账、改状态。业务侧通过统一支付成功处理与订单状态机保证;驱动侧不要绕过事件私自改业务表。 +3. **金额以服务端为准**:禁止信任客户端或通知体里未校验的金额去改价。 + +推荐流程(示意): + +```php +public function handleNotify(string $type = '') +{ + // 1. 读取原始报文(JSON / form / XML 按供应商而定) + $payload = /* ... */; + + // 2. 验签;失败必须让支付平台感知并重试 + if (!$this->verifySign($payload)) { + return 'fail'; + } + + // 3. 提取商户订单号(与下单时的 out_trade_no 一致) + $orderNo = $payload['out_trade_no'] ?? ''; + + // 4. 交给业务层完成「支付成功」闭环 + // 实际项目中通常由既有支付成功服务 / 事件完成: + // event('OrderPaySuccess', [$orderInfo]); + // 请对照 AliPay / V3WechatPay 的成熟写法,勿自建第二套入账逻辑 + + // 5. 按供应商要求返回成功应答(字符串或 Think Response) + return 'success'; +} +``` + +说明: + +- `PayServices::handleNotify` 若驱动返回 `think\Response`,会**保留 HTTP 状态码与响应头**,便于验签失败时触发平台重试。 +- 业务事件使用**字符串 key**(如 `OrderPaySuccess`),不要带 `Listener` 后缀。 +- 驱动内不要直接操作订单表;复用既有支付成功闭环,避免状态机分叉。 + +--- + +## 9. 前端与多端要点 + +支付驱动二开往往不止后端: + +1. **`supported_platforms`**:声明 H5 / 小程序 / 公众号 / APP / PC 等;后台按端开关写入 `eb_pay_channel_platform`。 +2. **`pay_type_map`**:决定传给驱动的 `pay_type`(如微信 `JSAPI` / `NATIVE`)。 +3. **`pay_status_type`**:决定用户端拿到支付接口响应后走哪种调起逻辑(微信内 JSAPI、H5 跳转、PC 二维码等)。新增类型时,需确认 `frontend/uni-app`(及必要时管理端)是否已有对应分支;没有则要补前端处理,否则后台能配、端上调不起。 +4. **`required_credentials`**:例如微信小程序/公众号需要 `openid`,业务在调 `create` 前必须带上,否则驱动侧无法下单。 + +不要在用户端写死「只有微信/支付宝」;以接口返回的可用渠道与 `pay_status_type` 为准。 + +--- + +## 10. 配置与数据表 + +| 表 | 作用 | +|---|---| +| `eb_pay_channel` | 支付方式字典:名称、图标、`channel_key`、绑定 `driver`、`config_fields` JSON、启停等 | +| `eb_pay_channel_platform` | 渠道在各客户端平台的启用与 pay_type 关系 | + +配置读写入口:`PayChannelServices`(列表、详情、保存配置、驱动扫描状态等)。 +管理端页面参考:`frontend/admin/src/pages/driver/pay/index.vue`。 +管理端路由:`backend/app/adminapi/route/driver.php` 中 `pay_channel/*` 一组接口。 + +原则: + +- 配置值写在 `config_fields` 各字段的 `value` 上 +- 同步/升级驱动字段定义时,应**保留已有 value 与启用状态** +- 密钥、证书路径属于运行时机密,**禁止提交进 Git**;日志中脱敏 + +--- + +## 11. 与订单中台的边界 + +支付驱动只负责「钱有没有从用户到商户 / 原路退回」。以下属于订单中台或模块,**不要塞进驱动**: + +- 库存锁定与扣减、自动发货、虚拟商品发卡 +- 分销记佣、营销核销、积分成长值 +- 售后退款审批流、退款单状态机、退款副作用幂等表 + +正确扩展方式:监听 `OrderPaySuccess`、退款相关事件,或在产品类型 / 交易模式模块内实现。驱动保持「薄适配器」。 + +--- + +## 12. 二开红线清单 + +| 序号 | 红线 | +|---|---| +| 1 | 不修改 `app/`、`crmeb/`、核心前端与安装 SQL 来「硬挂」新渠道 | +| 2 | 驱动内不访问数据库、不直接改订单/退款状态 | +| 3 | 支付配置不写 `system_config`,只写 `eb_pay_channel.config_fields` | +| 4 | `plugin.json` 顶层 `key` 与 `driver.driver_key` 必须一致且全局唯一 | +| 5 | 回调必须验签 + 幂等;金额以服务端订单为准 | +| 6 | 退款走原渠道驱动,禁止跨渠道乱退 | +| 7 | 商户密钥、证书内容不入库、不打明文日志 | +| 8 | 同级模块禁止互引;跨业务用字符串事件或核心契约 | + +完整插件边界见 `.crmeb/rules/插件开发规范.md`;安全条款见 `.crmeb/rules/安全红线.md`。 + +--- + +## 13. 验收清单 + +开发自测建议按下列勾选: + +**发现与配置** + +- [ ] `DriverRegistry::scan('pay')` 能看到新 `driver_key` +- [ ] `getDriverClass('pay', ...)` 返回正确类名,类可被自动加载 +- [ ] 管理端支付方式列表出现渠道,动态表单可保存并回显 +- [ ] 各目标客户端平台可独立开关 + +**支付主路径** + +- [ ] 各支持端能拉起支付(参数完整:openid、returnUrl 等) +- [ ] 支付成功后订单进入已支付,库存/履约/通知符合预期 +- [ ] 重复回调不会重复履约 + +**退款路径** + +- [ ] 全额 / 部分退款调用到正确驱动 +- [ ] `queryRefund` 结果可被上层判定(避免「未知状态」死循环) +- [ ] 退款失败有明确错误信息与日志(脱敏) + +**安全与运维** + +- [ ] 伪造回调(错误签名)被拒绝 +- [ ] 商户平台 notify URL、证书、时钟偏差已核对 +- [ ] 生产密钥仅存在于运行环境配置,不在仓库中 + +--- + +## 14. 常见问题排障 + +| 现象 | 可能原因 | 处理方向 | +|---|---|---| +| 后台看不到新驱动 | 未安装插件 / `plugin.json` 损坏 / `type` 非 `driver` | 检查安装日志;修正清单后重装或同步 | +| `class not exists` | 目录名与 Studly(`driver_key`) 不一致;未 `composer dump-autoload` | 对齐命名空间;重建自动加载 | +| 能下单无回调 | notify URL 配错;`channel_key` 与 URL `:type` 不一致;验签失败被当成功吞掉 | 对照第 7.3 节;看支付平台重试与服务器日志 | +| 回调了订单未变 | 未走统一支付成功逻辑;订单号不一致;幂等误判 | 核对 `out_trade_no`;对照既有驱动事件触发方式 | +| 退款报渠道不一致 | 支付时 channel 与退款 channel 不同;V2/V3 混用 | 以支付记录中的 `channel_key` 为准 | +| 改了配置不生效 | 门面实例缓存了旧 config;渠道信息 Cache | 重新 make 并传入新 config;清 `PayServices` 渠道缓存 | +| 在 `eb_driver` 查不到 | 支付不走该表 | 查 `eb_pay_channel` | + +--- + +## 15. 推荐学习路径 + +1. 读本文第 2~3 节,搞清分层与支付字典特例。 +2. 对照 `AliPay` 走通「最小四方法」实现。 +3. 对照 `V3WechatPay` 学习 `pay_type_map`、`capabilities`、服务商与转账扩展。 +4. 跟一次真实支付:下单 → 调起 → 回调 → `OrderPaySuccess` → 退款。 +5. 用第 13 节清单做回归后再交付。 + +相关文档: + +| 文档 | 用途 | +|---|---| +| `backend/driver/README.md` | 驱动底座总览与选型 | +| `backend/driver/pay/DEVELOPMENT.md` | 支付类型字段速查 | +| `help/docs/模块与驱动.md` | 模块/驱动清单与边界 | +| `help/docs/项目架构.md` | 订单与退款链路 | +| `.crmeb/skills/crmeb-plugin-dev/references/driver-plugin.md` | 驱动插件开发要点 | +| `.crmeb/skills/crmeb-order-dev/SKILL.md` | 订单全生命周期(含支付/退款) | + +--- + +## 16. 小结 + +支付驱动二开的本质是:**用标准包替换「供应商通信细节」,同时把业务状态机留在中台**。 + +做好三件事即可稳定落地: + +1. **合规落位**:`driver/pay/{Name}/` + 完整 `plugin.json` + `PayInterface` 实现。 +2. **接好三线**:下单 `create`、回调 `handleNotify`(验签/幂等)、退款 `refund`/`queryRefund`。 +3. **配齐两端**:`eb_pay_channel` 配置与用户端对 `pay_status_type` 的调起逻辑。 + +按此扩展,升级主线框架时支付通道可随驱动包独立演进,避免改核心、避免渠道枚举散落全库。 diff --git a/help/docs/文档索引.md b/help/docs/文档索引.md index db836dc40..17154db51 100644 --- a/help/docs/文档索引.md +++ b/help/docs/文档索引.md @@ -31,7 +31,7 @@ | 角色 | 推荐文档 | |---|---| | 产品/项目经理 | [项目说明](项目说明.md)、[项目功能](项目功能.md) | -| 后端开发 | [项目架构](项目架构.md)、[模块说明](模块与驱动.md)、[数据库设计](数据库设计.md)、[API 总览](api/接口总览.md) | +| 后端开发 | [项目架构](项目架构.md)、[模块说明](模块与驱动.md)、[支付驱动二开](支付驱动二开指南.md)、[数据库设计](数据库设计.md)、[API 总览](api/接口总览.md) | | 管理端开发 | [开发指南](开发指南.md)、[管理端 API](api/管理端接口.md)、[模块说明](模块与驱动.md) | | uni-app 开发 | [开发指南](开发指南.md)、[用户端 API](api/用户端接口.md)、[项目功能](项目功能.md) | | 产品/活动扩展开发 | [模块说明](模块与驱动.md)、[开发指南](开发指南.md) | @@ -47,6 +47,7 @@ | [项目功能](项目功能.md) | 管理端、用户端、交易、营销、渠道、客服、开放平台等功能 | | [项目架构](项目架构.md) | 技术栈、逻辑分层、核心链路、扩展机制、运行时组件 | | [模块说明](模块与驱动.md) | 22 个模块、30 个驱动、默认安装清单、安装生命周期与扩展规则 | +| [支付驱动二开指南](支付驱动二开指南.md) | 支付驱动定位、清单字段、PayInterface、下单/回调/退款链路、配置表、红线与验收排障 | | [AI Agent 模块](AI%20Agent%20模块.md) | `aiagent` 模块功能详述:对话与工具循环、模型/MCP/Skill 配置、自动化任务、数据表、接口与权限、已知限制 | | [数据库设计](数据库设计.md) | 数据来源、核心实体、表域划分、迁移与一致性原则 | | [开发指南](开发指南.md) | 环境准备、启动、目录约定、开发步骤、质量检查 |