# 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, 'shouxufei': str, 'shijidaozhang': str}}
    """
    if not audit_ids:
        return {}
    value_fields = ['id', 'shenhe_danhao', 'shouxufei', 'shijidaozhang']
    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),
            'shouxufei': str(row.get('shouxufei') if row.get('shouxufei') is not None else '0.00'),
            'shijidaozhang': str(row.get('shijidaozhang') if row.get('shijidaozhang') is not None else '0.00'),
        }
    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,
    )
    from jituan.services.gift_huiyuan import user_has_any_active_gift_huiyuan

    if dashou_has_active_formal_huiyuan(yonghuid):
        return True, ''
    if user_has_any_active_gift_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, {}

    # 投诉未结清 / 管事被诉牵连上级组长（仅俱乐部开关打开时）
    try:
        from gongdan.services import check_complaint_blocks_withdraw
        gd_ok, gd_msg, gd_extra = check_complaint_blocks_withdraw(user_main)
        if not gd_ok:
            return False, gd_msg, gd_extra or {}
    except Exception as e:
        import logging
        logging.getLogger(__name__).error('投诉提现拦截检查异常: %s', e, exc_info=True)

    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('无效的提现类型')


_LEIXING_FREEZE_ROLE = {
    1: 'dashou',
    2: 'guanshi',
    3: 'zuzhang',
    4: 'shenheguan',
}


def estimate_manual_shouxufei(user_main, leixing, shijidaozhang):
    """
    手动打款单未存手续费：按当前俱乐部费率从到账额反推。
    shijidaozhang = shenqing * (1-rate) → fee = shijidaozhang * rate / (1-rate)
    """
    from decimal import Decimal, ROUND_HALF_UP
    shijidaozhang = Decimal(str(shijidaozhang or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
    if shijidaozhang <= 0:
        return Decimal('0.00')
    try:
        from jituan.services.club_user import get_user_club_id
        from jituan.services.withdraw_config import get_tixian_feilv
        club_id = get_user_club_id(user_main)
        rate = Decimal(str(get_tixian_feilv(club_id, leixing) or 0))
    except Exception:
        rate = Decimal('0')
    if rate <= 0:
        return Decimal('0.00')
    if rate >= 1:
        return Decimal('0.00')
    fee = (shijidaozhang * rate / (Decimal('1') - rate)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
    return fee if fee > 0 else Decimal('0.00')


def refund_withdraw_reject(
    user_main,
    leixing,
    *,
    shijidaozhang,
    shouxufei=None,
    refund_fee=False,
    apply_entry_freeze=False,
    biz_id='',
    freeze_reason='',
):
    """
    提现驳回退款（须在 transaction.atomic 内）。

    - 默认：只退 shijidaozhang，不走冻结（与现网一致）
    - refund_fee=True：额外退还手续费
    - apply_entry_freeze=True：打手/管事/组长/考核官走冻结网关（按俱乐部配置；押金/商家仍直退）
    - leixing 只信提现单上的类型，不按用户当前身份推断
    """
    from decimal import Decimal, ROUND_HALF_UP
    from jituan.services.fund_freeze import (
        SOURCE_WITHDRAW_REJECT,
        credit_role_balance,
        settle_freeze_club_id,
    )

    shijidaozhang = Decimal(str(shijidaozhang or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
    fee = Decimal(str(shouxufei or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
    if fee < 0:
        fee = Decimal('0.00')
    amount = shijidaozhang + (fee if refund_fee else Decimal('0.00'))
    if amount <= 0:
        return {
            'amount': Decimal('0.00'),
            'shijidaozhang': shijidaozhang,
            'shouxufei_refunded': Decimal('0.00'),
            'frozen': Decimal('0.00'),
            'available': Decimal('0.00'),
        }

    role = _LEIXING_FREEZE_ROLE.get(int(leixing))
    use_freeze = bool(apply_entry_freeze and role)
    biz = (biz_id or '').strip()

    if use_freeze:
        result = credit_role_balance(
            club_id=settle_freeze_club_id(user_id=str(getattr(user_main, 'UserUID', '') or '')),
            user_id=str(user_main.UserUID),
            role=role,
            amount=amount,
            source_type=SOURCE_WITHDRAW_REJECT,
            biz_id=biz or f'withdraw_reject:{user_main.UserUID}:{leixing}:{amount}',
            freeze_reason=freeze_reason or '提现驳回退款',
            ignore_source_scope=True,
        )
        return {
            'amount': amount,
            'shijidaozhang': shijidaozhang,
            'shouxufei_refunded': fee if refund_fee else Decimal('0.00'),
            'frozen': result.get('frozen') or Decimal('0.00'),
            'available': result.get('available') or Decimal('0.00'),
            'ledger_id': result.get('ledger_id'),
            'apply_entry_freeze': True,
        }

    refund_balance(user_main, leixing, amount)
    return {
        'amount': amount,
        'shijidaozhang': shijidaozhang,
        'shouxufei_refunded': fee if refund_fee else Decimal('0.00'),
        'frozen': Decimal('0.00'),
        'available': amount,
        'apply_entry_freeze': False,
    }

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 = (getattr(rec, 'club_id', None) or '').strip() or 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.tixian_club import resolve_payout_club_id
    payout_club = resolve_payout_club_id(audit=None, jilu=auto_record, 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 _lock_auto_record_by_audit(audit):
    """
    按审核单锁定关联打款记录。
    audit.tixian_auto_id 存的是 TixianAutoRecord.tixian_id（业务单号），不是自增 pk。
    """
    auto = None
    auto_id = (getattr(audit, 'tixian_auto_id', None) or '').strip()
    if auto_id:
        auto = (
            TixianAutoRecord.objects.select_for_update()
            .filter(tixian_id=auto_id)
            .first()
        )
        # 兼容极少数历史数据误存自增 id
        if auto is None and auto_id.isdigit():
            auto = (
                TixianAutoRecord.objects.select_for_update()
                .filter(pk=int(auto_id))
                .first()
            )
    if auto is None and getattr(audit, 'shenhe_danhao', None):
        auto = (
            TixianAutoRecord.objects.select_for_update()
            .filter(shenhe_danhao=audit.shenhe_danhao)
            .order_by('-CreateTime')
            .first()
        )
    return auto


def admin_mark_pending_collect_success(audit, tixian, kefu_uid, *, with_accounting=True):
    """
    客服强制：待收款(6) → 提现成功(2)。
    顺序：先同步审核单+提现单状态，再处理打款记录与平台记账。
    须在 transaction.atomic 内调用。
    """
    audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk)
    tixian = Tixianjilu.objects.select_for_update().get(pk=tixian.pk)
    if int(audit.zhuangtai) != 6 or int(tixian.zhuangtai) != 6:
        raise ValueError(
            f'仅待收款状态可标记为已收款'
            f'(当前提现单={tixian.zhuangtai},审核单={audit.zhuangtai})'
        )

    # 1) 先改状态
    sync_audit_and_jilu_status(audit, 2, shenhe_ren_id=(kefu_uid or '')[:11])
    tixian.shenheid = (kefu_uid or tixian.shenheid or '')[:11]
    tixian.save(update_fields=['shenheid', 'UpdateTime'])

    # 2) 关联打款单：处理中→成功；无打款单则直接按审核金额记账
    auto = _lock_auto_record_by_audit(audit)

    if auto is not None:
        if int(auto.zhuangtai) == 0:
            auto.zhuangtai = 1
            auto.fail_reason = '客服标记已收款'
            auto.save(update_fields=['zhuangtai', 'fail_reason', 'UpdateTime'])
            if not _quota_already_reserved_for_record(auto):
                apply_transfer_success_quota(auto)
            if with_accounting:
                _apply_platform_accounting(auto)
        elif int(auto.zhuangtai) == 1:
            # 已成功过，不再重复记账
            pass
        else:
            # 失败单：改成功并记账（客服人工确认已打款）
            auto.zhuangtai = 1
            auto.fail_reason = '客服标记已收款(原失败单)'
            auto.save(update_fields=['zhuangtai', 'fail_reason', 'UpdateTime'])
            if with_accounting:
                _apply_platform_accounting(auto)
    elif with_accounting:
        from jituan.services.szjilu_accounting import apply_szjilu_expense
        from jituan.services.tixian_club import resolve_payout_club_id
        amount = decimal.Decimal(str(audit.shijidaozhang or tixian.jine or 0))
        if amount > 0:
            payout_club = resolve_payout_club_id(audit=audit, jilu=tixian, yonghuid=tixian.yonghuid)
            update_daily_payout(amount, payout_club)
            apply_szjilu_expense(amount, payout_club)

    return True


def admin_reject_pending_or_auditing(
    audit,
    tixian,
    user_main,
    kefu_uid,
    reason,
    *,
    refund_principal=True,
    refund_fee=False,
    apply_entry_freeze=False,
):
    """
    客服驳回：审核中(1) 或 待收款(6) → 审核失败(5)。
    顺序：先关打款预占（若有）→ 先改状态 → 再按选项退款。
    须在 transaction.atomic 内调用。
    """
    audit = TixianShenheJilu.objects.select_for_update().get(pk=audit.pk)
    tixian = Tixianjilu.objects.select_for_update().get(pk=tixian.pk)
    jt = int(tixian.zhuangtai)
    at = int(audit.zhuangtai)
    if jt not in (1, 6) or at not in (1, 6):
        raise ValueError(
            f'仅审核中/待收款可驳回(当前提现单={jt},审核单={at})'
        )
    # 两边都可驳回但不同步时，以审核单为准对齐提现单后再驳回
    if jt != at:
        logger.warning(
            '驳回前状态不一致，对齐提现单→审核单 tixian=%s jt=%s at=%s',
            tixian.id, jt, at,
        )
        tixian.zhuangtai = at
        tixian.save(update_fields=['zhuangtai', 'UpdateTime'])

    reason = (reason or '').strip()[:500] or '客服驳回'
    kefu_uid = (kefu_uid or '')[:11]

    # 待收款可能已有打款单：先失败释放限额，避免用户还能收款
    auto = _lock_auto_record_by_audit(audit)
    if auto is not None and int(auto.zhuangtai) == 0:
        _close_auto_record_failed(auto, reason)

    # 1) 先改状态
    sync_audit_and_jilu_status(
        audit, 5,
        bhliyou=reason,
        bo_hui_ren_id=kefu_uid or None,
        tixian_auto_id=None,
    )
    tixian.shenheid = kefu_uid or tixian.shenheid or ''
    tixian.bhliyou = reason
    tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])

    # 2) 再按选项退款（可不退）
    refund_result = None
    principal = decimal.Decimal(str(audit.shijidaozhang or 0)) if refund_principal else decimal.Decimal('0.00')
    if refund_principal or refund_fee:
        refund_result = refund_withdraw_reject(
            user_main,
            audit.leixing,
            shijidaozhang=principal,
            shouxufei=audit.shouxufei,
            refund_fee=bool(refund_fee),
            apply_entry_freeze=bool(apply_entry_freeze),
            biz_id=f'withdraw_reject:admin:{tixian.id}:{at}',
            freeze_reason=f'提现驳回退款 #{tixian.id}',
        )
    return refund_result



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
