From 143965da6275e433c25ade2816d817ea2cb873a4 Mon Sep 17 00:00:00 2001 From: XingQue Date: Mon, 15 Jun 2026 22:24:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=80=E4=B8=8B=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=8F=90=E7=8E=B0=E6=8E=A5=E5=8F=A3=E5=B0=B1=E6=98=AF?= =?UTF-8?q?user/tixian=5Fshenhe=5Fservers.py,tixian=5Fshenhe=5Fviews.py,?= =?UTF-8?q?=E8=AE=A9=E6=96=87=E4=BB=B6=E4=BB=A5=E5=8F=8A=E5=AF=B9=E5=BA=94?= =?UTF-8?q?=E7=9A=84=E7=94=B3=E8=AF=B7=EF=BC=8C=E5=9B=9E=E8=B0=83=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E6=8E=A5=E5=8F=A3=EF=BC=8C=E8=AE=A9=E5=AE=83?= =?UTF-8?q?=E6=9B=B4=E5=A5=BD=E7=94=A8=E4=B8=80=E7=82=B9=EF=BC=8C=E5=85=81?= =?UTF-8?q?=E8=AE=B8=E7=94=A8=E6=88=B7=E5=A4=9A=E6=AC=A1=E6=8F=90=E7=8E=B0?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=EF=BC=8C=E5=BC=BA=E5=8C=96=E6=94=B6=E6=AC=BE?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=8C=E5=8F=8A=E6=97=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E4=B8=AD=E7=9A=84=E6=94=B6=E6=AC=BE?= =?UTF-8?q?=E7=8A=B6=E6=80=81=EF=BC=8C=E5=85=B6=E6=AC=A1=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E4=BA=86=E5=AE=A2=E6=9C=8D=E6=8F=90=E7=8E=B0=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E6=8E=A5=E5=8F=A3=EF=BC=8C=E5=85=B3=E4=BA=8E=E5=A4=A7?= =?UTF-8?q?=E4=BA=8E200=E5=85=83=E7=9A=84=E6=8F=90=E7=8E=B0=EF=BC=8C?= =?UTF-8?q?=E5=90=8C=E6=84=8F=E4=B9=8B=E5=90=8E=E7=9B=B4=E6=8E=A5=E6=8C=89?= =?UTF-8?q?=E9=A9=B3=E5=9B=9E=E5=A4=84=E7=90=86=EF=BC=8C=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=AE=98=E6=96=B9=E4=B8=8D=E6=94=AF=E6=8C=81=E5=A4=A7=E4=BA=8E?= =?UTF-8?q?200=E5=85=83=E7=9A=84=E6=8F=90=E7=8E=B0=E7=94=B3=E8=AF=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- a_long_dianjing/celery.py | 12 +-- a_long_dianjing/settings.py | 2 +- orders/signals.py | 51 ++++------ orders/tasks.py | 12 ++- users/tixian_shenhe_services.py | 173 +++++++++++++++++++++++--------- users/tixian_shenhe_views.py | 25 ++++- users/views.py | 116 +++++++++++++++------ 7 files changed, 277 insertions(+), 114 deletions(-) diff --git a/a_long_dianjing/celery.py b/a_long_dianjing/celery.py index 4b34db4..93d3f8f 100644 --- a/a_long_dianjing/celery.py +++ b/a_long_dianjing/celery.py @@ -42,12 +42,12 @@ app.conf.beat_schedule = { 'options': {'queue': 'periodic_tasks', 'priority': 4}, }, - # 补偿检查任务(每5分钟运行) - 'check_order_expire_task': { - 'task': 'orders.tasks.check_order_expire_task', - 'schedule': crontab(minute='*/5000'), - 'options': {'queue': 'order_tasks', 'priority': 3}, - }, + # 补偿检查任务(订单自动结算已禁用) + # 'check_order_expire_task': { + # 'task': 'orders.tasks.check_order_expire_task', + # 'schedule': crontab(minute='*/5000'), + # 'options': {'queue': 'order_tasks', 'priority': 3}, + # }, # 1. 每日凌晨0点执行 - 清零任务 'daily_reset_task': { diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py index 36df169..ab5edc5 100644 --- a/a_long_dianjing/settings.py +++ b/a_long_dianjing/settings.py @@ -278,7 +278,7 @@ CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = 'Asia/Shanghai' CELERY_ENABLE_UTC = True -CELERY_IMPORTS = ('dingdan.notice_tasks',) +CELERY_IMPORTS = ('orders.notice_tasks',) CELERY_TASK_TIME_LIMIT = 300 CELERY_TASK_SOFT_TIME_LIMIT = 240 CELERY_WORKER_CONCURRENCY = 4 diff --git a/orders/signals.py b/orders/signals.py index 5bcfc3c..9a94849 100644 --- a/orders/signals.py +++ b/orders/signals.py @@ -56,36 +56,29 @@ def handle_order_status_8(sender, instance, **kwargs): # 状态从 非8 变为 8 if old != 8 and instance.zhuangtai == 8: - #logger.info(f"📅 订单 {instance.dingdan_id} 状态变为8,提交定时任务") - - # 设置时间标记 + # 订单自动结算已关闭:仅记录时间标记,不再提交 Celery 延时结算任务 instance.status_8_time = timezone.now() - instance.pending_dispatch = True - - # 使用配置中的超时时间 - expire_seconds = getattr(settings, 'ORDER_EXPIRE_SECONDS', 48 * 60 * 60) - - # 提交Celery任务 - try: - from orders.tasks import process_expired_order - - task_result = process_expired_order.apply_async( - args=[instance.dingdan_id], - countdown=expire_seconds, - queue='order_tasks', - priority=9 - ) - - #logger.info(f"✅ 任务提交成功,订单: {instance.dingdan_id}, 任务ID: {task_result.id}") - - # 保存任务信息 - instance.auto_task_id = task_result.id - instance.auto_expire_at = timezone.now() + timezone.timedelta(seconds=expire_seconds) - - except Exception as e: - logger.error(f"❌ 任务提交失败: {e}") - instance.pending_dispatch = True - instance.auto_task_id = f"failed_{timezone.now().timestamp()}" + instance.pending_dispatch = False + instance.auto_task_id = '' + instance.auto_expire_at = None + logger.info(f"订单 {instance.dingdan_id} 状态变为8(自动结算已禁用,不提交结算任务)") + + # --- 原自动结算逻辑(已禁用,保留代码备查)--- + # expire_seconds = getattr(settings, 'ORDER_EXPIRE_SECONDS', 48 * 60 * 60) + # try: + # from orders.tasks import process_expired_order + # task_result = process_expired_order.apply_async( + # args=[instance.dingdan_id], + # countdown=expire_seconds, + # queue='order_tasks', + # priority=9 + # ) + # instance.auto_task_id = task_result.id + # instance.auto_expire_at = timezone.now() + timezone.timedelta(seconds=expire_seconds) + # except Exception as e: + # logger.error(f"❌ 任务提交失败: {e}") + # instance.pending_dispatch = True + # instance.auto_task_id = f"failed_{timezone.now().timestamp()}" # 状态从 8 变为 非8 elif old == 8 and instance.zhuangtai != 8: diff --git a/orders/tasks.py b/orders/tasks.py index 39aea2c..0e426c6 100644 --- a/orders/tasks.py +++ b/orders/tasks.py @@ -26,7 +26,12 @@ logger = logging.getLogger(__name__) def process_expired_order(self, dingdan_id): """ 处理单个超时订单(订单状态=8,超过48小时未处理) + 【已禁用】保留任务定义避免 Celery 注册/引用报错,实际不执行结算逻辑 """ + logger.info(f"订单{dingdan_id}自动结算任务已禁用,跳过执行") + return f"订单{dingdan_id}自动结算已禁用,跳过" + + # --- 以下原结算逻辑保留备查 --- try: # 使用select_for_update锁定记录,防止并发修改 with transaction.atomic(): @@ -134,9 +139,12 @@ def process_expired_order(self, dingdan_id): def check_order_expire_task(): """ 批量检查超时订单(补偿机制) - 每5分钟执行一次,作为信号处理的备用方案 - 查找状态为8且创建时间超过72小时的订单(宽松时间,防止误处理) + 【已禁用】保留任务定义避免 Beat/Worker 引用报错,实际不执行 """ + logger.info("订单自动结算补偿检查已禁用,跳过") + return {"success": True, "message": "自动结算已禁用", "count": 0} + + # --- 以下原补偿逻辑保留备查 --- try: # 计算更宽松的时间点:当前时间减去72小时(48+24小时) # 这样避免误处理刚变为状态8的订单 diff --git a/users/tixian_shenhe_services.py b/users/tixian_shenhe_services.py index 9ce1d7c..86ec2f0 100644 --- a/users/tixian_shenhe_services.py +++ b/users/tixian_shenhe_services.py @@ -3,11 +3,11 @@ 自动提现审核流程业务服务层(仅服务于 zddksh 申请接口 与 tixiansq 收款接口) 流程: - ① zddksh 提交审核:资格校验 → 扣款(shenqing_jine) → 写审核表+提现记录表(状态=1) - (不在申请阶段校验/占用每日限额) - ② 后台审核通过:状态变为 6(待收款)—— 由原有后台接口处理,此处不改动 - ③ tixiansq 收款:资格校验 → 只读预估 → 事务内原子预占限额 → 新建打款记录 → 调微信 - 微信明确失败/取消则回滚预占;待查单期间保留预占防并发超额 + ① zddksh 提交审核:完整资格校验 → 扣款 → 写审核表+提现记录表(状态=1) + 允许多笔并行申请;单笔实际到账(shijidaozhang)不得超过200元 + ② 后台审核通过:校验到账≤200 → 状态 6(待收款) + ③ tixiansq 收款:严格资格+限额 → 到账>200则关单退款(不退手续费),不调微信 + 微信官方 FAIL 关单不退款;SUCCESS 落库;进行中禁止新建打款单 ④ 打款成功(tixianqr/回调/查单):若收款前已预占则不再累加;未预占的旧单/查单直成则补记 状态码(与前端 TX_STATUS 一致): @@ -65,9 +65,6 @@ LEIXING_RATE_KEY = { 6: '10', } -# 进行中的审核状态(不允许重复提交申请) -PENDING_AUDIT_STATUSES = [1, 4, 6] - DAKUAN_MODE_MANUAL = 1 DAKUAN_MODE_AUTO = 2 @@ -81,6 +78,10 @@ RECONCILE_COMPLETED = 'completed' RECONCILE_WAIT_CONFIRM = 'wait_confirm' RECONCILE_ALLOW_NEW = 'allow_new' RECONCILE_PENDING = 'pending_query' +RECONCILE_AUDIT_CLOSED = 'audit_closed' + +# 单笔实际到账上限(仅收款时校验) +MAX_SINGLE_COLLECT_AMOUNT = decimal.Decimal('200') # 查单失败且本地 zhuangtai=0 超过该分钟数,才允许新建打款记录 PENDING_QUERY_GRACE_MINUTES = 10 @@ -182,7 +183,8 @@ def check_dashou_has_valid_huiyuan(yonghuid): def validate_withdraw_eligibility(user_main, leixing, jine): """ - 提现资格校验(对齐手动提现 TixianShenqingView) + 提现申请资格校验(完整基本条件;仅放宽「不限制并行申请笔数」)。 + 与手动提现 TixianShenqingView 对齐。 """ yonghuid = user_main.yonghuid @@ -199,7 +201,6 @@ def validate_withdraw_eligibility(user_main, leixing, jine): return False, '打手账号已被封禁,无法提现', {} if dashou.zhuangtai != 1: return False, '有订单进行中,请完成后提现', {} - # 与手动提现一致:积分必须正好 10 分 if dashou.jifen != 10: return False, '积分不足10分,请补充积分后提现', {} huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) @@ -293,11 +294,23 @@ def validate_withdraw_eligibility(user_main, leixing, jine): return False, '无效的提现类型', {} +def check_shijidaozhang_within_limit(shijidaozhang): + """校验单笔实际到账不超过上限(申请/审核/收款共用)""" + if shijidaozhang > MAX_SINGLE_COLLECT_AMOUNT: + return False, f'单笔实际到账不能超过{MAX_SINGLE_COLLECT_AMOUNT}元,请调整提现金额后重新申请' + return True, '' + + +def validate_collect_amount(audit): + """收款时校验单笔实际到账上限(超限将关单退款)""" + return check_shijidaozhang_within_limit(audit.shijidaozhang) + + def validate_collect_eligibility(user_main, leixing): """ - 收款前二次校验(申请时已扣款,不再校余额/金额)。 - 打手佣金/押金:罚单、封禁、接单中、积分、会员等; - 其他身份:账号状态、罚单/订单等(不含金额)。 + 收款前严格校验(申请时已扣款,不再校余额)。 + 打手佣金/押金:罚单、封禁、接单中、积分、会员、订单等; + 其他身份:账号状态、罚单/订单等。 """ yonghuid = user_main.yonghuid @@ -354,24 +367,6 @@ def validate_collect_eligibility(user_main, leixing): return False, '管事账号已被封禁,无法收款' return True, '' - if leixing == 3: - try: - zuzhang = UserZuzhang.objects.get(user=user_main) - except UserZuzhang.DoesNotExist: - return False, '您不是组长身份,无法收款' - if zuzhang.zhuangtai != 1: - return False, '组长账号已被封禁,无法收款' - return True, '' - - if leixing == 4: - try: - kaoheguan = UserShenheguan.objects.get(user=user_main) - except UserShenheguan.DoesNotExist: - return False, '您不是考核官身份,无法收款' - if kaoheguan.zhuangtai != 1: - return False, '考核官账号已被封禁,无法收款' - return True, '' - if leixing == 6: try: shangjia = UserShangjia.objects.get(user=user_main) @@ -392,6 +387,24 @@ def validate_collect_eligibility(user_main, leixing): return False, '您发起的处罚尚未处理完毕,无法收款' return True, '' + if leixing == 3: + try: + zuzhang = UserZuzhang.objects.get(user=user_main) + except UserZuzhang.DoesNotExist: + return False, '您不是组长身份,无法收款' + if zuzhang.zhuangtai != 1: + return False, '组长账号已被封禁,无法收款' + return True, '' + + if leixing == 4: + try: + kaoheguan = UserShenheguan.objects.get(user=user_main) + except UserShenheguan.DoesNotExist: + return False, '您不是考核官身份,无法收款' + if kaoheguan.zhuangtai != 1: + return False, '考核官账号已被封禁,无法收款' + return True, '' + return False, '无效的提现类型' @@ -891,13 +904,6 @@ def get_balance_response_data(user_main, leixing): return data -def has_pending_audit(yonghuid): - return TixianShenheJilu.objects.filter( - yonghuid=yonghuid, - zhuangtai__in=PENDING_AUDIT_STATUSES, - ).exists() - - def sync_audit_and_jilu_status(audit, new_status, **extra_fields): audit.zhuangtai = new_status for k, v in extra_fields.items(): @@ -910,6 +916,69 @@ def sync_audit_and_jilu_status(audit, new_status, **extra_fields): Tixianjilu.objects.filter(id=audit.tixianjilu_id).update(**update_fields) +def _close_auto_record_failed(auto_record, fail_reason): + """标记打款记录失败并释放收款预占限额""" + if auto_record is None or auto_record.zhuangtai == 1: + return + reason = (fail_reason or '打款失败')[:500] + auto_record.zhuangtai = 2 + auto_record.fail_reason = reason + auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) + release_collect_quota_for_record(auto_record) + + +def close_audit_wechat_failed(audit, auto_record=None, fail_reason='微信转账失败'): + """ + 微信官方终态 FAIL/CANCELLED:审核单关闭(3),不退款(申请时已扣款,手续费不退)。 + 须在 transaction.atomic 内调用。 + """ + _close_auto_record_failed(auto_record, fail_reason) + + audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk) + if audit.zhuangtai in (2, 3, 5): + return False + + reason = (fail_reason or '微信转账失败')[:500] + sync_audit_and_jilu_status( + audit, 3, + bhliyou=reason, + fail_reason=reason, + tixian_auto_id=None, + ) + logger.warning( + '微信失败关单(不退款) yonghuid=%s shenhe_danhao=%s reason=%s', + audit.yonghuid, audit.shenhe_danhao, reason, + ) + return True + + +def close_audit_over_limit_refund(audit, auto_record=None, fail_reason='单笔实际到账超过限额'): + """ + 到账超过200元等:审核单关闭(3) + 退还可到账金额 shijidaozhang(手续费不退)。 + 须在 transaction.atomic 内调用。 + """ + reason = (fail_reason or '单笔实际到账超过限额')[:500] + _close_auto_record_failed(auto_record, reason) + + audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk) + if audit.zhuangtai in (2, 3, 5): + return False + + user_main = UserMain.objects.select_for_update().get(yonghuid=audit.yonghuid) + refund_balance(user_main, audit.leixing, audit.shijidaozhang) + sync_audit_and_jilu_status( + audit, 3, + bhliyou=reason, + fail_reason=reason, + tixian_auto_id=None, + ) + logger.warning( + '超限关单并退款 yonghuid=%s shenhe_danhao=%s reason=%s', + audit.yonghuid, audit.shenhe_danhao, reason, + ) + return True + + def query_wechat_transfer_bill(out_bill_no): """ 向微信查转账单状态 @@ -1066,14 +1135,24 @@ def _sync_record_from_wx_query(auto_record, wx_data): } if state in WX_STATE_FAIL: + fail_reason = ( + 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 = ( - wx_data.get('fail_reason') or wx_data.get('close_reason') or state - )[:500] + auto_record.fail_reason = fail_reason auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'update_time']) release_collect_quota_for_record(auto_record) - return RECONCILE_ALLOW_NEW, {} + if auto_record.shenhe_danhao: + try: + audit = TixianShenheJilu.objects.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': '微信已确认转账失败,该笔提现已关闭,请联系客服处理', + } if auto_record.zhuangtai == 0: return RECONCILE_PENDING, { @@ -1145,6 +1224,8 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao): return action, payload if action == RECONCILE_WAIT_CONFIRM: return action, payload + if action == RECONCILE_AUDIT_CLOSED: + return action, payload if action == RECONCILE_PENDING: if pending_hit is None: pending_hit = (action, payload) @@ -1286,12 +1367,10 @@ def create_audit_application(user_main, leixing, jine): """ 提交审核申请:校验 → 扣款 → 写审核表 + 提现记录表 不创建打款记录表(TixianAutoRecord 仅在收款时创建) + 允许多笔并行申请(不阻塞待收款/审核中单) """ yonghuid = user_main.yonghuid - if has_pending_audit(yonghuid): - raise ValueError('账户余额不足或您有进行中的提现申请,请等待处理完成或10分钟后后再提交') - ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine) if not ok: raise ValueError(msg) @@ -1301,6 +1380,10 @@ def create_audit_application(user_main, leixing, jine): if shijidaozhang <= 0: raise ValueError('提现金额过低,扣除手续费后无实际到账') + ok, limit_msg = check_shijidaozhang_within_limit(shijidaozhang) + if not ok: + raise ValueError(limit_msg) + shenhe_danhao = generate_shenhe_danhao() nicheng = extra.get('nicheng', '') diff --git a/users/tixian_shenhe_views.py b/users/tixian_shenhe_views.py index 2fd8c31..7d671df 100644 --- a/users/tixian_shenhe_views.py +++ b/users/tixian_shenhe_views.py @@ -25,17 +25,21 @@ from utils.wechat_v3 import build_authorization from .models import TixianAutoRecord from .tixian_shenhe_services import ( RECONCILE_ALLOW_NEW, + RECONCILE_AUDIT_CLOSED, RECONCILE_COMPLETED, RECONCILE_PENDING, RECONCILE_WAIT_CONFIRM, check_collect_quota_limits, create_audit_application, + close_audit_over_limit_refund, + close_audit_wechat_failed, get_audit_for_collect, handle_post_transfer_failure, reconcile_shenhe_wechat_bills, release_collect_quota_for_record, reserve_collect_quota_limits, resolve_collect_context, + validate_collect_amount, validate_collect_eligibility, ) @@ -217,7 +221,16 @@ def process_audit_collect(request): tixianjilu_id_val = ctx['jilu'].id if ctx.get('jilu') else audit.tixianjilu_id leixing = audit.leixing - # 二次资格校验:不通过仅返回错误,审核单保持待收款(6),不退款,用户补齐资质后可再点收款 + amount_ok, amount_msg = validate_collect_amount(audit) + if not amount_ok: + with transaction.atomic(): + close_audit_over_limit_refund(audit, None, amount_msg) + return Response({ + 'code': 400, + 'msg': f'{amount_msg},可到账金额已退回余额(手续费不退),请重新申请', + }) + + # 收款前严格资格校验:不通过仅返回错误,审核单保持待收款(6),不退款 collect_ok, collect_msg = validate_collect_eligibility(user_main, leixing) if not collect_ok: logger.warning( @@ -241,6 +254,11 @@ def process_audit_collect(request): action, payload = reconcile_shenhe_wechat_bills(shenhe_danhao) if action == RECONCILE_COMPLETED: return Response({'code': 8, 'msg': payload.get('msg', '该提现已完成,请勿重复操作')}) + if action == RECONCILE_AUDIT_CLOSED: + return Response({ + 'code': 400, + 'msg': payload.get('msg', '该笔提现已关闭,请重新申请'), + }) if action == RECONCILE_WAIT_CONFIRM: quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao) if quota_resp: @@ -344,6 +362,11 @@ def process_audit_collect(request): return _build_collect_success_response(post_payload) if post_action == RECONCILE_COMPLETED: return Response({'code': 8, 'msg': '该提现已完成,请勿重复操作'}) + if post_action == RECONCILE_AUDIT_CLOSED: + return Response({ + 'code': 400, + 'msg': post_payload.get('msg', '该笔提现已关闭,请重新申请'), + }) if post_action == RECONCILE_PENDING: pending_msg = ( post_payload.get('msg', post_payload) diff --git a/users/views.py b/users/views.py index fdd29e5..33bc2ca 100644 --- a/users/views.py +++ b/users/views.py @@ -65,7 +65,10 @@ from .models import ( ) from .tixian_shenhe_services import ( load_audit_meta_map, refund_balance, sync_audit_and_jilu_status, - mark_transfer_success, release_collect_quota_for_record + mark_transfer_success, release_collect_quota_for_record, + close_audit_wechat_failed, query_wechat_transfer_bill, + check_shijidaozhang_within_limit, + WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING, ) from .tixian_shenhe_views import process_audit_collect @@ -10046,6 +10049,18 @@ class KefuWithdrawActionView(APIView): raise ValueError(f'用户{req_yonghuid}不存在') if action == 1: + limit_ok, limit_msg = check_shijidaozhang_within_limit(audit.shijidaozhang) + if not limit_ok: + sync_audit_and_jilu_status( + audit, 5, + bhliyou=limit_msg, + bo_hui_ren_id=kefu_user.yonghuid, + ) + refund_balance(user, audit.leixing, audit.shijidaozhang) + tixian.shenheid = kefu_user.yonghuid + tixian.bhliyou = limit_msg + tixian.save(update_fields=['shenheid', 'bhliyou', 'update_time']) + raise ValueError(f'提现记录{tixian_id}:{limit_msg},已自动驳回并退款') sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.yonghuid) tixian.shenheid = kefu_user.yonghuid tixian.save(update_fields=['shenheid', 'update_time']) @@ -12788,21 +12803,20 @@ class TixianCallbackV3View(APIView): 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: - # 申请时已扣款,回调失败不退余额,恢复待收款(6)可重试 try: audit = TixianShenheJilu.objects.select_for_update().get( shenhe_danhao=auto_record.shenhe_danhao ) - audit.tixian_auto_id = None - audit.fail_reason = auto_record.fail_reason - sync_audit_and_jilu_status(audit, 6, fail_reason=auto_record.fail_reason) + close_audit_wechat_failed( + audit, None, auto_record.fail_reason, + ) except TixianShenheJilu.DoesNotExist: logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}') - release_collect_quota_for_record(auto_record) return Response({'code': 'SUCCESS', 'message': 'ok'}) @@ -12812,7 +12826,7 @@ class TixianQueRenAutoView(APIView): """ 用户确认提现结果 POST /yonghu/tixianqr - 新审核流程:打款记录带 shenhe_danhao 时,确认成功同步审核表+提现记录表为 2;取消不退款(申请已扣),恢复待收款 6 可重试 + 以微信官方查单状态为准:SUCCESS 落库成功;FAIL/CANCELLED 关闭审核单+退款;进行中则提示等待 """ permission_classes = [permissions.IsAuthenticated] @@ -12847,37 +12861,79 @@ class TixianQueRenAutoView(APIView): if auto_record.zhuangtai == 1: return Response({'code': 0, 'msg': '提现成功', 'data': None}) - if auto_record.zhuangtai != 0: - return Response({'code': 5, 'msg': '该提现已处理'}) + wx_data = query_wechat_transfer_bill(tixian_id) + if wx_data is None: + return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'}) - mark_transfer_success(auto_record, with_accounting=True) + if wx_data.get('_not_found'): + return Response({'code': 12, 'msg': '收款处理中,请稍后再试'}) + state = (wx_data.get('state') or '').upper() + if state in WX_STATE_SUCCESS: + mark_transfer_success( + auto_record, + wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'), + with_accounting=True, + ) + return Response({'code': 0, 'msg': '提现成功', 'data': None}) + + 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': '微信已确认转账失败,该笔提现已关闭,请联系客服处理'}) + + if state in WX_STATE_WAIT or state in WX_STATE_CANCELING: + return Response({'code': 12, 'msg': '收款处理中,请在微信中完成或稍后再试'}) + + return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'}) + + # 用户报告取消/失败:以微信查单为准,不本地盲目标记 + if auto_record.zhuangtai == 2: + return Response({'code': 0, 'msg': '提现已取消', 'data': None}) + if auto_record.zhuangtai == 1: return Response({'code': 0, 'msg': '提现成功', 'data': None}) - # 用户取消或失败 - auto_record.zhuangtai = 2 - auto_record.fail_reason = '用户取消收款' - auto_record.save(update_fields=['zhuangtai', 'fail_reason']) + wx_data = query_wechat_transfer_bill(tixian_id) + if wx_data is None: + return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试,切勿重复操作'}) - if not auto_record.shenhe_danhao: - logger.error('确认取消但打款记录无审核单号 bill=%s', tixian_id) - return Response({'code': 5, 'msg': '该提现已处理'}) + if wx_data.get('_not_found'): + return Response({'code': 12, 'msg': '收款处理中,请稍后再试'}) - # 申请时已扣款,取消不退余额,审核单恢复 6 待收款,可再次点收款 - try: - audit = TixianShenheJilu.objects.select_for_update().get( - shenhe_danhao=auto_record.shenhe_danhao, + state = (wx_data.get('state') or '').upper() + if state in WX_STATE_SUCCESS: + mark_transfer_success( + auto_record, + wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'), + with_accounting=True, ) - audit.tixian_auto_id = None - sync_audit_and_jilu_status( - audit, 6, - fail_reason='用户取消收款,可重新发起', - ) - except TixianShenheJilu.DoesNotExist: - logger.warning(f'确认取消但审核记录不存在: {auto_record.shenhe_danhao}') - release_collect_quota_for_record(auto_record) + return Response({'code': 0, 'msg': '提现成功', 'data': None}) - return Response({'code': 0, 'msg': '提现已取消', 'data': None}) + 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': '提现已关闭,请联系客服处理'}) + + if state in WX_STATE_WAIT or state in WX_STATE_CANCELING: + return Response({'code': 12, 'msg': '收款仍在进行中,请在微信中操作或稍后再试'}) + + return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'}) except Exception as e: logger.error(f'提现确认异常: {str(e)}', exc_info=True)