Initial travel knowledge graph release

This commit is contained in:
2026-06-09 09:56:26 +08:00
commit 5f061295d8
402 changed files with 103877 additions and 0 deletions

113
app/api/areas.py Normal file
View File

@@ -0,0 +1,113 @@
"""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