"""
会员微信充值履约（回调 / 前端确认共用，幂等）。

分红策略（定稿 v2）：
- formal_cishu：本俱乐部 + 打手 + 会员，已支付正式单笔数 + 1（体验单及历史脏数据不计入）。
- formal_cishu=1：duoci_fenhong 或 club 默认正式分成。
- formal_cishu>=2：仅 duoci_fenhong；无配置则不分。
- 体验单：固定 trial_guanshifc / trial_zuzhangfc；终身仅可购 1 次。
- 积分：首次充值会员（体验/正式均可）加满至 10；此后任意会员充值不再加分。
- 履约（回调/轮询/余额）均二次校验体验资格；分红失败回滚事务。

财务：每笔成功履约必须 czjilu.zhuangtai=3 + 收支入账（record_wechat_income_once）。
"""
from __future__ import annotations

import hashlib
import logging
import random
import string
from datetime import datetime, timedelta
from decimal import Decimal

import defusedxml.ElementTree as ET
import requests
from django.db import transaction
from django.db.models import F
from django.utils import timezone

from backend.utils import (
    update_guanshi_daily_by_action,
    update_guanshi_xufei_daily,
    update_zuzhang_daily_by_action,
)
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import ClubHuiyuanPrice
from jituan.services.club_config import get_huiyuan_fenchong
from jituan.services.club_penalty import resolve_gsfenhong_club_id
from jituan.services.club_user import get_user_club_id
from jituan.services.wechat_pay import club_id_from_czjilu, get_wechat_v2_config
from products.models import Czjilu, DuociFenhong, Gsfenhong, Huiyuan, Huiyuangoumai
from users.business_models import User
from users.models import UserDashou, UserGuanshi, UserZuzhang

logger = logging.getLogger(__name__)

MEMBER_FENHONG_LEIXING = 1
MEMBER_LEIXING = 1
MEMBER_PAID_STATUS = 3
MEMBER_PENDING_STATUS = 9
MEMBER_FAILED_STATUS = 8
MEMBER_CANCELLED_STATUS = 2
# 与 czjilu_types.CZJILU_FULFILLABLE_STATUSES 一致：仅这些状态允许入账
MEMBER_FULFILLABLE_STATUSES = (MEMBER_PENDING_STATUS, MEMBER_CANCELLED_STATUS)
MEMBER_CANCELLED_STATUS = 2
ENTITLEMENT_REVOKED_MARKER = '权益已撤销'
MEMBER_JIFEN_CAP = 10


class MemberRechargeError(Exception):
    def __init__(self, message, code='error'):
        super().__init__(message)
        self.code = code


def get_club_huiyuan_row(club_id, huiyuan_id, require_enabled=False):
    club_id = club_id or CLUB_ID_DEFAULT
    qs = ClubHuiyuanPrice.query.filter(club_id=club_id, huiyuan_id=huiyuan_id)
    if require_enabled:
        qs = qs.filter(is_enabled=True)
    return qs.first()


def club_huiyuan_sellable(club_id, huiyuan_id, is_trial=False):
    """正式/体验是否可售及价格、天数。"""
    club_id = club_id or CLUB_ID_DEFAULT
    row = get_club_huiyuan_row(club_id, huiyuan_id, require_enabled=True)
    if not row:
        return False, Decimal('0'), 0

    if is_trial:
        if not row.trial_enabled:
            return False, Decimal('0'), 0
        price = Decimal(str(row.trial_price or 0))
        days = int(row.trial_days or 0)
        if price <= 0 or days <= 0:
            return False, Decimal('0'), 0
        return True, price, days

    price = Decimal(str(row.jiage or 0))
    days = int(row.formal_days or 30)
    if price <= 0:
        return False, Decimal('0'), 0
    return True, price, days


def _order_is_trial_like(order, club_id=None):
    """识别体验单（含历史 is_trial 未回填的脏数据）。"""
    if bool(getattr(order, 'is_trial', False)):
        return True
    club_id = club_id or getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
    huiyuan_id = getattr(order, 'huiyuan_id', None)
    if not huiyuan_id:
        return False
    row = get_club_huiyuan_row(club_id, huiyuan_id, require_enabled=False)
    if not row or not row.trial_enabled:
        return False
    trial_price = Decimal(str(row.trial_price or 0))
    trial_days = int(row.trial_days or 0)
    if trial_price <= 0 or trial_days <= 0:
        return False
    order_jine = Decimal(str(order.jine or 0))
    if abs(order_jine - trial_price) > Decimal('0.01'):
        return False
    order_days = int(getattr(order, 'purchase_days', None) or 0)
    return order_days in (0, trial_days)


def _iter_paid_member_orders(yonghuid, huiyuan_id, club_id, exclude_dingdan_id=None):
    club_id = club_id or CLUB_ID_DEFAULT
    qs = Czjilu.query.filter(
        yonghuid=yonghuid,
        huiyuan_id=huiyuan_id,
        leixing=MEMBER_LEIXING,
        zhuangtai=MEMBER_PAID_STATUS,
        club_id=club_id,
    )
    if exclude_dingdan_id:
        qs = qs.exclude(dingdan_id=exclude_dingdan_id)
    return qs


def has_paid_trial_purchase(yonghuid, huiyuan_id, club_id, exclude_dingdan_id=None):
    """是否已有成功支付体验单（含历史脏数据）。"""
    club_id = club_id or CLUB_ID_DEFAULT
    for order in _iter_paid_member_orders(yonghuid, huiyuan_id, club_id, exclude_dingdan_id):
        if _order_is_trial_like(order, club_id):
            return True
    return False


def count_formal_paid_member_orders(yonghuid, huiyuan_id, club_id, exclude_dingdan_id=None):
    """本俱乐部该会员已成功支付的正式单数（不含体验）。"""
    club_id = club_id or CLUB_ID_DEFAULT
    n = 0
    for order in _iter_paid_member_orders(yonghuid, huiyuan_id, club_id, exclude_dingdan_id):
        if not _order_is_trial_like(order, club_id):
            n += 1
    return n


def migrate_formal_purchase_floor(yonghuid, huiyuan_id, club_id):
    """
    跨店迁入该会员时写入的正式购买次数下限。
    保证迁入后再充至少按第 2 次起算（不可能走首次管事分红），
    并能尽量继承源店正式购买次数。
    """
    club_id = club_id or CLUB_ID_DEFAULT
    from jituan.models import ClubMigrateLedger, ClubMigrateLedgerItem

    ledger_ids = list(
        ClubMigrateLedger.query.filter(
            to_uid=str(yonghuid),
            to_club_id=str(club_id),
            status=ClubMigrateLedger.STATUS_SUCCESS,
        ).values_list('id', flat=True)
    )
    if not ledger_ids:
        return 0
    floor = 0
    for it in ClubMigrateLedgerItem.query.filter(
        ledger_id__in=ledger_ids,
        field='huiyuan',
        huiyuan_id=str(huiyuan_id),
        item_type=ClubMigrateLedgerItem.ITEM_HUIYUAN,
    ):
        meta = it.meta_json or {}
        try:
            v = int(meta.get('formal_purchase_floor') or 0)
        except (TypeError, ValueError):
            v = 0
        if v > floor:
            floor = v
    return floor


