# -*- 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