fix(deploy): m0014补m0012静默丢失的llm_usage_status FAILED字典行(INSERT IGNORE先于legacy挪移+CI排序规则撞唯一索引被跳过)+import_init单行冲突告警跳过不中止全量(计算id与legacy行CI撞PRIMARY实测)
This commit is contained in:
parent
20a6fd8283
commit
44e6f5b18a
11
deploy/migrations/m0014_llm_usage_status_failed_dict.json
Normal file
11
deploy/migrations/m0014_llm_usage_status_failed_dict.json
Normal 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'"}
|
||||||
|
]}
|
||||||
@ -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, os.path.join(ROOT_DIR, 'py3', 'lib', 'python3.10', 'site-packages'))
|
||||||
sys.path.insert(0, ROOT_DIR)
|
sys.path.insert(0, ROOT_DIR)
|
||||||
|
|
||||||
|
from pymysql.err import IntegrityError
|
||||||
|
|
||||||
from sqlor.dbpools import DBPools
|
from sqlor.dbpools import DBPools
|
||||||
from appPublic.jsonConfig import getConfig
|
from appPublic.jsonConfig import getConfig
|
||||||
from appPublic.folderUtils import ProgramPath
|
from appPublic.folderUtils import ProgramPath
|
||||||
@ -64,6 +66,24 @@ def normalize(data):
|
|||||||
return raw_ac, data.get('appcodes_kv', []), data.get('organization', [])
|
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():
|
async def main():
|
||||||
config = getConfig(ROOT_DIR, NS={'workdir': ROOT_DIR, 'ProgramPath': ProgramPath()})
|
config = getConfig(ROOT_DIR, NS={'workdir': ROOT_DIR, 'ProgramPath': ProgramPath()})
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
@ -83,12 +103,11 @@ async def main():
|
|||||||
for ac in appcodes:
|
for ac in appcodes:
|
||||||
recs = await sor.R('appcodes', {'id': ac['id']})
|
recs = await sor.R('appcodes', {'id': ac['id']})
|
||||||
if not recs:
|
if not recs:
|
||||||
await sor.C('appcodes', {
|
await _safe_insert(sor, 'appcodes', {
|
||||||
'id': ac['id'],
|
'id': ac['id'],
|
||||||
'name': ac.get('name', ''),
|
'name': ac.get('name', ''),
|
||||||
'hierarchy_flg': ac.get('hierarchy_flg', '0'),
|
'hierarchy_flg': ac.get('hierarchy_flg', '0'),
|
||||||
})
|
}, total, 'appcodes')
|
||||||
total['appcodes'] += 1
|
|
||||||
for kv in appcodes_kv:
|
for kv in appcodes_kv:
|
||||||
# 幂等检查用 (parentid, k) 组合(appcodes_kv 有 parentid+k 唯一索引,
|
# 幂等检查用 (parentid, k) 组合(appcodes_kv 有 parentid+k 唯一索引,
|
||||||
# 且历史数据 id 是 UUID 非 parentid_k 组合键,用 id 检查会漏判 → 唯一索引冲突)
|
# 且历史数据 id 是 UUID 非 parentid_k 组合键,用 id 检查会漏判 → 唯一索引冲突)
|
||||||
@ -96,31 +115,28 @@ async def main():
|
|||||||
"SELECT id FROM appcodes_kv WHERE parentid=${p}$ AND k=${k}$",
|
"SELECT id FROM appcodes_kv WHERE parentid=${p}$ AND k=${k}$",
|
||||||
{"p": kv.get('parentid', ''), "k": kv.get('k', '')})
|
{"p": kv.get('parentid', ''), "k": kv.get('k', '')})
|
||||||
if not recs:
|
if not recs:
|
||||||
await sor.C('appcodes_kv', {
|
await _safe_insert(sor, 'appcodes_kv', {
|
||||||
'id': kv['id'],
|
'id': kv['id'],
|
||||||
'parentid': kv.get('parentid', ''),
|
'parentid': kv.get('parentid', ''),
|
||||||
'k': kv.get('k', ''),
|
'k': kv.get('k', ''),
|
||||||
'v': kv.get('v', ''),
|
'v': kv.get('v', ''),
|
||||||
})
|
}, total, 'appcodes_kv')
|
||||||
total['appcodes_kv'] += 1
|
|
||||||
for org in organization:
|
for org in organization:
|
||||||
recs = await sor.R('organization', {'id': org['id']})
|
recs = await sor.R('organization', {'id': org['id']})
|
||||||
if not recs:
|
if not recs:
|
||||||
await sor.C('organization', {
|
await _safe_insert(sor, 'organization', {
|
||||||
'id': org['id'],
|
'id': org['id'],
|
||||||
'orgname': org.get('orgname', ''),
|
'orgname': org.get('orgname', ''),
|
||||||
})
|
}, total, 'organization')
|
||||||
total['organization'] += 1
|
|
||||||
# RBAC 角色(如 app_audit 的 owner.audit 审计独立角色),幂等按主键 id 检查
|
# RBAC 角色(如 app_audit 的 owner.audit 审计独立角色),幂等按主键 id 检查
|
||||||
for r in data.get('roles', []):
|
for r in data.get('roles', []):
|
||||||
recs = await sor.R('role', {'id': r['id']})
|
recs = await sor.R('role', {'id': r['id']})
|
||||||
if not recs:
|
if not recs:
|
||||||
await sor.C('role', {
|
await _safe_insert(sor, 'role', {
|
||||||
'id': r['id'],
|
'id': r['id'],
|
||||||
'orgtypeid': r.get('orgtypeid', '0'),
|
'orgtypeid': r.get('orgtypeid', '0'),
|
||||||
'name': r.get('name', ''),
|
'name': r.get('name', ''),
|
||||||
})
|
}, total, 'roles')
|
||||||
total['roles'] += 1
|
|
||||||
# 记账配置表(accounting 模块:subject/account_config/accounting_config/currency/exchange_rate)
|
# 记账配置表(accounting 模块:subject/account_config/accounting_config/currency/exchange_rate)
|
||||||
# 幂等按主键 id 检查,字段名与表结构一致
|
# 幂等按主键 id 检查,字段名与表结构一致
|
||||||
for tbl, recs_data in [
|
for tbl, recs_data in [
|
||||||
@ -135,8 +151,7 @@ async def main():
|
|||||||
if not existing:
|
if not existing:
|
||||||
# 去掉 None 值字段(DB 用 NULL 而非 None)
|
# 去掉 None 值字段(DB 用 NULL 而非 None)
|
||||||
clean = {k: v for k, v in rec.items() if v is not None}
|
clean = {k: v for k, v in rec.items() if v is not None}
|
||||||
await sor.C(tbl, clean)
|
await _safe_insert(sor, tbl, clean, total, tbl)
|
||||||
total[tbl] = total.get(tbl, 0) + 1
|
|
||||||
summary = ' '.join(f'{k} {v}' for k, v in total.items())
|
summary = ' '.join(f'{k} {v}' for k, v in total.items())
|
||||||
print(f'import_init: {summary}')
|
print(f'import_init: {summary}')
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user