def has_formal_member_purchase(yonghuid, huiyuan_id, club_id, exclude_dingdan_id=None):
    """是否已有成功支付的正式会员单（用于禁止再买体验）。"""
    club_id = club_id or CLUB_ID_DEFAULT
    if count_formal_paid_member_orders(yonghuid, huiyuan_id, club_id, exclude_dingdan_id) > 0:
        return True
    # 跨店迁入正式会员视同已购正式（不可再买体验）
    return migrate_formal_purchase_floor(yonghuid, huiyuan_id, club_id) >= 1


def has_any_prior_paid_member_recharge(yonghuid, exclude_dingdan_id=None):
    """是否已有成功支付的会员充值单（任意会员类型；不含后台赠送 jine=0）。"""
    qs = Czjilu.query.filter(
        yonghuid=yonghuid,
        leixing=MEMBER_LEIXING,
        zhuangtai=MEMBER_PAID_STATUS,
        jine__gt=0,
    )
    if exclude_dingdan_id:
        qs = qs.exclude(dingdan_id=exclude_dingdan_id)
    return qs.exists()


def _sync_trial_usage_flags(yonghuid, huiyuan_id, club_id):
    """根据 czjilu 历史回填 huiyuangoumai.has_used_trial。"""
    goumai = Huiyuangoumai.query.filter(
        yonghu_id=yonghuid, huiyuan_id=huiyuan_id,
    ).first()
    if not goumai:
        return
    if has_paid_trial_purchase(yonghuid, huiyuan_id, club_id) and not goumai.has_used_trial:
        goumai.has_used_trial = True
        goumai.save(update_fields=['has_used_trial', 'UpdateTime'])


def can_purchase_trial(yonghuid, huiyuan_id, club_id):
    """体验会员是否允许购买。"""
    club_id = club_id or CLUB_ID_DEFAULT
    sellable, _, _ = club_huiyuan_sellable(club_id, huiyuan_id, is_trial=True)
    if not sellable:
        return False, '本俱乐部未开启体验会员'

    _sync_trial_usage_flags(yonghuid, huiyuan_id, club_id)

    if has_paid_trial_purchase(yonghuid, huiyuan_id, club_id):
        return False, '体验会员仅可购买一次'

    goumai = Huiyuangoumai.query.filter(
        yonghu_id=yonghuid, huiyuan_id=huiyuan_id,
    ).first()
    if goumai and goumai.has_used_trial:
        return False, '体验会员仅可购买一次'

    if has_formal_member_purchase(yonghuid, huiyuan_id, club_id):
        return False, '已购买过正式会员，不可再购买体验版'

    return True, None


def validate_member_purchase(yonghuid, huiyuan_id, club_id, is_trial):
    """下单前校验。"""
    is_trial = bool(is_trial)
    if is_trial:
        ok, msg = can_purchase_trial(yonghuid, huiyuan_id, club_id)
        if not ok:
            return False, msg, Decimal('0'), 0
        sellable, price, days = club_huiyuan_sellable(club_id, huiyuan_id, is_trial=True)
    else:
        sellable, price, days = club_huiyuan_sellable(club_id, huiyuan_id, is_trial=False)
        if not sellable:
            return False, '该俱乐部未配置此会员或已下架', Decimal('0'), 0

    if not sellable:
        return False, '会员不可购买', Decimal('0'), 0
    return True, None, price, days


def resolve_formal_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id):
    """
    打手第几次正式购买该会员（duoci_fenhong.cishu）。
    体验单（含历史脏数据）不计入。
    跨店迁入会员：按 formal_purchase_floor 抬高下限，迁入后再充不可能算首次。
    """
    club_id = club_id or CLUB_ID_DEFAULT
    prior_paid = count_formal_paid_member_orders(
        yonghuid, huiyuan_id, club_id, current_dingdan_id,
    )
    floor = migrate_formal_purchase_floor(yonghuid, huiyuan_id, club_id)
    return max(prior_paid, floor) + 1


def _truncate_shuoming(text, max_len=100):
    text = (text or '').strip()
    return text[:max_len] if text else ''


def _mark_member_order_failed(order, reason, *, record_income=False, club_id=None):
    """已收款或已扣款但履约失败：写失败状态，不发会员/积分/分红。"""
    club_id = club_id or getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
    base = (order.shuoming or '').strip()
    order.zhuangtai = MEMBER_FAILED_STATUS
    order.shuoming = _truncate_shuoming(f'{base} | 履约失败:{reason}')
    order.save(update_fields=['zhuangtai', 'shuoming', 'UpdateTime'])
    if record_income:
        try:
            _record_member_income(order, club_id)
        except MemberRechargeError:
            logger.error('失败单记收入异常 dingdan=%s', order.dingdan_id, exc_info=True)
    logger.error('会员订单履约失败 dingdan=%s reason=%s', order.dingdan_id, reason)


def _require_paid_order_for_benefits(order):
    """仅已支付(3)订单可发会员权益/积分/分红。"""
    if int(getattr(order, 'zhuangtai', 0) or 0) != MEMBER_PAID_STATUS:
        raise MemberRechargeError(
            f'订单未支付成功(zhuangtai={order.zhuangtai})，禁止发放权益',
            'not_paid',
        )


def _validate_member_fulfillment(order, club_id):
    """履约前二次校验（防止待支付旧单绕过下单校验）。"""
    club_id = club_id or CLUB_ID_DEFAULT
    is_trial = bool(getattr(order, 'is_trial', False))
    if is_trial or _order_is_trial_like(order, club_id):
        ok, msg = can_purchase_trial(order.yonghuid, order.huiyuan_id, club_id)
        if not ok:
            raise MemberRechargeError(msg or '体验会员不可购买', 'trial_not_allowed')
        return
    ok, msg, _, _ = validate_member_purchase(
        order.yonghuid, order.huiyuan_id, club_id, is_trial=False,
    )
    if not ok:
        raise MemberRechargeError(msg or '会员不可购买', 'purchase_not_allowed')


# 兼容旧名
def resolve_member_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id):
    return resolve_formal_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id)


