Files
Django/jituan/services/wechat_native_pay.py

210 lines
6.3 KiB
Python

"""微信 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"""<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',
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 ''