From dacd55bb1eaee162924f90e2e3de79ece99cfd07 Mon Sep 17 00:00:00 2001 From: XingQue Date: Tue, 21 Jul 2026 20:57:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E4=BB=98=E5=91=97/?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E7=9B=B4=E8=BF=9E=E5=AE=98=E6=96=B9=E6=9F=A5?= =?UTF-8?q?=E5=8D=95=E7=A1=AE=E8=AE=A4=EF=BC=8C=E8=BD=AE=E8=AF=A2=E5=B9=82?= =?UTF-8?q?=E7=AD=89=E5=85=A5=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复查与充值轮询按通道查官方单:付呗单查付呗、直连查微信;履约仍幂等防多加,未完成返回业务码便于继续轮询。 Co-authored-by: Cursor --- jituan/services/czjilu_fulfill.py | 41 +++----------- jituan/services/fubei_query.py | 83 ++++++++++++++++++++-------- jituan/services/member_recharge.py | 35 +++--------- jituan/services/pay_confirm.py | 88 ++++++++++++++++++++++++++++++ orders/views/payment.py | 47 ++++------------ products/views/chongzhi_payment.py | 4 +- products/views/huiyuan_payment.py | 4 +- products/views/jifen_payment.py | 4 +- products/views/yajin_payment.py | 4 +- 9 files changed, 185 insertions(+), 125 deletions(-) create mode 100644 jituan/services/pay_confirm.py diff --git a/jituan/services/czjilu_fulfill.py b/jituan/services/czjilu_fulfill.py index b764ff2..945a0c4 100644 --- a/jituan/services/czjilu_fulfill.py +++ b/jituan/services/czjilu_fulfill.py @@ -372,9 +372,7 @@ def fulfill_shangjia(dingdan_id, paid_amount_yuan=None): def confirm_czjilu_paid(dingdan_id, yonghuid, expected_leixing): - """轮询确认:支付宝订单直接看本地状态,微信订单查微信 SUCCESS 后幂等履约。""" - from django.core.cache import cache - + """轮询确认:付呗/微信/支付宝官方查单成功后幂等履约(防多加)。""" try: order = Czjilu.query.get(dingdan_id=dingdan_id, yonghuid=yonghuid) except Czjilu.DoesNotExist: @@ -398,38 +396,17 @@ def confirm_czjilu_paid(dingdan_id, yonghuid, expected_leixing): if not is_czjilu_fulfillable_status(order.zhuangtai): raise CzjiluFulfillError(f'订单状态异常: {order.zhuangtai}', 'status_error') - # 主动查单确认支付状态 - is_alipay = cache.get(f'alipay_pay_url_{dingdan_id}') is not None paid = Decimal(str(order.jine)) club_id = getattr(order, 'club_id', None) - # 付呗单:必须查付呗(勿回落微信直连,否则永远「支付中」) - 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) - if trade_status not in ('TRADE_SUCCESS', 'TRADE_FINISHED'): - raise CzjiluFulfillError('支付宝支付结果尚未同步,请稍后再试', 'not_paid') - else: - # 微信订单:主动查单 - from jituan.services.member_recharge import query_wechat_v2_trade_state - trade_state, _ = query_wechat_v2_trade_state(dingdan_id, club_id) - if trade_state != 'SUCCESS': - # 兜底:cache 标记可能已过期(30分钟),尝试查支付宝确认 - from jituan.services.alipay_pay import query_alipay_trade - alipay_status = query_alipay_trade(dingdan_id, club_id) - if alipay_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'): - logger.info('cache 过期但支付宝查单成功 dingdan=%s', dingdan_id) - else: - raise CzjiluFulfillError('微信侧尚未支付成功,请稍后再试', 'not_paid') + from jituan.services.pay_confirm import confirm_remote_payment + remote = confirm_remote_payment(dingdan_id, club_id, prefer_amount=paid) + if not remote.get('ok'): + if remote.get('pending'): + raise CzjiluFulfillError('支付结果尚未同步,请稍后再试', 'not_paid') + raise CzjiluFulfillError(remote.get('msg') or '支付未成功', 'not_paid') + if remote.get('amount') is not None: + paid = Decimal(str(remote['amount'])) # 查单确认支付成功,执行履约(fulfill_* 内行锁+仅 9/2→3,防多加) if expected_leixing == 1: diff --git a/jituan/services/fubei_query.py b/jituan/services/fubei_query.py index bd42358..13897d7 100644 --- a/jituan/services/fubei_query.py +++ b/jituan/services/fubei_query.py @@ -1,13 +1,23 @@ -"""付呗支付结果确认:主动查单(轮询/复查兜底,不依赖回调)。""" +"""付呗支付结果确认:主动查单(轮询/复查兜底,不依赖回调)。 + +规则: +- 流水表标明付呗 → 只查付呗(绝不误查微信直连) +- 无流水且俱乐部当前为付呗 → 先尝试付呗查单;查无单再回落微信 +- 其它 → 返回 None,由调用方走微信/支付宝 +""" from __future__ import annotations import logging from typing import Any, Dict, Optional, Tuple -from jituan.constants import PAY_CHANNEL_FUBEI_WX_MINI +from jituan.constants import ( + CLUB_ID_DEFAULT, + MINI_PAY_CHANNEL_FUBEI, + 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 +from jituan.services.mini_pay_router import get_club_mini_pay_channel, get_pay_ledger logger = logging.getLogger(__name__) @@ -17,34 +27,55 @@ def is_fubei_out_trade_no(out_trade_no: str) -> bool: return bool(ledger and ledger.channel == PAY_CHANNEL_FUBEI_WX_MINI) +def _should_try_fubei(out_trade_no: str, club_id: Optional[str]) -> Tuple[bool, str, str]: + """ + 返回 (是否应付呗查单, club_id, provider_order_sn)。 + 有付呗流水 → 强制付呗;无流水但俱乐部选用付呗 → 尝试付呗。 + """ + cid = (club_id or '').strip() or CLUB_ID_DEFAULT + ledger = get_pay_ledger(out_trade_no) + if ledger and ledger.channel == PAY_CHANNEL_FUBEI_WX_MINI: + return True, (ledger.club_id or cid), (ledger.provider_order_sn or '') + if ledger: + # 明确记了其它通道,不查付呗 + return False, cid, '' + if get_club_mini_pay_channel(cid) == MINI_PAY_CHANNEL_FUBEI: + return True, cid, '' + return False, cid, '' + + def query_fubei_trade( out_trade_no: str, club_id: Optional[str] = None, ) -> Optional[Dict[str, Any]]: """ 查询付呗订单。 - 非付呗单返回 None;付呗单返回 data 字典(含 trade_state 等)。 + 不应查付呗时返回 None; + 应查付呗时返回 data(含 trade_state);查单业务失败时抛 Exception。 """ - ledger = get_pay_ledger(out_trade_no) - if not ledger or ledger.channel != PAY_CHANNEL_FUBEI_WX_MINI: + try_fb, cid, order_sn = _should_try_fubei(out_trade_no, club_id) + if not try_fb: 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('付呗查单配置缺失') + # 俱乐部标了付呗但配置不齐:有流水则报错;仅「尝试」则回落微信 + ledger = get_pay_ledger(out_trade_no) + if ledger and ledger.channel == PAY_CHANNEL_FUBEI_WX_MINI: + logger.error('[FUBEI_QUERY] 配置缺失 club=%s out_trade_no=%s', cid, out_trade_no) + raise Exception('付呗查单配置缺失') + return None 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 '', + order_sn=order_sn, ) logger.info( - '[FUBEI_QUERY] out_trade_no=%s trade_state=%s', - out_trade_no, data.get('trade_state'), + '[FUBEI_QUERY] out_trade_no=%s club=%s trade_state=%s', + out_trade_no, cid, data.get('trade_state'), ) return data @@ -61,22 +92,30 @@ def fubei_success_payload( club_id: Optional[str] = None, ) -> Optional[Tuple[str, Any]]: """ - 若是付呗单且已支付成功,返回 (transaction_id, total_fee); - 若是付呗单但未成功,返回 ('', None) 且调用方可据此判断「支付中」; - 若不是付呗单,返回 None(继续走微信/支付宝查单)。 + 返回值约定(供复查/轮询统一使用): + - None:不是付呗单(或不应走付呗),调用方继续微信/支付宝 + - ('', None):是付呗路径,但尚未支付成功(支付中) + - (transaction_id, total_fee):付呗已支付成功 """ + try_fb, _, _ = _should_try_fubei(out_trade_no, club_id) + if not try_fb: + return None + + forced = is_fubei_out_trade_no(out_trade_no) 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 + msg = str(exc) + logger.error('[FUBEI_QUERY] 查单异常 %s: %s', out_trade_no, exc, exc_info=True) + # 无流水的「尝试」查询:订单不存在等可回落微信直连 + if not forced and any(k in msg for k in ('不存在', '无此', '未找到', 'ORDER', 'order')): + return None + # 有流水的付呗单:绝不能回落微信 + return ('', None) if data is None: - return None + return None if not forced else ('', None) + if not fubei_trade_is_success(data): return ('', None) diff --git a/jituan/services/member_recharge.py b/jituan/services/member_recharge.py index e46d223..796c4ea 100644 --- a/jituan/services/member_recharge.py +++ b/jituan/services/member_recharge.py @@ -1099,36 +1099,15 @@ def confirm_member_recharge_paid(dingdan_id, yonghuid): if order.zhuangtai != MEMBER_PENDING_STATUS and order.zhuangtai != MEMBER_CANCELLED_STATUS: raise MemberRechargeError(f'订单状态异常: {order.zhuangtai}', 'status_error') - # 主动查单确认支付状态 - from django.core.cache import cache club_id = getattr(order, 'club_id', None) - is_alipay = cache.get(f'alipay_pay_url_{dingdan_id}') is not None + from jituan.services.pay_confirm import confirm_remote_payment + remote = confirm_remote_payment(dingdan_id, club_id, prefer_amount=order.jine) + if not remote.get('ok'): + if remote.get('pending'): + raise MemberRechargeError('支付结果尚未同步,请稍后再试', 'not_paid') + raise MemberRechargeError(remote.get('msg') or '支付未成功', 'not_paid') - 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) - if trade_status not in ('TRADE_SUCCESS', 'TRADE_FINISHED'): - raise MemberRechargeError('支付宝支付结果尚未同步,请稍后再试', 'not_paid') - else: - # 微信订单:主动查单 - trade_state, _ = query_wechat_v2_trade_state(dingdan_id, club_id) - if trade_state != 'SUCCESS': - # 兜底:cache 标记可能已过期(30分钟),尝试查支付宝确认 - from jituan.services.alipay_pay import query_alipay_trade - alipay_status = query_alipay_trade(dingdan_id, club_id) - if alipay_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'): - logger.info('cache 过期但支付宝查单成功 dingdan=%s', dingdan_id) - else: - raise MemberRechargeError('微信侧尚未支付成功,请稍后再试', 'not_paid') - - total_fee_yuan = Decimal(str(order.jine)) + total_fee_yuan = Decimal(str(remote.get('amount') if remote.get('amount') is not None else order.jine)) result = fulfill_member_recharge( dingdan_id, paid_amount_yuan=total_fee_yuan, source='confirm', ) diff --git a/jituan/services/pay_confirm.py b/jituan/services/pay_confirm.py new file mode 100644 index 0000000..ae13f7e --- /dev/null +++ b/jituan/services/pay_confirm.py @@ -0,0 +1,88 @@ +"""统一远程支付确认:微信直连 / 付呗 / 支付宝,供复查与轮询调用。 + +返回: + {'ok': True, 'channel': 'fubei'|'wechat'|'alipay', 'transaction_id': str|None, 'amount': Decimal|None} + {'ok': False, 'pending': True, 'channel': ...} # 支付中,前端应继续轮询 + {'ok': False, 'pending': False, 'channel': ..., 'msg': ...} # 明确失败(少见) +""" +from __future__ import annotations + +import logging +from decimal import Decimal +from typing import Any, Dict, Optional + +from django.core.cache import cache + +logger = logging.getLogger(__name__) + + +def confirm_remote_payment( + out_trade_no: str, + club_id: Optional[str] = None, + *, + prefer_amount: Any = None, +) -> Dict[str, Any]: + """按通道查官方支付状态(付呗优先于微信直连,互不串单)。""" + from jituan.services.fubei_query import fubei_success_payload + + fb = fubei_success_payload(out_trade_no, club_id) + if fb is not None: + txn, total_fee = fb + if not txn and total_fee is None: + return {'ok': False, 'pending': True, 'channel': 'fubei'} + amount = total_fee if total_fee not in (None, '') else prefer_amount + return { + 'ok': True, + 'pending': False, + 'channel': 'fubei', + 'transaction_id': txn, + 'amount': Decimal(str(amount)) if amount not in (None, '') else None, + } + + is_alipay = cache.get(f'alipay_pay_url_{out_trade_no}') is not None + if is_alipay: + from jituan.services.alipay_pay import query_alipay_trade + trade_status = query_alipay_trade(out_trade_no, club_id) + if trade_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'): + return { + 'ok': True, + 'pending': False, + 'channel': 'alipay', + 'transaction_id': None, + 'amount': Decimal(str(prefer_amount)) if prefer_amount not in (None, '') else None, + } + return {'ok': False, 'pending': True, 'channel': 'alipay'} + + from jituan.services.member_recharge import query_wechat_v2_trade_state + trade_state, transaction_id = query_wechat_v2_trade_state(out_trade_no, club_id) + if trade_state == 'SUCCESS': + return { + 'ok': True, + 'pending': False, + 'channel': 'wechat', + 'transaction_id': transaction_id, + 'amount': Decimal(str(prefer_amount)) if prefer_amount not in (None, '') else None, + } + + # cache 过期兜底:再试支付宝 + from jituan.services.alipay_pay import query_alipay_trade + alipay_status = query_alipay_trade(out_trade_no, club_id) + if alipay_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'): + logger.info('confirm_remote: cache 过期但支付宝查单成功 %s', out_trade_no) + return { + 'ok': True, + 'pending': False, + 'channel': 'alipay', + 'transaction_id': None, + 'amount': Decimal(str(prefer_amount)) if prefer_amount not in (None, '') else None, + } + + if trade_state in (None, 'NOTPAY', 'USERPAYING', ''): + return {'ok': False, 'pending': True, 'channel': 'wechat', 'trade_state': trade_state} + return { + 'ok': False, + 'pending': False, + 'channel': 'wechat', + 'trade_state': trade_state, + 'msg': f'支付状态异常: {trade_state}', + } diff --git a/orders/views/payment.py b/orders/views/payment.py index 349694f..b22fb23 100644 --- a/orders/views/payment.py +++ b/orders/views/payment.py @@ -575,45 +575,22 @@ class PaymentVerifyView(APIView): return Response({'code': 1, 'msg': '订单正在处理中,请稍后'}) try: - # 判断支付方式并主动查单 - is_alipay = cache.get(f'alipay_pay_url_{dingdanid}') is not None trade_state = None transaction_id = None - # 付呗单优先:勿用微信直连 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)) - if alipay_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'): - trade_state = 'SUCCESS' - else: - return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}}) + from jituan.services.pay_confirm import confirm_remote_payment + remote = confirm_remote_payment( + dingdanid, + getattr(dingdan, 'ClubID', None), + prefer_amount=getattr(dingdan, 'Amount', None), + ) + if remote.get('ok'): + trade_state = 'SUCCESS' + transaction_id = remote.get('transaction_id') + elif remote.get('pending'): + return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}}) else: - # 微信订单:主动查单(按订单 club 读后台商户配置) - trade_state, transaction_id = self._query_wechat_payment( - dingdanid, club_id=getattr(dingdan, 'ClubID', None), - ) - if trade_state != 'SUCCESS': - # 兜底:cache 标记可能已过期(30分钟),尝试查支付宝确认 - from jituan.services.alipay_pay import query_alipay_trade - alipay_status = query_alipay_trade(dingdanid, getattr(dingdan, 'ClubID', None)) - if alipay_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'): - trade_state = 'SUCCESS' - logger.info('cache 过期但支付宝查单成功 dingdan=%s', dingdanid) - elif trade_state in ['NOTPAY', 'USERPAYING']: - return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}}) - else: - return Response({'code': 2, 'msg': '支付失败'}) + return Response({'code': 2, 'msg': remote.get('msg') or '支付失败'}) if trade_state == 'SUCCESS': # 支付成功,执行本地更新 diff --git a/products/views/chongzhi_payment.py b/products/views/chongzhi_payment.py index be030b3..669aa43 100644 --- a/products/views/chongzhi_payment.py +++ b/products/views/chongzhi_payment.py @@ -637,7 +637,7 @@ class ShangjiaChenggong(APIView): return Response({ 'code': 400, 'message': '订单尚未支付完成' - }, status=status.HTTP_400_BAD_REQUEST) + }, status=status.HTTP_200_OK) # 4. 检查订单状态是否为已支付(3) if order.zhuangtai != 3: @@ -645,7 +645,7 @@ class ShangjiaChenggong(APIView): return Response({ 'code': 400, 'message': '订单尚未支付完成' - }, status=status.HTTP_400_BAD_REQUEST) + }, status=status.HTTP_200_OK) # 5. 获取商家余额(通过反向查询) try: diff --git a/products/views/huiyuan_payment.py b/products/views/huiyuan_payment.py index 3c1e949..08516eb 100644 --- a/products/views/huiyuan_payment.py +++ b/products/views/huiyuan_payment.py @@ -645,13 +645,13 @@ class HuiyuanLunxun(APIView): return Response({ 'code': 400, 'message': str(exc), - }, status=status.HTTP_400_BAD_REQUEST) + }, status=status.HTTP_200_OK) if order.zhuangtai != 3: return Response({ 'code': 400, 'message': '会员订单尚未支付完成' - }, status=status.HTTP_400_BAD_REQUEST) + }, status=status.HTTP_200_OK) from jituan.services.member_recharge import repair_member_entitlement_if_paid repair_member_entitlement_if_paid(dingdanid) diff --git a/products/views/jifen_payment.py b/products/views/jifen_payment.py index 28a07be..5cf5b6b 100644 --- a/products/views/jifen_payment.py +++ b/products/views/jifen_payment.py @@ -603,14 +603,14 @@ class JifenLunxun(APIView): return Response({ 'code': 400, 'message': str(exc), - }, status=status.HTTP_400_BAD_REQUEST) + }, status=status.HTTP_200_OK) if order.zhuangtai != 3: logger.info(f"积分订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") return Response({ 'code': 400, 'message': '积分订单尚未支付完成' - }, status=status.HTTP_400_BAD_REQUEST) + }, status=status.HTTP_200_OK) # 5. 获取打手积分(通过反向查询) try: diff --git a/products/views/yajin_payment.py b/products/views/yajin_payment.py index 5c829a9..d338b13 100644 --- a/products/views/yajin_payment.py +++ b/products/views/yajin_payment.py @@ -609,14 +609,14 @@ class YajinLunxun(APIView): return Response({ 'code': 400, 'message': str(exc), - }, status=status.HTTP_400_BAD_REQUEST) + }, status=status.HTTP_200_OK) if order.zhuangtai != 3: logger.info(f"订单{dingdanid}尚未支付完成,当前状态: {order.zhuangtai}") return Response({ 'code': 400, 'message': '订单尚未支付完成' - }, status=status.HTTP_400_BAD_REQUEST) + }, status=status.HTTP_200_OK) # 4. 获取打手押金(通过反向查询,最快捷的方式) try: