fix: 扫码二维码恢复官方weixin付款码,他人扫瞬间出密码

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQueandCursor committed 2026-08-09 07:12:32 +08:00
1 parent 2d5f2ada79
commit a6d2f878ab
1 file changed
+108 -33
+108 -33
View File
@@ -1,12 +1,13 @@
"""微信「扫码」支付(相册可用版)。
"""微信 Native 扫码支付(trade_type=NATIVE)。
关键点:
- 二维码内容必须是 HTTPS(微信允许相册识别普通链接)
- 不能把 weixin://wxpay 付款码直接印成图(相册必被「保障支付安全」拦截)
- 用户扫 HTTPS 后进 H5 → 服务号 OAuth 拿 openid → JSAPI 统一下单
→ WeixinJSBridge.invoke 直接出密码
「一瞬间出密码」= 二维码必须是微信返回的 weixin://wxpay/bizpayurl?...
他人微信摄像头扫屏幕 → 系统直接弹付款,中间没有网页。
本机小程序内仍可用「本机支付」走小程序 JSAPI。
本机相册扫这种付款码会被微信官方拦截(保障支付安全)——不是商户能关的。
本机付款请走小程序「本机支付」JSAPI,同样是一瞬间出密码。
相册可扫的 HTTPS 中转页仍保留(wx-native-pay),但不印进主二维码,
因为那条路必然经过网页授权,做不到和 weixin:// 一样的「无感弹窗」。
"""
from __future__ import annotations
@@ -21,11 +22,17 @@ import requests
from django.core.cache import cache
from jituan.constants import PAY_METHOD_WECHAT_NATIVE
from jituan.services.wechat_pay import (
get_wechat_v2_config,
log_wechat_v2_pay_diag,
parse_wechat_v2_unifiedorder_response,
)
from utils.money import yuan_to_fen
logger = logging.getLogger(__name__)
CACHE_KEY_PREFIX = 'wx_native_pay_'
CACHE_KEY_PREFIX = 'wx_native_code_'
SESSION_KEY_PREFIX = 'wx_native_pay_'
CACHE_TTL_SECONDS = 60 * 30
BRIDGE_URL_BASE = 'https://www.abas.asia/hqhd/dingdan/wx-native-pay/'
@@ -34,6 +41,10 @@ def _cache_key(dingdanid: str) -> str:
return f'{CACHE_KEY_PREFIX}{dingdanid}'
def _session_key(dingdanid: str) -> str:
return f'{SESSION_KEY_PREFIX}{dingdanid}'
def build_native_bridge_url(dingdanid: str, club_id=None) -> str:
q = f'out_trade_no={quote(dingdanid or "", safe="")}'
if club_id:
@@ -43,7 +54,7 @@ def build_native_bridge_url(dingdanid: str, club_id=None) -> str:
def save_native_pay_session(dingdanid, *, jine, body, notify_url, club_id=None):
cache.set(
_cache_key(dingdanid),
_session_key(dingdanid),
{
'jine': str(jine),
'body': (body or '微信支付').strip() or '微信支付',
@@ -55,10 +66,23 @@ def save_native_pay_session(dingdanid, *, jine, body, notify_url, club_id=None):
def get_native_pay_session(dingdanid: str):
data = cache.get(_cache_key(dingdanid))
data = cache.get(_session_key(dingdanid))
return data if isinstance(data, dict) else None
def _native_pay_response(code_url: str, *, bridge_url: str = '') -> dict:
from jituan.services.alipay_pay import qr_link_to_base64
out = {
'qr_code': code_url,
'qr_image': qr_link_to_base64(code_url),
'pay_method': PAY_METHOD_WECHAT_NATIVE,
}
if bridge_url:
out['bridge_url'] = bridge_url
return out
def generate_wechat_native_pay_params(
*,
dingdanid,
@@ -69,13 +93,7 @@ def generate_wechat_native_pay_params(
client_ip='127.0.0.1',
scene='native',
):
"""生成「相册可扫」支付参数:HTTPS 中转码 + 会话缓存。
不再调用 Native unifiedorder(weixin:// 码会被相册拦截)。
真实收款在中转页用服务号 JSAPI 完成,notify_url/履约不变。
"""
from jituan.services.alipay_pay import qr_link_to_base64
"""统一下单 NATIVE,二维码印官方 weixin:// 付款码(他人扫瞬间出密码)。"""
dingdanid = (dingdanid or '').strip()
notify_url = (notify_url or '').strip()
body = (body or '微信支付').strip() or '微信支付'
@@ -84,6 +102,7 @@ def generate_wechat_native_pay_params(
if not notify_url:
raise Exception('缺少微信回调地址 notify_url')
# 给相册/链接兜底页留会话(主二维码仍是 weixin://)
save_native_pay_session(
dingdanid,
jine=jine,
@@ -92,17 +111,79 @@ def generate_wechat_native_pay_params(
club_id=club_id,
)
bridge = build_native_bridge_url(dingdanid, club_id)
logger.warning(
'[WX_NATIVE_ALBUM] session ok dingdan=%s club=%s scene=%s bridge=%s',
dingdanid, club_id, scene, bridge,
cached = cache.get(_cache_key(dingdanid)) or {}
cached_url = (cached.get('code_url') or '').strip()
if cached_url:
logger.info(
'[WX_NATIVE] reuse code_url dingdan=%s club=%s scene=%s',
dingdanid, club_id, scene,
)
return _native_pay_response(cached_url, bridge_url=bridge)
pay_cfg = get_wechat_v2_config(club_id)
log_wechat_v2_pay_diag(
pay_cfg, scene=f'native:{scene}', openid='', order_hint=dingdanid,
)
return {
# 复制也给 HTTPS,打开后同一套支付
'qr_code': bridge,
'qr_image': qr_link_to_base64(bridge),
'bridge_url': bridge,
'pay_method': PAY_METHOD_WECHAT_NATIVE,
appid = pay_cfg.get('appid') or ''
mch_id = pay_cfg.get('mch_id') or ''
key = pay_cfg.get('key') or ''
if not all([appid, mch_id, key]):
raise Exception(
f'微信 Native 配置不完整 club={pay_cfg.get("club_id")} '
f'appid={bool(appid)} mch_id={bool(mch_id)} mch_key={bool(key)}'
)
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
total_fee = yuan_to_fen(jine)
ip = (client_ip or '127.0.0.1').split(',')[0].strip() or '127.0.0.1'
sign_data = {
'appid': appid,
'mch_id': mch_id,
'nonce_str': nonce_str,
'body': body,
'out_trade_no': dingdanid,
'total_fee': total_fee,
'spbill_create_ip': ip,
'notify_url': notify_url,
'trade_type': 'NATIVE',
}
sorted_items = sorted(sign_data.items(), key=lambda x: x[0])
sign_string = '&'.join(f'{k}={v}' for k, v in sorted_items if v) + f'&key={key}'
sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
xml_data = f"""<xml>
<appid>{appid}</appid>
<body>{body}</body>
<mch_id>{mch_id}</mch_id>
<nonce_str>{nonce_str}</nonce_str>
<notify_url>{notify_url}</notify_url>
<out_trade_no>{dingdanid}</out_trade_no>
<spbill_create_ip>{ip}</spbill_create_ip>
<total_fee>{total_fee}</total_fee>
<trade_type>NATIVE</trade_type>
<sign>{sign}</sign>
</xml>"""
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
timeout=10,
)
parsed = parse_wechat_v2_unifiedorder_response(
response.content,
log_prefix=f'WX_NATIVE_{scene}',
expect='code_url',
)
code_url = parsed['code_url']
cache.set(_cache_key(dingdanid), {'code_url': code_url}, CACHE_TTL_SECONDS)
logger.warning(
'[WX_NATIVE] unifiedorder ok dingdan=%s club=%s mch=%s scene=%s code=%s',
dingdanid, pay_cfg.get('club_id'), mch_id, scene, code_url[:48],
)
return _native_pay_response(code_url, bridge_url=bridge)
def build_official_jsapi_pay_params(
@@ -115,13 +196,7 @@ def build_official_jsapi_pay_params(
club_id=None,
client_ip='127.0.0.1',
):
"""服务号 AppID + 服务号 openid 做 JSAPI 统一下单,供 H5 WeixinJSBridge 调起。"""
from jituan.services.wechat_pay import (
get_wechat_v2_config,
log_wechat_v2_pay_diag,
parse_wechat_v2_unifiedorder_response,
)
"""服务号 AppID + 服务号 openid 做 JSAPI,供相册 HTTPS 中转页调起。"""
pay_cfg = get_wechat_v2_config(club_id, app_source='official')
log_wechat_v2_pay_diag(
pay_cfg, scene='native_album_jsapi', openid=openid, order_hint=dingdanid,