401 lines
17 KiB
HTML
401 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<title>微信支付诊断</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #111; color: #eee; }
|
||
.wrap { max-width: 640px; margin: 0 auto; padding: 24px 16px 48px; }
|
||
.title { font-size: 18px; font-weight: 700; margin-bottom: 8px; color: #fff; }
|
||
.sub { font-size: 14px; color: #aaa; line-height: 1.5; margin-bottom: 16px; word-break: break-all; }
|
||
.btn {
|
||
display: block; width: 100%; padding: 14px; font-size: 16px; font-weight: 600;
|
||
border: none; border-radius: 10px; background: #07c160; color: #fff; margin-bottom: 10px;
|
||
}
|
||
.btn.gray { background: #333; }
|
||
.btn[disabled] { opacity: 0.55; }
|
||
pre {
|
||
margin-top: 12px; padding: 12px; background: #000; color: #0f0;
|
||
font-size: 11px; line-height: 1.45; white-space: pre-wrap; word-break: break-all;
|
||
border-radius: 8px; max-height: 70vh; overflow: auto;
|
||
border: 1px solid #333;
|
||
}
|
||
.ok { color: #07c160; }
|
||
.err { color: #ff5a5a; }
|
||
.banner {
|
||
margin: 0 0 16px; padding: 14px 12px; border-radius: 10px;
|
||
background: #3a0000; border: 2px solid #ff2d2d; color: #ffe0e0;
|
||
font-size: 15px; font-weight: 700; line-height: 1.5; word-break: break-all;
|
||
}
|
||
.warn {
|
||
margin: 0 0 16px; padding: 12px; border-radius: 10px;
|
||
background: #2a2200; border: 1px solid #c9a227; color: #ffe9a8;
|
||
font-size: 13px; line-height: 1.55;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<div class="title">微信支付(诊断模式)</div>
|
||
<div class="banner" id="banner">收款商户号加载中…</div>
|
||
<div class="warn">
|
||
请<strong>手动点下方绿色按钮</strong>调起支付。<br/>
|
||
秒关时看有没有灰字「当前页面的URL未注册」。<br/>
|
||
目录必须挂在<strong>公众号 AppID wxd400adf1911c534f</strong>下(不要挂在小程序 AppID 下)。
|
||
</div>
|
||
<div class="sub" id="tip">初始化…</div>
|
||
<button class="btn" type="button" id="payBtn">立即支付 / 重试</button>
|
||
<button class="btn gray" type="button" id="copyBtn">复制下方全部诊断到剪贴板</button>
|
||
<pre id="log">loading…</pre>
|
||
</div>
|
||
{% if server_pay %}{{ server_pay|json_script:"server-pay-data" }}{% endif %}
|
||
<script>
|
||
(function () {
|
||
var STORAGE_KEY = 'wx_h5_jsapi_pay';
|
||
var tip = document.getElementById('tip');
|
||
var btn = document.getElementById('payBtn');
|
||
var copyBtn = document.getElementById('copyBtn');
|
||
var logEl = document.getElementById('log');
|
||
var banner = document.getElementById('banner');
|
||
var lines = [];
|
||
var payload = null;
|
||
var pay = null;
|
||
var returnUrl = '';
|
||
var diag = {};
|
||
|
||
// 服务端 OAuth 重签注入(优先于 sessionStorage)
|
||
var SERVER_PAY = null;
|
||
var SERVER_ERROR = '';
|
||
var SERVER_OID = '';
|
||
var PAY_SOURCE = 'sessionStorage';
|
||
var SERVER_CLUB = '';
|
||
var SERVER_ORDER = '';
|
||
var SERVER_RETURN = '';
|
||
try {
|
||
var _el = document.getElementById('server-pay-data');
|
||
SERVER_PAY = _el ? JSON.parse(_el.textContent) : null;
|
||
} catch (e) { SERVER_PAY = null; }
|
||
try {
|
||
SERVER_ERROR = '{{ server_error|default:""|escapejs }}';
|
||
SERVER_OID = '{{ oauth_openid_preview|default:""|escapejs }}';
|
||
PAY_SOURCE = '{{ pay_source|default:"sessionStorage"|escapejs }}';
|
||
SERVER_CLUB = '{{ club_id|default:""|escapejs }}';
|
||
SERVER_ORDER = '{{ out_trade_no|default:""|escapejs }}';
|
||
SERVER_RETURN = '{{ return_url|default:""|escapejs }}';
|
||
} catch (e) {}
|
||
|
||
function setBanner() {
|
||
var mch = (diag && diag.mch_id) || '(未知,请看下方日志)';
|
||
var app = (pay && (pay.appId || pay.appid)) || (diag && (diag.appid || diag.backendAppId)) || '(未知)';
|
||
var club = (diag && diag.club_id) || SERVER_CLUB || '(未知)';
|
||
banner.innerHTML =
|
||
'【必须看】收款商户号 mch_id = <span style="color:#0f0">' + mch + '</span><br/>' +
|
||
'appId = ' + app + '<br/>' +
|
||
'club_id = ' + club + '<br/>' +
|
||
'pay来源 = ' + PAY_SOURCE + '<br/>' +
|
||
'OAuth openid = ' + (SERVER_OID || '(无)') + '<br/>' +
|
||
'请到商户 <strong>1748512512</strong> → 选中公众号 AppID <strong>wxd400adf1911c534f</strong><br/>' +
|
||
'「产品中心 → JSAPI支付 → 支付授权目录」添加:<br/>' +
|
||
'https://www.abas.asia/hqhd/dingdan/<br/>' +
|
||
'https://www.abas.asia/hqhd/dingdan/wx-h5-pay/';
|
||
}
|
||
|
||
function cleanPayUrl() {
|
||
var clean = 'https://www.abas.asia/hqhd/dingdan/wx-h5-pay/';
|
||
try {
|
||
if (String(location.href || '').split('#')[0] !== clean) {
|
||
history.replaceState(null, '', clean);
|
||
push('已 replaceState 成干净校验 URL', clean);
|
||
}
|
||
} catch (e) {
|
||
push('replaceState 失败', String(e && e.message || e));
|
||
}
|
||
}
|
||
|
||
function now() {
|
||
try { return new Date().toISOString(); } catch (e) { return String(Date.now()); }
|
||
}
|
||
|
||
function push() {
|
||
var args = Array.prototype.slice.call(arguments);
|
||
var s = '[' + now() + '] ' + args.map(function (x) {
|
||
if (x == null) return String(x);
|
||
if (typeof x === 'string') return x;
|
||
try { return JSON.stringify(x, null, 2); } catch (e) { return String(x); }
|
||
}).join(' ');
|
||
lines.push(s);
|
||
logEl.textContent = lines.join('\n\n');
|
||
try { console.log.apply(console, args); } catch (e) {}
|
||
}
|
||
|
||
function setTip(t, isErr) {
|
||
tip.innerHTML = t;
|
||
tip.className = 'sub ' + (isErr ? 'err' : '');
|
||
}
|
||
|
||
function safeGetStorage() {
|
||
try {
|
||
return sessionStorage.getItem(STORAGE_KEY);
|
||
} catch (e) {
|
||
push('sessionStorage.getItem 异常', String(e && e.message || e));
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function dumpEnv(tag) {
|
||
var href = '';
|
||
var hash = '';
|
||
var origin = '';
|
||
var path = '';
|
||
var beforeHash = '';
|
||
try {
|
||
href = String(location.href || '');
|
||
hash = String(location.hash || '');
|
||
origin = String(location.origin || '');
|
||
path = String(location.pathname || '');
|
||
beforeHash = href.split('#')[0];
|
||
} catch (e) {
|
||
push('读 location 失败', String(e));
|
||
}
|
||
push('==== ' + tag + ' ====');
|
||
push('location.href', href);
|
||
push('微信校验用的 URL(# 前面)', beforeHash);
|
||
push('location.origin', origin);
|
||
push('location.pathname', path);
|
||
push('location.hash', hash);
|
||
push('document.URL', String(document.URL || ''));
|
||
push('document.referrer', String(document.referrer || '(空)'));
|
||
push('userAgent', String(navigator.userAgent || ''));
|
||
push('WeixinJSBridge', typeof WeixinJSBridge === 'undefined' ? 'undefined' : 'ready');
|
||
push('应匹配的授权目录前缀', 'https://www.abas.asia/hqhd/dingdan/');
|
||
push('beforeHash 是否以该前缀开头', beforeHash.indexOf('https://www.abas.asia/hqhd/dingdan/') === 0);
|
||
push('beforeHash 是否以 xqh5 开头', beforeHash.indexOf('https://www.abas.asia/xqh5/') === 0);
|
||
}
|
||
|
||
push('!!!! 支付参数来源', PAY_SOURCE);
|
||
push('server_error', SERVER_ERROR || '(无)');
|
||
push('oauth_openid_preview', SERVER_OID || '(无)');
|
||
push('out_trade_no', SERVER_ORDER || '(无)');
|
||
|
||
if (SERVER_PAY && typeof SERVER_PAY === 'object' && SERVER_PAY.timeStamp) {
|
||
pay = {
|
||
appId: SERVER_PAY.appId || SERVER_PAY.appid,
|
||
timeStamp: String(SERVER_PAY.timeStamp),
|
||
nonceStr: SERVER_PAY.nonceStr,
|
||
package: SERVER_PAY.package,
|
||
signType: SERVER_PAY.signType || 'MD5',
|
||
paySign: SERVER_PAY.paySign
|
||
};
|
||
diag = Object.assign({}, SERVER_PAY._diag || {}, {
|
||
mch_id: (SERVER_PAY._diag && SERVER_PAY._diag.mch_id) || '',
|
||
club_id: (SERVER_PAY._diag && SERVER_PAY._diag.club_id) || SERVER_CLUB,
|
||
backendAppId: (SERVER_PAY._diag && SERVER_PAY._diag.appid) || pay.appId,
|
||
openid_preview: (SERVER_PAY._diag && SERVER_PAY._diag.openid_preview) || SERVER_OID,
|
||
key_source: (SERVER_PAY._diag && SERVER_PAY._diag.key_source) || ''
|
||
});
|
||
returnUrl = SERVER_RETURN || '';
|
||
push('使用服务端 OAuth 重签 pay', {
|
||
appId: pay.appId,
|
||
package: pay.package,
|
||
openid_preview: diag.openid_preview,
|
||
mch_id: diag.mch_id,
|
||
key_source: diag.key_source,
|
||
key_len: diag.key_len,
|
||
string_a: diag.string_a || '(无)'
|
||
});
|
||
} else {
|
||
try {
|
||
var raw = safeGetStorage();
|
||
push('sessionStorage 原始长度', raw ? raw.length : 0);
|
||
push('sessionStorage 原始内容', raw || '(空)');
|
||
payload = raw ? JSON.parse(raw) : null;
|
||
} catch (e) {
|
||
payload = null;
|
||
push('解析 sessionStorage JSON 失败', String(e && e.message || e));
|
||
}
|
||
pay = payload && payload.pay ? payload.pay : null;
|
||
returnUrl = SERVER_RETURN || (payload && payload.returnUrl) || '';
|
||
diag = (payload && payload.debug) || {};
|
||
if (SERVER_ERROR) {
|
||
push('!!!! 服务端 OAuth/下单失败,回落 sessionStorage', SERVER_ERROR);
|
||
}
|
||
}
|
||
|
||
setBanner();
|
||
push('!!!! 收款商户号 mch_id', diag.mch_id || '(空)');
|
||
push('diag 完整', diag);
|
||
dumpEnv('页面加载');
|
||
if (pay) {
|
||
push('pay.appId', pay.appId || pay.appid || '(空)');
|
||
push('pay.timeStamp', pay.timeStamp);
|
||
push('pay.nonceStr', pay.nonceStr);
|
||
push('pay.package', pay.package);
|
||
push('pay.signType', pay.signType || '(默认MD5)');
|
||
push('pay.paySign 前8位', String(pay.paySign || '').slice(0, 8));
|
||
push('pay.paySign 长度', String(pay.paySign || '').length);
|
||
push('完整 pay 对象', {
|
||
appId: pay.appId || pay.appid,
|
||
timeStamp: pay.timeStamp,
|
||
nonceStr: pay.nonceStr,
|
||
package: pay.package,
|
||
signType: pay.signType,
|
||
paySign: pay.paySign
|
||
});
|
||
} else {
|
||
push('pay 对象', '(没有,无法调起支付)');
|
||
}
|
||
|
||
function goBack() {
|
||
try { sessionStorage.removeItem(STORAGE_KEY); } catch (e) {}
|
||
if (returnUrl) {
|
||
location.replace(returnUrl);
|
||
return;
|
||
}
|
||
history.length > 1 ? history.back() : setTip('支付完成,请返回点单页');
|
||
}
|
||
|
||
var invokeAt = 0;
|
||
|
||
function invokePay() {
|
||
cleanPayUrl();
|
||
dumpEnv('点击调起前');
|
||
if (!pay || !pay.timeStamp || !pay.nonceStr || !pay.package || !pay.paySign) {
|
||
setTip('支付参数丢失:' + (SERVER_ERROR || '请返回重新下单'), true);
|
||
push('中止:pay 参数不完整', pay);
|
||
return;
|
||
}
|
||
if (typeof WeixinJSBridge === 'undefined') {
|
||
setTip('WeixinJSBridge 不存在,请用微信内置浏览器打开', true);
|
||
push('中止:无 WeixinJSBridge');
|
||
return;
|
||
}
|
||
var appId = pay.appId || pay.appid || '';
|
||
if (!appId) {
|
||
setTip('缺少 appId', true);
|
||
push('中止:无 appId');
|
||
return;
|
||
}
|
||
var req = {
|
||
appId: appId,
|
||
timeStamp: String(pay.timeStamp),
|
||
nonceStr: pay.nonceStr,
|
||
package: pay.package,
|
||
signType: pay.signType || 'MD5',
|
||
paySign: pay.paySign
|
||
};
|
||
push('即将 WeixinJSBridge.invoke getBrandWCPayRequest', {
|
||
appId: req.appId,
|
||
timeStamp: req.timeStamp,
|
||
nonceStr: req.nonceStr,
|
||
package: req.package,
|
||
signType: req.signType,
|
||
paySign_len: String(req.paySign || '').length,
|
||
pay_source: PAY_SOURCE
|
||
});
|
||
setTip('正在调起微信支付…');
|
||
btn.disabled = true;
|
||
invokeAt = Date.now();
|
||
try {
|
||
WeixinJSBridge.invoke('getBrandWCPayRequest', req, function (res) {
|
||
btn.disabled = false;
|
||
var costMs = Date.now() - invokeAt;
|
||
dumpEnv('微信回调时');
|
||
push('调起到回调耗时ms', costMs);
|
||
push('微信回调 res 完整对象', res);
|
||
push('微信回调 JSON', (function () {
|
||
try { return JSON.stringify(res); } catch (e) { return String(res); }
|
||
})());
|
||
var msg = (res && res.err_msg) || '';
|
||
push('err_msg', msg);
|
||
try {
|
||
var keys = [];
|
||
for (var k in res) {
|
||
if (Object.prototype.hasOwnProperty.call(res, k)) {
|
||
keys.push(k + '=' + res[k]);
|
||
}
|
||
}
|
||
push('res 所有字段', keys.join(' | '));
|
||
} catch (e) {}
|
||
|
||
var checkUrl = String(location.href || '').split('#')[0];
|
||
|
||
if (msg === 'get_brand_wcpay_request:ok') {
|
||
setTip('<span class="ok">支付成功,正在返回…</span>');
|
||
setTimeout(goBack, 500);
|
||
return;
|
||
}
|
||
|
||
if (msg.indexOf('cancel') >= 0) {
|
||
var quick = costMs < 2500;
|
||
push('判定', quick ? '疑似秒关/失败(耗时<2.5s)' : '用户取消(耗时较长)');
|
||
push('当前校验 URL', checkUrl);
|
||
push('调起用的 appId', appId);
|
||
push('pay_source', PAY_SOURCE);
|
||
push('oauth_openid', SERVER_OID || '(无)');
|
||
push('关键:秒关时屏幕是否出现「当前页面的URL未注册」?有=目录问题;无=把本页绿日志发回');
|
||
if (quick) {
|
||
setTip('<span class="err">微信秒关 cancel</span><br/>请看清闪过的灰字:URL未注册 / 支付验证签名失败 / 什么都没有', true);
|
||
try {
|
||
alert(
|
||
'支付秒关 cancel(约' + costMs + 'ms)\n\n' +
|
||
'请务必看清闪过的提示(三选一回复我):\n' +
|
||
'A. 当前页面的URL未注册\n' +
|
||
'B. 支付验证签名失败\n' +
|
||
'C. 什么字都没有 / 只闪一下\n\n' +
|
||
'mch=' + ((diag && diag.mch_id) || '?') + '\n' +
|
||
'appId=' + appId + '\n' +
|
||
'URL=' + checkUrl
|
||
);
|
||
} catch (e) {}
|
||
} else {
|
||
setTip('已取消支付。可再点「立即支付」');
|
||
}
|
||
return;
|
||
}
|
||
|
||
setTip('<span class="err">支付失败:' + (msg || '未知') + '</span><br/>把下方绿色日志整段复制发给技术', true);
|
||
push('!!!! 支付失败,请把本页全部绿色日志复制发送 !!!!');
|
||
push('当前校验 URL', checkUrl);
|
||
push('调起用的 appId', appId);
|
||
});
|
||
} catch (e) {
|
||
btn.disabled = false;
|
||
push('invoke 抛异常', String(e && e.message || e), e);
|
||
setTip('调起异常:' + String(e && e.message || e), true);
|
||
}
|
||
}
|
||
|
||
copyBtn.onclick = function () {
|
||
var text = logEl.textContent || '';
|
||
function ok() { setTip('已复制,去粘贴发给技术'); }
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(ok).catch(function () {
|
||
prompt('请手动全选复制:', text);
|
||
});
|
||
} else {
|
||
prompt('请手动全选复制:', text);
|
||
}
|
||
};
|
||
|
||
btn.onclick = invokePay;
|
||
|
||
if (SERVER_ERROR && !pay) {
|
||
setTip('<span class="err">' + SERVER_ERROR + '</span><br/>请返回点单页重新下单', true);
|
||
return;
|
||
}
|
||
if (!pay) {
|
||
setTip('支付参数丢失:请从星阙点单页重新点「确认支付」', true);
|
||
push('提示:请勿单独打开本页;须从点单页跳转');
|
||
return;
|
||
}
|
||
cleanPayUrl();
|
||
dumpEnv('干净 URL 后');
|
||
setTip('参数已就绪。请<strong>手动点击</strong>「立即支付」(已关闭自动调起)');
|
||
push('已关闭自动调起,等待用户点击');
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|