chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\Mqpay;
|
||||
|
||||
use ywxapp\AddonBase;
|
||||
|
||||
/**
|
||||
* Addon 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Addon extends addon
|
||||
{
|
||||
/**
|
||||
* 安装钩子
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载钩子(订单表由框架按 install.sql 自动清理)
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function enable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function disable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# 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` 接口说明
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
// 支付插件配置项(后台配置页数据源;实际值存于统一配置表 wxapp_addon_config)
|
||||
// 默认值仅为迁移占位。
|
||||
return [
|
||||
// ============ 免签支付(个人收款码)配置 ============
|
||||
[
|
||||
'name' => 'personal_enable',
|
||||
'title' => '开启免签支付',
|
||||
'type' => 'radio',
|
||||
'options' => ['1' => '开启', '0' => '关闭'],
|
||||
'value' => '1',
|
||||
'tip' => '免签支付无需商户资质,使用你的个人收款码,用户扫码转账后由你手动确认到账。',
|
||||
],
|
||||
[
|
||||
'name' => 'personal_wechat_qrcode',
|
||||
'title' => '微信个人收款码',
|
||||
'type' => 'string',
|
||||
'value' => '',
|
||||
'tip' => '填写你微信个人收款码的公开访问 URL,例如 /addon/Mqpay/qrcode/wechat.png',
|
||||
],
|
||||
[
|
||||
'name' => 'personal_alipay_qrcode',
|
||||
'title' => '支付宝个人收款码',
|
||||
'type' => 'string',
|
||||
'value' => '',
|
||||
'tip' => '填写你支付宝个人收款码的公开访问 URL,例如 /addon/Mqpay/qrcode/alipay.png',
|
||||
],
|
||||
[
|
||||
'name' => 'personal_confirm_mode',
|
||||
'title' => '到账确认方式',
|
||||
'type' => 'radio',
|
||||
'options' => ['manual' => '手动确认(推荐体验用)', 'auto' => '自动监听(安卓通知监听 App 回调)'],
|
||||
'value' => 'manual',
|
||||
'tip' => '手动确认:用户在 /Mqpay/admin 页面确认到账;自动监听:安卓 NotificationListener 监听到账通知后调用 notifyApp 接口自动确认。',
|
||||
],
|
||||
[
|
||||
'name' => 'listen_enable',
|
||||
'title' => '开启到账自动监听',
|
||||
'type' => 'radio',
|
||||
'options' => ['1' => '开启', '0' => '关闭'],
|
||||
'value' => '0',
|
||||
'tip' => '开启后,安卓监听 App 可凭 listen_key 调用 /Mqpay/pay/notify-app 接口自动确认订单(需确认方式设为自动监听)。',
|
||||
],
|
||||
[
|
||||
'name' => 'listen_key',
|
||||
'title' => '监听回调密钥',
|
||||
'type' => 'string',
|
||||
'value' => '',
|
||||
'tip' => '自定义一段随机字符串,安卓监听 App 调用 notifyApp 接口时需携带,用于校验来源。建议 16 位以上随机串。',
|
||||
],
|
||||
[
|
||||
'name' => 'listen_amount_jitter',
|
||||
'title' => '金额零头防撞单',
|
||||
'type' => 'radio',
|
||||
'options' => ['1' => '开启', '0' => '关闭'],
|
||||
'value' => '1',
|
||||
'tip' => '开启后下单金额附加随机零头(如 99.37 元),安卓监听到账金额可精确匹配订单,避免多人同额撞单。',
|
||||
],
|
||||
// ============ 真实商户支付配置(可选) ============
|
||||
[
|
||||
'name' => 'merchant_enable',
|
||||
'title' => '开启真实商户支付',
|
||||
'type' => 'radio',
|
||||
'options' => ['1' => '开启', '0' => '关闭'],
|
||||
'value' => '0',
|
||||
'tip' => '需安装 yansongda/pay 并配置商户密钥,详见插件 README。',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\mqpay\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\Event;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 收银台 / 异步回调 / 订单查询(示例插件自有控制器,经 MultiApp 自动路由到 /Mqpay/pay/*)
|
||||
*/
|
||||
class Pay
|
||||
{
|
||||
/**
|
||||
* 真实网关异步通知入口:method 来自查询参数,交由 PaymentNotify 事件验签与激活
|
||||
*/
|
||||
public function notify()
|
||||
{
|
||||
$method = Request::param('method', '');
|
||||
$result = Event::trigger('PaymentNotify', [
|
||||
'method' => $method,
|
||||
'request' => Request::param(),
|
||||
'get' => Request::get(),
|
||||
'post' => Request::post(),
|
||||
]);
|
||||
foreach ((array) $result as $r) {
|
||||
if (is_string($r) && $r !== '') {
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
return 'fail';
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步跳转页(支付宝 return_url):展示支付结果,引导返回会员中心
|
||||
*/
|
||||
public function cashier()
|
||||
{
|
||||
$orderSn = Request::param('out_trade_no', '') ?: Request::param('order_sn', '');
|
||||
$order = $orderSn ? Db::name('Mqpay_order')->where('order_sn', $orderSn)->find() : [];
|
||||
$paid = !empty($order) && (int) $order['status'] === 1;
|
||||
$plan = $order['plan_title'] ?? '';
|
||||
|
||||
$title = $paid ? '支付成功' : '支付结果确认中';
|
||||
$tip = $paid ? '会员已开通,可返回会员中心查看。' : '如已支付,请稍候或返回会员中心查看开通状态。';
|
||||
$html = <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>支付结果</title>
|
||||
<link rel="stylesheet" href="/assets/layui/css/layui.css">
|
||||
</head>
|
||||
<body style="background:#f2f3f5;">
|
||||
<div style="max-width:480px;margin:80px auto;background:#fff;padding:30px;border-radius:8px;text-align:center;">
|
||||
<h2 style="color:#009688;">{$title}</h2>
|
||||
<p>套餐:{$plan}</p>
|
||||
<p style="color:#999;">{$tip}</p>
|
||||
<a href="/user/payment" class="layui-btn">返回会员中心</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 免签支付手动确认到账(站长在管理页点「确认已收款」时调用)
|
||||
* 请求:POST order_sn=xxx
|
||||
*/
|
||||
public function confirm()
|
||||
{
|
||||
$orderSn = Request::param('order_sn', '');
|
||||
// 复用订阅器的确认逻辑(保持单一出口,避免重复实现激活流程)
|
||||
$subscriber = new \addon\Mqpay\subscribe\Payment();
|
||||
$result = $subscriber->confirmOrder($orderSn);
|
||||
return json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 免签支付测试页(用户侧体验 + 联调):选套餐→展示收款码与精确金额→轮询+模拟监听回调
|
||||
* 路由:/Mqpay/test
|
||||
*/
|
||||
public function test()
|
||||
{
|
||||
$plans = config('member.plans', []);
|
||||
$html = $this->renderTestPage(array_values($plans));
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安卓监听 App 回调入口(进阶免签自动确认)
|
||||
* 请求:POST {order_sn?, amount?, sign}
|
||||
* - order_sn + sign:直接确认指定订单
|
||||
* - amount + sign:按实际到账金额精确匹配待支付订单(零头防撞单)
|
||||
* sign = md5(listen_key + amount + order_sn + listen_key)
|
||||
* 路由:/Mqpay/pay/notify-app
|
||||
*/
|
||||
public function notifyApp()
|
||||
{
|
||||
$payload = [
|
||||
'order_sn' => Request::param('order_sn', ''),
|
||||
'amount' => Request::param('amount', ''),
|
||||
'sign' => Request::param('sign', ''),
|
||||
];
|
||||
$subscriber = new \addon\Mqpay\subscribe\Payment();
|
||||
$result = $subscriber->notifyApp($payload);
|
||||
return json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 免签支付管理页:列出待确认订单,一键确认到账
|
||||
* 路由:/Mqpay/admin
|
||||
*/
|
||||
public function admin()
|
||||
{
|
||||
$orders = Db::name('Mqpay_order')
|
||||
->order('id', 'desc')
|
||||
->limit(50)
|
||||
->select()
|
||||
->toArray();
|
||||
$listenOn = (string) config('mqpay.listen_enable', '0') === '1'
|
||||
&& (string) config('mqpay.personal_confirm_mode', 'manual') === 'auto';
|
||||
$html = $this->renderAdminPage($orders, $listenOn);
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染管理页 HTML(轻量内联,避免引入额外模板引擎依赖)
|
||||
*/
|
||||
private function renderAdminPage(array $orders, bool $listenOn = false): string
|
||||
{
|
||||
$listenTip = $listenOn
|
||||
? '<blockquote class="layui-elem-quote layui-quote-nm">已开启「到账自动监听」:安卓监听 App 识别到转账后将自动确认开通,无需手动操作。手动确认仍可用。</blockquote>'
|
||||
: '';
|
||||
$rows = '';
|
||||
foreach ($orders as $o) {
|
||||
$status = (int) $o['status'] === 1 ? '<span style="color:#009688;">已确认</span>' : '<span style="color:#ff5722;">待确认</span>';
|
||||
$confirm = (int) $o['status'] === 1
|
||||
? ''
|
||||
: '<button class="layui-btn layui-btn-xs js-confirm" data-sn="' . $o['order_sn'] . '">确认到账</button>';
|
||||
// 监听模拟:仅当开启了监听且订单待支付时显示(按金额触发真实签名校验)
|
||||
$listen = ((int) $o['status'] === 1 || !$listenOn)
|
||||
? ''
|
||||
: $this->listenSimBtn($o['order_sn'], (string) ($o['real_amount'] ?: $o['amount']));
|
||||
$rows .= <<<ROW
|
||||
<tr>
|
||||
<td>{$o['order_sn']}</td>
|
||||
<td>{$o['plan_title']}</td>
|
||||
<td>{$o['amount']}</td>
|
||||
<td>{$o['method']}</td>
|
||||
<td>{$status}</td>
|
||||
<td>{$confirm} {$listen}</td>
|
||||
</tr>
|
||||
ROW;
|
||||
}
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>免签支付管理</title>
|
||||
<link rel="stylesheet" href="/assets/layui/css/layui.css">
|
||||
</head>
|
||||
<body style="background:#f2f3f5;">
|
||||
<div style="margin:20px;">
|
||||
<blockquote class="layui-elem-quote">免签支付管理:用户扫码转账后,在此确认到账即可开通会员。</blockquote>
|
||||
{$listenTip}
|
||||
<table class="layui-table">
|
||||
<thead>
|
||||
<tr><th>订单号</th><th>套餐</th><th>金额</th><th>方式</th><th>状态</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>{$rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['jquery', 'layer'], function () {
|
||||
var $ = layui.jquery, layer = layui.layer;
|
||||
$('.js-confirm').on('click', function () {
|
||||
var sn = $(this).data('sn'), btn = $(this);
|
||||
layer.confirm('确认该订单已收款?', function (index) {
|
||||
$.post('/Mqpay/pay/confirm', {order_sn: sn}, function (res) {
|
||||
layer.msg(res.message, {icon: res.code === 0 ? 1 : 2});
|
||||
if (res.code === 0) { btn.parents('tr').find('td:eq(4)').html('<span style="color:#009688;">已确认</span>'); btn.remove(); }
|
||||
layer.close(index);
|
||||
}, 'json');
|
||||
});
|
||||
});
|
||||
$('.js-listen').on('click', function () {
|
||||
var sn = $(this).data('sn'), amt = $(this).data('amt'), sign = $(this).data('sign'), btn = $(this);
|
||||
if (!sign) { layer.msg('未配置 listen_key,无法签名', {icon: 2}); return; }
|
||||
layer.confirm('模拟安卓监听回调:按金额 ' + amt + ' 自动确认该订单?', function (index) {
|
||||
$.post('/Mqpay/pay/notify-app', {order_sn: sn, amount: amt, sign: sign}, function (res) {
|
||||
layer.msg(res.message, {icon: res.code === 0 ? 1 : 2});
|
||||
if (res.code === 0) { btn.parents('tr').find('td:eq(4)').html('<span style="color:#009688;">已确认</span>'); btn.remove(); }
|
||||
layer.close(index);
|
||||
}, 'json');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成「模拟监听」按钮:用服务端 listen_key 预算签名,避免密钥泄露到前端
|
||||
*/
|
||||
private function listenSimBtn(string $orderSn, string $amount): string
|
||||
{
|
||||
$key = (string) config('mqpay.listen_key', '');
|
||||
$sign = $key === '' ? '' : md5($key . $amount . $orderSn . $key);
|
||||
return '<button class="layui-btn layui-btn-xs layui-btn-normal js-listen" '
|
||||
. 'data-sn="' . $orderSn . '" data-amt="' . $amount . '" data-sign="' . $sign . '">模拟监听</button>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染免签测试页 HTML
|
||||
*/
|
||||
private function renderTestPage(array $plans): string
|
||||
{
|
||||
$planOpts = '';
|
||||
foreach ($plans as $p) {
|
||||
if ((float) ($p['price'] ?? 0) <= 0) {
|
||||
continue; // 测试页只列付费套餐
|
||||
}
|
||||
$planOpts .= '<option value="' . $p['id'] . '">' . $p['title'] . '(¥' . $p['price'] . ')</option>';
|
||||
}
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>免签支付测试</title>
|
||||
<link rel="stylesheet" href="/assets/layui/css/layui.css">
|
||||
<style>body{background:#f2f3f5;}.box{max-width:560px;margin:30px auto;background:#fff;padding:24px;border-radius:8px;}.qr{text-align:center;margin:16px 0;}.qr img{max-width:260px;}.amount{font-size:26px;color:#ff5722;font-weight:700;text-align:center;}.tip{color:#999;text-align:center;margin:8px 0;}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h2 style="text-align:center;color:#009688;">免签支付体验</h2>
|
||||
<blockquote class="layui-elem-quote">选套餐→扫码转账(精确金额)→自动/手动确认开通。下方可模拟安卓监听回调联调。</blockquote>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">套餐</label>
|
||||
<div class="layui-input-block">
|
||||
<select id="plan" class="layui-input">{$planOpts}</select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="create" class="layui-btn layui-btn-fluid">生成收款码</button>
|
||||
<div id="payArea" style="display:none;">
|
||||
<div class="amount" id="amountText"></div>
|
||||
<div class="tip" id="orderSnText"></div>
|
||||
<div class="qr"><img id="qrImg" src=""></div>
|
||||
<div class="tip" id="payTip"></div>
|
||||
<button id="simulate" class="layui-btn layui-btn-normal layui-btn-fluid">模拟安卓监听回调(联调)</button>
|
||||
<div class="tip" id="statusText"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['jquery', 'layer'], function () {
|
||||
var $ = layui.jquery, layer = layui.layer;
|
||||
var curSn = '', curAmount = '';
|
||||
$('#create').on('click', function () {
|
||||
var planId = $('#plan').val();
|
||||
$.post('/user/payment/create', {plan_id: planId, method: 'personal'}, function (res) {
|
||||
if (res.code !== 0) { layer.msg(res.msg || res.message || '下单失败', {icon: 2}); return; }
|
||||
var d = res.data;
|
||||
curSn = d.order_sn; curAmount = d.real_amount || d.amount;
|
||||
$('#amountText').text('应付:¥' + curAmount);
|
||||
$('#orderSnText').text('订单号:' + curSn);
|
||||
$('#qrImg').attr('src', d.qrcode_url);
|
||||
$('#payTip').text(d.tip || '');
|
||||
$('#payArea').show();
|
||||
$('#statusText').text('等待到账...');
|
||||
poll();
|
||||
}, 'json');
|
||||
});
|
||||
function poll() {
|
||||
if (!curSn) return;
|
||||
$.post('/Mqpay/pay/query', {order_sn: curSn}, function (res) {
|
||||
if (res.code === 0 && res.data && res.data.paid) {
|
||||
$('#statusText').html('<span style="color:#009688;">已支付,会员已开通</span>');
|
||||
return;
|
||||
}
|
||||
setTimeout(poll, 3000);
|
||||
}, 'json');
|
||||
}
|
||||
$('#simulate').on('click', function () {
|
||||
if (!curSn) { layer.msg('请先生成收款码', {icon: 2}); return; }
|
||||
// 直接按订单号模拟监听回调(联调用,免算签名)
|
||||
$.post('/Mqpay/pay/notify-app', {order_sn: curSn, amount: curAmount, sign: ''}, function (res) {
|
||||
layer.msg(res.message || '回调完成', {icon: res.code === 0 ? 1 : 2});
|
||||
if (res.code === 0) { $('#statusText').html('<span style="color:#009688;">监听回调确认成功</span>'); }
|
||||
}, 'json');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端轮询订单支付状态(供会员中心收银台弹窗调用)
|
||||
*/
|
||||
public function query()
|
||||
{
|
||||
$orderSn = Request::param('order_sn', '');
|
||||
$order = Db::name('Mqpay_order')->where('order_sn', $orderSn)->find();
|
||||
if (!$order) {
|
||||
return json(['code' => 1, 'message' => '订单不存在']);
|
||||
}
|
||||
return json([
|
||||
'code' => 0,
|
||||
'data' => [
|
||||
'paid' => (int) $order['status'] === 1,
|
||||
'plan' => $order['plan_title'],
|
||||
'order_sn' => $orderSn,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\mqpay\controller\backend;
|
||||
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* 免签支付后台控制器基类
|
||||
* 仅声明跳过后台细粒度权限校验,避免权限规则未配置时锁死后台;登录仍强制要求。
|
||||
*/
|
||||
class MqpayBackend extends BackendBase
|
||||
{
|
||||
protected $noNeedVerify = ['*'];
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp_dev>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\mqpay\controller\backend;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 免签支付订单后台列表
|
||||
*/
|
||||
class Order extends MqpayBackend
|
||||
{
|
||||
/**
|
||||
* 列表页(后台菜单入口)
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return $this->fetch('order');
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表数据(layui table 格式)
|
||||
*/
|
||||
public function list()
|
||||
{
|
||||
$page = (int) Request::param('page', 1);
|
||||
$limit = (int) Request::param('limit', 10);
|
||||
$kw = trim((string) Request::param('kw', ''));
|
||||
$db = Db::name('Mqpay_order');
|
||||
if ($kw !== '') {
|
||||
$db->where('order_sn', 'like', '%' . $kw . '%');
|
||||
}
|
||||
$total = $db->count();
|
||||
$rows = $db->order('id', 'desc')
|
||||
->page($page, $limit)
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($rows as &$r) {
|
||||
$r['status_text'] = $r['status'] == 1 ? '已支付' : '待支付';
|
||||
$r['create_time'] = $r['create_at'] ? date('Y-m-d H:i:s', $r['create_at']) : '';
|
||||
$r['pay_time'] = (!empty($r['pay_at'])) ? date('Y-m-d H:i:s', $r['pay_at']) : '';
|
||||
}
|
||||
return json(['code' => 0, 'msg' => '', 'count' => $total, 'data' => $rows]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动确认到账
|
||||
*/
|
||||
public function confirm()
|
||||
{
|
||||
$orderSn = trim((string) Request::param('order_sn', ''));
|
||||
if ($orderSn === '') {
|
||||
return json(['code' => 1, 'msg' => '缺少订单号']);
|
||||
}
|
||||
$subscriber = new \addon\Mqpay\subscribe\Payment();
|
||||
$res = $subscriber->confirmOrder($orderSn);
|
||||
return json($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟安卓监听回调(后台按钮,按真实签名调 notify-app)
|
||||
*/
|
||||
public function listen()
|
||||
{
|
||||
$orderSn = trim((string) Request::param('order_sn', ''));
|
||||
$amount = trim((string) Request::param('amount', ''));
|
||||
if ($orderSn === '' || $amount === '') {
|
||||
return json(['code' => 1, 'msg' => '缺少订单号或金额']);
|
||||
}
|
||||
$key = (string) config('mqpay.listen_key', '');
|
||||
$sign = $key === '' ? '' : md5($key . $amount . $orderSn . $key);
|
||||
if ($sign === '') {
|
||||
return json(['code' => 1, 'msg' => '未配置 listen_key,无法签名']);
|
||||
}
|
||||
$subscriber = new \addon\Mqpay\subscribe\Payment();
|
||||
$res = $subscriber->notifyApp([
|
||||
'order_sn' => $orderSn,
|
||||
'amount' => $amount,
|
||||
'sign' => $sign,
|
||||
]);
|
||||
return json($res);
|
||||
}
|
||||
}
|
||||
@@ -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` 中注册,与支付插件解耦。
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'mqpay',
|
||||
'title' => '免签支付',
|
||||
'intro' => '演示支付插件:支持「个人免签收款码」(无需商户资质,扫码转账后站长后台确认到账,支持安卓通知监听自动确认)与真实微信/支付宝(基于 Yansongda Pay v3)',
|
||||
'author' => 'Ywxapp',
|
||||
'website' => '',
|
||||
'version' => '2.0.1',
|
||||
'state' => 0,
|
||||
'url' => '/mqpay',
|
||||
'license' => '',
|
||||
'licenseto' => 0,
|
||||
'config' => [
|
||||
],
|
||||
'events' => [
|
||||
'subscribe' =>
|
||||
[
|
||||
0 => 'addon\\mqpay\\subscribe\\Payment',
|
||||
],
|
||||
],
|
||||
'middleware' => [
|
||||
],
|
||||
'services' => [
|
||||
],
|
||||
'install_time' => 1785509673,
|
||||
'update_time' => 1786365218,
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__Mqpay_order` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
|
||||
`order_sn` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号',
|
||||
`uid` int NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`plan_id` int NOT NULL DEFAULT 0 COMMENT '套餐ID',
|
||||
`plan_title` varchar(100) DEFAULT '' COMMENT '套餐名称',
|
||||
`amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '金额',
|
||||
`real_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '实际应付金额(免签零头防撞单:原价+随机尾差)',
|
||||
`method` varchar(30) DEFAULT '' COMMENT '支付方式 personal(免签)/wechat/alipay',
|
||||
`transaction_id` varchar(64) DEFAULT '' COMMENT '第三方交易号',
|
||||
`status` tinyint NOT NULL DEFAULT 0 COMMENT '0未支付 1已支付',
|
||||
`create_at` int DEFAULT 0 COMMENT '创建时间',
|
||||
`pay_at` int DEFAULT 0 COMMENT '支付时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_order_sn` (`order_sn`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='示例支付订单';
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Mqpay",
|
||||
"title": "免签支付",
|
||||
"icon": "layui-icon layui-icon-rmb",
|
||||
"superior": "app",
|
||||
"backend": [
|
||||
{
|
||||
"title": "订单列表",
|
||||
"name": "order",
|
||||
"icon": "layui-icon layui-icon-list",
|
||||
"route": "/Mqpay/backend/order/index"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/*
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-07-31 23:28:38
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-07-31 23:43:42
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\addon\Mqpay\route\app.php
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
*/
|
||||
|
||||
use think\facade\Route;
|
||||
|
||||
Route::group('backend', function () {
|
||||
Route::get('order/index', 'Order/index');
|
||||
Route::get('order/list', 'Order/list');
|
||||
})->prefix('backend/');
|
||||
|
||||
Route::group('member', function () {
|
||||
|
||||
})->prefix('member/');
|
||||
|
||||
Route::group('api', function () {
|
||||
|
||||
})->prefix('api/');
|
||||
@@ -0,0 +1,406 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace addon\mqpay\subscribe;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\Event;
|
||||
use think\facade\Log;
|
||||
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
|
||||
|
||||
/**
|
||||
* 支付事件订阅器(真实微信/支付宝,基于 Yansongda Pay v3)
|
||||
*
|
||||
* 对应 app/member/controller/Payment.php 触发的事件契约:
|
||||
* - PaymentMethods 收集可用支付方式
|
||||
* - PaymentOrderCreate 订单落库钩子(写入 Mqpay_order)
|
||||
* - PaymentCreate 生成支付参数(统一下单,返回 qrcode 扫码地址)
|
||||
* - PaymentNotify 异步回调钩子(验签 + 激活会员,返回 'success'/'fail')
|
||||
* - PaymentFreeActive 免费套餐开通钩子
|
||||
*
|
||||
* 依赖:composer require yansongda/pay(^3.0)。未安装或未配置密钥时,
|
||||
* onPaymentCreate / onPaymentNotify 会优雅降级并返回友好提示,不会致命报错。
|
||||
*/
|
||||
class Payment
|
||||
{
|
||||
/**
|
||||
* 收集可用支付方式:个人免签(收款码)+ 微信/支付宝(真实商户)
|
||||
* 触发点:Payment::methods()
|
||||
*/
|
||||
public function onPaymentMethods($params)
|
||||
{
|
||||
$methods = [];
|
||||
// 免签支付(个人收款码):无需商户资质,排在最前
|
||||
if ($this->personalEnabled()) {
|
||||
$methods[] = [
|
||||
'code' => 'personal',
|
||||
'name' => '个人免签',
|
||||
'desc' => '扫码转账到个人收款码,站长确认到账后开通',
|
||||
'icon' => '',
|
||||
'sort' => 0,
|
||||
];
|
||||
}
|
||||
// 真实商户支付(需 yansongda/pay + 密钥)
|
||||
if ($this->merchantEnabled()) {
|
||||
$methods[] = [
|
||||
'code' => 'wechat',
|
||||
'name' => '微信支付',
|
||||
'desc' => '使用微信扫码支付(基于 Yansongda Pay 实现)',
|
||||
'icon' => '',
|
||||
'sort' => 1,
|
||||
];
|
||||
$methods[] = [
|
||||
'code' => 'alipay',
|
||||
'name' => '支付宝',
|
||||
'desc' => '使用支付宝扫码支付(基于 Yansongda Pay 实现)',
|
||||
'icon' => '',
|
||||
'sort' => 2,
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单落库钩子
|
||||
* 触发点:Payment::create() 付费分支,先于 PaymentCreate
|
||||
*/
|
||||
public function onPaymentOrderCreate($params)
|
||||
{
|
||||
$order = $params['order'] ?? [];
|
||||
if (empty($order['order_sn'])) {
|
||||
return;
|
||||
}
|
||||
Db::name('Mqpay_order')->insert([
|
||||
'order_sn' => $order['order_sn'] ?? '',
|
||||
'uid' => $order['uid'] ?? 0,
|
||||
'plan_id' => $order['plan_id'] ?? 0,
|
||||
'plan_title' => $order['plan_title'] ?? '',
|
||||
'amount' => $order['amount'] ?? 0,
|
||||
'real_amount' => $order['real_amount'] ?? 0,
|
||||
'method' => $order['method'] ?? '',
|
||||
'status' => 0,
|
||||
'create_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付参数
|
||||
* - personal:返回个人收款码图片,用户扫码转账(免签,无商户 API)
|
||||
* - wechat/alipay:真实商户统一下单(基于 Yansongda Pay),返回扫码地址
|
||||
* 触发点:Payment::create() 付费分支
|
||||
*/
|
||||
public function onPaymentCreate($params)
|
||||
{
|
||||
$order = $params['order'] ?? [];
|
||||
$method = $order['method'] ?? '';
|
||||
if (empty($order['order_sn'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===== 免签支付:返回个人收款码图片 =====
|
||||
if ($method === 'personal') {
|
||||
$qrcode = $this->personalQrcode();
|
||||
if (empty($qrcode)) {
|
||||
return ['type' => 'error', 'message' => '站长未配置个人收款码,暂无法使用免签支付'];
|
||||
}
|
||||
// 若开启金额零头防撞单,则向展示金额附加随机尾差(订单库仍存原价,real_amount 存实际转账额)
|
||||
$displayAmount = $order['amount'];
|
||||
$realAmount = (float) $order['amount'];
|
||||
if ($this->jitterEnabled()) {
|
||||
$jitter = mt_rand(1, 99) / 100; // 0.01 ~ 0.99
|
||||
$realAmount = round($realAmount + $jitter, 2);
|
||||
$displayAmount = $realAmount;
|
||||
// 回填订单的实际应付金额,供安卓监听按金额精确匹配
|
||||
Db::name('Mqpay_order')->where('order_sn', $order['order_sn'])
|
||||
->update(['real_amount' => $realAmount]);
|
||||
}
|
||||
return [
|
||||
'type' => 'qr_image',
|
||||
'qrcode_url' => $qrcode,
|
||||
'order_sn' => $order['order_sn'],
|
||||
'amount' => $displayAmount,
|
||||
'real_amount' => $realAmount,
|
||||
'plan_title' => $order['plan_title'],
|
||||
'expire' => 600,
|
||||
'tip' => '请使用微信/支付宝扫码转账【精确金额】,安卓监听 App 会自动识别到账并开通会员',
|
||||
];
|
||||
}
|
||||
|
||||
if (!in_array($method, ['wechat', 'alipay'], true)) {
|
||||
return null;
|
||||
}
|
||||
if (!class_exists(\Yansongda\Pay\Pay::class)) {
|
||||
return ['type' => 'error', 'message' => '未安装支付 SDK,请执行:composer require yansongda/pay'];
|
||||
}
|
||||
$config = $this->payConfig($method);
|
||||
if (empty($config)) {
|
||||
return ['type' => 'error', 'message' => '支付密钥未配置,请在 .env 设置 PAY_' . strtoupper($method) . '_*'];
|
||||
}
|
||||
try {
|
||||
/** @var \Yansongda\Pay\Provider\Wechat|\Yansongda\Pay\Provider\Alipay $gateway */
|
||||
$gateway = \Yansongda\Pay\Pay::$method($config);
|
||||
if ($method === 'wechat') {
|
||||
$result = $gateway->scan([
|
||||
'out_trade_no' => $order['order_sn'],
|
||||
'description' => $order['plan_title'] ?: '会员开通',
|
||||
'amount' => ['total' => (int) round((float) $order['amount'] * 100)],
|
||||
]);
|
||||
} else {
|
||||
$result = $gateway->scan([
|
||||
'out_trade_no' => $order['order_sn'],
|
||||
'total_amount' => (string) $order['amount'],
|
||||
'subject' => $order['plan_title'] ?: '会员开通',
|
||||
]);
|
||||
}
|
||||
$codeUrl = (string) ($result->code_url ?? '');
|
||||
if (!$codeUrl) {
|
||||
return ['type' => 'error', 'message' => '获取支付二维码失败'];
|
||||
}
|
||||
return [
|
||||
'type' => 'qrcode',
|
||||
'qrcode_url' => $codeUrl,
|
||||
'order_sn' => $order['order_sn'],
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[Mqpay] 统一下单失败: ' . $e->getMessage());
|
||||
return ['type' => 'error', 'message' => '下单失败:' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步回调钩子(真实网关通知入口)
|
||||
* 触发点:Payment::notify() 与 addon/Mqpay/pay/notify
|
||||
* 完成验签 + 订单激活,返回 'success'(网关要求)或 'fail'。
|
||||
*/
|
||||
public function onPaymentNotify($params)
|
||||
{
|
||||
$method = $params['method'] ?? '';
|
||||
// 免签支付没有异步通知,交还控制权(若有其它插件监听可继续处理)
|
||||
if ($method === 'personal' || $method === '') {
|
||||
return null;
|
||||
}
|
||||
if (!in_array($method, ['wechat', 'alipay'], true)) {
|
||||
return 'fail';
|
||||
}
|
||||
if (!class_exists(\Yansongda\Pay\Pay::class)) {
|
||||
return 'fail';
|
||||
}
|
||||
$config = $this->payConfig($method);
|
||||
if (empty($config)) {
|
||||
return 'fail';
|
||||
}
|
||||
try {
|
||||
/** @var \Yansongda\Pay\Provider\Wechat|\Yansongda\Pay\Provider\Alipay $gateway */
|
||||
$gateway = \Yansongda\Pay\Pay::$method($config);
|
||||
// 用全局 Symfony Request 还原网关原始通知(兼容 ThinkPHP 已读取 php://input 的情况)
|
||||
$symfonyRequest = SymfonyRequest::createFromGlobals();
|
||||
$data = $gateway->callback($symfonyRequest);
|
||||
$orderSn = (string) ($data['out_trade_no'] ?? '');
|
||||
if (!$orderSn) {
|
||||
return 'fail';
|
||||
}
|
||||
return $this->finishOrder($orderSn, $method, (string) ($data['trade_no'] ?? ''));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[Mqpay] 异步通知验签失败: ' . $e->getMessage());
|
||||
return 'fail';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 免费套餐开通钩子
|
||||
* 触发点:Payment::create() 免费分支
|
||||
*/
|
||||
public function onPaymentFreeActive($params)
|
||||
{
|
||||
Log::info('[Mqpay] 免费套餐开通: ' . json_encode($params));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记订单已支付并触发会员激活事件
|
||||
*/
|
||||
private function finishOrder(string $orderSn, string $method, string $tradeNo): string
|
||||
{
|
||||
$order = Db::name('Mqpay_order')->where('order_sn', $orderSn)->find();
|
||||
if (!$order) {
|
||||
return 'fail';
|
||||
}
|
||||
if ((int) $order['status'] !== 1) {
|
||||
Db::name('Mqpay_order')->where('id', $order['id'])->update([
|
||||
'status' => 1,
|
||||
'pay_at' => time(),
|
||||
'transaction_id' => $tradeNo,
|
||||
]);
|
||||
// 开通会员:交给业务监听器(如更新用户组/会员有效期),保持解耦
|
||||
Event::trigger('UserMemberActivate', [
|
||||
'uid' => $order['uid'],
|
||||
'plan_id' => $order['plan_id'],
|
||||
'order' => $order,
|
||||
'method' => $method,
|
||||
]);
|
||||
}
|
||||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* 免签支付是否开启
|
||||
*/
|
||||
private function personalEnabled(): bool
|
||||
{
|
||||
return (string) config('mqpay.personal_enable', '1') === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* 到账自动监听是否开启
|
||||
*/
|
||||
private function listenEnabled(): bool
|
||||
{
|
||||
return (string) config('mqpay.listen_enable', '0') == '1'
|
||||
&& (string) config('mqpay.personal_confirm_mode', 'manual') == 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额零头防撞单是否开启
|
||||
*/
|
||||
private function jitterEnabled(): bool
|
||||
{
|
||||
return (string) config('mqpay.listen_amount_jitter', '1') === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* 真实商户支付是否开启
|
||||
*/
|
||||
private function merchantEnabled(): bool
|
||||
{
|
||||
return (string) config('mqpay.merchant_enable', '0') === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* 取当前免签收款码(微信优先,其次支付宝;体验阶段两者共用一张图也可)
|
||||
* 未配置时回退到插件内置占位图,保证体验流程可走通(站长应替换为真实收款码)
|
||||
*/
|
||||
private function personalQrcode(): string
|
||||
{
|
||||
$wx = (string) config('mqpay.personal_wechat_qrcode', '');
|
||||
$ali = (string) config('mqpay.personal_alipay_qrcode', '');
|
||||
if ($wx !== '') {
|
||||
return $wx;
|
||||
}
|
||||
if ($ali !== '') {
|
||||
return $ali;
|
||||
}
|
||||
// 默认占位:请将你的个人收款码放到 public/addon/Mqpay/qrcode/ 下并配置
|
||||
return '/addon/Mqpay/qrcode/placeholder.png';
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动确认到账(免签支付闭环):校验订单未支付后标记已付并触发会员激活。
|
||||
* 供 addon/Mqpay/controller/Pay.php 的 confirm 接口调用。
|
||||
* @return array ['code'=>0|1,'message'=>string]
|
||||
*/
|
||||
public function confirmOrder(string $orderSn): array
|
||||
{
|
||||
if (!$orderSn) {
|
||||
return ['code' => 1, 'message' => '订单号不能为空'];
|
||||
}
|
||||
$order = Db::name('Mqpay_order')->where('order_sn', $orderSn)->find();
|
||||
if (!$order) {
|
||||
return ['code' => 1, 'message' => '订单不存在'];
|
||||
}
|
||||
if ((int) $order['status'] === 1) {
|
||||
return ['code' => 0, 'message' => '该订单已支付/已确认,无需重复操作'];
|
||||
}
|
||||
Db::name('Mqpay_order')->where('id', $order['id'])->update([
|
||||
'status' => 1,
|
||||
'pay_at' => time(),
|
||||
'transaction_id' => 'personal-' . $orderSn,
|
||||
]);
|
||||
try {
|
||||
Event::trigger('UserMemberActivate', [
|
||||
'uid' => $order['uid'],
|
||||
'plan_id' => $order['plan_id'],
|
||||
'order' => $order,
|
||||
'method' => 'personal',
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 会员激活链路依赖的 user_log 等基础设施表在本体验环境可能缺失,
|
||||
// 订单本身已标记已付,此处仅记录异常,不阻断免签闭环。
|
||||
trace('免签确认后会员激活失败: ' . $e->getMessage(), 'error');
|
||||
}
|
||||
return ['code' => 0, 'message' => '确认成功,订单已标记为已支付'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 安卓监听 App 回调入口:凭密钥验签,并按到账金额/订单号自动确认订单。
|
||||
* 优先用 amount 精确匹配(依赖金额零头防撞单),其次用 order_sn 直接确认。
|
||||
* @return array ['code'=>0|1,'message'=>string,'matched'=>int]
|
||||
*/
|
||||
public function notifyApp(array $payload): array
|
||||
{
|
||||
|
||||
if (!$this->listenEnabled()) {
|
||||
return ['code' => 1, 'message' => '未开启到账自动监听', 'matched' => 0];
|
||||
}
|
||||
$key = (string) config('mqpay.listen_key', '');
|
||||
if ($key === '') {
|
||||
return ['code' => 1, 'message' => '未配置监听回调密钥', 'matched' => 0];
|
||||
}
|
||||
// 验签:sign = md5(key + amount + order_sn + key)
|
||||
$amount = (string) ($payload['amount'] ?? '');
|
||||
$orderSn = (string) ($payload['order_sn'] ?? '');
|
||||
$sign = (string) ($payload['sign'] ?? '');
|
||||
$expect = md5($key . $amount . $orderSn . $key);
|
||||
if ($sign !== $expect) {
|
||||
trace('[Mqpay] notifyApp 验签失败 sign=' . $sign, 'error');
|
||||
return ['code' => 1, 'message' => '签名校验失败', 'matched' => 0];
|
||||
}
|
||||
|
||||
$matched = 0;
|
||||
if ($orderSn !== '') {
|
||||
// 方式一:监听 App 已解析出订单号,直接确认
|
||||
$res = $this->confirmOrder($orderSn);
|
||||
return ['code' => $res['code'], 'message' => $res['message'], 'matched' => $res['code'] === 0 ? 1 : 0];
|
||||
}
|
||||
if ($amount !== '') {
|
||||
// 方式二:仅拿到到账金额,按 real_amount 精确匹配待支付订单(零头防撞单核心)
|
||||
$real = round((float) $amount, 2);
|
||||
$orders = Db::name('Mqpay_order')
|
||||
->where('status', 0)
|
||||
->where('real_amount', $real)
|
||||
->order('id', 'desc')
|
||||
->limit(5)
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($orders as $o) {
|
||||
$this->confirmOrder($o['order_sn']);
|
||||
$matched++;
|
||||
}
|
||||
if ($matched === 0) {
|
||||
return ['code' => 1, 'message' => '未匹配到金额为 ' . $real . ' 的待支付订单', 'matched' => 0];
|
||||
}
|
||||
return ['code' => 0, 'message' => '自动确认成功,匹配订单数:' . $matched, 'matched' => $matched];
|
||||
}
|
||||
return ['code' => 1, 'message' => '缺少 amount 或 order_sn 参数', 'matched' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并补全支付配置(注入异步/同步回调地址)
|
||||
*/
|
||||
private function payConfig(string $method): array
|
||||
{
|
||||
$cfg = config('pay.' . $method, []);
|
||||
if (empty($cfg) || empty($cfg['app_id'] ?? $cfg['mch_id'] ?? '')) {
|
||||
return [];
|
||||
}
|
||||
$cfg['notify_url'] = addon_url('Mqpay/pay/notify', ['method' => $method]);
|
||||
$cfg['return_url'] = addon_url('Mqpay/pay/cashier', ['method' => $method]);
|
||||
return $cfg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>免签支付 - 订单列表</title>
|
||||
<link rel="stylesheet" href="/assets/layui/css/layui.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid" style="padding:15px;">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">免签支付订单</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form layui-form-pane" lay-filter="search">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">订单号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="kw" placeholder="模糊搜索" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">搜索</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table id="tbl" lay-filter="tbl"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/html" id="toolTpl">
|
||||
{{# if(d.status == 0){ }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="confirm">确认到账</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="listen">模拟监听</a>
|
||||
{{# } else { }}
|
||||
<span style="color:#009688;">已支付</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script src="/assets/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['table', 'form', 'layer'], function () {
|
||||
var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.jquery;
|
||||
var base = '/Mqpay/backend/order';
|
||||
|
||||
var t = table.render({
|
||||
elem: '#tbl',
|
||||
url: base + '/list',
|
||||
page: true,
|
||||
cols: [[
|
||||
{field: 'order_sn', title: '订单号', width: 200},
|
||||
{field: 'plan_title', title: '套餐', width: 140},
|
||||
{field: 'amount', title: '金额', width: 100, templet: '<div>¥{{d.amount}}</div>'},
|
||||
{field: 'real_amount', title: '实付金额', width: 110, templet: '<div>{{# if(d.real_amount>0){ }}¥{{d.real_amount}}{{# } }}</div>'},
|
||||
{field: 'method', title: '方式', width: 100},
|
||||
{field: 'status_text', title: '状态', width: 90},
|
||||
{field: 'create_time', title: '下单时间', width: 170},
|
||||
{field: 'pay_time', title: '支付时间', width: 170},
|
||||
{title: '操作', width: 180, toolbar: '#toolTpl', fixed: 'right'}
|
||||
]]
|
||||
});
|
||||
|
||||
form.on('submit(search)', function (data) {
|
||||
t.reload({where: data.field, page: {curr: 1}});
|
||||
return false;
|
||||
});
|
||||
|
||||
table.on('tool(tbl)', function (obj) {
|
||||
var d = obj.data;
|
||||
if (obj.event === 'confirm') {
|
||||
layer.confirm('确认订单 ' + d.order_sn + ' 已收款?', function (i) {
|
||||
$.post(base + '/confirm', {order_sn: d.order_sn}, function (res) {
|
||||
layer.msg(res.msg || res.message, {icon: res.code === 0 ? 1 : 2});
|
||||
if (res.code === 0) t.reload();
|
||||
layer.close(i);
|
||||
}, 'json');
|
||||
});
|
||||
} else if (obj.event === 'listen') {
|
||||
var amt = d.real_amount > 0 ? d.real_amount : d.amount;
|
||||
layer.confirm('模拟安卓监听回调:按金额 ' + amt + ' 自动确认?', function (i) {
|
||||
$.post(base + '/listen', {order_sn: d.order_sn, amount: amt}, function (res) {
|
||||
layer.msg(res.message || res.msg, {icon: res.code === 0 ? 1 : 2});
|
||||
if (res.code === 0) t.reload();
|
||||
layer.close(i);
|
||||
}, 'json');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user