收款失败静默换下一商户,全部失败才回前端。

日额度等 INVALID_REQUEST 不再因白名单漏匹配提前返回;查无单默认可换号,仅频率/系统错误禁换。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-14 13:39:47 +08:00
parent 537e538528
commit bcf304dd37

View File

@@ -28,7 +28,6 @@ from .tixian_shenhe_services import (
create_audit_application, create_audit_application,
ensure_shenhe_collect_completed, ensure_shenhe_collect_completed,
get_audit_for_collect, get_audit_for_collect,
handle_post_transfer_failure,
query_wechat_transfer_bill, query_wechat_transfer_bill,
reconcile_shenhe_wechat_bills, reconcile_shenhe_wechat_bills,
reject_audit_and_refund, reject_audit_and_refund,
@@ -64,21 +63,32 @@ def _is_wx_balance_insufficient(err_code='', err_msg=''):
return any(k in blob for k in ('NOT_ENOUGH', 'INSUFFICIENT', '余额不足', '资金不足')) return any(k in blob for k in ('NOT_ENOUGH', 'INSUFFICIENT', '余额不足', '资金不足'))
def _is_wx_switchable_error(err_code='', err_msg=''): def _extract_wx_err(wx_result):
"""解析微信错误码/文案(兼容 detail 为对象)。"""
err_code = str((wx_result or {}).get('code') or '')
raw = (wx_result or {}).get('message')
if raw is None:
raw = (wx_result or {}).get('detail')
if isinstance(raw, dict):
raw = raw.get('message') or raw.get('detail') or json.dumps(raw, ensure_ascii=False)
err_msg = str(raw or '微信接口调用失败')
return err_code, err_msg
def _is_wx_unsafe_to_switch(err_code='', err_msg='', status_code=None):
""" """
可立刻换下一商户重试的错误(本笔尚未进入 WAIT_USER_CONFIRM 结果可能已受理:换号有双笔风险,禁止立刻换商户,只能查单/待确认
仅白名单,避免误伤导致重复开单。不含裸词 LIMIT防误匹配 其余明确业务失败日额度、余额不足、INVALID_REQUEST 等)默认可换号
""" """
if _is_wx_balance_insufficient(err_code, err_msg): if status_code in (429, 500, 502, 503):
return True return True
blob = f'{err_code} {err_msg}'.upper() blob = f'{err_code} {err_msg}'.upper()
keywords = ( keys = (
'FREQUENCY_LIMITED', 'RATE_LIMIT', 'QUOTA_EXCEEDED', 'QUOTA', 'FREQUENCY_LIMIT', 'FREQUENCY_LIMITED', 'FREQUENCY_LIMIT_EXCEED',
'日限额', '超出限额', '超过限额', '限额', '次数超限', 'RATELIMIT', 'RATE_LIMIT', 'SYSTEM_ERROR',
'ACCOUNT_ERROR', 'NO_AUTH', 'PERMISSION', '商户号异常', '账户异常', '频率超限', '系统错误', '结果不明确', '结果未明确',
'CERT_ERROR', 'PRIVATE_KEY', 'SIGN_ERROR', '商户证书', '私钥',
) )
return any(k in blob for k in keywords) return any(k in blob for k in keys)
def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao): def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao):
@@ -393,6 +403,7 @@ def process_audit_collect(request):
quota_reserved = False quota_reserved = False
return Response({'code': 99, 'msg': '微信商户配置异常,请联系管理员'}) return Response({'code': 99, 'msg': '微信商户配置异常,请联系管理员'})
# 成功进待确认 → 立刻回前端(这是唯一「中途成功回包」的路径)
if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM': if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM':
package_info = wx_result.get('package_info') package_info = wx_result.get('package_info')
auto_record = TixianAutoRecord.objects.get(tixian_id=tixian_id) auto_record = TixianAutoRecord.objects.get(tixian_id=tixian_id)
@@ -411,98 +422,79 @@ def process_audit_collect(request):
'tixianjilu_id': tixianjilu_id_val, 'tixianjilu_id': tixianjilu_id_val,
}) })
err_code = wx_result.get('code', '') err_code, err_msg = _extract_wx_err(wx_result)
err_msg = wx_result.get('message', wx_result.get('detail', '微信接口调用失败'))
logger.error( logger.error(
'微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s', '微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s',
tixian_id, mch_id, err_code, err_msg, resp.status_code, tixian_id, mch_id, err_code, err_msg, resp.status_code,
) )
post_action, post_payload = handle_post_transfer_failure( # 查单决定:已成功/待确认 → 回前端;无单可换 → 静默试下一户;中间失败不回前端
tixian_id, shenhe_danhao, err_code, err_msg, has_next = mch_idx < len(mch_cfgs) - 1
) unsafe = _is_wx_unsafe_to_switch(err_code, err_msg, resp.status_code)
if post_action == RECONCILE_WAIT_CONFIRM:
if isinstance(post_payload, dict): wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg)
post_payload.setdefault('mch_id', mch_id) if wx_q is None:
post_payload.setdefault('tixianjilu_id', tixianjilu_id_val) return Response({
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao) 'code': 12,
if quota_resp: 'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
return quota_resp })
return _build_collect_success_response(post_payload)
if post_action == RECONCILE_COMPLETED: if not wx_q.get('_not_found'):
ensure_shenhe_collect_completed(shenhe_danhao) ar = TixianAutoRecord.objects.get(tixian_id=tixian_id)
return Response({ sync_action, sync_payload = _sync_record_from_wx_query(ar, wx_q)
'code': 0, if sync_action == RECONCILE_WAIT_CONFIRM:
'msg': '提现已成功到账', if isinstance(sync_payload, dict):
'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val}, sync_payload.setdefault('mch_id', mch_id)
}) sync_payload.setdefault('tixianjilu_id', tixianjilu_id_val)
if post_action == RECONCILE_PENDING: quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
# 关键漏洞修复:微信已返回明确可切换业务错误时,查单宽限会得到 PENDING(not_found) if quota_resp:
# 若不处理会死卡「请稍后再试」永不换号。确认查单仍 not_found 后才换;查单不可用则仍禁换。 return quota_resp
can_try_switch = ( return _build_collect_success_response(sync_payload)
_is_wx_switchable_error(err_code, err_msg) if sync_action == RECONCILE_COMPLETED:
and mch_idx < len(mch_cfgs) - 1 ensure_shenhe_collect_completed(shenhe_danhao)
) return Response({
if can_try_switch: 'code': 0,
wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg) 'msg': '提现已成功到账',
if wx_q is None: 'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val},
return Response({ })
'code': 12, if sync_action == RECONCILE_PENDING:
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击', return Response({
}) 'code': 12,
if wx_q.get('_not_found'): 'msg': (
user_msg = err_msg or '微信收款发起失败,请稍后重试' sync_payload.get('msg')
last_user_msg = user_msg if isinstance(sync_payload, dict) else sync_payload
last_was_balance = _is_wx_balance_insufficient(err_code, err_msg) ) or '有收款处理中,请稍后再试',
_mark_auto_record_failed(tixian_id, f'[{mch_id}] {user_msg}') })
logger.warning( # ALLOW_NEW微信侧该单已明确失败下面可换号
'商户转账失败(查单无单),切换下一商户 shenhe=%s from=%s idx=%s/%s',
shenhe_danhao, mch_id, mch_idx + 1, len(mch_cfgs), if unsafe:
) # 频率/5xx即便暂查无单也不换号结果不确定
continue return Response({
# 微信侧其实已有单:按查单结果走,禁止盲换 'code': 12,
ar = TixianAutoRecord.objects.get(tixian_id=tixian_id) 'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
sync_action, sync_payload = _sync_record_from_wx_query(ar, wx_q) })
if sync_action == RECONCILE_WAIT_CONFIRM:
if isinstance(sync_payload, dict):
sync_payload.setdefault('mch_id', mch_id)
sync_payload.setdefault('tixianjilu_id', tixianjilu_id_val)
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
if quota_resp:
return quota_resp
return _build_collect_success_response(sync_payload)
if sync_action == RECONCILE_COMPLETED:
ensure_shenhe_collect_completed(shenhe_danhao)
return Response({
'code': 0,
'msg': '提现已成功到账',
'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val},
})
pending_msg = (
post_payload.get('msg', post_payload)
if isinstance(post_payload, dict) else post_payload
)
return Response({'code': 12, 'msg': pending_msg or '有收款处理中,请稍后再试'})
# 明确失败:标失败;可切换则试下一商户(不释放限额,直到全部失败)
user_msg = err_msg or '微信收款发起失败,请稍后重试' user_msg = err_msg or '微信收款发起失败,请稍后重试'
last_user_msg = user_msg last_user_msg = user_msg
last_was_balance = _is_wx_balance_insufficient(err_code, err_msg) last_was_balance = _is_wx_balance_insufficient(err_code, err_msg)
_mark_auto_record_failed(tixian_id, f'[{mch_id}] {user_msg}') _mark_auto_record_failed(tixian_id, f'[{mch_id}] {err_msg}')
if _is_wx_switchable_error(err_code, err_msg) and mch_idx < len(mch_cfgs) - 1: if has_next:
logger.warning( logger.warning(
'商户转账失败,切换下一商户 shenhe=%s from=%s idx=%s/%s', '商户转账失败,静默切换下一商户(不回前端) shenhe=%s from=%s '
shenhe_danhao, mch_id, mch_idx + 1, len(mch_cfgs), 'code=%s msg=%s idx=%s/%s',
shenhe_danhao, mch_id, err_code, err_msg,
mch_idx + 1, len(mch_cfgs),
) )
continue continue
# 全部商户都失败了才回前端
if quota_reserved: if quota_reserved:
release_collect_quota_for_record( release_collect_quota_for_record(
TixianAutoRecord.objects.get(tixian_id=tixian_id), TixianAutoRecord.objects.get(tixian_id=tixian_id),
) )
quota_reserved = False quota_reserved = False
if last_was_balance and mch_idx >= len(mch_cfgs) - 1: if last_was_balance:
return Response({ return Response({
'code': 99, 'code': 99,
'msg': '运营账户资金不足,需等管理员充值后才能提现', 'msg': '运营账户资金不足,需等管理员充值后才能提现',