feat(pay): 统一收银台四种模式(站内二维码/当前页跳转/表单提交/JSAPI)
- PaymentOrchestrator 统一返回 mode + code_url/payment_url/form_html/jsapi_params
- 支付宝:当面付二维码 / 电脑网站支付 / H5;微信:Native / H5 / JSAPI(WechatOpenid 静默授权)
- 易支付:PC mapi 二维码,移动端自动提交表单
- 删除旧键 alipay_payment_type / wechat_payment_type / epay_device 与 redirect/qr_content 字段
- 新增 pay-cashier.js,移除 window.open,订单状态接口改为 /pay/order/{n}
- 拆分 credits.js(affiliate / withdraw-history)至 1000 行以内
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b340b77d5a
commit
1df2822f01
28 files changed
+1253
-834
No files matched your search
@@ -54,13 +54,15 @@ export const gatewaySchema = {
|
|||||||
pay_sdk_order: {},
|
pay_sdk_order: {},
|
||||||
alipay_appid: '',
|
alipay_appid: '',
|
||||||
alipay_privatekey: '',
|
alipay_privatekey: '',
|
||||||
alipay_payment_type: 'page',
|
alipay_pc_mode: 'facepay',
|
||||||
|
alipay_h5: false,
|
||||||
alipay_app_public_cert_path: '',
|
alipay_app_public_cert_path: '',
|
||||||
alipay_public_cert_path: '',
|
alipay_public_cert_path: '',
|
||||||
alipay_root_cert_path: '',
|
alipay_root_cert_path: '',
|
||||||
wechat_merchantid: '',
|
wechat_merchantid: '',
|
||||||
wechat_appid: '',
|
wechat_appid: '',
|
||||||
wechat_payment_type: 'native',
|
wechat_h5: false,
|
||||||
|
wechat_jsapi: false,
|
||||||
wechat_api_v3_key: '',
|
wechat_api_v3_key: '',
|
||||||
wechat_private_key_path: '',
|
wechat_private_key_path: '',
|
||||||
wechat_cert_path: '',
|
wechat_cert_path: '',
|
||||||
@@ -78,7 +80,7 @@ export const gatewaySchema = {
|
|||||||
epay_apiurl: '',
|
epay_apiurl: '',
|
||||||
epay_pid: '',
|
epay_pid: '',
|
||||||
epay_key: '',
|
epay_key: '',
|
||||||
epay_device: 'auto',
|
epay_qrcode: true,
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
@@ -206,20 +208,22 @@ export const gatewaySchema = {
|
|||||||
label: '',
|
label: '',
|
||||||
type: 'content',
|
type: 'content',
|
||||||
ui_only: true,
|
ui_only: true,
|
||||||
content: '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return}</li><li>同时填写了企业支付以及当面付参数,则优先使用当面付</li></ul>',
|
content: '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return}</li><li>电脑端按「电脑端收款」选择站内二维码或跳转;手机端开启 H5 时跳转,否则显示二维码</li></ul>',
|
||||||
},
|
},
|
||||||
{ key: 'alipay_appid', label: '当面付:APPID', type: 'input', default: '' },
|
{ key: 'alipay_appid', label: '当面付:APPID', type: 'input', default: '' },
|
||||||
{ key: 'alipay_privatekey', label: '当面付:应用私钥', type: 'textarea', sanitize_type: 'code', default: '', rows: 4, secret: true },
|
{ key: 'alipay_privatekey', label: '当面付:应用私钥', type: 'textarea', sanitize_type: 'code', default: '', rows: 4, secret: true },
|
||||||
{
|
{
|
||||||
key: 'alipay_payment_type',
|
key: 'alipay_pc_mode',
|
||||||
label: '支付方式',
|
label: '电脑端收款',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
default: 'page',
|
default: 'facepay',
|
||||||
options: [
|
options: [
|
||||||
{ label: '电脑网站支付(web)', value: 'page' },
|
{ label: '当面付(站内二维码)', value: 'facepay' },
|
||||||
{ label: '手机网站支付(wap)', value: 'wap' },
|
{ label: '电脑网站支付(跳转支付宝)', value: 'web' },
|
||||||
],
|
],
|
||||||
|
help: '当面付在站内弹出二维码;电脑网站支付在当前页跳转到支付宝收银台',
|
||||||
},
|
},
|
||||||
|
{ key: 'alipay_h5', label: '移动端 H5 跳转', type: 'switch', default: false, help: '需签约手机网站支付。开启后手机浏览器跳转支付宝 App/收银台付款;关闭则手机端也显示二维码(附唤起链接)' },
|
||||||
{ key: 'alipay_app_public_cert_path', label: '应用公钥证书路径', type: 'input', default: '', help: 'appCertPublicKey.crt 绝对路径。' },
|
{ key: 'alipay_app_public_cert_path', label: '应用公钥证书路径', type: 'input', default: '', help: 'appCertPublicKey.crt 绝对路径。' },
|
||||||
{ key: 'alipay_public_cert_path', label: '支付宝公钥证书路径', type: 'input', default: '', help: 'alipayCertPublicKey_RSA2.crt 绝对路径。' },
|
{ key: 'alipay_public_cert_path', label: '支付宝公钥证书路径', type: 'input', default: '', help: 'alipayCertPublicKey_RSA2.crt 绝对路径。' },
|
||||||
{ key: 'alipay_root_cert_path', label: '支付宝根证书路径', type: 'input', default: '', help: 'alipayRootCert.crt 绝对路径。' },
|
{ key: 'alipay_root_cert_path', label: '支付宝根证书路径', type: 'input', default: '', help: 'alipayRootCert.crt 绝对路径。' },
|
||||||
@@ -239,20 +243,12 @@ export const gatewaySchema = {
|
|||||||
label: '',
|
label: '',
|
||||||
type: 'content',
|
type: 'content',
|
||||||
ui_only: true,
|
ui_only: true,
|
||||||
content: '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li></ul>',
|
content: '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li><li>电脑端显示站内二维码;手机端按下方开关选择 H5 跳转或微信内 JSAPI,未开启时同样显示二维码</li></ul>',
|
||||||
},
|
},
|
||||||
{ key: 'wechat_merchantid', label: '商户号 PartnerID', type: 'input', default: '' },
|
{ key: 'wechat_merchantid', label: '商户号 PartnerID', type: 'input', default: '' },
|
||||||
{ key: 'wechat_appid', label: '授权绑定的AppID', type: 'input', default: '' },
|
{ key: 'wechat_appid', label: '授权绑定的AppID', type: 'input', default: '' },
|
||||||
{
|
{ key: 'wechat_h5', label: '移动端 H5 跳转', type: 'switch', default: false, help: '需开通 H5 支付。开启后手机浏览器(非微信内)跳转微信付款;关闭则显示站内二维码' },
|
||||||
key: 'wechat_payment_type',
|
{ key: 'wechat_jsapi', label: '微信内 JSAPI 支付', type: 'switch', default: false, help: '需开通 JSAPI 支付并在商户平台配置支付授权目录。微信内打开时直接调起支付;用户 openid 通过「用户→微信公众号」的 AppID/AppSecret 静默授权获取,公众号 AppID 必须与上方 AppID 一致' },
|
||||||
label: '支付方式',
|
|
||||||
type: 'select',
|
|
||||||
default: 'native',
|
|
||||||
options: [
|
|
||||||
{ label: '扫码支付(Native)', value: 'native' },
|
|
||||||
{ label: 'H5支付', value: 'h5' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ key: 'wechat_api_v3_key', label: '支付APIv3密钥', type: 'input', inputType: 'password', secret: true, default: '', help: '微信商户平台设置的 APIv3 密钥(32位)。' },
|
{ key: 'wechat_api_v3_key', label: '支付APIv3密钥', type: 'input', inputType: 'password', secret: true, default: '', help: '微信商户平台设置的 APIv3 密钥(32位)。' },
|
||||||
{ key: 'wechat_private_key_path', label: '商户私钥路径', type: 'input', default: '', help: 'apiclient_key.pem 绝对路径,用于签名。' },
|
{ key: 'wechat_private_key_path', label: '商户私钥路径', type: 'input', default: '', help: 'apiclient_key.pem 绝对路径,用于签名。' },
|
||||||
{ key: 'wechat_cert_path', label: '商户API证书路径', type: 'input', default: '', help: 'apiclient_cert.pem 绝对路径。' },
|
{ key: 'wechat_cert_path', label: '商户API证书路径', type: 'input', default: '', help: 'apiclient_cert.pem 绝对路径。' },
|
||||||
@@ -329,17 +325,7 @@ export const gatewaySchema = {
|
|||||||
{ key: 'epay_apiurl', label: 'API接口网址', type: 'input', default: '', help: '请填写接口地址,例如:https://pay.xxxxxxx.cn/' },
|
{ key: 'epay_apiurl', label: 'API接口网址', type: 'input', default: '', help: '请填写接口地址,例如:https://pay.xxxxxxx.cn/' },
|
||||||
{ key: 'epay_pid', label: '商户号', type: 'input', default: '' },
|
{ key: 'epay_pid', label: '商户号', type: 'input', default: '' },
|
||||||
{ key: 'epay_key', label: '商户秘钥', type: 'input', inputType: 'password', secret: true, default: '', help: 'MD5 通信密钥。' },
|
{ key: 'epay_key', label: '商户秘钥', type: 'input', inputType: 'password', secret: true, default: '', help: 'MD5 通信密钥。' },
|
||||||
{
|
{ key: 'epay_qrcode', label: '电脑端站内二维码', type: 'switch', default: true, help: '开启后电脑端通过 mapi 接口获取二维码在站内展示(接口未返回二维码时跳转支付页);关闭则跳转易支付收银台。移动端始终跳转' },
|
||||||
key: 'epay_device',
|
|
||||||
label: '下单终端',
|
|
||||||
type: 'select',
|
|
||||||
default: 'auto',
|
|
||||||
options: [
|
|
||||||
{ label: '自动(按 UA)', value: 'auto' },
|
|
||||||
{ label: '电脑端 submit', value: 'pc' },
|
|
||||||
{ label: '移动端 mapi', value: 'mobile' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+14
-13
@@ -281,7 +281,7 @@
|
|||||||
const maxTries = 40;
|
const maxTries = 40;
|
||||||
const timer = setInterval(function () {
|
const timer = setInterval(function () {
|
||||||
tries += 1;
|
tries += 1;
|
||||||
fetch(REST_BASE + '/pay/content/order/' + encodeURIComponent(orderNumber), {
|
fetch(REST_BASE + '/pay/order/' + encodeURIComponent(orderNumber), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: restHeaders(),
|
headers: restHeaders(),
|
||||||
@@ -673,13 +673,17 @@
|
|||||||
}
|
}
|
||||||
const data = (r.json && r.json.data) ? r.json.data : r.json;
|
const data = (r.json && r.json.data) ? r.json.data : r.json;
|
||||||
delete button.dataset.aksrtOperationKey;
|
delete button.dataset.aksrtOperationKey;
|
||||||
if (data && data.redirect && (data.payment_url || data.qr_content)) {
|
if (data && data.mode) {
|
||||||
if (data.payment_url) {
|
|
||||||
window.open(data.payment_url, '_blank');
|
|
||||||
}
|
|
||||||
renderScanPay(data, returnFocusEl);
|
|
||||||
if (window.AksrtModalRuntime) window.AksrtModalRuntime.invalidate(['orders']);
|
if (window.AksrtModalRuntime) window.AksrtModalRuntime.invalidate(['orders']);
|
||||||
if (data.order_number) pollOrderPaid(data.order_number, returnFocusEl);
|
if (data.mode === 'qrcode') {
|
||||||
|
renderScanPay(data, returnFocusEl);
|
||||||
|
} else if (window.AksrtPayCashier) {
|
||||||
|
close();
|
||||||
|
window.AksrtPayCashier.handle(data, {});
|
||||||
|
}
|
||||||
|
if ((data.mode === 'qrcode' || data.mode === 'jsapi') && data.order_number) {
|
||||||
|
pollOrderPaid(data.order_number, returnFocusEl);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -693,12 +697,9 @@
|
|||||||
|
|
||||||
function renderScanPay(data, returnFocusEl) {
|
function renderScanPay(data, returnFocusEl) {
|
||||||
data = data || {};
|
data = data || {};
|
||||||
let body = '';
|
const body = window.AksrtPayCashier
|
||||||
if (data.qr_content) {
|
? window.AksrtPayCashier.qrHtml(data)
|
||||||
body += '<div class="aksrt-dl-modal__code-card"><div class="aksrt-dl-modal__code-caption">凭证内容</div>' +
|
: '<p class="aksrt-dl-modal__msg">' + escapeHtml('收银台组件未加载,请刷新页面重试。') + '</p>';
|
||||||
'<code class="aksrt-dl-modal__code aksrt-dl-modal__code--wrap">' + escapeHtml(String(data.qr_content)) + '</code></div>';
|
|
||||||
}
|
|
||||||
body += '<p class="aksrt-dl-modal__msg">' + escapeHtml(data.message || '请使用手机扫码完成支付,支付后刷新页面。') + '</p>';
|
|
||||||
open(
|
open(
|
||||||
dlView('pay', {
|
dlView('pay', {
|
||||||
subtitle: '支付完成后页面会自动刷新',
|
subtitle: '支付完成后页面会自动刷新',
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
/**
|
||||||
|
* AKSRT affiliate revenue center + referral poster (split from credits.js).
|
||||||
|
* Exposes window.AKSRTAffiliateRevenueInit for the user-center router.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function api(path, options) {
|
||||||
|
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
||||||
|
return { ok: response.ok, status: response.status, json: response.json || {}, data: response.data };
|
||||||
|
}).catch(function (error) {
|
||||||
|
var response = error && error.response ? error.response : {};
|
||||||
|
return { ok: false, status: (error && error.status) || response.status || 0, json: (error && error.json) || response.json || {} };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Forward-only affiliate revenue center =====
|
||||||
|
/* 推广链接由 PHP 直出,首屏即可见、可复制;这里只补异步才拿得到的
|
||||||
|
* 海报配置与佣金比例,取失败也不影响复制。 */
|
||||||
|
function initAffiliateRevenue() {
|
||||||
|
var root = document.querySelector('[data-aksrt-revenue-page]');
|
||||||
|
if (!root || root.dataset.affiliateBound) return;
|
||||||
|
root.dataset.affiliateBound = '1';
|
||||||
|
var input = root.querySelector('[data-affiliate-link]');
|
||||||
|
var copy = root.querySelector('[data-affiliate-copy]');
|
||||||
|
var url = input ? input.value : '';
|
||||||
|
if (copy) copy.disabled = !url;
|
||||||
|
api('/revenue/me').then(function (r) {
|
||||||
|
if (!r.ok) return;
|
||||||
|
var j = r.json || {};
|
||||||
|
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
|
||||||
|
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title, j.referrer_name || '');
|
||||||
|
}).catch(function () { /* 海报与比例为增强项,静默失败即可。 */ });
|
||||||
|
if (copy) copy.addEventListener('click', function () {
|
||||||
|
if (!input || !input.value) return;
|
||||||
|
var done = function () { notyf('推广链接已复制', 'success', 3, 'aksrt-affiliate-copy-toast'); };
|
||||||
|
var fail = function () { input.focus(); input.select(); notyf('复制失败,请手动复制', 'danger', 3, 'aksrt-affiliate-copy-toast'); };
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(input.value).then(done).catch(fail);
|
||||||
|
else { input.focus(); input.select(); try { document.execCommand('copy'); done(); } catch (e) { fail(); } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Referral poster =====
|
||||||
|
/* 布局 1:1 复刻参考主题 poster-share.js 的 rebate 分支(非 product):
|
||||||
|
* 768×1168,顶部主图占上半屏、左上角日期、标题竖线 + 标题、红色推荐人标签层、
|
||||||
|
* 简介,底部带边框盒内左侧 contain 居中 logo 图 + 右下二维码,最底部居中文案。
|
||||||
|
* 顶部主图按参考主题的三级回退加载(banner → banner_default → banner_spare)。 */
|
||||||
|
function initAffiliatePoster(root, url, poster, siteName, referrerName) {
|
||||||
|
var button = root.querySelector('[data-affiliate-poster]');
|
||||||
|
var modal = document.querySelector('[data-aksrt-affiliate-poster-modal]');
|
||||||
|
var stage = modal ? modal.querySelector('[data-affiliate-poster-stage]') : null;
|
||||||
|
var loading = modal ? modal.querySelector('[data-affiliate-poster-loading]') : null;
|
||||||
|
var content = modal ? modal.querySelector('.modal-content') : null;
|
||||||
|
var download = modal ? modal.querySelector('[data-affiliate-poster-download]') : null;
|
||||||
|
if (!button || !modal) return;
|
||||||
|
if (!url || !poster || poster.enabled !== true) return;
|
||||||
|
button.hidden = false;
|
||||||
|
|
||||||
|
var backdrop = null;
|
||||||
|
var canvas = null;
|
||||||
|
|
||||||
|
function openPosterModal() {
|
||||||
|
if (modal.classList.contains('in')) return;
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
void modal.offsetWidth; // 强制 reflow,让 .fade 过渡从 scale(0.9) 起手
|
||||||
|
modal.classList.add('in');
|
||||||
|
modal.setAttribute('aria-hidden', 'false');
|
||||||
|
if (!backdrop) {
|
||||||
|
backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'modal-backdrop fade in';
|
||||||
|
backdrop.setAttribute('data-affiliate-poster-backdrop', '1');
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
}
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePosterModal() {
|
||||||
|
if (!modal.classList.contains('in')) return;
|
||||||
|
modal.classList.remove('in');
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
|
||||||
|
backdrop = null;
|
||||||
|
// 等过渡结束再撤 display,复刻 Bootstrap 的 fade-out 收尾
|
||||||
|
window.setTimeout(function () {
|
||||||
|
if (!modal.classList.contains('in')) modal.style.display = '';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.addEventListener('click', function (event) {
|
||||||
|
// 点遮罩层(.modal 自身)或关闭按钮(data-dismiss="modal")
|
||||||
|
if (event.target === modal || (event.target.closest && event.target.closest('[data-dismiss="modal"]'))) {
|
||||||
|
closePosterModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function (event) {
|
||||||
|
if (event.key === 'Escape' && modal.classList.contains('in')) closePosterModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (download) {
|
||||||
|
download.addEventListener('click', function () {
|
||||||
|
if (!canvas) return;
|
||||||
|
try {
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.download = 'referral-poster.png';
|
||||||
|
link.href = canvas.toDataURL('image/png');
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
} catch (err) {
|
||||||
|
/* Canvas is same-origin generated; download can only fail if the browser blocks it. */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
if (typeof window.qrcode !== 'function') {
|
||||||
|
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '二维码组件未加载。' + '</p>'; }
|
||||||
|
openPosterModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stage) { stage.innerHTML = ''; stage.classList.remove('loaded'); }
|
||||||
|
if (loading) loading.style.display = '';
|
||||||
|
if (download) download.classList.add('hide');
|
||||||
|
openPosterModal();
|
||||||
|
button.disabled = true;
|
||||||
|
buildPosterCanvas(url, poster, siteName, referrerName).then(function (built) {
|
||||||
|
canvas = built;
|
||||||
|
if (!stage) return;
|
||||||
|
// 参考主题:img.loaded-img 直接进 .poster-imgbox,遮罩淡出,
|
||||||
|
// 内容块高度从 220px 过渡到海报实际高度后清掉内联样式。
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = canvas.toDataURL('image/png');
|
||||||
|
img.alt = '推广海报';
|
||||||
|
img.className = 'loaded-img';
|
||||||
|
// 参考主题按 0.7 比例预设尺寸;.poster-imgbox > img 的 max-width:360px
|
||||||
|
// 会在窄屏把它压回弹窗宽度,height:100% 让 offsetHeight 反映真实渲染高度。
|
||||||
|
img.width = 768 * 0.7;
|
||||||
|
img.height = 1168 * 0.7;
|
||||||
|
stage.appendChild(img);
|
||||||
|
stage.classList.add('loaded');
|
||||||
|
if (download) {
|
||||||
|
download.href = img.src;
|
||||||
|
download.classList.remove('hide');
|
||||||
|
}
|
||||||
|
if (loading) loading.style.display = 'none';
|
||||||
|
if (content) {
|
||||||
|
content.style.height = stage.offsetHeight + 'px';
|
||||||
|
content.style.transition = '.2s';
|
||||||
|
window.setTimeout(function () {
|
||||||
|
content.style.height = '';
|
||||||
|
content.style.overflow = '';
|
||||||
|
content.style.transition = '';
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}).catch(function () {
|
||||||
|
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '海报生成失败,请重试。' + '</p>'; }
|
||||||
|
}).finally(function () {
|
||||||
|
button.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 三级回退加载图片:任一源成功即返回,全部失败才 reject(参考主题 loadCount 0→1→2)。
|
||||||
|
function loadPosterImage(sources) {
|
||||||
|
var list = (sources || []).filter(function (src) { return !!src; });
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var index = 0;
|
||||||
|
function next() {
|
||||||
|
if (index >= list.length) { reject(new Error('poster image unavailable')); return; }
|
||||||
|
var image = new Image();
|
||||||
|
image.crossOrigin = 'anonymous';
|
||||||
|
image.onload = function () { resolve(image); };
|
||||||
|
image.onerror = function () { index += 1; next(); };
|
||||||
|
image.src = list[index];
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// contain 裁切:参考主题 containImg(),等比缩放塞进盒子并居中,不裁切。
|
||||||
|
function containRect(sx, sy, boxW, boxH, srcW, srcH) {
|
||||||
|
var dx = sx, dy = sy, dWidth = boxW, dHeight = boxH;
|
||||||
|
if (srcW > srcH || (srcW === srcH && boxW < boxH)) {
|
||||||
|
dHeight = (srcH * dWidth) / srcW;
|
||||||
|
dy = sy + (boxH - dHeight) / 2;
|
||||||
|
} else if (srcW < srcH || (srcW === srcH && boxW > boxH)) {
|
||||||
|
dWidth = (srcW * dHeight) / srcH;
|
||||||
|
dx = sx + (boxW - dWidth) / 2;
|
||||||
|
}
|
||||||
|
return { x: dx, y: dy, width: dWidth, height: dHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参考主题 drawTextOverflow():按 maxWidth-120 换行、x 偏移 38,最多 maxRow 行,溢出补 ...。
|
||||||
|
// 返回实际测量宽度,供价格/标签层做横向排版。
|
||||||
|
function drawTextOverflow(ctx, text, maxWidth, maxRow, font, color, lineHeight, x, y) {
|
||||||
|
var rows = [];
|
||||||
|
var temp = '';
|
||||||
|
var limit = maxWidth - 120;
|
||||||
|
x = x + 38;
|
||||||
|
text = String(text || '').replace(/[\r\n]/g, '');
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.font = font;
|
||||||
|
var chars = text.split('');
|
||||||
|
var measured = 0;
|
||||||
|
if (ctx.measureText(text).width <= limit) {
|
||||||
|
rows.push(text);
|
||||||
|
} else {
|
||||||
|
for (var i = 0; i < chars.length; i++) {
|
||||||
|
if (rows.length === maxRow && i < chars.length - 1) {
|
||||||
|
rows[rows.length - 1] += '...';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ctx.measureText(temp).width < limit) {
|
||||||
|
temp += chars[i];
|
||||||
|
if (i === chars.length - 1) rows.push(temp);
|
||||||
|
} else {
|
||||||
|
i--; // 参考主题:回退一格防止字符丢失
|
||||||
|
rows.push(temp);
|
||||||
|
temp = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var r = 0; r < rows.length; r++) {
|
||||||
|
measured = Math.max(measured, ctx.measureText(rows[r]).width);
|
||||||
|
ctx.fillText(rows[r], x, y + r * lineHeight, limit);
|
||||||
|
}
|
||||||
|
return { width: measured, height: rows.length * lineHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参考主题 buildPosterCanvas rebate 分支:768×1168,顶部主图占上半屏,左上角日期,
|
||||||
|
// 标题竖蓝线 + 标题,推荐人标签(红),简介,底部盒(logo + 二维码 + 底部文案)。
|
||||||
|
function buildPosterCanvas(url, poster, siteName, referrerName) {
|
||||||
|
var width = 768, height = 1168;
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
var imgHeight = height / 2; // rebate 分支:顶部主图占上半屏
|
||||||
|
// 站点名走「周」前缀,与参考主题 zib__('week_prefix') 一致。
|
||||||
|
var weekLabel = '周' + '日一二三四五六'.charAt(new Date().getDay());
|
||||||
|
// 推荐人标签:参考主题 tags = `推荐人: {昵称}`,缺昵称时整层不画。
|
||||||
|
var tags = referrerName ? '推荐人: ' + String(referrerName) : '';
|
||||||
|
// 底部文案:优先推广海报配置,回退到分享设置 share_desc,再回退默认文案。
|
||||||
|
var bottom = String(poster.bottom || '扫描二维码阅读全文');
|
||||||
|
var title = String(poster.title || siteName || '');
|
||||||
|
var description = String(poster.description || '');
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
loadPosterImage([poster.image, poster.banner_default, poster.banner_spare]),
|
||||||
|
poster.logo ? loadPosterImage([poster.logo]).catch(function () { return null; }) : Promise.resolve(null)
|
||||||
|
]).then(function (assets) {
|
||||||
|
var banner = assets[0], logo = assets[1];
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
// 顶部主图(cover 裁切)。
|
||||||
|
var rect = coverRect(width, imgHeight, banner.width, banner.height);
|
||||||
|
ctx.drawImage(banner, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
|
||||||
|
// 左上角日期(白字,覆盖在主图上)。
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,1)';
|
||||||
|
ctx.font = '35px Arial';
|
||||||
|
ctx.fillText(weekLabel, 30, 37);
|
||||||
|
ctx.font = '28px Arial';
|
||||||
|
ctx.fillText((new Date().getMonth() + 1) + ' / ' + new Date().getDate(), 30, 87);
|
||||||
|
// 标题竖蓝线。
|
||||||
|
ctx.strokeStyle = '#00a1ff';
|
||||||
|
ctx.lineWidth = 7;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.moveTo(40, imgHeight + 38);
|
||||||
|
ctx.lineTo(40, imgHeight + 74);
|
||||||
|
ctx.stroke();
|
||||||
|
// 标题层(粗体)。参考 rebate 分支把 $title 子画布置于 (20, img_height+40),
|
||||||
|
// 子画布内左对齐 x=40、文字基线偏移 2,故绝对坐标 (40, img_height+42);
|
||||||
|
// drawTextOverflow 内部会 +38,所以这里传 2。
|
||||||
|
drawTextOverflow(ctx, title, width, 2, 'bold 35px Arial', '#424242', 52, 2, imgHeight + 42);
|
||||||
|
// 推荐人标签层(红,最多 1 行):$tags 置于 (0, img_height+160)。
|
||||||
|
if (tags) {
|
||||||
|
drawTextOverflow(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight + 162);
|
||||||
|
}
|
||||||
|
// 简介层(灰):$content 置于 (0, img_height+220)。
|
||||||
|
if (description) {
|
||||||
|
drawTextOverflow(ctx, description, width, 4, '25px Arial', 'rgba(88,88,88,1)', 39, 2, imgHeight + 222);
|
||||||
|
}
|
||||||
|
// 底部带边框盒。
|
||||||
|
ctx.strokeStyle = 'rgba(122,122,122,0.2)';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(30, height - 230, width - 60, 200);
|
||||||
|
ctx.stroke();
|
||||||
|
// logo 图:contain 居中,缺图时整块不画(参考主题 logo.naturalWidth 守卫)。
|
||||||
|
if (logo && logo.width && logo.height) {
|
||||||
|
var logoRect = containRect(80, height - 190, 300, 100, logo.width, logo.height);
|
||||||
|
ctx.drawImage(logo, logoRect.x, logoRect.y, logoRect.width, logoRect.height);
|
||||||
|
}
|
||||||
|
// 二维码(右下角)。
|
||||||
|
var qr = window.qrcode(0, 'M');
|
||||||
|
qr.addData(url);
|
||||||
|
qr.make();
|
||||||
|
var count = qr.getModuleCount();
|
||||||
|
var qrBox = 120;
|
||||||
|
var cell = qrBox / count;
|
||||||
|
var qrX = width - 200, qrY = height - 200;
|
||||||
|
ctx.fillStyle = '#0f172a';
|
||||||
|
for (var r = 0; r < count; r++) {
|
||||||
|
for (var c = 0; c < count; c++) {
|
||||||
|
if (qr.isDark(r, c)) {
|
||||||
|
ctx.fillRect(qrX + Math.floor(c * cell), qrY + Math.floor(r * cell), Math.ceil(cell), Math.ceil(cell));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 底部文案(居中,盒下方)。
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillStyle = '#b4b4b4';
|
||||||
|
ctx.font = '22px Arial';
|
||||||
|
ctx.fillText(String(bottom).slice(0, 46), width / 2, height - 58);
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
return canvas;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// cover 裁切:把源图按短边铺满目标框并居中裁切。
|
||||||
|
function coverRect(boxW, boxH, srcW, srcH) {
|
||||||
|
var sx = 0, sy = 0, sWidth = srcW, sHeight = srcH;
|
||||||
|
if (srcW / srcH > boxW / boxH) {
|
||||||
|
sWidth = (boxW * srcH) / boxH;
|
||||||
|
sx = (srcW - sWidth) / 2;
|
||||||
|
} else {
|
||||||
|
sHeight = (boxH * srcW) / boxW;
|
||||||
|
sy = (srcH - sHeight) / 2;
|
||||||
|
}
|
||||||
|
return { sx: sx, sy: sy, sWidth: sWidth, sHeight: sHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AKSRTAffiliateRevenueInit = initAffiliateRevenue;
|
||||||
|
|
||||||
|
jQuery(document).off("auto_fun.aksrt-credits-affiliate").on("auto_fun.aksrt-credits-affiliate", initAffiliateRevenue);
|
||||||
|
})();
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* AKSRT withdraw history modal (split from credits.js).
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function initWithdrawHistory() {
|
||||||
|
var modal = document.querySelector('[data-aksrt-withdraw-history-modal]');
|
||||||
|
if (!modal || modal.dataset.aksrtWithdrawHistoryInitialized === '1') return;
|
||||||
|
modal.dataset.aksrtWithdrawHistoryInitialized = '1';
|
||||||
|
var list = modal.querySelector('[data-aksrt-withdraw-history-list]');
|
||||||
|
var pagination = modal.querySelector('[data-aksrt-withdraw-history-pagination]');
|
||||||
|
var statusLabels = {
|
||||||
|
pending: '待处理',
|
||||||
|
processing: '处理中',
|
||||||
|
completed: '已完成',
|
||||||
|
rejected: '已拒绝'
|
||||||
|
};
|
||||||
|
var currentPage = 1;
|
||||||
|
var loading = false;
|
||||||
|
|
||||||
|
function text(value) {
|
||||||
|
return value === null || value === undefined ? '' : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendText(parent, value, className) {
|
||||||
|
var node = document.createElement('span');
|
||||||
|
if (className) node.className = className;
|
||||||
|
node.textContent = text(value);
|
||||||
|
parent.appendChild(node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGrossAmount(value) {
|
||||||
|
return text(value).replace(/\.00$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNetAmount(value) {
|
||||||
|
return text(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItems(items) {
|
||||||
|
if (!list) return;
|
||||||
|
list.textContent = '';
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
var empty = document.createElement('div');
|
||||||
|
empty.className = 'uc-transactions__empty';
|
||||||
|
var emptyText = document.createElement('p');
|
||||||
|
emptyText.textContent = '暂无提现记录';
|
||||||
|
empty.appendChild(emptyText);
|
||||||
|
list.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items.forEach(function (item) {
|
||||||
|
var status = text(item && item.status).toLowerCase();
|
||||||
|
var row = document.createElement('div');
|
||||||
|
row.className = 'uc-withdraw-history__item';
|
||||||
|
var badge = appendText(row, statusLabels[status] || status, 'badg badg-sm uc-withdraw-history__status');
|
||||||
|
badge.classList.add(status === 'rejected' ? 'c-red' : (status === 'completed' ? 'c-green' : 'c-blue'));
|
||||||
|
var gross = text(item && (item.gross_amount || item.amount));
|
||||||
|
var net = text(item && item.amount);
|
||||||
|
var amount = document.createElement('span');
|
||||||
|
amount.className = 'uc-withdraw-history__amount em12';
|
||||||
|
amount.appendChild(document.createTextNode(formatGrossAmount(gross)));
|
||||||
|
if (net !== '') {
|
||||||
|
var netText = document.createElement('span');
|
||||||
|
netText.className = 'uc-withdraw-history__net';
|
||||||
|
netText.textContent = '(实际到账:' + formatNetAmount(net) + '元)';
|
||||||
|
amount.appendChild(netText);
|
||||||
|
}
|
||||||
|
row.appendChild(amount);
|
||||||
|
var number = document.createElement('span');
|
||||||
|
number.className = 'uc-withdraw-history__number muted-2-color em09';
|
||||||
|
var numberLabel = document.createElement('span');
|
||||||
|
numberLabel.textContent = '订单号:';
|
||||||
|
number.appendChild(numberLabel);
|
||||||
|
var code = document.createElement('code');
|
||||||
|
code.textContent = text(item && item.withdrawal_number);
|
||||||
|
number.appendChild(code);
|
||||||
|
row.appendChild(number);
|
||||||
|
appendText(row, text(item && item.created_at), 'uc-withdraw-history__meta muted-2-color em09');
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(page, maxPages) {
|
||||||
|
if (!pagination) return;
|
||||||
|
pagination.textContent = '';
|
||||||
|
if (maxPages <= 1) return;
|
||||||
|
function addButton(label, target, disabled, current) {
|
||||||
|
var button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = 'uc-withdraw-history__page' + (current ? ' is-current' : '');
|
||||||
|
button.textContent = label;
|
||||||
|
button.disabled = !!disabled;
|
||||||
|
if (current) button.setAttribute('aria-current', 'page');
|
||||||
|
button.dataset.aksrtWithdrawHistoryPage = String(target);
|
||||||
|
pagination.appendChild(button);
|
||||||
|
}
|
||||||
|
addButton('上一页', page - 1, page <= 1, false);
|
||||||
|
for (var index = 1; index <= maxPages; index += 1) addButton(String(index), index, false, index === page);
|
||||||
|
addButton('下一页', page + 1, page >= maxPages, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPage(page) {
|
||||||
|
if (loading || !list) return;
|
||||||
|
loading = true;
|
||||||
|
list.textContent = '加载中…';
|
||||||
|
var rest = (window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || '';
|
||||||
|
if (!rest) {
|
||||||
|
list.textContent = '记录加载失败,请重试';
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var url = rest.replace(/\/$/, '') + '/earnings/withdrawals?page=' + encodeURIComponent(Math.max(1, page)) + '&per_page=10';
|
||||||
|
fetch(url, {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'X-WP-Nonce': (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || '' }
|
||||||
|
}).then(function (response) {
|
||||||
|
return response.json().then(function (payload) {
|
||||||
|
if (!response.ok || !payload || !Array.isArray(payload.items)) throw new Error('withdraw-history-load-failed');
|
||||||
|
currentPage = Math.max(1, parseInt(payload.page, 10) || 1);
|
||||||
|
var maxPages = Math.max(1, parseInt(payload.max_pages, 10) || 1);
|
||||||
|
renderItems(payload.items);
|
||||||
|
renderPagination(currentPage, maxPages);
|
||||||
|
});
|
||||||
|
}).catch(function () {
|
||||||
|
list.textContent = '记录加载失败,请重试';
|
||||||
|
if (pagination) pagination.textContent = '';
|
||||||
|
}).finally(function () {
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
modal.hidden = false;
|
||||||
|
document.body.classList.add('uc-modal-open');
|
||||||
|
loadPage(1);
|
||||||
|
}
|
||||||
|
function close() {
|
||||||
|
modal.hidden = true;
|
||||||
|
document.body.classList.remove('uc-modal-open');
|
||||||
|
}
|
||||||
|
document.querySelectorAll('[data-aksrt-withdraw-history-open]').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function (e) { e.preventDefault(); open(); });
|
||||||
|
});
|
||||||
|
modal.querySelectorAll('[data-aksrt-withdraw-history-close]').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function (e) { e.preventDefault(); close(); });
|
||||||
|
});
|
||||||
|
if (pagination) {
|
||||||
|
pagination.addEventListener('click', function (e) {
|
||||||
|
var button = e.target.closest('[data-aksrt-withdraw-history-page]');
|
||||||
|
if (!button || button.disabled) return;
|
||||||
|
e.preventDefault();
|
||||||
|
loadPage(parseInt(button.dataset.aksrtWithdrawHistoryPage || '1', 10) || 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jQuery(document).off("auto_fun.aksrt-withdraw-history").on("auto_fun.aksrt-withdraw-history", initWithdrawHistory);
|
||||||
|
})();
|
||||||
+12
-478
@@ -124,327 +124,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Forward-only affiliate revenue center =====
|
|
||||||
/* 推广链接由 PHP 直出,首屏即可见、可复制;这里只补异步才拿得到的
|
|
||||||
* 海报配置与佣金比例,取失败也不影响复制。 */
|
|
||||||
function initAffiliateRevenue() {
|
|
||||||
var root = document.querySelector('[data-aksrt-revenue-page]');
|
|
||||||
if (!root || root.dataset.affiliateBound) return;
|
|
||||||
root.dataset.affiliateBound = '1';
|
|
||||||
var input = root.querySelector('[data-affiliate-link]');
|
|
||||||
var copy = root.querySelector('[data-affiliate-copy]');
|
|
||||||
var url = input ? input.value : '';
|
|
||||||
if (copy) copy.disabled = !url;
|
|
||||||
api('/revenue/me').then(function (r) {
|
|
||||||
if (!r.ok) return;
|
|
||||||
var j = r.json || {};
|
|
||||||
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
|
|
||||||
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title, j.referrer_name || '');
|
|
||||||
}).catch(function () { /* 海报与比例为增强项,静默失败即可。 */ });
|
|
||||||
if (copy) copy.addEventListener('click', function () {
|
|
||||||
if (!input || !input.value) return;
|
|
||||||
var done = function () { notyf('推广链接已复制', 'success', 3, 'aksrt-affiliate-copy-toast'); };
|
|
||||||
var fail = function () { input.focus(); input.select(); notyf('复制失败,请手动复制', 'danger', 3, 'aksrt-affiliate-copy-toast'); };
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(input.value).then(done).catch(fail);
|
|
||||||
else { input.focus(); input.select(); try { document.execCommand('copy'); done(); } catch (e) { fail(); } }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Referral poster =====
|
|
||||||
/* 布局 1:1 复刻参考主题 poster-share.js 的 rebate 分支(非 product):
|
|
||||||
* 768×1168,顶部主图占上半屏、左上角日期、标题竖线 + 标题、红色推荐人标签层、
|
|
||||||
* 简介,底部带边框盒内左侧 contain 居中 logo 图 + 右下二维码,最底部居中文案。
|
|
||||||
* 顶部主图按参考主题的三级回退加载(banner → banner_default → banner_spare)。 */
|
|
||||||
function initAffiliatePoster(root, url, poster, siteName, referrerName) {
|
|
||||||
var button = root.querySelector('[data-affiliate-poster]');
|
|
||||||
var modal = document.querySelector('[data-aksrt-affiliate-poster-modal]');
|
|
||||||
var stage = modal ? modal.querySelector('[data-affiliate-poster-stage]') : null;
|
|
||||||
var loading = modal ? modal.querySelector('[data-affiliate-poster-loading]') : null;
|
|
||||||
var content = modal ? modal.querySelector('.modal-content') : null;
|
|
||||||
var download = modal ? modal.querySelector('[data-affiliate-poster-download]') : null;
|
|
||||||
if (!button || !modal) return;
|
|
||||||
if (!url || !poster || poster.enabled !== true) return;
|
|
||||||
button.hidden = false;
|
|
||||||
|
|
||||||
var backdrop = null;
|
|
||||||
var canvas = null;
|
|
||||||
|
|
||||||
function openPosterModal() {
|
|
||||||
if (modal.classList.contains('in')) return;
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
void modal.offsetWidth; // 强制 reflow,让 .fade 过渡从 scale(0.9) 起手
|
|
||||||
modal.classList.add('in');
|
|
||||||
modal.setAttribute('aria-hidden', 'false');
|
|
||||||
if (!backdrop) {
|
|
||||||
backdrop = document.createElement('div');
|
|
||||||
backdrop.className = 'modal-backdrop fade in';
|
|
||||||
backdrop.setAttribute('data-affiliate-poster-backdrop', '1');
|
|
||||||
document.body.appendChild(backdrop);
|
|
||||||
}
|
|
||||||
document.body.classList.add('modal-open');
|
|
||||||
}
|
|
||||||
|
|
||||||
function closePosterModal() {
|
|
||||||
if (!modal.classList.contains('in')) return;
|
|
||||||
modal.classList.remove('in');
|
|
||||||
modal.setAttribute('aria-hidden', 'true');
|
|
||||||
document.body.classList.remove('modal-open');
|
|
||||||
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
|
|
||||||
backdrop = null;
|
|
||||||
// 等过渡结束再撤 display,复刻 Bootstrap 的 fade-out 收尾
|
|
||||||
window.setTimeout(function () {
|
|
||||||
if (!modal.classList.contains('in')) modal.style.display = '';
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.addEventListener('click', function (event) {
|
|
||||||
// 点遮罩层(.modal 自身)或关闭按钮(data-dismiss="modal")
|
|
||||||
if (event.target === modal || (event.target.closest && event.target.closest('[data-dismiss="modal"]'))) {
|
|
||||||
closePosterModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('keydown', function (event) {
|
|
||||||
if (event.key === 'Escape' && modal.classList.contains('in')) closePosterModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (download) {
|
|
||||||
download.addEventListener('click', function () {
|
|
||||||
if (!canvas) return;
|
|
||||||
try {
|
|
||||||
var link = document.createElement('a');
|
|
||||||
link.download = 'referral-poster.png';
|
|
||||||
link.href = canvas.toDataURL('image/png');
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
} catch (err) {
|
|
||||||
/* Canvas is same-origin generated; download can only fail if the browser blocks it. */
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
button.addEventListener('click', function () {
|
|
||||||
if (typeof window.qrcode !== 'function') {
|
|
||||||
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '二维码组件未加载。' + '</p>'; }
|
|
||||||
openPosterModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (stage) { stage.innerHTML = ''; stage.classList.remove('loaded'); }
|
|
||||||
if (loading) loading.style.display = '';
|
|
||||||
if (download) download.classList.add('hide');
|
|
||||||
openPosterModal();
|
|
||||||
button.disabled = true;
|
|
||||||
buildPosterCanvas(url, poster, siteName, referrerName).then(function (built) {
|
|
||||||
canvas = built;
|
|
||||||
if (!stage) return;
|
|
||||||
// 参考主题:img.loaded-img 直接进 .poster-imgbox,遮罩淡出,
|
|
||||||
// 内容块高度从 220px 过渡到海报实际高度后清掉内联样式。
|
|
||||||
var img = document.createElement('img');
|
|
||||||
img.src = canvas.toDataURL('image/png');
|
|
||||||
img.alt = '推广海报';
|
|
||||||
img.className = 'loaded-img';
|
|
||||||
// 参考主题按 0.7 比例预设尺寸;.poster-imgbox > img 的 max-width:360px
|
|
||||||
// 会在窄屏把它压回弹窗宽度,height:100% 让 offsetHeight 反映真实渲染高度。
|
|
||||||
img.width = 768 * 0.7;
|
|
||||||
img.height = 1168 * 0.7;
|
|
||||||
stage.appendChild(img);
|
|
||||||
stage.classList.add('loaded');
|
|
||||||
if (download) {
|
|
||||||
download.href = img.src;
|
|
||||||
download.classList.remove('hide');
|
|
||||||
}
|
|
||||||
if (loading) loading.style.display = 'none';
|
|
||||||
if (content) {
|
|
||||||
content.style.height = stage.offsetHeight + 'px';
|
|
||||||
content.style.transition = '.2s';
|
|
||||||
window.setTimeout(function () {
|
|
||||||
content.style.height = '';
|
|
||||||
content.style.overflow = '';
|
|
||||||
content.style.transition = '';
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}).catch(function () {
|
|
||||||
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '海报生成失败,请重试。' + '</p>'; }
|
|
||||||
}).finally(function () {
|
|
||||||
button.disabled = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 三级回退加载图片:任一源成功即返回,全部失败才 reject(参考主题 loadCount 0→1→2)。
|
|
||||||
function loadPosterImage(sources) {
|
|
||||||
var list = (sources || []).filter(function (src) { return !!src; });
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
var index = 0;
|
|
||||||
function next() {
|
|
||||||
if (index >= list.length) { reject(new Error('poster image unavailable')); return; }
|
|
||||||
var image = new Image();
|
|
||||||
image.crossOrigin = 'anonymous';
|
|
||||||
image.onload = function () { resolve(image); };
|
|
||||||
image.onerror = function () { index += 1; next(); };
|
|
||||||
image.src = list[index];
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// contain 裁切:参考主题 containImg(),等比缩放塞进盒子并居中,不裁切。
|
|
||||||
function containRect(sx, sy, boxW, boxH, srcW, srcH) {
|
|
||||||
var dx = sx, dy = sy, dWidth = boxW, dHeight = boxH;
|
|
||||||
if (srcW > srcH || (srcW === srcH && boxW < boxH)) {
|
|
||||||
dHeight = (srcH * dWidth) / srcW;
|
|
||||||
dy = sy + (boxH - dHeight) / 2;
|
|
||||||
} else if (srcW < srcH || (srcW === srcH && boxW > boxH)) {
|
|
||||||
dWidth = (srcW * dHeight) / srcH;
|
|
||||||
dx = sx + (boxW - dWidth) / 2;
|
|
||||||
}
|
|
||||||
return { x: dx, y: dy, width: dWidth, height: dHeight };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 参考主题 drawTextOverflow():按 maxWidth-120 换行、x 偏移 38,最多 maxRow 行,溢出补 ...。
|
|
||||||
// 返回实际测量宽度,供价格/标签层做横向排版。
|
|
||||||
function drawTextOverflow(ctx, text, maxWidth, maxRow, font, color, lineHeight, x, y) {
|
|
||||||
var rows = [];
|
|
||||||
var temp = '';
|
|
||||||
var limit = maxWidth - 120;
|
|
||||||
x = x + 38;
|
|
||||||
text = String(text || '').replace(/[\r\n]/g, '');
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.font = font;
|
|
||||||
var chars = text.split('');
|
|
||||||
var measured = 0;
|
|
||||||
if (ctx.measureText(text).width <= limit) {
|
|
||||||
rows.push(text);
|
|
||||||
} else {
|
|
||||||
for (var i = 0; i < chars.length; i++) {
|
|
||||||
if (rows.length === maxRow && i < chars.length - 1) {
|
|
||||||
rows[rows.length - 1] += '...';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (ctx.measureText(temp).width < limit) {
|
|
||||||
temp += chars[i];
|
|
||||||
if (i === chars.length - 1) rows.push(temp);
|
|
||||||
} else {
|
|
||||||
i--; // 参考主题:回退一格防止字符丢失
|
|
||||||
rows.push(temp);
|
|
||||||
temp = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var r = 0; r < rows.length; r++) {
|
|
||||||
measured = Math.max(measured, ctx.measureText(rows[r]).width);
|
|
||||||
ctx.fillText(rows[r], x, y + r * lineHeight, limit);
|
|
||||||
}
|
|
||||||
return { width: measured, height: rows.length * lineHeight };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 参考主题 buildPosterCanvas rebate 分支:768×1168,顶部主图占上半屏,左上角日期,
|
|
||||||
// 标题竖蓝线 + 标题,推荐人标签(红),简介,底部盒(logo + 二维码 + 底部文案)。
|
|
||||||
function buildPosterCanvas(url, poster, siteName, referrerName) {
|
|
||||||
var width = 768, height = 1168;
|
|
||||||
var canvas = document.createElement('canvas');
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
var ctx = canvas.getContext('2d');
|
|
||||||
var imgHeight = height / 2; // rebate 分支:顶部主图占上半屏
|
|
||||||
// 站点名走「周」前缀,与参考主题 zib__('week_prefix') 一致。
|
|
||||||
var weekLabel = '周' + '日一二三四五六'.charAt(new Date().getDay());
|
|
||||||
// 推荐人标签:参考主题 tags = `推荐人: {昵称}`,缺昵称时整层不画。
|
|
||||||
var tags = referrerName ? '推荐人: ' + String(referrerName) : '';
|
|
||||||
// 底部文案:优先推广海报配置,回退到分享设置 share_desc,再回退默认文案。
|
|
||||||
var bottom = String(poster.bottom || '扫描二维码阅读全文');
|
|
||||||
var title = String(poster.title || siteName || '');
|
|
||||||
var description = String(poster.description || '');
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
loadPosterImage([poster.image, poster.banner_default, poster.banner_spare]),
|
|
||||||
poster.logo ? loadPosterImage([poster.logo]).catch(function () { return null; }) : Promise.resolve(null)
|
|
||||||
]).then(function (assets) {
|
|
||||||
var banner = assets[0], logo = assets[1];
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
// 顶部主图(cover 裁切)。
|
|
||||||
var rect = coverRect(width, imgHeight, banner.width, banner.height);
|
|
||||||
ctx.drawImage(banner, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
|
|
||||||
// 左上角日期(白字,覆盖在主图上)。
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,1)';
|
|
||||||
ctx.font = '35px Arial';
|
|
||||||
ctx.fillText(weekLabel, 30, 37);
|
|
||||||
ctx.font = '28px Arial';
|
|
||||||
ctx.fillText((new Date().getMonth() + 1) + ' / ' + new Date().getDate(), 30, 87);
|
|
||||||
// 标题竖蓝线。
|
|
||||||
ctx.strokeStyle = '#00a1ff';
|
|
||||||
ctx.lineWidth = 7;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.moveTo(40, imgHeight + 38);
|
|
||||||
ctx.lineTo(40, imgHeight + 74);
|
|
||||||
ctx.stroke();
|
|
||||||
// 标题层(粗体)。参考 rebate 分支把 $title 子画布置于 (20, img_height+40),
|
|
||||||
// 子画布内左对齐 x=40、文字基线偏移 2,故绝对坐标 (40, img_height+42);
|
|
||||||
// drawTextOverflow 内部会 +38,所以这里传 2。
|
|
||||||
drawTextOverflow(ctx, title, width, 2, 'bold 35px Arial', '#424242', 52, 2, imgHeight + 42);
|
|
||||||
// 推荐人标签层(红,最多 1 行):$tags 置于 (0, img_height+160)。
|
|
||||||
if (tags) {
|
|
||||||
drawTextOverflow(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight + 162);
|
|
||||||
}
|
|
||||||
// 简介层(灰):$content 置于 (0, img_height+220)。
|
|
||||||
if (description) {
|
|
||||||
drawTextOverflow(ctx, description, width, 4, '25px Arial', 'rgba(88,88,88,1)', 39, 2, imgHeight + 222);
|
|
||||||
}
|
|
||||||
// 底部带边框盒。
|
|
||||||
ctx.strokeStyle = 'rgba(122,122,122,0.2)';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.rect(30, height - 230, width - 60, 200);
|
|
||||||
ctx.stroke();
|
|
||||||
// logo 图:contain 居中,缺图时整块不画(参考主题 logo.naturalWidth 守卫)。
|
|
||||||
if (logo && logo.width && logo.height) {
|
|
||||||
var logoRect = containRect(80, height - 190, 300, 100, logo.width, logo.height);
|
|
||||||
ctx.drawImage(logo, logoRect.x, logoRect.y, logoRect.width, logoRect.height);
|
|
||||||
}
|
|
||||||
// 二维码(右下角)。
|
|
||||||
var qr = window.qrcode(0, 'M');
|
|
||||||
qr.addData(url);
|
|
||||||
qr.make();
|
|
||||||
var count = qr.getModuleCount();
|
|
||||||
var qrBox = 120;
|
|
||||||
var cell = qrBox / count;
|
|
||||||
var qrX = width - 200, qrY = height - 200;
|
|
||||||
ctx.fillStyle = '#0f172a';
|
|
||||||
for (var r = 0; r < count; r++) {
|
|
||||||
for (var c = 0; c < count; c++) {
|
|
||||||
if (qr.isDark(r, c)) {
|
|
||||||
ctx.fillRect(qrX + Math.floor(c * cell), qrY + Math.floor(r * cell), Math.ceil(cell), Math.ceil(cell));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 底部文案(居中,盒下方)。
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillStyle = '#b4b4b4';
|
|
||||||
ctx.font = '22px Arial';
|
|
||||||
ctx.fillText(String(bottom).slice(0, 46), width / 2, height - 58);
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
return canvas;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// cover 裁切:把源图按短边铺满目标框并居中裁切。
|
|
||||||
function coverRect(boxW, boxH, srcW, srcH) {
|
|
||||||
var sx = 0, sy = 0, sWidth = srcW, sHeight = srcH;
|
|
||||||
if (srcW / srcH > boxW / boxH) {
|
|
||||||
sWidth = (boxW * srcH) / boxH;
|
|
||||||
sx = (srcW - sWidth) / 2;
|
|
||||||
} else {
|
|
||||||
sHeight = (boxH * srcW) / boxW;
|
|
||||||
sy = (srcH - sHeight) / 2;
|
|
||||||
}
|
|
||||||
return { sx: sx, sy: sy, sWidth: sWidth, sHeight: sHeight };
|
|
||||||
}
|
|
||||||
|
|
||||||
window.AKSRTAffiliateRevenueInit = initAffiliateRevenue;
|
|
||||||
|
|
||||||
// ===== VIP 开通 =====
|
// ===== VIP 开通 =====
|
||||||
function initVipShop() {
|
function initVipShop() {
|
||||||
@@ -488,7 +167,7 @@
|
|||||||
busy = false;
|
busy = false;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.classList.remove('is-loading');
|
btn.classList.remove('is-loading');
|
||||||
if (data.payment_url) window.open(data.payment_url, '_blank');
|
if (window.AksrtPayCashier) window.AksrtPayCashier.handle(data, {});
|
||||||
setTip(data.message || '请完成支付,支付成功后将自动刷新。');
|
setTip(data.message || '请完成支付,支付成功后将自动刷新。');
|
||||||
},
|
},
|
||||||
onSuccess: function (result) {
|
onSuccess: function (result) {
|
||||||
@@ -573,6 +252,7 @@
|
|||||||
tags: tags || ['credits', 'orders'],
|
tags: tags || ['credits', 'orders'],
|
||||||
onTerminal: function (result) {
|
onTerminal: function (result) {
|
||||||
activePoll = null;
|
activePoll = null;
|
||||||
|
if (window.AksrtPayCashier) window.AksrtPayCashier.close();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
notyf('支付成功,正在刷新…', 'success', 5, 'aksrt-balance-charge-payment-toast');
|
notyf('支付成功,正在刷新…', 'success', 5, 'aksrt-balance-charge-payment-toast');
|
||||||
refreshAssetBalances();
|
refreshAssetBalances();
|
||||||
@@ -767,9 +447,17 @@
|
|||||||
}
|
}
|
||||||
var data = (r.json && r.json.data) ? r.json.data : r.json;
|
var data = (r.json && r.json.data) ? r.json.data : r.json;
|
||||||
resetOperationKey(form);
|
resetOperationKey(form);
|
||||||
if (data && data.redirect && (data.payment_url || data.qr_content)) {
|
if (data && data.mode) {
|
||||||
if (data.payment_url) window.open(data.payment_url, '_blank');
|
|
||||||
if (window.AksrtModalRuntime) window.AksrtModalRuntime.invalidate(['orders']);
|
if (window.AksrtModalRuntime) window.AksrtModalRuntime.invalidate(['orders']);
|
||||||
|
if (window.AksrtPayCashier) {
|
||||||
|
window.AksrtPayCashier.handle(data, {
|
||||||
|
onClose: function () {
|
||||||
|
if (activePoll && typeof activePoll.abort === 'function') activePoll.abort();
|
||||||
|
activePoll = null;
|
||||||
|
notyf('已停止查询支付状态', 'warning', 3, 'aksrt-balance-charge-payment-toast');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
notyf(data.message || '请完成支付,支付成功后将自动刷新。', 'info', 5, 'aksrt-balance-charge-toast');
|
notyf(data.message || '请完成支付,支付成功后将自动刷新。', 'info', 5, 'aksrt-balance-charge-toast');
|
||||||
if (data.order_number) activePoll = pollPaidOrder(data.order_number, ['credits', 'orders']);
|
if (data.order_number) activePoll = pollPaidOrder(data.order_number, ['credits', 'orders']);
|
||||||
return;
|
return;
|
||||||
@@ -1244,158 +932,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function initWithdrawHistory() {
|
|
||||||
var modal = document.querySelector('[data-aksrt-withdraw-history-modal]');
|
|
||||||
if (!modal || modal.dataset.aksrtWithdrawHistoryInitialized === '1') return;
|
|
||||||
modal.dataset.aksrtWithdrawHistoryInitialized = '1';
|
|
||||||
var list = modal.querySelector('[data-aksrt-withdraw-history-list]');
|
|
||||||
var pagination = modal.querySelector('[data-aksrt-withdraw-history-pagination]');
|
|
||||||
var statusLabels = {
|
|
||||||
pending: '待处理',
|
|
||||||
processing: '处理中',
|
|
||||||
completed: '已完成',
|
|
||||||
rejected: '已拒绝'
|
|
||||||
};
|
|
||||||
var currentPage = 1;
|
|
||||||
var loading = false;
|
|
||||||
|
|
||||||
function text(value) {
|
|
||||||
return value === null || value === undefined ? '' : String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendText(parent, value, className) {
|
|
||||||
var node = document.createElement('span');
|
|
||||||
if (className) node.className = className;
|
|
||||||
node.textContent = text(value);
|
|
||||||
parent.appendChild(node);
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatGrossAmount(value) {
|
|
||||||
return text(value).replace(/\.00$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatNetAmount(value) {
|
|
||||||
return text(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderItems(items) {
|
|
||||||
if (!list) return;
|
|
||||||
list.textContent = '';
|
|
||||||
if (!Array.isArray(items) || !items.length) {
|
|
||||||
var empty = document.createElement('div');
|
|
||||||
empty.className = 'uc-transactions__empty';
|
|
||||||
var emptyText = document.createElement('p');
|
|
||||||
emptyText.textContent = '暂无提现记录';
|
|
||||||
empty.appendChild(emptyText);
|
|
||||||
list.appendChild(empty);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
items.forEach(function (item) {
|
|
||||||
var status = text(item && item.status).toLowerCase();
|
|
||||||
var row = document.createElement('div');
|
|
||||||
row.className = 'uc-withdraw-history__item';
|
|
||||||
var badge = appendText(row, statusLabels[status] || status, 'badg badg-sm uc-withdraw-history__status');
|
|
||||||
badge.classList.add(status === 'rejected' ? 'c-red' : (status === 'completed' ? 'c-green' : 'c-blue'));
|
|
||||||
var gross = text(item && (item.gross_amount || item.amount));
|
|
||||||
var net = text(item && item.amount);
|
|
||||||
var amount = document.createElement('span');
|
|
||||||
amount.className = 'uc-withdraw-history__amount em12';
|
|
||||||
amount.appendChild(document.createTextNode(formatGrossAmount(gross)));
|
|
||||||
if (net !== '') {
|
|
||||||
var netText = document.createElement('span');
|
|
||||||
netText.className = 'uc-withdraw-history__net';
|
|
||||||
netText.textContent = '(实际到账:' + formatNetAmount(net) + '元)';
|
|
||||||
amount.appendChild(netText);
|
|
||||||
}
|
|
||||||
row.appendChild(amount);
|
|
||||||
var number = document.createElement('span');
|
|
||||||
number.className = 'uc-withdraw-history__number muted-2-color em09';
|
|
||||||
var numberLabel = document.createElement('span');
|
|
||||||
numberLabel.textContent = '订单号:';
|
|
||||||
number.appendChild(numberLabel);
|
|
||||||
var code = document.createElement('code');
|
|
||||||
code.textContent = text(item && item.withdrawal_number);
|
|
||||||
number.appendChild(code);
|
|
||||||
row.appendChild(number);
|
|
||||||
appendText(row, text(item && item.created_at), 'uc-withdraw-history__meta muted-2-color em09');
|
|
||||||
list.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPagination(page, maxPages) {
|
|
||||||
if (!pagination) return;
|
|
||||||
pagination.textContent = '';
|
|
||||||
if (maxPages <= 1) return;
|
|
||||||
function addButton(label, target, disabled, current) {
|
|
||||||
var button = document.createElement('button');
|
|
||||||
button.type = 'button';
|
|
||||||
button.className = 'uc-withdraw-history__page' + (current ? ' is-current' : '');
|
|
||||||
button.textContent = label;
|
|
||||||
button.disabled = !!disabled;
|
|
||||||
if (current) button.setAttribute('aria-current', 'page');
|
|
||||||
button.dataset.aksrtWithdrawHistoryPage = String(target);
|
|
||||||
pagination.appendChild(button);
|
|
||||||
}
|
|
||||||
addButton('上一页', page - 1, page <= 1, false);
|
|
||||||
for (var index = 1; index <= maxPages; index += 1) addButton(String(index), index, false, index === page);
|
|
||||||
addButton('下一页', page + 1, page >= maxPages, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPage(page) {
|
|
||||||
if (loading || !list) return;
|
|
||||||
loading = true;
|
|
||||||
list.textContent = '加载中…';
|
|
||||||
var rest = (window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || '';
|
|
||||||
if (!rest) {
|
|
||||||
list.textContent = '记录加载失败,请重试';
|
|
||||||
loading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var url = rest.replace(/\/$/, '') + '/earnings/withdrawals?page=' + encodeURIComponent(Math.max(1, page)) + '&per_page=10';
|
|
||||||
fetch(url, {
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: { 'X-WP-Nonce': (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || '' }
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json().then(function (payload) {
|
|
||||||
if (!response.ok || !payload || !Array.isArray(payload.items)) throw new Error('withdraw-history-load-failed');
|
|
||||||
currentPage = Math.max(1, parseInt(payload.page, 10) || 1);
|
|
||||||
var maxPages = Math.max(1, parseInt(payload.max_pages, 10) || 1);
|
|
||||||
renderItems(payload.items);
|
|
||||||
renderPagination(currentPage, maxPages);
|
|
||||||
});
|
|
||||||
}).catch(function () {
|
|
||||||
list.textContent = '记录加载失败,请重试';
|
|
||||||
if (pagination) pagination.textContent = '';
|
|
||||||
}).finally(function () {
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
modal.hidden = false;
|
|
||||||
document.body.classList.add('uc-modal-open');
|
|
||||||
loadPage(1);
|
|
||||||
}
|
|
||||||
function close() {
|
|
||||||
modal.hidden = true;
|
|
||||||
document.body.classList.remove('uc-modal-open');
|
|
||||||
}
|
|
||||||
document.querySelectorAll('[data-aksrt-withdraw-history-open]').forEach(function (btn) {
|
|
||||||
btn.addEventListener('click', function (e) { e.preventDefault(); open(); });
|
|
||||||
});
|
|
||||||
modal.querySelectorAll('[data-aksrt-withdraw-history-close]').forEach(function (btn) {
|
|
||||||
btn.addEventListener('click', function (e) { e.preventDefault(); close(); });
|
|
||||||
});
|
|
||||||
if (pagination) {
|
|
||||||
pagination.addEventListener('click', function (e) {
|
|
||||||
var button = e.target.closest('[data-aksrt-withdraw-history-page]');
|
|
||||||
if (!button || button.disabled) return;
|
|
||||||
e.preventDefault();
|
|
||||||
loadPage(parseInt(button.dataset.aksrtWithdrawHistoryPage || '1', 10) || 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.AKSRTWithdrawInit = initWithdraw;
|
window.AKSRTWithdrawInit = initWithdraw;
|
||||||
window.AKSRTAssetBalances = {
|
window.AKSRTAssetBalances = {
|
||||||
@@ -1406,13 +942,11 @@
|
|||||||
function bootAssets() {
|
function bootAssets() {
|
||||||
refreshAssetBalances();
|
refreshAssetBalances();
|
||||||
initSigninGoto();
|
initSigninGoto();
|
||||||
initAffiliateRevenue();
|
|
||||||
initVipShop();
|
initVipShop();
|
||||||
initBalanceCharge();
|
initBalanceCharge();
|
||||||
initTransfer();
|
initTransfer();
|
||||||
initPointsPay();
|
initPointsPay();
|
||||||
initWithdraw();
|
initWithdraw();
|
||||||
initWithdrawHistory();
|
|
||||||
initAssetTabs();
|
initAssetTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -386,6 +386,23 @@
|
|||||||
return loadView(state);
|
return loadView(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pollOrder(state, number) {
|
||||||
|
var coordinator = window.AksrtVipPurchaseCoordinator;
|
||||||
|
if (!coordinator || typeof coordinator.poll !== "function") return null;
|
||||||
|
return coordinator.poll(number, {
|
||||||
|
tags: ["orders"],
|
||||||
|
onTerminal: function (result) {
|
||||||
|
if (window.AksrtPayCashier) window.AksrtPayCashier.close();
|
||||||
|
notyf(result.success ? "支付成功" : "支付未完成,请重试", result.success ? "success" : "danger");
|
||||||
|
invalidateOrder(number, null);
|
||||||
|
if (activeDetail(state, number)) openDetail(state, number);
|
||||||
|
},
|
||||||
|
onTimeout: function () {
|
||||||
|
notyf("支付状态暂未确认,请稍后刷新订单", "danger");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function startPayment(state, button) {
|
function startPayment(state, button) {
|
||||||
var number = button.getAttribute("data-aksrt-order-pay") || "";
|
var number = button.getAttribute("data-aksrt-order-pay") || "";
|
||||||
var method = button.getAttribute("data-aksrt-order-method") || "";
|
var method = button.getAttribute("data-aksrt-order-method") || "";
|
||||||
@@ -397,8 +414,15 @@
|
|||||||
delete state.operationKeys[actionKey];
|
delete state.operationKeys[actionKey];
|
||||||
var data = result.data || {};
|
var data = result.data || {};
|
||||||
invalidateOrder(number, data.order);
|
invalidateOrder(number, data.order);
|
||||||
if (data.redirect && data.payment_url) {
|
if (data.mode) {
|
||||||
window.location.assign(data.payment_url);
|
button.disabled = false;
|
||||||
|
if (!window.AksrtPayCashier) return;
|
||||||
|
var handled = window.AksrtPayCashier.handle(data, {
|
||||||
|
onClose: function () { if (poll) poll.abort(); }
|
||||||
|
});
|
||||||
|
var poll = handled === "qrcode" || handled === "jsapi"
|
||||||
|
? pollOrder(state, number)
|
||||||
|
: null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
notyf(data.message || "支付成功", "success");
|
notyf(data.message || "支付成功", "success");
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* Unified cashier: renders the gateway result returned by PaymentOrchestrator::pay().
|
||||||
|
*
|
||||||
|
* mode=qrcode on-site QR modal (+ optional mobile deep link)
|
||||||
|
* mode=redirect same-tab navigation to the gateway page
|
||||||
|
* mode=form auto-submit the server-built POST form
|
||||||
|
* mode=jsapi WeChat in-app WeixinJSBridge payment
|
||||||
|
*
|
||||||
|
* Polling stays with the caller (AksrtVipPurchaseCoordinator.poll); this module
|
||||||
|
* only presents the payment step and reports when the QR modal is dismissed.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
(function (window, document) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var KIND = "pay-cashier";
|
||||||
|
var sequence = 0;
|
||||||
|
var listeners = Object.create(null);
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
|
||||||
|
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMobile() {
|
||||||
|
return /Android|iPhone|iPad|iPod|Mobile/i.test(window.navigator.userAgent || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function qrDataUrl(text) {
|
||||||
|
if (typeof window.qrcode !== "function" || !text) return "";
|
||||||
|
try {
|
||||||
|
var qr = window.qrcode(0, "M");
|
||||||
|
qr.addData(String(text));
|
||||||
|
qr.make();
|
||||||
|
return qr.createDataURL(6, 12);
|
||||||
|
} catch (error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function amountText(data) {
|
||||||
|
if (data.amount === undefined || data.amount === null || data.amount === "") return "";
|
||||||
|
var mark = (window._win && window._win.pay_mark) || "¥";
|
||||||
|
return mark + String(data.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inner QR markup; reusable by callers that own their own modal. */
|
||||||
|
function qrHtml(data) {
|
||||||
|
var src = qrDataUrl(data.code_url);
|
||||||
|
var html = '<div class="aksrt-pay-cashier">';
|
||||||
|
var amount = amountText(data);
|
||||||
|
if (amount) html += '<div class="aksrt-pay-cashier__amount">' + escapeHtml(amount) + "</div>";
|
||||||
|
html += src
|
||||||
|
? '<img class="aksrt-pay-cashier__qr" src="' + src + '" width="200" height="200" alt="支付二维码">'
|
||||||
|
: '<p class="aksrt-pay-cashier__error">二维码组件未加载,请刷新页面重试。</p>';
|
||||||
|
html += '<p class="aksrt-pay-cashier__tip">' + escapeHtml(data.message || "请使用手机扫码完成支付") + "</p>";
|
||||||
|
if (data.payment_url && isMobile()) {
|
||||||
|
html += '<a class="but jb-blue aksrt-pay-cashier__open" href="' + escapeHtml(data.payment_url) + '">打开支付应用</a>';
|
||||||
|
}
|
||||||
|
if (data.order_number) {
|
||||||
|
html += '<p class="aksrt-pay-cashier__order muted-2-color em09">订单号:' + escapeHtml(data.order_number) + "</p>";
|
||||||
|
}
|
||||||
|
return html + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openQr(data, options) {
|
||||||
|
var runtime = window.AksrtModalRuntime;
|
||||||
|
if (!runtime) return false;
|
||||||
|
var id = String(++sequence);
|
||||||
|
if (typeof options.onClose === "function") listeners[id] = options.onClose;
|
||||||
|
runtime.open({
|
||||||
|
kind: KIND,
|
||||||
|
cashierId: id,
|
||||||
|
title: options.title || "扫码支付",
|
||||||
|
className: "aksrt-pay-cashier-modal",
|
||||||
|
telemetry: false,
|
||||||
|
html: qrHtml(data),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitForm(html) {
|
||||||
|
var host = document.createElement("div");
|
||||||
|
host.hidden = true;
|
||||||
|
host.innerHTML = String(html || "");
|
||||||
|
var form = host.querySelector("form");
|
||||||
|
if (!form) return false;
|
||||||
|
document.body.appendChild(host);
|
||||||
|
form.submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeJsapi(params, options) {
|
||||||
|
var run = function () {
|
||||||
|
window.WeixinJSBridge.invoke("getBrandWCPayRequest", params, function (res) {
|
||||||
|
var msg = res && res.err_msg ? String(res.err_msg) : "";
|
||||||
|
if (msg === "get_brand_wcpay_request:ok") return;
|
||||||
|
if (typeof options.onClose === "function") {
|
||||||
|
options.onClose(msg === "get_brand_wcpay_request:cancel" ? "cancel" : "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (window.WeixinJSBridge && typeof window.WeixinJSBridge.invoke === "function") {
|
||||||
|
run();
|
||||||
|
} else {
|
||||||
|
document.addEventListener("WeixinJSBridgeReady", run, { once: true });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Present a pending payment. Returns the handled mode, or "" when the
|
||||||
|
* payload carries no cashier step (synchronous gateways).
|
||||||
|
*
|
||||||
|
* options.onClose(reason) fires when the QR modal is dismissed or the
|
||||||
|
* JSAPI sheet is cancelled; callers stop polling there.
|
||||||
|
*/
|
||||||
|
function handle(data, options) {
|
||||||
|
data = data || {};
|
||||||
|
options = options || {};
|
||||||
|
switch (data.mode) {
|
||||||
|
case "qrcode":
|
||||||
|
return data.code_url && openQr(data, options) ? "qrcode" : "";
|
||||||
|
case "redirect":
|
||||||
|
if (!data.payment_url) return "";
|
||||||
|
window.location.assign(data.payment_url);
|
||||||
|
return "redirect";
|
||||||
|
case "form":
|
||||||
|
return submitForm(data.form_html) ? "form" : "";
|
||||||
|
case "jsapi":
|
||||||
|
return data.jsapi_params ? invokeJsapi(data.jsapi_params, options) && "jsapi" : "";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Close the cashier QR modal after a terminal poll result. */
|
||||||
|
function close() {
|
||||||
|
var runtime = window.AksrtModalRuntime;
|
||||||
|
var shell = runtime ? runtime.getActive() : null;
|
||||||
|
if (shell && shell.classList.contains("aksrt-pay-cashier-modal")) runtime.close("paid");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("aksrt:modal:close", function (event) {
|
||||||
|
var detail = event && event.detail ? event.detail : {};
|
||||||
|
var opts = detail.session && detail.session.options ? detail.session.options : {};
|
||||||
|
if (opts.kind !== KIND || !listeners[opts.cashierId]) return;
|
||||||
|
var fn = listeners[opts.cashierId];
|
||||||
|
delete listeners[opts.cashierId];
|
||||||
|
if (detail.reason !== "paid") fn(detail.reason || "close");
|
||||||
|
});
|
||||||
|
|
||||||
|
window.AksrtPayCashier = {
|
||||||
|
KIND: KIND,
|
||||||
|
handle: handle,
|
||||||
|
close: close,
|
||||||
|
qrHtml: qrHtml,
|
||||||
|
isPending: function (data) { return !!(data && data.mode); },
|
||||||
|
};
|
||||||
|
})(window, document);
|
||||||
+13
-5
@@ -577,7 +577,9 @@
|
|||||||
tags: ["vip", "orders"],
|
tags: ["vip", "orders"],
|
||||||
onPending: function (data) {
|
onPending: function (data) {
|
||||||
waitingPayment = true;
|
waitingPayment = true;
|
||||||
if (data.payment_url) window.open(data.payment_url, "_blank");
|
if (window.AksrtPayCashier) {
|
||||||
|
window.AksrtPayCashier.handle(data, { onClose: cancelActivePurchase });
|
||||||
|
}
|
||||||
notyf(data.message || "请完成支付,支付成功后将自动刷新。", "info", 5, activePurchaseToastId);
|
notyf(data.message || "请完成支付,支付成功后将自动刷新。", "info", 5, activePurchaseToastId);
|
||||||
activePurchaseToastId = "aksrt-vip-payment-toast";
|
activePurchaseToastId = "aksrt-vip-payment-toast";
|
||||||
notyf("等待支付结果…", "load", "", activePurchaseToastId);
|
notyf("等待支付结果…", "load", "", activePurchaseToastId);
|
||||||
@@ -831,16 +833,22 @@
|
|||||||
vipPay(trigger);
|
vipPay(trigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("aksrt:modal:close", function (event) {
|
function cancelActivePurchase() {
|
||||||
var detail = event && event.detail ? event.detail : {};
|
if (!activePurchase) return;
|
||||||
var options = detail.session && detail.session.options ? detail.session.options : {};
|
|
||||||
if (options.kind !== "vip" || !activePurchase) return;
|
|
||||||
if (typeof activePurchase.abort === "function") activePurchase.abort();
|
if (typeof activePurchase.abort === "function") activePurchase.abort();
|
||||||
activePurchase = null;
|
activePurchase = null;
|
||||||
if (activePurchaseToastId) {
|
if (activePurchaseToastId) {
|
||||||
notyf("已取消会员购买", "warning", 3, activePurchaseToastId);
|
notyf("已取消会员购买", "warning", 3, activePurchaseToastId);
|
||||||
activePurchaseToastId = "";
|
activePurchaseToastId = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VIP 弹窗被收银二维码弹窗替换(reason=replace)时保持轮询;用户主动关闭才取消。
|
||||||
|
document.addEventListener("aksrt:modal:close", function (event) {
|
||||||
|
var detail = event && event.detail ? event.detail : {};
|
||||||
|
var options = detail.session && detail.session.options ? detail.session.options : {};
|
||||||
|
if (options.kind !== "vip" || detail.reason === "replace") return;
|
||||||
|
cancelActivePurchase();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 积分兑换入口(data-aksrt-vip-points-open)文档级委托:一处绑定同时覆盖
|
// 积分兑换入口(data-aksrt-vip-points-open)文档级委托:一处绑定同时覆盖
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
(function (window, document) {
|
(function (window, document) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var ORDER_PATH = "/pay/content/order/";
|
var ORDER_PATH = "/pay/order/";
|
||||||
var DEFAULT_MAX_ATTEMPTS = 12;
|
var DEFAULT_MAX_ATTEMPTS = 12;
|
||||||
var DEFAULT_MAX_ELAPSED = 120000;
|
var DEFAULT_MAX_ELAPSED = 120000;
|
||||||
var MAX_DELAY = 15000;
|
var MAX_DELAY = 15000;
|
||||||
@@ -374,7 +374,7 @@
|
|||||||
callback(options, "onError", response);
|
callback(options, "onError", response);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
if (data.redirect && (data.payment_url || data.qr_content)) {
|
if (data.mode) {
|
||||||
invalidate(["orders"]);
|
invalidate(["orders"]);
|
||||||
callback(options, "onPending", data);
|
callback(options, "onPending", data);
|
||||||
if (!data.order_number) {
|
if (!data.order_number) {
|
||||||
|
|||||||
@@ -486,25 +486,24 @@ class SchemaPayment {
|
|||||||
], 'help' => '收银台支付方式显示顺序;仅显示已开启的接口'],
|
], 'help' => '收银台支付方式显示顺序;仅显示已开启的接口'],
|
||||||
]],
|
]],
|
||||||
['key' => 'alipay', 'title' => '支付宝官方', 'collapsible' => true, 'defaultCollapsed' => true, 'dependsOn' => ['key' => 'pay_alipay_sdk_options', 'value' => 'official_alipay'], 'fields' => [
|
['key' => 'alipay', 'title' => '支付宝官方', 'collapsible' => true, 'defaultCollapsed' => true, 'dependsOn' => ['key' => 'pay_alipay_sdk_options', 'value' => 'official_alipay'], 'fields' => [
|
||||||
['key' => 'alipay_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return}</li><li>同时填写了企业支付以及当面付参数,则优先使用当面付</li></ul>'],
|
['key' => 'alipay_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return}</li><li>电脑端按「电脑端收款」选择站内二维码或跳转;手机端开启 H5 时跳转,否则显示二维码</li></ul>'],
|
||||||
['key' => 'alipay_appid', 'label' => '当面付:APPID', 'type' => 'input', 'default' => ''],
|
['key' => 'alipay_appid', 'label' => '当面付:APPID', 'type' => 'input', 'default' => ''],
|
||||||
['key' => 'alipay_privatekey', 'label' => '当面付:应用私钥', 'type' => 'textarea', 'sanitize_type' => 'code', 'default' => '', 'rows' => 4, 'secret' => true],
|
['key' => 'alipay_privatekey', 'label' => '当面付:应用私钥', 'type' => 'textarea', 'sanitize_type' => 'code', 'default' => '', 'rows' => 4, 'secret' => true],
|
||||||
['key' => 'alipay_payment_type', 'label' => '支付方式', 'type' => 'select', 'default' => 'page', 'options' => [
|
['key' => 'alipay_pc_mode', 'label' => '电脑端收款', 'type' => 'select', 'default' => 'facepay', 'options' => [
|
||||||
['label' => '电脑网站支付(web)', 'value' => 'page'],
|
['label' => '当面付(站内二维码)', 'value' => 'facepay'],
|
||||||
['label' => '手机网站支付(wap)', 'value' => 'wap'],
|
['label' => '电脑网站支付(跳转支付宝)', 'value' => 'web'],
|
||||||
]],
|
], 'help' => '当面付在站内弹出二维码;电脑网站支付在当前页跳转到支付宝收银台'],
|
||||||
|
['key' => 'alipay_h5', 'label' => '移动端 H5 跳转', 'type' => 'switch', 'default' => false, 'help' => '需签约手机网站支付。开启后手机浏览器跳转支付宝 App/收银台付款;关闭则手机端也显示二维码(附唤起链接)'],
|
||||||
['key' => 'alipay_app_public_cert_path', 'label' => '应用公钥证书路径', 'type' => 'input', 'default' => '', 'help' => 'appCertPublicKey.crt 绝对路径。'],
|
['key' => 'alipay_app_public_cert_path', 'label' => '应用公钥证书路径', 'type' => 'input', 'default' => '', 'help' => 'appCertPublicKey.crt 绝对路径。'],
|
||||||
['key' => 'alipay_public_cert_path', 'label' => '支付宝公钥证书路径', 'type' => 'input', 'default' => '', 'help' => 'alipayCertPublicKey_RSA2.crt 绝对路径。'],
|
['key' => 'alipay_public_cert_path', 'label' => '支付宝公钥证书路径', 'type' => 'input', 'default' => '', 'help' => 'alipayCertPublicKey_RSA2.crt 绝对路径。'],
|
||||||
['key' => 'alipay_root_cert_path', 'label' => '支付宝根证书路径', 'type' => 'input', 'default' => '', 'help' => 'alipayRootCert.crt 绝对路径。'],
|
['key' => 'alipay_root_cert_path', 'label' => '支付宝根证书路径', 'type' => 'input', 'default' => '', 'help' => 'alipayRootCert.crt 绝对路径。'],
|
||||||
]],
|
]],
|
||||||
['key' => 'wechat', 'title' => '微信官方', 'collapsible' => true, 'defaultCollapsed' => true, 'dependsOn' => ['key' => 'pay_wechat_sdk_options', 'value' => 'official_wechat'], 'fields' => [
|
['key' => 'wechat', 'title' => '微信官方', 'collapsible' => true, 'defaultCollapsed' => true, 'dependsOn' => ['key' => 'pay_wechat_sdk_options', 'value' => 'official_wechat'], 'fields' => [
|
||||||
['key' => 'wechat_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li></ul>'],
|
['key' => 'wechat_notice', 'label' => '', 'type' => 'content', 'ui_only' => true, 'content' => '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li><li>电脑端显示站内二维码;手机端按下方开关选择 H5 跳转或微信内 JSAPI,未开启时同样显示二维码</li></ul>'],
|
||||||
['key' => 'wechat_merchantid', 'label' => '商户号 PartnerID', 'type' => 'input', 'default' => ''],
|
['key' => 'wechat_merchantid', 'label' => '商户号 PartnerID', 'type' => 'input', 'default' => ''],
|
||||||
['key' => 'wechat_appid', 'label' => '授权绑定的AppID', 'type' => 'input', 'default' => ''],
|
['key' => 'wechat_appid', 'label' => '授权绑定的AppID', 'type' => 'input', 'default' => ''],
|
||||||
['key' => 'wechat_payment_type', 'label' => '支付方式', 'type' => 'select', 'default' => 'native', 'options' => [
|
['key' => 'wechat_h5', 'label' => '移动端 H5 跳转', 'type' => 'switch', 'default' => false, 'help' => '需开通 H5 支付。开启后手机浏览器(非微信内)跳转微信付款;关闭则显示站内二维码'],
|
||||||
['label' => '扫码支付(Native)', 'value' => 'native'],
|
['key' => 'wechat_jsapi', 'label' => '微信内 JSAPI 支付', 'type' => 'switch', 'default' => false, 'help' => '需开通 JSAPI 支付并在商户平台配置支付授权目录。微信内打开时直接调起支付;用户 openid 通过「用户→微信公众号」的 AppID/AppSecret 静默授权获取,公众号 AppID 必须与上方 AppID 一致'],
|
||||||
['label' => 'H5支付', 'value' => 'h5'],
|
|
||||||
]],
|
|
||||||
['key' => 'wechat_api_v3_key', 'label' => '支付APIv3密钥', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => '', 'help' => '微信商户平台设置的 APIv3 密钥(32位)。'],
|
['key' => 'wechat_api_v3_key', 'label' => '支付APIv3密钥', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => '', 'help' => '微信商户平台设置的 APIv3 密钥(32位)。'],
|
||||||
['key' => 'wechat_private_key_path', 'label' => '商户私钥路径', 'type' => 'input', 'default' => '', 'help' => 'apiclient_key.pem 绝对路径,用于签名。'],
|
['key' => 'wechat_private_key_path', 'label' => '商户私钥路径', 'type' => 'input', 'default' => '', 'help' => 'apiclient_key.pem 绝对路径,用于签名。'],
|
||||||
['key' => 'wechat_cert_path', 'label' => '商户API证书路径', 'type' => 'input', 'default' => '', 'help' => 'apiclient_cert.pem 绝对路径。'],
|
['key' => 'wechat_cert_path', 'label' => '商户API证书路径', 'type' => 'input', 'default' => '', 'help' => 'apiclient_cert.pem 绝对路径。'],
|
||||||
@@ -534,11 +533,7 @@ class SchemaPayment {
|
|||||||
['key' => 'epay_apiurl', 'label' => 'API接口网址', 'type' => 'input', 'default' => '', 'help' => '请填写接口地址,例如:https://pay.xxxxxxx.cn/'],
|
['key' => 'epay_apiurl', 'label' => 'API接口网址', 'type' => 'input', 'default' => '', 'help' => '请填写接口地址,例如:https://pay.xxxxxxx.cn/'],
|
||||||
['key' => 'epay_pid', 'label' => '商户号', 'type' => 'input', 'default' => ''],
|
['key' => 'epay_pid', 'label' => '商户号', 'type' => 'input', 'default' => ''],
|
||||||
['key' => 'epay_key', 'label' => '商户秘钥', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => '', 'help' => 'MD5 通信密钥。'],
|
['key' => 'epay_key', 'label' => '商户秘钥', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => '', 'help' => 'MD5 通信密钥。'],
|
||||||
['key' => 'epay_device', 'label' => '下单终端', 'type' => 'select', 'default' => 'auto', 'options' => [
|
['key' => 'epay_qrcode', 'label' => '电脑端站内二维码', 'type' => 'switch', 'default' => true, 'help' => '开启后电脑端通过 mapi 接口获取二维码在站内展示(接口未返回二维码时跳转支付页);关闭则跳转易支付收银台。移动端始终跳转'],
|
||||||
['label' => '自动(按 UA)', 'value' => 'auto'],
|
|
||||||
['label' => '电脑端 submit', 'value' => 'pc'],
|
|
||||||
['label' => '移动端 mapi', 'value' => 'mobile'],
|
|
||||||
]],
|
|
||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -544,21 +544,18 @@ trait PayOrders {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($payment_result['redirect'])) {
|
$cashier = \AKSRT\Payment\PaymentOrchestrator::cashier_payload($payment_result);
|
||||||
|
if ($cashier !== []) {
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => array_merge($cashier, [
|
||||||
'redirect' => true,
|
|
||||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
|
||||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
'amount' => $amount_decimal,
|
'amount' => $amount_decimal,
|
||||||
'pay_modo' => '0',
|
'pay_modo' => '0',
|
||||||
'message' => $payment_result['message'],
|
'message' => $payment_result['message'],
|
||||||
'reload' => false,
|
'reload' => false,
|
||||||
'poll' => true,
|
|
||||||
'replayed' => false,
|
'replayed' => false,
|
||||||
],
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ trait PayRest {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
register_rest_route('aksrt/v1', '/pay/content/order/(?P<order_number>[A-Za-z0-9_-]+)', [
|
register_rest_route('aksrt/v1', '/pay/order/(?P<order_number>[A-Za-z0-9_-]+)', [
|
||||||
'methods' => 'GET',
|
'methods' => 'GET',
|
||||||
'callback' => [$this, 'rest_order_status'],
|
'callback' => [$this, 'rest_order_status'],
|
||||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||||
@@ -164,7 +164,7 @@ trait PayRest {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /pay/content/order/{order_number}
|
* GET /pay/order/{order_number} (all order types; guests limited to content orders)
|
||||||
*/
|
*/
|
||||||
public function rest_order_status(\WP_REST_Request $request) {
|
public function rest_order_status(\WP_REST_Request $request) {
|
||||||
$uid = get_current_user_id();
|
$uid = get_current_user_id();
|
||||||
|
|||||||
@@ -488,20 +488,17 @@ trait VipServicePurchase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($payment_result['redirect'])) {
|
$cashier = \AKSRT\Payment\PaymentOrchestrator::cashier_payload($payment_result);
|
||||||
|
if ($cashier !== []) {
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => array_merge($cashier, [
|
||||||
'redirect' => true,
|
|
||||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
|
||||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
'amount' => $amount_decimal,
|
'amount' => $amount_decimal,
|
||||||
'message' => $payment_result['message'],
|
'message' => $payment_result['message'],
|
||||||
'reload' => false,
|
'reload' => false,
|
||||||
'poll' => true,
|
|
||||||
'replayed' => false,
|
'replayed' => false,
|
||||||
],
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
-18
@@ -93,6 +93,24 @@ trait ScriptsPart1b {
|
|||||||
&& class_exists('\AKSRT\Credits\Signin')
|
&& class_exists('\AKSRT\Credits\Signin')
|
||||||
&& class_exists('\AKSRT\Settings\Settings')
|
&& class_exists('\AKSRT\Settings\Settings')
|
||||||
&& !empty(\AKSRT\Settings\Settings::get_signin_reward_settings()['enabled']);
|
&& !empty(\AKSRT\Settings\Settings::get_signin_reward_settings()['enabled']);
|
||||||
|
// 统一收银台(站内二维码 / 当前页跳转 / 表单提交 / JSAPI):按需作为依赖入队。
|
||||||
|
$qrcode_js_path = get_template_directory() . '/assets/js/vendor/qrcode.min.js';
|
||||||
|
wp_register_script(
|
||||||
|
'aksrt-qrcode',
|
||||||
|
$base_uri . '/assets/js/vendor/qrcode.min.js',
|
||||||
|
[],
|
||||||
|
file_exists($qrcode_js_path) ? (string) filemtime($qrcode_js_path) : $version,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
$pay_cashier_js = get_template_directory() . '/assets/js/pay-cashier.js';
|
||||||
|
wp_register_script(
|
||||||
|
'aksrt-pay-cashier',
|
||||||
|
$base_uri . '/assets/js/pay-cashier.js',
|
||||||
|
['loader_js', 'aksrt-qrcode'],
|
||||||
|
file_exists($pay_cashier_js) ? (string) filemtime($pay_cashier_js) : $version,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
$vip_coordinator = get_template_directory() . '/assets/js/vip-purchase-coordinator.js';
|
$vip_coordinator = get_template_directory() . '/assets/js/vip-purchase-coordinator.js';
|
||||||
if ((($vip_on && is_user_logged_in()) || $credits_on || $affiliate_on) && file_exists($vip_coordinator)) {
|
if ((($vip_on && is_user_logged_in()) || $credits_on || $affiliate_on) && file_exists($vip_coordinator)) {
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
@@ -120,7 +138,7 @@ trait ScriptsPart1b {
|
|||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'aksrt-pay-vip',
|
'aksrt-pay-vip',
|
||||||
$base_uri . '/assets/js/pay-vip.js',
|
$base_uri . '/assets/js/pay-vip.js',
|
||||||
['loader_js', 'aksrt-vip-coordinator'],
|
['loader_js', 'aksrt-vip-coordinator', 'aksrt-pay-cashier'],
|
||||||
(string) filemtime($vip_js),
|
(string) filemtime($vip_js),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -407,7 +425,7 @@ trait ScriptsPart1b {
|
|||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'aksrt-order-center',
|
'aksrt-order-center',
|
||||||
$base_uri . '/assets/js/order-center.js',
|
$base_uri . '/assets/js/order-center.js',
|
||||||
['loader_js'],
|
['loader_js', 'aksrt-pay-cashier'],
|
||||||
(string) filemtime($order_center_js),
|
(string) filemtime($order_center_js),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -667,7 +685,7 @@ trait ScriptsPart1b {
|
|||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'aksrt-cms-download',
|
'aksrt-cms-download',
|
||||||
$base_uri . '/assets/js/cms-download.js',
|
$base_uri . '/assets/js/cms-download.js',
|
||||||
['loader_js'],
|
['loader_js', 'aksrt-pay-cashier'],
|
||||||
file_exists($cms_download_js_path) ? (string) filemtime($cms_download_js_path) : $version,
|
file_exists($cms_download_js_path) ? (string) filemtime($cms_download_js_path) : $version,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -692,28 +710,25 @@ trait ScriptsPart1b {
|
|||||||
|
|
||||||
// profile 页:加载 credits.js(签到 + 邀请 + 提现交互)
|
// profile 页:加载 credits.js(签到 + 邀请 + 提现交互)
|
||||||
if ($is_profile_page) {
|
if ($is_profile_page) {
|
||||||
$affiliate_deps = [];
|
// aksrt-pay-cashier 依赖 aksrt-qrcode(推广海报二维码同用)。
|
||||||
if ($affiliate_on) {
|
|
||||||
// 推广二维码生成库(本地打包,无外部 CDN)。
|
|
||||||
$qrcode_js_path = get_template_directory() . '/assets/js/vendor/qrcode.min.js';
|
|
||||||
wp_enqueue_script(
|
|
||||||
'aksrt-qrcode',
|
|
||||||
$base_uri . '/assets/js/vendor/qrcode.min.js',
|
|
||||||
[],
|
|
||||||
file_exists($qrcode_js_path) ? (string) filemtime($qrcode_js_path) : $version,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
$affiliate_deps[] = 'aksrt-qrcode';
|
|
||||||
}
|
|
||||||
$credits_js_path = get_template_directory() . '/assets/js/credits.js';
|
$credits_js_path = get_template_directory() . '/assets/js/credits.js';
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'aksrt-credits',
|
'aksrt-credits',
|
||||||
$base_uri . '/assets/js/credits.js',
|
$base_uri . '/assets/js/credits.js',
|
||||||
array_merge(['loader_js', 'aksrt-vip-coordinator'], $affiliate_deps),
|
['loader_js', 'aksrt-vip-coordinator', 'aksrt-pay-cashier'],
|
||||||
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
|
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
foreach (['credits-withdraw-history' => ['loader_js']] + ($affiliate_on ? ['credits-affiliate' => ['loader_js', 'aksrt-qrcode']] : []) as $credits_part => $credits_part_deps) {
|
||||||
|
$credits_part_path = get_template_directory() . '/assets/js/' . $credits_part . '.js';
|
||||||
|
wp_enqueue_script(
|
||||||
|
'aksrt-' . $credits_part,
|
||||||
|
$base_uri . '/assets/js/' . $credits_part . '.js',
|
||||||
|
$credits_part_deps,
|
||||||
|
file_exists($credits_part_path) ? (string) filemtime($credits_part_path) : $version,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -389,7 +389,6 @@ trait BalanceServiceCharge {
|
|||||||
}
|
}
|
||||||
if (is_array($existing) && (string) ($order['status'] ?? '') === \AKSRT\Payment\Order::STATUS_PAID) {
|
if (is_array($existing) && (string) ($order['status'] ?? '') === \AKSRT\Payment\Order::STATUS_PAID) {
|
||||||
return [
|
return [
|
||||||
'redirect' => false,
|
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
'amount' => $pay_decimal,
|
'amount' => $pay_decimal,
|
||||||
'credit_amount' => $credit_decimal,
|
'credit_amount' => $credit_decimal,
|
||||||
@@ -425,26 +424,21 @@ trait BalanceServiceCharge {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($payment_result['redirect'])) {
|
$cashier = \AKSRT\Payment\PaymentOrchestrator::cashier_payload($payment_result);
|
||||||
return [
|
if ($cashier !== []) {
|
||||||
'redirect' => true,
|
return array_merge($cashier, [
|
||||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
|
||||||
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
|
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
'amount' => $pay_decimal,
|
'amount' => $pay_decimal,
|
||||||
'credit_amount' => $credit_decimal,
|
'credit_amount' => $credit_decimal,
|
||||||
'message' => $payment_result['message'],
|
'message' => $payment_result['message'],
|
||||||
'reload' => false,
|
'reload' => false,
|
||||||
'poll' => true,
|
|
||||||
'transaction_id' => 0,
|
|
||||||
'replayed' => false,
|
'replayed' => false,
|
||||||
'wallet' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
'wallet' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The synchronous gateway has already committed the recharge and order state.
|
// The synchronous gateway has already committed the recharge and order state.
|
||||||
return [
|
return [
|
||||||
'redirect' => false,
|
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
'amount' => $pay_decimal,
|
'amount' => $pay_decimal,
|
||||||
'credit_amount' => $credit_decimal,
|
'credit_amount' => $credit_decimal,
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ class Gateway {
|
|||||||
register_rest_route('aksrt/v1', '/payment/(?P<gateway>[a-z0-9_]+)/cancel', array_merge($args, [
|
register_rest_route('aksrt/v1', '/payment/(?P<gateway>[a-z0-9_]+)/cancel', array_merge($args, [
|
||||||
'callback' => [$this, 'handle_cancel'],
|
'callback' => [$this, 'handle_cancel'],
|
||||||
]));
|
]));
|
||||||
|
// WeChat JSAPI snsapi_base callback: one-time state transient guards it.
|
||||||
|
register_rest_route('aksrt/v1', '/payment/wechatpay/openid', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
'callback' => ['\AKSRT\Payment\Gateways\WechatOpenid', 'handle_callback'],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ interface GatewayInterface {
|
|||||||
* 创建支付单
|
* 创建支付单
|
||||||
*
|
*
|
||||||
* @param array $order 订单数据
|
* @param array $order 订单数据
|
||||||
* @return array{success: bool, payment_url?: string, payment_data?: array, message?: string}
|
* 外部网关返回 mode(qrcode|redirect|form|jsapi)及对应的
|
||||||
|
* code_url / payment_url / form_html / jsapi_params;同步网关(余额/积分)不返回 mode。
|
||||||
|
*
|
||||||
|
* @return array{success: bool, mode?: string, code_url?: string, payment_url?: string, form_html?: string, jsapi_params?: array<string,string>, message?: string}
|
||||||
*/
|
*/
|
||||||
public function create_payment(array $order): array;
|
public function create_payment(array $order): array;
|
||||||
|
|
||||||
|
|||||||
@@ -46,41 +46,52 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
? \AKSRT\Payment\OrderRepository::provider_out_trade_no($order)
|
? \AKSRT\Payment\OrderRepository::provider_out_trade_no($order)
|
||||||
: $order_number;
|
: $order_number;
|
||||||
}
|
}
|
||||||
$amount = $this->amount_string($order['amount'] ?? null);
|
$amount = $this->amount_string($order['amount'] ?? null);
|
||||||
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
||||||
$payment_type = $this->get_config()['payment_type'];
|
$config = $this->get_config();
|
||||||
|
|
||||||
if ($order_number === '' || $out_trade_no === '' || $amount === '') {
|
if ($order_number === '' || $out_trade_no === '' || $amount === '') {
|
||||||
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'out_trade_no' => $out_trade_no,
|
||||||
|
'total_amount' => $amount,
|
||||||
|
'subject' => mb_substr($subject, 0, 256),
|
||||||
|
];
|
||||||
|
$mobile = wp_is_mobile();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
YansongdaPayFactory::boot_alipay();
|
YansongdaPayFactory::boot_alipay();
|
||||||
|
|
||||||
$payload = [
|
// Off-site cashier: mobile H5 when signed, desktop web when chosen.
|
||||||
'_method' => 'GET',
|
if (($mobile && $config['h5']) || (!$mobile && $config['pc_mode'] === 'web')) {
|
||||||
'out_trade_no' => $out_trade_no,
|
$payload['_method'] = 'GET';
|
||||||
'total_amount' => $amount,
|
$result = $mobile ? Pay::alipay()->h5($payload) : Pay::alipay()->web($payload);
|
||||||
'subject' => mb_substr($subject, 0, 256),
|
$payment_url = YansongdaPayFactory::extract_payment_url($result);
|
||||||
];
|
if ($payment_url === '') {
|
||||||
|
return ['success' => false, 'message' => __('支付宝支付链接生成失败', 'aksrt')];
|
||||||
$result = $payment_type === 'wap'
|
}
|
||||||
? Pay::alipay()->h5($payload)
|
return [
|
||||||
: Pay::alipay()->web($payload);
|
'success' => true,
|
||||||
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||||
$payment_url = YansongdaPayFactory::extract_payment_url($result);
|
'payment_url' => $payment_url,
|
||||||
if ($payment_url === '') {
|
'message' => __('正在跳转支付宝…', 'aksrt'),
|
||||||
return ['success' => false, 'message' => __('支付宝支付链接生成失败', 'aksrt')];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Face-to-face precreate: on-site QR code (deep link on mobile).
|
||||||
|
$data = YansongdaPayFactory::to_array(Pay::alipay()->scan($payload));
|
||||||
|
$qr_code = (string) ($data['qr_code'] ?? '');
|
||||||
|
if ($qr_code === '') {
|
||||||
|
return ['success' => false, 'message' => __('支付宝二维码生成失败', 'aksrt')];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'payment_url' => $payment_url,
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_QRCODE,
|
||||||
'payment_data' => [
|
'code_url' => $qr_code,
|
||||||
'order_number' => $order_number,
|
'payment_url' => $mobile ? $qr_code : '',
|
||||||
'out_trade_no' => $out_trade_no,
|
'message' => __('请使用支付宝扫码支付', 'aksrt'),
|
||||||
],
|
|
||||||
'message' => __('支付宝支付链接已生成', 'aksrt'),
|
|
||||||
];
|
];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['success' => false, 'message' => $e->getMessage()];
|
return ['success' => false, 'message' => $e->getMessage()];
|
||||||
@@ -277,15 +288,15 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{app_id:string,private_key:string,app_public_cert:string,alipay_public_cert:string,alipay_root_cert:string,payment_type:string}
|
* @return array{app_id:string,private_key:string,app_public_cert:string,alipay_public_cert:string,alipay_root_cert:string,pc_mode:string,h5:bool}
|
||||||
*/
|
*/
|
||||||
private function get_config(): array {
|
private function get_config(): array {
|
||||||
$all = class_exists('\AKSRT\Settings\Settings')
|
$all = class_exists('\AKSRT\Settings\Settings')
|
||||||
? \AKSRT\Settings\Settings::get_all()
|
? \AKSRT\Settings\Settings::get_all()
|
||||||
: [];
|
: [];
|
||||||
$type = sanitize_key((string) ($all['alipay_payment_type'] ?? 'page'));
|
$pc_mode = sanitize_key((string) ($all['alipay_pc_mode'] ?? 'facepay'));
|
||||||
if (!in_array($type, ['page', 'wap'], true)) {
|
if (!in_array($pc_mode, ['facepay', 'web'], true)) {
|
||||||
$type = 'page';
|
$pc_mode = 'facepay';
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
'app_id' => (string) ($all['alipay_appid'] ?? ''),
|
'app_id' => (string) ($all['alipay_appid'] ?? ''),
|
||||||
@@ -293,7 +304,8 @@ class AlipayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
'app_public_cert' => (string) ($all['alipay_app_public_cert_path'] ?? ''),
|
'app_public_cert' => (string) ($all['alipay_app_public_cert_path'] ?? ''),
|
||||||
'alipay_public_cert' => (string) ($all['alipay_public_cert_path'] ?? ''),
|
'alipay_public_cert' => (string) ($all['alipay_public_cert_path'] ?? ''),
|
||||||
'alipay_root_cert' => (string) ($all['alipay_root_cert_path'] ?? ''),
|
'alipay_root_cert' => (string) ($all['alipay_root_cert_path'] ?? ''),
|
||||||
'payment_type' => $type,
|
'pc_mode' => $pc_mode,
|
||||||
|
'h5' => !empty($all['alipay_h5']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,56 +65,74 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
'return_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/return'),
|
'return_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/return'),
|
||||||
'name' => mb_substr(wp_strip_all_tags($subject), 0, 127),
|
'name' => mb_substr(wp_strip_all_tags($subject), 0, 127),
|
||||||
'money' => $amount,
|
'money' => $amount,
|
||||||
|
'sitename' => mb_substr(wp_strip_all_tags((string) get_bloginfo('name')), 0, 64),
|
||||||
];
|
];
|
||||||
$params['sign'] = $this->sign($params, $config['key']);
|
|
||||||
$params['sign_type'] = 'MD5';
|
|
||||||
|
|
||||||
$use_mobile = $this->prefer_mobile($config['device']);
|
// Mobile, or desktop with the on-site QR switched off: hand off to the
|
||||||
if ($use_mobile) {
|
// Epay cashier via a self-submitting form (current tab).
|
||||||
// mapi.php returns JSON with payurl / qrcode / urlscheme.
|
if (wp_is_mobile() || !$config['qrcode']) {
|
||||||
$api = $this->endpoint($config['apiurl'], 'mapi.php');
|
return $this->submit_form_result($config, $params);
|
||||||
$response = wp_remote_post($api, [
|
|
||||||
'timeout' => 30,
|
|
||||||
'body' => $params,
|
|
||||||
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8'],
|
|
||||||
]);
|
|
||||||
if (is_wp_error($response)) {
|
|
||||||
return ['success' => false, 'message' => $response->get_error_message()];
|
|
||||||
}
|
|
||||||
$body = json_decode((string) wp_remote_retrieve_body($response), true);
|
|
||||||
if (!is_array($body)) {
|
|
||||||
// Fall back to browser submit if mapi is unavailable.
|
|
||||||
return $this->browser_submit_result($config['apiurl'], $params, $order_number, $out_trade_no);
|
|
||||||
}
|
|
||||||
$code = (int) ($body['code'] ?? 0);
|
|
||||||
if ($code !== 1) {
|
|
||||||
$msg = (string) ($body['msg'] ?? $body['message'] ?? __('易支付下单失败', 'aksrt'));
|
|
||||||
return ['success' => false, 'message' => $msg];
|
|
||||||
}
|
|
||||||
$pay_url = (string) ($body['payurl'] ?? $body['urlscheme'] ?? '');
|
|
||||||
$qrcode = (string) ($body['qrcode'] ?? '');
|
|
||||||
if ($pay_url === '' && $qrcode === '') {
|
|
||||||
return ['success' => false, 'message' => __('易支付未返回支付链接', 'aksrt')];
|
|
||||||
}
|
|
||||||
$result = [
|
|
||||||
'success' => true,
|
|
||||||
'payment_data' => [
|
|
||||||
'order_number' => $order_number,
|
|
||||||
'out_trade_no' => $out_trade_no,
|
|
||||||
'code_url' => $qrcode,
|
|
||||||
'channel' => $this->channel,
|
|
||||||
],
|
|
||||||
'message' => __('易支付订单已创建', 'aksrt'),
|
|
||||||
];
|
|
||||||
if ($pay_url !== '') {
|
|
||||||
$result['payment_url'] = $pay_url;
|
|
||||||
} elseif ($qrcode !== '') {
|
|
||||||
$result['payment_url'] = $qrcode;
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->browser_submit_result($config['apiurl'], $params, $order_number, $out_trade_no);
|
// Desktop on-site QR: mapi.php returns qrcode / payurl as JSON.
|
||||||
|
$params['clientip'] = function_exists('aksrt_get_client_ip') ? aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||||
|
$params['device'] = 'pc';
|
||||||
|
$params['sign'] = $this->sign($params, $config['key']);
|
||||||
|
$params['sign_type'] = 'MD5';
|
||||||
|
$response = wp_remote_post($this->endpoint($config['apiurl'], 'mapi.php'), [
|
||||||
|
'timeout' => 30,
|
||||||
|
'body' => $params,
|
||||||
|
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8'],
|
||||||
|
]);
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return ['success' => false, 'message' => $response->get_error_message()];
|
||||||
|
}
|
||||||
|
$body = json_decode((string) wp_remote_retrieve_body($response), true);
|
||||||
|
if (!is_array($body) || (int) ($body['code'] ?? 0) !== 1) {
|
||||||
|
$msg = is_array($body) ? (string) ($body['msg'] ?? $body['message'] ?? '') : '';
|
||||||
|
return ['success' => false, 'message' => $msg !== '' ? $msg : __('易支付下单失败', 'aksrt')];
|
||||||
|
}
|
||||||
|
$qrcode = (string) ($body['qrcode'] ?? $body['code_url'] ?? '');
|
||||||
|
$pay_url = (string) ($body['payurl'] ?? '');
|
||||||
|
if ($qrcode !== '') {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_QRCODE,
|
||||||
|
'code_url' => $qrcode,
|
||||||
|
'message' => $this->channel === 'wxpay' ? __('请使用微信扫码支付', 'aksrt') : __('请使用支付宝扫码支付', 'aksrt'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($pay_url !== '') {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||||
|
'payment_url' => $pay_url,
|
||||||
|
'message' => __('正在跳转支付页面…', 'aksrt'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return ['success' => false, 'message' => __('易支付未返回支付链接', 'aksrt')];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Self-submitting POST form to submit.php (signed params as hidden inputs).
|
||||||
|
*
|
||||||
|
* @param array{apiurl:string,key:string} $config
|
||||||
|
* @param array<string,string> $params
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private function submit_form_result(array $config, array $params): array {
|
||||||
|
$params['sign'] = $this->sign($params, $config['key']);
|
||||||
|
$params['sign_type'] = 'MD5';
|
||||||
|
$inputs = '';
|
||||||
|
foreach ($params as $name => $value) {
|
||||||
|
$inputs .= '<input type="hidden" name="' . esc_attr((string) $name) . '" value="' . esc_attr((string) $value) . '">';
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_FORM,
|
||||||
|
'form_html' => '<form method="post" action="' . esc_url($this->endpoint($config['apiurl'], 'submit.php')) . '" accept-charset="utf-8">' . $inputs . '</form>',
|
||||||
|
'message' => __('正在跳转支付页面…', 'aksrt'),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verify_callback(\WP_REST_Request $request): array {
|
public function verify_callback(\WP_REST_Request $request): array {
|
||||||
@@ -231,16 +249,12 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{apiurl:string,pid:string,key:string,device:string}
|
* @return array{apiurl:string,pid:string,key:string,qrcode:bool}
|
||||||
*/
|
*/
|
||||||
private function get_config(): array {
|
private function get_config(): array {
|
||||||
$all = class_exists('\AKSRT\Settings\Settings')
|
$all = class_exists('\AKSRT\Settings\Settings')
|
||||||
? \AKSRT\Settings\Settings::get_all()
|
? \AKSRT\Settings\Settings::get_all()
|
||||||
: [];
|
: [];
|
||||||
$device = sanitize_key((string) ($all['epay_device'] ?? 'auto'));
|
|
||||||
if (!in_array($device, ['auto', 'pc', 'mobile'], true)) {
|
|
||||||
$device = 'auto';
|
|
||||||
}
|
|
||||||
$apiurl = (string) ($all['epay_apiurl'] ?? '');
|
$apiurl = (string) ($all['epay_apiurl'] ?? '');
|
||||||
$apiurl = rtrim(trim($apiurl), '/') . '/';
|
$apiurl = rtrim(trim($apiurl), '/') . '/';
|
||||||
if ($apiurl === '/') {
|
if ($apiurl === '/') {
|
||||||
@@ -250,7 +264,7 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
'apiurl' => $apiurl,
|
'apiurl' => $apiurl,
|
||||||
'pid' => (string) ($all['epay_pid'] ?? ''),
|
'pid' => (string) ($all['epay_pid'] ?? ''),
|
||||||
'key' => (string) ($all['epay_key'] ?? ''),
|
'key' => (string) ($all['epay_key'] ?? ''),
|
||||||
'device' => $device,
|
'qrcode' => !array_key_exists('epay_qrcode', $all) || !empty($all['epay_qrcode']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,43 +314,6 @@ class EpayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function prefer_mobile(string $device): bool {
|
|
||||||
if ($device === 'mobile') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if ($device === 'pc') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// auto
|
|
||||||
if (!empty($_SERVER['HTTP_USER_AGENT'])) {
|
|
||||||
$ua = (string) $_SERVER['HTTP_USER_AGENT'];
|
|
||||||
return (bool) preg_match('/Mobile|Android|iPhone|iPad|iPod|MicroMessenger/i', $ua);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string,string> $params
|
|
||||||
* @return array{success:bool,payment_url?:string,payment_data?:array,message?:string}
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @param array<string,string> $params
|
|
||||||
* @return array{success:bool,payment_url?:string,payment_data?:array,message?:string}
|
|
||||||
*/
|
|
||||||
private function browser_submit_result(string $apiurl, array $params, string $order_number, string $out_trade_no = ''): array {
|
|
||||||
$url = add_query_arg($params, $this->endpoint($apiurl, 'submit.php'));
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'payment_url' => $url,
|
|
||||||
'payment_data' => [
|
|
||||||
'order_number' => $order_number,
|
|
||||||
'out_trade_no' => $out_trade_no !== '' ? $out_trade_no : $order_number,
|
|
||||||
'channel' => $this->channel,
|
|
||||||
],
|
|
||||||
'message' => __('易支付支付链接已生成', 'aksrt'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map local order_number → provider out_trade_no (supports resume attempts).
|
* Map local order_number → provider out_trade_no (supports resume attempts).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -90,14 +90,10 @@ class PayPalGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'payment_url' => $approveUrl,
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||||
'payment_data' => [
|
'payment_url' => $approveUrl,
|
||||||
'order_number' => $order_number,
|
'message' => __('正在跳转 PayPal…', 'aksrt'),
|
||||||
'out_trade_no' => $out_trade_no,
|
|
||||||
'paypal_id' => $data['id'] ?? '',
|
|
||||||
],
|
|
||||||
'message' => __('PayPal 订单已创建', 'aksrt'),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,14 +110,10 @@ class StripeGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'payment_url' => $session->url,
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||||
'payment_data' => [
|
'payment_url' => (string) $session->url,
|
||||||
'order_number' => $order_number,
|
'message' => __('正在跳转 Stripe…', 'aksrt'),
|
||||||
'out_trade_no' => $out_trade_no,
|
|
||||||
'session_id' => $session->id,
|
|
||||||
],
|
|
||||||
'message' => __('Stripe 支付页面已创建', 'aksrt'),
|
|
||||||
];
|
];
|
||||||
} catch (\Stripe\Exception\ApiErrorException $e) {
|
} catch (\Stripe\Exception\ApiErrorException $e) {
|
||||||
return ['success' => false, 'message' => $e->getMessage()];
|
return ['success' => false, 'message' => $e->getMessage()];
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Payer openid for WeChat JSAPI payments (official-account silent OAuth).
|
||||||
|
*
|
||||||
|
* Sources, in order:
|
||||||
|
* 1. the logged-in user's bound official-account openid (same AppID only)
|
||||||
|
* 2. a signed short-lived cookie set by the snsapi_base callback below
|
||||||
|
* When neither exists the gateway sends the browser through authorize_url();
|
||||||
|
* the callback exchanges the code, stores the cookie and returns to the page
|
||||||
|
* so the user can tap pay again.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Payment\Gateways;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class WechatOpenid {
|
||||||
|
|
||||||
|
private const COOKIE = 'aksrt_wxpay_openid';
|
||||||
|
private const STATE_KEY = 'aksrt_wxpay_oauth_';
|
||||||
|
private const STATE_TTL = 10 * MINUTE_IN_SECONDS;
|
||||||
|
private const COOKIE_TTL = 30 * DAY_IN_SECONDS;
|
||||||
|
|
||||||
|
public static function in_wechat(): bool {
|
||||||
|
$ua = (string) ($_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||||
|
return $ua !== '' && stripos($ua, 'MicroMessenger') !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resolve(string $pay_appid): string {
|
||||||
|
if ($pay_appid === '' || !self::gzh_matches($pay_appid)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
if ($user_id > 0) {
|
||||||
|
$bound = sanitize_text_field((string) get_user_meta($user_id, 'aksrt_weixingzh_id', true));
|
||||||
|
if ($bound !== '') {
|
||||||
|
return $bound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self::read_cookie($pay_appid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* snsapi_base authorize URL that comes back to the current page. Empty when
|
||||||
|
* the official account is not configured with the pay AppID.
|
||||||
|
*/
|
||||||
|
public static function authorize_url(string $pay_appid): string {
|
||||||
|
if ($pay_appid === '' || !self::gzh_matches($pay_appid) || self::gzh()['appsecret'] === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$back = wp_validate_redirect((string) wp_get_referer(), home_url('/'));
|
||||||
|
$state = wp_generate_password(24, false, false);
|
||||||
|
set_transient(self::STATE_KEY . $state, ['appid' => $pay_appid, 'back' => $back], self::STATE_TTL);
|
||||||
|
|
||||||
|
return 'https://open.weixin.qq.com/connect/oauth2/authorize?' . http_build_query([
|
||||||
|
'appid' => $pay_appid,
|
||||||
|
'redirect_uri' => rest_url('aksrt/v1/payment/wechatpay/openid'),
|
||||||
|
'response_type' => 'code',
|
||||||
|
'scope' => 'snsapi_base',
|
||||||
|
'state' => $state,
|
||||||
|
], '', '&', PHP_QUERY_RFC3986) . '#wechat_redirect';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /payment/wechatpay/openid?code&state — one-time state, then redirect back.
|
||||||
|
*/
|
||||||
|
public static function handle_callback(\WP_REST_Request $request) {
|
||||||
|
$state = preg_replace('/[^A-Za-z0-9]/', '', (string) $request->get_param('state'));
|
||||||
|
$code = sanitize_text_field((string) $request->get_param('code'));
|
||||||
|
$saved = $state !== '' ? get_transient(self::STATE_KEY . $state) : false;
|
||||||
|
if ($state !== '') {
|
||||||
|
delete_transient(self::STATE_KEY . $state);
|
||||||
|
}
|
||||||
|
if (!is_array($saved) || $code === '') {
|
||||||
|
return new \WP_REST_Response(['message' => __('微信授权已失效,请返回重新支付', 'aksrt')], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$appid = (string) ($saved['appid'] ?? '');
|
||||||
|
$back = wp_validate_redirect((string) ($saved['back'] ?? ''), home_url('/'));
|
||||||
|
$gzh = self::gzh();
|
||||||
|
if ($appid === '' || !self::gzh_matches($appid) || $gzh['appsecret'] === '') {
|
||||||
|
return new \WP_REST_Response(['message' => __('微信支付授权配置无效', 'aksrt')], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get(add_query_arg([
|
||||||
|
'appid' => rawurlencode($appid),
|
||||||
|
'secret' => rawurlencode($gzh['appsecret']),
|
||||||
|
'code' => rawurlencode($code),
|
||||||
|
'grant_type' => 'authorization_code',
|
||||||
|
], 'https://api.weixin.qq.com/sns/oauth2/access_token'), ['timeout' => 10]);
|
||||||
|
$body = is_wp_error($response) ? null : json_decode((string) wp_remote_retrieve_body($response), true);
|
||||||
|
$openid = is_array($body) ? sanitize_text_field((string) ($body['openid'] ?? '')) : '';
|
||||||
|
if ($openid !== '') {
|
||||||
|
self::write_cookie($appid, $openid);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_safe_redirect($back);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{appid:string,appsecret:string} */
|
||||||
|
private static function gzh(): array {
|
||||||
|
$gzh = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_weixingzh_settings() : [];
|
||||||
|
return [
|
||||||
|
'appid' => trim((string) ($gzh['appid'] ?? '')),
|
||||||
|
'appsecret' => trim((string) ($gzh['appsecret'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function gzh_matches(string $pay_appid): bool {
|
||||||
|
$appid = self::gzh()['appid'];
|
||||||
|
return $appid !== '' && hash_equals($appid, $pay_appid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function signature(string $appid, string $openid): string {
|
||||||
|
return hash_hmac('sha256', $appid . '|' . $openid, wp_salt('auth'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function read_cookie(string $appid): string {
|
||||||
|
$raw = isset($_COOKIE[self::COOKIE]) ? sanitize_text_field(wp_unslash((string) $_COOKIE[self::COOKIE])) : '';
|
||||||
|
$parts = explode('.', $raw, 2);
|
||||||
|
if (count($parts) !== 2 || $parts[0] === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return hash_equals(self::signature($appid, $parts[0]), $parts[1]) ? $parts[0] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function write_cookie(string $appid, string $openid): void {
|
||||||
|
if (headers_sent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setcookie(self::COOKIE, $openid . '.' . self::signature($appid, $openid), [
|
||||||
|
'expires' => time() + self::COOKIE_TTL,
|
||||||
|
'path' => COOKIEPATH ?: '/',
|
||||||
|
'domain' => COOKIE_DOMAIN ?: '',
|
||||||
|
'secure' => is_ssl(),
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
: $order_number;
|
: $order_number;
|
||||||
}
|
}
|
||||||
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
|
||||||
$payment_type = $this->get_config()['payment_type'];
|
$config = $this->get_config();
|
||||||
try {
|
try {
|
||||||
$total_fen = \AKSRT\Ledger\AssetAmount::balance(is_string($order['amount'] ?? null) ? trim($order['amount']) : '')->minor();
|
$total_fen = \AKSRT\Ledger\AssetAmount::balance(is_string($order['amount'] ?? null) ? trim($order['amount']) : '')->minor();
|
||||||
} catch (\InvalidArgumentException $e) {
|
} catch (\InvalidArgumentException $e) {
|
||||||
@@ -71,39 +71,73 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
switch ($payment_type) {
|
$in_wechat = WechatOpenid::in_wechat();
|
||||||
case 'h5':
|
|
||||||
$payload['scene_info'] = [
|
// Inside WeChat: JSAPI with the payer's official-account openid.
|
||||||
'payer_client_ip' => (string) ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'),
|
if ($in_wechat && $config['jsapi']) {
|
||||||
'h5_info' => ['type' => 'Wap'],
|
$openid = WechatOpenid::resolve($config['appid']);
|
||||||
|
if ($openid !== '') {
|
||||||
|
$payload['payer'] = ['openid' => $openid];
|
||||||
|
$invoke = YansongdaPayFactory::to_array(Pay::wechat()->mp($payload));
|
||||||
|
$params = [];
|
||||||
|
foreach (['appId', 'timeStamp', 'nonceStr', 'package', 'signType', 'paySign'] as $key) {
|
||||||
|
$params[$key] = (string) ($invoke[$key] ?? '');
|
||||||
|
}
|
||||||
|
if (in_array('', $params, true)) {
|
||||||
|
return ['success' => false, 'message' => __('微信支付调起参数生成失败', 'aksrt')];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_JSAPI,
|
||||||
|
'jsapi_params' => $params,
|
||||||
|
'message' => __('正在调起微信支付…', 'aksrt'),
|
||||||
];
|
];
|
||||||
$result = Pay::wechat()->h5($payload);
|
}
|
||||||
break;
|
$authorize = WechatOpenid::authorize_url($config['appid']);
|
||||||
case 'native':
|
if ($authorize !== '') {
|
||||||
default:
|
return [
|
||||||
$result = Pay::wechat()->scan($payload);
|
'success' => true,
|
||||||
break;
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||||
|
'payment_url' => $authorize,
|
||||||
|
'message' => __('正在获取微信授权,返回后请重新支付', 'aksrt'),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = YansongdaPayFactory::to_array($result);
|
// Mobile browser outside WeChat: H5 hand-off in the current tab.
|
||||||
$payment_data = array_merge($data, [
|
if (!$in_wechat && $config['h5'] && wp_is_mobile()) {
|
||||||
'order_number' => $order_number,
|
$payload['scene_info'] = [
|
||||||
'out_trade_no' => $out_trade_no,
|
'payer_client_ip' => function_exists('aksrt_get_client_ip')
|
||||||
]);
|
? aksrt_get_client_ip()
|
||||||
|
: (string) ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'),
|
||||||
|
'h5_info' => ['type' => 'Wap'],
|
||||||
|
];
|
||||||
|
$data = YansongdaPayFactory::to_array(Pay::wechat()->h5($payload));
|
||||||
|
$h5_url = (string) ($data['h5_url'] ?? '');
|
||||||
|
if ($h5_url === '') {
|
||||||
|
return ['success' => false, 'message' => __('微信 H5 支付链接生成失败', 'aksrt')];
|
||||||
|
}
|
||||||
|
$back = add_query_arg('out_trade_no', $out_trade_no, rest_url('aksrt/v1/payment/wechatpay/return'));
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_REDIRECT,
|
||||||
|
'payment_url' => $h5_url . (str_contains($h5_url, '?') ? '&' : '?') . 'redirect_url=' . rawurlencode($back),
|
||||||
|
'message' => __('正在跳转微信支付…', 'aksrt'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$result_data = [
|
// Default: Native on-site QR code.
|
||||||
'success' => true,
|
$data = YansongdaPayFactory::to_array(Pay::wechat()->scan($payload));
|
||||||
'payment_data' => $payment_data,
|
$code_url = (string) ($data['code_url'] ?? '');
|
||||||
'message' => __('微信支付订单已创建', 'aksrt'),
|
if ($code_url === '') {
|
||||||
|
return ['success' => false, 'message' => __('微信支付二维码生成失败', 'aksrt')];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'mode' => \AKSRT\Payment\PaymentOrchestrator::MODE_QRCODE,
|
||||||
|
'code_url' => $code_url,
|
||||||
|
'message' => __('请使用微信扫码支付', 'aksrt'),
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($payment_type === 'native' && !empty($data['code_url'])) {
|
|
||||||
$result_data['payment_url'] = (string) $data['code_url'];
|
|
||||||
} elseif ($payment_type === 'h5' && !empty($data['h5_url'])) {
|
|
||||||
$result_data['payment_url'] = (string) $data['h5_url'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result_data;
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['success' => false, 'message' => $e->getMessage()];
|
return ['success' => false, 'message' => $e->getMessage()];
|
||||||
}
|
}
|
||||||
@@ -224,10 +258,9 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
YansongdaPayFactory::boot_wechat();
|
YansongdaPayFactory::boot_wechat();
|
||||||
$action = $this->query_action();
|
// The default (jsapi) query plugin hits the same out-trade-no endpoint for every pay type.
|
||||||
$result = Pay::wechat()->query([
|
$result = Pay::wechat()->query([
|
||||||
'out_trade_no' => $out_trade_no,
|
'out_trade_no' => $out_trade_no,
|
||||||
'_action' => $action,
|
|
||||||
]);
|
]);
|
||||||
$data = YansongdaPayFactory::to_array($result);
|
$data = YansongdaPayFactory::to_array($result);
|
||||||
|
|
||||||
@@ -267,7 +300,6 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
YansongdaPayFactory::boot_wechat();
|
YansongdaPayFactory::boot_wechat();
|
||||||
Pay::wechat()->close([
|
Pay::wechat()->close([
|
||||||
'out_trade_no' => $out_trade_no,
|
'out_trade_no' => $out_trade_no,
|
||||||
'_action' => $this->query_action(),
|
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// ignore — new attempt still uses a fresh out_trade_no
|
// ignore — new attempt still uses a fresh out_trade_no
|
||||||
@@ -291,11 +323,8 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
YansongdaPayFactory::boot_wechat();
|
YansongdaPayFactory::boot_wechat();
|
||||||
$action = $this->query_action();
|
|
||||||
|
|
||||||
$query = Pay::wechat()->query([
|
$query = Pay::wechat()->query([
|
||||||
'out_trade_no' => $out_trade_no,
|
'out_trade_no' => $out_trade_no,
|
||||||
'_action' => $action,
|
|
||||||
]);
|
]);
|
||||||
$q = YansongdaPayFactory::to_array($query);
|
$q = YansongdaPayFactory::to_array($query);
|
||||||
$total_fen = (int) YansongdaPayFactory::dig($q, 'amount.total', 0);
|
$total_fen = (int) YansongdaPayFactory::dig($q, 'amount.total', 0);
|
||||||
@@ -313,7 +342,6 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
$request_id !== '' ? $request_id : 'REF_' . $order_number
|
$request_id !== '' ? $request_id : 'REF_' . $order_number
|
||||||
);
|
);
|
||||||
$result = Pay::wechat()->refund([
|
$result = Pay::wechat()->refund([
|
||||||
'_action' => 'refund_' . $action,
|
|
||||||
'out_trade_no' => $out_trade_no,
|
'out_trade_no' => $out_trade_no,
|
||||||
'out_refund_no' => $refund_request_id,
|
'out_refund_no' => $refund_request_id,
|
||||||
'reason' => __('用户申请退款', 'aksrt'),
|
'reason' => __('用户申请退款', 'aksrt'),
|
||||||
@@ -351,7 +379,7 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
YansongdaPayFactory::boot_wechat();
|
YansongdaPayFactory::boot_wechat();
|
||||||
$result = Pay::wechat()->query([
|
$result = Pay::wechat()->query([
|
||||||
'out_refund_no' => $request_id,
|
'out_refund_no' => $request_id,
|
||||||
'_action' => 'refund_' . $this->query_action(),
|
'_action' => 'refund',
|
||||||
]);
|
]);
|
||||||
$data = YansongdaPayFactory::to_array($result);
|
$data = YansongdaPayFactory::to_array($result);
|
||||||
$status = strtoupper((string) ($data['status'] ?? ''));
|
$status = strtoupper((string) ($data['status'] ?? ''));
|
||||||
@@ -369,35 +397,23 @@ class WechatPayGateway implements \AKSRT\Payment\GatewayInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{mch_id:string,appid:string,api_v3_key:string,private_key:string,cert_path:string,payment_type:string}
|
* @return array{mch_id:string,appid:string,api_v3_key:string,private_key:string,cert_path:string,h5:bool,jsapi:bool}
|
||||||
*/
|
*/
|
||||||
private function get_config(): array {
|
private function get_config(): array {
|
||||||
$all = class_exists('\AKSRT\Settings\Settings')
|
$all = class_exists('\AKSRT\Settings\Settings')
|
||||||
? \AKSRT\Settings\Settings::get_all()
|
? \AKSRT\Settings\Settings::get_all()
|
||||||
: [];
|
: [];
|
||||||
$type = sanitize_key((string) ($all['wechat_payment_type'] ?? 'native'));
|
|
||||||
if (!in_array($type, ['native', 'h5'], true)) {
|
|
||||||
$type = 'native';
|
|
||||||
}
|
|
||||||
return [
|
return [
|
||||||
'mch_id' => (string) ($all['wechat_merchantid'] ?? ''),
|
'mch_id' => (string) ($all['wechat_merchantid'] ?? ''),
|
||||||
'appid' => (string) ($all['wechat_appid'] ?? ''),
|
'appid' => (string) ($all['wechat_appid'] ?? ''),
|
||||||
'api_v3_key' => (string) ($all['wechat_api_v3_key'] ?? ''),
|
'api_v3_key' => (string) ($all['wechat_api_v3_key'] ?? ''),
|
||||||
'private_key' => (string) ($all['wechat_private_key_path'] ?? ''),
|
'private_key' => (string) ($all['wechat_private_key_path'] ?? ''),
|
||||||
'cert_path' => (string) ($all['wechat_cert_path'] ?? ''),
|
'cert_path' => (string) ($all['wechat_cert_path'] ?? ''),
|
||||||
'payment_type' => $type,
|
'h5' => !empty($all['wechat_h5']),
|
||||||
|
'jsapi' => !empty($all['wechat_jsapi']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function query_action(): string {
|
|
||||||
$map = [
|
|
||||||
'native' => 'native',
|
|
||||||
'h5' => 'h5',
|
|
||||||
];
|
|
||||||
$type = $this->get_config()['payment_type'];
|
|
||||||
return $map[$type] ?? 'native';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fen_decimal(int $amount): string {
|
private function fen_decimal(int $amount): string {
|
||||||
return \AKSRT\Ledger\AssetAmount::from_minor(
|
return \AKSRT\Ledger\AssetAmount::from_minor(
|
||||||
\AKSRT\Ledger\AssetType::BALANCE,
|
\AKSRT\Ledger\AssetType::BALANCE,
|
||||||
|
|||||||
@@ -187,21 +187,6 @@ class YansongdaPayFactory {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* HTML body from alipay Response (form auto-submit) when no Location header.
|
|
||||||
*
|
|
||||||
* @param mixed $result
|
|
||||||
*/
|
|
||||||
public static function extract_html_body($result): string {
|
|
||||||
if ($result instanceof \Psr\Http\Message\ResponseInterface) {
|
|
||||||
if ($result->getHeaderLine('Location') !== '') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return (string) $result->getBody();
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit SDK success()/fail() ResponseInterface (or string) and exit.
|
* Emit SDK success()/fail() ResponseInterface (or string) and exit.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -21,17 +21,29 @@ class PaymentOrchestrator {
|
|||||||
/** Methods settled synchronously in-process (no provider round trip). */
|
/** Methods settled synchronously in-process (no provider round trip). */
|
||||||
private const SYNC_METHODS = ['balance', 'points'];
|
private const SYNC_METHODS = ['balance', 'points'];
|
||||||
|
|
||||||
|
/** Cashier modes a provider gateway may return (see cashier_fields()). */
|
||||||
|
public const MODE_QRCODE = 'qrcode';
|
||||||
|
public const MODE_REDIRECT = 'redirect';
|
||||||
|
public const MODE_FORM = 'form';
|
||||||
|
public const MODE_JSAPI = 'jsapi';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $order Order row plus subject/amount overrides.
|
* @param array<string,mixed> $order Order row plus subject/amount overrides.
|
||||||
* @param array<string,mixed> $opts paid_note: string, order_context: array
|
* @param array<string,mixed> $opts paid_note: string, order_context: array
|
||||||
*
|
*
|
||||||
|
* Provider gateways answer with `mode` (qrcode|redirect|form|jsapi) plus the
|
||||||
|
* matching code_url / payment_url / form_html / jsapi_params; synchronous
|
||||||
|
* gateways settle in-process and carry no `mode`.
|
||||||
|
*
|
||||||
* @return array{
|
* @return array{
|
||||||
* success:bool,
|
* success:bool,
|
||||||
* code?:string,
|
* code?:string,
|
||||||
* message:string,
|
* message:string,
|
||||||
* redirect?:bool,
|
* mode?:string,
|
||||||
|
* code_url?:string,
|
||||||
* payment_url?:string,
|
* payment_url?:string,
|
||||||
* qr_content?:string,
|
* form_html?:string,
|
||||||
|
* jsapi_params?:array<string,string>,
|
||||||
* transaction_id?:string,
|
* transaction_id?:string,
|
||||||
* out_trade_no?:string
|
* out_trade_no?:string
|
||||||
* }
|
* }
|
||||||
@@ -108,19 +120,18 @@ class PaymentOrchestrator {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$payment_url = (string) ($result['payment_url'] ?? '');
|
$cashier = self::cashier_fields($result);
|
||||||
$qr_content = (string) ($result['payment_data']['code_url'] ?? '');
|
if ($cashier !== []) {
|
||||||
if ($payment_url !== '' || $qr_content !== '') {
|
// Provider gateways: the order stays pending until the signed
|
||||||
// Redirect/scan gateways: the order stays pending until the signed
|
|
||||||
// notify/return callback confirms it. Never pre-mark as paid here.
|
// notify/return callback confirms it. Never pre-mark as paid here.
|
||||||
return [
|
return array_merge($cashier, [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'redirect' => true,
|
|
||||||
'payment_url' => $payment_url,
|
|
||||||
'qr_content' => $qr_content,
|
|
||||||
'out_trade_no' => (string) ($order['out_trade_no'] ?? $order_number),
|
'out_trade_no' => (string) ($order['out_trade_no'] ?? $order_number),
|
||||||
'message' => (string) ($result['message'] ?? __('请在新页面完成支付。', 'aksrt')),
|
'message' => (string) ($result['message'] ?? __('请完成支付。', 'aksrt')),
|
||||||
];
|
]);
|
||||||
|
}
|
||||||
|
if (!in_array($method, self::SYNC_METHODS, true)) {
|
||||||
|
return self::fail('aksrt_pay_failed', __('支付网关未返回收银信息。', 'aksrt'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synchronous asset gateways commit the debit and paid transition together.
|
// Synchronous asset gateways commit the debit and paid transition together.
|
||||||
@@ -128,7 +139,6 @@ class PaymentOrchestrator {
|
|||||||
if (!empty($result['order_committed'])) {
|
if (!empty($result['order_committed'])) {
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'redirect' => false,
|
|
||||||
'transaction_id' => $transaction_id,
|
'transaction_id' => $transaction_id,
|
||||||
'replayed' => !empty($result['replayed']),
|
'replayed' => !empty($result['replayed']),
|
||||||
'message' => (string) ($result['message'] ?? __('支付成功。', 'aksrt')),
|
'message' => (string) ($result['message'] ?? __('支付成功。', 'aksrt')),
|
||||||
@@ -149,7 +159,6 @@ class PaymentOrchestrator {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'redirect' => false,
|
|
||||||
'transaction_id' => $transaction_id,
|
'transaction_id' => $transaction_id,
|
||||||
'message' => (string) ($result['message'] ?? __('支付成功。', 'aksrt')),
|
'message' => (string) ($result['message'] ?? __('支付成功。', 'aksrt')),
|
||||||
];
|
];
|
||||||
@@ -177,6 +186,67 @@ class PaymentOrchestrator {
|
|||||||
return ['gateway' => $gateway];
|
return ['gateway' => $gateway];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cashier keys of a pay() result, for REST builders to pass through verbatim.
|
||||||
|
* Empty for synchronous settlements, so `!empty(...)` doubles as "awaiting provider".
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $pay_result
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function cashier_payload(array $pay_result): array {
|
||||||
|
if (empty($pay_result['mode'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return array_intersect_key($pay_result, array_flip(['mode', 'code_url', 'payment_url', 'form_html', 'jsapi_params']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a provider create_payment() result into the cashier contract
|
||||||
|
* shared by every front-end cashier. Empty when the gateway is synchronous.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $result
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function cashier_fields(array $result): array {
|
||||||
|
$mode = sanitize_key((string) ($result['mode'] ?? ''));
|
||||||
|
switch ($mode) {
|
||||||
|
case self::MODE_QRCODE:
|
||||||
|
$code_url = (string) ($result['code_url'] ?? '');
|
||||||
|
return $code_url === '' ? [] : [
|
||||||
|
'mode' => $mode,
|
||||||
|
'code_url' => $code_url,
|
||||||
|
// Optional mobile deep link shown beside the QR code.
|
||||||
|
'payment_url' => (string) ($result['payment_url'] ?? ''),
|
||||||
|
];
|
||||||
|
case self::MODE_REDIRECT:
|
||||||
|
$url = (string) ($result['payment_url'] ?? '');
|
||||||
|
return $url === '' ? [] : ['mode' => $mode, 'payment_url' => $url];
|
||||||
|
case self::MODE_FORM:
|
||||||
|
$html = (string) ($result['form_html'] ?? '');
|
||||||
|
return $html === '' ? [] : ['mode' => $mode, 'form_html' => $html];
|
||||||
|
case self::MODE_JSAPI:
|
||||||
|
$params = $result['jsapi_params'] ?? null;
|
||||||
|
return is_array($params) && $params !== [] ? ['mode' => $mode, 'jsapi_params' => $params] : [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields every cashier builder forwards to the browser for a pending provider payment.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $payment_result PaymentOrchestrator::pay() success result.
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function public_cashier(array $payment_result): array {
|
||||||
|
$out = ['mode' => (string) ($payment_result['mode'] ?? '')];
|
||||||
|
foreach (['code_url', 'payment_url', 'form_html', 'jsapi_params'] as $key) {
|
||||||
|
if (isset($payment_result[$key]) && $payment_result[$key] !== '') {
|
||||||
|
$out[$key] = $payment_result[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
private static function failure_code(string $method, string $message): string {
|
private static function failure_code(string $method, string $message): string {
|
||||||
if ($method === 'balance' && (str_contains($message, '余额') || str_contains($message, '不足'))) {
|
if ($method === 'balance' && (str_contains($message, '余额') || str_contains($message, '不足'))) {
|
||||||
return 'aksrt_balance_insufficient';
|
return 'aksrt_balance_insufficient';
|
||||||
|
|||||||
@@ -422,8 +422,6 @@ class UserOrders {
|
|||||||
}
|
}
|
||||||
if ($this->status_group((string) ($order['status'] ?? '')) === OrderCenterQuery::STATUS_PAID) {
|
if ($this->status_group((string) ($order['status'] ?? '')) === OrderCenterQuery::STATUS_PAID) {
|
||||||
return ['success' => true, 'data' => [
|
return ['success' => true, 'data' => [
|
||||||
'redirect' => false,
|
|
||||||
'payment_url' => '',
|
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
'order' => $this->get_order_detail_view($user_id, $order_number),
|
'order' => $this->get_order_detail_view($user_id, $order_number),
|
||||||
'replayed' => true,
|
'replayed' => true,
|
||||||
@@ -455,14 +453,13 @@ class UserOrders {
|
|||||||
'message' => (string) ($result['message'] ?? __('支付失败。', 'aksrt')),
|
'message' => (string) ($result['message'] ?? __('支付失败。', 'aksrt')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return ['success' => true, 'data' => [
|
$cashier = PaymentOrchestrator::cashier_payload($result);
|
||||||
'redirect' => !empty($result['redirect']),
|
return ['success' => true, 'data' => array_merge($cashier, [
|
||||||
'payment_url' => (string) ($result['payment_url'] ?? ''),
|
|
||||||
'order_number' => $order_number,
|
'order_number' => $order_number,
|
||||||
'order' => empty($result['redirect']) ? $this->get_order_detail_view($user_id, $order_number) : null,
|
'order' => $cashier === [] ? $this->get_order_detail_view($user_id, $order_number) : null,
|
||||||
'replayed' => !empty($result['replayed']),
|
'replayed' => !empty($result['replayed']),
|
||||||
'balance' => $payment_method === 'balance' ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : null,
|
'balance' => $payment_method === 'balance' ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : null,
|
||||||
'message' => (string) ($result['message'] ?? ''),
|
'message' => (string) ($result['message'] ?? ''),
|
||||||
]];
|
])];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,7 +410,7 @@ class PaymentRuntime {
|
|||||||
'apiurl' => esc_url_raw((string) ($settings['epay_apiurl'] ?? '')),
|
'apiurl' => esc_url_raw((string) ($settings['epay_apiurl'] ?? '')),
|
||||||
'pid' => sanitize_text_field((string) ($settings['epay_pid'] ?? '')),
|
'pid' => sanitize_text_field((string) ($settings['epay_pid'] ?? '')),
|
||||||
'key' => sanitize_text_field((string) ($settings['epay_key'] ?? '')),
|
'key' => sanitize_text_field((string) ($settings['epay_key'] ?? '')),
|
||||||
'device' => sanitize_key((string) ($settings['epay_device'] ?? 'auto')),
|
'qrcode' => !array_key_exists('epay_qrcode', $settings) || !empty($settings['epay_qrcode']),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"generated_at": "2026-10-26T13:32:51+00:00",
|
"generated_at": "2026-10-29T05:44:00+00:00",
|
||||||
"errors": [],
|
"errors": [],
|
||||||
"warnings": [
|
"warnings": [
|
||||||
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
||||||
"customPage users-wechat-template section 'users/wechat-template' not in PHP schema (ok if pure operational page)",
|
"customPage users-wechat-template section 'users/wechat-template' not in PHP schema (ok if pure operational page)",
|
||||||
"enum incomplete parse for 'alipay_payment_type' (php subset of js): php=[page] js=[page,wap]",
|
"enum incomplete parse for 'alipay_pc_mode' (php subset of js): php=[facepay] js=[facepay,web]",
|
||||||
"enum incomplete parse for 'avatar_external' (php subset of js): php=[] js=[,gravatar,weavatar,cravatar,geekzu,loli,sep,v2ex]",
|
"enum incomplete parse for 'avatar_external' (php subset of js): php=[] js=[,gravatar,weavatar,cravatar,geekzu,loli,sep,v2ex]",
|
||||||
"enum incomplete parse for 'backup_frequency' (php subset of js): php=[daily] js=[daily,weekly,monthly]",
|
"enum incomplete parse for 'backup_frequency' (php subset of js): php=[daily] js=[daily,weekly,monthly]",
|
||||||
"enum incomplete parse for 'bind_reminder' (php subset of js): php=[email] js=[email,phone,email_phone,close]",
|
"enum incomplete parse for 'bind_reminder' (php subset of js): php=[email] js=[email,phone,email_phone,close]",
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
"enum incomplete parse for 'content_audit_provider' (php subset of js): php=[baidu] js=[baidu,null]",
|
"enum incomplete parse for 'content_audit_provider' (php subset of js): php=[baidu] js=[baidu,null]",
|
||||||
"enum incomplete parse for 'direction' (php subset of js): php=[horizontal] js=[horizontal,vertical]",
|
"enum incomplete parse for 'direction' (php subset of js): php=[horizontal] js=[horizontal,vertical]",
|
||||||
"enum incomplete parse for 'effect' (php subset of js): php=[slide] js=[slide,fade,cube,coverflow,flip]",
|
"enum incomplete parse for 'effect' (php subset of js): php=[slide] js=[slide,fade,cube,coverflow,flip]",
|
||||||
"enum incomplete parse for 'epay_device' (php subset of js): php=[auto] js=[auto,pc,mobile]",
|
|
||||||
"enum incomplete parse for 'font_family' (php subset of js): php=[system] js=[system,noto-sans-sc,noto-serif-sc,custom]",
|
"enum incomplete parse for 'font_family' (php subset of js): php=[system] js=[system,noto-sans-sc,noto-serif-sc,custom]",
|
||||||
"enum incomplete parse for 'i18n_default_lang' (php subset of js): php=[zh-CN] js=[zh-CN,en,zh-TW,ja,ko,fr,de,es,ru]",
|
"enum incomplete parse for 'i18n_default_lang' (php subset of js): php=[zh-CN] js=[zh-CN,en,zh-TW,ja,ko,fr,de,es,ru]",
|
||||||
"enum incomplete parse for 'i18n_switcher_position' (php subset of js): php=[header] js=[header,footer,both,none]",
|
"enum incomplete parse for 'i18n_switcher_position' (php subset of js): php=[header] js=[header,footer,both,none]",
|
||||||
@@ -58,13 +57,12 @@
|
|||||||
"enum incomplete parse for 'user_email_limit_type' (php subset of js): php=[not] js=[not,allow]",
|
"enum incomplete parse for 'user_email_limit_type' (php subset of js): php=[not] js=[not,allow]",
|
||||||
"enum incomplete parse for 'user_sign_type' (php subset of js): php=[modal] js=[modal,page]",
|
"enum incomplete parse for 'user_sign_type' (php subset of js): php=[modal] js=[modal,page]",
|
||||||
"enum incomplete parse for 'user_signin_nopas_active' (php subset of js): php=[nopas] js=[nopas,pas,only_nopas]",
|
"enum incomplete parse for 'user_signin_nopas_active' (php subset of js): php=[nopas] js=[nopas,pas,only_nopas]",
|
||||||
"enum incomplete parse for 'wechat_payment_type' (php subset of js): php=[native] js=[native,h5]",
|
|
||||||
"enum incomplete parse for 'weixingzh_gzh_type' (php subset of js): php=[service] js=[service,subscribe]"
|
"enum incomplete parse for 'weixingzh_gzh_type' (php subset of js): php=[service] js=[service,subscribe]"
|
||||||
],
|
],
|
||||||
"info": [
|
"info": [
|
||||||
"PHP schema domains: global, page_display, post_list, permissions, users, payment, extensions, tools, ExtensionsAudit, ExtensionsIP, ExtensionsPush, ExtensionsSecurity, ExtensionsSystem, ToolsBackup, ToolsHealth, ToolsImportExport, PostListList",
|
"PHP schema domains: global, page_display, post_list, permissions, users, payment, extensions, tools, ExtensionsAudit, ExtensionsIP, ExtensionsPush, ExtensionsSecurity, ExtensionsSystem, ToolsBackup, ToolsHealth, ToolsImportExport, PostListList",
|
||||||
"Menu pages discovered: 61",
|
"Menu pages discovered: 61",
|
||||||
"admin build ok; chunks=31"
|
"admin build ok; chunks=33"
|
||||||
],
|
],
|
||||||
"domains": {
|
"domains": {
|
||||||
"global": {
|
"global": {
|
||||||
@@ -260,24 +258,26 @@
|
|||||||
"entry": true,
|
"entry": true,
|
||||||
"missing_chunks": [],
|
"missing_chunks": [],
|
||||||
"chunks": [
|
"chunks": [
|
||||||
"SchemaPage.C79jkx8B.js",
|
"SchemaPage.BBTFBTmo.js",
|
||||||
"vendor-vue.AXlcobq-.js",
|
"vendor-vue.AXlcobq-.js",
|
||||||
"FaIcon.Cta3HYOc.js",
|
"FaIcon.Cta3HYOc.js",
|
||||||
"AksMediaField.CiF8KSwp.js",
|
"useCollapsibleItems.BqcC9pDK.js",
|
||||||
|
"vendor-draggable.CsbWt1CM.js",
|
||||||
"FaIconPicker.DJI1XtMq.js",
|
"FaIconPicker.DJI1XtMq.js",
|
||||||
"shared-api.GLIRMK1p.js",
|
"shared-api.GLIRMK1p.js",
|
||||||
"vendor-misc.DSSPvhm2.js",
|
"vendor-misc.DSSPvhm2.js",
|
||||||
"vendor-draggable.CsbWt1CM.js",
|
"AksColorPicker.JO2GmxwS.js",
|
||||||
|
"sortable-options.Do4PkQoR.js",
|
||||||
"level.E3ANSDAX.js",
|
"level.E3ANSDAX.js",
|
||||||
"badge.CKTsmH1z.js",
|
"badge.ClviT45t.js",
|
||||||
"vendor-naive.DgdCsaiA.js",
|
"vendor-naive.DgdCsaiA.js",
|
||||||
"DeferredPage.CDpa6_lq.js",
|
"DeferredPage.CDpa6_lq.js",
|
||||||
"NativeOpsRedirect.CD8j4uaH.js",
|
"NativeOpsRedirect.CD8j4uaH.js",
|
||||||
"FloatingButtonsSettings.BB4Ds-Vh.js",
|
"FloatingButtonsSettings.BI3xnao1.js",
|
||||||
"PageHeader.C1t645NL.js",
|
"PageHeader.C1t645NL.js",
|
||||||
"FooterTabbarSettings.D7iCVwp2.js",
|
"FooterTabbarSettings.DcDH4CdV.js",
|
||||||
"PostPageSettings.Cnn7DRgi.js",
|
"PostPageSettings.CIkb2bLS.js",
|
||||||
"CommentSettings.1g5G6LQL.js",
|
"CommentSettings.d47RErzk.js",
|
||||||
"PermissionsCapabilities.eEUosHCJ.js",
|
"PermissionsCapabilities.eEUosHCJ.js",
|
||||||
"CmsCredits.DUise9Hl.js",
|
"CmsCredits.DUise9Hl.js",
|
||||||
"ThemeInfoPage.DuqoCync.js",
|
"ThemeInfoPage.DuqoCync.js",
|
||||||
@@ -285,9 +285,9 @@
|
|||||||
"DocsPage.CBhfO0JX.js",
|
"DocsPage.CBhfO0JX.js",
|
||||||
"UsersVerify.CWq6m0ZY.js",
|
"UsersVerify.CWq6m0ZY.js",
|
||||||
"UsersLevel.BnvrTSj0.js",
|
"UsersLevel.BnvrTSj0.js",
|
||||||
"UsersBadge.CZSYaoC4.js",
|
"UsersBadge.Cv_yOzWA.js",
|
||||||
"UsersWechatTemplate.BGKwGvbY.js",
|
"UsersWechatTemplate.BGKwGvbY.js",
|
||||||
"UsersTicket.CoFrcj_8.js",
|
"UsersTicket.B_nhzy9S.js",
|
||||||
"ToolsImportExport.CuVI1mrN.js",
|
"ToolsImportExport.CuVI1mrN.js",
|
||||||
"ToolsHealth.BT7gJFVm.js",
|
"ToolsHealth.BT7gJFVm.js",
|
||||||
"ToolsUpdate.59iH8s4m.js"
|
"ToolsUpdate.59iH8s4m.js"
|
||||||
|
|||||||
Reference in new issue
Block a user