Files

148 lines
8.8 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.
# Mqpay — 示例支付插件(真实微信 / 支付宝)
通过监听 `app/member/controller/Payment.php` 暴露的 **Payment\* 事件** 接入真实微信/支付宝支付,
核心控制器零改动,所有支付能力由本插件注入(基于 [Yansongda Pay v3](https://pay.yansongda.cn))。
## 前置依赖
```bash
composer require yansongda/pay # ^3.0
```
`.env` 中配置真实密钥(对应 `config/pay.php`):
```env
# 微信支付
PAY_WECHAT_MCH_ID=你的商户号
PAY_WECHAT_APP_ID=你的AppID
PAY_WECHAT_SECRET_KEY=APIv3密钥
# PAY_WECHAT_CERT_CLIENT=/path/apiclient_cert.pem
# PAY_WECHAT_CERT_KEY=/path/apiclient_key.pem
# 支付宝
PAY_ALIPAY_APP_ID=你的AppID
PAY_ALIPAY_PUBLIC_KEY=支付宝公钥
PAY_ALIPAY_PRIVATE_KEY=应用私钥
```
> 未安装 SDK 或未配置密钥时,插件会优雅降级:`methods` 仍返回通道,`create` 返回友好错误提示,不会致命报错。
## 监听的事件契约
| 事件 | 订阅方法 | 入参 | 返回值 |
|------|----------|------|--------|
| `PaymentMethods` | `onPaymentMethods` | `[]` | `[['code'=>'wechat'\|'alipay','name','desc','icon','sort']]` |
| `PaymentOrderCreate` | `onPaymentOrderCreate` | `['order'=>[...]]` | 无(落库 Mqpay_order |
| `PaymentCreate` | `onPaymentCreate` | `['order'=>[...],'params'=>[...]]` | `['type'=>'qrcode','qrcode_url','order_sn']` |
| `PaymentNotify` | `onPaymentNotify` | `['method','post','get']` | `'success'` / `'fail'`(网关要求) |
| `PaymentFreeActive` | `onPaymentFreeActive` | `['user','plan']` | 无 |
## 前端流程
1. 用户选择套餐 → 弹窗列出 `wechat` / `alipay` 两个通道;
2. 确认后 `POST /user/payment/create` → 触发 `PaymentOrderCreate` + `PaymentCreate`
3. 插件调用 Yansongda 统一下单(扫码),前端用返回的 `qrcode_url` 渲染二维码;
4. 前端每 2 秒轮询 `POST /Mqpay/pay/query`(带 `order_sn`)检测支付结果;
5. 微信/支付宝异步通知 `POST /Mqpay/pay/notify?method=xxx` → 插件验签、标记订单、触发 `UserMemberActivate` 事件,返回 `success`
## 会员开通:UserMemberActivate 监听器
支付成功后本插件仅完成**订单激活**(把 `wxapp_mqpay_order.status` 置 1),
真正的「会员权益写入」(更新用户组 / 会员有效期 / 资金流水 / 操作日志)通过触发
`UserMemberActivate` 事件交给业务层处理,保证插件与业务解耦。
### 监听器位置
- 实现:`app/member/listener/UserMemberActivate.php`
- `handle($event)` 读取 `$event['uid']``$event['plan_id']``$event['order']``$event['method']`
- 依据 `config('member.plans')` 取套餐的 `duration`/`level`/`gid`
- 在原未过期时间基础上**顺延**会员期限(避免重复购买覆盖);
- 写入 4 处:用户主表 `vip_expire`/`vip_level`/`gid`、用户组关联 `user_group_access`
资金流水 `user_bill`、操作日志 `user_log`
- 注册:`app/event.php``listen` 数组(`app/event.php:27-29`):
```php
'UserMemberActivate' => [
app\member\listener\UserMemberActivate::class,
],
```
> 注:该监听器注册在全局 `app/event.php`(非插件 `info.php`),因此与具体支付插件解耦——
> 任何触发 `UserMemberActivate` 事件的插件都能复用同一套会员开通逻辑。
> `app/event.php` 注释里残留的 `paydemo` 字样是重命名前身,可忽略。)
### 触发点
本插件在两处触发该事件:
| 触发路径 | 调用链 |
|---|---|
| 安卓免签自动确认 | `POST /mqpay/pay/notify-app` → `notifyApp()` 验签 → 按 `real_amount` 精确匹配 → `confirmOrder()` |
| 后台人工确认 | 后台 `Confirm` 接口 → `Pay.php` → `confirmOrder()` |
`confirmOrder()` 触发代码示例(`subscribe/Payment.php:326`):
```php
Event::trigger('UserMemberActivate', [
'uid' => $order['uid'],
'plan_id' => $order['plan_id'],
'order' => $order,
'method' => 'personal',
]);
```
## 支付钩子完整调用链路(时序图)
```
用户浏览器 app/member/controller/Payment addon/mqpay/subscribe/Payment 安卓监听App 会员监听器
│ │ │ │ │
│─ GET methods ───────▶│ │ │ │
│ │ Event::trigger('PaymentMethods') │ │
│ │────────────────────────────▶│ onPaymentMethods() │ │
│◀─ 方式列表 ──────────│◀─────────────────────────────│ │ │
│ │ │ │ │
│─ POST create(personal)▶│ │ │ │
│ │ 生成 order │ │ │
│ │ Event::trigger('PaymentOrderCreate') │ │
│ │────────────────────────────▶│ onPaymentOrderCreate() │ │
│ │ Event::trigger('PaymentCreate') │ │
│ │────────────────────────────▶│ onPaymentCreate() │ │
│ │ │ personalQrcode()+jitter │ │
│◀─ 收款码+精确金额 ───│◀─────────────────────────────│ │ │
│ │ │ ║ 用户扫码转账到账 ║ │ │
│ │ │ │ 监听通知栏 │
│ │ │ │─ POST /mqpay/pay/notify-app(amount)▶│
│ │ │ notifyApp() 验签 md5(key+amount+sn+key) │
│ │ │ 按 real_amount 精确匹配 │ │
│ │ │ confirmOrder(order_sn) │ │
│ │ │ status=1 │ │
│ │ │ Event::trigger('UserMemberActivate') │
│ │ │────────────────────────── 开通会员 ──────▶│
│◀─ 轮询/刷新看到已开通─│ │ │ │
```
> 网关支付(微信/支付宝)走 `onPaymentNotify` 异步回调;**免签**(personal)无网关回调,
> 由 `notify-app`(安卓)或后台 `Confirm` 触发 `confirmOrder()`,殊途同归到 `UserMemberActivate`。
## 钩子 / 事件速查表
| 事件名 | 订阅方法 | 订阅位置 | 触发位置(Payment.php | 返回值 |
|---|---|---|---|---|
| `PaymentMethods` | `onPaymentMethods` | `subscribe/Payment.php` | `methods()` | `array` 方式列表 |
| `PaymentOrderCreate` | `onPaymentOrderCreate` | `subscribe/Payment.php` | `create()` | 无(写库) |
| `PaymentCreate` | `onPaymentCreate` | `subscribe/Payment.php` | `create()` | `array`/`null` 支付参数 |
| `PaymentNotify` | `onPaymentNotify` | `subscribe/Payment.php` | `notify()` | `'success'`/`'fail'` |
| `PaymentFreeActive` | `onPaymentFreeActive` | `subscribe/Payment.php` | `create()` | 无 |
| `UserMemberActivate` | `handle()` | `app/member/listener/UserMemberActivate.php` | `confirmOrder()``subscribe/Payment.php` | 无(写库/日志) |
### 注册方式对照
- 支付钩子(前 5 个):在 `addon/mqpay/info.php` 的 `events.subscribe` 声明订阅器类,
框架 `AppService::loadAddonRelevant()` 启动时自动 `loadEvent` 注册。
- 会员开通(最后 1 个):在全局 `app/event.php` 的 `listen` 中注册,与支付插件解耦。
## 相关文档
- [支付钩子调用机制与时序图](docs/payment_hooks.md) — 事件系统、订阅器注册、会员监听器链路详解
- [安卓到账监听 App 接入指南](docs/listen_app_guide.md) — 免签监听 App 对接 `notify-app` 接口说明