category_tree.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. """Build nested category tree from global_tree_category (+ optional weights)."""
  2. from __future__ import annotations
  3. from decimal import Decimal
  4. from typing import Any
  5. from supply_infra.db.models.category_tree_weight import CategoryTreeWeight
  6. from supply_infra.db.models.global_tree_category import GlobalTreeCategory
  7. from supply_infra.db.repositories.category_tree_weight_repo import (
  8. CategoryTreeWeightRepository,
  9. )
  10. from supply_infra.db.repositories.global_tree_category_repo import (
  11. GlobalTreeCategoryRepository,
  12. )
  13. from supply_infra.db.session import get_session
  14. DIM_KEYS: tuple[str, ...] = (
  15. "ext_pop",
  16. "plat_sust_pop",
  17. "plat_ly_pop",
  18. "recent_pop",
  19. "real_rov_7d",
  20. "real_vov_7d",
  21. )
  22. DIM_META: list[dict[str, str]] = [
  23. {"key": "ext_pop", "label": "外部热度"},
  24. {"key": "plat_sust_pop", "label": "平台持续热度"},
  25. {"key": "plat_ly_pop", "label": "去年同期热度"},
  26. {"key": "recent_pop", "label": "近期热度"},
  27. {"key": "real_rov_7d", "label": "真实ROV(7日)"},
  28. {"key": "real_vov_7d", "label": "真实VOV(7日)"},
  29. ]
  30. def _normalize_parent_id(parent_id: int | None) -> int | None:
  31. if parent_id is None or parent_id == 0:
  32. return None
  33. return parent_id
  34. def _to_float(value: Decimal | float | int | None) -> float:
  35. if value is None:
  36. return 0.0
  37. return float(value)
  38. def _weights_payload(row: CategoryTreeWeight | None) -> dict[str, float]:
  39. if row is None:
  40. return {dim: 0.0 for dim in DIM_KEYS}
  41. return {dim: _to_float(getattr(row, f"{dim}_avg", None)) for dim in DIM_KEYS}
  42. def _counts_payload(row: CategoryTreeWeight | None) -> dict[str, int]:
  43. if row is None:
  44. return {dim: 0 for dim in DIM_KEYS}
  45. return {dim: int(getattr(row, f"{dim}_count", 0) or 0) for dim in DIM_KEYS}
  46. def _build_children_map(
  47. categories: list[GlobalTreeCategory],
  48. ) -> dict[int | None, list[GlobalTreeCategory]]:
  49. children_map: dict[int | None, list[GlobalTreeCategory]] = {}
  50. for category in categories:
  51. parent_key = _normalize_parent_id(category.parent_id)
  52. children_map.setdefault(parent_key, []).append(category)
  53. for children in children_map.values():
  54. children.sort(key=lambda c: (c.level or 0, c.id))
  55. return children_map
  56. def _to_node(
  57. category: GlobalTreeCategory,
  58. children_map: dict[int | None, list[GlobalTreeCategory]],
  59. weight_by_id: dict[int, CategoryTreeWeight],
  60. ) -> dict[str, Any]:
  61. children = [
  62. _to_node(child, children_map, weight_by_id)
  63. for child in children_map.get(category.id, [])
  64. ]
  65. weight_row = weight_by_id.get(int(category.id))
  66. return {
  67. "id": category.id,
  68. "name": category.name,
  69. "level": category.level,
  70. "description": category.description,
  71. "weights": _weights_payload(weight_row),
  72. "counts": _counts_payload(weight_row),
  73. "hung_word_count": int(weight_row.hung_word_count) if weight_row else 0,
  74. "children": children,
  75. }
  76. def build_category_tree(biz_dt: str | None = None) -> dict[str, Any]:
  77. """Load active categories with per-dim avg; nest as roots.
  78. Returns ``{biz_dt, dims, nodes}``. When no weight rows exist, ``biz_dt`` is
  79. ``None`` and each node still carries zeroed ``weights`` / ``counts``.
  80. """
  81. with get_session() as session:
  82. categories = GlobalTreeCategoryRepository(session).list_active_categories()
  83. weight_repo = CategoryTreeWeightRepository(session)
  84. resolved_dt = biz_dt or weight_repo.get_latest_biz_dt()
  85. weight_rows: list[CategoryTreeWeight] = []
  86. if resolved_dt:
  87. weight_rows = weight_repo.list_by_biz_dt(resolved_dt)
  88. weight_by_id = {int(r.category_id): r for r in weight_rows}
  89. children_map = _build_children_map(categories)
  90. nodes = [
  91. _to_node(root, children_map, weight_by_id)
  92. for root in children_map.get(None, [])
  93. ]
  94. return {
  95. "biz_dt": resolved_dt,
  96. "dims": DIM_META,
  97. "nodes": nodes,
  98. }