fix: H5支付中转页OAuth重签并禁止自动调起
This commit is contained in:
1 parent
45a9508ed4
commit
6c5c9a378a
3 files changed
+250
-56
No files matched your search
@@ -166,12 +166,43 @@ def build_native_bridge_url(dingdanid: str, club_id=None) -> str:
|
||||
return f'https://www.abas.asia/hqhd/dingdan/wx-native-pay/?{q}'
|
||||
|
||||
|
||||
def build_official_jsapi_pay_params(**kwargs):
|
||||
raise Exception('已改为官方 Native 付款码,不再走服务号 H5 JSAPI')
|
||||
def build_official_jsapi_pay_params(
|
||||
*,
|
||||
dingdanid,
|
||||
jine,
|
||||
body,
|
||||
notify_url='',
|
||||
openid,
|
||||
club_id=None,
|
||||
client_ip='127.0.0.1',
|
||||
):
|
||||
"""服务号 JSAPI 统一下单(H5 / 相册中转页)。notify_url 由订单侧 settings 接管。"""
|
||||
from orders.views.payment import CreateOrderView
|
||||
|
||||
helper = CreateOrderView()
|
||||
return helper.generate_wechat_pay_params(
|
||||
dingdanid=dingdanid,
|
||||
jine=jine,
|
||||
beizhu=(body or '微信支付')[:40] or '微信支付',
|
||||
user_ip=client_ip or '127.0.0.1',
|
||||
openid=openid,
|
||||
club_id=club_id,
|
||||
app_source='official',
|
||||
)
|
||||
|
||||
|
||||
def build_mini_jsapi_pay_params(**kwargs):
|
||||
raise Exception('已改为官方 Native 付款码,不再走小程序码 JSAPI')
|
||||
raise Exception('小程序请走 CreateOrderView.generate_wechat_pay_params(app_source=mini)')
|
||||
|
||||
|
||||
def build_h5_jsapi_bridge_url(dingdanid: str, club_id=None, return_url='') -> str:
|
||||
from urllib.parse import quote
|
||||
q = f'out_trade_no={quote(dingdanid or "", safe="")}'
|
||||
if club_id:
|
||||
q += f'&club_id={quote(str(club_id), safe="")}'
|
||||
if return_url:
|
||||
q += f'&return_url={quote(str(return_url), safe="")}'
|
||||
return f'https://www.abas.asia/hqhd/dingdan/wx-h5-pay/?{q}'
|
||||
|
||||
|
||||
def resolve_scene_to_dingdanid(scene: str) -> str:
|
||||
|
||||
+97
-4
@@ -533,11 +533,101 @@ def alipay_pay_page(request):
|
||||
def wx_h5_jsapi_pay_page(request):
|
||||
"""H5 点单密码支付中转页(落在已登记的 /hqhd/dingdan/ 授权目录)。
|
||||
|
||||
不是扫码。仅把 WeixinJSBridge 调起挪到商户已配置的目录下,
|
||||
避免 /xqh5/ 报「当前页面的URL未注册」。
|
||||
支付参数由同域 sessionStorage 传入(前端写入)。
|
||||
不是扫码。流程:
|
||||
1) 带 out_trade_no 进入 → 服务号静默 OAuth 取当前微信 openid
|
||||
2) 用该 openid 对未付款订单重新统一下单,服务端注入 pay 参数
|
||||
3) 页面仅用户点击后 WeixinJSBridge.invoke(避免自动调起被微信秒关)
|
||||
|
||||
sessionStorage 仅作无 OAuth / 旧链接兜底。
|
||||
"""
|
||||
return render(request, 'wechat/h5_jsapi_pay.html', {})
|
||||
from urllib.parse import quote
|
||||
from jituan.services.club_resolver import get_club_official_credentials, oauth2_access_token
|
||||
from jituan.services.wechat_native_pay import (
|
||||
build_official_jsapi_pay_params,
|
||||
build_h5_jsapi_bridge_url,
|
||||
)
|
||||
|
||||
out_trade_no = (request.GET.get('out_trade_no') or request.GET.get('dingdanid') or '').strip()
|
||||
club_id = (request.GET.get('club_id') or '').strip() or 'ufo'
|
||||
code = (request.GET.get('code') or '').strip()
|
||||
return_url = (request.GET.get('return_url') or '').strip()
|
||||
|
||||
ctx = {
|
||||
'server_pay': None,
|
||||
'server_error': '',
|
||||
'out_trade_no': out_trade_no,
|
||||
'club_id': club_id,
|
||||
'return_url': return_url,
|
||||
'oauth_openid_preview': '',
|
||||
'pay_source': 'sessionStorage',
|
||||
}
|
||||
|
||||
if not out_trade_no:
|
||||
# 兼容仅 sessionStorage 的旧跳转
|
||||
return render(request, 'wechat/h5_jsapi_pay.html', ctx)
|
||||
|
||||
# 查未付款订单
|
||||
order = None
|
||||
jine = None
|
||||
try:
|
||||
from orders.models import Order
|
||||
order = Order.query.filter(OrderID=out_trade_no).first()
|
||||
if order and int(getattr(order, 'Status', -1) or -1) == 9:
|
||||
jine = getattr(order, 'Amount', None)
|
||||
club_id = (getattr(order, 'ClubID', None) or club_id or '').strip() or club_id
|
||||
except Exception:
|
||||
logger.exception('[WX_H5_PAY] 查订单失败 dingdan=%s', out_trade_no)
|
||||
|
||||
if order is None or jine is None:
|
||||
ctx['server_error'] = '订单不存在或已支付/已取消,请返回点单页重新下单'
|
||||
return render(request, 'wechat/h5_jsapi_pay.html', ctx)
|
||||
|
||||
appid, secret, _club = get_club_official_credentials(club_id)
|
||||
if not appid or not secret:
|
||||
ctx['server_error'] = f'俱乐部 {club_id} 未配置服务号 official_appid/secret'
|
||||
return render(request, 'wechat/h5_jsapi_pay.html', ctx)
|
||||
|
||||
if not code:
|
||||
redirect_uri = build_h5_jsapi_bridge_url(out_trade_no, club_id, return_url)
|
||||
oauth = (
|
||||
'https://open.weixin.qq.com/connect/oauth2/authorize'
|
||||
f'?appid={quote(appid)}'
|
||||
f'&redirect_uri={quote(redirect_uri, safe="")}'
|
||||
'&response_type=code&scope=snsapi_base&state=h5pay#wechat_redirect'
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
return redirect(oauth)
|
||||
|
||||
wx = oauth2_access_token(code, club_id=club_id)
|
||||
openid = (wx or {}).get('openid') or ''
|
||||
if not openid:
|
||||
ctx['server_error'] = (wx or {}).get('errmsg') or '微信授权失败,请返回重试'
|
||||
return render(request, 'wechat/h5_jsapi_pay.html', ctx)
|
||||
|
||||
oid = openid.strip()
|
||||
ctx['oauth_openid_preview'] = (oid[:6] + '...' + oid[-4:]) if len(oid) > 10 else oid
|
||||
|
||||
xff = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
client_ip = (xff.split(',')[0].strip() if xff else '') or request.META.get('REMOTE_ADDR') or '127.0.0.1'
|
||||
try:
|
||||
pay = build_official_jsapi_pay_params(
|
||||
dingdanid=out_trade_no,
|
||||
jine=jine,
|
||||
body=getattr(settings, 'WEIXIN_PAY_DESCRIPTION', '星阙订单'),
|
||||
openid=oid,
|
||||
club_id=club_id,
|
||||
client_ip=client_ip,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception('[WX_H5_PAY] OAuth 重下单失败 dingdan=%s', out_trade_no)
|
||||
ctx['server_error'] = f'统一下单失败:{exc}'
|
||||
return render(request, 'wechat/h5_jsapi_pay.html', ctx)
|
||||
|
||||
# 去掉前端不需要的私有字段副本;_diag 保留给诊断页
|
||||
ctx['server_pay'] = pay
|
||||
ctx['club_id'] = club_id
|
||||
ctx['pay_source'] = 'oauth_server'
|
||||
return render(request, 'wechat/h5_jsapi_pay.html', ctx)
|
||||
|
||||
|
||||
def wx_native_pay_page(request):
|
||||
@@ -1331,6 +1421,9 @@ class CreateOrderView(APIView):
|
||||
'club_id': str(club_id or ''),
|
||||
'appid': str(APPID or ''),
|
||||
'app_source': str(source or ''),
|
||||
'openid_preview': _oid_preview,
|
||||
'openid_len': len(openid or ''),
|
||||
'key_source': str(pay_cfg.get('_key_source') or ''),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -29,17 +29,27 @@
|
||||
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未注册」——有=目录仍未生效;没有=把下方绿日志整段复制发回。
|
||||
</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';
|
||||
@@ -54,15 +64,40 @@
|
||||
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) || '(未知,请看下方日志 payload.debug)';
|
||||
var app = (pay && (pay.appId || pay.appid)) || (diag && diag.backendAppId) || '(未知)';
|
||||
var club = (diag && diag.club_id) || '(未知)';
|
||||
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/>' +
|
||||
'请到<strong>这个商户号</strong>的后台配 JSAPI 目录:https://www.abas.asia/hqhd/dingdan/';
|
||||
'pay来源 = ' + PAY_SOURCE + '<br/>' +
|
||||
'OAuth openid = ' + (SERVER_OID || '(无)') + '<br/>' +
|
||||
'请到<strong>这个商户号</strong>的「产品中心 → JSAPI支付 → 支付授权目录」配:<br/>' +
|
||||
'https://www.abas.asia/hqhd/dingdan/<br/>' +
|
||||
'以及精确目录:https://www.abas.asia/hqhd/dingdan/wx-h5-pay/';
|
||||
}
|
||||
|
||||
function now() {
|
||||
@@ -125,27 +160,57 @@
|
||||
push('beforeHash 是否以 xqh5 开头', beforeHash.indexOf('https://www.abas.asia/xqh5/') === 0);
|
||||
}
|
||||
|
||||
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));
|
||||
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
|
||||
});
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
pay = payload && payload.pay ? payload.pay : null;
|
||||
returnUrl = (payload && payload.returnUrl) || '';
|
||||
diag = (payload && payload.debug) || {};
|
||||
setBanner();
|
||||
push('!!!! 收款商户号 mch_id', diag.mch_id || '(空——后端未返回 _diag,请先部署后端)');
|
||||
push('payload.debug 完整', diag);
|
||||
|
||||
push('!!!! 收款商户号 mch_id', diag.mch_id || '(空)');
|
||||
push('diag 完整', diag);
|
||||
dumpEnv('页面加载');
|
||||
push('payload.orderId', payload && payload.orderId);
|
||||
push('payload.returnUrl', returnUrl || '(空)');
|
||||
push('payload.ts', payload && payload.ts);
|
||||
if (pay) {
|
||||
push('pay.appId', pay.appId || pay.appid || '(空)');
|
||||
push('pay.timeStamp', pay.timeStamp);
|
||||
@@ -154,7 +219,14 @@
|
||||
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 对象', pay);
|
||||
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 对象', '(没有,无法调起支付)');
|
||||
}
|
||||
@@ -171,9 +243,9 @@
|
||||
var invokeAt = 0;
|
||||
|
||||
function invokePay() {
|
||||
dumpEnv('点击/自动调起前');
|
||||
dumpEnv('点击调起前');
|
||||
if (!pay || !pay.timeStamp || !pay.nonceStr || !pay.package || !pay.paySign) {
|
||||
setTip('支付参数丢失,请返回重新下单(见下方日志)', true);
|
||||
setTip('支付参数丢失:' + (SERVER_ERROR || '请返回重新下单'), true);
|
||||
push('中止:pay 参数不完整', pay);
|
||||
return;
|
||||
}
|
||||
@@ -202,7 +274,8 @@
|
||||
nonceStr: req.nonceStr,
|
||||
package: req.package,
|
||||
signType: req.signType,
|
||||
paySign_len: String(req.paySign || '').length
|
||||
paySign_len: String(req.paySign || '').length,
|
||||
pay_source: PAY_SOURCE
|
||||
});
|
||||
setTip('正在调起微信支付…');
|
||||
btn.disabled = true;
|
||||
@@ -218,9 +291,7 @@
|
||||
try { return JSON.stringify(res); } catch (e) { return String(res); }
|
||||
})());
|
||||
var msg = (res && res.err_msg) || '';
|
||||
var errDesc = (res && (res.err_desc || res.errMsg || res.err_msg)) || '';
|
||||
push('err_msg', msg);
|
||||
push('err_desc/其它', errDesc);
|
||||
try {
|
||||
var keys = [];
|
||||
for (var k in res) {
|
||||
@@ -239,27 +310,30 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// cancel:真正取消,或微信秒关(常见于未注册等,却只回 cancel)
|
||||
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/>顶部红框的 mch_id 才是要配目录的商户号', true);
|
||||
setTip('<span class="err">微信秒关 cancel</span><br/>请确认是否闪过「URL未注册」灰字,并把绿日志复制发回', true);
|
||||
try {
|
||||
alert(
|
||||
'支付未完成(cancel 秒关)\n\n' +
|
||||
'【收款商户号 mch_id】\n' + ((diag && diag.mch_id) || '(未知,先部署后端)') + '\n\n' +
|
||||
'必须在这个商户后台配:\nhttps://www.abas.asia/hqhd/dingdan/\n\n' +
|
||||
'appId=' + appId + '\nURL=' + checkUrl + '\nms=' + costMs
|
||||
'支付秒关 cancel\n\n' +
|
||||
'mch_id=' + ((diag && diag.mch_id) || '?') + '\n' +
|
||||
'appId=' + appId + '\n' +
|
||||
'来源=' + PAY_SOURCE + '\n' +
|
||||
'openid=' + (SERVER_OID || '?') + '\n' +
|
||||
'URL=' + checkUrl + '\n' +
|
||||
'ms=' + costMs + '\n\n' +
|
||||
'请问:有没有灰字「当前页面的URL未注册」?'
|
||||
);
|
||||
} catch (e) {}
|
||||
} else {
|
||||
setTip('已取消支付。可点「立即支付」重试');
|
||||
try {
|
||||
alert('已取消\nmch_id=' + ((diag && diag.mch_id) || '(未知)') + '\nappId=' + appId + '\nURL=' + checkUrl);
|
||||
} catch (e) {}
|
||||
setTip('已取消支付。可再点「立即支付」');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -268,9 +342,6 @@
|
||||
push('!!!! 支付失败,请把本页全部绿色日志复制发送 !!!!');
|
||||
push('当前校验 URL', checkUrl);
|
||||
push('调起用的 appId', appId);
|
||||
try {
|
||||
alert('支付失败诊断\n\n' + msg + '\n\nappId=' + appId + '\nURL=' + checkUrl + '\n\n完整日志在页面绿色区域,点「复制」');
|
||||
} catch (e) {}
|
||||
});
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
@@ -293,18 +364,17 @@
|
||||
|
||||
btn.onclick = invokePay;
|
||||
|
||||
if (!pay) {
|
||||
setTip('支付参数丢失:请从星阙点单页重新点「确认支付」再跳进来', true);
|
||||
push('提示:同域 sessionStorage 丢失常见于开了新窗口/清理缓存;请勿复制链接单独打开本页');
|
||||
if (SERVER_ERROR && !pay) {
|
||||
setTip('<span class="err">' + SERVER_ERROR + '</span><br/>请返回点单页重新下单', true);
|
||||
return;
|
||||
}
|
||||
setTip('参数已就绪,自动调起支付…');
|
||||
if (typeof WeixinJSBridge === 'undefined') {
|
||||
document.addEventListener('WeixinJSBridgeReady', invokePay, false);
|
||||
push('等待 WeixinJSBridgeReady');
|
||||
} else {
|
||||
setTimeout(invokePay, 80);
|
||||
if (!pay) {
|
||||
setTip('支付参数丢失:请从星阙点单页重新点「确认支付」', true);
|
||||
push('提示:请勿单独打开本页;须从点单页跳转');
|
||||
return;
|
||||
}
|
||||
setTip('参数已就绪。请<strong>手动点击</strong>「立即支付」(已关闭自动调起)');
|
||||
push('已关闭自动调起,等待用户点击');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user