原迁移只按固定 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 条,不重复插入
131 lines
4.8 KiB
Python
131 lines
4.8 KiB
Python
"""add cookie manager menus
|
||
|
||
「Cookie管理」一级菜单 + 二级「登录用户」/「登录网站」及各权限点,
|
||
此前只在本地库里手工 INSERT 过,从未随迁移分发。
|
||
|
||
由于 data/ 目录被 .gitignore 排除、部署环境数据库是独立 volume,
|
||
只同步代码无法带上这些菜单行,导致线上后台看不到 Cookie 管理模块。
|
||
|
||
本迁移以幂等方式补齐:
|
||
- 记录不存在 → 插入
|
||
- 已存在但 open_type 为空 → 补齐为 _iframe(兼容早期手工插入的数据)
|
||
|
||
不依赖数据库方言的 UPSERT 语法,SQLite / MySQL 均可执行。
|
||
|
||
Revision ID: d4e5f6a7b8c9
|
||
Revises: b2c3d4e5f6a7
|
||
"""
|
||
from alembic import op
|
||
import sqlalchemy as sa
|
||
import datetime
|
||
|
||
|
||
# revision identifiers, used by Alembic.
|
||
revision = 'd4e5f6a7b8c9'
|
||
down_revision = 'b2c3d4e5f6a7'
|
||
branch_labels = None
|
||
depends_on = None
|
||
|
||
|
||
# (id, name, type, code, url, open_type, parent_id, icon, sort, enable)
|
||
MENUS = [
|
||
(76, 'Cookie管理', '0', '', '', '', 0, 'layui-icon layui-icon-ear-phone', 5, 1),
|
||
(77, '登录用户', '1', 'system:cookie-user:main', '/system/cookie-user/', '_iframe',
|
||
76, 'layui-icon layui-icon-username', 1, 1),
|
||
(78, '登录用户新增', '2', 'system:cookie-user:add', '', '', 77, '', 1, 1),
|
||
(79, '登录用户编辑', '2', 'system:cookie-user:edit', '', '', 77, '', 2, 1),
|
||
(80, '登录用户删除', '2', 'system:cookie-user:remove', '', '', 77, '', 3, 1),
|
||
(81, '登录网站', '1', 'system:cookie-site:main', '/system/cookie-site/', '_iframe',
|
||
76, 'layui-icon layui-icon-web', 2, 1),
|
||
(82, '登录网站新增', '2', 'system:cookie-site:add', '', '', 81, '', 1, 1),
|
||
(83, '登录网站编辑', '2', 'system:cookie-site:edit', '', '', 81, '', 2, 1),
|
||
(84, '登录网站删除', '2', 'system:cookie-site:remove', '', '', 81, '', 3, 1),
|
||
]
|
||
|
||
|
||
def upgrade():
|
||
"""幂等补齐 Cookie 管理菜单。
|
||
|
||
与"仅按固定 id 判断"不同:先按业务键(code / name+type)查找;
|
||
固定 id 若已被其它菜单占用则自动顺延到 max(id)+1,
|
||
避免线上 id 分配与本地不一致时静默跳过、菜单仍然不出现。
|
||
"""
|
||
conn = op.get_bind()
|
||
now = datetime.datetime.now()
|
||
|
||
def next_free_id(preferred):
|
||
exist = conn.execute(
|
||
sa.text("SELECT id FROM admin_power WHERE id = :i"), {"i": preferred}
|
||
).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
|
||
|
||
def find_by_code(code):
|
||
if not code:
|
||
return None
|
||
return conn.execute(
|
||
sa.text("SELECT id FROM admin_power WHERE code = :c"), {"c": code}
|
||
).scalar()
|
||
|
||
def find_by_name_type(name, mtype):
|
||
return conn.execute(
|
||
sa.text("SELECT id FROM admin_power WHERE name = :n AND type = :t"),
|
||
{"n": name, "t": mtype},
|
||
).scalar()
|
||
|
||
id_map = {} # 迁移里声明的固定 id -> 实际落库的 id(用于修正 parent_id)
|
||
|
||
for mid, name, mtype, code, url, open_type, parent_id, icon, sort, enable in MENUS:
|
||
found = find_by_code(code) or find_by_name_type(name, mtype)
|
||
if found is not None:
|
||
id_map[mid] = int(found)
|
||
if open_type:
|
||
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():
|
||
conn = op.get_bind()
|
||
ids = [m[0] for m in MENUS]
|
||
|
||
# 先清角色-权限关联,避免外键/脏数据残留
|
||
conn.execute(
|
||
sa.text("DELETE FROM admin_role_power WHERE power_id IN :ids").bindparams(
|
||
sa.bindparam("ids", expanding=True)
|
||
),
|
||
{"ids": ids},
|
||
)
|
||
conn.execute(
|
||
sa.text("DELETE FROM admin_power WHERE id IN :ids").bindparams(
|
||
sa.bindparam("ids", expanding=True)
|
||
),
|
||
{"ids": ids},
|
||
)
|