using System.Data; using System.Text; using FreeSql; using F9WebAiTest.Application.Interfaces.SnowflakeID; using F9WebAiTest.Domain.Common; using F9WebAiTest.Domain.Entities; using F9WebAiTest.Domain.Entities.Base; using F9WebAiTest.Domain.Entities.Notice; using F9WebAiTest.Domain.Enums.SYS.RBAC; using SysMenu = F9WebAiTest.Domain.Entities.SYS.RBAC.Menu; using SysRole = F9WebAiTest.Domain.Entities.SYS.RBAC.Role; using SysPermission = F9WebAiTest.Domain.Entities.SYS.RBAC.Permission; using SysRolePermission = F9WebAiTest.Domain.Entities.SYS.RBAC.RolePermission; using SysReport = F9WebAiTest.Domain.Entities.SYS.Reports.Report; using SysReportFile = F9WebAiTest.Domain.Entities.SYS.Reports.ReportFile; namespace F9WebAiTest.Infrastructure.Data; public static class DbSeeder { public static void Seed(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { // 1. 建表(CodeFirst SyncStructure,增量同步,兼容旧库) SyncStructure(fsql); // 2. 默认角色 if (!fsql.Select().Any()) { fsql.Insert(new Role { Name = "超级管理员", Code = "admin", Description = "拥有所有权限", Sort = 1 }).ExecuteAffrows(); fsql.Insert(new Role { Name = "普通用户", Code = "user", Description = "普通员工", Sort = 2 }).ExecuteAffrows(); } // 3. 默认菜单(系统管理) if (!fsql.Select().Any()) { var systemId = InsertAndGetId(fsql, new Menu { Name = "系统管理", Type = "directory", Path = "/system", Icon = "Setting", Sort = 1 }); var deptId = InsertAndGetId(fsql, new Menu { Name = "部门管理", Type = "menu", Path = "/system/department", Component = "system/department/index", Icon = "OfficeBuilding", Permission = "system:department:list", Sort = 1, ParentId = systemId }); var empId = InsertAndGetId(fsql, new Menu { Name = "职工管理", Type = "menu", Path = "/system/employee", Component = "system/employee/index", Icon = "User", Permission = "system:employee:list", Sort = 2, ParentId = systemId }); var roleId = InsertAndGetId(fsql, new Menu { Name = "角色管理", Type = "menu", Path = "/system/role", Component = "system/role/index", Icon = "UserFilled", Permission = "system:role:list", Sort = 3, ParentId = systemId }); var menuId = InsertAndGetId(fsql, new Menu { Name = "菜单管理", Type = "menu", Path = "/system/menu", Component = "system/menu/index", Icon = "Menu", Permission = "system:menu:list", Sort = 4, ParentId = systemId }); var logId = InsertAndGetId(fsql, new Menu { Name = "操作日志", Type = "menu", Path = "/system/log", Component = "system/log/index", Icon = "Document", Permission = "system:log:list", Sort = 5, ParentId = systemId }); fsql.Insert(new Menu[] { new Menu { Name = "部门操作", Type = "button", Permission = "system:department:btn", Sort = 1, ParentId = deptId }, new Menu { Name = "职工操作", Type = "button", Permission = "system:employee:btn", Sort = 1, ParentId = empId }, new Menu { Name = "角色操作", Type = "button", Permission = "system:role:btn", Sort = 1, ParentId = roleId }, new Menu { Name = "菜单操作", Type = "button", Permission = "system:menu:btn", Sort = 1, ParentId = menuId }, }).ExecuteAffrows(); } // 4. 考勤菜单(增量) SeedAttendanceMenus(fsql); // 5. 业务基础数据(部门树 + 机组车台 + 庄口 + 规格 + 定级指标 + 菜单) BusinessSeedData.Seed(fsql); // 6. 通知:消息类型种子 + 菜单 SeedNotificationData(fsql, snowflake); // 6.5 数据迁移:旧 RBAC 表 → 新 DDD RBAC 表(先迁移,确保 admin 角色/用户已在新表) MigrateRbacData(fsql); // 6.6 F9 风格 RBAC 菜单(用户/角色/菜单/部门管理,写入新 sys_rbac_menus 表) SeedSysRbacMenus(fsql, snowflake); // 6.65 基础数据(工种 + 注册表配置 + 原料庄口 + 物资 + 往来单位 + 职工,F9 导出数据幂等导入) WorkTypeSeedData.Seed(fsql, snowflake); RegistrySeedData.Seed(fsql); YuanliaoZhuangkouSeedData.Seed(fsql); WuziSeedData.Seed(fsql); GongyingshangSeedData.Seed(fsql); EmployeeSeedData.Seed(fsql); // 6.655 生产模块基础数据(机组车台 + 车间班次,F9 导出数据幂等导入) ProdSeedData.Seed(fsql, snowflake); // 6.66 基础数据菜单(工种管理、账期管理、注册表、原料庄口、物资目录、往来单位) SeedWorkTypeMenus(fsql, snowflake); SeedZhangqiMenus(fsql, snowflake); SeedRegistryMenus(fsql, snowflake); SeedYuanliaoZhuangkouMenus(fsql, snowflake); SeedWuziMenus(fsql, snowflake); SeedGongyingshangMenus(fsql, snowflake); // 6.67 职工管理(一级菜单,与"基础数据"平级) SeedEmployeeTopMenu(fsql, snowflake); // 6.68 生产模块(按工序拆分:生产-前缫 / 生产-后缫) SeedProdMenus(fsql, snowflake); // 6.69 修正前缫部门的职责类型(车间班次菜单随"生产-前缫"一起处理) FixQianxianDutyType(fsql); // 6.695 质量模块(工艺执行书 / 样茧试缫) SeedQcMenus(fsql, snowflake); // 6.71 报表中心菜单(/report 目录 + 报表中心/报表管理两个页面) SeedReportMenus(fsql, snowflake); // 6.72 旧版内嵌模板(sys_reports.template_data)迁移为独立模板实例 ReportFile MigrateReportTemplateToFile(fsql, snowflake); // 6.72 报表中心示例报表(职工花名册) SeedSampleReport(fsql, snowflake); // 6.73 落丝记录报表(生产-前缫,DsName=RepDt_Prod_Lsjl) SeedLsjlReport(fsql, snowflake); // 6.7 为菜单创建权限单元(Permission),并修正旧菜单权限关联(统一权限模型) FixMenuPermissions(fsql, snowflake); // 7. 管理员账号(幂等) var admin = fsql.Select().Where(e => e.Username == "admin").First(); if (admin == null) { var dept = fsql.Select().Where(d => d.Name == "主干运维").First() ?? fsql.Select().First(); var adminId = InsertAndGetId(fsql, new Employee { Username = "admin", Password = BCrypt.Net.BCrypt.HashPassword("123456"), RealName = "系统管理员", Nickname = "管理员", DepartmentId = dept.Id, Email = "admin@company.com", Phone = "13800000000", Status = 1 }); admin = fsql.Select().Where(e => e.Id == adminId).First(); } // 8. admin 关联 admin 角色 var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); if (!fsql.Select().Any(er => er.EmployeeId == admin.Id && er.RoleId == adminRole.Id)) { fsql.Insert(new EmployeeRole { EmployeeId = admin.Id, RoleId = adminRole.Id }).ExecuteAffrows(); } // 9. admin 角色分配所有菜单(幂等补充) var allMenus = fsql.Select().ToList(); foreach (var m in allMenus) { if (!fsql.Select().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id)) { fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows(); } } } /// CodeFirst 建表(新表建表,旧表同步失败则跳过)+ 旧库加列兜底 private static void SyncStructure(IFreeSql fsql) { // 扫描 BaseEntity(旧)与 Entity(新 DDD 基类)两类实体的具体子类建表 var entityTypes = typeof(BaseEntity).Assembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && (t.IsSubclassOf(typeof(BaseEntity)) || t.IsSubclassOf(typeof(Entity)))); foreach (var t in entityTypes) { try { fsql.CodeFirst.SyncStructure(t); } catch (Exception ex) { // 旧表同步失败(如唯一索引与历史数据冲突),跳过,保持旧库兼容 Console.WriteLine($"[DbSeeder] 表 {t.Name} 结构同步跳过:{ex.Message}"); } } // 旧库加列兜底(SyncStructure 不会为已有表加列) EnsureColumn(fsql, "LeaveRequests", "CurrentOrderNo", "INTEGER NOT NULL DEFAULT 1"); EnsureColumn(fsql, "NotificationTypes", "Category", "INTEGER NOT NULL DEFAULT 0"); EnsureColumn(fsql, "NotificationTypes", "Icon", "TEXT NOT NULL DEFAULT ''"); EnsureColumn(fsql, "NotificationTypes", "RouteTemplate", "TEXT NOT NULL DEFAULT ''"); EnsureColumn(fsql, "NotificationMessages", "Category", "INTEGER NOT NULL DEFAULT 0"); EnsureColumn(fsql, "NotificationMessages", "RoutePath", "TEXT NOT NULL DEFAULT ''"); EnsureColumn(fsql, "sys_reports", "report_file_id", "INTEGER NOT NULL DEFAULT 0"); } /// 为已有表补加列(列已存在则忽略) private static void EnsureColumn(IFreeSql fsql, string table, string column, string definition) { try { fsql.Ado.ExecuteNonQuery($"ALTER TABLE \"{table}\" ADD COLUMN \"{column}\" {definition};"); } catch { // 列已存在,忽略 } } /// 插入并返回自增主键 ID(SQLite 用 last_insert_rowid 可靠获取) public static long InsertAndGetId(IFreeSql fsql, T entity) where T : BaseEntity { fsql.Insert(entity).ExecuteAffrows(); return Convert.ToInt64(fsql.Ado.ExecuteScalar("SELECT last_insert_rowid();")); } /// 通知种子:消息类型 + 消息中心菜单 private static void SeedNotificationData(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { // 消息类型:发现特野 if (!fsql.Select().Any(t => t.Code == "TeyeFound")) { fsql.Insert(new NotificationType { Code = "TeyeFound", Name = "发现特野", Description = "纤度检验发现特野时自动推送提醒", RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 1, Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/houxian/xdjy" }).ExecuteAffrows(); } // 消息类型:发现特大糙 if (!fsql.Select().Any(t => t.Code == "TdcFound")) { fsql.Insert(new NotificationType { Code = "TdcFound", Name = "发现特大糙", Description = "黑板检验发现特大糙时自动推送提醒", RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 2, Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/houxian/hbjy" }).ExecuteAffrows(); } // 消息类型:请假审批(订阅权限用独立码,避免与业务按钮权限冲突) if (!fsql.Select().Any(t => t.Code == "LeaveApproval")) { fsql.Insert(new NotificationType { Code = "LeaveApproval", Name = "请假审批", Description = "员工提交请假申请后,推送待办给审批人", RequiredPermission = "notification:subscribe:leaveapproval", IsActive = true, CreatedBy = 0, Sort = 3, Category = NotificationCategory.Todo, Icon = "Checked", RouteTemplate = "/attendance/approve?leaveId={businessId}" }).ExecuteAffrows(); } // 消息类型:请假结果通知 if (!fsql.Select().Any(t => t.Code == "LeaveResult")) { fsql.Insert(new NotificationType { Code = "LeaveResult", Name = "请假结果通知", Description = "请假审批完成后通知申请人结果", RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 4, Category = NotificationCategory.Notification, Icon = "Message", RouteTemplate = "/attendance/my" }).ExecuteAffrows(); } // 消息类型:广播通知(有权限的用户主动发送的广播,推送给所有订阅者) if (!fsql.Select().Any(t => t.Code == "Broadcast")) { fsql.Insert(new NotificationType { Code = "Broadcast", Name = "广播通知", Description = "用户发送的广播通知,推送给所有订阅该类型的用户", RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 5, Category = NotificationCategory.Announcement, Icon = "Bell", RouteTemplate = "" }).ExecuteAffrows(); } // 广播发送权限单元(ResourceType=操作,Code=notification:broadcast,admin 自动拥有,普通用户可在角色管理分配) var broadcastPerm = fsql.Select() .Where(p => p.Code == "notification:broadcast").First(); if (broadcastPerm == null) { var perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Enable, Resource_Type_Enum.操作, "broadcast", 0); perm.SetCode("notification:broadcast"); fsql.Insert(perm).ExecuteAffrows(); } // 菜单:消息中心 var notiDirId = fsql.Select().Where(m => m.Path == "/notification").First()?.Id ?? 0; if (notiDirId == 0) { notiDirId = InsertAndGetId(fsql, new Menu { Name = "消息中心", Type = "directory", Path = "/notification", Icon = "Bell", Sort = 5 }); } AddNotificationMenu(fsql, notiDirId, "我的消息", "/notification/messages", "notification/messages/index", "Message", "notification:messages:list", 1); AddNotificationMenu(fsql, notiDirId, "消息订阅", "/notification/subscribe", "notification/subscribe/index", "BellFilled", "notification:subscribe:list", 2); AddNotificationMenu(fsql, notiDirId, "消息类型管理", "/notification/type", "notification/type/index", "SetUp", "notification:type:list", 3); // 清理历史遗留:发现特野/发现特大糙 改为「所有人可订阅」 foreach (var (code, oldPerm) in new[] { ("TeyeFound", "notification:subscribe:foundteye"), ("TdcFound", "notification:subscribe:foundtedacao") }) { var t = fsql.Select().Where(x => x.Code == code).First(); if (t == null) continue; // 1. RequiredPermission 改为空(所有人可订阅) if (!string.IsNullOrEmpty(t.RequiredPermission)) { t.RequiredPermission = ""; fsql.Update().SetSource(t).ExecuteAffrows(); } // 2. 删除订阅权限单元及其角色关联(新 Permission 表) var perm = fsql.Select() .Where(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == t.Id).First(); if (perm != null) { fsql.Delete().Where(rp => rp.PermissionId == perm.Id).ExecuteAffrows(); fsql.Delete().Where(p => p.Id == perm.Id).ExecuteAffrows(); } // 3. 删除历史遗留的旧"订阅按钮"菜单及其角色关联(新表 sys_rbac_menus + 旧表 Menus) fsql.Ado.ExecuteNonQuery($@" DELETE FROM sys_rbac_role_permissions WHERE permission_id IN (SELECT id FROM sys_rbac_menus WHERE permission = '{oldPerm}'); DELETE FROM sys_rbac_menus WHERE permission = '{oldPerm}'; DELETE FROM RoleMenus WHERE MenuId IN (SELECT Id FROM Menus WHERE Permission = '{oldPerm}'); DELETE FROM Menus WHERE Permission = '{oldPerm}';"); } // 清理所有旧的"订阅按钮"菜单(已被 Permission 表的消息订阅权限替代,避免与消息订阅权限重复冗余) fsql.Ado.ExecuteNonQuery(@" DELETE FROM sys_rbac_role_permissions WHERE permission_id IN (SELECT id FROM sys_rbac_menus WHERE name LIKE '订阅:%' AND menu_type = 0); DELETE FROM sys_rbac_menus WHERE name LIKE '订阅:%' AND menu_type = 0; DELETE FROM RoleMenus WHERE MenuId IN (SELECT Id FROM Menus WHERE Name LIKE '订阅:%' AND Type = 'button'); DELETE FROM Menus WHERE Name LIKE '订阅:%' AND Type = 'button';"); // 清理 RequiredPermission 为空的消息类型的订阅权限(历史遗留,所有人可订阅无需权限单元) foreach (var t in fsql.Select().Where(x => x.RequiredPermission == "").ToList()) { var perm = fsql.Select() .Where(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == t.Id).First(); if (perm != null) { fsql.Delete().Where(rp => rp.PermissionId == perm.Id).ExecuteAffrows(); fsql.Delete().Where(p => p.Id == perm.Id).ExecuteAffrows(); } } // 同步订阅权限到 RBAC 权限单元(ResourceType=消息订阅,Code=RequiredPermission) foreach (var t in fsql.Select().ToList()) { if (string.IsNullOrEmpty(t.RequiredPermission)) continue; var existPerm = fsql.Select() .Where(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == t.Id).First(); if (existPerm == null) { var perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Visble, Resource_Type_Enum.消息订阅, t.Code, t.Id); perm.SetCode(t.RequiredPermission); fsql.Insert(perm).ExecuteAffrows(); } else if (existPerm.Code != t.RequiredPermission) { // 修正 Code 为业务权限码(覆盖旧的自动生成码) existPerm.SetCode(t.RequiredPermission); fsql.Update().SetSource(existPerm).ExecuteAffrows(); } } // admin 角色分配消息中心菜单权限 var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); if (adminRole != null) { var notiMenus = fsql.Select().Where(m => m.Path.StartsWith("/notification")).ToList(); foreach (var m in notiMenus) { if (!fsql.Select().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id)) fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows(); } } } private static void AddNotificationMenu(IFreeSql fsql, long parentId, string name, string path, string component, string icon, string permission, int sort) { if (!fsql.Select().Any(m => m.Path == path)) { fsql.Insert(new Menu { ParentId = parentId, Name = name, Type = "menu", Path = path, Component = component, Icon = icon, Permission = permission, Sort = sort }).ExecuteAffrows(); } } private static void SeedAttendanceMenus(IFreeSql fsql) { var attDirId = fsql.Select().Where(m => m.Path == "/attendance").First()?.Id ?? 0; if (attDirId == 0) { attDirId = InsertAndGetId(fsql, new Menu { Name = "考勤管理", Type = "directory", Path = "/attendance", Icon = "Clock", Sort = 2 }); } void AddMenu(string name, string path, string component, string icon, string permission, int sort) { if (!fsql.Select().Any(m => m.Path == path)) { fsql.Insert(new Menu { Name = name, Type = "menu", Path = path, Component = component, Icon = icon, Permission = permission, Sort = sort, ParentId = attDirId }).ExecuteAffrows(); } } AddMenu("考勤打卡", "/attendance/punch", "attendance/punch/index", "AlarmClock", "attendance:punch:list", 1); AddMenu("我的考勤", "/attendance/my", "attendance/my/index", "Calendar", "attendance:my:list", 2); AddMenu("请假申请", "/attendance/leave", "attendance/leave/index", "Suitcase", "attendance:leave:list", 3); AddMenu("考勤记录", "/attendance/records", "attendance/records/index", "List", "attendance:records:list", 4); AddMenu("请假审批", "/attendance/approve", "attendance/approve/index", "Checked", "attendance:approve:list", 5); void AddButton(string menuPath, string name, string permission) { var menu = fsql.Select().Where(m => m.Path == menuPath).First(); if (menu != null && !fsql.Select().Any(m => m.Permission == permission)) { fsql.Insert(new Menu { Name = name, Type = "button", Permission = permission, Sort = 1, ParentId = menu.Id }).ExecuteAffrows(); } } AddButton("/attendance/punch", "打卡操作", "attendance:punch:btn"); AddButton("/attendance/leave", "请假操作", "attendance:leave:btn"); AddButton("/attendance/approve", "审批操作", "attendance:approve:btn"); var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); if (adminRole != null) { var attMenus = fsql.Select().Where(m => m.Path.StartsWith("/attendance") || m.Permission.StartsWith("attendance:")).ToList(); foreach (var m in attMenus) { if (!fsql.Select().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id)) fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows(); } } } /// 数据迁移:旧 RBAC 表 → 新 DDD RBAC 表(幂等,仅迁移尚不存在的记录) private static void MigrateRbacData(IFreeSql fsql) { try { // 1. 部门:Departments → sys_departments fsql.Ado.ExecuteNonQuery(@" INSERT INTO sys_departments (id, code, name, parent_id, manage_type, duty_type, description, is_active, sort_order, created_by, created_at, update_by, updated_at, is_deleted) SELECT Id, COALESCE(Code, ''), Name, COALESCE(ParentId, 0), 10, 5, '', Status, Sort, '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted FROM Departments WHERE Id NOT IN (SELECT id FROM sys_departments);"); // 2. 角色:Roles → sys_rbac_roles fsql.Ado.ExecuteNonQuery(@" INSERT INTO sys_rbac_roles (id, name, code, sort_order, description, is_active, created_by, created_at, update_by, updated_at, is_deleted) SELECT Id, Name, Code, Sort, COALESCE(Description, ''), Status, '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted FROM Roles WHERE Id NOT IN (SELECT id FROM sys_rbac_roles);"); // 3. 菜单:Menus → sys_rbac_menus fsql.Ado.ExecuteNonQuery(@" INSERT INTO sys_rbac_menus (id, icon_svg, code, name, menu_type, assembly_name, type_full_name, parent_id, level, is_active, sort_order, path, component, permission, created_by, created_at, update_by, updated_at, is_deleted) SELECT Id, COALESCE(Icon, ''), '', Name, CASE WHEN Type = 'directory' THEN 1 WHEN Type = 'menu' THEN 2 ELSE 0 END, '', '', COALESCE(ParentId, 0), 1, Status, Sort, COALESCE(Path, ''), COALESCE(Component, ''), COALESCE(Permission, ''), '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted FROM Menus WHERE Id NOT IN (SELECT id FROM sys_rbac_menus);"); // 4. 用户:Employees → sys_rbac_users fsql.Ado.ExecuteNonQuery(@" INSERT INTO sys_rbac_users (id, department_id, user_name, phone_number, login_name, password_hash, is_active, sort_order, lastlogin_at, nickname, avatar, email, gender, created_by, created_at, update_by, updated_at, is_deleted) SELECT Id, DepartmentId, RealName, COALESCE(Phone, ''), Username, Password, Status, 0, NULL, COALESCE(Nickname, ''), COALESCE(Avatar, ''), COALESCE(Email, ''), COALESCE(Gender, '男'), '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted FROM Employees WHERE Id NOT IN (SELECT id FROM sys_rbac_users);"); // 5. 用户角色:EmployeeRoles → sys_rbac_user_roles fsql.Ado.ExecuteNonQuery(@" INSERT INTO sys_rbac_user_roles (id, user_id, role_id, created_by, created_at, update_by, updated_at, is_deleted) SELECT Id, EmployeeId, RoleId, '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted FROM EmployeeRoles WHERE Id NOT IN (SELECT id FROM sys_rbac_user_roles);"); // 6. 角色权限:RoleMenus → sys_rbac_role_permissions fsql.Ado.ExecuteNonQuery(@" INSERT INTO sys_rbac_role_permissions (id, role_id, permission_id, created_by, created_at, update_by, updated_at, is_deleted) SELECT Id, RoleId, MenuId, '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted FROM RoleMenus WHERE Id NOT IN (SELECT id FROM sys_rbac_role_permissions);"); Console.WriteLine("[DbSeeder] RBAC 数据迁移完成"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] RBAC 数据迁移跳过:{ex.Message}"); } } /// F9 风格 RBAC 前端菜单种子(写入新 sys_rbac_menus 表,驱动前端导航) private static void SeedSysRbacMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { // 删除旧"系统管理"菜单(/system/*,对接已下线的旧接口),仅保留新的 /sys 管理菜单 fsql.Ado.ExecuteNonQuery("DELETE FROM sys_rbac_role_permissions WHERE permission_id IN (SELECT id FROM sys_rbac_menus WHERE path LIKE '/system%')"); fsql.Ado.ExecuteNonQuery("DELETE FROM sys_rbac_menus WHERE path LIKE '/system%'"); // sys 目录 long sysDirId; var sysDir = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_menus WHERE path = '/sys'"); if (sysDir.Rows.Count == 0) { sysDirId = snowflake.CreateId(); fsql.Ado.ExecuteNonQuery($"INSERT INTO sys_rbac_menus (id, icon_svg, name, menu_type, parent_id, level, is_active, sort_order, path, component, permission, created_at, is_deleted) VALUES ({sysDirId}, 'UserFilled', '系统管理(RBAC)', 1, 0, 1, 0, 6, '/sys', '', '', datetime('now'), 0)"); } else { sysDirId = Convert.ToInt64(sysDir.Rows[0]["id"]); } void AddMenu(string name, string path, string component, string icon, string permission, int sort) { var exist = fsql.Ado.ExecuteDataTable($"SELECT id FROM sys_rbac_menus WHERE path = '{path}'"); if (exist.Rows.Count > 0) return; var id = snowflake.CreateId(); fsql.Ado.ExecuteNonQuery($"INSERT INTO sys_rbac_menus (id, icon_svg, name, menu_type, parent_id, level, is_active, sort_order, path, component, permission, created_at, is_deleted) VALUES ({id}, '{icon}', '{name}', 2, {sysDirId}, 2, 0, {sort}, '{path}', '{component}', '{permission}', datetime('now'), 0)"); } AddMenu("用户管理", "/sys/user", "sys/user/index", "User", "sys:user:list", 1); AddMenu("角色管理", "/sys/role", "sys/role/index", "UserFilled", "sys:role:list", 2); AddMenu("菜单管理", "/sys/menu", "sys/menu/index", "Menu", "sys:menu:list", 3); AddMenu("部门管理", "/sys/department", "sys/department/index", "OfficeBuilding", "sys:department:list", 4); // admin 角色分配 sys 菜单权限 var adminRole = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_roles WHERE code = 'admin'"); if (adminRole.Rows.Count > 0) { var roleId = Convert.ToInt64(adminRole.Rows[0]["id"]); var menus = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_menus WHERE path LIKE '/sys%'"); foreach (DataRow row in menus.Rows) { var menuId = Convert.ToInt64(row["id"]); var exist = fsql.Ado.ExecuteDataTable($"SELECT id FROM sys_rbac_role_permissions WHERE role_id = {roleId} AND permission_id = {menuId}"); if (exist.Rows.Count == 0) { fsql.Ado.ExecuteNonQuery($"INSERT INTO sys_rbac_role_permissions (id, role_id, permission_id, created_at, is_deleted) VALUES ({snowflake.CreateId()}, {roleId}, {menuId})"); } } } } catch (Exception ex) { Console.WriteLine($"[DbSeeder] RBAC 菜单种子跳过:{ex}"); } } /// 工种管理菜单(挂在"基础数据"目录下) private static void SeedWorkTypeMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { EnsureBaseDataMenu(fsql, snowflake, "/base/worktype", "工种管理", "base/worktype/index", "base:worktype:list", "Postcard", 5); Console.WriteLine("[DbSeeder] 工种管理菜单已就绪"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 工种菜单种子跳过:{ex}"); } } /// 账期管理菜单(挂在"基础数据"目录下) private static void SeedZhangqiMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { EnsureBaseDataMenu(fsql, snowflake, "/base/zhangqi", "账期管理", "base/zhangqi/index", "base:zhangqi:list", "Tickets", 6); Console.WriteLine("[DbSeeder] 账期管理菜单已就绪"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 账期菜单种子跳过:{ex}"); } } /// 注册表菜单(挂在"基础数据"目录下) private static void SeedRegistryMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { EnsureBaseDataMenu(fsql, snowflake, "/base/registry", "注册表", "base/registry/index", "base:registry:list", "Setting", 7); Console.WriteLine("[DbSeeder] 注册表菜单已就绪"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 注册表菜单种子跳过:{ex}"); } } /// 原料庄口菜单(挂在"基础数据"目录下) private static void SeedYuanliaoZhuangkouMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { EnsureBaseDataMenu(fsql, snowflake, "/base/yuanliaozhuangkou", "原料庄口", "base/yuanliaozhuangkou/index", "base:yuanliaozhuangkou:list", "Box", 8); Console.WriteLine("[DbSeeder] 原料庄口菜单已就绪"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 原料庄口菜单种子跳过:{ex}"); } } /// 物资目录菜单(挂在"基础数据"目录下) private static void SeedWuziMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { EnsureBaseDataMenu(fsql, snowflake, "/base/wuzimulu", "物资目录", "base/wuzimulu/index", "base:wuzimulu:list", "Goods", 9); Console.WriteLine("[DbSeeder] 物资目录菜单已就绪"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 物资目录菜单种子跳过:{ex}"); } } /// 往来单位菜单(挂在"基础数据"目录下) private static void SeedGongyingshangMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { EnsureBaseDataMenu(fsql, snowflake, "/base/gongyingshang", "往来单位", "base/gongyingshang/index", "base:gongyingshang:list", "Connection", 10); Console.WriteLine("[DbSeeder] 往来单位菜单已就绪"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 往来单位菜单种子跳过:{ex}"); } } /// /// 职工管理菜单:独立的一级菜单(原作为"基础数据"的子菜单)。 /// 会清理历史遗留的 /base/employee 子菜单,并规范一级菜单排序,避免同一页面出现两个入口。 /// private static void SeedEmployeeTopMenu(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { const string menuPath = "/employee"; const string menuIcon = "Avatar"; // 1) 清理历史:曾是"基础数据"下的子菜单 var legacy = fsql.Select().Where(m => m.Path == "/base/employee").First(); if (legacy != null) { fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {legacy.Id}"); fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {legacy.Id}"); Console.WriteLine("[DbSeeder] 已清理历史子菜单 /base/employee"); } // 2) 创建/校正一级菜单 var menu = fsql.Select().Where(m => m.Path == menuPath).First(); if (menu == null) { menu = new SysMenu(snowflake.CreateId(), "职工管理", menuIcon, MenuType_Enum.页面, null, null, 0, 4, 0); menu.SetWebInfo(menuPath, "base/employee/index", "base:employee:list"); fsql.Insert(menu).ExecuteAffrows(); } else if (!menu.IsActive || menu.IconSvg != menuIcon || menu.ParentId != 0) { fsql.Update() .Set(x => x.IconSvg, menuIcon) .Set(x => x.IsActive, true) .Set(x => x.ParentId, 0) .Set(x => x.Level, 1) .Where(x => x.Id == menu.Id) .ExecuteAffrows(); } // 3) 规范一级菜单顺序:考勤管理 / 基础数据 / 职工管理 / 生产管理 / 消息中心 / 系统管理 foreach (var (path, sort) in new (string Path, int Sort)[] { ("/attendance", 2), ("/base", 3), ("/employee", 4), ("/prod", 5), ("/notification", 6), ("/sys", 7) }) { fsql.Update() .Set(x => x.SortOrder, sort) .Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort) .ExecuteAffrows(); } // 4) admin 角色授权(后续 FixMenuPermissions 会把 permission_id 修正为权限单元 Id) var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); if (adminRole != null) { var any = fsql.Select() .Where(x => x.RoleId == adminRole.Id && x.PermissionId == menu.Id) .First(); if (any == null) { fsql.Insert(new SysRolePermission(snowflake.CreateId(), adminRole.Id, menu.Id)).ExecuteAffrows(); } } Console.WriteLine("[DbSeeder] 职工管理一级菜单已就绪"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 职工管理菜单种子跳过:{ex}"); } } /// /// 生产模块菜单:按缫丝工序拆分为「生产-前缫」(落丝记录)与「生产-后缫」(纤度检验、黑板检验)两个一级菜单。 /// 会清理旧的「生产管理」(/prod) 目录及其子菜单;页面组件与权限标识保持不变。 /// private static void SeedProdMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { // 1) 清理旧「生产管理」结构(精确匹配历史路径) foreach (var legacyPath in new[] { "/prod", "/prod/lsjl", "/prod/xdjy", "/prod/hbjy" }) { var legacy = fsql.Select().Where(m => m.Path == legacyPath).First(); if (legacy == null) continue; fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {legacy.Id}"); fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {legacy.Id}"); Console.WriteLine($"[DbSeeder] 已清理旧生产菜单 {legacyPath}"); } // 2) 两个一级目录 + 各自页面菜单 // 车间班次原先挂在"基础数据"目录下,现归入"生产-前缫" var legacyChejianbanci = fsql.Select().Where(m => m.Path == "/base/chejianbanci").First(); if (legacyChejianbanci != null) { fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {legacyChejianbanci.Id}"); fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {legacyChejianbanci.Id}"); Console.WriteLine("[DbSeeder] 已清理旧菜单 /base/chejianbanci(车间班次移入生产-前缫)"); } // 一并清理旧菜单遗留的权限单元(权限标识已从 base:chejianbanci:list 改为 prod:chejianbanci:list) var legacyPerms = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_permissions WHERE code = 'base:chejianbanci:list'"); foreach (DataRow permRow in legacyPerms.Rows) { var permId = Convert.ToInt64(permRow["id"]); fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {permId}"); fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_permissions WHERE id = {permId}"); Console.WriteLine("[DbSeeder] 已清理旧权限单元 base:chejianbanci:list"); } // 机组车台原先挂在"基础数据"目录下,按 F9 归属改到"生产-前缫" var legacyJizuChetai = fsql.Select().Where(m => m.Path == "/base/jizuchetai").First(); if (legacyJizuChetai != null) { fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {legacyJizuChetai.Id}"); fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {legacyJizuChetai.Id}"); Console.WriteLine("[DbSeeder] 已清理旧菜单 /base/jizuchetai(机组车台移入生产-前缫)"); } var legacyJzctPerms = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_permissions WHERE code = 'base:jizuchetai:list'"); foreach (DataRow permRow in legacyJzctPerms.Rows) { var permId = Convert.ToInt64(permRow["id"]); fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {permId}"); fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_permissions WHERE id = {permId}"); Console.WriteLine("[DbSeeder] 已清理旧权限单元 base:jizuchetai:list"); } EnsureTopMenuWithChildren(fsql, snowflake, "/prod/qianxian", "生产-前缫", "Cpu", 5, new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] { ("机组车台", "/prod/qianxian/jizuchetai", "base/jizuchetai/index", "prod:jizuchetai:list", "SetUp", 1), ("落丝记录", "/prod/qianxian/lsjl", "prod/lsjl/index", "prod:lsjl:list", "Document", 2), ("车间班次", "/prod/qianxian/chejianbanci", "base/chejianbanci/index", "prod:chejianbanci:list", "Timer", 3), ("看台排班", "/prod/qianxian/ktpb", "prod/ktpb/index", "prod:ktpb:list", "Grid", 4) }); // 子菜单顺序归一(EnsureTopMenuWithChildren 只补新菜单,不调整已存在菜单的排序) foreach (var (path, sort) in new (string Path, int Sort)[] { ("/prod/qianxian/jizuchetai", 1), ("/prod/qianxian/lsjl", 2), ("/prod/qianxian/chejianbanci", 3), ("/prod/qianxian/ktpb", 4) }) { fsql.Update() .Set(x => x.SortOrder, sort) .Where(x => x.Path == path && x.SortOrder != sort) .ExecuteAffrows(); } EnsureTopMenuWithChildren(fsql, snowflake, "/prod/houxian", "生产-后缫", "DataLine", 6, new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] { ("纤度检验", "/prod/houxian/xdjy", "prod/xdjy/index", "prod:xdjy:list", "EditPen", 1), ("黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "prod:hbjy:list", "Notebook", 2) }); // 3) 规范一级菜单顺序 foreach (var (path, sort) in new (string Path, int Sort)[] { ("/attendance", 2), ("/base", 3), ("/employee", 4), ("/prod/qianxian", 5), ("/prod/houxian", 6), ("/notification", 7), ("/sys", 8) }) { fsql.Update() .Set(x => x.SortOrder, sort) .Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort) .ExecuteAffrows(); } // 4) 消息跳转路径同步到新路由 fsql.Ado.ExecuteNonQuery("UPDATE NotificationTypes SET RouteTemplate = '/prod/houxian/xdjy' WHERE Code = 'TeyeFound'"); fsql.Ado.ExecuteNonQuery("UPDATE NotificationTypes SET RouteTemplate = '/prod/houxian/hbjy' WHERE Code = 'TdcFound'"); Console.WriteLine("[DbSeeder] 生产模块菜单已就绪(生产-前缫 / 生产-后缫)"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 生产菜单种子跳过:{ex}"); } } /// /// 质量模块菜单:一级目录「质量」+ 工艺执行书 / 样茧试缫 两个页面。 /// 模块落在「生产-后缫」之后、消息中心之前。 /// private static void SeedQcMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { EnsureTopMenuWithChildren(fsql, snowflake, "/qc", "质量", "Medal", 7, new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] { ("工艺执行书", "/qc/gyzxs", "qc/gyzxs/index", "qc:gyzxs:list", "Document", 1), ("样茧试缫", "/qc/yjss", "qc/yjss/index", "qc:yjss:list", "DataAnalysis", 2) }); foreach (var (path, sort) in new (string Path, int Sort)[] { ("/attendance", 2), ("/base", 3), ("/employee", 4), ("/prod/qianxian", 5), ("/prod/houxian", 6), ("/qc", 7), ("/notification", 8), ("/sys", 9), ("/report", 10) }) { fsql.Update() .Set(x => x.SortOrder, sort) .Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort) .ExecuteAffrows(); } Console.WriteLine("[DbSeeder] 质量模块菜单已就绪(工艺执行书 / 样茧试缫)"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 质量模块菜单种子跳过:{ex.Message}"); } } /// /// 确保一个一级目录菜单及其下属页面菜单存在(含父级、图标、启用状态校正),并给 admin 角色授权。 /// 注意:图标名必须是 Element Plus 图标组件名。 /// private static void EnsureTopMenuWithChildren(IFreeSql fsql, ISnowflakeIdGenerator snowflake, string dirPath, string dirName, string dirIcon, int dirSort, (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] children) { var dir = fsql.Select().Where(m => m.Path == dirPath).First(); if (dir == null) { dir = new SysMenu(snowflake.CreateId(), dirName, dirIcon, MenuType_Enum.分组, null, null, 0, dirSort, 0); dir.SetWebInfo(dirPath, "", ""); fsql.Insert(dir).ExecuteAffrows(); } else if (!dir.IsActive || dir.IconSvg != dirIcon || dir.Name != dirName || dir.ParentId != 0) { fsql.Update() .Set(x => x.Name, dirName) .Set(x => x.IconSvg, dirIcon) .Set(x => x.IsActive, true) .Set(x => x.ParentId, 0) .Set(x => x.Level, 1) .Where(x => x.Id == dir.Id) .ExecuteAffrows(); } var adminRoleId = fsql.Select().Where(r => r.Code == "admin").First()?.Id ?? 0; EnsureMenuPermission(fsql, snowflake, adminRoleId, dir.Id); foreach (var (name, path, component, permission, icon, sort) in children) { var child = fsql.Select().Where(m => m.Path == path).First(); if (child == null) { child = new SysMenu(snowflake.CreateId(), name, icon, MenuType_Enum.页面, null, null, dir.Id, sort, 1); child.SetWebInfo(path, component, permission); fsql.Insert(child).ExecuteAffrows(); } else if (!child.IsActive || child.IconSvg != icon || child.ParentId != dir.Id) { fsql.Update() .Set(x => x.IconSvg, icon) .Set(x => x.IsActive, true) .Set(x => x.ParentId, dir.Id) .Set(x => x.Level, 2) .Where(x => x.Id == child.Id) .ExecuteAffrows(); } EnsureMenuPermission(fsql, snowflake, adminRoleId, child.Id); } } /// /// 修正前缫部门的职责类型:历史数据迁移时 duty_type 统一写成了"其他"(5), /// 而车间班次等模块需要按职责类型(前缫=25)筛选部门。 /// private static void FixQianxianDutyType(IFreeSql fsql) { try { var rows = fsql.Ado.ExecuteNonQuery( "UPDATE sys_departments SET duty_type = 25 WHERE is_deleted = 0 AND name LIKE '前缫%' AND duty_type <> 25"); if (rows > 0) Console.WriteLine($"[DbSeeder] 已修正 {rows} 个前缫部门的职责类型"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 部门职责类型修正跳过:{ex.Message}"); } } /// 确保指定菜单已授权给角色(后续 FixMenuPermissions 会把 permission_id 修正为权限单元 Id) private static void EnsureMenuPermission(IFreeSql fsql, ISnowflakeIdGenerator snowflake, long roleId, long menuId) { if (roleId <= 0 || menuId <= 0) return; var any = fsql.Select() .Where(x => x.RoleId == roleId && x.PermissionId == menuId) .First(); if (any == null) { fsql.Insert(new SysRolePermission(snowflake.CreateId(), roleId, menuId)).ExecuteAffrows(); } } /// /// 确保"基础数据"目录下的菜单存在(含启用状态与图标校正),并给 admin 角色授权。 /// 注意:图标名必须是 Element Plus 图标组件名,前端按图标名渲染菜单图标。 /// private static void EnsureBaseDataMenu(IFreeSql fsql, ISnowflakeIdGenerator snowflake, string path, string name, string component, string permission, string icon, int sort) { // 基础数据目录:不存在则创建,已存在则复用 var baseDir = fsql.Select().Where(m => m.Path == "/base").First(); if (baseDir == null) { baseDir = new SysMenu(snowflake.CreateId(), "基础数据", "Files", MenuType_Enum.分组, null, null, 0, 3, 0); baseDir.SetWebInfo("/base", "", ""); fsql.Insert(baseDir).ExecuteAffrows(); } var menu = fsql.Select().Where(m => m.Path == path).First(); if (menu == null) { menu = new SysMenu(snowflake.CreateId(), name, icon, MenuType_Enum.页面, null, null, baseDir.Id, sort, 1); menu.SetWebInfo(path, component, permission); fsql.Insert(menu).ExecuteAffrows(); } else if (!menu.IsActive || menu.IconSvg != icon) { // 兼容历史数据:补齐启用状态与图标 fsql.Update() .Set(x => x.IconSvg, icon) .Set(x => x.IsActive, true) .Where(x => x.Id == menu.Id) .ExecuteAffrows(); } // admin 角色授权(后续 FixMenuPermissions 会把 permission_id 修正为权限单元 Id) var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); if (adminRole != null) { var any = fsql.Select() .Where(x => x.RoleId == adminRole.Id && x.PermissionId == menu.Id) .First(); if (any == null) { fsql.Insert(new SysRolePermission(snowflake.CreateId(), adminRole.Id, menu.Id)).ExecuteAffrows(); } } } /// 为菜单创建权限单元(Permission),并修正旧菜单权限关联(RolePermission.PermissionId 从 Menu.Id 改为 Permission.Id) private static void FixMenuPermissions(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { foreach (var menu in fsql.Select().ToList()) { // 1. 查或创建菜单权限单元(ResourceType=菜单,ResourceId=Menu.Id) var perm = fsql.Select() .Where(p => p.ResourceType == Resource_Type_Enum.菜单 && p.ResourceId == menu.Id).First(); if (perm == null) { var resourceIdentity = string.IsNullOrEmpty(menu.Code) ? menu.Id.ToString() : menu.Code; perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Visble, Resource_Type_Enum.菜单, resourceIdentity, menu.Id); if (!string.IsNullOrEmpty(menu.Permission)) perm.SetCode(menu.Permission); fsql.Insert(perm).ExecuteAffrows(); } else if (!string.IsNullOrEmpty(menu.Permission) && perm.Code != menu.Permission) { perm.SetCode(menu.Permission); fsql.Update().SetSource(perm).ExecuteAffrows(); } // 2. 修正旧菜单权限关联(permission_id 旧值 = Menu.Id,改为 Permission.Id) fsql.Ado.ExecuteNonQuery($"UPDATE sys_rbac_role_permissions SET permission_id = {perm.Id} WHERE permission_id = {menu.Id};"); } // 3. 清理失效的菜单权限单元:ResourceId 不对应任何菜单 // (前端曾用 Number() 传雪花 Id,超出 2^53 后精度丢失,写入的 resource_id 在菜单表里查不到, // 表现为「勾选保存后重新打开仍是未勾选、该角色登录也看不到菜单」) var menuIds = fsql.Select().ToList().Select(m => m.Id).ToHashSet(); var orphanPerms = fsql.Select() .Where(p => p.ResourceType == Resource_Type_Enum.菜单).ToList() .Where(p => !menuIds.Contains(p.ResourceId)).ToList(); foreach (var bad in orphanPerms) { fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {bad.Id};"); fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_permissions WHERE id = {bad.Id};"); } if (orphanPerms.Count > 0) Console.WriteLine($"[DbSeeder] 已清理 {orphanPerms.Count} 条失效菜单权限单元(资源 Id 不存在)"); Console.WriteLine("[DbSeeder] 菜单权限单元同步完成"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 菜单权限单元同步跳过:{ex.Message}"); } } /// 报表中心菜单(/report 目录 + 报表中心 / 报表管理 两个页面菜单,admin 授权) private static void SeedReportMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { EnsureTopMenuWithChildren(fsql, snowflake, "/report", "报表中心", "DataAnalysis", 9, new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] { ("报表中心", "/report/center", "report/center/index", "report:center:list", "PieChart", 1), ("报表管理", "/report/manage", "report/manage/index", "report:manage:list", "Document", 2) }); Console.WriteLine("[DbSeeder] 报表中心菜单已就绪"); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 报表中心菜单种子跳过:{ex.Message}"); } } /// /// 旧版把模板二进制直接存在 sys_reports.template_data,现模板是独立实体 ReportFile(DesignContent 文本), /// Report 通过 ReportFileId 关联。启动时把旧数据迁移过去,迁移完成后清空旧字段。 /// private static void MigrateReportTemplateToFile(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { var legacy = fsql.Select() .Where(r => !r.IsDeleted && r.ReportFileId == 0 && r.TemplateData != null && r.TemplateData.Length > 0) .ToList(); foreach (var report in legacy) { var text = Encoding.UTF8.GetString(report.TemplateData!); if (text.Length > 0 && text[0] == '\uFEFF') text = text.Substring(1); var file = new SysReportFile(snowflake.CreateId(), report.Id, text); fsql.Insert(file).ExecuteAffrows(); report.BindReportFile(file.Id); report.ClearLegacyTemplate(); fsql.Update().SetSource(report).ExecuteAffrows(); Console.WriteLine($"[DbSeeder] 报表「{report.Name}」模板已迁移到 ReportFile#{file.Id}"); } } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 报表模板迁移跳过:{ex.Message}"); } } /// 报表中心示例报表:职工花名册(模板为独立 ReportFile 实例 + F9 ReportConfig 查询配置) private static void SeedSampleReport(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { const string code = "employee_directory"; const string name = "职工花名册"; const string sql = "SELECT login_name AS LoginName, user_name AS UserName, phone_number AS PhoneNumber, email AS Email FROM sys_rbac_users WHERE is_deleted = 0 ORDER BY id LIMIT 1000"; // F9 ReportConfig:DsName 指向后端查询提供者;Parms 定义动态查询控件 const string config = "{\"DsName\":\"RepDt_Sys_Users\",\"ViewerType\":0,\"Parms\":[" + "{\"Name\":\"q_dep\",\"Label\":\"部门\",\"Prompt\":\"含子部门,留空为全部\",\"IsRequired\":false," + "\"ControlType\":\"AntdUI.Select\",\"Ds_Static\":null,\"Ds_Service\":\"/sys/department\",\"DefaultValue\":\"\"}," + "{\"Name\":\"q_like\",\"Label\":\"关键字\",\"Prompt\":\"按工号 / 姓名模糊查询\",\"IsRequired\":false," + "\"ControlType\":\"AntdUI.Input\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}]}"; var exist = fsql.Select().Where(r => r.Code == code && !r.IsDeleted).First(); var templateJson = LoadEmbeddedTemplateJson("employee_directory.grf"); // 老版本种子生成的是手写 6.6.8.1 模板(设计器 6.8 无法打开),这里统一替换为 6.8.5.0 官方结构模板 bool needReplaceTemplate = false; if (exist != null) { var file = fsql.Select().Where(f => f.ReportId == exist.Id && !f.IsDeleted).First(); needReplaceTemplate = file == null || !file.DesignContent.Contains("6.8.5.0"); } if (exist == null) { var report = new SysReport(snowflake.CreateId(), code, name, "职工管理", "示例报表:按部门 / 关键字查询职工基本信息。", sql, "[]", 1, config); // 模板实例:ReportFile(对齐 F9 Report.ReportFileId) if (templateJson != null) { var file = new SysReportFile(snowflake.CreateId(), report.Id, templateJson); fsql.Insert(file).ExecuteAffrows(); report.BindReportFile(file.Id); } fsql.Insert(report).ExecuteAffrows(); // 创建对应权限单元 var perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Visble, Resource_Type_Enum.报表, code, report.Id); perm.SetCode($"report:{code}:visble"); fsql.Insert(perm).ExecuteAffrows(); // 授权给 admin 角色 var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); if (adminRole != null) { fsql.Insert(new SysRolePermission(snowflake.CreateId(), adminRole.Id, perm.Id)).ExecuteAffrows(); } Console.WriteLine("[DbSeeder] 示例报表「职工花名册」已创建"); } else { // 已存在:替换旧模板 / 补全缺失的模板与查询配置 bool changed = false; if (needReplaceTemplate && templateJson != null) { var file = fsql.Select().Where(f => f.ReportId == exist.Id && !f.IsDeleted).First(); if (file != null) { file.UpdateContent(templateJson); fsql.Update().SetSource(file).ExecuteAffrows(); } else { file = new SysReportFile(snowflake.CreateId(), exist.Id, templateJson); fsql.Insert(file).ExecuteAffrows(); exist.BindReportFile(file.Id); } changed = true; } if (string.IsNullOrEmpty(exist.Config) || !exist.Config.Contains("q_dep")) { exist.UpdateBasic(exist.Code, exist.Name, exist.Category, exist.Description, exist.DataSql, exist.DataParams, exist.SortOrder, config); changed = true; } if (changed) { fsql.Update().SetSource(exist).ExecuteAffrows(); Console.WriteLine("[DbSeeder] 已更新示例报表模板/查询配置"); } } } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 示例报表种子跳过:{ex.Message}"); } } /// /// 落丝记录报表(生产-前缫):DsName=RepDt_Prod_Lsjl,参数为日期范围 / 车间 / 机组 / 庄口 / 关键字。 /// 已存在的报表只补齐查询配置,不覆盖用户在设计器里维护的 .grf 模板。 /// private static void SeedLsjlReport(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try { const string code = "PROD_QS_1"; const string name = "落丝记录"; const string category = "生产.前缫"; const string dsName = "RepDt_Prod_Lsjl"; // 日期范围参数默认值用「不限」:避免 paramInitialValue 把空值默认成今天,导致只查当天 const string config = "{\"DsName\":\"RepDt_Prod_Lsjl\",\"ViewerType\":0,\"Parms\":[" + "{\"Name\":\"q_date_start\",\"Label\":\"开始日期\",\"Prompt\":\"留空则不限开始日期\",\"IsRequired\":false," + "\"ControlType\":\"AntdUI.DatePicker\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"不限\"}," + "{\"Name\":\"q_date_end\",\"Label\":\"结束日期\",\"Prompt\":\"留空则不限结束日期\",\"IsRequired\":false," + "\"ControlType\":\"AntdUI.DatePicker\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"不限\"}," + "{\"Name\":\"q_dep\",\"Label\":\"车间\",\"Prompt\":\"含子车间,留空为全部\",\"IsRequired\":false," + "\"ControlType\":\"F9-DEP-QS-SELECT\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}," + "{\"Name\":\"q_jizu\",\"Label\":\"机组\",\"Prompt\":\"留空为全部\",\"IsRequired\":false," + "\"ControlType\":\"AntdUI.Input\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}," + "{\"Name\":\"q_zk\",\"Label\":\"庄口\",\"Prompt\":\"点击选择庄口\",\"IsRequired\":false," + "\"ControlType\":\"F9-GYZK-SELECT\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}," + "{\"Name\":\"q_like\",\"Label\":\"关键字\",\"Prompt\":\"庄口 / 规格 / 颜色\",\"IsRequired\":false," + "\"ControlType\":\"AntdUI.Input\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}]}"; const string description = "生产-前缫:按日期范围 / 车间 / 机组 / 庄口查询落丝记录明细。"; var exist = fsql.Select().Where(r => r.Code == code && !r.IsDeleted).First(); if (exist == null) { var report = new SysReport(snowflake.CreateId(), code, name, category, description, "", "[]", 2, config); var newTemplate = LoadEmbeddedTemplateJson("lsjl.grf"); if (newTemplate != null) { var newFile = new SysReportFile(snowflake.CreateId(), report.Id, newTemplate); fsql.Insert(newFile).ExecuteAffrows(); report.BindReportFile(newFile.Id); } fsql.Insert(report).ExecuteAffrows(); var perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Visble, Resource_Type_Enum.报表, code, report.Id); perm.SetCode($"report:{code}:visble"); fsql.Insert(perm).ExecuteAffrows(); var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); if (adminRole != null) fsql.Insert(new SysRolePermission(snowflake.CreateId(), adminRole.Id, perm.Id)).ExecuteAffrows(); Console.WriteLine("[DbSeeder] 落丝记录报表已创建(DsName=RepDt_Prod_Lsjl)"); return; } // 已存在:DsName 未指向该 Provider、或参数标签全为空(程序生成的占位配置)时补齐查询配置 // (数据 SQL 清空,避免与 DsName 混淆) var changed = false; if (string.IsNullOrEmpty(exist.Config) || !exist.Config.Contains(dsName) || ConfigParamsMissingLabel(exist.Config)) { exist.UpdateBasic(exist.Code, exist.Name, string.IsNullOrWhiteSpace(exist.Category) ? category : exist.Category, string.IsNullOrWhiteSpace(exist.Description) ? description : exist.Description, "", exist.DataParams, exist.SortOrder, config); changed = true; Console.WriteLine("[DbSeeder] 落丝记录报表查询配置已更新(DsName=RepDt_Prod_Lsjl)"); } // 模板是占位模板(无标题 / 无参数声明,多为早期上传测试产物)时,替换为内置规范模板; // 旧版内置模板有两类问题一并升级: // 1) 用 Parameter 属性绑定报表参数(GR 推荐 [#{参数名}#] 文本标记) // 2) 把「查询条件」(q_*) 误当成模板参数(那是查询面板的东西,不是报表参数) var templateJson = LoadEmbeddedTemplateJson("lsjl.grf"); var file = fsql.Select().Where(f => f.ReportId == exist.Id && !f.IsDeleted).First(); var oursTemplate = file != null && file.DesignContent.Contains("\"Author\": \"F9 报表中心\""); var legacyParamBinding = oursTemplate && (file!.DesignContent.Contains("\"Parameter\": \"开始日期\"") || TemplateHasQueryParams(file.DesignContent) || TemplateHasStaticBoxParamBinding(file.DesignContent)); if (templateJson != null && (file == null || legacyParamBinding || IsPlaceholderReportTemplate(file.DesignContent))) { if (file == null) { file = new SysReportFile(snowflake.CreateId(), exist.Id, templateJson); fsql.Insert(file).ExecuteAffrows(); } else { file.UpdateContent(templateJson); fsql.Update().SetSource(file).ExecuteAffrows(); } exist.BindReportFile(file.Id); changed = true; Console.WriteLine("[DbSeeder] 落丝记录报表模板已更新为内置模板(标题 + 查询参数区)"); } if (changed) fsql.Update().SetSource(exist).ExecuteAffrows(); } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 落丝记录报表种子跳过:{ex.Message}"); } } /// /// 判断查询条件配置是否缺少标签(Parms 为空,或所有 Label 都为空 → 程序生成的占位配置)。 /// 用户手工配过标签的配置不会被种子覆盖。 /// private static bool ConfigParamsMissingLabel(string config) { if (string.IsNullOrWhiteSpace(config)) return true; try { using var doc = System.Text.Json.JsonDocument.Parse(config); if (!doc.RootElement.TryGetProperty("Parms", out var ps) || ps.ValueKind != System.Text.Json.JsonValueKind.Array || ps.GetArrayLength() == 0) return true; foreach (var p in ps.EnumerateArray()) { if (p.ValueKind != System.Text.Json.JsonValueKind.Object) continue; var label = p.TryGetProperty("Label", out var l) && l.ValueKind == System.Text.Json.JsonValueKind.String ? l.GetString() ?? "" : ""; if (!string.IsNullOrWhiteSpace(label)) return false; } return true; } catch { return true; } } /// /// 判断模板是否把报表参数绑在 StaticBox 上(实测 [#{参数}#] 只在 MemoBox 生效,StaticBox 原样输出)。 /// private static bool TemplateHasStaticBoxParamBinding(string content) { if (string.IsNullOrWhiteSpace(content) || content.StartsWith("_WR", StringComparison.Ordinal)) return false; try { using var doc = System.Text.Json.JsonDocument.Parse(content); return ContainsStaticBoxParamBinding(doc.RootElement); } catch { return false; } } private static bool ContainsStaticBoxParamBinding(System.Text.Json.JsonElement el) { if (el.ValueKind == System.Text.Json.JsonValueKind.Object) { var type = el.TryGetProperty("Type", out var t) && t.ValueKind == System.Text.Json.JsonValueKind.String ? t.GetString() ?? "" : ""; var text = el.TryGetProperty("Text", out var x) && x.ValueKind == System.Text.Json.JsonValueKind.String ? x.GetString() ?? "" : ""; if (type == "StaticBox" && text.Contains("[#{")) return true; foreach (var prop in el.EnumerateObject()) if (ContainsStaticBoxParamBinding(prop.Value)) return true; } else if (el.ValueKind == System.Text.Json.JsonValueKind.Array) { foreach (var item in el.EnumerateArray()) if (ContainsStaticBoxParamBinding(item)) return true; } return false; } /// /// 判断模板是否把「查询条件」(q_* 参数) 误写成了模板参数。 /// 模板参数应来自后端 Parameter_Data(如 车间 / 记录数 / 查询时间),不是查询面板的 q_*。 /// private static bool TemplateHasQueryParams(string content) { if (string.IsNullOrWhiteSpace(content) || content.StartsWith("_WR", StringComparison.Ordinal)) return false; try { using var doc = System.Text.Json.JsonDocument.Parse(content); if (!doc.RootElement.TryGetProperty("Parameter", out var ps) || ps.ValueKind != System.Text.Json.JsonValueKind.Array) return false; foreach (var p in ps.EnumerateArray()) { if (p.ValueKind != System.Text.Json.JsonValueKind.Object) continue; if (!p.TryGetProperty("Name", out var n) || n.ValueKind != System.Text.Json.JsonValueKind.String) continue; if ((n.GetString() ?? "").StartsWith("q_", StringComparison.Ordinal)) return true; } return false; } catch { return false; } } /// /// 判断模板是否为占位模板:JSON 格式、Title 为空、且没有声明任何 Parameter。 /// GR7 设计器保存的 _WR 压缩模板无法判断,一律视为正常模板(不覆盖)。 /// private static bool IsPlaceholderReportTemplate(string content) { if (string.IsNullOrWhiteSpace(content) || content.StartsWith("_WR", StringComparison.Ordinal)) return false; try { using var doc = System.Text.Json.JsonDocument.Parse(content); var root = doc.RootElement; var title = root.TryGetProperty("Title", out var t) && t.ValueKind == System.Text.Json.JsonValueKind.String ? t.GetString() ?? "" : ""; var hasParamDecl = root.TryGetProperty("Parameter", out var p) && p.ValueKind == System.Text.Json.JsonValueKind.Array && p.GetArrayLength() > 0; return string.IsNullOrWhiteSpace(title) && !hasParamDecl; } catch { return false; } } /// 从嵌入资源读取报表 .grf 模板 JSON 文本(.grf 为 UTF-8 JSON,GR7 向下兼容 GR6 格式) private static string? LoadEmbeddedTemplateJson(string fileName) { try { var asm = typeof(DbSeeder).Assembly; var resName = asm.GetManifestResourceNames() .FirstOrDefault(n => n.EndsWith("." + fileName, StringComparison.OrdinalIgnoreCase)); if (string.IsNullOrEmpty(resName)) return null; using var stream = asm.GetManifestResourceStream(resName); if (stream == null) return null; using var reader = new StreamReader(stream, new UTF8Encoding(false)); var json = reader.ReadToEnd(); // 统一去 BOM 后保存(ReportFile.DesignContent 存纯文本,输出 .grf 时再补 BOM) return json.Length > 0 && json[0] == '\uFEFF' ? json.Substring(1) : json; } catch (Exception ex) { Console.WriteLine($"[DbSeeder] 读取嵌入模板失败:{ex.Message}"); return null; } } }