def fulfill_member_balance_purchase(czjilu, huiyuan, club_id=None):
    """余额抵扣购买会员：按俱乐部配置天数履约，并计入购买次数/分红。"""
    club_id = club_id or getattr(czjilu, 'club_id', None) or CLUB_ID_DEFAULT
    is_trial = bool(getattr(czjilu, 'is_trial', False))
    if is_trial:
        raise MemberRechargeError(
            '体验会员仅支持微信支付，不可用佣金、分红或押金抵扣',
            'trial_balance_forbidden',
        )

    ok, err_msg, _, _ = validate_member_purchase(
        czjilu.yonghuid, czjilu.huiyuan_id, club_id, is_trial=False,
    )
    if not ok:
        raise MemberRechargeError(err_msg or '会员不可购买', 'purchase_not_allowed')

    purchase_days = int(getattr(czjilu, 'purchase_days', None) or 0)
    if purchase_days <= 0:
        _, _, purchase_days = club_huiyuan_sellable(club_id, czjilu.huiyuan_id, is_trial=False)

    formal_cishu = resolve_formal_purchase_cishu(
        czjilu.yonghuid, czjilu.huiyuan_id, czjilu.dingdan_id, club_id,
    )
    trial_guanshi_fc = Decimal('0')
    trial_zuzhangfc = Decimal('0')

    czjilu.zhuangtai = MEMBER_PAID_STATUS
    if not czjilu.purchase_days:
        czjilu.purchase_days = purchase_days
    czjilu.is_trial = False
    czjilu.save(update_fields=['zhuangtai', 'purchase_days', 'is_trial'])
    _require_paid_order_for_benefits(czjilu)

    goumai_record, _ = _apply_huiyuan_entitlement(
        czjilu.yonghuid, czjilu.huiyuan_id, huiyuan, club_id, False, purchase_days,
    )
    new_jifen = _apply_member_purchase_jifen(
        czjilu.yonghuid, is_trial=False, huiyuan_id=czjilu.huiyuan_id,
        club_id=club_id, current_dingdan_id=czjilu.dingdan_id,
    )

    huiyuan.goumai_cishu = (huiyuan.goumai_cishu or 0) + 1
    huiyuan.save(update_fields=['goumai_cishu'])

    _apply_member_fenhong(
        czjilu, huiyuan,
        formal_cishu=formal_cishu,
        trial_guanshi_fc=trial_guanshi_fc,
        trial_zuzhangfc=trial_zuzhangfc,
    )

    return goumai_record, new_jifen


def _lookup_duoci_fenhong(club_id, huiyuan_id, beneficiary_id, cishu, field):
    """返回 (金额, 是否已配置行)。已配置为 0 时不应再回落到永久分红。"""
    try:
        row = DuociFenhong.query.get(
            club_id=club_id or CLUB_ID_DEFAULT,
            huiyuan=huiyuan_id,
            yonghuid=beneficiary_id,
            cishu=cishu,
        )
        return Decimal(str(getattr(row, field) or 0)), True
    except DuociFenhong.DoesNotExist:
        return None, False


def calc_guanshi_member_fenhong(huiyuan_id, guanshi_id, cishu, club_id, default_guanshi_fc):
    club_id = club_id or CLUB_ID_DEFAULT
    custom, configured = _lookup_duoci_fenhong(
        club_id, huiyuan_id, guanshi_id, cishu, 'guanshi_fenhong',
    )
    if configured:
        return custom if custom > 0 else None
    if cishu == 1:
        base = default_guanshi_fc if default_guanshi_fc is not None else Decimal('0')
        return base if base > 0 else None
    try:
        guanshi = UserGuanshi.query.get(user__UserUID=guanshi_id)
        if guanshi.fenghong_erci_enabled:
            permanent = Decimal(str(guanshi.fenghong_erci or 0))
            if permanent > 0:
                return permanent
    except UserGuanshi.DoesNotExist:
        pass
    return None


def calc_zuzhang_member_fenhong(huiyuan_id, zuzhang_id, cishu, club_id, default_zuzhang_fc):
    if not zuzhang_id:
        return None
    club_id = club_id or CLUB_ID_DEFAULT
    custom, configured = _lookup_duoci_fenhong(
        club_id, huiyuan_id, zuzhang_id, cishu, 'zuzhang_fenhong',
    )
    if configured:
        return custom if custom > 0 else None
    if cishu == 1:
        base = default_zuzhang_fc if default_zuzhang_fc is not None else Decimal('0')
        return base if base > 0 else None
    try:
        zuzhang = UserZuzhang.query.get(user__UserUID=zuzhang_id)
        if zuzhang.kaioi_ewai_fenhong:
            permanent = Decimal(str(zuzhang.ewai_fenhong_jine or 0))
            if permanent > 0:
                return permanent
    except UserZuzhang.DoesNotExist:
        pass
    return None


def _calc_new_daoqi(existing, days):
    """未过期叠加；已过期从今天算。"""
    now = timezone.now()
    days = max(1, int(days))
    if existing and existing.daoqi_time and existing.daoqi_time > now and existing.huiyuan_zhuangtai == 1:
        return existing.daoqi_time + timedelta(days=days)
    return now + timedelta(days=days)


def _record_member_income(order, club_id):
    """会员充值收入入账（幂等 biz_ref=cz:dingdan_id）。"""
    jine_decimal = Decimal(str(order.jine))
    try:
        from jituan.services.szjilu_accounting import record_wechat_income_once
        ok = record_wechat_income_once(jine_decimal, club_id, biz_ref=f'cz:{order.dingdan_id}')
        if not ok:
            from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
            repair_wechat_income_if_missing(jine_decimal, club_id, biz_ref=f'cz:{order.dingdan_id}')
        logger.info(
            '会员收入入账 dingdan=%s club=%s jine=%s is_trial=%s',
            order.dingdan_id, club_id, jine_decimal, getattr(order, 'is_trial', False),
        )
    except Exception as exc:
        logger.error(
            '会员收入入账失败 dingdan=%s club=%s jine=%s err=%s',
            order.dingdan_id, club_id, order.jine, exc, exc_info=True,
        )
        raise MemberRechargeError('收入统计失败，请联系客服', 'income_error')


