# -*- coding: utf-8 -*- """ 行业划分接口 (设计 §5 的可插拔适配器) ====================================== IndustryClassifier.get(ts_code) -> 行业名 | None 数据源经参数 PMS_SECTOR_SOURCE 切换: custom_table 自定义映射表 pms_industry_map (默认建议; 用户后续灌何种划分都只是灌数) gp_stock_category 决策系统生态已有的行业表 (映射表就绪前的临时数据源) ""(空) 未配置 → 行业约束自动停用, 页面显著提示 (不静默放行也不误拦) """ from __future__ import annotations import logging import threading import time from app.repo import downstream_repo, pms_repo from app.services import param_store logger = logging.getLogger("pms.industry") _cache = {"at": 0.0, "map": {}, "source": None} _lock = threading.Lock() CACHE_TTL = 300.0 def source() -> str: return (param_store.get("PMS_SECTOR_SOURCE", "") or "").strip() def ready() -> bool: """行业约束是否生效 (数据源已配置)。""" return source() in ("custom_table", "gp_stock_category") def status() -> dict: src = source() st = {"source": src, "ready": ready(), "count": 0, "hint": "行业划分数据源未配置 —— 行业集中度硬拦截已自动停用, " "行业类命令置灰 (设计 §5/§13)"} if src == "custom_table": try: st["count"] = len(_load_custom()) st["hint"] = f"自定义映射表 pms_industry_map 已加载 {st['count']} 条" except Exception as e: st["ready"] = False st["hint"] = f"映射表读取失败: {type(e).__name__}: {e}" elif src == "gp_stock_category": st["hint"] = "临时数据源 gp_stock_category (逐票查询, 建议尽快切 custom_table)" return st def _load_custom() -> dict: now = time.time() if _cache["source"] == "custom_table" and now - _cache["at"] < CACHE_TTL: return _cache["map"] with _lock: rows = pms_repo.list_industry() m = {r["ts_code"]: r["industry"] for r in rows} _cache.update({"at": now, "map": m, "source": "custom_table"}) return m def get(ts_code: str): """返回行业名; 未配置数据源或查不到 → None (调用方据此跳过行业约束)。""" src = source() if not ts_code or src not in ("custom_table", "gp_stock_category"): return None try: if src == "custom_table": return _load_custom().get(ts_code) return downstream_repo.fetch_sector_from_category(ts_code) except Exception as e: logger.warning("行业查询失败 [%s]: %s", ts_code, e) return None def get_many(codes) -> dict: src = source() if src != "custom_table": return {c: get(c) for c in (codes or [])} m = _load_custom() return {c: m.get(c) for c in (codes or [])} def invalidate(): _cache["at"] = 0.0