feat: resolve shortage names from Git-synced Kingdee material cache
This commit is contained in:
@@ -22,6 +22,11 @@ truststore.inject_into_ssl()
|
||||
|
||||
import lark_oapi as lark
|
||||
|
||||
if __package__:
|
||||
from .material_name_cache import MaterialNameCacheError, load_material_names
|
||||
else:
|
||||
from material_name_cache import MaterialNameCacheError, load_material_names
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
QUERY_SCRIPT = (
|
||||
@@ -599,6 +604,8 @@ def shortage_material_names_line(shortages: list[Any]) -> str:
|
||||
"""Show only names from shortage records, never codes or other raw fields."""
|
||||
names: list[str] = []
|
||||
missing_names = 0
|
||||
cached_names: dict[str, str] | None = None
|
||||
cache_error: str | None = None
|
||||
for item in shortages:
|
||||
name = None
|
||||
if isinstance(item, dict):
|
||||
@@ -607,6 +614,16 @@ def shortage_material_names_line(shortages: list[Any]) -> str:
|
||||
if isinstance(value, str) and value.strip():
|
||||
name = " ".join(value.split())
|
||||
break
|
||||
if name is None:
|
||||
code = item.get("缺料物料")
|
||||
if isinstance(code, str) and code.strip():
|
||||
if cached_names is None and cache_error is None:
|
||||
try:
|
||||
cached_names = load_material_names()
|
||||
except MaterialNameCacheError as exc:
|
||||
cache_error = str(exc)
|
||||
if cached_names is not None:
|
||||
name = cached_names.get(code.strip())
|
||||
if name is None:
|
||||
missing_names += 1
|
||||
elif name not in names:
|
||||
@@ -614,7 +631,12 @@ def shortage_material_names_line(shortages: list[Any]) -> str:
|
||||
|
||||
text = ";".join(names)
|
||||
if missing_names:
|
||||
missing_text = f"{missing_names}项缺料记录未返回物料名称,需人工复核"
|
||||
missing_text = f"{missing_names}项缺料记录未找到物料名称"
|
||||
missing_text += (
|
||||
f"({cache_error}),需补齐缓存后复核"
|
||||
if cache_error is not None
|
||||
else ",请核对物料编码并更新本地名称缓存后复核"
|
||||
)
|
||||
text = f"{text}(另有{missing_text})" if text else missing_text
|
||||
return f"- 缺料物料明细:{text}"
|
||||
|
||||
|
||||
64
shared/runtime/material_name_cache.py
Normal file
64
shared/runtime/material_name_cache.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Portable, read-only material-code/name lookup for delivery replies."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
MATERIAL_NAME_CACHE_FILE = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "capabilities/am516-delivery-prediction/data/material_names.json"
|
||||
)
|
||||
|
||||
|
||||
class MaterialNameCacheError(ValueError):
|
||||
"""A cache error that contains no credentials or raw ERP records."""
|
||||
|
||||
|
||||
def material_names_hash(materials: dict[str, str]) -> str:
|
||||
content = json.dumps(materials, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def material_names_from_records(records: list[dict[str, Any]]) -> dict[str, str]:
|
||||
if not records:
|
||||
raise MaterialNameCacheError("物料查询结果为空,保留原缓存。")
|
||||
materials: dict[str, str] = {}
|
||||
for row in records:
|
||||
code, name = row.get("FNumber"), row.get("FName")
|
||||
if not isinstance(code, str) or not code.strip() or not isinstance(name, str) or not name.strip():
|
||||
raise MaterialNameCacheError("物料记录缺少有效编码或名称,保留原缓存。")
|
||||
code, name = code.strip(), " ".join(name.split())
|
||||
if code in materials and materials[code] != name:
|
||||
raise MaterialNameCacheError("同一物料编码存在不同名称,需核对后再刷新缓存。")
|
||||
materials[code] = name
|
||||
return dict(sorted(materials.items()))
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def load_material_names(path: Path = MATERIAL_NAME_CACHE_FILE) -> dict[str, str]:
|
||||
"""Load once per process; a Git update takes effect after the bot restarts."""
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError as exc:
|
||||
raise MaterialNameCacheError("本地物料名称缓存文件缺失") from exc
|
||||
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
||||
raise MaterialNameCacheError("本地物料名称缓存文件不可读取或格式错误") from exc
|
||||
if not isinstance(payload, dict) or payload.get("schema_version") != 1:
|
||||
raise MaterialNameCacheError("本地物料名称缓存版本或结构无效")
|
||||
materials = payload.get("materials")
|
||||
if not isinstance(materials, dict) or not materials:
|
||||
raise MaterialNameCacheError("本地物料名称缓存为空或结构无效")
|
||||
if any(
|
||||
not isinstance(code, str) or not code.strip()
|
||||
or not isinstance(name, str) or not name.strip()
|
||||
for code, name in materials.items()
|
||||
):
|
||||
raise MaterialNameCacheError("本地物料名称缓存包含无效编码或名称")
|
||||
if payload.get("material_count") != len(materials) or payload.get("content_sha256") != material_names_hash(materials):
|
||||
raise MaterialNameCacheError("本地物料名称缓存计数或完整性校验失败")
|
||||
return materials
|
||||
Reference in New Issue
Block a user