diff --git a/users/tixian_shenhe_services.py b/users/tixian_shenhe_services.py index d3587a1..b0a9196 100644 --- a/users/tixian_shenhe_services.py +++ b/users/tixian_shenhe_services.py @@ -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: diff --git a/users/tixian_shenhe_views.py b/users/tixian_shenhe_views.py index 0bf8abb..7e126ed 100644 --- a/users/tixian_shenhe_views.py +++ b/users/tixian_shenhe_views.py @@ -399,7 +399,7 @@ def process_audit_collect(request): ) return Response({ 'code': 12, - 'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击', + 'msg': '收款请求已提交,请稍后再试', }) finally: diff --git a/users/views/tixian.py b/users/views/tixian.py index a9484dd..6b5a892 100644 --- a/users/views/tixian.py +++ b/users/views/tixian.py @@ -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)