后台条件真正按罚款率/单量等评估;选项式配置所需字段规范化;Celery 改为每5分钟;小程序说明文案接口补齐。 Co-authored-by: Cursor <cursoragent@cursor.com>
251 lines
8.5 KiB
Python
251 lines
8.5 KiB
Python
"""资金冻结:解冻条件评估(实时算窗口指标,并回写角色 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
|