fix: 扫码二维码恢复官方weixin付款码,他人扫瞬间出密码
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2d5f2ada79
commit
a6d2f878ab
1 file changed
+108
-33
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user