def _apply_huiyuan_entitlement(yonghuid, huiyuan_id, huiyuan, club_id, is_trial, purchase_days):
    """开通/续期会员权益。"""
    is_trial = bool(is_trial)
    purchase_days = max(1, int(purchase_days))
    existing = Huiyuangoumai.query.filter(
        yonghu_id=yonghuid, huiyuan_id=huiyuan_id,
    ).first()
    is_first_buy_any = False
    new_daoqi = _calc_new_daoqi(existing, purchase_days)

    if existing:
        existing.daoqi_time = new_daoqi
        existing.huiyuan_zhuangtai = 1
        existing.jieshao = huiyuan.jieshao
        if is_trial:
            existing.is_trial = True
            existing.has_used_trial = True
        else:
            existing.is_trial = False
            if has_paid_trial_purchase(yonghuid, huiyuan_id, club_id):
                existing.has_used_trial = True
        existing.save(update_fields=[
            'daoqi_time', 'huiyuan_zhuangtai', 'jieshao', 'is_trial', 'has_used_trial', 'UpdateTime',
        ])
        record = existing
    else:
        other = Huiyuangoumai.query.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count()
        if other == 0:
            is_first_buy_any = True
        used_trial_before = has_paid_trial_purchase(yonghuid, huiyuan_id, club_id)
        resolved_club = club_id or get_user_club_id(User.query.filter(UserUID=yonghuid).first())
        record = Huiyuangoumai.query.create(
            yonghu_id=yonghuid,
            huiyuan_id=huiyuan_id,
            jieshao=huiyuan.jieshao,
            huiyuan_zhuangtai=1,
            daoqi_time=new_daoqi,
            club_id=resolved_club,
            is_trial=is_trial,
            has_used_trial=is_trial or used_trial_before,
        )
    return record, is_first_buy_any


def _apply_member_fenhong(order, huiyuan, formal_cishu=None, trial_guanshi_fc=None, trial_zuzhang_fc=None):
    if int(getattr(order, 'zhuangtai', 0) or 0) != MEMBER_PAID_STATUS:
        logger.warning(
            '订单未支付成功，禁止分红 dingdan=%s zhuangtai=%s',
            order.dingdan_id, order.zhuangtai,
        )
        return
    if Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists():
        logger.info('会员分红已处理，跳过: %s', order.dingdan_id)
        return

    try:
        dashou = UserDashou.query.get(user__UserUID=order.yonghuid)
    except UserDashou.DoesNotExist:
        logger.warning('打手不存在，跳过会员分红: %s', order.yonghuid)
        return

    guanshi_id = dashou.yaoqingren
    if not guanshi_id:
        logger.info('打手%s无邀请管事，跳过会员分红', order.yonghuid)
        return

    try:
        guanshi = UserGuanshi.query.get(user__UserUID=guanshi_id)
    except UserGuanshi.DoesNotExist:
        logger.warning('管事不存在: %s', guanshi_id)
        return

    zuzhang_id = guanshi.yaoqingren
    zuzhang = None
    if zuzhang_id:
        try:
            zuzhang = UserZuzhang.query.get(user__UserUID=zuzhang_id)
        except UserZuzhang.DoesNotExist:
            logger.warning('组长不存在: %s', zuzhang_id)

    club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
    huiyuan_id = huiyuan.huiyuan_id
    order_amount = Decimal(str(order.jine))
    is_trial_order = _order_is_trial_like(order, club_id)

    if is_trial_order:
        guanshi_fenhong = Decimal(str(trial_guanshi_fc or 0))
        zuzhang_fenhong = Decimal(str(trial_zuzhang_fc or 0)) if zuzhang_id else Decimal('0')
        cishu = None
    else:
        cishu = formal_cishu or resolve_formal_purchase_cishu(
            order.yonghuid, huiyuan_id, order.dingdan_id, club_id,
        )
        guanshi_fc, zuzhang_fc = get_huiyuan_fenchong(club_id, huiyuan_id)
        guanshi_fenhong = calc_guanshi_member_fenhong(
            huiyuan_id, guanshi_id, cishu, club_id, guanshi_fc,
        )
        guanshi_fenhong = guanshi_fenhong if guanshi_fenhong is not None else Decimal('0')
        zuzhang_fenhong = calc_zuzhang_member_fenhong(
            huiyuan_id, zuzhang_id, cishu, club_id, zuzhang_fc,
        )
        zuzhang_fenhong = zuzhang_fenhong if zuzhang_fenhong is not None else Decimal('0')

    total = guanshi_fenhong + zuzhang_fenhong
    if total > order_amount:
        logger.warning('分红总额%s超订单%s，组长置0', total, order_amount)
        zuzhang_fenhong = Decimal('0')
        total = guanshi_fenhong
        if total > order_amount:
            logger.warning('管事分红%s超订单，置0', guanshi_fenhong)
            guanshi_fenhong = Decimal('0')

    if guanshi_fenhong <= 0 and zuzhang_fenhong <= 0:
        logger.info(
            '本次无会员分红 dingdan=%s club=%s cishu=%s is_trial=%s',
            order.dingdan_id, club_id, cishu, is_trial_order,
        )
        return

    user_main = dashou.user
    dingdan_id = str(order.dingdan_id or '')
    from jituan.services.fund_freeze import credit_guanshi_fenhong, credit_zuzhang_fenhong

    if guanshi_fenhong > 0:
        result = credit_guanshi_fenhong(
            user_id=str(guanshi_id),
            amount=guanshi_fenhong,
            biz_id=f'{dingdan_id}:guanshi:member',
            profile=guanshi,
            czjilu_id=dingdan_id,
            freeze_reason=f'会员充值分红 {dingdan_id}',
        )
        if result.get('idempotent'):
            logger.warning('会员管事分红幂等跳过 dingdan=%s guanshi=%s', dingdan_id, guanshi_id)
        else:
            type(guanshi).query.filter(pk=guanshi.pk).update(
                chongzhifenrun=F('chongzhifenrun') + guanshi_fenhong,
                jinrichongzhi=F('jinrichongzhi') + 1,
                jinyuechongzhi=F('jinyuechongzhi') + 1,
            )

    if zuzhang_fenhong > 0 and zuzhang:
        result = credit_zuzhang_fenhong(
            user_id=str(zuzhang_id),
            amount=zuzhang_fenhong,
            biz_id=f'{dingdan_id}:zuzhang:member',
            profile=zuzhang,
            czjilu_id=dingdan_id,
            freeze_reason=f'会员充值分红 {dingdan_id}',
        )
        if result.get('idempotent'):
            logger.warning('会员组长分红幂等跳过 dingdan=%s zuzhang=%s', dingdan_id, zuzhang_id)
        else:
            type(zuzhang).query.filter(pk=zuzhang.pk).update(
                fenyong_zonge=F('fenyong_zonge') + zuzhang_fenhong,
                jinri_fenyong=F('jinri_fenyong') + zuzhang_fenhong,
                jinyue_fenyong=F('jinyue_fenyong') + zuzhang_fenhong,
            )

    Gsfenhong.query.create(
        dingdan_id=order.dingdan_id,
        guanshi=guanshi_id,
        dashouid=order.yonghuid,
        shuoming=order.shuoming,
        fenhong=guanshi_fenhong,
        avatar=user_main.Avatar,
        nicheng=dashou.nicheng or '未知打手',
        zuzhang_id=zuzhang_id if zuzhang_fenhong > 0 else None,
        zuzhang_fenhong=zuzhang_fenhong if zuzhang_fenhong > 0 else None,
        huiyuan_id=huiyuan_id,
        fenhong_leixing=MEMBER_FENHONG_LEIXING,
        club_id=resolve_gsfenhong_club_id(dingdan_id=order.dingdan_id, czjilu=order),
        is_trial=is_trial_order,
    )

    if guanshi_fenhong > 0 and not is_trial_order and cishu is not None:
        try:
            if cishu == 1:
                update_guanshi_daily_by_action(yonghuid=guanshi_id, action=2, amount=guanshi_fenhong)
            else:
                update_guanshi_xufei_daily(yonghuid=guanshi_id, xufei_jine=guanshi_fenhong)
        except Exception as exc:
            logger.error('管事日统计更新失败: %s', exc)

    if zuzhang_fenhong > 0 and zuzhang_id:
        try:
            update_zuzhang_daily_by_action(yonghuid=zuzhang_id, action=2, amount=zuzhang_fenhong)
        except Exception as exc:
            logger.error('组长日统计更新失败: %s', exc)

    logger.info(
        '会员分红完成 dingdan=%s club=%s formal_cishu=%s is_trial=%s 管事=%s 组长=%s',
        order.dingdan_id, club_id, cishu, is_trial_order, guanshi_fenhong, zuzhang_fenhong,
    )


