114 lines
3.3 KiB
Python
114 lines
3.3 KiB
Python
"""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
|