复查与充值轮询按通道查官方单:付呗单查付呗、直连查微信;履约仍幂等防多加,未完成返回业务码便于继续轮询。 Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
4.5 KiB
Python
131 lines
4.5 KiB
Python
"""付呗支付结果确认:主动查单(轮询/复查兜底,不依赖回调)。
|
||
|
||
规则:
|
||
- 流水表标明付呗 → 只查付呗(绝不误查微信直连)
|
||
- 无流水且俱乐部当前为付呗 → 先尝试付呗查单;查无单再回落微信
|
||
- 其它 → 返回 None,由调用方走微信/支付宝
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any, Dict, Optional, Tuple
|
||
|
||
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_club_mini_pay_channel, 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 _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);查单业务失败时抛 Exception。
|
||
"""
|
||
try_fb, cid, order_sn = _should_try_fubei(out_trade_no, club_id)
|
||
if not try_fb:
|
||
return None
|
||
|
||
cfg = get_fubei_config(cid)
|
||
if not cfg or not cfg.app_id or not cfg.api_key:
|
||
# 俱乐部标了付呗但配置不齐:有流水则报错;仅「尝试」则回落微信
|
||
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=order_sn,
|
||
)
|
||
logger.info(
|
||
'[FUBEI_QUERY] out_trade_no=%s club=%s trade_state=%s',
|
||
out_trade_no, cid, 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]]:
|
||
"""
|
||
返回值约定(供复查/轮询统一使用):
|
||
- 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:
|
||
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 if not forced else ('', 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)
|