210 lines
6.3 KiB
Python
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 ''
|