# -*- 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())