"""City areas & responsibility (P3) — list, tree, sync-from-graph, assign.""" from fastapi import APIRouter, HTTPException from app.auth import CurrentUser from app.contracts import AreaUpdate from app.db import list_areas, update_area, upsert_area from app.api.graph import _get_graph router = APIRouter() def _area_level(area_id: str) -> str: """Derive admin level from the GB/T 2260 6-digit code.""" if area_id.isdigit() and len(area_id) == 6: if area_id.endswith("0000"): return "province" if area_id.endswith("00"): return "city" return "county" return "poi" # semantic graph areas (e.g. jiaxiu_lou) def _area_parent(area_id: str, level: str) -> str | None: if level == "city": return area_id[:2] + "0000" if level == "county": return area_id[:4] + "00" return None @router.get("/areas") async def _list(_user: CurrentUser = None): return await list_areas() @router.get("/areas/tree") async def _tree(_user: CurrentUser = None): """省 > 市 > 区县 hierarchy, plus a group for 专题/自定义 areas.""" rows = await list_areas() by_id = {r["area_id"]: r for r in rows} def node(r): return { "value": r["area_id"], "title": f'{r["name"]}({r["area_id"]})', "name": r["name"], "level": r["level"], "children": [], } nodes = {aid: node(r) for aid, r in by_id.items()} roots: list = [] specials: list = [] for aid, r in by_id.items(): lvl = r["level"] if lvl == "province": roots.append(nodes[aid]) elif lvl in ("city", "county"): parent = r.get("parent_id") if parent and parent in nodes: nodes[parent]["children"].append(nodes[aid]) else: roots.append(nodes[aid]) else: specials.append(nodes[aid]) # collapse empty children def clean(n): n["children"] = [clean(c) for c in n["children"]] if not n["children"]: n.pop("children") return n tree = [clean(r) for r in roots] if specials: tree.append({ "value": "__special__", "title": "专题 / 自定义区域", "selectable": False, "children": specials, }) return tree @router.post("/areas/sync-from-graph") async def _sync(_user: CurrentUser): """Pull Area nodes from the knowledge graph, computing admin levels.""" try: g = _get_graph() res = g.query("MATCH (a:Area) RETURN a") except Exception as e: raise HTTPException(400, f"读取图谱失败:{str(e)[:200]}") synced = 0 for row in res.result_set: node = row[0] props = getattr(node, "properties", {}) or {} area_id = str(props.get("area_id") or "").strip() if not area_id: continue name = str(props.get("name") or area_id) lvl = _area_level(area_id) await upsert_area(area_id, name, lvl, _area_parent(area_id, lvl)) synced += 1 return {"synced": synced} @router.patch("/areas/{area_id}") async def _update(area_id: str, body: AreaUpdate, _user: CurrentUser): row = await update_area(area_id, body.model_dump(exclude_none=True)) if not row: raise HTTPException(404, "区域不存在") return row