config_store.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import json
  2. import re
  3. from dataclasses import dataclass
  4. from typing import Any
  5. from sqlalchemy import text
  6. from app.core.config import settings
  7. from app.db.mysql import SessionLocal
  8. IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
  9. def _safe_identifier(name: str) -> str:
  10. if not IDENTIFIER_RE.match(name):
  11. raise ValueError(f"invalid sql identifier: {name}")
  12. return name
  13. @dataclass(frozen=True)
  14. class StrategyConfigRecord:
  15. strategy_id: str
  16. name: str
  17. version: str
  18. params: dict[str, Any]
  19. active: bool
  20. def _parse_params(raw: object) -> dict[str, Any]:
  21. if raw is None:
  22. return {}
  23. if isinstance(raw, dict):
  24. return raw
  25. if isinstance(raw, (bytes, bytearray)):
  26. raw = raw.decode("utf-8")
  27. if isinstance(raw, str):
  28. text_value = raw.strip()
  29. if not text_value:
  30. return {}
  31. return json.loads(text_value)
  32. raise TypeError(f"unsupported params type: {type(raw)!r}")
  33. def ensure_strategy_tables() -> None:
  34. config_table = _safe_identifier(settings.strategy_config_table)
  35. staging_table = _safe_identifier(settings.strategy_staging_table)
  36. config_sql = f"""
  37. CREATE TABLE IF NOT EXISTS {config_table}
  38. (
  39. id BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增id',
  40. strategy_id VARCHAR(64) NOT NULL COMMENT '全局唯一id,英文名称',
  41. name VARCHAR(128) NOT NULL COMMENT '策略名称',
  42. version VARCHAR(32) NOT NULL COMMENT '策略版本,用于追踪代码变更',
  43. params JSON NULL COMMENT '策略运行参数,如阈值、窗口期等',
  44. active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用,0=不参与产出和合并',
  45. create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  46. updated_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  47. PRIMARY KEY (id),
  48. UNIQUE KEY uk_strategy_id (strategy_id)
  49. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='策略配置表'
  50. """
  51. staging_sql = f"""
  52. CREATE TABLE IF NOT EXISTS {staging_table}
  53. (
  54. id BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增id',
  55. strategy_config_id VARCHAR(64) NOT NULL COMMENT '来源策略ID,关联 strategy_config.strategy_id',
  56. strategy VARCHAR(32) NULL COMMENT '策略名称',
  57. demand_id VARCHAR(64) NULL COMMENT '需求id',
  58. demand_name VARCHAR(512) NULL COMMENT '需求',
  59. weight DOUBLE NULL COMMENT '需求权重',
  60. `type` VARCHAR(64) NULL COMMENT '需求类型',
  61. video_count INT NULL COMMENT '视频数量',
  62. video_list JSON NULL COMMENT '视频列表',
  63. extend JSON NULL COMMENT '拓展字段',
  64. batch_date VARCHAR(32) NULL COMMENT '归属批次日期,e.g. 20250526',
  65. create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  66. PRIMARY KEY (id),
  67. KEY idx_strategy_config_id (strategy_config_id),
  68. KEY idx_batch_date (batch_date),
  69. KEY idx_demand_id (demand_id)
  70. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='策略暂存表'
  71. """
  72. with SessionLocal() as session:
  73. session.execute(text(config_sql))
  74. session.execute(text(staging_sql))
  75. session.commit()
  76. def fetch_all_configs() -> list[StrategyConfigRecord]:
  77. config_table = _safe_identifier(settings.strategy_config_table)
  78. query = text(
  79. f"""
  80. SELECT strategy_id, name, version, params, active
  81. FROM {config_table}
  82. ORDER BY strategy_id ASC
  83. """
  84. )
  85. with SessionLocal() as session:
  86. rows = session.execute(query).mappings().all()
  87. return [
  88. StrategyConfigRecord(
  89. strategy_id=str(row["strategy_id"]),
  90. name=str(row["name"]),
  91. version=str(row["version"]),
  92. params=_parse_params(row["params"]),
  93. active=bool(row["active"]),
  94. )
  95. for row in rows
  96. ]
  97. def fetch_config_by_id(strategy_id: str) -> StrategyConfigRecord | None:
  98. config_table = _safe_identifier(settings.strategy_config_table)
  99. query = text(
  100. f"""
  101. SELECT strategy_id, name, version, params, active
  102. FROM {config_table}
  103. WHERE strategy_id = :strategy_id
  104. LIMIT 1
  105. """
  106. )
  107. with SessionLocal() as session:
  108. row = session.execute(query, {"strategy_id": strategy_id}).mappings().first()
  109. if row is None:
  110. return None
  111. return StrategyConfigRecord(
  112. strategy_id=str(row["strategy_id"]),
  113. name=str(row["name"]),
  114. version=str(row["version"]),
  115. params=_parse_params(row["params"]),
  116. active=bool(row["active"]),
  117. )