"""资金冻结:解冻条件评估(实时算窗口指标,并回写角色 risk_* 快照)。""" from __future__ import annotations import logging from datetime import timedelta from decimal import Decimal, ROUND_HALF_UP from typing import Any, Dict, Optional, Tuple from django.utils import timezone logger = logging.getLogger(__name__) def _dec(v, default='0') -> Decimal: try: return Decimal(str(v if v is not None else default)).quantize( Decimal('0.000001'), rounding=ROUND_HALF_UP ) except Exception: return Decimal(default) def _int(v, default=0) -> int: try: return int(v) except (TypeError, ValueError): return default def _flag(cfg: dict, enable_key: str, legacy_active: bool) -> bool: """新配置用 enable_*;旧配置无 enable 时按 legacy_active 推断。""" if enable_key in cfg: return bool(cfg.get(enable_key)) return legacy_active def compute_risk_metrics( club_id: str, user_id: str, role: str, condition_json: Optional[dict] = None, ) -> Dict[str, Any]: """按窗口统计完成单量 / 罚款率 / 投诉率 / 有效邀请。失败时返回安全默认。""" cfg = condition_json or {} fine_days = max(1, _int(cfg.get('fine_rate_window_days'), 30)) complaint_days = max(1, _int(cfg.get('complaint_rate_window_days'), 30)) order_days = max(fine_days, complaint_days, _int(cfg.get('order_window_days'), 30)) now = timezone.now() since_order = now - timedelta(days=order_days) since_fine = now - timedelta(days=fine_days) since_complaint = now - timedelta(days=complaint_days) completed = 0 fine_count = 0 complaint_count = 0 valid_invites = 0 try: from orders.models import Order, Penalty if role == 'dashou': completed = Order.query.filter( ClubID=club_id, PlayerID=user_id, Status=3, CreateTime__gte=since_order, ).count() fine_count = Penalty.query.filter( PenalizedUserID=user_id, Identity=1, Status__in=[1, 2, 3, 5], CreateTime__gte=since_fine, ).count() # 投诉:窗口内该打手完成单关联的工单数 try: from gongdan.models import Gongdan order_ids = list( Order.query.filter( ClubID=club_id, PlayerID=user_id, Status=3, CreateTime__gte=since_complaint, ).values_list('OrderID', flat=True)[:800] ) if order_ids: complaint_count = Gongdan.query.filter( order_id__in=order_ids, ).count() except Exception as e: logger.warning('complaint metrics skip: %s', e) valid_invites = 0 elif role == 'guanshi': from users.models import User, UserGuanshi, UserDashou u = User.query.filter(UserUID=user_id).first() gs = UserGuanshi.query.filter(user=u).first() if u else None if gs is not None: valid_invites = int(getattr(gs, 'yaogingshuliang', 0) or 0) # 管事「完成单」近似:窗口内其邀请打手的成交单量 dashou_uids = list( UserDashou.query.filter(yaoqingren=user_id).values_list( 'user__UserUID', flat=True )[:500] ) if dashou_uids: completed = Order.query.filter( ClubID=club_id, PlayerID__in=dashou_uids, Status=3, CreateTime__gte=since_order, ).count() fine_count = Penalty.query.filter( PenalizedUserID=user_id, Identity=2, Status__in=[1, 2, 3, 5], CreateTime__gte=since_fine, ).count() try: from gongdan.models import Gongdan complaint_count = Gongdan.query.filter( guanshi_id=user_id, CreateTime__gte=since_complaint, ).count() except Exception: pass elif role == 'zuzhang': from users.models import User, UserGuanshi, UserZuzhang u = User.query.filter(UserUID=user_id).first() zz = UserZuzhang.query.filter(user=u).first() if u else None if zz is not None: valid_invites = UserGuanshi.query.filter(yaoqingren=user_id).count() fine_count = Penalty.query.filter( PenalizedUserID=user_id, Identity=4, Status__in=[1, 2, 3, 5], CreateTime__gte=since_fine, ).count() completed = valid_invites # 组长无直接结单,用邀请管事数作「完成」近似时可被 min_orders 覆盖 else: # shenheguan fine_count = Penalty.query.filter( PenalizedUserID=user_id, Status__in=[1, 2, 3, 5], CreateTime__gte=since_fine, ).count() except Exception as e: logger.exception('compute_risk_metrics failed user=%s role=%s: %s', user_id, role, e) denom = max(completed, 1) fine_rate = _dec(fine_count) / _dec(denom) complaint_rate = _dec(complaint_count) / _dec(denom) return { 'completed_orders': completed, 'fine_count': fine_count, 'complaint_count': complaint_count, 'fine_rate': str(fine_rate), 'complaint_rate': str(complaint_rate), 'valid_invites': valid_invites, 'order_window_days': order_days, 'fine_rate_window_days': fine_days, 'complaint_rate_window_days': complaint_days, } def write_risk_snapshot(profile, metrics: dict) -> None: if profile is None: return try: profile.__class__.query.filter(pk=profile.pk).update( risk_fine_rate=_dec(metrics.get('fine_rate')), risk_complaint_rate=_dec(metrics.get('complaint_rate')), risk_completed_orders=_int(metrics.get('completed_orders')), risk_valid_invites=_int(metrics.get('valid_invites')), risk_stat_at=timezone.now(), ) except Exception as e: logger.warning('write_risk_snapshot failed: %s', e) def evaluate_unfreeze_conditions( club_id: str, user_id: str, role: str, condition_json: Optional[dict], profile=None, ) -> Tuple[bool, Dict[str, Any]]: """ 返回 (是否达标, 指标快照)。 未勾选的条件不参与;全部未勾选视为「无条件限制」→ 达标。 """ cfg = condition_json or {} metrics = compute_risk_metrics(club_id, user_id, role, cfg) write_risk_snapshot(profile, metrics) checks = [] reasons = [] # 最低完成单量 min_orders = _int(cfg.get('min_completed_orders'), 0) if _flag(cfg, 'enable_min_completed_orders', min_orders > 0): ok = metrics['completed_orders'] >= min_orders checks.append(ok) if not ok: reasons.append( f"完成单量 {metrics['completed_orders']}/{min_orders}" ) # 最高罚款率 max_fine = _dec(cfg.get('max_fine_rate'), '1') if _flag(cfg, 'enable_max_fine_rate', 0 <= max_fine < 1): ok = _dec(metrics['fine_rate']) <= max_fine checks.append(ok) if not ok: reasons.append( f"罚款率 {metrics['fine_rate']} > {max_fine}" ) # 最高投诉率 max_comp = _dec(cfg.get('max_complaint_rate'), '1') if _flag(cfg, 'enable_max_complaint_rate', 0 <= max_comp < 1): ok = _dec(metrics['complaint_rate']) <= max_comp checks.append(ok) if not ok: reasons.append( f"投诉率 {metrics['complaint_rate']} > {max_comp}" ) # 最低有效邀请 min_inv = _int(cfg.get('min_valid_invites'), 0) if _flag(cfg, 'enable_min_valid_invites', min_inv > 0): ok = metrics['valid_invites'] >= min_inv checks.append(ok) if not ok: reasons.append( f"有效邀请 {metrics['valid_invites']}/{min_inv}" ) if not checks: passed = True else: passed = all(checks) snap = { **metrics, 'passed': passed, 'fail_reasons': reasons, } return passed, snap