仰望星辰工作室

better-staridc-MNBT

better-staridc-MNBT/ app_plugins/alipay_official/bootstrap.php 10.8 KB · 280 行 原始文件
Z zfhsh first commit 1 天前
1<?php
2/**
3 * 支付宝官方插件主入口
4 *
5 * V1.81 P3:基于支付宝官方 API 直连,提供两种付款方式:
6 * - pc 电脑网站支付(alipay.trade.page.pay)
7 * - qrcode 当面付扫码(alipay.trade.precreate)
8 *
9 * 配置项(保存在 MN_plugin_option 表):
10 * - app_id 应用 APPID
11 * - private_key 应用私钥(RSA2)
12 * - public_key 支付宝公钥
13 * - gateway 网关地址(默认 https://openapi.alipay.com/gateway.do,沙箱可换)
14 *
15 * 依赖:PHP openssl + curl 扩展
16 */
17
18if (!defined('IN_CRONLITE')) {
19 exit;
20}
21
22require_once __DIR__ . '/lib/AlipayService.php';
23
24mnbt_plugin_register('alipay_official', [
25 'name' => '支付宝官方',
26 'description' => '支付宝官方 API 直连,PC + 当面付',
27 'icon' => 'mdi-alpha-a-circle',
28]);
29
30// ============================================================
31// 配置读取辅助
32// ============================================================
33function alipay_official_get_config()
34{
35 return [
36 'app_id' => trim((string)mnbt_plugin_option_get('alipay_official', 'app_id', '')),
37 'private_key' => trim((string)mnbt_plugin_option_get('alipay_official', 'private_key', '')),
38 'public_key' => trim((string)mnbt_plugin_option_get('alipay_official', 'public_key', '')),
39 'gateway' => trim((string)mnbt_plugin_option_get('alipay_official', 'gateway', '')),
40 ];
41}
42
43function alipay_official_is_configured()
44{
45 $c = alipay_official_get_config();
46 return $c['app_id'] !== '' && $c['private_key'] !== '' && $c['public_key'] !== '';
47}
48
49function alipay_official_get_service()
50{
51 if (!alipay_official_is_configured()) {
52 return null;
53 }
54 return new Alipay_Official_Service(alipay_official_get_config());
55}
56
57// ============================================================
58// 注册支付插件
59// ============================================================
60mnbt_register_payment('alipay_official', [
61 'name' => '支付宝官方',
62 'description' => '支付宝官方 API 直连(RSA2)',
63 'icon' => 'mdi-alpha-a-circle',
64 'methods' => [
65 'pc' => ['name' => '支付宝(电脑网站)', 'icon' => 'mdi-desktop-mac'],
66 'qrcode' => ['name' => '支付宝(扫码)', 'icon' => 'mdi-qrcode-scan'],
67 ],
68 'build' => function ($method, $order, $plugin_config) {
69 $svc = alipay_official_get_service();
70 if (!$svc) {
71 error_log('[MNBT alipay_official] 未配置 app_id/private_key/public_key,无法发起支付');
72 return false;
73 }
74 $siteurl = isset($order['siteurl']) ? $order['siteurl'] : '';
75 $siteurl = rtrim((string)$siteurl, '/') . '/';
76 // 回调地址统一用 index.php?_r= 兼容路由:不依赖服务器伪静态重写(同 epay)
77 $notifyUrl = $siteurl . 'index.php?_r=/pay/alipay_official/notify';
78 $returnUrl = $siteurl . 'index.php?_r=/pay/alipay_official/return';
79
80 $params = [
81 'out_trade_no' => $order['out_trade_no'],
82 'total_amount' => $order['money'],
83 'subject' => $order['name'],
84 'notify_url' => $notifyUrl,
85 ];
86
87 if ($method === 'pc') {
88 $params['return_url'] = $returnUrl;
89 return $svc->buildPagePay($params);
90 }
91
92 if ($method === 'qrcode') {
93 $result = $svc->buildPrecreate($params);
94 if (empty($result['ok']) || empty($result['qr_code'])) {
95 error_log('[MNBT alipay_official] precreate 失败: ' . json_encode($result['raw'], JSON_UNESCAPED_UNICODE));
96 return alipay_official_render_qr_error($result);
97 }
98 return alipay_official_render_qr_page($order, $result['qr_code'], $returnUrl);
99 }
100
101 return false;
102 },
103]);
104
105/**
106 * 渲染扫码支付页面(HTML)。
107 */
108function alipay_official_render_qr_page($order, $qrCode, $returnUrl)
109{
110 $amount = htmlspecialchars((string)$order['money'], ENT_QUOTES, 'UTF-8');
111 $subject = htmlspecialchars((string)$order['name'], ENT_QUOTES, 'UTF-8');
112 $orderno = htmlspecialchars((string)$order['out_trade_no'], ENT_QUOTES, 'UTF-8');
113 $qrEscape = htmlspecialchars($qrCode, ENT_QUOTES, 'UTF-8');
114 $qrImg = 'https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=' . rawurlencode($qrCode);
115 $returnEsc = htmlspecialchars($returnUrl, ENT_QUOTES, 'UTF-8');
116
117 return <<<HTML
118<!DOCTYPE html>
119<html lang="zh-CN">
120<head>
121<meta charset="utf-8">
122<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
123<title>支付宝扫码支付</title>
124<style>
125*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;}
126body{background:#f5f7fa;color:#1e293b;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:16px;}
127.qr-card{background:#fff;border-radius:16px;box-shadow:0 4px 24px rgba(15,23,42,.08);max-width:380px;width:100%;padding:28px 24px;text-align:center;}
128.qr-card h1{font-size:18px;font-weight:600;margin-bottom:6px;color:#0f172a;}
129.qr-card .sub{font-size:13px;color:#64748b;margin-bottom:20px;}
130.qr-card .amount{font-size:28px;font-weight:600;color:#1677ff;margin:8px 0 16px;}
131.qr-card .amount small{font-size:14px;font-weight:400;color:#64748b;}
132.qr-img-wrap{background:#fafbfc;border:1px solid #eef1f5;border-radius:12px;padding:16px;margin-bottom:18px;display:flex;justify-content:center;}
133.qr-img-wrap img{width:240px;height:240px;display:block;}
134.qr-tip{font-size:12px;color:#94a3b8;margin-bottom:18px;}
135.qr-tip b{color:#475569;}
136.qr-btn{display:inline-block;background:#1677ff;color:#fff;text-decoration:none;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;transition:background .15s;}
137.qr-btn:hover{background:#0958d9;}
138.orderno{font-size:11px;color:#cbd5e1;margin-top:14px;word-break:break-all;}
139</style>
140</head>
141<body>
142<div class="qr-card">
143 <h1>支付宝扫码支付</h1>
144 <div class="sub">{$subject}</div>
145 <div class="amount">¥{$amount}</div>
146 <div class="qr-img-wrap">
147 <img src="{$qrImg}" alt="支付二维码">
148 </div>
149 <div class="qr-tip">请使用 <b>支付宝</b> 扫描上方二维码完成支付<br>支付完成后请点击下方按钮返回</div>
150 <a href="{$returnEsc}" class="qr-btn">我已完成支付</a>
151 <div class="orderno">订单号:{$orderno}</div>
152</div>
153</body>
154</html>
155HTML;
156}
157
158/**
159 * 渲染扫码失败页面(HTML)。
160 */
161function alipay_official_render_qr_error($result)
162{
163 $raw = is_array($result['raw'] ?? null) ? $result['raw'] : [];
164 $msg = $raw['msg'] ?? ($raw['error'] ?? '未知错误');
165 $sub = $raw['sub_msg'] ?? '';
166 $msgEsc = htmlspecialchars((string)$msg, ENT_QUOTES, 'UTF-8');
167 $subEsc = htmlspecialchars((string)$sub, ENT_QUOTES, 'UTF-8');
168 // 预先计算子消息行,避免在 heredoc 中使用嵌套 {$} 三元表达式(部分 PHP 版本解析异常)
169 $subLine = $subEsc !== '' ? '<p>' . $subEsc . '</p>' : '';
170
171 return <<<HTML
172<!DOCTYPE html>
173<html lang="zh-CN">
174<head>
175<meta charset="utf-8">
176<meta name="viewport" content="width=device-width, initial-scale=1">
177<title>支付发起失败</title>
178<style>
179body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:#f5f7fa;color:#1e293b;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:16px;}
180.err-card{background:#fff;border-radius:12px;box-shadow:0 4px 24px rgba(15,23,42,.08);max-width:420px;width:100%;padding:24px;text-align:center;}
181.err-card h1{font-size:18px;color:#b91c1c;margin-bottom:10px;}
182.err-card p{font-size:13px;color:#64748b;margin-bottom:6px;}
183.err-card .back{display:inline-block;margin-top:14px;color:#1677ff;text-decoration:none;font-size:14px;}
184</style>
185</head>
186<body>
187<div class="err-card">
188 <h1>支付发起失败</h1>
189 <p>{$msgEsc}</p>
190 {$subLine}
191 <a href="javascript:history.back()" class="back">返回上一页</a>
192</div>
193</body>
194</html>
195HTML;
196}
197
198// ============================================================
199// 回调路由:/pay/alipay_official/notify(异步通知)
200// ============================================================
201mnbt_register_route('POST', '/pay/alipay_official/notify', function ($params, $ctx) {
202 @header('Content-Type: text/plain; charset=UTF-8');
203 $svc = alipay_official_get_service();
204 if (!$svc) {
205 echo 'fail';
206 return;
207 }
208 $post = $_POST;
209 if (empty($post)) {
210 mnbt_pay_log('支付宝官方异步通知无数据', '回调异常', '');
211 echo 'fail';
212 return;
213 }
214 if (!$svc->verifyNotify($post)) {
215 mnbt_pay_log('支付宝官方异步通知验签失败', '验签失败', $post['out_trade_no'] ?? '');
216 echo 'fail';
217 return;
218 }
219 $outTradeNo = isset($post['out_trade_no']) ? (string)$post['out_trade_no'] : '';
220 $tradeStatus = isset($post['trade_status']) ? (string)$post['trade_status'] : '';
221 $totalAmount = isset($post['total_amount']) ? (string)$post['total_amount'] : '';
222 if ($outTradeNo === '') {
223 echo 'fail';
224 return;
225 }
226 $result = mnbt_pay_settle_order($outTradeNo, $tradeStatus, $totalAmount);
227 if (!empty($result['ok'])) {
228 echo 'success';
229 } else {
230 echo 'fail';
231 }
232}, 10, null, true);
233
234// ============================================================
235// 回调路由:/pay/alipay_official/return(同步返回)
236// ============================================================
237mnbt_register_route('GET', '/pay/alipay_official/return', function ($params, $ctx) {
238 $base = isset($ctx['base']) ? $ctx['base'] : '';
239 // 同步返回仅作展示,订单最终状态以异步通知为准
240 @header('Location: ' . $base . '/user');
241});
242
243// ============================================================
244// 后台设置页
245// ============================================================
246mnbt_register_page('admin', 'settings', 'admin/settings.php', '支付宝官方设置');
247
248mnbt_register_settings_tab([
249 'title' => '支付宝官方设置',
250 'page' => 'settings',
251 'order' => 20,
252]);
253
254mnbt_register_menu('admin', [
255 'title' => '支付宝官方设置',
256 'page' => 'settings',
257 'icon' => 'mdi-alpha-a-circle',
258 'order' => 61,
259 'multitabs' => true,
260]);
261
262// ============================================================
263// AJAX:保存配置
264// ============================================================
265mnbt_register_ajax('admin', 'alipay_official_save', function () {
266 mnbt_plugin_require_admin();
267 $appId = isset($_POST['app_id']) ? trim((string)$_POST['app_id']) : '';
268 $privateKey = isset($_POST['private_key']) ? trim((string)$_POST['private_key']) : '';
269 $publicKey = isset($_POST['public_key']) ? trim((string)$_POST['public_key']) : '';
270 $gateway = isset($_POST['gateway']) ? trim((string)$_POST['gateway']) : '';
271 if (mb_strlen($appId) > 60) json_exit('APPID 过长');
272 if (mb_strlen($gateway) > 200) json_exit('网关地址过长');
273 if (mb_strlen($privateKey) > 4000) json_exit('私钥过长');
274 if (mb_strlen($publicKey) > 4000) json_exit('公钥过长');
275 mnbt_plugin_option_set('alipay_official', 'app_id', $appId);
276 mnbt_plugin_option_set('alipay_official', 'private_key', $privateKey);
277 mnbt_plugin_option_set('alipay_official', 'public_key', $publicKey);
278 mnbt_plugin_option_set('alipay_official', 'gateway', $gateway);
279 json_exit('保存成功');
280});