177 lines
7.0 KiB
Python
177 lines
7.0 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""徽章体系核心服务:规则评估 → 授予/回收。
|
|||
|
|
|
|||
|
|
徽章授予规则(grant_rule_json):
|
|||
|
|
- {"type":"credit_level","op":">=","value":3} 信用等级达到 Ln
|
|||
|
|
- {"type":"cert_count","op":">=","value":1,"cert_category":"skill"}
|
|||
|
|
- {"type":"task_completed_count","op":">=","value":10}
|
|||
|
|
- {"type":"five_star_count","op":">=","value":5}
|
|||
|
|
- {"type":"on_time_streak","op":">=","value":10}
|
|||
|
|
- {"type":"training_completed_count","op":">=","value":3}
|
|||
|
|
- {"type":"training_hours","op":">=","value":50}
|
|||
|
|
- {"type":"cert_expert_count","op":">=","value":1}
|
|||
|
|
- {"type":"fast_response","op":">=","value":1}
|
|||
|
|
- {"all":[...]} 多条件同时满足
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import logging
|
|||
|
|
|
|||
|
|
from ..infrastructure.repositories import Database
|
|||
|
|
|
|||
|
|
log = logging.getLogger("credit.badge")
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def evaluate_badges(db: Database, user_id: str) -> list[dict]:
|
|||
|
|
"""评估该用户全部激活徽章的授予条件;新满足的授予并返回列表。"""
|
|||
|
|
badges = await db.badges.list_active()
|
|||
|
|
granted: list[dict] = []
|
|||
|
|
for badge in badges:
|
|||
|
|
rule = badge.get("grant_rule") or {}
|
|||
|
|
if not rule:
|
|||
|
|
continue
|
|||
|
|
try:
|
|||
|
|
if await _match_rule(db, user_id, rule):
|
|||
|
|
has = await db.user_badges.get_active(user_id, badge["code"])
|
|||
|
|
if has is None:
|
|||
|
|
entry = await db.user_badges.grant(
|
|||
|
|
user_id=user_id, badge_code=badge["code"], badge_id=badge["id"],
|
|||
|
|
source="auto",
|
|||
|
|
)
|
|||
|
|
granted.append({**badge, "user_badge": entry})
|
|||
|
|
# 徽章加分:直写流水(badge.granted,delta=credit_bonus),再重算
|
|||
|
|
bonus = int(badge.get("credit_bonus", 0) or 0)
|
|||
|
|
if bonus:
|
|||
|
|
await db.credit_ledger.add(
|
|||
|
|
user_id=user_id, event_code="badge.granted", delta=bonus,
|
|||
|
|
dimension="system", reason=f"获得徽章「{badge['name']}」",
|
|||
|
|
ref_type="badge", ref_id=badge["id"], source="badge",
|
|||
|
|
)
|
|||
|
|
# 重算总分/等级(徽章可能触发等级徽章链,交给外层循环下一轮)
|
|||
|
|
from .credit_engine import recalc_user # 避免循环 import
|
|||
|
|
await recalc_user(db, user_id)
|
|||
|
|
except Exception as exc: # noqa: BLE001
|
|||
|
|
log.warning("badge eval failed %s/%s: %s", user_id, badge.get("code"), exc)
|
|||
|
|
return granted
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _match_rule(db: Database, user_id: str, rule: dict) -> bool:
|
|||
|
|
if "all" in rule and isinstance(rule["all"], list):
|
|||
|
|
for sub in rule["all"]:
|
|||
|
|
if not await _match_rule(db, user_id, sub):
|
|||
|
|
return False
|
|||
|
|
return True
|
|||
|
|
if "any" in rule and isinstance(rule["any"], list):
|
|||
|
|
for sub in rule["any"]:
|
|||
|
|
if await _match_rule(db, user_id, sub):
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
rtype = rule.get("type")
|
|||
|
|
op = rule.get("op", ">=")
|
|||
|
|
value = rule.get("value")
|
|||
|
|
actual = await _get_metric(db, user_id, rtype, rule)
|
|||
|
|
return _compare(actual, op, value)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _get_metric(db: Database, user_id: str, rtype: str, rule: dict):
|
|||
|
|
"""按指标类型取用户实际值。"""
|
|||
|
|
if rtype == "credit_level":
|
|||
|
|
score = await db.credit_scores.get(user_id)
|
|||
|
|
lv = (score or {}).get("level", "L1")
|
|||
|
|
order = {"L1": 1, "L2": 2, "L3": 3, "L4": 4, "L5": 5, "L6": 6, "L7": 7, "L8": 8}
|
|||
|
|
return order.get(lv, 1)
|
|||
|
|
|
|||
|
|
if rtype == "cert_count":
|
|||
|
|
cat = rule.get("cert_category", "")
|
|||
|
|
try:
|
|||
|
|
certs = await db.certifications.list(user_id=user_id, status="active")
|
|||
|
|
if not cat:
|
|||
|
|
return len(certs)
|
|||
|
|
return sum(1 for c in certs if (c.get("cert_category") or c.get("cert_type", "")) == cat)
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
if rtype == "cert_expert_count":
|
|||
|
|
try:
|
|||
|
|
certs = await db.certifications.list(user_id=user_id, status="active")
|
|||
|
|
return sum(1 for c in certs if str(c.get("level", "")).lower() in ("expert", "advanced"))
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
if rtype == "task_completed_count":
|
|||
|
|
try:
|
|||
|
|
rows = await db.credit_ledger.list_all(user_id=user_id, limit=1000)
|
|||
|
|
return sum(1 for r in rows if r["event_code"] in ("task.complete.on_time", "task.complete.overdue"))
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
if rtype == "five_star_count":
|
|||
|
|
try:
|
|||
|
|
ratings = await db.ratings.list_for(user_id)
|
|||
|
|
return sum(1 for r in ratings if float(r.get("score", 0) or 0) >= 5)
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
if rtype == "on_time_streak":
|
|||
|
|
# 简化:以按时交付事件数作为连续次数近似(后续可按任务时间序精确计算)
|
|||
|
|
try:
|
|||
|
|
rows = await db.credit_ledger.list_all(user_id=user_id, limit=1000)
|
|||
|
|
return sum(1 for r in rows if r["event_code"] == "task.complete.on_time")
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
if rtype == "training_completed_count":
|
|||
|
|
try:
|
|||
|
|
return len(await db.course_certificates.list_by_user(user_id))
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
if rtype == "training_hours":
|
|||
|
|
try:
|
|||
|
|
rows = await db.credit_ledger.list_all(user_id=user_id, limit=1000)
|
|||
|
|
total = 0
|
|||
|
|
for r in rows:
|
|||
|
|
if r["event_code"] == "training.hours":
|
|||
|
|
total += abs(r["delta"]) * 2 # 每 +5 = 10 小时
|
|||
|
|
return total
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
if rtype == "fast_response":
|
|||
|
|
# 简化:以接单/响应活跃事件估算(后续按真实响应时长)
|
|||
|
|
try:
|
|||
|
|
rows = await db.credit_ledger.list_all(user_id=user_id, limit=1000)
|
|||
|
|
return 1 if any(r["event_code"] in ("task.accept", "service.publish") for r in rows) else 0
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
if rtype == "acceptance_rate":
|
|||
|
|
# 以按时交付占比近似(credit_ledger 统计)
|
|||
|
|
try:
|
|||
|
|
rows = await db.credit_ledger.list_all(user_id=user_id, limit=1000)
|
|||
|
|
on_time = sum(1 for r in rows if r["event_code"] == "task.complete.on_time")
|
|||
|
|
done = on_time + sum(1 for r in rows if r["event_code"] == "task.complete.overdue")
|
|||
|
|
return round(on_time / done, 3) if done else 0.0
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _compare(actual, op, value) -> bool:
|
|||
|
|
try:
|
|||
|
|
if op == ">=":
|
|||
|
|
return actual >= value
|
|||
|
|
if op == ">":
|
|||
|
|
return actual > value
|
|||
|
|
if op == "<=":
|
|||
|
|
return actual <= value
|
|||
|
|
if op == "<":
|
|||
|
|
return actual < value
|
|||
|
|
if op == "==":
|
|||
|
|
return actual == value
|
|||
|
|
except TypeError:
|
|||
|
|
return False
|
|||
|
|
return False
|