Files

333 lines
14 KiB
PHP
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.
<?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,
],
]);
}
}