From 44e6f5b18acece7524d824e38cb275418b7e8379 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 7 Sep 2026 14:26:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20m0014=E8=A1=A5m0012=E9=9D=99?= =?UTF-8?q?=E9=BB=98=E4=B8=A2=E5=A4=B1=E7=9A=84llm=5Fusage=5Fstatus=20FAIL?= =?UTF-8?q?ED=E5=AD=97=E5=85=B8=E8=A1=8C(INSERT=20IGNORE=E5=85=88=E4=BA=8E?= =?UTF-8?q?legacy=E6=8C=AA=E7=A7=BB+CI=E6=8E=92=E5=BA=8F=E8=A7=84=E5=88=99?= =?UTF-8?q?=E6=92=9E=E5=94=AF=E4=B8=80=E7=B4=A2=E5=BC=95=E8=A2=AB=E8=B7=B3?= =?UTF-8?q?=E8=BF=87)+import=5Finit=E5=8D=95=E8=A1=8C=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E5=91=8A=E8=AD=A6=E8=B7=B3=E8=BF=87=E4=B8=8D=E4=B8=AD=E6=AD=A2?= =?UTF-8?q?=E5=85=A8=E9=87=8F(=E8=AE=A1=E7=AE=97id=E4=B8=8Elegacy=E8=A1=8C?= =?UTF-8?q?CI=E6=92=9EPRIMARY=E5=AE=9E=E6=B5=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../m0014_llm_usage_status_failed_dict.json | 11 +++++ scripts/import_init.py | 43 +++++++++++++------ 2 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 deploy/migrations/m0014_llm_usage_status_failed_dict.json diff --git a/deploy/migrations/m0014_llm_usage_status_failed_dict.json b/deploy/migrations/m0014_llm_usage_status_failed_dict.json new file mode 100644 index 0000000..3ff47a7 --- /dev/null +++ b/deploy/migrations/m0014_llm_usage_status_failed_dict.json @@ -0,0 +1,11 @@ +{"id": "m0014", + "title": "补 m0012 静默丢失的 llm_usage_status FAILED 字典行——m0012 的 INSERT IGNORE 先于 legacy 挪移 UPDATE 执行,CI 排序规则下 k='FAILED' 与旧行 k='failed' 撞 (parentid,k) 唯一索引被 IGNORE 跳过,随后旧行挪去 legacy 导致新枚举行缺失;import_init 补插又因计算 id 与 legacy 行 CI 撞 PRIMARY 而中止", + "backup_tables": ["appcodes_kv"], + "up": [ + {"op": "sql", + "sql": "INSERT IGNORE INTO appcodes_kv (id,parentid,k,v) VALUES ('llumg_FAILED','llm_usage_status','FAILED','失败')"} + ], + "down": [ + {"op": "sql", + "sql": "DELETE FROM appcodes_kv WHERE id='llumg_FAILED' AND parentid='llm_usage_status'"} + ]} diff --git a/scripts/import_init.py b/scripts/import_init.py index dd16bcb..b076ee0 100644 --- a/scripts/import_init.py +++ b/scripts/import_init.py @@ -14,6 +14,8 @@ ROOT_DIR = os.path.dirname(SCRIPT_DIR) sys.path.insert(0, os.path.join(ROOT_DIR, 'py3', 'lib', 'python3.10', 'site-packages')) sys.path.insert(0, ROOT_DIR) +from pymysql.err import IntegrityError + from sqlor.dbpools import DBPools from appPublic.jsonConfig import getConfig from appPublic.folderUtils import ProgramPath @@ -64,6 +66,24 @@ def normalize(data): return raw_ac, data.get('appcodes_kv', []), data.get('organization', []) +async def _safe_insert(sor, tbl, row, total, tkey): + """插入一行,主键/唯一键冲突时告警跳过而非中止全量导入。 + + 实测事故(2026-09-07):appcodes_kv 的 CI 排序规则下,计算 id + 'llm_usage_status_FAILED' 与 legacy 行 id 'llm_usage_status_failed' PRIMARY 撞车, + (parentid,k) 检查通过但 INSERT 抛 1062 → 后续所有模块的导入整段中止。 + 单行冲突属数据态问题(迁移/历史数据),不应阻断其余行导入。 + """ + try: + await sor.C(tbl, row) + total[tkey] = total.get(tkey, 0) + 1 + except IntegrityError as e: + if e.args[0] in (1062,): + print(f' WARN: {tbl} 行冲突跳过(已存在等值行): id={row.get("id")} {e.args[1][:80]}') + else: + raise + + async def main(): config = getConfig(ROOT_DIR, NS={'workdir': ROOT_DIR, 'ProgramPath': ProgramPath()}) DBPools(config.databases) @@ -83,12 +103,11 @@ async def main(): for ac in appcodes: recs = await sor.R('appcodes', {'id': ac['id']}) if not recs: - await sor.C('appcodes', { + await _safe_insert(sor, 'appcodes', { 'id': ac['id'], 'name': ac.get('name', ''), 'hierarchy_flg': ac.get('hierarchy_flg', '0'), - }) - total['appcodes'] += 1 + }, total, 'appcodes') for kv in appcodes_kv: # 幂等检查用 (parentid, k) 组合(appcodes_kv 有 parentid+k 唯一索引, # 且历史数据 id 是 UUID 非 parentid_k 组合键,用 id 检查会漏判 → 唯一索引冲突) @@ -96,31 +115,28 @@ async def main(): "SELECT id FROM appcodes_kv WHERE parentid=${p}$ AND k=${k}$", {"p": kv.get('parentid', ''), "k": kv.get('k', '')}) if not recs: - await sor.C('appcodes_kv', { + await _safe_insert(sor, 'appcodes_kv', { 'id': kv['id'], 'parentid': kv.get('parentid', ''), 'k': kv.get('k', ''), 'v': kv.get('v', ''), - }) - total['appcodes_kv'] += 1 + }, total, 'appcodes_kv') for org in organization: recs = await sor.R('organization', {'id': org['id']}) if not recs: - await sor.C('organization', { + await _safe_insert(sor, 'organization', { 'id': org['id'], 'orgname': org.get('orgname', ''), - }) - total['organization'] += 1 + }, total, 'organization') # RBAC 角色(如 app_audit 的 owner.audit 审计独立角色),幂等按主键 id 检查 for r in data.get('roles', []): recs = await sor.R('role', {'id': r['id']}) if not recs: - await sor.C('role', { + await _safe_insert(sor, 'role', { 'id': r['id'], 'orgtypeid': r.get('orgtypeid', '0'), 'name': r.get('name', ''), - }) - total['roles'] += 1 + }, total, 'roles') # 记账配置表(accounting 模块:subject/account_config/accounting_config/currency/exchange_rate) # 幂等按主键 id 检查,字段名与表结构一致 for tbl, recs_data in [ @@ -135,8 +151,7 @@ async def main(): if not existing: # 去掉 None 值字段(DB 用 NULL 而非 None) clean = {k: v for k, v in rec.items() if v is not None} - await sor.C(tbl, clean) - total[tbl] = total.get(tbl, 0) + 1 + await _safe_insert(sor, tbl, clean, total, tbl) summary = ' '.join(f'{k} {v}' for k, v in total.items()) print(f'import_init: {summary}')