fix: 自动提现收款不再因查单失败卡死,失败可重试且防双笔
- 查单失败时优先用本地 package 直接调起微信确认收款 - 单次打款失败不关审核单(保持待收款6),允许同一审核单再次收款 - 跳过已失败打款记录;缩短 grace 至 2 分钟;去掉「核对状态」类死锁提示 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -399,7 +399,7 @@ def process_audit_collect(request):
|
||||
)
|
||||
return Response({
|
||||
'code': 12,
|
||||
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
|
||||
'msg': '收款请求已提交,请稍后再试',
|
||||
})
|
||||
|
||||
finally:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user