fix(migration): Cookie菜单迁移改为按业务键查找并支持 id 顺延

原迁移只按固定 id(76-84) 判断是否存在:若线上 admin_power 的
这些 id 已被其它菜单占用,升级会静默跳过,菜单依旧不出现,
而 start.sh 里 flask db upgrade 的失败/跳过只打 WARN,不易察觉。

调整:
- 先按 code / name+type 业务键查找,命中则仅补 open_type、不重复插入
- 固定 id 被占用时顺延到 max(id)+1,并用 id_map 修正父子 parent_id 关联

已用临时库验证两种场景:
- 76-84 被占用 -> 菜单落在 85-93,父子关联与 open_type 正确
- 重复执行 -> 仍为 9 条,不重复插入
This commit is contained in:
bwstudio
2026-09-10 18:59:31 +08:00
parent ecfb56bb0f
commit 1b68363bfd
@@ -44,37 +44,71 @@ MENUS = [
def upgrade(): def upgrade():
"""幂等补齐 Cookie 管理菜单。
"仅按固定 id 判断"不同:先按业务键(code / name+type)查找;
固定 id 若已被其它菜单占用则自动顺延到 max(id)+1,
避免线上 id 分配与本地不一致时静默跳过、菜单仍然不出现。
"""
conn = op.get_bind() conn = op.get_bind()
now = datetime.datetime.now() now = datetime.datetime.now()
for mid, name, mtype, code, url, open_type, parent_id, icon, sort, enable in MENUS: def next_free_id(preferred):
row = conn.execute( exist = conn.execute(
sa.text("SELECT id, open_type FROM admin_power WHERE id = :id"), sa.text("SELECT id FROM admin_power WHERE id = :i"), {"i": preferred}
{"id": mid},
).fetchone() ).fetchone()
if exist is None:
return preferred
mx = conn.execute(sa.text("SELECT MAX(id) FROM admin_power")).scalar() or 0
return int(mx) + 1
if row is None: def find_by_code(code):
conn.execute( if not code:
sa.text( return None
"INSERT INTO admin_power " return conn.execute(
"(id, name, type, code, url, open_type, parent_id, icon, sort, " sa.text("SELECT id FROM admin_power WHERE code = :c"), {"c": code}
" enable, create_time, update_time) " ).scalar()
"VALUES (:id, :name, :type, :code, :url, :open_type, :parent_id, "
" :icon, :sort, :enable, :ct, :ut)" def find_by_name_type(name, mtype):
), return conn.execute(
{ sa.text("SELECT id FROM admin_power WHERE name = :n AND type = :t"),
"id": mid, "name": name, "type": mtype, "code": code, {"n": name, "t": mtype},
"url": url, "open_type": open_type, "parent_id": parent_id, ).scalar()
"icon": icon, "sort": sort, "enable": enable,
"ct": now, "ut": now, id_map = {} # 迁移里声明的固定 id -> 实际落库的 id(用于修正 parent_id
},
) for mid, name, mtype, code, url, open_type, parent_id, icon, sort, enable in MENUS:
elif not row[1] and open_type: found = find_by_code(code) or find_by_name_type(name, mtype)
# 历史数据:菜单已存在但打开方式为空,补齐 _iframe if found is not None:
conn.execute( id_map[mid] = int(found)
sa.text("UPDATE admin_power SET open_type = :ot WHERE id = :id"), if open_type:
{"ot": open_type, "id": mid}, conn.execute(
) sa.text(
"UPDATE admin_power SET open_type = :ot WHERE id = :id "
"AND (open_type IS NULL OR open_type = '')"
),
{"ot": open_type, "id": int(found)},
)
continue
real_parent = id_map.get(parent_id, parent_id) if parent_id else 0
new_id = next_free_id(mid)
conn.execute(
sa.text(
"INSERT INTO admin_power "
"(id, name, type, code, url, open_type, parent_id, icon, sort, "
" enable, create_time, update_time) "
"VALUES (:id, :name, :type, :code, :url, :open_type, :parent_id, "
" :icon, :sort, :enable, :ct, :ut)"
),
{
"id": new_id, "name": name, "type": mtype, "code": code,
"url": url, "open_type": open_type, "parent_id": real_parent,
"icon": icon, "sort": sort, "enable": enable,
"ct": now, "ut": now,
},
)
id_map[mid] = new_id
def downgrade(): def downgrade():