diff --git a/jituan/services/pay_confirm.py b/jituan/services/pay_confirm.py
index ab9c8f7..d4eb417 100644
--- a/jituan/services/pay_confirm.py
+++ b/jituan/services/pay_confirm.py
@@ -40,7 +40,10 @@ def confirm_remote_payment(
}
is_alipay = cache.get(f'alipay_pay_url_{out_trade_no}') is not None
- is_wx_native = cache.get(f'wx_native_code_{out_trade_no}') is not None
+ is_wx_native = (
+ cache.get(f'wx_native_pay_{out_trade_no}') is not None
+ or cache.get(f'wx_native_code_{out_trade_no}') is not None
+ )
# 微信 Native 与支付宝 cache 互斥;有 Native 标记时直接走微信查单,避免误走支付宝
if is_alipay and not is_wx_native:
from jituan.services.alipay_pay import query_alipay_trade
diff --git a/jituan/services/wechat_native_pay.py b/jituan/services/wechat_native_pay.py
index 7f3696d..83cbc3f 100644
--- a/jituan/services/wechat_native_pay.py
+++ b/jituan/services/wechat_native_pay.py
@@ -1,7 +1,12 @@
-"""微信 Native 扫码支付(trade_type=NATIVE)。
+"""微信「扫码」支付(相册可用版)。
-与 JSAPI 共用普通商户 mch_id / API 密钥;不走付呗。
-返回 code_url + 二维码图;履约仍走各业务原有 notify_url / fulfill_*。
+关键点:
+- 二维码内容必须是 HTTPS(微信允许相册识别普通链接)
+- 不能把 weixin://wxpay 付款码直接印成图(相册必被「保障支付安全」拦截)
+- 用户扫 HTTPS 后进 H5 → 服务号 OAuth 拿 openid → JSAPI 统一下单
+ → WeixinJSBridge.invoke 直接出密码
+
+本机小程序内仍可用「本机支付」走小程序 JSAPI。
"""
from __future__ import annotations
@@ -9,38 +14,49 @@ import hashlib
import logging
import random
import string
+import time
+from urllib.parse import quote
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_code_'
+CACHE_KEY_PREFIX = 'wx_native_pay_'
CACHE_TTL_SECONDS = 60 * 30
+BRIDGE_URL_BASE = 'https://www.abas.asia/hqhd/dingdan/wx-native-pay/'
def _cache_key(dingdanid: str) -> str:
return f'{CACHE_KEY_PREFIX}{dingdanid}'
-def _native_pay_response(code_url: str) -> dict:
- """二维码直接编码官方 weixin://wxpay/bizpayurl(供另一台手机扫,直接出密码)。
- 本机相册识别付款码会被微信官方拦截,无法伪装;本机付款走小程序 JSAPI。
- """
- from jituan.services.alipay_pay import qr_link_to_base64
- return {
- 'qr_code': code_url,
- 'qr_image': qr_link_to_base64(code_url),
- 'pay_method': PAY_METHOD_WECHAT_NATIVE,
- }
+def build_native_bridge_url(dingdanid: str, club_id=None) -> str:
+ q = f'out_trade_no={quote(dingdanid or "", safe="")}'
+ if club_id:
+ q += f'&club_id={quote(str(club_id), safe="")}'
+ return f'{BRIDGE_URL_BASE}?{q}'
+
+
+def save_native_pay_session(dingdanid, *, jine, body, notify_url, club_id=None):
+ cache.set(
+ _cache_key(dingdanid),
+ {
+ 'jine': str(jine),
+ 'body': (body or '微信支付').strip() or '微信支付',
+ 'notify_url': (notify_url or '').strip(),
+ 'club_id': (club_id or '').strip(),
+ },
+ CACHE_TTL_SECONDS,
+ )
+
+
+def get_native_pay_session(dingdanid: str):
+ data = cache.get(_cache_key(dingdanid))
+ return data if isinstance(data, dict) else None
def generate_wechat_native_pay_params(
@@ -53,11 +69,13 @@ def generate_wechat_native_pay_params(
client_ip='127.0.0.1',
scene='native',
):
- """统一下单 NATIVE,返回 {qr_code, qr_image, pay_method}。
+ """生成「相册可扫」支付参数:HTTPS 中转码 + 会话缓存。
- 同一业务单号未过期时复用 cache 中的 code_url,避免重复点支付产生双码。
- 不需要 openid。第一期不走付呗。
+ 不再调用 Native unifiedorder(weixin:// 码会被相册拦截)。
+ 真实收款在中转页用服务号 JSAPI 完成,notify_url/履约不变。
"""
+ from jituan.services.alipay_pay import qr_link_to_base64
+
dingdanid = (dingdanid or '').strip()
notify_url = (notify_url or '').strip()
body = (body or '微信支付').strip() or '微信支付'
@@ -66,31 +84,63 @@ def generate_wechat_native_pay_params(
if not notify_url:
raise Exception('缺少微信回调地址 notify_url')
- 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)
+ save_native_pay_session(
+ dingdanid,
+ jine=jine,
+ body=body,
+ notify_url=notify_url,
+ 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,
+ )
+ return {
+ # 复制也给 HTTPS,打开后同一套支付
+ 'qr_code': bridge,
+ 'qr_image': qr_link_to_base64(bridge),
+ 'bridge_url': bridge,
+ 'pay_method': PAY_METHOD_WECHAT_NATIVE,
+ }
- pay_cfg = get_wechat_v2_config(club_id)
+
+def build_official_jsapi_pay_params(
+ *,
+ dingdanid,
+ jine,
+ body,
+ notify_url,
+ openid,
+ 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,
+ )
+
+ pay_cfg = get_wechat_v2_config(club_id, app_source='official')
log_wechat_v2_pay_diag(
- pay_cfg, scene=f'native:{scene}', openid='', order_hint=dingdanid,
+ pay_cfg, scene='native_album_jsapi', openid=openid, order_hint=dingdanid,
)
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)}'
+ f'服务号支付配置不完整 club={pay_cfg.get("club_id")} '
+ f'请配置 official_appid 与商户密钥'
)
+ if not openid:
+ raise Exception('缺少服务号 openid')
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'
+ body = (body or '微信支付').strip() or '微信支付'
sign_data = {
'appid': appid,
@@ -101,7 +151,8 @@ def generate_wechat_native_pay_params(
'total_fee': total_fee,
'spbill_create_ip': ip,
'notify_url': notify_url,
- 'trade_type': 'NATIVE',
+ 'trade_type': 'JSAPI',
+ 'openid': openid,
}
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}'
@@ -113,10 +164,11 @@ def generate_wechat_native_pay_params(