# yonghu/tixian_shenhe_services.py """ 自动提现审核流程业务服务层(仅服务于 zddksh 申请接口 与 tixiansq 收款接口) 流程: ① zddksh 提交审核:完整资格校验 → 扣款 → 写审核表+提现记录表(状态=1) 允许多笔并行申请;单笔实际到账(shijidaozhang)不得超过200元 ② 后台审核通过:校验到账≤200 → 状态 6(待收款) ③ tixiansq 收款:严格资格+限额 → 到账>200则关单退款(不退手续费),不调微信 微信官方 FAIL 关单不退款;SUCCESS 落库;进行中禁止新建打款单 ④ 打款成功(tixianqr/回调/查单):若收款前已预占则不再累加;未预占的旧单/查单直成则补记 状态码(与前端 TX_STATUS 一致): 1=审核中 2=提现成功 3=提现失败 4=审核成功 5=审核失败 6=待收款 """ import decimal import json import logging import random import time from datetime import date, timedelta import requests from django.conf import settings from django.db import transaction from django.utils import timezone from orders.models import Order, MerchantOrderExt, Penalty from jituan.services.wechat_pay import club_id_for_tixian_yonghuid from jituan.services.withdraw_config import ( get_or_create_platform_daily_stat, get_personal_daily_quota, get_platform_daily_limit, get_platform_daily_total, get_tixian_feilv as _get_tixian_feilv_for_club, ) from utils.fadan_utils import check_fadan_qiangdan_eligible from products.models import Huiyuangoumai from backend.models import WithdrawalDailyStats from utils.wechat_v3 import build_authorization from .models import ( TixianAutoRecord, TixianShenheJilu, Tixianjilu, UserDashou, UserGuanshi, UserShenheguan, UserShangjia, UserZuzhang, ) from users.business_models import User from orders.utils import update_daily_payout from django.db.models import Sum from gvsdsdk.fluent import db, func, FQ logger = logging.getLogger('yonghu.tixian_shenhe') ORDER_STATUS_COMPLETED = 3 ORDER_STATUS_REFUNDED = 5 # 打手提现申请 / 收款:仅 PlayerID=该用户 且 Status=2(进行中) 才拦截 # 退款中(4)、结算中(8) 等不拦 DASHOU_WITHDRAW_APPLY_BLOCKING_ORDER_STATUSES = (2,) LEIXING_RATE_KEY = { 1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10', } def _club_id_for_user(user_main): return club_id_for_tixian_yonghuid(user_main.UserUID) def get_tixian_feilv(leixing, yonghuid=None, club_id=None): """按用户所属俱乐部读取提现手续费率。""" if club_id is None: if not yonghuid: raise ValueError('缺少用户或俱乐部') club_id = club_id_for_tixian_yonghuid(yonghuid) return _get_tixian_feilv_for_club(club_id, leixing) DAKUAN_MODE_MANUAL = 1 DAKUAN_MODE_AUTO = 2 # 微信转账单状态(查单接口返回 state 字段) WX_STATE_SUCCESS = ('SUCCESS',) WX_STATE_WAIT = ('WAIT_USER_CONFIRM', 'TRANSFERING', 'ACCEPTED', 'PROCESSING') WX_STATE_FAIL = ('FAIL', 'CLOSED', 'CANCELLED') WX_STATE_CANCELING = ('CANCELING',) # 撤销中,非终态,禁止换新单号 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') # tixianqr 查单时:无 package 且微信 404 超过该秒数才提示处理中 PENDING_QUERY_GRACE_SECONDS = 20 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(): """生产库未跑 0032 迁移时模型无此字段,需兼容""" try: TixianShenheJilu._meta.get_field('dakuan_mode') return True except Exception: return False def is_auto_dakuan_audit(audit): """判断是否自动打款审核单;无 dakuan_mode 属性时,有审核记录即视为 zddksh 自动流程""" if audit is None: return False mode = getattr(audit, 'dakuan_mode', None) if mode is None: return True try: return int(mode) == DAKUAN_MODE_AUTO except (TypeError, ValueError): return True def load_audit_dakuan_mode_map(audit_ids): """批量取审核单打款方式;字段不存在时有关联审核即视为自动(2)""" return { aid: meta.get('dakuan_mode', DAKUAN_MODE_AUTO) for aid, meta in load_audit_meta_map(audit_ids).items() } def load_audit_meta_map(audit_ids): """ 批量取审核表元数据(供后台列表等) 返回 {审核记录id: {'dakuan_mode': int, 'shenhe_danhao': str}} """ if not audit_ids: return {} value_fields = ['id', 'shenhe_danhao'] if has_dakuan_mode_field(): value_fields.append('dakuan_mode') result = {} for row in TixianShenheJilu.query.filter(id__in=audit_ids).values(*value_fields): result[row['id']] = { 'shenhe_danhao': row.get('shenhe_danhao') or '', 'dakuan_mode': row.get('dakuan_mode', DAKUAN_MODE_AUTO), } return result def generate_shenhe_danhao(): """生成唯一审核单号:TXSH + 时间戳后10位 + 4位随机数""" for _ in range(10): timestamp = str(int(time.time() * 1000))[-10:] random_part = str(random.randint(1000, 9999)) danhao = f'TXSH{timestamp}{random_part}' if not TixianShenheJilu.query.filter(shenhe_danhao=danhao).exists(): return danhao raise RuntimeError('生成审核单号失败,请重试') def calc_fee_amounts(jine, feilv): """申请金额 jine;实际到账 = jine - 手续费""" shouxufei = (jine * feilv).quantize(decimal.Decimal('0.01')) shijidaozhang = (jine - shouxufei).quantize(decimal.Decimal('0.01')) return shouxufei, shijidaozhang def check_dashou_has_valid_huiyuan(yonghuid): """ 打手至少持有一个未过期会员(含体验,用于接单等场景) 返回 (True, '') 或 (False, 原因) """ records = Huiyuangoumai.query.filter(yonghu_id=yonghuid) if not records.exists(): return False, '您暂无会员记录,无法操作' for record in records: is_expired = record.jiance_shifou_daoqi() if not is_expired and record.huiyuan_zhuangtai == 1: return True, '' return False, '您的会员已全部过期,请先续费后再操作' def check_dashou_has_formal_huiyuan_for_withdraw(yonghuid): """ 打手佣金提现/收款:必须持有未过期的正式会员。 体验会员期内一律拒绝;体验到期后须购买正式会员。 """ from jituan.services.member_recharge import ( dashou_has_active_formal_huiyuan, dashou_in_active_trial_period, ) if dashou_has_active_formal_huiyuan(yonghuid): return True, '' if dashou_in_active_trial_period(yonghuid): return False, '体验会员不可提现佣金,请购买正式会员' if not Huiyuangoumai.query.filter(yonghu_id=yonghuid).exists(): return False, '您暂无会员记录,无法提现' return False, '您的会员已全部过期,请购买正式会员后提现' TRIAL_COMMISSION_WITHDRAW_MSG = ( '体验会员不可提现佣金,请购买正式会员' ) def check_dashou_trial_blocks_commission_withdraw(yonghuid): """打手佣金提现/收款:体验会员期内禁止。""" from jituan.services.member_recharge import dashou_in_active_trial_period if dashou_in_active_trial_period(yonghuid): return False, TRIAL_COMMISSION_WITHDRAW_MSG return True, '' def _dashou_has_unfinished_orders(yonghuid): return Order.query.filter( PlayerID=yonghuid, ).exclude( Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED], ).exists() def _dashou_has_orders_blocking_withdraw_apply(yonghuid): """打手提现申请/收款:仅拦该用户接单的进行中(Status=2)订单。""" return Order.query.filter( PlayerID=yonghuid, Status__in=DASHOU_WITHDRAW_APPLY_BLOCKING_ORDER_STATUSES, ).exists() def _dashou_has_open_penalties(yonghuid): return Penalty.query.filter( PenalizedUserID=yonghuid, ).exclude( Status__in=[2, 4], ).exists() def validate_payment_info_for_withdraw(user_main): """手动提现:须已绑定手机号,且收款账号与收款码至少一项。自动提现不走此函数。""" phone = (getattr(user_main, 'Phone', None) or '').strip() account = (getattr(user_main, 'PaymentAccount', None) or '').strip() qr = (getattr(user_main, 'PaymentQRCode', None) or '').strip() if not phone: return False, '请先完善收款手机号后再提现' if not account and not qr: return False, '请先设置收款账号或上传收款码后再提现' return True, '' def get_auto_withdraw_openid(user_main): """自动提现打款用 openid:兼容 user_wx_openid / User.OpenID / UserName(openid)。""" from jituan.services.club_user import get_wx_openid_for_user club_id = club_id_for_tixian_yonghuid(user_main.UserUID) return (get_wx_openid_for_user(user_main, club_id) or '').strip() def validate_auto_withdraw_prerequisites(user_main): """ 自动提现(zddksh / tixiansq):微信企业付款到用户 OpenID。 不要求手机号、收款码、收款账号;须能解析到微信 OpenID。 """ if not get_auto_withdraw_openid(user_main): return False, '请先使用微信登录后再申请自动提现' return True, '' def validate_withdraw_eligibility(user_main, leixing, jine): """ 提现资格校验(打手/管事/押金等业务条件;不校收款码/手机号)。 手动提现另在 TixianShenqingView 校验收款信息;自动提现另校 OpenID。 """ yonghuid = user_main.UserUID fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) if not fadan_ok: return False, fadan_msg, {} if leixing == 1: try: dashou = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '您不是打手身份,无法提现佣金', {} if dashou.zhanghaozhuangtai != 1: return False, '打手账号已被封禁,无法提现', {} if dashou.jifen < 10: return False, '积分不足10分,请补充积分后提现', {} huiyuan_ok, huiyuan_msg = check_dashou_has_formal_huiyuan_for_withdraw(yonghuid) if not huiyuan_ok: return False, huiyuan_msg, {} trial_ok, trial_msg = check_dashou_trial_blocks_commission_withdraw(yonghuid) if not trial_ok: return False, trial_msg, {} if _dashou_has_orders_blocking_withdraw_apply(yonghuid): return False, '您还有未完成或未退款的订单,请先处理完毕再提现', {} if _dashou_has_open_penalties(yonghuid): return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} if dashou.yue < jine: return False, f'余额不足,当前余额: {dashou.yue}', {} return True, '', {'nicheng': dashou.nicheng or ''} if leixing == 2: try: guanshi = UserGuanshi.query.get(user=user_main) except UserGuanshi.DoesNotExist: return False, '您不是管事身份,无法提现分红', {} if guanshi.zhuangtai != 1: return False, '管事账号已被封禁,无法提现', {} if guanshi.yue < jine: return False, f'余额不足,当前余额: {guanshi.yue}', {} return True, '', {'nicheng': '管事用户'} if leixing == 3: try: zuzhang = UserZuzhang.query.get(user=user_main) except UserZuzhang.DoesNotExist: return False, '您不是组长身份,无法提现组长分红', {} if zuzhang.zhuangtai != 1: return False, '组长账号已被封禁,无法提现', {} if zuzhang.ketixian_jine < jine: return False, f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}', {} return True, '', {'nicheng': ''} if leixing == 4: try: kaoheguan = UserShenheguan.query.get(user=user_main) except UserShenheguan.DoesNotExist: return False, '您不是考核官身份,无法提现分佣', {} if kaoheguan.zhuangtai != 1: return False, '考核官账号已被封禁,无法提现', {} if kaoheguan.yue < jine: return False, f'考核官可提现金额不足,当前余额: {kaoheguan.yue}', {} return True, '', {'nicheng': '考核官'} if leixing == 5: try: dashou = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '您不是打手身份,无法提取押金', {} if dashou.zhanghaozhuangtai != 1: return False, '打手账号已禁用,无法提取押金', {} if dashou.jifen < 10: return False, '积分不足10分,请补充积分后提取押金', {} huiyuan_ok, huiyuan_msg = check_dashou_has_formal_huiyuan_for_withdraw(yonghuid) if not huiyuan_ok: return False, huiyuan_msg, {} if dashou.yajin < jine: return False, f'押金余额不足,当前押金: {dashou.yajin}', {} if _dashou_has_orders_blocking_withdraw_apply(yonghuid): return False, '您还有未完成或未退款的订单,请先处理完毕再提取押金', {} if _dashou_has_open_penalties(yonghuid): return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} return True, '', {'nicheng': dashou.nicheng or ''} if leixing == 6: try: shangjia = UserShangjia.query.get(user=user_main) except UserShangjia.DoesNotExist: return False, '您不是商家身份,无法提现商家余额', {} if shangjia.zhuangtai != 1: return False, '商家账号已被禁用,无法提现', {} if shangjia.yue < jine: return False, f'商家余额不足,当前余额: {shangjia.yue}', {} unfinished_orders = MerchantOrderExt.query.filter( MerchantID=yonghuid, ).exclude(Order__Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() if unfinished_orders: return False, '您还有未完成或未退款的订单,请先处理完毕再提现余额', {} self_fadan = Penalty.query.filter(PenalizedUserID=yonghuid).exclude(Status__in=[2, 4]).exists() if self_fadan: return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} applied_fadan = Penalty.query.filter(ApplicantID=yonghuid).exclude(Status__in=[2, 4]).exists() if applied_fadan: return False, '您发起的对其他用户的处罚尚未处理完毕,请等待完结', {} return True, '', {'nicheng': shangjia.nicheng or ''} 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): """ 自动提现收款前严格校验(申请时已扣款,不再校余额)。 打手佣金/押金:罚单、封禁、接单中、积分、会员、订单等; 订单仅拦进行中(2);退款中/结算中等不拦。 不要求手机号/收款码(微信打款到 OpenID)。 """ wx_ok, wx_msg = validate_auto_withdraw_prerequisites(user_main) if not wx_ok: return False, wx_msg yonghuid = user_main.UserUID fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2) if not fadan_ok: return False, fadan_msg if leixing == 1: try: dashou = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '您不是打手身份,无法收款' if dashou.zhanghaozhuangtai != 1: return False, '打手账号已被封禁,无法收款' if dashou.zhuangtai != 1: return False, '您有订单进行中,请完成后再收款' if dashou.jifen < 10: return False, '积分不足10分,无法收款' huiyuan_ok, huiyuan_msg = check_dashou_has_formal_huiyuan_for_withdraw(yonghuid) if not huiyuan_ok: return False, huiyuan_msg trial_ok, trial_msg = check_dashou_trial_blocks_commission_withdraw(yonghuid) if not trial_ok: return False, trial_msg if _dashou_has_orders_blocking_withdraw_apply(yonghuid): return False, '您还有进行中的订单,请完成后再收款' if _dashou_has_open_penalties(yonghuid): return False, '您有未处理完毕的自身罚单,无法收款' return True, '' if leixing == 5: try: dashou = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '您不是打手身份,无法收款' if dashou.zhanghaozhuangtai != 1: return False, '打手账号已禁用,无法收款' if dashou.zhuangtai != 1: return False, '您有订单进行中,请完成后再收款' if dashou.jifen < 10: return False, '积分不足10分,无法收款' huiyuan_ok, huiyuan_msg = check_dashou_has_formal_huiyuan_for_withdraw(yonghuid) if not huiyuan_ok: return False, huiyuan_msg if _dashou_has_orders_blocking_withdraw_apply(yonghuid): return False, '您还有进行中的订单,请完成后再收款' if _dashou_has_open_penalties(yonghuid): return False, '您有未处理完毕的自身罚单,无法收款' return True, '' if leixing == 2: try: guanshi = UserGuanshi.query.get(user=user_main) except UserGuanshi.DoesNotExist: return False, '您不是管事身份,无法收款' if guanshi.zhuangtai != 1: return False, '管事账号已被封禁,无法收款' return True, '' if leixing == 6: try: shangjia = UserShangjia.query.get(user=user_main) except UserShangjia.DoesNotExist: return False, '您不是商家身份,无法收款' if shangjia.zhuangtai != 1: return False, '商家账号已被禁用,无法收款' unfinished_orders = MerchantOrderExt.query.filter( MerchantID=yonghuid, ).exclude(Order__Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() if unfinished_orders: return False, '您还有未完成或未退款的订单,无法收款' self_fadan = Penalty.query.filter(PenalizedUserID=yonghuid).exclude(Status__in=[2, 4]).exists() if self_fadan: return False, '您有未处理完毕的自身罚单,无法收款' applied_fadan = Penalty.query.filter(ApplicantID=yonghuid).exclude(Status__in=[2, 4]).exists() if applied_fadan: return False, '您发起的处罚尚未处理完毕,无法收款' return True, '' if leixing == 3: try: zuzhang = UserZuzhang.query.get(user=user_main) except UserZuzhang.DoesNotExist: return False, '您不是组长身份,无法收款' if zuzhang.zhuangtai != 1: return False, '组长账号已被封禁,无法收款' return True, '' if leixing == 4: try: kaoheguan = UserShenheguan.query.get(user=user_main) except UserShenheguan.DoesNotExist: return False, '您不是考核官身份,无法收款' if kaoheguan.zhuangtai != 1: return False, '考核官账号已被封禁,无法收款' return True, '' return False, '无效的提现类型' def _get_personal_daily_quota(club_id, leixing, profile): return get_personal_daily_quota(club_id, leixing, profile) def _get_active_auto_record(shenhe_danhao): if not shenhe_danhao: return None return ( TixianAutoRecord.query.filter( shenhe_danhao=shenhe_danhao, zhuangtai__in=[0, 1], ) .order_by('-CreateTime') .first() ) def _get_platform_daily_total(club_id, leixing, today=None): return get_platform_daily_total(club_id, leixing, today) def _calc_platform_projected(club_id, leixing, shijidaozhang, shenhe_danhao=''): daily_used = _get_platform_daily_total(club_id, leixing) active_rec = _get_active_auto_record(shenhe_danhao) if active_rec: return daily_used, daily_used, decimal.Decimal('0.00') additional = shijidaozhang or decimal.Decimal('0.00') return daily_used, daily_used + additional, additional def _projected_personal_total(personal_today, jine, shenhe_danhao=''): """个人今日占用预估(申请金额口径)""" rec = _get_active_auto_record(shenhe_danhao) already_counted = rec.jine if rec else decimal.Decimal('0.00') return personal_today - already_counted + jine def _get_platform_daily_limit(club_id, leixing): limit = get_platform_daily_limit(club_id, leixing) if limit <= 0: logger.warning( '平台每日总限额为0 club_id=%s leixing=%s', club_id, leixing, ) return limit def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''): """个人每日限额只读校验(打手/管事/组长)""" if leixing not in (1, 2, 3): return True, '', '' today = date.today() if leixing == 1: try: profile = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '打手信息不存在', 'personal' if profile.last_tixian_date != today: personal_today = decimal.Decimal('0.00') else: personal_today = profile.jinritixian_jine elif leixing == 2: try: profile = UserGuanshi.query.get(user=user_main) except UserGuanshi.DoesNotExist: return False, '管事信息不存在', 'personal' if profile.last_tixian_date != today: personal_today = decimal.Decimal('0.00') else: personal_today = profile.jinritixian_jine else: try: profile = UserZuzhang.query.get(user=user_main) except UserZuzhang.DoesNotExist: return False, '组长信息不存在', 'personal' if profile.last_tixian_time and profile.last_tixian_time.date() == today: personal_today = profile.jinri_tixian else: personal_today = decimal.Decimal('0.00') personal_quota = _get_personal_daily_quota(_club_id_for_user(user_main), leixing, profile) projected_personal = _projected_personal_total(personal_today, jine, shenhe_danhao) if projected_personal > personal_quota: return ( False, f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元', 'personal', ) return True, '', '' def _check_platform_quota_readonly(club_id, leixing, shijidaozhang, shenhe_danhao=''): """平台每日总限额只读校验:今日已用 + 本笔到账 > 总限额 则拒绝""" platform_limit = _get_platform_daily_limit(club_id, leixing) shijidaozhang = shijidaozhang or decimal.Decimal('0.00') if platform_limit <= 0 and shijidaozhang > 0: return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform' daily_used, projected, additional = _calc_platform_projected( club_id, leixing, shijidaozhang, shenhe_danhao, ) if projected > platform_limit: logger.warning( '平台总限额拦截 club_id=%s leixing=%s shenhe_danhao=%s daily_used=%s additional=%s ' 'projected=%s limit=%s', club_id, leixing, shenhe_danhao, daily_used, additional, projected, platform_limit, ) return ( False, f'今日该角色提现总额已达上限,今日已提{daily_used}元,本次到账{additional}元,限额{platform_limit}元', 'platform', ) return True, '', '' def check_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): """ 收款限额只读校验(复用待确认打款单等场景,不写入)。 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') """ ok, msg, kind = _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao) if not ok: return False, msg, kind club_id = _club_id_for_user(user_main) return _check_platform_quota_readonly(club_id, leixing, shijidaozhang, shenhe_danhao) def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): """ 收款前原子预占每日限额(必须在 transaction.atomic 内调用)。 锁定 WithdrawalDailyStats + 用户行,校验通过后立即写入统计,防止高并发超额。 本审核单已有进行中/成功打款记录则视为已预占,不重复加计。 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') """ today = date.today() shijidaozhang = shijidaozhang or decimal.Decimal('0.00') club_id = _club_id_for_user(user_main) if _get_active_auto_record(shenhe_danhao): return True, '', '' personal_profile = None if leixing in (1, 2, 3): if leixing == 1: personal_profile = UserDashou.objects.select_for_update().get(user=user_main) if personal_profile.last_tixian_date != today: personal_profile.jinritixian_jine = decimal.Decimal('0.00') personal_profile.last_tixian_date = today personal_today = personal_profile.jinritixian_jine elif leixing == 2: personal_profile = UserGuanshi.objects.select_for_update().get(user=user_main) if personal_profile.last_tixian_date != today: personal_profile.jinritixian_jine = decimal.Decimal('0.00') personal_profile.last_tixian_date = today personal_today = personal_profile.jinritixian_jine else: personal_profile = UserZuzhang.objects.select_for_update().get(user=user_main) if personal_profile.last_tixian_time and personal_profile.last_tixian_time.date() != today: personal_profile.jinri_tixian = decimal.Decimal('0.00') elif not personal_profile.last_tixian_time: personal_profile.jinri_tixian = decimal.Decimal('0.00') personal_today = personal_profile.jinri_tixian personal_quota = _get_personal_daily_quota(club_id, leixing, personal_profile) if personal_today + jine > personal_quota: return ( False, f'今日提现已达个人限额,今日已提{personal_today}元,本次申请{jine}元,限额{personal_quota}元', 'personal', ) platform_stat = get_or_create_platform_daily_stat(club_id, leixing, today) platform_limit = _get_platform_daily_limit(club_id, leixing) if platform_limit <= 0 and shijidaozhang > 0: return False, '今日该角色提现已关闭(总限额为0),暂无法收款', 'platform' if platform_stat.total_amount + shijidaozhang > platform_limit: return ( False, f'今日该角色提现总额已达上限,今日已提{platform_stat.total_amount}元,' f'本次到账{shijidaozhang}元,限额{platform_limit}元', 'platform', ) if personal_profile is not None: if leixing == 1: personal_profile.jinritixian_jine += jine personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date']) elif leixing == 2: personal_profile.jinritixian_jine += jine personal_profile.save(update_fields=['jinritixian_jine', 'last_tixian_date']) else: personal_profile.jinri_tixian += jine personal_profile.last_tixian_time = timezone.now() personal_profile.save(update_fields=['jinri_tixian', 'last_tixian_time']) platform_stat.total_amount += shijidaozhang platform_stat.save(update_fields=['total_amount']) logger.info( '收款限额预占成功 yonghuid=%s leixing=%s shenhe_danhao=%s jine=%s shijidaozhang=%s platform_total=%s', user_main.UserUID, leixing, shenhe_danhao, jine, shijidaozhang, platform_stat.total_amount, ) return True, '', '' def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang): """微信打款明确失败/用户取消时回滚收款预占的限额""" today = date.today() shijidaozhang = shijidaozhang or decimal.Decimal('0.00') club_id = _club_id_for_user(user_main) try: with transaction.atomic(): if leixing == 1: dashou = UserDashou.objects.select_for_update().get(user=user_main) if dashou.last_tixian_date == today and dashou.jinritixian_jine >= jine: dashou.jinritixian_jine -= jine dashou.save(update_fields=['jinritixian_jine']) elif leixing == 2: guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) if guanshi.last_tixian_date == today and guanshi.jinritixian_jine >= jine: guanshi.jinritixian_jine -= jine guanshi.save(update_fields=['jinritixian_jine']) elif leixing == 3: zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) if zuzhang.jinri_tixian >= jine: zuzhang.jinri_tixian -= jine zuzhang.save(update_fields=['jinri_tixian']) try: stat = WithdrawalDailyStats.objects.select_for_update().get( club_id=club_id, Date=today, WithdrawalType=leixing, ) stat.total_amount = max( stat.total_amount - shijidaozhang, decimal.Decimal('0.00'), ) stat.save(update_fields=['total_amount']) except WithdrawalDailyStats.DoesNotExist: pass except Exception as e: logger.error( '回滚收款限额预占失败 yonghuid=%s leixing=%s err=%s', user_main.UserUID, leixing, e, exc_info=True, ) return logger.info( '收款限额预占已回滚 yonghuid=%s leixing=%s jine=%s shijidaozhang=%s', user_main.UserUID, leixing, jine, shijidaozhang, ) def release_collect_quota_for_record(auto_record): """幂等回滚单笔打款对应的收款预占(查单/回调/取消等多路径共用,防重复扣减)""" if not auto_record.shenhe_danhao: return if not _quota_already_reserved_for_record(auto_record): return try: user_main = User.query.get(UserUID=auto_record.yonghuid) except User.DoesNotExist: logger.error('回滚限额预占失败:用户不存在 %s', auto_record.yonghuid) return release_collect_quota_reservation( user_main, auto_record.leixing, auto_record.jine, auto_record.shijidaozhang, ) def _quota_already_reserved_for_record(auto_record): """判断本笔打款是否已在收款预占时写入日统计(避免成功时重复累加)""" if not auto_record.shenhe_danhao: return False today = date.today() shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') club_id = club_id_for_tixian_yonghuid(auto_record.yonghuid) stat_total = _get_platform_daily_total(club_id, auto_record.leixing, today) other_sum = TixianAutoRecord.query.filter( club_id=club_id, leixing=auto_record.leixing, zhuangtai__in=[0, 1], CreateTime__date=today, ).exclude(tixian_id=auto_record.tixian_id).aggregate( total=Sum('shijidaozhang'), )['total'] or decimal.Decimal('0.00') return stat_total >= other_sum + shijidaozhang def apply_transfer_success_quota(auto_record): """ 打款成功时累加每日限额统计(仅自动审核流程 shenhe_danhao 非空)。 - houtai.WithdrawalDailyStats.total_amount += 实际到账 - 用户扩展表个人今日已提 += 申请金额(打手/管事/组长) 须在 transaction.atomic 内、且打款记录 zhuangtai 由 0→1 时调用一次。 """ if not auto_record.shenhe_danhao: return today = date.today() leixing = auto_record.leixing jine = auto_record.jine shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') try: user_main = User.query.get(UserUID=auto_record.yonghuid) except User.DoesNotExist: logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid) return club_id = _club_id_for_user(user_main) platform_stat = get_or_create_platform_daily_stat(club_id, leixing, today) platform_stat.total_amount += shijidaozhang platform_stat.save(update_fields=['total_amount']) if leixing == 1: dashou = UserDashou.objects.select_for_update().get(user=user_main) if dashou.last_tixian_date != today: dashou.jinritixian_jine = decimal.Decimal('0.00') dashou.last_tixian_date = today dashou.jinritixian_jine += jine dashou.save(update_fields=['jinritixian_jine', 'last_tixian_date']) elif leixing == 2: guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) if guanshi.last_tixian_date != today: guanshi.jinritixian_jine = decimal.Decimal('0.00') guanshi.last_tixian_date = today guanshi.jinritixian_jine += jine guanshi.save(update_fields=['jinritixian_jine', 'last_tixian_date']) elif leixing == 3: zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) if zuzhang.last_tixian_time and zuzhang.last_tixian_time.date() != today: zuzhang.jinri_tixian = decimal.Decimal('0.00') elif not zuzhang.last_tixian_time: zuzhang.jinri_tixian = decimal.Decimal('0.00') zuzhang.jinri_tixian += jine zuzhang.last_tixian_time = timezone.now() zuzhang.save(update_fields=['jinri_tixian', 'last_tixian_time']) logger.info( '打款成功已累加限额 bill=%s yonghuid=%s leixing=%s jine=%s shijidaozhang=%s ' 'platform_total=%s', auto_record.tixian_id, auto_record.yonghuid, leixing, jine, shijidaozhang, platform_stat.total_amount, ) def refund_balance(user_main, leixing, jine): """审核拒绝时退还可到账金额 shijidaozhang(须在 transaction.atomic 内;手续费已在申请扣款时扣除不退还)""" if leixing == 1: dashou = UserDashou.objects.select_for_update().get(user=user_main) dashou.yue += jine dashou.save() elif leixing == 2: guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) guanshi.yue += jine guanshi.save() elif leixing == 3: zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) zuzhang.ketixian_jine += jine zuzhang.save() elif leixing == 4: kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main) kaoheguan.yue += jine kaoheguan.save() elif leixing == 5: dashou = UserDashou.objects.select_for_update().get(user=user_main) dashou.yajin += jine dashou.save(update_fields=['yajin']) elif leixing == 6: shangjia = UserShangjia.objects.select_for_update().get(user=user_main) shangjia.yue += jine shangjia.save() else: raise ValueError('无效的提现类型') def deduct_balance(user_main, leixing, jine): """扣减申请金额 jine(必须在 transaction.atomic 内调用)""" if leixing == 1: dashou = UserDashou.objects.select_for_update().get(user=user_main) if dashou.yue < jine: raise ValueError('余额不足') dashou.yue -= jine dashou.save() return dashou.nicheng or '' if leixing == 2: guanshi = UserGuanshi.objects.select_for_update().get(user=user_main) if guanshi.yue < jine: raise ValueError('余额不足') guanshi.yue -= jine guanshi.save() return '管事用户' if leixing == 3: zuzhang = UserZuzhang.objects.select_for_update().get(user=user_main) if zuzhang.ketixian_jine < jine: raise ValueError('可提现金额不足') zuzhang.ketixian_jine -= jine zuzhang.save() return '' if leixing == 4: kaoheguan = UserShenheguan.objects.select_for_update().get(user=user_main) if kaoheguan.yue < jine: raise ValueError('可提现金额不足') kaoheguan.yue -= jine kaoheguan.save() return '考核官' if leixing == 5: dashou = UserDashou.objects.select_for_update().get(user=user_main) if dashou.yajin < jine: raise ValueError('押金余额不足') dashou.yajin -= jine dashou.save(update_fields=['yajin']) return dashou.nicheng or '' if leixing == 6: shangjia = UserShangjia.objects.select_for_update().get(user=user_main) if shangjia.yue < jine: raise ValueError('商家余额不足') shangjia.yue -= jine shangjia.save() return shangjia.nicheng or '' raise ValueError('无效的提现类型') def get_balance_response_data(user_main, leixing): data = {} if leixing == 1: try: dashou = UserDashou.query.get(user=user_main) data['new_yongjin'] = str(dashou.yue.quantize(decimal.Decimal('0.01'))) except UserDashou.DoesNotExist: pass elif leixing == 2: try: guanshi = UserGuanshi.query.get(user=user_main) data['new_fenyong'] = str(guanshi.yue.quantize(decimal.Decimal('0.01'))) except UserGuanshi.DoesNotExist: pass elif leixing == 3: try: zuzhang = UserZuzhang.query.get(user=user_main) data['new_zuzhang_ketixian'] = str(zuzhang.ketixian_jine.quantize(decimal.Decimal('0.01'))) except UserZuzhang.DoesNotExist: pass elif leixing == 4: try: kg = UserShenheguan.query.get(user=user_main) data['new_kaoheguan_yue'] = str(kg.yue.quantize(decimal.Decimal('0.01'))) except UserShenheguan.DoesNotExist: pass return data def sync_audit_and_jilu_status(audit, new_status, **extra_fields): audit.zhuangtai = new_status for k, v in extra_fields.items(): setattr(audit, k, v) audit.save() if audit.tixianjilu_id: update_fields = {'zhuangtai': new_status, 'UpdateTime': timezone.now()} if 'bhliyou' in extra_fields: update_fields['bhliyou'] = extra_fields['bhliyou'] Tixianjilu.query.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', 'UpdateTime']) 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 = User.objects.select_for_update().get(UserUID=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, club_id=None): """ 向微信查转账单状态 GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no} 返回 dict;网络/系统异常返回 None;微信无此单返回 {'_not_found': True} """ from jituan.services.wechat_pay import club_id_for_tixian_yonghuid if not club_id: try: rec = TixianAutoRecord.query.filter(tixian_id=out_bill_no).first() if rec: club_id = club_id_for_tixian_yonghuid(rec.yonghuid) except Exception: pass url = ( 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/' f'transfer-bills/out-bill-no/{out_bill_no}' ) try: auth = build_authorization('GET', url, '', club_id=club_id) headers = { 'Authorization': auth, 'Accept': 'application/json', 'User-Agent': 'Mozilla/5.0', } resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp.json() if resp.status_code == 404: return {'_not_found': True} logger.error( '微信查单失败 bill=%s HTTP%s body=%s', out_bill_no, resp.status_code, resp.text[:300], ) return None except (requests.RequestException, OSError, ValueError) as e: logger.error('微信查单异常 bill=%s err=%s', out_bill_no, e, exc_info=True) return None except Exception as e: logger.error('微信查单未知异常 bill=%s err=%s', out_bill_no, e, exc_info=True) return None def _parse_package_info(raw): if not raw: return None if isinstance(raw, dict): return raw try: return json.loads(raw) except (TypeError, ValueError): return None def _apply_platform_accounting(auto_record): """平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)""" from jituan.services.szjilu_accounting import apply_szjilu_expense from jituan.services.wechat_pay import club_id_for_tixian_yonghuid payout_club = club_id_for_tixian_yonghuid(auto_record.yonghuid) update_daily_payout(auto_record.shijidaozhang, payout_club) apply_szjilu_expense(auto_record.shijidaozhang, payout_club) def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounting=True): """ 打款成功落库(tixianqr / callback / 查单对账 共用) select_for_update + zhuangtai==0 才更新并记账,防双写 返回 True=本次新标记成功;False=已处理过 """ with transaction.atomic(): ar = TixianAutoRecord.objects.select_for_update().get(pk=auto_record.pk) if ar.zhuangtai != 0: return False ar.zhuangtai = 1 update_fields = ['zhuangtai', 'UpdateTime'] if wechat_transfer_no: ar.wechat_transfer_no = wechat_transfer_no update_fields.append('wechat_transfer_no') ar.save(update_fields=update_fields) if ar.shenhe_danhao: try: audit = TixianShenheJilu.objects.select_for_update().get( shenhe_danhao=ar.shenhe_danhao, ) if audit.zhuangtai != 2: sync_audit_and_jilu_status(audit, 2) except TixianShenheJilu.DoesNotExist: logger.warning('打款成功但审核记录不存在: %s', ar.shenhe_danhao) if ar.shenhe_danhao and not _quota_already_reserved_for_record(ar): apply_transfer_success_quota(ar) if with_accounting: _apply_platform_accounting(ar) return True def _sync_record_from_wx_query(auto_record, wx_data): """根据微信查单结果修正本地打款记录,返回 (action, payload)""" if wx_data.get('_not_found'): if auto_record.zhuangtai == 0 and not _parse_package_info(auto_record.wechat_package): _mark_transfer_attempt_failed( auto_record, '微信未找到该打款单,请重新发起收款', close_audit=False, ) return RECONCILE_ALLOW_NEW, {} age = timezone.now() - auto_record.CreateTime if auto_record.zhuangtai == 0 and age < timedelta(seconds=PENDING_QUERY_GRACE_SECONDS): local = _maybe_wait_confirm_from_local(auto_record) if local: return local return RECONCILE_PENDING, { 'msg': '收款发起中,请稍后再试', 'tixian_id': auto_record.tixian_id, } if auto_record.zhuangtai == 0: auto_record.zhuangtai = 2 auto_record.fail_reason = '微信未找到该打款单,请重新发起收款' auto_record.save(update_fields=['zhuangtai', 'fail_reason', 'UpdateTime']) release_collect_quota_for_record(auto_record) return RECONCILE_ALLOW_NEW, {} 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'), ) return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} 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': '收款单处理中,请稍后再试', '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']) return RECONCILE_WAIT_CONFIRM, _wait_confirm_payload(auto_record, package) if state in WX_STATE_CANCELING: return RECONCILE_PENDING, { 'msg': '收款撤销处理中,请稍后再试', 'tixian_id': auto_record.tixian_id, } 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: _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'收款处理中,请稍后再试', 'tixian_id': auto_record.tixian_id, } return RECONCILE_ALLOW_NEW, {} def get_auto_record_for_collect(shenhe_danhao): """ 取本笔提现申请应对接微信的那条打款记录: 优先 zhuangtai=0(进行中),否则取最新一条(可能本地误标失败但微信未终态) """ pending = ( TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao, zhuangtai=0) .order_by('-CreateTime') .first() ) if pending: return pending return ( TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao) .order_by('-CreateTime') .first() ) def _collapse_stale_pending_records(shenhe_danhao): """同一审核单只保留最新一条进行中打款,其余作废(防多次点击堆叠导致永远处理中)。""" pending = list( TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao, zhuangtai=0) .order_by('-CreateTime') ) if len(pending) <= 1: return pending[0] if pending else None keep = pending[0] for stale in pending[1:]: _mark_transfer_attempt_failed(stale, '重复发起已作废', close_audit=False) return keep def reconcile_shenhe_wechat_bills(shenhe_danhao): """ 收款前对账(仅读本地库,不阻塞查微信)。 早期可用版本也是:有 package 直接调起确认;没有 package 就重新发起转账。 多俱乐部改造后「每次收款先查微信」在 V3 配置/查单异常时会永远返回处理中,已移除。 """ records = list( TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao).order_by('-CreateTime') ) if any(r.zhuangtai == 1 for r in records): try: audit = TixianShenheJilu.query.get(shenhe_danhao=shenhe_danhao) if audit.zhuangtai != 2: sync_audit_and_jilu_status(audit, 2) except TixianShenheJilu.DoesNotExist: pass return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'} pending = _collapse_stale_pending_records(shenhe_danhao) if not pending: return RECONCILE_ALLOW_NEW, {} local_wait = _maybe_wait_confirm_from_local(pending) if local_wait: return local_wait # 进行中但无 package = 上次调微信没成功,立刻作废,允许本次重新发起 _mark_transfer_attempt_failed( pending, '上次发起未成功,请重新收款', close_audit=False, ) return RECONCILE_ALLOW_NEW, {} def resolve_collect_context(yonghuid, tixianjilu_id=None, shenhe_danhao='', shenhe_jilu_id=None): """ 从提现记录 ID 进门(P0 收款主路径): Tixianjilu → TixianShenheJilu,校验归属、状态=6 待收款 返回 ({jilu, audit}, err_msg) """ if tixianjilu_id is None and not shenhe_danhao: return None, '缺少提现记录ID' jilu = None if tixianjilu_id is not None: try: jilu = Tixianjilu.query.get(id=int(tixianjilu_id), yonghuid=yonghuid) except (Tixianjilu.DoesNotExist, TypeError, ValueError): return None, '提现记录不存在或不属于当前用户' if jilu.zhuangtai != 6: return None, f'当前状态不可收款(状态{jilu.zhuangtai}),请等待审核通过' if not jilu.shenhe_jilu_id or not jilu.shenhe_danhao: return None, '提现记录未关联审核单,无法收款' if shenhe_jilu_id is not None: try: if int(shenhe_jilu_id) != jilu.shenhe_jilu_id: return None, '审核记录ID与提现记录不匹配' except (TypeError, ValueError): return None, '审核记录ID格式错误' if shenhe_danhao and shenhe_danhao != jilu.shenhe_danhao: return None, '审核单号与提现记录不匹配' shenhe_danhao = jilu.shenhe_danhao shenhe_jilu_id = jilu.shenhe_jilu_id try: if shenhe_jilu_id: audit = TixianShenheJilu.query.get( id=int(shenhe_jilu_id), shenhe_danhao=shenhe_danhao, yonghuid=yonghuid, ) else: audit = TixianShenheJilu.query.get( shenhe_danhao=shenhe_danhao, yonghuid=yonghuid, ) except (TixianShenheJilu.DoesNotExist, TypeError, ValueError): return None, '审核记录不存在' if audit.zhuangtai == 2: return None, '该提现已完成,请勿重复操作' if audit.zhuangtai != 6: return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过' if not is_auto_dakuan_audit(audit): return None, '该提现单为手动打款,无法使用自动收款' if jilu is None: try: jilu = Tixianjilu.query.get( id=audit.tixianjilu_id, yonghuid=yonghuid, ) if audit.tixianjilu_id else None except Tixianjilu.DoesNotExist: jilu = None return {'jilu': jilu, 'audit': audit, 'shenhe_danhao': shenhe_danhao}, '' def handle_post_transfer_failure(tixian_id, shenhe_danhao, err_code='', err_msg=''): """ 发起转账 HTTP 非成功:本地已有 package 则继续调起确认;否则标失败并返回可重试。 """ try: ar = TixianAutoRecord.query.get(tixian_id=tixian_id) except TixianAutoRecord.DoesNotExist: return RECONCILE_ALLOW_NEW, {'wx_err': err_msg or '微信发起转账失败'} local = _maybe_wait_confirm_from_local(ar) if local: return local reason = (err_msg or err_code or '微信发起转账失败')[:500] _mark_transfer_attempt_failed(ar, reason, close_audit=False) return RECONCILE_ALLOW_NEW, {'wx_err': reason} def get_audit_for_collect(shenhe_danhao, yonghuid, shenhe_jilu_id=None): """ 收款前数据库校验(须在 transaction.atomic 内调用;微信查单在事务外单独做) """ try: audit = TixianShenheJilu.objects.select_for_update().get( shenhe_danhao=shenhe_danhao, yonghuid=yonghuid, ) except TixianShenheJilu.DoesNotExist: return None, '审核记录不存在' if shenhe_jilu_id is not None: try: if int(shenhe_jilu_id) != audit.id: return None, '审核记录ID不匹配' except (TypeError, ValueError): return None, '审核记录ID格式错误' if audit.zhuangtai == 2: return None, '该提现已完成,请勿重复操作' if audit.zhuangtai != 6: return None, f'当前状态不可收款(状态{audit.zhuangtai}),请等待审核通过' if not is_auto_dakuan_audit(audit): return None, '该提现单为手动打款,无法使用自动收款' return audit, '' def create_audit_application(user_main, leixing, jine): """ 提交审核申请:校验 → 扣款 → 写审核表 + 提现记录表 不创建打款记录表(TixianAutoRecord 仅在收款时创建) 允许多笔并行申请(不阻塞待收款/审核中单) """ yonghuid = user_main.UserUID wx_ok, wx_msg = validate_auto_withdraw_prerequisites(user_main) if not wx_ok: raise ValueError(wx_msg) ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine) if not ok: raise ValueError(msg) feilv = get_tixian_feilv(leixing, yonghuid=yonghuid) shouxufei, shijidaozhang = calc_fee_amounts(jine, feilv) 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', '') with transaction.atomic(): nicheng = deduct_balance(user_main, leixing, jine) or nicheng club_id = club_id_for_tixian_yonghuid(yonghuid) audit_kwargs = { 'shenhe_danhao': shenhe_danhao, 'club_id': club_id, 'yonghuid': yonghuid, 'leixing': leixing, 'zhuangtai': 1, 'shenqing_jine': jine, 'shouxufei': shouxufei, 'shijidaozhang': shijidaozhang, 'feilv': feilv, } if has_dakuan_mode_field(): audit_kwargs['dakuan_mode'] = DAKUAN_MODE_AUTO audit = TixianShenheJilu.query.create(**audit_kwargs) tixian_record = Tixianjilu.query.create( club_id=club_id, yonghuid=yonghuid, avatar=user_main.Avatar or '', phone=user_main.Phone or '', nicheng=nicheng, leixing=leixing, zhifu=user_main.PaymentQRCode or '', skzhanghao=user_main.PaymentAccount or '', jine=shijidaozhang, zhuangtai=1, fangshi=1, shenheid=None, bhliyou='', shenhe_jilu_id=audit.id, shenhe_danhao=shenhe_danhao, ) audit.tixianjilu_id = tixian_record.id audit.save(update_fields=['tixianjilu_id']) response_data = get_balance_response_data(user_main, leixing) response_data.update({ 'shenhe_danhao': shenhe_danhao, 'shenhe_jilu_id': audit.id, 'tixianjilu_id': tixian_record.id, 'shouxufei': str(shouxufei), 'shijidaozhang': str(shijidaozhang), 'current_rate': str(feilv.quantize(decimal.Decimal('0.0001'))), 'leixing': leixing, 'zhuangtai': 1, }) return response_data