chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# 进阶免签:安卓到账监听 App 接入指南
|
||||
|
||||
本插件内置「个人收款码免签 + 安卓通知监听自动确认」骨架,无需商户资质即可实现近似自动到账。
|
||||
|
||||
## 原理
|
||||
|
||||
个人微信/支付宝收款码**没有支付回调 API**,网关不会主动通知服务器。但手机本地会收到一条
|
||||
「微信支付:收到转账 XX.XX 元」系统通知。安卓 `NotificationListenerService` 可以监听并解析
|
||||
这条通知的文本,从而知道「到账了」以及「到账金额」。
|
||||
|
||||
为避免多人同时付款金额相同导致撞单,下单时给金额附加**随机零头**
|
||||
(如订单 99.00 元 → 实际让用户转 99.37 元)。监听 App 解析到金额 99.37 后,
|
||||
调用服务器 `notify-app` 接口按 `real_amount` 精确匹配订单,即可无歧义确认。
|
||||
|
||||
## 服务端接口
|
||||
|
||||
`POST /Mqpay/pay/notify-app`
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `order_sn` | 否 | 已知订单号时直接确认(优先) |
|
||||
| `amount` | 否 | 实际到账金额,按 `real_amount` 精确匹配待支付订单 |
|
||||
| `sign` | 是 | `md5(listen_key + amount + order_sn + listen_key)` |
|
||||
|
||||
配置项(后台或 `wxapp_addon_config`):
|
||||
- `listen_enable` = 1 开启监听
|
||||
- `personal_confirm_mode` = auto 确认方式设为自动监听
|
||||
- `listen_key` 自定义随机密钥(安卓端与服务端必须一致)
|
||||
- `listen_amount_jitter` = 1 开启金额零头防撞单
|
||||
|
||||
返回:`{code:0|1, message:'...', matched:int}`
|
||||
|
||||
## 安卓端关键代码(Kotlin 骨架)
|
||||
|
||||
```kotlin
|
||||
class PayNotifyListener : NotificationListenerService() {
|
||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||
val pkg = sbn.packageName
|
||||
// 仅处理微信/支付宝通知
|
||||
if (pkg != "com.tencent.mm" && pkg != "com.eg.android.AlipayGphone") return
|
||||
val text = sbn.notification.extras?.getCharSequence("android.text")?.toString() ?: return
|
||||
// 正则提取金额:如 "收到转账 99.37 元"
|
||||
val m = Regex("""([0-9]+(\.[0-9]{1,2})?)\s*元""").find(text) ?: return
|
||||
val amount = m.groupValues[1]
|
||||
val key = "你的_listen_key" // 与服务端一致
|
||||
val sign = md5(key + amount + "" + key)
|
||||
val body = "amount=$amount&sign=$sign"
|
||||
// 上报到服务器
|
||||
Http.post("https://你的域名/Mqpay/pay/notify-app", body)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`md5` 实现略(标准 MD5 小写十六进制)。需引导用户在系统设置中授予「通知使用权」。
|
||||
|
||||
## 安全说明
|
||||
|
||||
- `sign` 校验保证只有持有 `listen_key` 的 App 能触发确认,防止外部伪造到账。
|
||||
- 金额匹配仅在 `status=0` 待支付订单中查找,已支付订单不会被重复激活。
|
||||
- 监听 App 仅运行在站长本人手机上,不上传任何隐私数据到第三方。
|
||||
|
||||
## 局限
|
||||
|
||||
- 依赖手机在线且通知未被系统折叠/清理;极端情况下仍可手动在 `/Mqpay/admin` 补确认。
|
||||
- 微信/支付宝通知文案随版本变化,正则需按实际机型调整。
|
||||
- 本骨架聚焦「服务端闭环 + 安卓监听思路」,完整 App 需自行打包签名安装。
|
||||
|
||||
## 内置测试 / 管理页面(无需安卓 App 即可联调)
|
||||
|
||||
插件自带两个可直接浏览器打开的页面,方便体验与验证自动监听闭环:
|
||||
|
||||
1. **免签测试页 `/Mqpay/test`**(用户侧体验)
|
||||
- 选付费套餐 → 点「生成收款码」→ 调用 `/user/payment/create` 拿到收款码图片 + 精确金额(带零头)。
|
||||
- 展示二维码与「应付 ¥99.37」、订单号;前端每 3 秒轮询 `/Mqpay/pay/query` 查支付状态。
|
||||
- 「模拟安卓监听回调」按钮:直接按 `order_sn` 调 `/Mqpay/pay/notify-app`(联调用,签名留空走 order_sn 直确认分支)。
|
||||
- 不依赖会员中心前端,独立可跑。
|
||||
|
||||
2. **免签管理页 `/Mqpay/admin`**(站长侧)
|
||||
- 列出订单 + 「确认到账」手动确认按钮。
|
||||
- 开启监听后,待支付订单额外出现「模拟监听」按钮:由**服务端用 `listen_key` 预签 `sign`**(密钥不暴露前端),点按即按真实签名调 `/Mqpay/pay/notify-app`,验证签名校验 + 按金额精确匹配自动确认。
|
||||
|
||||
> 注意:自动监听按 `real_amount` 精确匹配依赖 `wxapp_Mqpay_order` 表的 `real_amount` 列。
|
||||
> 若此前已建表缺该列,需先执行:
|
||||
> `ALTER TABLE wxapp_Mqpay_order ADD real_amount decimal(10,2) NOT NULL DEFAULT 0.00;`
|
||||
> 未加列时,下单不会写回零头金额,「模拟监听」按金额匹配会找不到订单(仍可走 order_sn 直确认)。
|
||||
@@ -0,0 +1,108 @@
|
||||
# 支付钩子调用机制与时序图
|
||||
|
||||
本文档说明 mqpay 支付插件内部基于 ThinkPHP **事件系统(Event)+ 订阅器(Subscriber)** 的钩子调用方式,
|
||||
以及与会员开通(`UserMemberActivate`)的解耦关系。完整插件说明见 `../README.md`。
|
||||
|
||||
## 一、整体机制
|
||||
|
||||
支付钩子**不是**传统 `Hook::add`/`Hook::listen`,而是 ThinkPHP 的事件系统:
|
||||
|
||||
- 业务核心 `app/member/controller/Payment.php` 只负责「触发事件」(`Event::trigger`);
|
||||
- 支付插件 `addon/mqpay/subscribe/Payment.php` 通过「事件订阅器」监听并注入实现;
|
||||
- 两者解耦,框架在启动阶段按 `info.php` 声明自动注册,无需手动绑定。
|
||||
|
||||
## 二、调用链路(5 步)
|
||||
|
||||
1. **声明订阅器** — `addon/mqpay/info.php` 的 `events.subscribe`:
|
||||
```php
|
||||
'events' => [
|
||||
'subscribe' => [
|
||||
'addon\\mqpay\\subscribe\\Payment',
|
||||
],
|
||||
],
|
||||
```
|
||||
2. **自动注册** — `ywxapp/service/AppService.php::loadAddonRelevant()`
|
||||
遍历启用插件,调用 `app()->loadEvent($info['events'])`,把订阅器注册进事件系统。
|
||||
3. **订阅方法即钩子** — `subscribe/Payment.php` 中方法名约定 `on + 事件名`:
|
||||
- `onPaymentMethods` ↔ `PaymentMethods`
|
||||
- `onPaymentOrderCreate` ↔ `PaymentOrderCreate`
|
||||
- `onPaymentCreate` ↔ `PaymentCreate`
|
||||
- `onPaymentNotify` ↔ `PaymentNotify`
|
||||
- `onPaymentFreeActive` ↔ `PaymentFreeActive`
|
||||
4. **业务点火** — `app/member/controller/Payment.php`:
|
||||
```php
|
||||
$results = Event::trigger('PaymentMethods', []); // Payment.php:77
|
||||
Event::trigger('PaymentOrderCreate', ['order' => $order]); // Payment.php:153
|
||||
$results = Event::trigger('PaymentCreate', [...]); // Payment.php:156
|
||||
$results = Event::trigger('PaymentNotify', [...]); // Payment.php:180
|
||||
Event::trigger('PaymentFreeActive', [...]); // Payment.php:134
|
||||
```
|
||||
5. **返回值消费**:
|
||||
- `PaymentMethods` → 返回 `array`,合并去重后返回前端(按 `sort` 排序)。
|
||||
- `PaymentCreate` → 返回 `array`(如 `['type'=>'qrcode','qrcode_url'=>...]`)或 `null`(不处理);`create()` 取第一个非空作为支付参数。
|
||||
- `PaymentNotify` → 返回 `'success'`/`'fail'`,`notify()` 直接作为网关响应体输出。
|
||||
- `PaymentOrderCreate` / `PaymentFreeActive` → 副作用型(写库/日志),返回值忽略。
|
||||
|
||||
## 三、会员开通:UserMemberActivate 监听器
|
||||
|
||||
支付成功后本插件仅完成**订单激活**(`wxapp_mqpay_order.status=1`),真正的「会员权益写入」
|
||||
(用户组 / 会员有效期 / 资金流水 / 操作日志)通过触发 `UserMemberActivate` 事件交给业务层处理。
|
||||
|
||||
- **实现**:`app/member/listener/UserMemberActivate.php`
|
||||
- `handle($event)` 读取 `uid` / `plan_id` / `order` / `method`;
|
||||
- 按 `config('member.plans')` 取套餐 `duration`/`level`/`gid`;
|
||||
- 在原未过期时间上**顺延**期限(防重复购买覆盖);
|
||||
- 写 4 处:用户主表 `vip_expire`/`vip_level`/`gid`、`user_group_access`、`user_bill`、`user_log`。
|
||||
- **注册**:全局 `app/event.php` 的 `listen` 数组(**非**插件 `info.php`)—— 与具体支付插件解耦。
|
||||
- **触发**:本插件 `confirmOrder()`(`subscribe/Payment.php:326`),两条路径殊途同归:
|
||||
- 安卓免签:`POST /mqpay/pay/notify-app` → `notifyApp()` 验签 → 按 `real_amount` 匹配 → `confirmOrder()`
|
||||
- 后台人工:后台 `Confirm` 接口 → `Pay.php` → `confirmOrder()`
|
||||
|
||||
## 四、完整时序图(以"个人免签"为例)
|
||||
|
||||
```
|
||||
用户浏览器 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`。
|
||||
|
||||
## 五、钩子 / 事件速查表
|
||||
|
||||
| 事件名 | 订阅方法 | 订阅位置 | 触发位置 | 返回值 |
|
||||
|---|---|---|---|---|
|
||||
| `PaymentMethods` | `onPaymentMethods` | `subscribe/Payment.php` | `Payment.php::methods()` | `array` 方式列表 |
|
||||
| `PaymentOrderCreate` | `onPaymentOrderCreate` | `subscribe/Payment.php` | `Payment.php::create()` | 无(写库) |
|
||||
| `PaymentCreate` | `onPaymentCreate` | `subscribe/Payment.php` | `Payment.php::create()` | `array`/`null` 支付参数 |
|
||||
| `PaymentNotify` | `onPaymentNotify` | `subscribe/Payment.php` | `Payment.php::notify()` | `'success'`/`'fail'` |
|
||||
| `PaymentFreeActive` | `onPaymentFreeActive` | `subscribe/Payment.php` | `Payment.php::create()` | 无 |
|
||||
| `UserMemberActivate` | `handle()` | `app/member/listener/UserMemberActivate.php` | `confirmOrder()` | 无(写库/日志) |
|
||||
|
||||
### 注册方式对照
|
||||
|
||||
- 支付钩子(前 5 个):`addon/mqpay/info.php` 的 `events.subscribe` 声明,框架启动时自动 `loadEvent` 注册。
|
||||
- 会员开通(最后 1 个):全局 `app/event.php` 的 `listen` 中注册,与支付插件解耦。
|
||||
Reference in New Issue
Block a user