fix: bind knowledge maps to explicit regions

This commit is contained in:
xuelong committed 2026-08-26 02:26:24 -07:00
1 parent 329a94d457
commit 90f75d41ae
15 files changed
+1126 -275

No files matched your search

+109 -1
View File
@@ -20,6 +20,7 @@ from app.config import settings
SAFE_RESOURCE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$")
SAFE_SCHEMA_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,127}$")
SAFE_SCHEMA_VERSION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
SAFE_ADCODE = re.compile(r"^\d{6}$")
TYPE_ALIASES = {
"str": "string",
@@ -51,6 +52,15 @@ SUPPORTED_FIELD_TYPES = {
GRAPH_IMPORT_BATCH_THRESHOLD = 500
GRAPH_IMPORT_BATCH_SIZE = 250
SPATIAL_REGION_LEVELS = {"province", "city", "district"}
MAP_ENTITY_TYPES = {
"FoodPlace",
"Hotel",
"ScenicSpot",
"TransitFacility",
"BusStop",
}
class ProjectValidationError(ValueError):
"""Raised when a provision payload is invalid before any write occurs."""
@@ -69,6 +79,48 @@ class GraphImportError(RuntimeError):
self.cleanup_required = cleanup_error is not None
def _adcode_belongs_to_region(adcode: str, region_adcode: str, level: str) -> bool:
if not SAFE_ADCODE.fullmatch(adcode) or not SAFE_ADCODE.fullmatch(region_adcode):
return False
if level == "province":
return adcode[:2] == region_adcode[:2]
if level == "city":
return adcode[:4] == region_adcode[:4]
return adcode == region_adcode
def _region_names_match(value: Any, region_name: str) -> bool:
candidate = str(value or "").strip()
expected = str(region_name or "").strip()
return bool(candidate and expected and (candidate in expected or expected in candidate))
def _is_spatial_map_node(node: Mapping[str, Any]) -> bool:
properties = node.get("properties")
if not isinstance(properties, Mapping):
return False
has_coordinates = (
properties.get("lng") is not None
and properties.get("lat") is not None
) or (
properties.get("longitude") is not None
and properties.get("latitude") is not None
)
if not has_coordinates:
return False
labels = {
str(label)
for label in (node.get("labels") or [])
if str(label).strip()
}
labels.add(str(node.get("type") or ""))
return bool(
labels & MAP_ENTITY_TYPES
or str(properties.get("type_label") or "").strip()
or str(properties.get("place_type") or "").strip()
)
def _as_mapping(value: Any) -> dict[str, Any]:
return dict(value) if isinstance(value, Mapping) else {}
@@ -246,6 +298,20 @@ def normalize_provision_payload(body: Any) -> dict[str, Any]:
map_scope = str(raw_spatial_map.get("scope") or "libo").strip().lower()
if map_scope not in {"libo", "guizhou"}:
errors.append("spatial_map.scope 仅支持 libo 或 guizhou")
map_region_name = str(raw_spatial_map.get("region_name") or "").strip()
map_region_adcode = str(raw_spatial_map.get("region_adcode") or "").strip()
map_region_level = str(raw_spatial_map.get("region_level") or "").strip().lower()
if map_enabled:
if not map_region_name:
errors.append("启用知识地图时必须选择地图区域")
if not SAFE_ADCODE.fullmatch(map_region_adcode):
errors.append("spatial_map.region_adcode 必须是 6 位行政区划代码")
if map_region_level not in SPATIAL_REGION_LEVELS:
errors.append("spatial_map.region_level 仅支持 province、city 或 district")
if SAFE_ADCODE.fullmatch(map_region_adcode):
expected_scope = "libo" if map_region_adcode == "522722" else "guizhou"
if map_scope != expected_scope:
errors.append("spatial_map.scope 与所选地图区域不一致")
if not project_id:
errors.append("项目英文标识不能为空")
@@ -399,6 +465,42 @@ def normalize_provision_payload(body: Any) -> dict[str, Any]:
normalized_node["labels"] = normalized_labels
nodes.append(normalized_node)
if map_enabled:
map_nodes = [node for node in nodes if _is_spatial_map_node(node)]
if not map_nodes:
errors.append("启用知识地图时,图谱数据至少需要一个带经纬度和业态的空间节点")
elif SAFE_ADCODE.fullmatch(map_region_adcode) and map_region_level in SPATIAL_REGION_LEVELS:
unverified_region = 0
mismatched_adcodes: set[str] = set()
for node in map_nodes:
properties = node["properties"]
adcode = str(properties.get("adcode") or "").strip()
if adcode and not _adcode_belongs_to_region(
adcode,
map_region_adcode,
map_region_level,
):
mismatched_adcodes.add(adcode)
if adcode:
continue
region_field = {
"province": properties.get("province"),
"city": properties.get("city"),
"district": properties.get("district"),
}[map_region_level]
if not _region_names_match(region_field, map_region_name):
unverified_region += 1
if unverified_region:
errors.append(
f"知识地图空间节点有 {unverified_region} 条既无可用 adcode,"
"也没有与所选区域匹配的省市区县字段"
)
if mismatched_adcodes:
examples = "、".join(sorted(mismatched_adcodes)[:5])
errors.append(
f"知识地图数据与所选区域 {map_region_name} 不一致,发现行政区划代码:{examples}"
)
relations: list[dict[str, Any]] = []
for index, raw_relation in enumerate(raw_relations):
path = f"graph_data.relations[{index}]"
@@ -462,7 +564,13 @@ def normalize_provision_payload(body: Any) -> dict[str, Any]:
"display_name": display_name,
"description": str(body.get("description") or "").strip() or None,
"graph_name": graph_name,
"spatial_map": {"enabled": map_enabled, "scope": map_scope},
"spatial_map": {
"enabled": map_enabled,
"scope": map_scope,
"region_name": map_region_name,
"region_adcode": map_region_adcode,
"region_level": map_region_level or "province",
},
"schema": schema,
"graph_data": {"nodes": nodes, "relations": relations},
"counts": {