Files
Django/jituan/services/fubei_client.py
XingQue b19430213a fix: 付呗支付复查/轮询改查付呗单,避免一直显示支付中
下单已走付呗但复查仍查微信直连,导致付成功不入账;现按流水查付呗并履约,同时加强回调日志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 20:50:14 +08:00

228 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""付呗开放平台客户端:签名、小程序下单、退款、回调验签。"""
from __future__ import annotations
import hashlib
import json
import logging
import random
import string
import time
from decimal import Decimal, ROUND_HALF_UP
from typing import Any, Dict, Optional, Tuple
import requests
logger = logging.getLogger(__name__)
METHOD_MINA = 'openapi.payment.order.mina'
METHOD_REFUND = 'openapi.payment.order.refund'
METHOD_QUERY = 'openapi.payment.order.query'
def _yuan_str(amount) -> str:
d = Decimal(str(amount)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return format(d, 'f')
def build_sign(params: Dict[str, Any], app_secret: str) -> str:
"""付呗签名:参数 ASCII 排序后 key=value&... 再无缝拼接 secretMD5 大写。"""
items = []
for k in sorted(params.keys()):
if k == 'sign':
continue
v = params[k]
if v is None or v == '':
continue
items.append(f'{k}={v}')
raw = '&'.join(items) + (app_secret or '')
return hashlib.md5(raw.encode('utf-8')).hexdigest().upper()
def verify_callback_sign(params: Dict[str, Any], app_secret: str) -> bool:
got = (params.get('sign') or '').strip().upper()
if not got:
return False
expect = build_sign(params, app_secret)
return got == expect
def _nonce(n: int = 24) -> str:
return ''.join(random.choices(string.ascii_letters + string.digits, k=n))
def call_fubei_api(
*,
gateway_url: str,
app_id: str,
app_secret: str,
method: str,
biz: Dict[str, Any],
timeout: int = 15,
) -> Dict[str, Any]:
"""调用付呗 gateway成功返回 data 字典;失败抛 Exception。"""
biz_content = json.dumps(biz, ensure_ascii=False, separators=(',', ':'))
payload = {
'app_id': app_id,
'method': method,
'format': 'json',
'sign_method': 'md5',
'nonce': _nonce(),
'version': '1.0',
'biz_content': biz_content,
}
payload['sign'] = build_sign(payload, app_secret)
url = (gateway_url or '').strip() or 'https://shq-api.51fubei.com/gateway'
logger.info('[FUBEI] method=%s app_id=%s biz_keys=%s', method, app_id, list(biz.keys()))
resp = requests.post(url, json=payload, timeout=timeout, headers={'Content-Type': 'application/json; charset=utf-8'})
try:
body = resp.json()
except Exception:
logger.error('[FUBEI] 非JSON响应 status=%s body=%s', resp.status_code, resp.text[:500])
raise Exception(f'付呗接口异常: HTTP {resp.status_code}')
result_code = body.get('result_code')
if str(result_code) not in ('200', '200.0'):
msg = body.get('result_message') or body.get('sub_code') or '付呗下单失败'
logger.warning('[FUBEI] 业务失败 method=%s code=%s msg=%s body=%s', method, result_code, msg, body)
raise Exception(str(msg))
data = body.get('data')
if isinstance(data, str):
try:
data = json.loads(data)
except Exception:
pass
if not isinstance(data, dict):
raise Exception('付呗返回 data 格式异常')
return data
def mina_prepay(
*,
gateway_url: str,
app_id: str,
app_secret: str,
merchant_order_sn: str,
sub_openid: str,
total_fee,
store_id: int,
call_back_url: str,
body: str = '',
attach: str = '',
) -> Dict[str, Any]:
"""小程序支付下单,返回含 sign_params / order_sn 等。"""
biz = {
'merchant_order_sn': merchant_order_sn,
'sub_openid': sub_openid,
'total_fee': float(_yuan_str(total_fee)),
'store_id': int(store_id),
}
if call_back_url:
biz['call_back_url'] = call_back_url
if body:
biz['body'] = (body or '')[:128]
if attach:
biz['attach'] = (attach or '')[:127]
return call_fubei_api(
gateway_url=gateway_url,
app_id=app_id,
app_secret=app_secret,
method=METHOD_MINA,
biz=biz,
)
def refund_order(
*,
gateway_url: str,
app_id: str,
app_secret: str,
merchant_order_sn: str,
merchant_refund_sn: str,
refund_money=None,
order_sn: str = '',
) -> Dict[str, Any]:
biz: Dict[str, Any] = {
'merchant_refund_sn': merchant_refund_sn,
}
if merchant_order_sn:
biz['merchant_order_sn'] = merchant_order_sn
if order_sn:
biz['order_sn'] = order_sn
if refund_money is not None:
biz['refund_money'] = float(_yuan_str(refund_money))
return call_fubei_api(
gateway_url=gateway_url,
app_id=app_id,
app_secret=app_secret,
method=METHOD_REFUND,
biz=biz,
)
def query_order(
*,
gateway_url: str,
app_id: str,
app_secret: str,
merchant_order_sn: str = '',
order_sn: str = '',
) -> Dict[str, Any]:
"""订单查询。trade_state=SUCCESS 表示已支付。"""
biz: Dict[str, Any] = {}
if merchant_order_sn:
biz['merchant_order_sn'] = merchant_order_sn
if order_sn:
biz['order_sn'] = order_sn
if not biz:
raise Exception('付呗查单缺少订单号')
return call_fubei_api(
gateway_url=gateway_url,
app_id=app_id,
app_secret=app_secret,
method=METHOD_QUERY,
biz=biz,
)
def parse_callback_form(post_dict: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""解析付呗 form 回调,返回 (顶层参数含 sign, data 业务字典)。"""
params = {k: (v if not isinstance(v, list) else (v[0] if v else '')) for k, v in post_dict.items()}
# 兼容 QueryDict.dict()
data_raw = params.get('data') or ''
data: Dict[str, Any] = {}
if isinstance(data_raw, dict):
data = data_raw
elif isinstance(data_raw, str) and data_raw.strip():
try:
data = json.loads(data_raw)
except Exception:
# 偶发双重编码
try:
data = json.loads(data_raw.replace("'", '"'))
except Exception:
logger.error('[FUBEI_NOTIFY] data 解析失败: %s', data_raw[:300])
data = {}
return params, data
def extract_jsapi_pay_params(sign_params: Dict[str, Any]) -> Dict[str, str]:
"""从付呗 sign_params 抽出小程序 wx.requestPayment 所需字段。"""
if not sign_params:
raise Exception('付呗未返回支付签名包')
# 部分字段可能混有 store_id 等,只取调起支付字段
out = {
'appId': str(sign_params.get('appId') or sign_params.get('appid') or ''),
'timeStamp': str(sign_params.get('timeStamp') or sign_params.get('timestamp') or ''),
'nonceStr': str(sign_params.get('nonceStr') or sign_params.get('noncestr') or ''),
'package': str(sign_params.get('package') or ''),
'signType': str(sign_params.get('signType') or sign_params.get('sign_type') or 'MD5'),
'paySign': str(sign_params.get('paySign') or sign_params.get('pay_sign') or ''),
}
if not out['timeStamp'] or not out['package'] or not out['paySign']:
raise Exception('付呗签名包字段不完整')
return out