diff --git a/orders/views/payment.py b/orders/views/payment.py index d6c345e..8a2e58a 100644 --- a/orders/views/payment.py +++ b/orders/views/payment.py @@ -535,18 +535,26 @@ def wx_h5_jsapi_pay_page(request): 不是扫码。流程: 1) 带 out_trade_no 进入 → 服务号静默 OAuth 取当前微信 openid - 2) 用该 openid 对未付款订单重新统一下单,服务端注入 pay 参数 - 3) 页面仅用户点击后 WeixinJSBridge.invoke(避免自动调起被微信秒关) + 2) 用该 openid 对未付款订单重新统一下单 + 3) 支付参数写入短时 cache,302 到干净 URL ?k=token(去掉 code/长 query) + 4) 前端 history.replaceState 成无 query 的目录 URL,再手动点支付 - sessionStorage 仅作无 OAuth / 旧链接兜底。 + 微信校验整页 URL;OAuth 回调带超长 query 时部分机型会秒关 cancel。 """ + import secrets from urllib.parse import quote + from django.shortcuts import redirect 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, ) + CACHE_PREFIX = 'wx_h5_jsapi_pay_pack_' + CACHE_TTL = 60 * 15 + CLEAN_PAY_URL = 'https://www.abas.asia/hqhd/dingdan/wx-h5-pay/' + + pack_key = (request.GET.get('k') or '').strip() 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() @@ -560,13 +568,27 @@ def wx_h5_jsapi_pay_page(request): 'return_url': return_url, 'oauth_openid_preview': '', 'pay_source': 'sessionStorage', + 'force_clean_url': True, } - if not out_trade_no: - # 兼容仅 sessionStorage 的旧跳转 + # ---- 干净页:用 k 取回 OAuth 后缓存的支付包 ---- + if pack_key and not code: + pack = cache.get(f'{CACHE_PREFIX}{pack_key}') + if not isinstance(pack, dict) or not pack.get('pay'): + ctx['server_error'] = '支付会话已过期,请返回点单页重新下单' + return render(request, 'wechat/h5_jsapi_pay.html', ctx) + pay = pack['pay'] + ctx['server_pay'] = pay + ctx['out_trade_no'] = pack.get('out_trade_no') or '' + ctx['club_id'] = pack.get('club_id') or club_id + ctx['return_url'] = pack.get('return_url') or '' + ctx['oauth_openid_preview'] = pack.get('openid_preview') or '' + ctx['pay_source'] = 'oauth_server_clean' + return render(request, 'wechat/h5_jsapi_pay.html', ctx) + + if not out_trade_no: return render(request, 'wechat/h5_jsapi_pay.html', ctx) - # 查未付款订单 order = None jine = None try: @@ -587,15 +609,20 @@ def wx_h5_jsapi_pay_page(request): ctx['server_error'] = f'俱乐部 {club_id} 未配置服务号 official_appid/secret' return render(request, 'wechat/h5_jsapi_pay.html', ctx) + if return_url: + cache.set(f'wx_h5_return_{out_trade_no}', return_url, CACHE_TTL) + else: + return_url = (cache.get(f'wx_h5_return_{out_trade_no}') or '').strip() + if not code: - redirect_uri = build_h5_jsapi_bridge_url(out_trade_no, club_id, return_url) + # OAuth 回调 URI 尽量短,不带 return_url + 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) @@ -605,7 +632,7 @@ def wx_h5_jsapi_pay_page(request): 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 + oid_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' @@ -621,13 +648,23 @@ def wx_h5_jsapi_pay_page(request): except Exception as exc: logger.exception('[WX_H5_PAY] OAuth 重下单失败 dingdan=%s', out_trade_no) ctx['server_error'] = f'统一下单失败:{exc}' + ctx['oauth_openid_preview'] = oid_preview 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) + token = secrets.token_urlsafe(16) + cache.set( + f'{CACHE_PREFIX}{token}', + { + 'pay': pay, + 'out_trade_no': out_trade_no, + 'club_id': club_id, + 'return_url': return_url, + 'openid_preview': oid_preview, + }, + CACHE_TTL, + ) + return redirect(f'{CLEAN_PAY_URL}?k={quote(token)}') + def wx_native_pay_page(request): diff --git a/templates/wechat/h5_jsapi_pay.html b/templates/wechat/h5_jsapi_pay.html index 7513745..a1f9f3e 100644 --- a/templates/wechat/h5_jsapi_pay.html +++ b/templates/wechat/h5_jsapi_pay.html @@ -41,8 +41,9 @@
微信支付(诊断模式)
- 请手动点下方绿色按钮调起支付(不要依赖自动弹窗)。
- 若闪一下就关:看有没有灰字「当前页面的URL未注册」——有=目录仍未生效;没有=把下方绿日志整段复制发回。 + 请手动点下方绿色按钮调起支付。
+ 秒关时看有没有灰字「当前页面的URL未注册」。
+ 目录必须挂在公众号 AppID wxd400adf1911c534f下(不要挂在小程序 AppID 下)。
初始化…
@@ -95,9 +96,22 @@ 'club_id = ' + club + '
' + 'pay来源 = ' + PAY_SOURCE + '
' + 'OAuth openid = ' + (SERVER_OID || '(无)') + '
' + - '请到这个商户号的「产品中心 → JSAPI支付 → 支付授权目录」配:
' + + '请到商户 1748512512 → 选中公众号 AppID wxd400adf1911c534f
' + + '「产品中心 → JSAPI支付 → 支付授权目录」添加:
' + 'https://www.abas.asia/hqhd/dingdan/
' + - '以及精确目录:https://www.abas.asia/hqhd/dingdan/wx-h5-pay/'; + '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() { @@ -243,6 +257,7 @@ var invokeAt = 0; function invokePay() { + cleanPayUrl(); dumpEnv('点击调起前'); if (!pay || !pay.timeStamp || !pay.nonceStr || !pay.package || !pay.paySign) { setTip('支付参数丢失:' + (SERVER_ERROR || '请返回重新下单'), true); @@ -373,6 +388,8 @@ push('提示:请勿单独打开本页;须从点单页跳转'); return; } + cleanPayUrl(); + dumpEnv('干净 URL 后'); setTip('参数已就绪。请手动点击「立即支付」(已关闭自动调起)'); push('已关闭自动调起,等待用户点击'); })();