diff --git a/jituan/services/wechat_native_pay.py b/jituan/services/wechat_native_pay.py
index f55adb5..e9158fd 100644
--- a/jituan/services/wechat_native_pay.py
+++ b/jituan/services/wechat_native_pay.py
@@ -1,22 +1,14 @@
-"""微信扫码支付:小程序码(本机相册可扫,直接弹密码)。
+"""微信 Native 扫码支付(trade_type=NATIVE)。
-为什么不用 weixin:// 付款码:
-- 那才是「另一台手机摄像头扫 → 瞬间出密码」的官方 Native
-- 但微信禁止「扫一扫 → 相册」识别付款码(保障支付安全)
-
-为什么改用小程序码(微信官方文档也写了可用小程序码替代收款码):
-- 相册可以识别小程序码
-- 扫完直接进小程序支付页 → wx.requestPayment 出密码
-- 用户体感就是:相册一扫,马上出支付,没有乱七八糟的 H5 网页
+二维码 = 微信返回的官方付款码 weixin://wxpay/bizpayurl?...
+支付结果以商户回调 / 查单为准;小程序侧用轮询确认,不依赖 wx.requestPayment。
"""
from __future__ import annotations
-import base64
import hashlib
import logging
import random
import string
-import time
import requests
from django.core.cache import cache
@@ -31,107 +23,35 @@ from utils.money import yuan_to_fen
logger = logging.getLogger(__name__)
+CACHE_KEY_PREFIX = 'wx_native_code_'
SESSION_KEY_PREFIX = 'wx_native_pay_'
-SCENE_KEY_PREFIX = 'wx_native_scene_'
CACHE_TTL_SECONDS = 60 * 30
-# getwxacodeunlimit scene ≤ 32 字节;前缀 + 随机
-SCENE_PREFIX = 'np'
-PAY_PAGE = 'pages/wx-scan-pay/wx-scan-pay'
+
+
+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 _scene_key(scene: str) -> str:
- return f'{SCENE_KEY_PREFIX}{scene}'
-
-
-def save_native_pay_session(dingdanid, *, jine, body, notify_url, club_id=None, scene=''):
- cache.set(
- _session_key(dingdanid),
- {
- 'jine': str(jine),
- 'body': (body or '微信支付').strip() or '微信支付',
- 'notify_url': (notify_url or '').strip(),
- 'club_id': (club_id or '').strip(),
- 'scene': (scene or '').strip(),
- 'dingdanid': (dingdanid or '').strip(),
- },
- CACHE_TTL_SECONDS,
- )
-
-
def get_native_pay_session(dingdanid: str):
data = cache.get(_session_key(dingdanid))
return data if isinstance(data, dict) else None
-def resolve_scene_to_dingdanid(scene: str) -> str:
- scene = (scene or '').strip()
- if not scene:
- return ''
- dingdanid = cache.get(_scene_key(scene)) or ''
- return str(dingdanid).strip()
+def _native_pay_response(code_url: str) -> dict:
+ from jituan.services.alipay_pay import qr_link_to_base64
-
-def _new_scene() -> str:
- # np + 22 位 = 24,远小于 32
- rnd = ''.join(random.choices(string.ascii_letters + string.digits, k=22))
- return f'{SCENE_PREFIX}{rnd}'
-
-
-def _png_to_data_url(png_bytes: bytes) -> str:
- b64 = base64.b64encode(png_bytes).decode('ascii')
- return f'data:image/png;base64,{b64}'
-
-
-def _fetch_mini_program_code_png(*, club_id: str, scene: str) -> bytes:
- from utils.weixin_token import get_club_mini_access_token, is_weixin_token_invalid
-
- access_token = get_club_mini_access_token(club_id)
- if not access_token:
- raise Exception(f'俱乐部 {club_id} 获取小程序 access_token 失败,无法生成支付码')
-
- post_data = {
- 'scene': scene,
- 'page': PAY_PAGE,
- 'width': 430,
- 'auto_color': False,
- 'line_color': {'r': 0, 'g': 0, 'b': 0},
- 'check_path': False,
+ return {
+ 'qr_code': code_url,
+ 'qr_image': qr_link_to_base64(code_url),
+ # 前端「复制支付链接」用同一条官方付款链接
+ 'pay_link': code_url,
+ 'pay_method': PAY_METHOD_WECHAT_NATIVE,
}
- def _req(tok: str):
- url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={tok}'
- return requests.post(url, json=post_data, timeout=15)
-
- wx_resp = _req(access_token)
- if wx_resp.headers.get('content-type', '').startswith('application/json'):
- try:
- err = wx_resp.json()
- if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
- access_token = get_club_mini_access_token(club_id, force_refresh=True)
- if access_token:
- wx_resp = _req(access_token)
- except Exception:
- pass
-
- content_type = wx_resp.headers.get('content-type', '')
- if wx_resp.status_code != 200 or 'image' not in content_type:
- errmsg = '未知错误'
- try:
- error_info = wx_resp.json()
- errmsg = error_info.get('errmsg', errmsg)
- logger.error('[WX_NATIVE_MINICODE] fail club=%s err=%s', club_id, error_info)
- except Exception:
- logger.error(
- '[WX_NATIVE_MINICODE] fail club=%s status=%s body=%s',
- club_id, wx_resp.status_code, wx_resp.content[:300],
- )
- raise Exception(f'生成小程序支付码失败: {errmsg}')
- return wx_resp.content
-
def generate_wechat_native_pay_params(
*,
@@ -143,110 +63,51 @@ def generate_wechat_native_pay_params(
client_ip='127.0.0.1',
scene='native',
):
- """生成「相册可扫」小程序码二维码。"""
+ """统一下单 NATIVE,返回官方付款码 + 可复制链接。"""
dingdanid = (dingdanid or '').strip()
notify_url = (notify_url or '').strip()
body = (body or '微信支付').strip() or '微信支付'
- club_id = (club_id or '').strip()
if not dingdanid:
raise Exception('订单号不能为空')
if not notify_url:
raise Exception('缺少微信回调地址 notify_url')
- if not club_id:
- raise Exception('缺少 club_id,无法生成小程序支付码')
- cached = get_native_pay_session(dingdanid) or {}
- cached_img = (cached.get('qr_image') or '').strip()
- cached_scene = (cached.get('scene') or '').strip()
- if cached_img and cached_scene and resolve_scene_to_dingdanid(cached_scene) == dingdanid:
+ cache.set(
+ _session_key(dingdanid),
+ {
+ 'jine': str(jine),
+ 'body': body,
+ 'notify_url': notify_url,
+ 'club_id': (club_id or '').strip(),
+ },
+ CACHE_TTL_SECONDS,
+ )
+
+ cached = cache.get(_cache_key(dingdanid)) or {}
+ cached_url = (cached.get('code_url') or '').strip()
+ if cached_url:
logger.info(
- '[WX_NATIVE_MINICODE] reuse dingdan=%s club=%s scene=%s',
- dingdanid, club_id, cached_scene,
+ '[WX_NATIVE] reuse code_url dingdan=%s club=%s scene=%s',
+ dingdanid, club_id, scene,
)
- return {
- 'qr_code': f'minicode:{cached_scene}',
- 'qr_image': cached_img,
- 'scene': cached_scene,
- 'pay_method': PAY_METHOD_WECHAT_NATIVE,
- }
+ return _native_pay_response(cached_url)
- pay_scene = _new_scene()
- png = _fetch_mini_program_code_png(club_id=club_id, scene=pay_scene)
- qr_image = _png_to_data_url(png)
-
- save_native_pay_session(
- dingdanid,
- jine=jine,
- body=body,
- notify_url=notify_url,
- club_id=club_id,
- scene=pay_scene,
- )
- # 把图片也塞进 session,便于复用
- sess = get_native_pay_session(dingdanid) or {}
- sess['qr_image'] = qr_image
- cache.set(_session_key(dingdanid), sess, CACHE_TTL_SECONDS)
- cache.set(_scene_key(pay_scene), dingdanid, CACHE_TTL_SECONDS)
-
- logger.warning(
- '[WX_NATIVE_MINICODE] ok dingdan=%s club=%s biz_scene=%s pay_scene=%s',
- dingdanid, club_id, scene, pay_scene,
- )
- return {
- 'qr_code': f'minicode:{pay_scene}',
- 'qr_image': qr_image,
- 'scene': pay_scene,
- 'pay_method': PAY_METHOD_WECHAT_NATIVE,
- }
-
-
-def build_mini_jsapi_pay_params(
- *,
- dingdanid,
- jine,
- body,
- notify_url,
- openid,
- club_id=None,
- client_ip='127.0.0.1',
-):
- """小程序 AppID + 小程序 openid → JSAPI,供相册扫码落地页调起密码。"""
- from jituan.constants import FUBEI_BIZ_CZJILU, FUBEI_BIZ_ORDER
- from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
-
- pay_cfg = get_wechat_v2_config(club_id, app_source='mini')
+ pay_cfg = get_wechat_v2_config(club_id)
log_wechat_v2_pay_diag(
- pay_cfg, scene='native_minicode_jsapi', openid=openid, order_hint=dingdanid,
+ pay_cfg, scene=f'native:{scene}', 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 openid:
- raise Exception('缺少小程序 openid')
-
- biz = FUBEI_BIZ_CZJILU if str(dingdanid).startswith('Cz') or str(dingdanid).startswith('FK') or str(dingdanid).startswith('KH') else FUBEI_BIZ_ORDER
- fb = maybe_create_fubei_jsapi_params(
- club_id=club_id,
- out_trade_no=dingdanid,
- amount_yuan=jine,
- body=body or '微信支付',
- openid=openid,
- notify_url=notify_url,
- biz_type=biz,
- )
- if fb is not None:
- return fb
-
if not all([appid, mch_id, key]):
raise Exception(
- f'小程序支付配置不完整 club={pay_cfg.get("club_id")} '
+ 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'
- body = (body or '微信支付').strip() or '微信支付'
sign_data = {
'appid': appid,
@@ -257,8 +118,7 @@ def build_mini_jsapi_pay_params(
'total_fee': total_fee,
'spbill_create_ip': ip,
'notify_url': notify_url,
- 'trade_type': 'JSAPI',
- 'openid': openid,
+ '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}'
@@ -270,11 +130,10 @@ def build_mini_jsapi_pay_params(
{mch_id}
{nonce_str}
{notify_url}
-{openid}
{dingdanid}
{ip}
{total_fee}
-JSAPI
+NATIVE
{sign}
"""
@@ -286,37 +145,19 @@ def build_mini_jsapi_pay_params(
)
parsed = parse_wechat_v2_unifiedorder_response(
response.content,
- log_prefix='WX_NATIVE_MINICODE_JSAPI',
- expect='prepay_id',
+ log_prefix=f'WX_NATIVE_{scene}',
+ expect='code_url',
)
- prepay_id = parsed['prepay_id']
- timestamp = str(int(time.time()))
- pay_nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
- package = f'prepay_id={prepay_id}'
- sign_type = 'MD5'
- pay_sign_data = {
- 'appId': appid,
- 'timeStamp': timestamp,
- 'nonceStr': pay_nonce,
- 'package': package,
- 'signType': sign_type,
- }
- pay_sign_string = '&'.join(
- f'{k}={pay_sign_data[k]}' for k in sorted(pay_sign_data.keys())
- ) + f'&key={key}'
- pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
-
- return {
- 'appId': appid,
- 'timeStamp': timestamp,
- 'nonceStr': pay_nonce,
- 'package': package,
- 'signType': sign_type,
- 'paySign': pay_sign,
- }
+ 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',
+ dingdanid, pay_cfg.get('club_id'), mch_id, scene,
+ )
+ return _native_pay_response(code_url)
-# 兼容旧 H5 中转页导入(若仍有请求)
+# 旧 H5 / 小程序码落地页仍可能 import,保留最小兼容,避免 ImportError
def build_native_bridge_url(dingdanid: str, club_id=None) -> str:
from urllib.parse import quote
q = f'out_trade_no={quote(dingdanid or "", safe="")}'
@@ -326,89 +167,12 @@ def build_native_bridge_url(dingdanid: str, club_id=None) -> str:
def build_official_jsapi_pay_params(**kwargs):
- """保留给旧 H5 页;新主路径已改小程序码。"""
- kwargs = dict(kwargs)
- # 复用 mini 逻辑会错 openid 体系;仍走服务号
- from jituan.services.wechat_pay import get_wechat_v2_config as _cfg
- pay_cfg = _cfg(kwargs.get('club_id'), app_source='official')
- log_wechat_v2_pay_diag(
- pay_cfg,
- scene='native_album_jsapi_legacy',
- openid=kwargs.get('openid'),
- order_hint=kwargs.get('dingdanid'),
- )
- appid = pay_cfg.get('appid') or ''
- mch_id = pay_cfg.get('mch_id') or ''
- key = pay_cfg.get('key') or ''
- openid = kwargs.get('openid') or ''
- if not all([appid, mch_id, key, openid]):
- raise Exception('服务号支付配置不完整')
- dingdanid = kwargs.get('dingdanid')
- jine = kwargs.get('jine')
- body = kwargs.get('body') or '微信支付'
- notify_url = kwargs.get('notify_url')
- client_ip = kwargs.get('client_ip') or '127.0.0.1'
- 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': '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}'
- sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
- xml_data = f"""
-{appid}
-{body}
-{mch_id}
-{nonce_str}
-{notify_url}
-{openid}
-{dingdanid}
-{ip}
-{total_fee}
-JSAPI
-{sign}
-"""
- 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='WX_NATIVE_OA_JSAPI', expect='prepay_id',
- )
- prepay_id = parsed['prepay_id']
- timestamp = str(int(time.time()))
- pay_nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
- package = f'prepay_id={prepay_id}'
- sign_type = 'MD5'
- pay_sign_data = {
- 'appId': appid,
- 'timeStamp': timestamp,
- 'nonceStr': pay_nonce,
- 'package': package,
- 'signType': sign_type,
- }
- pay_sign_string = '&'.join(
- f'{k}={pay_sign_data[k]}' for k in sorted(pay_sign_data.keys())
- ) + f'&key={key}'
- pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper()
- return {
- 'appId': appid,
- 'timeStamp': timestamp,
- 'nonceStr': pay_nonce,
- 'package': package,
- 'signType': sign_type,
- 'paySign': pay_sign,
- }
+ raise Exception('已改为官方 Native 付款码,不再走服务号 H5 JSAPI')
+
+
+def build_mini_jsapi_pay_params(**kwargs):
+ raise Exception('已改为官方 Native 付款码,不再走小程序码 JSAPI')
+
+
+def resolve_scene_to_dingdanid(scene: str) -> str:
+ return ''
diff --git a/orders/views/payment.py b/orders/views/payment.py
index ffc296b..daac006 100644
--- a/orders/views/payment.py
+++ b/orders/views/payment.py
@@ -652,80 +652,15 @@ def wx_native_pay_page(request):
class WxNativeScanPayView(APIView):
- """相册扫小程序码落地:用当前用户 openid 对未付款单做 JSAPI,直接出密码。"""
+ """已废弃:现统一走 Native 付款码 + 复制链接,不再用小程序码落地支付。"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
- from jituan.services.wechat_native_pay import (
- build_mini_jsapi_pay_params,
- get_native_pay_session,
- resolve_scene_to_dingdanid,
- )
-
- scene = (request.data.get('scene') or '').strip()
- dingdanid = (request.data.get('dingdanid') or request.data.get('out_trade_no') or '').strip()
- if scene and not dingdanid:
- dingdanid = resolve_scene_to_dingdanid(scene)
- if not dingdanid:
- return Response({'code': 400, 'message': '无效支付码'}, status=status.HTTP_400_BAD_REQUEST)
-
- session = get_native_pay_session(dingdanid)
- if not session:
- return Response({'code': 400, 'message': '支付已过期,请返回重新发起'}, status=status.HTTP_400_BAD_REQUEST)
-
- club_id = (session.get('club_id') or '').strip()
- notify_url = (session.get('notify_url') or '').strip()
- if not notify_url:
- if dingdanid.startswith('CzHY'):
- notify_url = getattr(settings, 'HUIYUAN_PAY_NOTIFY_URL', '')
- elif dingdanid.startswith('CzYJ'):
- notify_url = getattr(settings, 'YAJIN_PAY_NOTIFY_URL', '')
- elif dingdanid.startswith('CzJF'):
- notify_url = getattr(settings, 'JIFEN_PAY_NOTIFY_URL', '')
- elif dingdanid.startswith('CzSJ'):
- notify_url = getattr(settings, 'SHANGJIA_CZ_PAY_NOTIFY_URL', '')
- elif dingdanid.startswith('FK'):
- notify_url = getattr(settings, 'FAKUAN_PAY_NOTIFY_URL', '')
- elif dingdanid.startswith('KH'):
- notify_url = getattr(settings, 'KAOHE_PAY_NOTIFY_URL', '')
- elif dingdanid.startswith('CzZP'):
- notify_url = getattr(settings, 'ZHUANPAN_PAY_NOTIFY_URL', '')
- else:
- notify_url = getattr(settings, 'WEIXIN_NOTIFY_URL', '')
-
- pay_openid, _ = get_payment_openid(request, club_id=club_id or None)
- if not pay_openid:
- return Response({
- 'code': 400,
- 'message': '当前小程序未绑定微信,请退出后重新登录再支付',
- }, status=status.HTTP_400_BAD_REQUEST)
-
- 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_params = build_mini_jsapi_pay_params(
- dingdanid=dingdanid,
- jine=session.get('jine'),
- body=session.get('body') or '微信支付',
- notify_url=notify_url,
- openid=pay_openid,
- club_id=club_id or None,
- client_ip=client_ip,
- )
- except Exception as exc:
- logger.exception('小程序码扫码 JSAPI 下单失败 dingdan=%s', dingdanid)
- return Response({
- 'code': 500,
- 'message': f'下单失败:{exc}',
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
return Response({
- 'code': 200,
- 'message': 'ok',
- 'dingdanid': dingdanid,
- 'payParams': pay_params,
- })
+ 'code': 410,
+ 'message': '请返回使用付款码扫码或复制支付链接支付',
+ }, status=status.HTTP_410_GONE)
class PaymentVerifyView(APIView):