def _apply_member_purchase_jifen(yonghuid, is_trial, huiyuan_id, club_id, current_dingdan_id=None):
    """
    会员购积分：首次充值会员（体验/正式/任意类型）加满至 10；此后不再加分。
    """
    try:
        dashou = UserDashou.query.get(user__UserUID=yonghuid)
    except UserDashou.DoesNotExist:
        logger.warning('会员购积分失败，打手不存在: %s', yonghuid)
        return 0

    current = int(dashou.jifen or 0)
    if current >= MEMBER_JIFEN_CAP:
        return current

    if has_any_prior_paid_member_recharge(yonghuid, exclude_dingdan_id=current_dingdan_id):
        logger.info(
            '会员购积分跳过（非首次充值） yonghu=%s is_trial=%s huiyuan=%s jifen=%s',
            yonghuid, is_trial, huiyuan_id, current,
        )
        return current

    delta = MEMBER_JIFEN_CAP - current
    if delta <= 0:
        return current

    dashou.jifen = min(current + delta, MEMBER_JIFEN_CAP)
    dashou.save(update_fields=['jifen'])
    logger.info(
        '会员购积分 yonghu=%s is_trial=%s huiyuan=%s +%s -> %s',
        yonghuid, is_trial, huiyuan_id, delta, dashou.jifen,
    )
    return dashou.jifen


def _add_first_buy_jifen(yonghuid):
    """兼容旧调用：等价于直接买正式且从未购会员积分。"""
    logger.warning('_add_first_buy_jifen 已废弃，请改用 _apply_member_purchase_jifen')
    try:
        dashou = UserDashou.query.get(user__UserUID=yonghuid)
    except UserDashou.DoesNotExist:
        return
    current = int(dashou.jifen or 0)
    if current >= MEMBER_JIFEN_CAP:
        return
    dashou.jifen = MEMBER_JIFEN_CAP
    dashou.save(update_fields=['jifen'])


def _czjilu_entitlement_revoked(order):
    return ENTITLEMENT_REVOKED_MARKER in (getattr(order, 'shuoming', None) or '')


def _czjilu_entitlement_end(order, club_id=None):
    """充值单对应权益的原定到期时刻（CreateTime + purchase_days）。"""
    from jituan.services.huiyuan_bundle import _normalize_datetime_for_compare

    cid = getattr(order, 'club_id', None) or club_id or CLUB_ID_DEFAULT
    is_trial = _order_is_trial_like(order, cid)
    days = int(getattr(order, 'purchase_days', None) or 0)
    if days <= 0:
        _, _, days = club_huiyuan_sellable(cid, order.huiyuan_id, is_trial=is_trial)
    days = max(int(days or 0), 1)
    start = _normalize_datetime_for_compare(order.CreateTime or timezone.now())
    return start + timedelta(days=days)


def czjilu_entitlement_active(order, club_id=None):
    """充值单权益是否仍在原定期限内（未撤销且未过期）。"""
    if not order or _czjilu_entitlement_revoked(order):
        return False
    from jituan.services.huiyuan_bundle import _normalize_datetime_for_compare
    now = _normalize_datetime_for_compare(timezone.now())
    return _czjilu_entitlement_end(order, club_id) > now


def user_has_active_czjilu_entitlement(yonghu_id, huiyuan_id, club_id=None):
    """用户是否仍有未过期的已付充值单支撑该会员类型。"""
    from jituan.services.huiyuan_bundle import huiyuan_ids_match

    for order in Czjilu.query.filter(
        yonghuid=yonghu_id,
        leixing=MEMBER_LEIXING,
        zhuangtai=MEMBER_PAID_STATUS,
    ).order_by('-CreateTime'):
        if not huiyuan_ids_match(order.huiyuan_id, huiyuan_id):
            continue
        if czjilu_entitlement_active(order, club_id):
            return True
    return False


def _paid_member_orders_for(yonghu_id, huiyuan_id):
    from jituan.services.huiyuan_bundle import huiyuan_ids_match

    return [
        o for o in Czjilu.query.filter(
            yonghuid=yonghu_id,
            leixing=MEMBER_LEIXING,
            zhuangtai=MEMBER_PAID_STATUS,
        )
        if huiyuan_ids_match(o.huiyuan_id, huiyuan_id)
    ]


def order_purchase_days_locked(order, club_id=None):
    """充值单锁定的有效天数（与履约/repair 同源）。"""
    cid = getattr(order, 'club_id', None) or club_id or CLUB_ID_DEFAULT
    is_trial = _order_is_trial_like(order, cid)
    days = int(getattr(order, 'purchase_days', None) or 0)
    if days <= 0:
        _, _, days = club_huiyuan_sellable(cid, order.huiyuan_id, is_trial=is_trial)
    return max(int(days or 0), 1)


def _parse_since_option(since):
    """管理命令 --since 字符串 → 可比较的 datetime。"""
    from django.utils.dateparse import parse_date, parse_datetime
    from jituan.services.huiyuan_bundle import _normalize_datetime_for_compare

    if not since:
        return None
    if not isinstance(since, str):
        return _normalize_datetime_for_compare(since)
    dt = parse_datetime(since)
    if dt is None:
        d = parse_date(since)
        if d is None:
            return None
        dt = datetime.combine(d, datetime.min.time())
    return _normalize_datetime_for_compare(dt)


