|
@@ -814,8 +814,79 @@ if STATIC_DIR.exists():
|
|
|
app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="assets")
|
|
app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="assets")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# --- 缓存自动失效中间件 ---
|
|
|
|
|
+# 任何对核心实体的写操作(POST/PATCH/DELETE)自动清除对应缓存
|
|
|
|
|
+_DASHBOARD_INVALIDATE_PREFIXES = ("/api/requirement", "/api/capability", "/api/tool", "/api/strategy", "/api/knowledge")
|
|
|
|
|
+
|
|
|
|
|
+@app.middleware("http")
|
|
|
|
|
+async def auto_invalidate_caches(request: Request, call_next):
|
|
|
|
|
+ response = await call_next(request)
|
|
|
|
|
+ if request.method in ("POST", "PATCH", "PUT", "DELETE") and response.status_code < 400:
|
|
|
|
|
+ path = request.url.path
|
|
|
|
|
+ if any(path.startswith(p) for p in _DASHBOARD_INVALIDATE_PREFIXES):
|
|
|
|
|
+ _invalidate_dashboard_cache()
|
|
|
|
|
+ if path.startswith("/api/resource") and not path.endswith("/batch"):
|
|
|
|
|
+ _invalidate_resource_cache()
|
|
|
|
|
+ return response
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
# --- Knowledge API ---
|
|
# --- Knowledge API ---
|
|
|
|
|
|
|
|
|
|
+# --- Resource Batch API ---
|
|
|
|
|
+
|
|
|
|
|
+# --- Resource 缓存(与 Dashboard 同 TTL,写入时失效) ---
|
|
|
|
|
+_resource_cache: Dict[str, dict] = {}
|
|
|
|
|
+_resource_cache_ts: float = 0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _invalidate_resource_cache():
|
|
|
|
|
+ global _resource_cache, _resource_cache_ts
|
|
|
|
|
+ _resource_cache.clear()
|
|
|
|
|
+ _resource_cache_ts = 0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _get_cached_resource(rid: str) -> Optional[dict]:
|
|
|
|
|
+ """从缓存取 resource,miss 时查 DB 并写入缓存"""
|
|
|
|
|
+ global _resource_cache_ts
|
|
|
|
|
+ now = time.time()
|
|
|
|
|
+ if now - _resource_cache_ts > _DASHBOARD_CACHE_TTL:
|
|
|
|
|
+ _resource_cache.clear()
|
|
|
|
|
+ _resource_cache_ts = now
|
|
|
|
|
+ if rid in _resource_cache:
|
|
|
|
|
+ return _resource_cache[rid]
|
|
|
|
|
+ row = pg_resource_store.get_by_id(rid)
|
|
|
|
|
+ if not row:
|
|
|
|
|
+ return None
|
|
|
|
|
+ entry = {
|
|
|
|
|
+ "id": row["id"],
|
|
|
|
|
+ "title": row["title"],
|
|
|
|
|
+ "body": row["body"],
|
|
|
|
|
+ "content_type": row.get("content_type", "text"),
|
|
|
|
|
+ "metadata": row.get("metadata", {}),
|
|
|
|
|
+ "images": row.get("images", []),
|
|
|
|
|
+ }
|
|
|
|
|
+ _resource_cache[rid] = entry
|
|
|
|
|
+ return entry
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.post("/api/resource/batch")
|
|
|
|
|
+def batch_get_resources(body: dict = Body(...)):
|
|
|
|
|
+ """批量获取 resource 基本信息(不含 toc/children/siblings 导航),用于 Dashboard 等场景。
|
|
|
|
|
+ 带后端内存缓存(24h TTL,resource 写入时失效)。"""
|
|
|
|
|
+ ids = body.get("ids", [])
|
|
|
|
|
+ if not ids:
|
|
|
|
|
+ return {"resources": {}}
|
|
|
|
|
+ resources: Dict[str, dict] = {}
|
|
|
|
|
+ for rid in ids:
|
|
|
|
|
+ try:
|
|
|
|
|
+ entry = _get_cached_resource(rid)
|
|
|
|
|
+ if entry:
|
|
|
|
|
+ resources[rid] = entry
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[batch_get_resources] Failed to fetch {rid}: {e}")
|
|
|
|
|
+ return {"resources": resources}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@app.post("/api/resource", status_code=201)
|
|
@app.post("/api/resource", status_code=201)
|
|
|
def submit_resource(resource: ResourceIn):
|
|
def submit_resource(resource: ResourceIn):
|
|
|
"""提交资源(存入 PostgreSQL resources 表)"""
|
|
"""提交资源(存入 PostgreSQL resources 表)"""
|
|
@@ -2595,6 +2666,67 @@ async def get_relations(table_name: str, request: Request):
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
+# --- Dashboard Snapshot (缓存聚合接口) ---
|
|
|
|
|
+
|
|
|
|
|
+_dashboard_snapshot_cache: Optional[dict] = None
|
|
|
|
|
+_dashboard_snapshot_ts: float = 0
|
|
|
|
|
+_DASHBOARD_CACHE_TTL = 24 * 3600 # 24 小时
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _invalidate_dashboard_cache():
|
|
|
|
|
+ """数据写入后调用,清除 dashboard 快照缓存"""
|
|
|
|
|
+ global _dashboard_snapshot_cache, _dashboard_snapshot_ts
|
|
|
|
|
+ _dashboard_snapshot_cache = None
|
|
|
|
|
+ _dashboard_snapshot_ts = 0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _build_dashboard_snapshot() -> dict:
|
|
|
|
|
+ """在后端一次性构建 Dashboard 所需的全部数据"""
|
|
|
|
|
+ tree_file = STATIC_DIR / "category_tree.json"
|
|
|
|
|
+ tree_data = None
|
|
|
|
|
+ if tree_file.exists():
|
|
|
|
|
+ tree_data = json.loads(tree_file.read_text(encoding="utf-8"))
|
|
|
|
|
+
|
|
|
|
|
+ reqs = pg_requirement_store.list_all(limit=1000, offset=0)
|
|
|
|
|
+ caps = pg_capability_store.list_all(limit=1000, offset=0)
|
|
|
|
|
+ tools = pg_tool_store.list_all(limit=1000, offset=0)
|
|
|
|
|
+ procs = pg_strategy_store.list_all(limit=1000, offset=0)
|
|
|
|
|
+ know_raw = pg_store.query('(status == "approved" or status == "checked")', limit=1000)
|
|
|
|
|
+ know = [to_serializable(r) for r in know_raw]
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "tree": tree_data,
|
|
|
|
|
+ "reqs": reqs,
|
|
|
|
|
+ "caps": caps,
|
|
|
|
|
+ "tools": tools,
|
|
|
|
|
+ "procs": procs,
|
|
|
|
|
+ "know": know,
|
|
|
|
|
+ "built_at": time.time(),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.get("/api/dashboard/snapshot")
|
|
|
|
|
+def get_dashboard_snapshot():
|
|
|
|
|
+ """返回 Dashboard 所需的全部数据快照,带服务端内存缓存(24h TTL,写入时失效)"""
|
|
|
|
|
+ global _dashboard_snapshot_cache, _dashboard_snapshot_ts
|
|
|
|
|
+ now = time.time()
|
|
|
|
|
+ if _dashboard_snapshot_cache and (now - _dashboard_snapshot_ts < _DASHBOARD_CACHE_TTL):
|
|
|
|
|
+ return _dashboard_snapshot_cache
|
|
|
|
|
+ try:
|
|
|
|
|
+ _dashboard_snapshot_cache = _build_dashboard_snapshot()
|
|
|
|
|
+ _dashboard_snapshot_ts = now
|
|
|
|
|
+ return _dashboard_snapshot_cache
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.post("/api/dashboard/invalidate")
|
|
|
|
|
+def invalidate_dashboard_cache():
|
|
|
|
|
+ """手动清除 dashboard 缓存"""
|
|
|
|
|
+ _invalidate_dashboard_cache()
|
|
|
|
|
+ return {"status": "ok"}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@app.get("/category_tree.json")
|
|
@app.get("/category_tree.json")
|
|
|
def serve_category_tree():
|
|
def serve_category_tree():
|
|
|
"""类别树JSON数据"""
|
|
"""类别树JSON数据"""
|