"""微信 Native 扫码支付(trade_type=NATIVE)。 二维码 = 微信返回的官方付款码 weixin://wxpay/bizpayurl?... 支付结果以商户回调 / 查单为准;小程序侧用轮询确认,不依赖 wx.requestPayment。 """ from __future__ import annotations import hashlib import logging import random import string 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_' SESSION_KEY_PREFIX = 'wx_native_pay_' CACHE_TTL_SECONDS = 60 * 30 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 get_native_pay_session(dingdanid: str): data = cache.get(_session_key(dingdanid)) return data if isinstance(data, dict) else None def _native_pay_response(code_url: str) -> dict: from jituan.services.alipay_pay import qr_link_to_base64 return { 'qr_code': code_url, 'qr_image': qr_link_to_base64(code_url), # 前端「复制支付链接」用同一条官方付款链接 'pay_link': code_url, 'pay_method': PAY_METHOD_WECHAT_NATIVE, } def generate_wechat_native_pay_params( *, dingdanid, jine, body, notify_url, club_id=None, client_ip='127.0.0.1', scene='native', ): """统一下单 NATIVE,返回官方付款码 + 可复制链接。""" dingdanid = (dingdanid or '').strip() notify_url = (notify_url or '').strip() body = (body or '微信支付').strip() or '微信支付' if not dingdanid: raise Exception('订单号不能为空') if not notify_url: raise Exception('缺少微信回调地址 notify_url') 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] reuse code_url dingdan=%s club=%s scene=%s', dingdanid, club_id, scene, ) return _native_pay_response(cached_url) pay_cfg = get_wechat_v2_config(club_id) log_wechat_v2_pay_diag( 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 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""" {appid} {body} {mch_id} {nonce_str} {notify_url} {dingdanid} {ip} {total_fee} NATIVE {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=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', dingdanid, pay_cfg.get('club_id'), mch_id, scene, ) return _native_pay_response(code_url) # 旧 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="")}' if club_id: q += f'&club_id={quote(str(club_id), safe="")}' return f'https://www.abas.asia/hqhd/dingdan/wx-native-pay/?{q}' def build_official_jsapi_pay_params( *, dingdanid, jine, body, notify_url='', openid, club_id=None, client_ip='127.0.0.1', ): """服务号 JSAPI 统一下单(H5 / 相册中转页)。notify_url 由订单侧 settings 接管。""" from orders.views.payment import CreateOrderView helper = CreateOrderView() return helper.generate_wechat_pay_params( dingdanid=dingdanid, jine=jine, beizhu=(body or '微信支付')[:40] or '微信支付', user_ip=client_ip or '127.0.0.1', openid=openid, club_id=club_id, app_source='official', ) def build_mini_jsapi_pay_params(**kwargs): raise Exception('小程序请走 CreateOrderView.generate_wechat_pay_params(app_source=mini)') def build_h5_jsapi_bridge_url(dingdanid: str, club_id=None, return_url='') -> str: from urllib.parse import quote q = f'out_trade_no={quote(dingdanid or "", safe="")}' if club_id: q += f'&club_id={quote(str(club_id), safe="")}' if return_url: q += f'&return_url={quote(str(return_url), safe="")}' return f'https://www.abas.asia/hqhd/dingdan/wx-h5-pay/?{q}' def resolve_scene_to_dingdanid(scene: str) -> str: return ''