def is_suspected_bug_repair_restore(rec, since=None):
    """
    Bug repair 指纹：UpdateTime 时将 daoqi 设为 UpdateTime+days，
    且存在匹配充值单的原定期限在 UpdateTime 之前已结束。
    正常续费：必有未过期充值单支撑，或 daoqi 与充值单原定期限一致。
    """
    from jituan.services.huiyuan_bundle import _normalize_datetime_for_compare

    updated = _normalize_datetime_for_compare(rec.UpdateTime)
    since_dt = _parse_since_option(since)
    if since_dt and updated < since_dt:
        return None
    daoqi = _normalize_datetime_for_compare(rec.daoqi_time)
    gap_days = (daoqi - updated).days
    if gap_days < 1 or gap_days > 400:
        return None

    for order in _paid_member_orders_for(rec.yonghu_id, rec.huiyuan_id):
        if _czjilu_entitlement_revoked(order):
            continue
        days = order_purchase_days_locked(order, rec.club_id)
        if abs(days - gap_days) > 2:
            continue
        if _czjilu_entitlement_end(order, rec.club_id) < updated:
            return order.dingdan_id
    return None


def should_rollback_erroneous_membership(rec, since=None, require_repair_signature=True):
    """
    是否应收回错误恢复的会员（保守策略，优先不误伤正常付费用户）。

    硬性豁免：任意未过期已付充值单支撑 → 绝不收回。
    """
    if rec.jiance_shifou_daoqi() or rec.huiyuan_zhuangtai != 1:
        return False, None
    if user_has_active_czjilu_entitlement(rec.yonghu_id, rec.huiyuan_id, rec.club_id):
        return False, '有未过期充值单支撑'
    orders = _paid_member_orders_for(rec.yonghu_id, rec.huiyuan_id)
    if not orders:
        return False, '无充值单不自动处理'
    if require_repair_signature:
        dingdan_id = is_suspected_bug_repair_restore(rec, since=since)
        if not dingdan_id:
            return False, '不符合 bug repair 指纹'
        return True, dingdan_id
    return True, None


def expire_huiyuangoumai_record(rec, reason=''):
    """将会员记录置为已过期（不删行，便于审计）。"""
    from jituan.services.huiyuan_bundle import _normalize_datetime_for_compare

    now = _normalize_datetime_for_compare(timezone.now())
    rec.daoqi_time = now - timedelta(seconds=1)
    rec.huiyuan_zhuangtai = 0
    rec.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai', 'UpdateTime'])
    logger.warning(
        '会员权益已收回 yonghu=%s huiyuan=%s reason=%s',
        rec.yonghu_id, rec.huiyuan_id, reason or 'cleanup',
    )
    return True


def repair_member_entitlement_if_paid(dingdan_id):
    order = Czjilu.query.filter(dingdan_id=dingdan_id, leixing=MEMBER_LEIXING).first()
    if not order or order.zhuangtai != MEMBER_PAID_STATUS or not order.huiyuan_id:
        return False
    if _czjilu_entitlement_revoked(order):
        return False

    from jituan.services.huiyuan_bundle import huiyuan_ids_match
    for rec in Huiyuangoumai.query.filter(yonghu_id=order.yonghuid):
        if huiyuan_ids_match(rec.huiyuan_id, order.huiyuan_id):
            if not rec.jiance_shifou_daoqi() and rec.huiyuan_zhuangtai == 1:
                return True

    try:
        huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id)
    except Huiyuan.DoesNotExist:
        logger.error('补开通会员失败 dingdan=%s huiyuan=%s', dingdan_id, order.huiyuan_id)
        return False

    club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
    is_trial = _order_is_trial_like(order, club_id)
    days = int(getattr(order, 'purchase_days', None) or 0)
    if days <= 0:
        _, _, days = club_huiyuan_sellable(club_id, order.huiyuan_id, is_trial=is_trial)
    days = max(int(days or 0), 1)
    if not czjilu_entitlement_active(order, club_id):
        return False
    with transaction.atomic():
        _apply_huiyuan_entitlement(
            order.yonghuid, order.huiyuan_id, huiyuan, club_id, is_trial, days,
        )
        _apply_member_purchase_jifen(
            order.yonghuid, is_trial=is_trial, huiyuan_id=order.huiyuan_id,
            club_id=club_id, current_dingdan_id=order.dingdan_id,
        )
        if not Gsfenhong.query.filter(dingdan_id=dingdan_id, fenhong_leixing=MEMBER_FENHONG_LEIXING).exists():
            trial_guanshi_fc = Decimal('0')
            trial_zuzhang_fc = Decimal('0')
            formal_cishu = None
            if is_trial:
                row = get_club_huiyuan_row(club_id, order.huiyuan_id)
                if row:
                    trial_guanshi_fc = Decimal(str(row.trial_guanshifc or 0))
                    trial_zuzhang_fc = Decimal(str(row.trial_zuzhangfc or 0))
            else:
                formal_cishu = resolve_formal_purchase_cishu(
                    order.yonghuid, order.huiyuan_id, order.dingdan_id, club_id,
                )
            _apply_member_fenhong(
                order, huiyuan,
                formal_cishu=formal_cishu,
                trial_guanshi_fc=trial_guanshi_fc,
                trial_zuzhang_fc=trial_zuzhang_fc,
            )
    logger.warning('已补开通会员 dingdan=%s', dingdan_id)
    return True


def sync_member_entitlements_for_user(yonghu_id, limit=15):
    """支付回调偶发失败时补权益；禁止在 dddhq 每次请求全量扫单（会覆盖后台移除）。"""
    return repair_recent_paid_entitlements_if_missing(yonghu_id, limit=limit)


def repair_recent_paid_entitlements_if_missing(yonghu_id, limit=3):
    """仅当用户当前无任何有效会员时，尝试补最近几笔已付单。"""
    from jituan.services.huiyuan_bundle import _membership_record_active

    if not yonghu_id:
        return 0
    has_active = any(
        _membership_record_active(rec)
        for rec in Huiyuangoumai.query.filter(yonghu_id=yonghu_id)
    )
    if has_active:
        return 0

    orders = Czjilu.query.filter(
        yonghuid=yonghu_id,
        leixing=MEMBER_LEIXING,
        zhuangtai=MEMBER_PAID_STATUS,
    ).order_by('-CreateTime')[: max(1, int(limit or 3))]
    fixed = 0
    for order in orders:
        if _czjilu_entitlement_revoked(order):
            continue
        try:
            if repair_member_entitlement_if_paid(order.dingdan_id):
                fixed += 1
        except Exception as exc:
            logger.error('补会员权益失败 dingdan=%s err=%s', order.dingdan_id, exc)
    return fixed


