From b19430213ae2e3f897d64ac01d4a861fa15f7428 Mon Sep 17 00:00:00 2001 From: XingQue Date: Tue, 21 Jul 2026 20:50:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BB=98=E5=91=97=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E5=A4=8D=E6=9F=A5/=E8=BD=AE=E8=AF=A2=E6=94=B9=E6=9F=A5?= =?UTF-8?q?=E4=BB=98=E5=91=97=E5=8D=95=EF=BC=8C=E9=81=BF=E5=85=8D=E4=B8=80?= =?UTF-8?q?=E7=9B=B4=E6=98=BE=E7=A4=BA=E6=94=AF=E4=BB=98=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 下单已走付呗但复查仍查微信直连,导致付成功不入账;现按流水查付呗并履约,同时加强回调日志。 Co-authored-by: Cursor --- jituan/services/czjilu_fulfill.py | 11 +++- jituan/services/fubei_client.py | 26 +++++++++ jituan/services/fubei_notify.py | 14 ++++- jituan/services/fubei_query.py | 91 ++++++++++++++++++++++++++++++ jituan/services/member_recharge.py | 8 ++- orders/views/payment.py | 12 +++- 6 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 jituan/services/fubei_query.py diff --git a/jituan/services/czjilu_fulfill.py b/jituan/services/czjilu_fulfill.py index 955c66f..b764ff2 100644 --- a/jituan/services/czjilu_fulfill.py +++ b/jituan/services/czjilu_fulfill.py @@ -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) diff --git a/jituan/services/fubei_client.py b/jituan/services/fubei_client.py index 6d88fbb..197b0e2 100644 --- a/jituan/services/fubei_client.py +++ b/jituan/services/fubei_client.py @@ -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()} diff --git a/jituan/services/fubei_notify.py b/jituan/services/fubei_notify.py index 676981d..13dc980 100644 --- a/jituan/services/fubei_notify.py +++ b/jituan/services/fubei_notify.py @@ -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'): diff --git a/jituan/services/fubei_query.py b/jituan/services/fubei_query.py new file mode 100644 index 0000000..bd42358 --- /dev/null +++ b/jituan/services/fubei_query.py @@ -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) diff --git a/jituan/services/member_recharge.py b/jituan/services/member_recharge.py index 9253433..e46d223 100644 --- a/jituan/services/member_recharge.py +++ b/jituan/services/member_recharge.py @@ -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) diff --git a/orders/views/payment.py b/orders/views/payment.py index dc15b43..349694f 100644 --- a/orders/views/payment.py +++ b/orders/views/payment.py @@ -580,7 +580,17 @@ class PaymentVerifyView(APIView): trade_state = None transaction_id = None - if is_alipay: + # 付呗单优先:勿用微信直连 orderquery(查不到会一直「支付中」) + from jituan.services.fubei_query import fubei_success_payload + fb = fubei_success_payload(dingdanid, getattr(dingdan, 'ClubID', None)) + if fb is not None: + txn, _fee = fb + if txn or _fee is not None: + trade_state = 'SUCCESS' + transaction_id = txn or None + else: + return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}}) + elif is_alipay: # 支付宝订单:主动查单(alipay.trade.query) from jituan.services.alipay_pay import query_alipay_trade alipay_status = query_alipay_trade(dingdanid, getattr(dingdan, 'ClubID', None))