"""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}, )