def revoke_member_entitlement_admin(yonghu_id, huiyuan_id, club_id=None):
    """后台移除会员：删 huiyuangoumai + 标记充值单已撤销，防止被 sync 补回。"""
    from jituan.services.huiyuan_bundle import huiyuan_ids_match, normalize_huiyuan_id

    hid = normalize_huiyuan_id(huiyuan_id)
    if not hid:
        return 0, '缺少会员ID'

    removed = 0
    for rec in list(Huiyuangoumai.query.filter(yonghu_id=yonghu_id)):
        if huiyuan_ids_match(rec.huiyuan_id, hid):
            rec.delete()
            removed += 1

    for order in Czjilu.query.filter(
        yonghuid=yonghu_id,
        leixing=MEMBER_LEIXING,
        zhuangtai=MEMBER_PAID_STATUS,
    ):
        if not huiyuan_ids_match(order.huiyuan_id, hid):
            continue
        if _czjilu_entitlement_revoked(order):
            continue
        note = (order.shuoming or '').strip()
        order.shuoming = _truncate_shuoming(
            f'{note} |{ENTITLEMENT_REVOKED_MARKER}|' if note else ENTITLEMENT_REVOKED_MARKER,
        )
        order.save(update_fields=['shuoming'])

    if removed == 0:
        return 0, '该打手未购买此会员'
    return removed, None


@transaction.atomic
def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback'):
    locked = Czjilu.query.filter(dingdan_id=dingdan_id).select_for_update()
    order = locked.first()
    if not order:
        raise MemberRechargeError('订单不存在', 'not_found')

    if order.leixing != MEMBER_LEIXING:
        raise MemberRechargeError('订单类型不是会员充值', 'type_error')

    if order.zhuangtai not in MEMBER_FULFILLABLE_STATUSES:
        logger.info(
            '会员订单已处理，跳过履约 dingdan=%s zhuangtai=%s source=%s',
            dingdan_id, order.zhuangtai, source,
        )
        try:
            club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
            _record_member_income(order, club_id)
        except MemberRechargeError:
            pass
        return {
            'already_done': True,
            'dingdan_id': dingdan_id,
            'zhuangtai': order.zhuangtai,
        }

    if paid_amount_yuan is not None:
        paid = Decimal(str(paid_amount_yuan))
        order_amt = Decimal(str(order.jine))
        if abs(paid - order_amt) > Decimal('0.01'):
            raise MemberRechargeError(
                f'支付金额{paid}与订单{order_amt}不一致', 'amount_mismatch',
            )

    huiyuan = Huiyuan.query.filter(huiyuan_id=order.huiyuan_id).first()
    if not huiyuan:
        raise MemberRechargeError(f'会员不存在: {order.huiyuan_id}', 'huiyuan_not_found')

    club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
    try:
        _validate_member_fulfillment(order, club_id)
    except MemberRechargeError as exc:
        _mark_member_order_failed(
            order, str(exc),
            record_income=paid_amount_yuan is not None,
            club_id=club_id,
        )
        raise

    is_trial = _order_is_trial_like(order, club_id)
    if is_trial and not order.is_trial:
        order.is_trial = True
        order.save(update_fields=['is_trial'])

    purchase_days = order.purchase_days or (30 if not is_trial else 0)
    if purchase_days <= 0:
        _, _, purchase_days = club_huiyuan_sellable(club_id, order.huiyuan_id, is_trial=is_trial)

    formal_cishu = None
    trial_guanshi_fc = Decimal('0')
    trial_zuzhang_fc = Decimal('0')
    if is_trial:
        row = get_club_huiyuan_row(club_id, order.huiyuan_id)
        if row:
            trial_guanshi_fc = Decimal(str(row.trial_guanshifc or 0))
            trial_zuzhang_fc = Decimal(str(row.trial_zuzhangfc or 0))
    else:
        formal_cishu = resolve_formal_purchase_cishu(
            order.yonghuid, order.huiyuan_id, order.dingdan_id, club_id,
        )

    order.zhuangtai = MEMBER_PAID_STATUS
    if not order.purchase_days:
        order.purchase_days = purchase_days
    if is_trial:
        order.is_trial = True
    order.save(update_fields=['zhuangtai', 'purchase_days', 'is_trial'])
    _require_paid_order_for_benefits(order)

    goumai_record, _ = _apply_huiyuan_entitlement(
        order.yonghuid, order.huiyuan_id, huiyuan, club_id, is_trial, purchase_days,
    )
    _apply_member_purchase_jifen(
        order.yonghuid, is_trial=is_trial, huiyuan_id=order.huiyuan_id,
        club_id=club_id, current_dingdan_id=order.dingdan_id,
    )

    huiyuan.goumai_cishu = (huiyuan.goumai_cishu or 0) + 1
    huiyuan.save(update_fields=['goumai_cishu'])

    _record_member_income(order, club_id)

    _apply_member_fenhong(
        order, huiyuan,
        formal_cishu=formal_cishu,
        trial_guanshi_fc=trial_guanshi_fc,
        trial_zuzhang_fc=trial_zuzhang_fc,
    )

    logger.info(
        '会员充值履约完成 dingdan=%s source=%s club=%s is_trial=%s formal_cishu=%s jine=%s',
        dingdan_id, source, club_id, is_trial, formal_cishu, order.jine,
    )
    return {
        'already_done': False,
        'dingdan_id': dingdan_id,
        'zhuangtai': MEMBER_PAID_STATUS,
        'formal_cishu': formal_cishu,
        'is_trial': is_trial,
        'daoqi_time': goumai_record.daoqi_time,
        'huiyuan_id': order.huiyuan_id,
    }


def query_wechat_v2_trade_state(out_trade_no, club_id=None):
    club_id = club_id or club_id_from_czjilu(out_trade_no)
    cfg = get_wechat_v2_config(club_id)
    nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
    params = {
        'appid': cfg['appid'],
        'mch_id': cfg['mch_id'],
        'out_trade_no': out_trade_no,
        'nonce_str': nonce_str,
    }
    string_a = '&'.join(f'{k}={params[k]}' for k in sorted(params.keys()))
    sign = hashlib.md5(f'{string_a}&key={cfg["key"]}'.encode()).hexdigest().upper()
    params['sign'] = sign
    xml_body = ''.join(f'<{k}>{params[k]}</{k}>' for k in params)
    xml_data = f'<xml>{xml_body}</xml>'

    try:
        resp = requests.post(
            'https://api.mch.weixin.qq.com/pay/orderquery',
            data=xml_data.encode('utf-8'),
            headers={'Content-Type': 'application/xml'},
            timeout=10,
        )
        root = ET.fromstring(resp.content)
        if root.find('return_code').text != 'SUCCESS':
            return None, None
        if root.find('result_code').text != 'SUCCESS':
            return None, None
        trade_state = root.find('trade_state').text
        txn = root.find('transaction_id')
        transaction_id = txn.text if txn is not None else None
        return trade_state, transaction_id
    except Exception as exc:
        logger.error('微信查单失败 %s: %s', out_trade_no, exc)
        return None, None


