Files
Django/jituan/services/fund_freeze_conditions.py
XingQue 6c96eb3bcb fix: 冻结条件可评估+分期规范化,定时扫描更及时
后台条件真正按罚款率/单量等评估;选项式配置所需字段规范化;Celery 改为每5分钟;小程序说明文案接口补齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 19:36:10 +08:00

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