fix: bind knowledge maps to explicit regions
This commit is contained in:
1 parent
329a94d457
commit
90f75d41ae
15 files changed
+1126
-275
No files matched your search
+109
-1
@@ -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": {
|
||||
|
||||
Reference in new issue
Block a user