Files

401 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>