chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -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,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user