强化多商户静默轮换:日额度/卡住状态自动换下一户。
查无单不再卡10分钟宽限期;跳过本单已失败商户;ACCEPTED/PROCESSING无package时放行换号;中间失败不回前端。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1264,8 +1264,15 @@ def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounti
|
||||
def _sync_record_from_wx_query(auto_record, wx_data):
|
||||
"""根据微信查单结果修正本地打款记录,返回 (action, payload)"""
|
||||
if wx_data.get('_not_found'):
|
||||
# 从未拿到 package = 未真正进入待确认,查无单应立刻放行换号/重发,
|
||||
# 不要卡 10 分钟宽限期(否则首户日额度失败后重试永远进不了多商户轮换)。
|
||||
has_package = bool(_parse_package_info(auto_record.wechat_package))
|
||||
age = timezone.now() - auto_record.create_time
|
||||
if auto_record.zhuangtai == 0 and age < timedelta(minutes=PENDING_QUERY_GRACE_MINUTES):
|
||||
if (
|
||||
auto_record.zhuangtai == 0
|
||||
and has_package
|
||||
and age < timedelta(minutes=PENDING_QUERY_GRACE_MINUTES)
|
||||
):
|
||||
return RECONCILE_PENDING, {
|
||||
'msg': '有收款处理中,请稍后再试',
|
||||
'tixian_id': auto_record.tixian_id,
|
||||
@@ -1286,7 +1293,8 @@ def _sync_record_from_wx_query(auto_record, wx_data):
|
||||
)
|
||||
return RECONCILE_COMPLETED, {'msg': '提现已成功到账', 'synced': True}
|
||||
|
||||
if state in WX_STATE_WAIT:
|
||||
# 仅待用户确认/转账中可回传 package;ACCEPTED/PROCESSING 视为卡在出资户,不假装可收款
|
||||
if state in ('WAIT_USER_CONFIRM', 'TRANSFERING'):
|
||||
package = wx_data.get('package_info') or _parse_package_info(auto_record.wechat_package)
|
||||
if not package:
|
||||
return RECONCILE_PENDING, {
|
||||
@@ -1311,6 +1319,13 @@ def _sync_record_from_wx_query(auto_record, wx_data):
|
||||
'shenhe_danhao': auto_record.shenhe_danhao or '',
|
||||
}
|
||||
|
||||
if state in ('ACCEPTED', 'PROCESSING'):
|
||||
return RECONCILE_PENDING, {
|
||||
'msg': f'收款处理中(微信状态:{state}),请稍后再试',
|
||||
'tixian_id': auto_record.tixian_id,
|
||||
'wx_state': state,
|
||||
}
|
||||
|
||||
if state in WX_STATE_CANCELING:
|
||||
return RECONCILE_PENDING, {
|
||||
'msg': '收款撤销处理中,请稍后再试',
|
||||
@@ -1394,6 +1409,25 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao):
|
||||
if action == RECONCILE_WAIT_CONFIRM:
|
||||
return action, payload
|
||||
if action == RECONCILE_PENDING:
|
||||
# ACCEPTED/PROCESSING 且无 package:视为该出资户卡住,标失败以便换商户重发
|
||||
qstate = _wx_transfer_state(wx_data)
|
||||
has_pkg = bool(
|
||||
wx_data.get('package_info') or _parse_package_info(rec.wechat_package)
|
||||
)
|
||||
if (
|
||||
rec.zhuangtai == 0
|
||||
and qstate in ('ACCEPTED', 'PROCESSING')
|
||||
and not has_pkg
|
||||
):
|
||||
rec.zhuangtai = 2
|
||||
rec.fail_reason = (f'微信状态卡住:{qstate},已切换其他商户重试')[:500]
|
||||
rec.save(update_fields=['zhuangtai', 'fail_reason', 'update_time'])
|
||||
# 不 release 限额:后续同笔收款仍占用预占
|
||||
logger.warning(
|
||||
'对账放行卡住状态 shenhe=%s bill=%s mch=%s state=%s',
|
||||
shenhe_danhao, rec.tixian_id, rec.mch_id, qstate,
|
||||
)
|
||||
continue
|
||||
if pending_hit is None:
|
||||
pending_hit = (action, payload)
|
||||
continue
|
||||
|
||||
@@ -78,15 +78,33 @@ def _extract_wx_err(wx_result):
|
||||
def _is_wx_unsafe_to_switch(err_code='', err_msg='', status_code=None):
|
||||
"""
|
||||
结果可能已受理:换号有双笔风险,禁止立刻换商户,只能查单/待确认。
|
||||
其余明确业务失败(日额度、余额不足、INVALID_REQUEST 等)默认可换号。
|
||||
"""
|
||||
if status_code in (429, 500, 502, 503):
|
||||
return True
|
||||
blob = f'{err_code} {err_msg}'.upper()
|
||||
keys = (
|
||||
'FREQUENCY_LIMIT', 'FREQUENCY_LIMITED', 'FREQUENCY_LIMIT_EXCEED',
|
||||
'RATELIMIT', 'RATE_LIMIT', 'SYSTEM_ERROR',
|
||||
'频率超限', '系统错误', '结果不明确', '结果未明确',
|
||||
'RATELIMIT', 'RATE_LIMIT', 'SYSTEM_ERROR', 'ALREADY_EXISTS',
|
||||
'频率超限', '系统错误', '结果不明确', '结果未明确', '订单已存在',
|
||||
)
|
||||
return any(k in blob for k in keys)
|
||||
|
||||
|
||||
def _is_wx_clear_switchable_fail(err_code='', err_msg='', status_code=None):
|
||||
"""
|
||||
明确未建单的业务失败:可静默换下一商户(日额度/余额不足/无权限等)。
|
||||
查单失败(None)时也允许换——避免首户卡死后永不试下一户。
|
||||
"""
|
||||
if _is_wx_unsafe_to_switch(err_code, err_msg, status_code):
|
||||
return False
|
||||
if status_code in (400, 401, 403):
|
||||
return True
|
||||
blob = f'{err_code} {err_msg}'.upper()
|
||||
keys = (
|
||||
'NOT_ENOUGH', 'INVALID_REQUEST', 'NO_AUTH', 'ACCOUNTERROR', 'ACCOUNT_ERROR',
|
||||
'PARAM_ERROR', 'SIGN_ERROR', 'APPID_MCHID_NOT_MATCH', 'CERT_ERROR',
|
||||
'额度', '限额', '余额不足', '资金不足', '无权限', '商户号异常', '账户异常',
|
||||
'收款受限', '付款受限',
|
||||
)
|
||||
return any(k in blob for k in keys)
|
||||
|
||||
@@ -339,6 +357,24 @@ def process_audit_collect(request):
|
||||
quota_reserved = True
|
||||
|
||||
# 多商户轮换:仅在发起阶段未进入待确认时切换;成功后 mch_id 必须落库
|
||||
# 本审核单已失败过的商户本轮优先跳过,直接试下一户
|
||||
failed_mchs = set(
|
||||
TixianAutoRecord.objects.filter(
|
||||
shenhe_danhao=shenhe_danhao, zhuangtai=2,
|
||||
).exclude(mch_id='').values_list('mch_id', flat=True)
|
||||
)
|
||||
prefer_cfgs = [c for c in mch_cfgs if c.get('MCHID') not in failed_mchs]
|
||||
if prefer_cfgs and len(prefer_cfgs) < len(mch_cfgs):
|
||||
logger.warning(
|
||||
'收款跳过已失败商户 shenhe=%s skip=%s try=%s',
|
||||
shenhe_danhao, sorted(failed_mchs),
|
||||
[c.get('MCHID') for c in prefer_cfgs],
|
||||
)
|
||||
mch_cfgs = prefer_cfgs
|
||||
logger.warning(
|
||||
'收款开始轮换商户 shenhe=%s count=%s mch_ids=%s',
|
||||
shenhe_danhao, len(mch_cfgs), [c.get('MCHID') for c in mch_cfgs],
|
||||
)
|
||||
last_user_msg = '微信收款发起失败,请稍后重试'
|
||||
last_was_balance = False
|
||||
tixian_id = None
|
||||
@@ -422,24 +458,38 @@ def process_audit_collect(request):
|
||||
'tixianjilu_id': tixianjilu_id_val,
|
||||
})
|
||||
|
||||
err_code, err_msg = _extract_wx_err(wx_result)
|
||||
logger.error(
|
||||
'微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s',
|
||||
tixian_id, mch_id, err_code, err_msg, resp.status_code,
|
||||
)
|
||||
|
||||
# 查单决定:已成功/待确认 → 回前端;无单可换 → 静默试下一户;中间失败不回前端
|
||||
has_next = mch_idx < len(mch_cfgs) - 1
|
||||
unsafe = _is_wx_unsafe_to_switch(err_code, err_msg, resp.status_code)
|
||||
|
||||
wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg)
|
||||
if wx_q is None:
|
||||
state200 = (wx_result.get('state') or '').upper() if resp.status_code == 200 else ''
|
||||
if resp.status_code == 200 and state200 == 'SUCCESS':
|
||||
ar = TixianAutoRecord.objects.get(tixian_id=tixian_id)
|
||||
from .tixian_shenhe_services import mark_transfer_success
|
||||
mark_transfer_success(
|
||||
ar,
|
||||
wechat_transfer_no=(
|
||||
wx_result.get('transfer_bill_no') or wx_result.get('transfer_no')
|
||||
),
|
||||
)
|
||||
ensure_shenhe_collect_completed(shenhe_danhao)
|
||||
return Response({
|
||||
'code': 12,
|
||||
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
|
||||
'code': 0,
|
||||
'msg': '提现已成功到账',
|
||||
'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val},
|
||||
})
|
||||
|
||||
if not wx_q.get('_not_found'):
|
||||
err_code, err_msg = _extract_wx_err(wx_result)
|
||||
if resp.status_code == 200 and (not err_code or err_msg == '微信接口调用失败'):
|
||||
err_msg = f'微信状态:{state200 or "未知"}'
|
||||
logger.error(
|
||||
'微信发起转账未进入待确认: bill=%s mch=%s code=%s msg=%s HTTP%s state=%s',
|
||||
tixian_id, mch_id, err_code, err_msg, resp.status_code, state200,
|
||||
)
|
||||
|
||||
has_next = mch_idx < len(mch_cfgs) - 1
|
||||
unsafe = _is_wx_unsafe_to_switch(err_code, err_msg, resp.status_code)
|
||||
clear_fail = _is_wx_clear_switchable_fail(err_code, err_msg, resp.status_code)
|
||||
|
||||
wx_q = query_wechat_transfer_bill(tixian_id, wx_cfg=wx_cfg)
|
||||
|
||||
if wx_q is not None and not wx_q.get('_not_found'):
|
||||
ar = TixianAutoRecord.objects.get(tixian_id=tixian_id)
|
||||
sync_action, sync_payload = _sync_record_from_wx_query(ar, wx_q)
|
||||
if sync_action == RECONCILE_WAIT_CONFIRM:
|
||||
@@ -458,6 +508,20 @@ def process_audit_collect(request):
|
||||
'data': {'already_completed': True, 'tixianjilu_id': tixianjilu_id_val},
|
||||
})
|
||||
if sync_action == RECONCILE_PENDING:
|
||||
from .tixian_shenhe_services import _parse_package_info
|
||||
ar_pkg = _parse_package_info(ar.wechat_package) or wx_q.get('package_info')
|
||||
qstate = (wx_q.get('state') or '').upper()
|
||||
# 卡在 ACCEPTED/PROCESSING 且无确认包:首户用不了,有下一户则换号
|
||||
if has_next and (not ar_pkg) and qstate in ('ACCEPTED', 'PROCESSING'):
|
||||
user_msg = err_msg or f'商户状态异常({qstate})'
|
||||
last_user_msg = user_msg
|
||||
last_was_balance = True
|
||||
_mark_auto_record_failed(tixian_id, f'[{mch_id}] {user_msg}')
|
||||
logger.warning(
|
||||
'商户卡在%s且无package,静默换下一户 shenhe=%s from=%s idx=%s/%s',
|
||||
qstate, shenhe_danhao, mch_id, mch_idx + 1, len(mch_cfgs),
|
||||
)
|
||||
continue
|
||||
return Response({
|
||||
'code': 12,
|
||||
'msg': (
|
||||
@@ -465,10 +529,10 @@ def process_audit_collect(request):
|
||||
if isinstance(sync_payload, dict) else sync_payload
|
||||
) or '有收款处理中,请稍后再试',
|
||||
})
|
||||
# ALLOW_NEW:微信侧该单已明确失败,下面可换号
|
||||
# ALLOW_NEW:微信终态失败,下面换号
|
||||
|
||||
if unsafe:
|
||||
# 频率/5xx:即便暂查无单也不换号(结果不确定)
|
||||
# 结果不确定且查单失败:禁换号
|
||||
if unsafe and not clear_fail and wx_q is None:
|
||||
return Response({
|
||||
'code': 12,
|
||||
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
|
||||
@@ -479,16 +543,17 @@ def process_audit_collect(request):
|
||||
last_was_balance = _is_wx_balance_insufficient(err_code, err_msg)
|
||||
_mark_auto_record_failed(tixian_id, f'[{mch_id}] {err_msg}')
|
||||
|
||||
# 有下一户:静默换号,绝不把中间失败文案回前端
|
||||
if has_next:
|
||||
logger.warning(
|
||||
'商户转账失败,静默切换下一商户(不回前端) shenhe=%s from=%s '
|
||||
'code=%s msg=%s idx=%s/%s',
|
||||
shenhe_danhao, mch_id, err_code, err_msg,
|
||||
mch_idx + 1, len(mch_cfgs),
|
||||
'code=%s msg=%s http=%s query=%s clear=%s idx=%s/%s',
|
||||
shenhe_danhao, mch_id, err_code, err_msg, resp.status_code,
|
||||
'none' if wx_q is None else ('404' if wx_q.get('_not_found') else 'synced'),
|
||||
clear_fail, mch_idx + 1, len(mch_cfgs),
|
||||
)
|
||||
continue
|
||||
|
||||
# 全部商户都失败了才回前端
|
||||
if quota_reserved:
|
||||
release_collect_quota_for_record(
|
||||
TixianAutoRecord.objects.get(tixian_id=tixian_id),
|
||||
|
||||
Reference in New Issue
Block a user