def confirm_member_recharge_paid(dingdan_id, yonghuid):
    try:
        order = Czjilu.query.get(dingdan_id=dingdan_id, yonghuid=yonghuid)
    except Czjilu.DoesNotExist:
        raise MemberRechargeError('订单不存在或不属于当前用户', 'not_found')

    if order.leixing != MEMBER_LEIXING:
        raise MemberRechargeError('不是会员订单', 'type_error')

    if order.zhuangtai == MEMBER_PAID_STATUS:
        goumai = Huiyuangoumai.query.filter(
            yonghu_id=yonghuid, huiyuan_id=order.huiyuan_id,
        ).first()
        if not goumai:
            repair_member_entitlement_if_paid(dingdan_id)
            goumai = Huiyuangoumai.query.filter(
                yonghu_id=yonghuid, huiyuan_id=order.huiyuan_id,
            ).first()
        if not goumai:
            raise MemberRechargeError('订单已付但会员记录缺失，请联系客服', 'data_error')
        return {
            'fulfilled': False,
            'already_done': True,
            'zhuangtai': MEMBER_PAID_STATUS,
            'daoqi_time': goumai.daoqi_time,
            'huiyuan_id': order.huiyuan_id,
            'is_trial': goumai.is_trial,
        }

    if order.zhuangtai != MEMBER_PENDING_STATUS and order.zhuangtai != MEMBER_CANCELLED_STATUS:
        raise MemberRechargeError(f'订单状态异常: {order.zhuangtai}', 'status_error')

    club_id = getattr(order, 'club_id', None)
    from jituan.services.pay_confirm import confirm_remote_payment
    remote = confirm_remote_payment(dingdan_id, club_id, prefer_amount=order.jine)
    if not remote.get('ok'):
        if remote.get('pending'):
            raise MemberRechargeError('支付结果尚未同步，请稍后再试', 'not_paid')
        raise MemberRechargeError(remote.get('msg') or '支付未成功', 'not_paid')

    total_fee_yuan = Decimal(str(remote.get('amount') if remote.get('amount') is not None else order.jine))
    result = fulfill_member_recharge(
        dingdan_id, paid_amount_yuan=total_fee_yuan, source='confirm',
    )
    result['fulfilled'] = not result.get('already_done')
    return result


def dashou_in_active_trial_period(yonghuid):
    """打手是否处于未过期的体验会员期（禁止佣金提现；与抢单池展示逻辑无关）。"""
    from jituan.services.huiyuan_bundle import huiyuan_ids_match

    def _cmp_dt(dt):
        """统一成可比较的 datetime，避免 naive/aware 混比。"""
        if dt is None:
            return None
        if timezone.is_naive(dt):
            return timezone.make_aware(dt, timezone.get_current_timezone())
        return dt

    now = _cmp_dt(timezone.now())
    for rec in Huiyuangoumai.query.filter(yonghu_id=yonghuid):
        if rec.jiance_shifou_daoqi():
            continue
        if rec.huiyuan_zhuangtai != 1:
            continue
        if rec.is_trial:
            return True

    for order in Czjilu.query.filter(
        yonghuid=yonghuid,
        leixing=MEMBER_LEIXING,
        zhuangtai=MEMBER_PAID_STATUS,
    ).order_by('-CreateTime')[:30]:
        cid = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
        if not _order_is_trial_like(order, cid):
            continue
        hid = order.huiyuan_id
        if not hid:
            continue
        has_formal = False
        for rec in Huiyuangoumai.query.filter(yonghu_id=yonghuid):
            if rec.is_trial:
                continue
            if (
                huiyuan_ids_match(rec.huiyuan_id, hid)
                and not rec.jiance_shifou_daoqi()
                and rec.huiyuan_zhuangtai == 1
            ):
                has_formal = True
                break
        if has_formal:
            continue
        days = int(getattr(order, 'purchase_days', None) or 0)
        if days <= 0:
            _, _, days = club_huiyuan_sellable(cid, hid, is_trial=True)
        days = max(days, 1)
        start = _cmp_dt(order.CreateTime) or now
        if start + timedelta(days=days) > now:
            return True
    return False


def dashou_has_active_formal_huiyuan(yonghuid):
    """打手是否持有未过期的正式会员（非体验）。"""
    for rec in Huiyuangoumai.query.filter(yonghu_id=yonghuid):
        if rec.is_trial:
            continue
        if not rec.jiance_shifou_daoqi() and rec.huiyuan_zhuangtai == 1:
            return True
    return False


def extend_member_entitlement_admin(yonghuid, huiyuan_id, days, club_id=None, operator_note=''):
    """后台赠送正式会员：写 czjilu 审计记录，无支付、无分红、无积分。"""
    try:
        huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
    except Huiyuan.DoesNotExist:
        return None, '会员类型不存在'
    days = max(1, int(days))
    club_id = club_id or CLUB_ID_DEFAULT
    record, _ = _apply_huiyuan_entitlement(
        yonghuid, huiyuan_id, huiyuan, club_id, is_trial=False, purchase_days=days,
    )

    timestamp = str(int(timezone.now().timestamp() * 1000))[-12:]
    rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
    dingdan_id = f'ADM{timestamp}{rand_str}'
    note = (operator_note or '').strip()
    shuoming = f'后台赠送会员 {huiyuan.jieshao} {days}天'
    if note:
        shuoming = _truncate_shuoming(f'{shuoming} | {note}')

    Czjilu.query.create(
        dingdan_id=dingdan_id,
        zhuangtai=MEMBER_PAID_STATUS,
        yonghuid=yonghuid,
        jine=Decimal('0'),
        leixing=MEMBER_LEIXING,
        shuoming=shuoming,
        huiyuan_id=huiyuan_id,
        club_id=club_id,
        is_trial=False,
        purchase_days=days,
    )
    logger.info('后台赠送会员 yonghu=%s huiyuan=%s days=%s dingdan=%s', yonghuid, huiyuan_id, days, dingdan_id)
    return record, None
