tree_state.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. """全局树热度状态的加载与格式化。"""
  2. from __future__ import annotations
  3. from collections import defaultdict
  4. from types import SimpleNamespace
  5. from typing import Any
  6. from supply_agent.ranking import rank_with_scores
  7. from supply_infra.db.repositories.category_tree_weight_repo import CategoryTreeWeightRepository
  8. from supply_infra.db.repositories.global_tree_category_repo import GlobalTreeCategoryRepository
  9. from supply_infra.db.session import get_session
  10. def load_tree_state(biz_dt: str) -> tuple[dict[int, Any], dict[int | None, list[int]], dict[int, Any]]:
  11. with get_session() as session:
  12. categories = [
  13. SimpleNamespace(id=int(row.id), name=row.name, parent_id=row.parent_id, level=row.level)
  14. for row in GlobalTreeCategoryRepository(session).list_active_categories()
  15. ]
  16. weights = [
  17. SimpleNamespace(
  18. category_id=int(row.category_id),
  19. total_score=row.total_score,
  20. hung_word_count=row.hung_word_count,
  21. real_rov_7d_avg=row.real_rov_7d_avg,
  22. real_rov_7d_count=row.real_rov_7d_count,
  23. real_vov_7d_avg=row.real_vov_7d_avg,
  24. real_vov_7d_count=row.real_vov_7d_count,
  25. )
  26. for row in CategoryTreeWeightRepository(session).list_by_biz_dt(biz_dt)
  27. ]
  28. by_id = {row.id: row for row in categories}
  29. children: dict[int | None, list[int]] = defaultdict(list)
  30. for row in categories:
  31. parent_id = int(row.parent_id) if row.parent_id not in (None, 0) else None
  32. children[parent_id].append(row.id)
  33. for ids in children.values():
  34. ids.sort()
  35. return by_id, children, {row.category_id: row for row in weights}
  36. def format_heat_score(weight: Any | None) -> str:
  37. """格式化原始 total_score:只保留两位小数,无数据时明确为 null。"""
  38. if weight is None or weight.total_score is None:
  39. return "null"
  40. return f"{float(weight.total_score):.2f}"
  41. def has_hung_demand(weight: Any | None) -> bool:
  42. """判断分类自身是否挂有需求。"""
  43. return weight is not None and int(weight.hung_word_count or 0) > 0
  44. def format_demand_count(weight: Any | None) -> str:
  45. """格式化节点挂载需求数量;无需求时返回空字符串。"""
  46. if not has_hung_demand(weight):
  47. return ""
  48. return str(int(weight.hung_word_count or 0))
  49. HEAT_LEVEL_DEFINITION: dict[str, dict[str, str | float | None]] = {
  50. "S": {"label": "高热", "min_global_rank_score": 0.90},
  51. "A": {"label": "较高热", "min_global_rank_score": 0.75},
  52. "B": {"label": "中热", "min_global_rank_score": 0.50},
  53. "C": {"label": "较低热", "min_global_rank_score": 0.25},
  54. "D": {"label": "低热", "min_global_rank_score": 0.00},
  55. "U": {"label": "数据不足", "min_global_rank_score": None},
  56. }
  57. def global_heat_positions(weights: dict[int, Any]) -> dict[int, dict[str, float | int]]:
  58. """返回节点 ``total_score`` 在整棵有分节点中的排名位置。"""
  59. return rank_with_scores([
  60. (category_id, float(weight.total_score))
  61. for category_id, weight in weights.items()
  62. if weight is not None and weight.total_score is not None
  63. ])
  64. def heat_level(position: dict[str, float | int] | None) -> str:
  65. """按整树排名分映射批次热度等级;无分节点单列 U。"""
  66. if position is None:
  67. return "U"
  68. score = float(position["normalized_score"])
  69. for level in ("S", "A", "B", "C"):
  70. threshold = HEAT_LEVEL_DEFINITION[level]["min_global_rank_score"]
  71. if threshold is not None and score >= float(threshold):
  72. return level
  73. return "D"
  74. def format_rank(position: dict[str, float | int] | None) -> str:
  75. if position is None:
  76. return "无排名"
  77. rank = float(position["rank"])
  78. rank_text = str(int(rank)) if rank.is_integer() else f"{rank:.1f}"
  79. return f"{rank_text}/{int(position['total'])}"
  80. def path(category_id: int | None, by_id: dict[int, Any]) -> str:
  81. names: list[str] = []
  82. current = by_id.get(category_id) if category_id is not None else None
  83. while current is not None:
  84. names.append(current.name or str(current.id))
  85. parent_id = int(current.parent_id) if current.parent_id not in (None, 0) else None
  86. current = by_id.get(parent_id) if parent_id is not None else None
  87. return " > ".join(reversed(names))