fix: 自动提现收款不再因查单失败卡死,失败可重试且防双笔

- 查单失败时优先用本地 package 直接调起微信确认收款
- 单次打款失败不关审核单(保持待收款6),允许同一审核单再次收款
- 跳过已失败打款记录;缩短 grace 至 2 分钟;去掉「核对状态」类死锁提示

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-10 21:48:00 +08:00
parent 49ad57bbb2
commit e1a3c07cef
3 changed files with 120 additions and 83 deletions

View File

@@ -106,7 +106,48 @@ RECONCILE_AUDIT_CLOSED = 'audit_closed'
MAX_SINGLE_COLLECT_AMOUNT = decimal.Decimal('200')
# 查单失败且本地 zhuangtai=0 超过该分钟数,才允许新建打款记录
PENDING_QUERY_GRACE_MINUTES = 10
PENDING_QUERY_GRACE_MINUTES = 2
def _wait_confirm_payload(auto_record, package):
from jituan.services.wechat_pay import get_wechat_v3_config, club_id_for_tixian_yonghuid
wx_cfg = get_wechat_v3_config(club_id_for_tixian_yonghuid(auto_record.yonghuid))
return {
'tixian_id': auto_record.tixian_id,
'package_info': package,
'shouxufei': str(auto_record.shouxufei),
'shijidaozhang': str(auto_record.shijidaozhang),
'current_rate': str(auto_record.feilv),
'mch_id': wx_cfg.get('MCHID', ''),
'shenhe_danhao': auto_record.shenhe_danhao or '',
}
def _maybe_wait_confirm_from_local(auto_record):
"""查单不可用时:若本地已有 package直接让用户调起微信确认收款。"""
if auto_record.zhuangtai != 0:
return None
package = _parse_package_info(auto_record.wechat_package)
if not package:
return None
return RECONCILE_WAIT_CONFIRM, _wait_confirm_payload(auto_record, package)
def _mark_transfer_attempt_failed(auto_record, fail_reason, *, close_audit=False):
"""
单次打款尝试失败:标记 TixianAutoRecord释放限额。
默认不关审核单,用户可在同一笔已审核通过的单上再次点「收款」。
"""
reason = (fail_reason or '打款失败')[:500]
_close_auto_record_failed(auto_record, reason)
if not close_audit or not auto_record.shenhe_danhao:
return
try:
audit = TixianShenheJilu.query.get(shenhe_danhao=auto_record.shenhe_danhao)
with transaction.atomic():
close_audit_wechat_failed(audit, None, reason)
except TixianShenheJilu.DoesNotExist:
logger.warning('打款失败但审核记录不存在: %s', auto_record.shenhe_danhao)
def has_dakuan_mode_field():
@@ -1170,24 +1211,17 @@ def _sync_record_from_wx_query(auto_record, wx_data):
if state in WX_STATE_WAIT:
package = wx_data.get('package_info') or _parse_package_info(auto_record.wechat_package)
if not package:
local = _maybe_wait_confirm_from_local(auto_record)
if local:
return local
return RECONCILE_PENDING, {
'msg': '收款处理中,请稍后再试',
'msg': '收款处理中,请稍后再试',
'tixian_id': auto_record.tixian_id,
}
if auto_record.zhuangtai == 0:
auto_record.wechat_package = json.dumps(package, ensure_ascii=False)
auto_record.save(update_fields=['wechat_package', 'UpdateTime'])
from jituan.services.wechat_pay import get_wechat_v3_config, club_id_for_tixian_yonghuid
wx_cfg = get_wechat_v3_config(club_id_for_tixian_yonghuid(auto_record.yonghuid))
return RECONCILE_WAIT_CONFIRM, {
'tixian_id': auto_record.tixian_id,
'package_info': package,
'shouxufei': str(auto_record.shouxufei),
'shijidaozhang': str(auto_record.shijidaozhang),
'current_rate': str(auto_record.feilv),
'mch_id': wx_cfg.get('MCHID', ''),
'shenhe_danhao': auto_record.shenhe_danhao or '',
}
return RECONCILE_WAIT_CONFIRM, _wait_confirm_payload(auto_record, package)
if state in WX_STATE_CANCELING:
return RECONCILE_PENDING, {
@@ -1200,24 +1234,16 @@ def _sync_record_from_wx_query(auto_record, wx_data):
wx_data.get('fail_reason') or wx_data.get('close_reason') or state
)[:500]
if auto_record.zhuangtai != 1:
auto_record.zhuangtai = 2
auto_record.fail_reason = fail_reason
auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'UpdateTime'])
release_collect_quota_for_record(auto_record)
if auto_record.shenhe_danhao:
try:
audit = TixianShenheJilu.query.get(shenhe_danhao=auto_record.shenhe_danhao)
with transaction.atomic():
close_audit_wechat_failed(audit, None, fail_reason)
except TixianShenheJilu.DoesNotExist:
logger.warning('微信失败但审核记录不存在: %s', auto_record.shenhe_danhao)
return RECONCILE_AUDIT_CLOSED, {
'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理',
}
_mark_transfer_attempt_failed(auto_record, fail_reason, close_audit=False)
# 审核单保持待收款(6),允许用户再次发起收款(新 out_bill_no防双笔由 reconcile 保证)
return RECONCILE_ALLOW_NEW, {}
if auto_record.zhuangtai == 0:
local = _maybe_wait_confirm_from_local(auto_record)
if local:
return local
return RECONCILE_PENDING, {
'msg': f'收款处理中(微信状态:{state or "未知"},请稍后再试',
'msg': f'收款处理中,请稍后再试',
'tixian_id': auto_record.tixian_id,
}
return RECONCILE_ALLOW_NEW, {}
@@ -1266,25 +1292,42 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao):
pending_hit = None
for rec in records:
if rec.zhuangtai == 2:
continue
if rec.zhuangtai == 1:
return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'}
local_wait = _maybe_wait_confirm_from_local(rec)
if local_wait:
return local_wait
wx_data = query_wechat_transfer_bill(rec.tixian_id, club_id_for_tixian_yonghuid(rec.yonghuid))
if wx_data is None:
logger.error(
'对账查单不可用 shenhe_danhao=%s bill=%s',
logger.warning(
'对账查单不可用 shenhe_danhao=%s bill=%s,尝试本地 package 或短 grace',
shenhe_danhao, rec.tixian_id,
)
return RECONCILE_PENDING, {
'msg': '有收款处理中,系统正在核对微信状态,请稍后再试',
'tixian_id': rec.tixian_id,
}
if rec.zhuangtai == 0:
age = timezone.now() - rec.CreateTime
if age >= timedelta(minutes=PENDING_QUERY_GRACE_MINUTES):
_mark_transfer_attempt_failed(
rec, '微信查单暂不可用,请重新发起收款', close_audit=False,
)
continue
if pending_hit is None:
pending_hit = (RECONCILE_PENDING, {
'msg': '收款处理中,请稍后再试',
'tixian_id': rec.tixian_id,
})
continue
action, payload = _sync_record_from_wx_query(rec, wx_data)
if action == RECONCILE_COMPLETED:
return action, payload
if action == RECONCILE_WAIT_CONFIRM:
return action, payload
if action == RECONCILE_ALLOW_NEW:
continue
if action == RECONCILE_AUDIT_CLOSED:
return action, payload
if action == RECONCILE_PENDING:
@@ -1377,11 +1420,18 @@ def handle_post_transfer_failure(tixian_id, shenhe_danhao, err_code='', err_msg=
wx_data = query_wechat_transfer_bill(tixian_id, club_id_for_tixian_yonghuid(ar.yonghuid))
if wx_data is None:
logger.error(
'发起转账失败且查单不可用 bill=%s code=%s msg=%s,保持待查单',
local = _maybe_wait_confirm_from_local(ar)
if local:
return local
logger.warning(
'发起转账失败且查单不可用 bill=%s code=%s msg=%s,短 grace 后允许重试',
tixian_id, err_code, err_msg,
)
return RECONCILE_PENDING, '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击'
age = timezone.now() - ar.CreateTime
if age >= timedelta(minutes=PENDING_QUERY_GRACE_MINUTES):
_mark_transfer_attempt_failed(ar, err_msg or '微信发起转账失败', close_audit=False)
return RECONCILE_ALLOW_NEW, {}
return RECONCILE_PENDING, '收款处理中,请稍后再试'
action, payload = _sync_record_from_wx_query(ar, wx_data)
if action == RECONCILE_ALLOW_NEW:

View File

@@ -399,7 +399,7 @@ def process_audit_collect(request):
)
return Response({
'code': 12,
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
'msg': '收款请求已提交,请稍后再试',
})
finally:

View File

@@ -70,6 +70,7 @@ from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
_mark_transfer_attempt_failed, club_id_for_tixian_yonghuid,
check_shijidaozhang_within_limit,
check_dashou_has_formal_huiyuan_for_withdraw,
check_dashou_trial_blocks_commission_withdraw,
@@ -1274,23 +1275,11 @@ class TixianCallbackV3View(APIView):
)
else: # transfer_status == 'FAIL'
auto_record.zhuangtai = 2
auto_record.fail_reason = fail_reason or '微信转账失败'
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
release_collect_quota_for_record(auto_record)
if not auto_record.shenhe_danhao:
logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no)
else:
try:
audit = TixianShenheJilu.objects.select_for_update().get(
shenhe_danhao=auto_record.shenhe_danhao
)
close_audit_wechat_failed(
audit, None, auto_record.fail_reason,
)
except TixianShenheJilu.DoesNotExist:
logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}')
_mark_transfer_attempt_failed(
auto_record,
fail_reason or '微信转账失败',
close_audit=False,
)
return Response({'code': 'SUCCESS', 'message': 'ok'})
@@ -1334,11 +1323,18 @@ class TixianQueRenAutoView(APIView):
if auto_record.zhuangtai == 1:
return Response({'code': 0, 'msg': '提现成功', 'data': None})
wx_data = query_wechat_transfer_bill(tixian_id)
payout_club = club_id_for_tixian_yonghuid(auto_record.yonghuid)
wx_data = query_wechat_transfer_bill(tixian_id, payout_club)
if wx_data is None:
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
return Response({'code': 0, 'msg': '收款确认已提交,请稍后刷新查看结果'})
if wx_data.get('_not_found'):
age = timezone.now() - auto_record.CreateTime
if auto_record.zhuangtai == 0 and age >= timedelta(minutes=2):
_mark_transfer_attempt_failed(
auto_record, '微信未找到该打款单', close_audit=False,
)
return Response({'code': 0, 'msg': '请再次点击收款重试'})
return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
state = (wx_data.get('state') or '').upper()
@@ -1352,21 +1348,15 @@ class TixianQueRenAutoView(APIView):
if state in WX_STATE_FAIL:
fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or state
auto_record.zhuangtai = 2
auto_record.fail_reason = str(fail_reason)[:500]
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
release_collect_quota_for_record(auto_record)
if auto_record.shenhe_danhao:
audit = TixianShenheJilu.objects.select_for_update().get(
shenhe_danhao=auto_record.shenhe_danhao,
)
close_audit_wechat_failed(audit, None, auto_record.fail_reason)
return Response({'code': 400, 'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理'})
_mark_transfer_attempt_failed(
auto_record, str(fail_reason)[:500], close_audit=False,
)
return Response({'code': 0, 'msg': '本次收款未成功,可再次点击收款重试'})
if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
return Response({'code': 12, 'msg': '收款处理中,请在微信中完成或稍后再试'})
return Response({'code': 12, 'msg': '请在微信中完成收款确认'})
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
return Response({'code': 0, 'msg': '收款确认已提交,请稍后刷新查看结果'})
# 用户报告取消/失败:以微信查单为准,不本地盲目标记
if auto_record.zhuangtai == 2:
@@ -1374,12 +1364,15 @@ class TixianQueRenAutoView(APIView):
if auto_record.zhuangtai == 1:
return Response({'code': 0, 'msg': '提现成功', 'data': None})
wx_data = query_wechat_transfer_bill(tixian_id)
wx_data = query_wechat_transfer_bill(
tixian_id, club_id_for_tixian_yonghuid(auto_record.yonghuid),
)
if wx_data is None:
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试,切勿重复操作'})
return Response({'code': 0, 'msg': '提现已取消,可再次点击收款重试'})
if wx_data.get('_not_found'):
return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
_mark_transfer_attempt_failed(auto_record, '用户取消收款', close_audit=False)
return Response({'code': 0, 'msg': '提现已取消,可再次点击收款重试'})
state = (wx_data.get('state') or '').upper()
if state in WX_STATE_SUCCESS:
@@ -1392,21 +1385,15 @@ class TixianQueRenAutoView(APIView):
if state in WX_STATE_FAIL:
fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or '用户取消收款'
auto_record.zhuangtai = 2
auto_record.fail_reason = str(fail_reason)[:500]
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
release_collect_quota_for_record(auto_record)
if auto_record.shenhe_danhao:
audit = TixianShenheJilu.objects.select_for_update().get(
shenhe_danhao=auto_record.shenhe_danhao,
)
close_audit_wechat_failed(audit, None, auto_record.fail_reason)
return Response({'code': 0, 'msg': '提现已关闭,请联系客服处理'})
_mark_transfer_attempt_failed(
auto_record, str(fail_reason)[:500], close_audit=False,
)
return Response({'code': 0, 'msg': '提现已取消,可再次点击收款重试'})
if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
return Response({'code': 12, 'msg': '收款仍在进行中,请在微信中操作或稍后再试'})
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再'})
return Response({'code': 0, 'msg': '提现已取消,可再次点击收款重'})
except Exception as e:
logger.error(f'提现确认异常: {str(e)}', exc_info=True)