落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-14 14:09:45 +08:00
co-authored by Cursor
parent 8bdf4990a2
commit a70bce3c8f
138 changed files with 7239 additions and 1085 deletions
+216
View File
@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
"""
远端修复:inq_quote_template_version.mapping_json 非法(PowerShell ConvertTo-Json/种子写入无引号)导致 previewSample 失败。
对每条版本:若 JSON 不可解析,则按上传 xlsx + 标签词典重算合法 mapping_json。
副作用:UPDATE MySQL mapping_json / mapping_point_count / parse_message;不改 object 文件。
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
try:
import pymysql
except ImportError:
pymysql = None
try:
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
except ImportError:
load_workbook = None
get_column_letter = None
def load_env(path: Path) -> dict[str, str]:
out: dict[str, str] = {}
if not path.is_file():
return out
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
out[k.strip()] = v.strip().strip('"')
return out
def try_parse(s: str):
if not s:
return None
try:
return json.loads(s)
except Exception:
return None
def repair_ps_style(s: str):
"""尽力把无引号对象字面量修成 JSON(仅用于种子脏数据)。"""
if not s or s[0] != "{":
return None
# 已是合法 JSON
if try_parse(s) is not None:
return try_parse(s)
# 给 key 加引号:{foo: 或 ,foo:
t = re.sub(r"([,{]\s*)([A-Za-z_][A-Za-z0-9_]*)\s*:", r'\1"\2":', s)
# 给像 Sheet2!B8 / quote-template-mapping-v1 / 纯标识 的裸字符串值加引号
# 匹配 :value 直到 ,} ]
def quote_val(m):
prefix, val, suffix = m.group(1), m.group(2), m.group(3)
val = val.strip()
if val in ("[]", "{}", "true", "false", "null") or re.match(r"^-?\d+(\.\d+)?$", val):
return prefix + val + suffix
if (val.startswith('"') and val.endswith('"')) or (val.startswith("{") or val.startswith("[")):
return prefix + val + suffix
return prefix + json.dumps(val, ensure_ascii=False) + suffix
t2 = re.sub(r'(:\s*)([^,\[\]{}]+)([,}\]])', quote_val, t)
return try_parse(t2)
def rough_fields(xlsx: Path, label_dict: dict[str, str]) -> dict[str, str]:
fields: dict[str, str] = {}
if load_workbook is None:
return fields
wb = load_workbook(xlsx, read_only=True, data_only=True)
try:
# 扫所有 sheet(种子映射常在 Sheet2)
for ws in wb.worksheets:
sheet = ws.title
for row in ws.iter_rows(max_row=80, max_col=24):
for cell in row:
val = cell.value
if val is None:
continue
text = str(val).strip()
if not text:
continue
norm = text.replace(":", ":")
code = None
for lab, c in label_dict.items():
lab_n = lab.replace(":", ":").replace(":", "").strip()
t_n = norm.replace(":", "").strip()
if lab_n and (t_n == lab_n or t_n.startswith(lab_n)):
code = c
break
if code and code not in fields:
col_i = int(getattr(cell, "column", 1) or 1)
fill = f"{sheet}!{get_column_letter(col_i + 1)}{cell.row}"
fields[code] = fill
finally:
wb.close()
return fields
def main() -> int:
env = load_env(Path(r"E:\wwwroot\.env"))
upload = env.get("JEECG_UPLOAD_PATH") or r"E:\wwwroot\data\upFiles"
user = env.get("MYSQL_USER") or "inquiry_robot"
password = env.get("MYSQL_PASSWORD") or ""
db = env.get("MYSQL_DB") or "inquiry_robot"
host = "127.0.0.1"
contract_paths = [
Path(r"E:\wwwroot\inquiry-new\config\contracts\quote-template-mapping-v1.json"),
Path(r"C:\Users\ytd_extdev_0903\incoming\quote-template-mapping-v1.json"),
]
label_dict: dict[str, str] = {}
for cp in contract_paths:
if cp.is_file():
label_dict = (json.loads(cp.read_text(encoding="utf-8")).get("labelDictionary") or {})
break
if not label_dict:
# 最小词典,避免完全空
label_dict = {
"客户名称": "customer_name",
"客户": "customer_name",
"报价单编号": "quote_no",
"报价日期": "quote_date",
"起运港": "pol",
"目的港": "pod",
"业务员": "sales_name",
}
if pymysql is None:
print("ERR no pymysql")
return 2
conn = pymysql.connect(
host=host, user=user, password=password, database=db, charset="utf8mb4", port=3306
)
fixed = 0
ok_already = 0
failed = 0
try:
with conn.cursor() as cur:
cur.execute(
"SELECT id, object_key, file_name, mapping_json FROM inq_quote_template_version"
)
rows = cur.fetchall()
for vid, object_key, file_name, mapping_json in rows:
parsed = try_parse(mapping_json or "")
if parsed is None:
parsed = repair_ps_style(mapping_json or "")
xlsx = Path(upload) / str(object_key).replace("/", os.sep)
if parsed is None or not isinstance(parsed.get("fields"), dict) or not parsed.get("fields"):
fields = {}
if xlsx.is_file():
fields = rough_fields(xlsx, label_dict)
# 若修复出的 fields 可用则保留其它键
if parsed and isinstance(parsed.get("fields"), dict) and parsed.get("fields"):
fields = parsed["fields"]
elif not fields and parsed and isinstance(parsed.get("fields"), dict):
fields = parsed["fields"]
mapping = {
"schemaVersion": "quote-template-mapping-v1",
"fields": fields,
"fixedYellowCells": [],
"unmappedRequired": [],
"note": "repaired_mapping_json",
}
else:
# 合法但规范化
mapping = {
"schemaVersion": parsed.get("schemaVersion") or "quote-template-mapping-v1",
"fields": parsed.get("fields") or {},
"fixedYellowCells": parsed.get("fixedYellowCells") or [],
"unmappedRequired": parsed.get("unmappedRequired") or [],
}
if try_parse(mapping_json or "") is not None and mapping_json.strip().startswith('{"'):
ok_already += 1
print(f"OK_ALREADY {vid} points={len(mapping['fields'])}")
continue
new_json = json.dumps(mapping, ensure_ascii=False, separators=(",", ":"))
points = len(mapping.get("fields") or {})
with conn.cursor() as cur:
cur.execute(
"UPDATE inq_quote_template_version SET mapping_json=%s, mapping_point_count=%s, "
"parse_status=%s, parse_message=%s WHERE id=%s",
(
new_json,
points,
"parsed",
f"repaired mapping, {points} points",
vid,
),
)
conn.commit()
fixed += 1
exists = xlsx.is_file()
print(f"FIXED {vid} points={points} file_exists={exists} name={file_name}")
except Exception as e:
failed += 1
print("ERR", e)
return 1
finally:
conn.close()
print(f"REPAIR_DONE fixed={fixed} ok_already={ok_already} failed={failed}")
return 0
if __name__ == "__main__":
sys.exit(main())