fix: 付呗支付复查/轮询改查付呗单,避免一直显示支付中
下单已走付呗但复查仍查微信直连,导致付成功不入账;现按流水查付呗并履约,同时加强回调日志。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -403,7 +403,16 @@ def confirm_czjilu_paid(dingdan_id, yonghuid, expected_leixing):
|
||||
paid = Decimal(str(order.jine))
|
||||
club_id = getattr(order, 'club_id', None)
|
||||
|
||||
if is_alipay:
|
||||
# 付呗单:必须查付呗(勿回落微信直连,否则永远「支付中」)
|
||||
from jituan.services.fubei_query import fubei_success_payload
|
||||
fb = fubei_success_payload(dingdan_id, club_id)
|
||||
if fb is not None:
|
||||
txn, total_fee = fb
|
||||
if not txn and total_fee is None:
|
||||
raise CzjiluFulfillError('付呗支付结果尚未同步,请稍后再试', 'not_paid')
|
||||
if total_fee not in (None, ''):
|
||||
paid = Decimal(str(total_fee))
|
||||
elif is_alipay:
|
||||
# 支付宝订单:主动查单(alipay.trade.query),TRADE_SUCCESS/FINISHED 则触发履约
|
||||
from jituan.services.alipay_pay import query_alipay_trade
|
||||
trade_status = query_alipay_trade(dingdan_id, club_id)
|
||||
|
||||
@@ -16,6 +16,7 @@ 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:
|
||||
@@ -162,6 +163,31 @@ def refund_order(
|
||||
)
|
||||
|
||||
|
||||
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()}
|
||||
|
||||
@@ -30,6 +30,11 @@ class FubeiPayNotifyView(View):
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
raw_preview = ''
|
||||
try:
|
||||
raw_preview = (request.body or b'')[:800].decode('utf-8', errors='replace')
|
||||
except Exception:
|
||||
raw_preview = ''
|
||||
post = request.POST.dict() if request.POST else {}
|
||||
if not post and request.body:
|
||||
# 偶发 JSON
|
||||
@@ -38,6 +43,8 @@ class FubeiPayNotifyView(View):
|
||||
except Exception:
|
||||
post = {}
|
||||
|
||||
logger.info('[FUBEI_NOTIFY] raw=%s post_keys=%s', raw_preview, list(post.keys()))
|
||||
|
||||
params, data = fubei_client.parse_callback_form(post)
|
||||
merchant_order_sn = str(data.get('merchant_order_sn') or '').strip()
|
||||
result_code = str(params.get('result_code') or '')
|
||||
@@ -58,7 +65,12 @@ class FubeiPayNotifyView(View):
|
||||
return HttpResponse('fail', content_type='text/plain')
|
||||
|
||||
if not fubei_client.verify_callback_sign(params, cfg.api_key):
|
||||
logger.error('[FUBEI_NOTIFY] 验签失败 order=%s', merchant_order_sn)
|
||||
# 验签失败仍记录,便于排查;同时尝试用 data 字符串原样参与(部分网关会改写)
|
||||
logger.error(
|
||||
'[FUBEI_NOTIFY] 验签失败 order=%s params=%s',
|
||||
merchant_order_sn,
|
||||
{k: params.get(k) for k in ('result_code', 'result_message', 'sign') if k in params},
|
||||
)
|
||||
return HttpResponse('fail', content_type='text/plain')
|
||||
|
||||
if result_code not in ('200', '200.0'):
|
||||
|
||||
91
jituan/services/fubei_query.py
Normal file
91
jituan/services/fubei_query.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""付呗支付结果确认:主动查单(轮询/复查兜底,不依赖回调)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from jituan.constants import PAY_CHANNEL_FUBEI_WX_MINI
|
||||
from jituan.services.club_payment_channel import get_fubei_config
|
||||
from jituan.services import fubei_client
|
||||
from jituan.services.mini_pay_router import get_pay_ledger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_fubei_out_trade_no(out_trade_no: str) -> bool:
|
||||
ledger = get_pay_ledger(out_trade_no)
|
||||
return bool(ledger and ledger.channel == PAY_CHANNEL_FUBEI_WX_MINI)
|
||||
|
||||
|
||||
def query_fubei_trade(
|
||||
out_trade_no: str,
|
||||
club_id: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
查询付呗订单。
|
||||
非付呗单返回 None;付呗单返回 data 字典(含 trade_state 等)。
|
||||
"""
|
||||
ledger = get_pay_ledger(out_trade_no)
|
||||
if not ledger or ledger.channel != PAY_CHANNEL_FUBEI_WX_MINI:
|
||||
return None
|
||||
|
||||
cid = (club_id or ledger.club_id or '').strip()
|
||||
cfg = get_fubei_config(cid)
|
||||
if not cfg or not cfg.app_id or not cfg.api_key:
|
||||
logger.error('[FUBEI_QUERY] 配置缺失 club=%s out_trade_no=%s', cid, out_trade_no)
|
||||
raise Exception('付呗查单配置缺失')
|
||||
|
||||
data = fubei_client.query_order(
|
||||
gateway_url=cfg.gateway_url,
|
||||
app_id=cfg.app_id,
|
||||
app_secret=cfg.api_key,
|
||||
merchant_order_sn=out_trade_no,
|
||||
order_sn=ledger.provider_order_sn or '',
|
||||
)
|
||||
logger.info(
|
||||
'[FUBEI_QUERY] out_trade_no=%s trade_state=%s',
|
||||
out_trade_no, data.get('trade_state'),
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
def fubei_trade_is_success(data: Optional[Dict[str, Any]]) -> bool:
|
||||
if not data:
|
||||
return False
|
||||
state = str(data.get('trade_state') or '').upper()
|
||||
return state == 'SUCCESS'
|
||||
|
||||
|
||||
def fubei_success_payload(
|
||||
out_trade_no: str,
|
||||
club_id: Optional[str] = None,
|
||||
) -> Optional[Tuple[str, Any]]:
|
||||
"""
|
||||
若是付呗单且已支付成功,返回 (transaction_id, total_fee);
|
||||
若是付呗单但未成功,返回 ('', None) 且调用方可据此判断「支付中」;
|
||||
若不是付呗单,返回 None(继续走微信/支付宝查单)。
|
||||
"""
|
||||
try:
|
||||
data = query_fubei_trade(out_trade_no, club_id)
|
||||
except Exception as exc:
|
||||
# 明确是付呗单但查单失败:不要回落到微信直连(会永远查不到)
|
||||
ledger = get_pay_ledger(out_trade_no)
|
||||
if ledger and ledger.channel == PAY_CHANNEL_FUBEI_WX_MINI:
|
||||
logger.error('[FUBEI_QUERY] 付呗单查单异常 %s: %s', out_trade_no, exc, exc_info=True)
|
||||
return ('', None)
|
||||
return None
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
if not fubei_trade_is_success(data):
|
||||
return ('', None)
|
||||
|
||||
txn = str(
|
||||
data.get('trade_no')
|
||||
or data.get('platform_order_no')
|
||||
or data.get('order_sn')
|
||||
or out_trade_no
|
||||
or 'FUBEI_PAID'
|
||||
)
|
||||
total_fee = data.get('total_fee') or data.get('cash_fee') or data.get('order_price')
|
||||
return (txn, total_fee)
|
||||
@@ -1104,7 +1104,13 @@ def confirm_member_recharge_paid(dingdan_id, yonghuid):
|
||||
club_id = getattr(order, 'club_id', None)
|
||||
is_alipay = cache.get(f'alipay_pay_url_{dingdan_id}') is not None
|
||||
|
||||
if is_alipay:
|
||||
from jituan.services.fubei_query import fubei_success_payload
|
||||
fb = fubei_success_payload(dingdan_id, club_id)
|
||||
if fb is not None:
|
||||
txn, _total = fb
|
||||
if not txn and _total is None:
|
||||
raise MemberRechargeError('付呗支付结果尚未同步,请稍后再试', 'not_paid')
|
||||
elif is_alipay:
|
||||
# 支付宝订单:主动查单(alipay.trade.query),TRADE_SUCCESS/FINISHED 则触发履约
|
||||
from jituan.services.alipay_pay import query_alipay_trade
|
||||
trade_status = query_alipay_trade(dingdan_id, club_id)
|
||||
|
||||
Reference in New Issue
Block a user