fix: 统一付呗/微信直连官方查单确认,轮询幂等入账
复查与充值轮询按通道查官方单:付呗单查付呗、直连查微信;履约仍幂等防多加,未完成返回业务码便于继续轮询。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
88
jituan/services/pay_confirm.py
Normal file
88
jituan/services/pay_confirm.py
Normal file
@@ -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}',
|
||||
}
|
||||
Reference in New Issue
Block a user