fix(deploy): m0014补m0012静默丢失的llm_usage_status FAILED字典行(INSERT IGNORE先于legacy挪移+CI排序规则撞唯一索引被跳过)+import_init单行冲突告警跳过不中止全量(计算id与legacy行CI撞PRIMARY实测)

This commit is contained in:
yumoqing 2026-09-07 14:26:01 +08:00
parent 20a6fd8283
commit 44e6f5b18a
2 changed files with 40 additions and 14 deletions

View File

@ -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'"}
]}

View File

@ -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-07appcodes_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}')