first commit

This commit is contained in:
yanpeng200212
2026-10-08 10:43:59 +08:00
commit 8430ba0452
288 changed files with 29595 additions and 0 deletions
@@ -0,0 +1,501 @@
using FreeSql;
using F9WebAiTest.Domain.Entities;
using F9WebAiTest.Domain.Entities.Base;
namespace F9WebAiTest.Infrastructure.Data;
/// <summary>业务基础数据种子(部门/工艺庄口/生丝规格/生丝定级指标/机组车台)</summary>
public static class BusinessSeedData
{
public static void Seed(IFreeSql fsql)
{
SeedMenus(fsql);
SeedCraftZhuangKou(fsql);
SeedShengsiGuige(fsql);
SeedShengsiDingjizhibiao(fsql);
SeedDepartmentsAndJizuChetai(fsql);
SeedEmployees(fsql);
}
// ==================== 部门 + 机组车台(真实组织架构) ====================
private static readonly (long Id, string Name, long ParentId, int Sort, string Code)[] DeptData =
{
(10920411881541632, "安泰茧丝绸", 0, 0, "123"),
(10929439026188288, "丝业生产中心", 10920411881541632, 100, "100"),
(19667471837433856, "主干运维", 10920411881541632, 9999, "9999"),
(22305637455106048, "人力资源部", 10920411881541632, 800, "800"),
(22305758536273920, "财务部", 10920411881541632, 400, "400"),
(22305841730293760, "营销中心", 10920411881541632, 500, "500"),
(22305998261719040, "计划运营部", 10920411881541632, 200, "200"),
(22306589042020352, "品管部", 10920411881541632, 700, "700"),
(22306677265010688, "采购部", 10920411881541632, 300, "300"),
(26900271946797056, "安全环保后勤部", 10920411881541632, 1000, "1000"),
(26901050904547328, "经理会", 10920411881541632, 1500, "1500"),
(26901341515288576, "监察室", 10920411881541632, 3000, "3000"),
(26901475166785536, "融资部", 10920411881541632, 2000, "2000"),
(34638098675535872, "武胜蚕业公司", 10920411881541632, 4000, "4000"),
(35981234102341632, "安泰丝绸人南店", 10920411881541632, 6200, "6200"),
(35981427686248448, "绸业生产中心", 10920411881541632, 5000, "5000"),
(35981496963567616, "夹江蚕业公司", 10920411881541632, 4100, "4100"),
(35982738976673792, "安泰丝绸融恒店", 10920411881541632, 6000, "6000"),
(35982891737419776, "帛锦汇", 10920411881541632, 6100, "6100"),
(35982956409393152, "蚕业技术服务部", 10920411881541632, 4900, "4900"),
(36277179469795328, "广安蚕业公司", 10920411881541632, 4200, "4200"),
(10929739564847104, "仓储", 10929439026188288, 1000, "1000"),
(22307875854487552, "质检科", 10929439026188288, 1100, "1100"),
(22308241237086208, "试样室", 10929439026188288, 1200, "1200"),
(22308361408090112, "选剥茧车间", 10929439026188288, 1300, "1300"),
(25256354205667328, "前缫车间", 10929439026188288, 1350, "1350"),
(22308805823959040, "后缫车间", 10929439026188288, 1800, "1800"),
(22309110082965504, "设备车间", 10929439026188288, 2100, "2100"),
(22309340027293696, "副产品车间", 10929439026188288, 2200, "2200"),
(68179345423732736, "临时", 10929439026188288, 3000, "3000"),
(16043931070369792, "前缫一车间", 25256354205667328, 1400, "1400"),
(16047387197837312, "前缫二车间", 25256354205667328, 1500, "1500"),
(22308600831545344, "前缫三车间", 25256354205667328, 1600, "1600"),
(22308683820044288, "前缫四车间", 25256354205667328, 1700, "1700"),
(25379428280307712, "煮茧室", 25256354205667328, 1350, "1350"),
(22308918461992960, "检验室", 22308805823959040, 1900, "1900"),
(22309003073687552, "整理室", 22308805823959040, 2000, "2000"),
(24029387057795072, "丝棉车间", 22309340027293696, 0, "999"),
};
private static readonly (long DeptId, int Jizu, int Start, int End)[] JizuData =
{
(16043931070369792, 1, 1, 20), (16043931070369792, 2, 21, 40), (16043931070369792, 3, 41, 60),
(16043931070369792, 4, 61, 80), (16043931070369792, 5, 81, 100), (16043931070369792, 6, 101, 120),
(16047387197837312, 1, 1, 20), (16047387197837312, 2, 21, 40), (16047387197837312, 3, 41, 60),
(16047387197837312, 4, 61, 80), (16047387197837312, 5, 81, 100), (16047387197837312, 6, 101, 120),
(22308600831545344, 1, 1, 20), (22308600831545344, 2, 21, 40), (22308600831545344, 3, 41, 60),
(22308600831545344, 4, 61, 80), (22308600831545344, 5, 81, 100), (22308600831545344, 6, 101, 120),
(22308683820044288, 1, 1, 20), (22308683820044288, 2, 21, 40), (22308683820044288, 3, 41, 60),
(22308683820044288, 4, 61, 80), (22308683820044288, 5, 81, 100), (22308683820044288, 6, 101, 120),
};
private static void SeedDepartmentsAndJizuChetai(IFreeSql fsql)
{
// 检测是否已有真实部门(以"安泰茧丝绸"为标记)
if (fsql.Select<Department>().Any(d => d.Name == "安泰茧丝绸")) return;
// 物理清空假数据(总公司/技术部等)及关联
fsql.Ado.ExecuteNonQuery("DELETE FROM JizuChetais");
fsql.Ado.ExecuteNonQuery("DELETE FROM AttendanceRecords");
fsql.Ado.ExecuteNonQuery("DELETE FROM LeaveRequests");
fsql.Ado.ExecuteNonQuery("DELETE FROM Employees");
fsql.Ado.ExecuteNonQuery("DELETE FROM Departments");
// 第一遍:创建所有部门(ParentId 暂不设)
var idMap = new Dictionary<long, long>();
foreach (var d in DeptData)
{
var dept = new Department { Name = d.Name, Code = d.Code, Sort = d.Sort };
var newId = DbSeeder.InsertAndGetId(fsql, dept);
idMap[d.Id] = newId;
}
// 第二遍:设置 ParentId
foreach (var d in DeptData)
{
if (d.ParentId != 0 && idMap.ContainsKey(d.ParentId))
{
var dept = fsql.Select<Department>().Where(x => x.Id == idMap[d.Id]).First();
dept.ParentId = idMap[d.ParentId];
fsql.Update<Department>().SetSource(dept).ExecuteAffrows();
}
}
// 机组车台
foreach (var j in JizuData)
{
if (idMap.ContainsKey(j.DeptId))
{
fsql.Insert(new JizuChetai
{
DepartmentId = idMap[j.DeptId],
Jizu = j.Jizu,
ChetaiStart = j.Start,
ChetaiEnd = j.End
}).ExecuteAffrows();
}
}
}
// ==================== 职工 ====================
private static readonly (long DeptId, string Name, string Phone, string Login)[] EmpData =
{
(16043931070369792, "主干-何启旭", "18840854628", "18840854628"),
(10920411881541632, "测试用户", "0", "test"),
(10920411881541632, "秦燊", "13982683923", "13982683923"),
(10920411881541632, "陈勇", "18908283908", "18908283908"),
(22305758536273920, "汪菊", "18782619006", "18782619006"),
(22305841730293760, "陈林", "18580851234", "18580851234"),
(10929439026188288, "李正平", "15378405846", "15378405846"),
(22305637455106048, "朱莹", "18908283738", "18908283738"),
(22305637455106048, "毛银春", "18180268728", "18180268728"),
(22305637455106048, "陆国君", "13696173949", "13696173949"),
(22305637455106048, "华静", "13882636653", "13882636653"),
(22305637455106048, "余思洁", "18398508220", "18398508220"),
(22305758536273920, "孙瑞", "15982664679", "15982664679"),
(22305758536273920, "唐琳", "18048103256", "18048103256"),
(22305758536273920, "蒋宜", "18982626800", "18982626800"),
(22305758536273920, "王清华", "15282625323", "15282625323"),
(22305998261719040, "李西宴", "13688261711", "13688261711"),
(22305998261719040, "文杨", "13699654826", "13699654826"),
(22306589042020352, "曹庆", "13541993164", "13541993164"),
(22306677265010688, "蒙海波", "15884923167", "15884923167"),
(22305841730293760, "陈菊", "13696182387", "13696182387"),
(22305841730293760, "刘婷", "18382610528", "18382610528"),
(19667471837433856, "主干-张建", "15398380348", "15398380348"),
(19667471837433856, "主干-严鹏", "19162687174", "19162687174"),
(10929739564847104, "游建梅", "13629091099", "13629091099"),
(22307875854487552, "邓滟旖", "18181821168", "18181821168"),
(10929439026188288, "李志颖", "14708217874", "14708217874"),
(22308241237086208, "曹志君", "13882695209", "13882695209"),
(22308241237086208, "胡小琴", "18160129785", "18160129785"),
(22308361408090112, "唐小红", "15983458330", "15983458330"),
(25256354205667328, "余华", "13547542219", "13547542219"),
(22308683820044288, "陈建红", "15182698365", "15182698365"),
(22308683820044288, "谢军辉", "19957339779", "19957339779"),
(22308600831545344, "陈丽华", "15982605862", "15982605862"),
(16047387197837312, "杨静", "15982696700", "15982696700"),
(25256354205667328, "刘芳", "13882653025", "13882653025"),
(22308805823959040, "刘春兰", "13982624682", "13982624682"),
(22309110082965504, "朱再民", "15328288332", "15328288332"),
(22309340027293696, "舒文", "13551613659", "13551613659"),
(10929439026188288, "汪瑜琳", "13541553564", "13541553564"),
(19667471837433856, "范鸿才", "18981977117", "18981977117"),
(22305758536273920, "谭波", "18282636380", "18282636380"),
(22308918461992960, "董宗华", "18384503138", "18384503138"),
(22308918461992960, "易林梅", "18381112479", "18381112479"),
(22309003073687552, "舒红群", "18398526317", "18398526317"),
(25379428280307712, "彭卫东", "18382668003", "18382668003"),
(25379428280307712, "王远辉", "15982666491", "15982666491"),
(25379428280307712, "姚光林", "19160637815", "19160637815"),
(19667471837433856, "李学兴", "18117362915", "18117362915"),
(16043931070369792, "李远玲", "13982638574", "13982638574"),
(26900271946797056, "杨福锋", "13982638165", "13982638165"),
(26901341515288576, "杨利容", "15928659870", "15928659870"),
(19667471837433856, "主干-张罗", "18109064045", "18109064045"),
(26900271946797056, "袁鑫", "14781753888", "14781753888"),
(19667471837433856, "刘香君", "15808107846", "15808107846"),
(22308805823959040, "沈跃成", "13822244141", "13822244141"),
(19667471837433856, "杨凤琼", "18011528128", "18011528128"),
(22308600831545344, "谭爱君", "13699658634", "13699658634"),
};
private static void SeedEmployees(IFreeSql fsql)
{
var defaultPwd = BCrypt.Net.BCrypt.HashPassword("123456");
var deptNameMap = DeptData.ToDictionary(d => d.Id, d => d.Name);
foreach (var e in EmpData)
{
if (fsql.Select<Employee>().Any(x => x.Username == e.Login)) continue;
long deptId = 0;
if (deptNameMap.TryGetValue(e.DeptId, out var deptName))
{
var dept = fsql.Select<Department>().Where(d => d.Name == deptName).First();
if (dept != null) deptId = dept.Id;
}
fsql.Insert(new Employee
{
Username = e.Login,
Password = defaultPwd,
RealName = e.Name,
Nickname = e.Name,
Phone = e.Phone,
DepartmentId = deptId,
Status = 1
}).ExecuteAffrows();
}
}
// ==================== 菜单 ====================
private static void SeedMenus(IFreeSql fsql)
{
var baseDirId = fsql.Select<Menu>().Where(m => m.Path == "/base").First()?.Id ?? 0;
if (baseDirId == 0)
{
baseDirId = DbSeeder.InsertAndGetId(fsql, new Menu { Name = "基础数据", Type = "directory", Path = "/base", Icon = "Files", Sort = 3 });
}
AddMenu(fsql, baseDirId, "工艺庄口", "/base/zhuangkou", "base/zhuangkou/index", "Collection", "base:zhuangkou:list", 1);
AddMenu(fsql, baseDirId, "生丝规格", "/base/guige", "base/guige/index", "Grid", "base:guige:list", 2);
AddMenu(fsql, baseDirId, "生丝定级指标", "/base/dingjizhibiao", "base/dingjizhibiao/index", "TrendCharts", "base:dingjizhibiao:list", 3);
AddMenu(fsql, baseDirId, "机组车台", "/base/jizuchetai", "base/jizuchetai/index", "SetUp", "base:jizuchetai:list", 4);
// 生产管理目录
var prodDirId = fsql.Select<Menu>().Where(m => m.Path == "/prod").First()?.Id ?? 0;
if (prodDirId == 0)
{
prodDirId = DbSeeder.InsertAndGetId(fsql, new Menu { Name = "生产管理", Type = "directory", Path = "/prod", Icon = "Cpu", Sort = 4 });
}
AddMenu(fsql, prodDirId, "落丝记录", "/prod/lsjl", "prod/lsjl/index", "Document", "prod:lsjl:list", 1);
AddMenu(fsql, prodDirId, "纤度检验", "/prod/xdjy", "prod/xdjy/index", "EditPen", "prod:xdjy:list", 2);
AddMenu(fsql, prodDirId, "黑板检验", "/prod/hbjy", "prod/hbjy/index", "Notebook", "prod:hbjy:list", 3);
var adminRole = fsql.Select<Role>().Where(r => r.Code == "admin").First();
if (adminRole != null)
{
var baseMenus = fsql.Select<Menu>().Where(m => m.Path.StartsWith("/base") || m.Path.StartsWith("/prod")).ToList();
foreach (var m in baseMenus)
{
if (!fsql.Select<RoleMenu>().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id))
fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows();
}
}
}
private static void AddMenu(IFreeSql fsql, long parentId, string name, string path, string component, string icon, string permission, int sort)
{
if (!fsql.Select<Menu>().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 SeedCraftZhuangKou(IFreeSql fsql)
{
if (fsql.Select<CraftZhuangKou>().Any()) return;
fsql.Insert(new CraftZhuangKou[]
{
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-2026-武工艺20", ZhuangKouName = "2026年基地春一批(青松皓月)", ProductionDisplayName = "2026年基地春一批(青松皓月)", Chandi = "基地", Sort = 20 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-2026-武工艺24", ZhuangKouName = "2026年基地春(雄)", ProductionDisplayName = "2026年基地春(雄)", Chandi = "大面积", Sort = 24 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-2026-武工艺-22", ZhuangKouName = "2026年会东春(7532)", ProductionDisplayName = "2026年会东春(7532)", Chandi = "会东", Sort = 22 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-2026-武工艺-23", ZhuangKouName = "2026年珙县九丝春(雄)", ProductionDisplayName = "2026年珙县九丝春(雄)", Chandi = "珙县", Sort = 23 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-2026-武工艺-7", ZhuangKouName = "2026会东二类茧(7532)宁兴", ProductionDisplayName = "2026会东二类茧(7532)宁兴", Chandi = "会东", Sort = 7 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-武工艺-2026-25", ZhuangKouName = "2026年广安春(青松皓月)", ProductionDisplayName = "2026年广安春(青松皓月)", Chandi = "基地", Sort = 25 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-2026-武工艺-26", ZhuangKouName = "2026年珙县罗渡春(雄)", ProductionDisplayName = "2026年珙县罗渡春(雄)", Chandi = "珙县", Sort = 26 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-武工艺-2026-9", ZhuangKouName = "2026年广安春(青松皓月)宁兴", ProductionDisplayName = "2026年广安春(青松皓月)宁兴", Chandi = "广安", Sort = 9 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-武工艺-2026-28", ZhuangKouName = "2026年基地春三批(青松皓月)", ProductionDisplayName = "2026年基地春三批(青松皓月)", Chandi = "基地", Sort = 28 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-武工艺-2026-11", ZhuangKouName = "2026年基地春三批(青松皓月)宁兴", ProductionDisplayName = "2026年基地春三批(青松皓月)宁兴", Chandi = "基地", Sort = 11 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-武工艺-2026-29", ZhuangKouName = "2026年珙县宝山等混六庄春一批(雄)", ProductionDisplayName = "2026年珙县宝山等混六庄春一批(雄)", Chandi = "珙县", Sort = 29 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-武工艺-2026-27", ZhuangKouName = "2026年珙县曹营等混四庄春一批(雄)", ProductionDisplayName = "2026年珙县曹营等混四庄春一批(雄)", Chandi = "珙县", Sort = 27 },
new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-武工艺-2026-10", ZhuangKouName = "2026年珙县罗渡春(雄)宁兴", ProductionDisplayName = "2026年珙县罗渡春(雄)宁兴", Chandi = "珙县", Sort = 10 }
}).ExecuteAffrows();
}
private static void SeedShengsiGuige(IFreeSql fsql)
{
if (fsql.Select<ShengsiGuige>().Any()) return;
fsql.Insert(new ShengsiGuige[]
{
new ShengsiGuige { Guige = "20/22", CenterrangeLolimit = 19.5, CenterrangeUplimit = 21.5, CenterXiandu = 20.5, ValidLolimit = 15.0, ValidUplimit = 27.0, XiaoyeLolimit = 17.5, XiaoyeUplimit = 24.5, DayeLolimit = 16.5, DayeUplimit = 25.5, TeyeLolimit = 15.5, TeyeUplimit = 26.5, YexianNkLolimitLeft = 15.0, YexianNkLolimitRight = 17.5, YexianNkUplimitLeft = 24.0, YexianNkUplimitRight = 27.5, LaodongJingsaiYexianLolimit = 17.5, LaodongJingsaiYexianUplimit = 23.5, Sort = 0 },
new ShengsiGuige { Guige = "27/29", CenterrangeLolimit = 26.5, CenterrangeUplimit = 28.5, CenterXiandu = 27.5, ValidLolimit = 23.0, ValidUplimit = 34.0, XiaoyeLolimit = 25.0, XiaoyeUplimit = 32.0, DayeLolimit = 24.0, DayeUplimit = 33.0, TeyeLolimit = 23.0, TeyeUplimit = 34.0, YexianNkLolimitLeft = 22.0, YexianNkLolimitRight = 24.5, YexianNkUplimitLeft = 31.0, YexianNkUplimitRight = 34.5, LaodongJingsaiYexianLolimit = 24.5, LaodongJingsaiYexianUplimit = 30.5, Sort = 100 },
new ShengsiGuige { Guige = "40/44", CenterrangeLolimit = 40.5, CenterrangeUplimit = 42.5, CenterXiandu = 41.5, ValidLolimit = 23.0, ValidUplimit = 34.0, XiaoyeLolimit = 35.0, XiaoyeUplimit = 49.0, DayeLolimit = 33.0, DayeUplimit = 51.0, TeyeLolimit = 31.0, TeyeUplimit = 53.0, YexianNkLolimitLeft = 0.0, YexianNkLolimitRight = 0.0, YexianNkUplimitLeft = 0.0, YexianNkUplimitRight = 0.0, LaodongJingsaiYexianLolimit = 40.0, LaodongJingsaiYexianUplimit = 42.0, Sort = 200 }
}).ExecuteAffrows();
}
private static void SeedShengsiDingjizhibiao(IFreeSql fsql)
{
if (fsql.Select<ShengsiDingjizhibiao>().Any()) return;
var list = new List<ShengsiDingjizhibiao>();
void Add(int std, string guige, string grade, double score, double piancha, double dacha, double erdu, double sandu, double qingjie, double jiejing)
{
list.Add(new ShengsiDingjizhibiao { Standard = std, Guige = guige, Grade = grade, GradeScore = score, Piancha = piancha, Dacha = dacha, Erdu = erdu, Sandu = sandu, Qingjie = qingjie, Jiejing = jiejing });
}
// ===== 厂标 (standard=0) =====
Add(0, "20/22", "A", 7.0, 2.0, 6.0, 6.0, 2.0, 90.0, 86.0);
Add(0, "20/22", "A50", 7.5, 1.85, 5.6, 6.0, 2.0, 91.5, 87.0);
Add(0, "20/22", "2A", 8.0, 1.7, 5.2, 5.0, 1.0, 93.0, 88.0);
Add(0, "20/22", "2A50", 8.5, 1.58, 4.8, 5.0, 0.0, 94.5, 89.0);
Add(0, "20/22", "3A", 9.0, 1.45, 4.4, 4.0, 0.0, 95.0, 90.0);
Add(0, "20/22", "3A50", 9.5, 1.33, 4.0, 4.0, 0.0, 96.0, 91.0);
Add(0, "20/22", "4A", 10.0, 1.2, 3.6, 3.0, 0.0, 96.5, 92.0);
Add(0, "20/22", "4A50", 10.5, 1.13, 3.3, 3.0, 0.0, 97.0, 93.0);
Add(0, "20/22", "5A", 11.0, 1.05, 3.0, 2.0, 0.0, 97.5, 94.0);
Add(0, "20/22", "5A50", 11.5, 0.98, 2.8, 2.0, 0.0, 97.8, 94.5);
Add(0, "20/22", "6A", 12.0, 0.9, 2.6, 1.0, 0.0, 98.0, 95.0);
Add(0, "27/29", "A", 7.0, 2.05, 6.1, 6.0, 0.0, 90.0, 86.0);
Add(0, "27/29", "A50", 7.5, 1.9, 5.6, 6.0, 0.0, 91.5, 87.0);
Add(0, "27/29", "2A", 8.0, 1.75, 5.1, 5.0, 0.0, 93.0, 88.0);
Add(0, "27/29", "2A50", 8.5, 1.65, 4.7, 5.0, 0.0, 94.5, 89.0);
Add(0, "27/29", "3A", 9.0, 1.55, 4.3, 4.0, 0.0, 95.0, 90.0);
Add(0, "27/29", "3A50", 9.5, 1.45, 4.0, 4.0, 0.0, 96.0, 91.0);
Add(0, "27/29", "4A", 10.0, 1.3, 3.7, 3.0, 0.0, 96.5, 92.0);
Add(0, "27/29", "4A50", 10.5, 1.25, 3.5, 3.0, 0.0, 97.0, 93.0);
Add(0, "27/29", "5A", 11.0, 1.15, 3.3, 2.0, 0.0, 97.5, 94.0);
Add(0, "27/29", "5A50", 11.5, 1.1, 3.1, 2.0, 0.0, 97.8, 94.5);
Add(0, "27/29", "6A", 12.0, 1.05, 3.0, 1.0, 0.0, 98.0, 95.0);
Add(0, "40/44", "A", 7.0, 2.85, 8.45, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "A50", 7.5, 2.69, 7.88, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "2A", 8.0, 2.53, 7.3, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "2A50", 8.5, 2.36, 6.79, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "3A", 9.0, 2.18, 6.28, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "3A50", 9.5, 2.04, 5.89, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "4A", 10.0, 1.9, 5.5, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "4A50", 10.5, 1.8, 5.2, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "5A", 11.0, 1.7, 4.9, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "5A50", 11.5, 1.6, 4.6, 0.0, 0.0, 0.0, 0.0);
Add(0, "40/44", "6A", 12.0, 1.5, 4.3, 0.0, 0.0, 0.0, 0.0);
Add(0, "20/22", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0);
Add(0, "27/29", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0);
Add(0, "40/44", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0);
// ===== 个人 (standard=2) =====
Add(2, "20/22", "A00", 7.0, 2.0, 6.0, 6.0, 2.0, 90.0, 86.0);
Add(2, "20/22", "A10", 7.1, 1.97, 5.92, 6.0, 2.0, 90.3, 86.2);
Add(2, "20/22", "A20", 7.2, 1.94, 5.84, 6.0, 2.0, 90.6, 86.4);
Add(2, "20/22", "A30", 7.3, 1.91, 5.76, 6.0, 2.0, 90.9, 86.6);
Add(2, "20/22", "A40", 7.4, 1.88, 5.68, 6.0, 2.0, 91.2, 86.8);
Add(2, "20/22", "A50", 7.5, 1.85, 5.6, 6.0, 2.0, 91.5, 87.0);
Add(2, "20/22", "A60", 7.6, 1.82, 5.52, 5.8, 1.8, 91.8, 87.2);
Add(2, "20/22", "A70", 7.7, 1.79, 5.44, 5.6, 1.6, 92.1, 87.4);
Add(2, "20/22", "A80", 7.8, 1.76, 5.36, 5.4, 1.4, 92.4, 87.6);
Add(2, "20/22", "A90", 7.9, 1.73, 5.28, 5.2, 1.2, 92.7, 87.8);
Add(2, "20/22", "2A00", 8.0, 1.7, 5.2, 5.0, 1.0, 93.0, 88.0);
Add(2, "20/22", "2A10", 8.1, 1.68, 5.12, 5.0, 0.8, 93.3, 88.2);
Add(2, "20/22", "2A20", 8.2, 1.65, 5.04, 5.0, 0.6, 93.6, 88.4);
Add(2, "20/22", "2A30", 8.3, 1.63, 4.96, 5.0, 0.4, 93.9, 88.6);
Add(2, "20/22", "2A40", 8.4, 1.6, 4.88, 5.0, 0.2, 94.2, 88.8);
Add(2, "20/22", "2A50", 8.5, 1.58, 4.8, 5.0, 0.0, 94.5, 89.0);
Add(2, "20/22", "2A60", 8.6, 1.55, 4.72, 4.8, 0.0, 94.6, 89.2);
Add(2, "20/22", "2A70", 8.7, 1.53, 4.64, 4.6, 0.0, 94.7, 89.4);
Add(2, "20/22", "2A80", 8.8, 1.5, 4.56, 4.4, 0.0, 94.8, 89.6);
Add(2, "20/22", "2A90", 8.9, 1.48, 4.48, 4.2, 0.0, 94.9, 89.8);
Add(2, "20/22", "3A00", 9.0, 1.45, 4.4, 4.0, 0.0, 95.0, 90.0);
Add(2, "20/22", "3A10", 9.1, 1.43, 4.32, 4.0, 0.0, 95.2, 90.2);
Add(2, "20/22", "3A20", 9.2, 1.4, 4.24, 4.0, 0.0, 95.4, 90.4);
Add(2, "20/22", "3A30", 9.3, 1.38, 4.16, 4.0, 0.0, 95.6, 90.6);
Add(2, "20/22", "3A40", 9.4, 1.35, 4.08, 4.0, 0.0, 95.8, 90.8);
Add(2, "20/22", "3A50", 9.5, 1.33, 4.0, 4.0, 0.0, 96.0, 91.0);
Add(2, "20/22", "3A60", 9.6, 1.3, 3.92, 3.8, 0.0, 96.1, 91.2);
Add(2, "20/22", "3A70", 9.7, 1.28, 3.84, 3.6, 0.0, 96.2, 91.4);
Add(2, "20/22", "3A80", 9.8, 1.25, 3.76, 3.4, 0.0, 96.3, 91.6);
Add(2, "20/22", "3A90", 9.9, 1.23, 3.68, 3.2, 0.0, 96.4, 91.8);
Add(2, "20/22", "4A00", 10.0, 1.2, 3.6, 3.0, 0.0, 96.5, 92.0);
Add(2, "20/22", "4A10", 10.1, 1.19, 3.54, 3.0, 0.0, 96.6, 92.2);
Add(2, "20/22", "4A20", 10.2, 1.17, 3.48, 3.0, 0.0, 96.7, 92.4);
Add(2, "20/22", "4A30", 10.3, 1.16, 3.42, 3.0, 0.0, 96.8, 92.6);
Add(2, "20/22", "4A40", 10.4, 1.14, 3.36, 3.0, 0.0, 96.9, 92.8);
Add(2, "20/22", "4A50", 10.5, 1.13, 3.3, 3.0, 0.0, 97.0, 93.0);
Add(2, "20/22", "4A60", 10.6, 1.11, 3.24, 2.8, 0.0, 97.1, 93.2);
Add(2, "20/22", "4A70", 10.7, 1.1, 3.18, 2.6, 0.0, 97.2, 93.4);
Add(2, "20/22", "4A80", 10.8, 1.08, 3.12, 2.4, 0.0, 97.3, 93.6);
Add(2, "20/22", "4A90", 10.9, 1.07, 3.06, 2.2, 0.0, 97.4, 93.8);
Add(2, "20/22", "5A00", 11.0, 1.05, 3.0, 2.0, 0.0, 97.5, 94.0);
Add(2, "20/22", "5A10", 11.1, 1.04, 2.96, 2.0, 0.0, 97.56, 94.1);
Add(2, "20/22", "5A20", 11.2, 1.03, 2.92, 2.0, 0.0, 97.62, 94.2);
Add(2, "20/22", "5A30", 11.3, 1.01, 2.88, 2.0, 0.0, 97.68, 94.3);
Add(2, "20/22", "5A40", 11.4, 0.99, 2.84, 2.0, 0.0, 97.74, 94.4);
Add(2, "20/22", "5A50", 11.5, 0.98, 2.8, 2.0, 0.0, 97.8, 94.5);
Add(2, "20/22", "5A60", 11.6, 0.96, 2.76, 1.8, 0.0, 97.84, 94.6);
Add(2, "20/22", "5A70", 11.7, 0.95, 2.72, 1.6, 0.0, 97.88, 94.7);
Add(2, "20/22", "5A80", 11.8, 0.93, 2.68, 1.4, 0.0, 97.92, 94.8);
Add(2, "20/22", "5A90", 11.9, 0.92, 2.64, 1.2, 0.0, 97.96, 94.9);
Add(2, "20/22", "6A00", 12.0, 0.9, 2.6, 1.0, 0.0, 98.0, 95.0);
Add(2, "27/29", "A00", 7.0, 2.05, 6.0, 12.0, 0.0, 90.0, 86.0);
Add(2, "27/29", "A10", 7.1, 2.02, 5.9, 11.6, 0.0, 90.3, 86.2);
Add(2, "27/29", "A20", 7.2, 1.99, 5.8, 11.2, 0.0, 90.6, 86.4);
Add(2, "27/29", "A30", 7.3, 1.96, 5.7, 10.8, 0.0, 90.9, 86.6);
Add(2, "27/29", "A40", 7.4, 1.93, 5.6, 10.4, 0.0, 91.2, 86.8);
Add(2, "27/29", "A50", 7.5, 1.9, 5.5, 10.0, 0.0, 91.5, 87.0);
Add(2, "27/29", "A60", 7.6, 1.87, 5.4, 9.6, 0.0, 91.8, 87.2);
Add(2, "27/29", "A70", 7.7, 1.84, 5.3, 9.2, 0.0, 92.1, 87.4);
Add(2, "27/29", "A80", 7.8, 1.81, 5.2, 8.8, 0.0, 92.4, 87.6);
Add(2, "27/29", "A90", 7.9, 1.78, 5.1, 8.4, 0.0, 92.7, 87.8);
Add(2, "27/29", "2A00", 8.0, 1.75, 5.0, 8.0, 0.0, 93.0, 88.0);
Add(2, "27/29", "2A10", 8.1, 1.73, 4.92, 7.7, 0.0, 93.2, 88.2);
Add(2, "27/29", "2A20", 8.2, 1.69, 4.76, 7.1, 0.0, 93.6, 88.6);
Add(2, "27/29", "2A30", 8.3, 1.67, 4.68, 6.8, 0.0, 93.8, 88.8);
Add(2, "27/29", "2A40", 8.4, 1.65, 4.6, 6.5, 0.0, 94.0, 89.0);
Add(2, "27/29", "2A50", 8.5, 1.63, 4.52, 6.2, 0.0, 94.2, 89.2);
Add(2, "27/29", "2A60", 8.6, 1.61, 4.44, 5.9, 0.0, 94.4, 89.4);
Add(2, "27/29", "2A70", 8.7, 1.59, 4.36, 5.6, 0.0, 94.6, 89.6);
Add(2, "27/29", "2A80", 8.8, 1.57, 4.28, 5.3, 0.0, 94.8, 89.8);
Add(2, "27/29", "2A90", 8.9, 1.55, 4.2, 5.0, 0.0, 95.0, 90.0);
Add(2, "27/29", "3A00", 9.0, 1.53, 4.14, 4.8, 0.0, 95.15, 90.2);
Add(2, "27/29", "3A10", 9.1, 1.51, 4.08, 4.6, 0.0, 95.3, 90.4);
Add(2, "27/29", "3A20", 9.2, 1.49, 4.02, 4.4, 0.0, 95.45, 90.6);
Add(2, "27/29", "3A30", 9.3, 1.47, 3.96, 4.2, 0.0, 95.6, 90.8);
Add(2, "27/29", "3A40", 9.4, 1.45, 3.9, 4.0, 0.0, 95.75, 91.0);
Add(2, "27/29", "3A50", 9.5, 1.43, 3.84, 3.8, 0.0, 95.9, 91.2);
Add(2, "27/29", "3A60", 9.6, 1.41, 3.78, 3.6, 0.0, 96.05, 91.4);
Add(2, "27/29", "3A70", 9.7, 1.39, 3.72, 3.4, 0.0, 96.2, 91.6);
Add(2, "27/29", "3A80", 9.8, 1.37, 3.66, 3.2, 0.0, 96.35, 91.8);
Add(2, "27/29", "3A90", 9.9, 1.35, 3.6, 3.0, 0.0, 96.5, 92.0);
Add(2, "27/29", "4A00", 10.0, 1.33, 3.66, 2.85, 0.0, 96.6, 92.2);
Add(2, "27/29", "4A10", 10.1, 1.31, 3.62, 2.7, 0.0, 96.7, 92.4);
Add(2, "27/29", "4A20", 10.2, 1.29, 3.58, 2.55, 0.0, 96.8, 92.6);
Add(2, "27/29", "4A30", 10.3, 1.27, 3.54, 2.4, 0.0, 96.9, 92.8);
Add(2, "27/29", "4A40", 10.4, 1.25, 3.5, 2.25, 0.0, 97.0, 93.0);
Add(2, "27/29", "4A50", 10.5, 1.23, 3.46, 2.1, 0.0, 97.1, 93.2);
Add(2, "27/29", "4A60", 10.6, 1.21, 3.42, 1.95, 0.0, 97.2, 93.4);
Add(2, "27/29", "4A70", 10.7, 1.19, 3.38, 1.8, 0.0, 97.3, 93.6);
Add(2, "27/29", "4A80", 10.8, 1.17, 3.34, 1.65, 0.0, 97.4, 93.8);
Add(2, "27/29", "4A90", 10.9, 1.15, 3.3, 1.5, 0.0, 97.5, 94.0);
Add(2, "27/29", "5A00", 11.0, 1.14, 3.27, 1.45, 0.0, 97.55, 94.1);
Add(2, "27/29", "5A10", 11.1, 1.13, 3.24, 1.4, 0.0, 97.6, 94.2);
Add(2, "27/29", "5A20", 11.2, 1.12, 3.21, 1.35, 0.0, 97.65, 94.3);
Add(2, "27/29", "5A30", 11.3, 1.11, 3.18, 1.3, 0.0, 97.7, 94.4);
Add(2, "27/29", "5A40", 11.4, 1.1, 3.15, 1.25, 0.0, 97.75, 94.5);
Add(2, "27/29", "5A50", 11.5, 1.09, 3.12, 1.2, 0.0, 97.8, 94.6);
Add(2, "27/29", "5A60", 11.6, 1.08, 3.09, 1.15, 0.0, 97.85, 94.7);
Add(2, "27/29", "5A70", 11.7, 1.07, 3.06, 1.1, 0.0, 97.9, 94.8);
Add(2, "27/29", "5A80", 11.8, 1.06, 3.03, 1.05, 0.0, 97.95, 94.9);
Add(2, "27/29", "5A90", 11.9, 1.05, 3.0, 1.0, 0.0, 98.0, 95.0);
Add(2, "27/29", "6A00", 12.0, 1.05, 3.0, 1.0, 0.0, 98.0, 95.0);
Add(2, "40/44", "A00", 7.0, 2.6, 7.55, 8.0, 0.0, 90.0, 86.0);
Add(2, "40/44", "A10", 7.1, 2.57, 7.43, 7.7, 0.0, 90.3, 86.2);
Add(2, "40/44", "A20", 7.2, 2.53, 7.31, 7.4, 0.0, 90.6, 86.4);
Add(2, "40/44", "A30", 7.3, 2.5, 7.19, 7.1, 0.0, 90.9, 86.6);
Add(2, "40/44", "A40", 7.4, 2.46, 7.07, 6.8, 0.0, 91.2, 86.8);
Add(2, "40/44", "A50", 7.5, 2.43, 6.95, 6.5, 0.0, 91.5, 87.0);
Add(2, "40/44", "A60", 7.6, 2.39, 6.83, 6.2, 0.0, 91.8, 87.2);
Add(2, "40/44", "A70", 7.7, 2.36, 6.71, 5.9, 0.0, 92.1, 87.4);
Add(2, "40/44", "A80", 7.8, 2.32, 6.59, 5.6, 0.0, 92.4, 87.6);
Add(2, "40/44", "A90", 7.9, 2.29, 6.47, 5.3, 0.0, 92.7, 87.8);
Add(2, "40/44", "2A00", 8.0, 2.25, 6.35, 5.0, 0.0, 93.0, 88.0);
Add(2, "40/44", "2A10", 8.1, 2.22, 6.24, 4.8, 0.0, 93.2, 88.2);
Add(2, "40/44", "2A20", 8.2, 2.18, 6.13, 4.6, 0.0, 93.4, 88.4);
Add(2, "40/44", "2A30", 8.3, 2.15, 6.02, 4.4, 0.0, 93.6, 88.6);
Add(2, "40/44", "2A40", 8.4, 2.11, 5.91, 4.2, 0.0, 93.8, 88.8);
Add(2, "40/44", "2A50", 8.5, 2.08, 5.8, 4.0, 0.0, 94.0, 89.0);
Add(2, "40/44", "2A60", 8.6, 2.04, 5.69, 3.8, 0.0, 94.2, 89.2);
Add(2, "40/44", "2A70", 8.7, 2.01, 5.58, 3.6, 0.0, 94.4, 89.4);
Add(2, "40/44", "2A80", 8.8, 1.97, 5.47, 3.4, 0.0, 94.6, 89.6);
Add(2, "40/44", "2A90", 8.9, 1.94, 5.36, 3.2, 0.0, 94.8, 89.8);
Add(2, "40/44", "3A00", 9.0, 1.9, 5.25, 3.0, 0.0, 95.0, 90.0);
Add(2, "40/44", "3A10", 9.1, 1.87, 5.16, 1.95, 0.0, 95.15, 90.2);
Add(2, "40/44", "3A20", 9.2, 1.83, 5.06, 1.9, 0.0, 95.3, 90.4);
Add(2, "40/44", "3A30", 9.3, 1.8, 4.97, 1.85, 0.0, 95.45, 90.6);
Add(2, "40/44", "3A40", 9.4, 1.76, 4.87, 1.8, 0.0, 95.6, 90.8);
Add(2, "40/44", "3A50", 9.5, 1.73, 4.78, 1.75, 0.0, 95.75, 91.0);
Add(2, "40/44", "3A60", 9.6, 1.69, 4.68, 1.7, 0.0, 95.9, 91.2);
Add(2, "40/44", "3A70", 9.7, 1.66, 4.59, 1.65, 0.0, 96.05, 91.4);
Add(2, "40/44", "3A80", 9.8, 1.62, 4.49, 1.6, 0.0, 96.2, 91.6);
Add(2, "40/44", "3A90", 9.9, 1.59, 4.4, 1.55, 0.0, 96.35, 91.8);
Add(2, "40/44", "4A00", 10.0, 1.55, 4.3, 1.5, 0.0, 96.5, 92.0);
Add(2, "40/44", "4A10", 10.1, 1.53, 4.24, 1.45, 0.0, 96.6, 92.2);
Add(2, "40/44", "4A20", 10.2, 1.51, 4.18, 1.4, 0.0, 96.7, 92.4);
Add(2, "40/44", "4A30", 10.3, 1.49, 4.12, 1.35, 0.0, 96.8, 92.6);
Add(2, "40/44", "4A40", 10.4, 1.47, 4.06, 1.3, 0.0, 96.9, 92.8);
Add(2, "40/44", "4A50", 10.5, 1.45, 4.0, 1.25, 0.0, 97.0, 93.0);
Add(2, "40/44", "4A60", 10.6, 1.43, 3.94, 1.2, 0.0, 97.1, 93.2);
Add(2, "40/44", "4A70", 10.7, 1.41, 3.88, 1.15, 0.0, 97.2, 93.4);
Add(2, "40/44", "4A80", 10.8, 1.39, 3.82, 1.1, 0.0, 97.3, 93.6);
Add(2, "40/44", "4A90", 10.9, 1.37, 3.76, 1.05, 0.0, 97.4, 93.8);
Add(2, "40/44", "5A00", 11.0, 1.35, 3.7, 1.0, 0.0, 97.5, 94.0);
Add(2, "40/44", "5A10", 11.1, 1.33, 3.64, 0.0, 0.0, 97.55, 94.1);
Add(2, "40/44", "5A20", 11.2, 1.31, 3.58, 0.0, 0.0, 97.6, 94.2);
Add(2, "40/44", "5A30", 11.3, 1.29, 3.52, 0.0, 0.0, 97.65, 94.3);
Add(2, "40/44", "5A40", 11.4, 1.27, 3.46, 0.0, 0.0, 97.7, 94.4);
Add(2, "40/44", "5A50", 11.5, 1.25, 3.4, 0.0, 0.0, 97.75, 94.5);
Add(2, "40/44", "5A60", 11.6, 1.23, 3.34, 0.0, 0.0, 97.8, 94.6);
Add(2, "40/44", "5A70", 11.7, 1.21, 3.28, 0.0, 0.0, 97.85, 94.7);
Add(2, "40/44", "5A80", 11.8, 1.19, 3.22, 0.0, 0.0, 97.9, 94.8);
Add(2, "40/44", "5A90", 11.9, 1.17, 3.16, 0.0, 0.0, 97.95, 94.9);
Add(2, "40/44", "6A00", 12.0, 1.15, 3.1, 0.0, 0.0, 98.0, 95.0);
Add(2, "20/22", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0);
Add(2, "27/29", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0);
Add(2, "40/44", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0);
fsql.Insert(list).ExecuteAffrows();
}
}
@@ -0,0 +1,509 @@
using System.Data;
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 SysPermission = F9WebAiTest.Domain.Entities.SYS.RBAC.Permission;
using SysRolePermission = F9WebAiTest.Domain.Entities.SYS.RBAC.RolePermission;
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<Role>().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<Menu>().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.7 为菜单创建权限单元(Permission),并修正旧菜单权限关联(统一权限模型)
FixMenuPermissions(fsql, snowflake);
// 7. 管理员账号(幂等)
var admin = fsql.Select<Employee>().Where(e => e.Username == "admin").First();
if (admin == null)
{
var dept = fsql.Select<Department>().Where(d => d.Name == "主干运维").First()
?? fsql.Select<Department>().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<Employee>().Where(e => e.Id == adminId).First();
}
// 8. admin 关联 admin 角色
var adminRole = fsql.Select<Role>().Where(r => r.Code == "admin").First();
if (!fsql.Select<EmployeeRole>().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<Menu>().ToList();
foreach (var m in allMenus)
{
if (!fsql.Select<RoleMenu>().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id))
{
fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows();
}
}
}
/// <summary>CodeFirst 建表(新表建表,旧表同步失败则跳过)+ 旧库加列兜底</summary>
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 ''");
}
/// <summary>为已有表补加列(列已存在则忽略)</summary>
private static void EnsureColumn(IFreeSql fsql, string table, string column, string definition)
{
try
{
fsql.Ado.ExecuteNonQuery($"ALTER TABLE \"{table}\" ADD COLUMN \"{column}\" {definition};");
}
catch
{
// 列已存在,忽略
}
}
/// <summary>插入并返回自增主键 ID(SQLite 用 last_insert_rowid 可靠获取)</summary>
public static long InsertAndGetId<T>(IFreeSql fsql, T entity) where T : BaseEntity
{
fsql.Insert(entity).ExecuteAffrows();
return Convert.ToInt64(fsql.Ado.ExecuteScalar("SELECT last_insert_rowid();"));
}
/// <summary>通知种子:消息类型 + 消息中心菜单</summary>
private static void SeedNotificationData(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
// 消息类型:发现特野
if (!fsql.Select<NotificationType>().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/xdjy"
}).ExecuteAffrows();
}
// 消息类型:发现特大糙
if (!fsql.Select<NotificationType>().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/hbjy"
}).ExecuteAffrows();
}
// 消息类型:请假审批(订阅权限用独立码,避免与业务按钮权限冲突)
if (!fsql.Select<NotificationType>().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<NotificationType>().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<NotificationType>().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<SysPermission>()
.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<Menu>().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<NotificationType>().Where(x => x.Code == code).First();
if (t == null) continue;
// 1. RequiredPermission 改为空(所有人可订阅)
if (!string.IsNullOrEmpty(t.RequiredPermission))
{
t.RequiredPermission = "";
fsql.Update<NotificationType>().SetSource(t).ExecuteAffrows();
}
// 2. 删除订阅权限单元及其角色关联(新 Permission 表)
var perm = fsql.Select<SysPermission>()
.Where(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == t.Id).First();
if (perm != null)
{
fsql.Delete<SysRolePermission>().Where(rp => rp.PermissionId == perm.Id).ExecuteAffrows();
fsql.Delete<SysPermission>().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<NotificationType>().Where(x => x.RequiredPermission == "").ToList())
{
var perm = fsql.Select<SysPermission>()
.Where(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == t.Id).First();
if (perm != null)
{
fsql.Delete<SysRolePermission>().Where(rp => rp.PermissionId == perm.Id).ExecuteAffrows();
fsql.Delete<SysPermission>().Where(p => p.Id == perm.Id).ExecuteAffrows();
}
}
// 同步订阅权限到 RBAC 权限单元(ResourceType=消息订阅,Code=RequiredPermission)
foreach (var t in fsql.Select<NotificationType>().ToList())
{
if (string.IsNullOrEmpty(t.RequiredPermission)) continue;
var existPerm = fsql.Select<SysPermission>()
.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<SysPermission>().SetSource(existPerm).ExecuteAffrows();
}
}
// admin 角色分配消息中心菜单权限
var adminRole = fsql.Select<Role>().Where(r => r.Code == "admin").First();
if (adminRole != null)
{
var notiMenus = fsql.Select<Menu>().Where(m => m.Path.StartsWith("/notification")).ToList();
foreach (var m in notiMenus)
{
if (!fsql.Select<RoleMenu>().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<Menu>().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<Menu>().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<Menu>().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<Menu>().Where(m => m.Path == menuPath).First();
if (menu != null && !fsql.Select<Menu>().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<Role>().Where(r => r.Code == "admin").First();
if (adminRole != null)
{
var attMenus = fsql.Select<Menu>().Where(m => m.Path.StartsWith("/attendance") || m.Permission.StartsWith("attendance:")).ToList();
foreach (var m in attMenus)
{
if (!fsql.Select<RoleMenu>().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id))
fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows();
}
}
}
/// <summary>数据迁移:旧 RBAC 表 → 新 DDD RBAC 表(幂等,仅迁移尚不存在的记录)</summary>
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}");
}
}
/// <summary>F9 风格 RBAC 前端菜单种子(写入新 sys_rbac_menus 表,驱动前端导航)</summary>
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) VALUES ({sysDirId}, 'UserFilled', '系统管理(RBAC)', 1, 0, 1, 0, 6, '/sys', '', '')");
}
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) VALUES ({id}, '{icon}', '{name}', 2, {sysDirId}, 2, 0, {sort}, '{path}', '{component}', '{permission}')");
}
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) VALUES ({snowflake.CreateId()}, {roleId}, {menuId})");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] RBAC 菜单种子跳过:{ex.Message}");
}
}
/// <summary>为菜单创建权限单元(Permission),并修正旧菜单权限关联(RolePermission.PermissionId 从 Menu.Id 改为 Permission.Id)</summary>
private static void FixMenuPermissions(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
foreach (var menu in fsql.Select<SysMenu>().ToList())
{
// 1. 查或创建菜单权限单元(ResourceType=菜单,ResourceId=Menu.Id)
var perm = fsql.Select<SysPermission>()
.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<SysPermission>().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};");
}
Console.WriteLine("[DbSeeder] 菜单权限单元同步完成");
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 菜单权限单元同步跳过:{ex.Message}");
}
}
}
@@ -0,0 +1,83 @@
using FreeSql;
using FreeSql.Aop;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Departments;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Menus;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users;
using F9WebAiTest.Application.Interfaces.Security;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Application.Interfaces.UOW;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities;
using F9WebAiTest.Domain.Interfaces.SYS.Departments;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Permissions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
using F9WebAiTest.Infrastructure.FreeSqls.Persistence;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Departments;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Menus;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Permissions;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Roles;
using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Users;
using F9WebAiTest.Infrastructure.InterfaceImplements.Security;
using F9WebAiTest.Infrastructure.InterfaceImplements.SnowflakeID;
using F9WebAiTest.Infrastructure.Repositories;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace F9WebAiTest.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
// 创建 FreeSql 实例(SQLite),关闭自动同步结构(只建新表,不修改旧库)
var connStr = configuration.GetConnectionString("Default");
var fsql = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, connStr)
.UseAutoSyncStructure(false)
.Build();
// 软删除全局过滤器:自动应用到新旧两套实体基类
fsql.GlobalFilter.Apply<BaseEntity>("软删除", a => a.IsDeleted == false);
fsql.GlobalFilter.Apply<Entity>("软删除", a => a.IsDeleted == false);
// 枚举统一映射为 int(与旧库 INTEGER 存储保持一致)
fsql.Aop.ConfigEntityProperty += (_, e) =>
{
if (e.Property.PropertyType.IsEnum)
e.ModifyResult.MapType = typeof(int);
};
services.AddSingleton(fsql);
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
// ==================== F9 风格 RBAC(DDD + CQRS)依赖注册 ====================
// 雪花 ID(有状态,单例)
services.AddSingleton<ISnowflakeIdGenerator, SnowflakeIdGenerator>();
// 密码哈希
services.AddScoped<IPasswordHasher, PasswordHasher>();
// 工作单元(事务)
services.AddScoped<IUnitOfWork_F9, UnitOfWork_F9>();
// 写仓储(Domain 接口)
services.AddScoped<IUserWriteRepository, UserWriteRepository>();
services.AddScoped<IUserRoleWriteRepository, UserRoleWriteRepository>();
services.AddScoped<IRoleWriteRepository, RoleWriteRepository>();
services.AddScoped<IRolePermissionWriteRepository, RolePermissionWriteRepository>();
services.AddScoped<IMenuWriteRepository, MenuWriteRepository>();
services.AddScoped<IPermissionWriteRepository, PermissionWriteRepository>();
services.AddScoped<IDepartmentWriteRepository, DepartmentWriteRepository>();
services.AddScoped<IDepartmentUserRepository, DepartmentUserRepository>();
// 读仓储(Application 接口)
services.AddScoped<IUserReadRepository, UserReadRepository>();
services.AddScoped<IRoleReadRepository, RoleReadRepository>();
services.AddScoped<IMenuReadRepository, MenuReadRepository>();
services.AddScoped<IDepartmentReadRepository, DepartmentReadRepository>();
return services;
}
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<RootNamespace>F9WebAiTest.Infrastructure</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\F9WebAiTest.Domain\F9WebAiTest.Domain.csproj" />
<ProjectReference Include="..\F9WebAiTest.Application\F9WebAiTest.Application.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="FreeSql" Version="3.5.213" />
<PackageReference Include="FreeSql.DbContext" Version="3.5.213" />
<PackageReference Include="FreeSql.Provider.Sqlite" Version="3.5.213" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
</ItemGroup>
</Project>
@@ -0,0 +1,46 @@
using FreeSql;
using F9WebAiTest.Application.Interfaces.UOW;
namespace F9WebAiTest.Infrastructure.FreeSqls.Persistence;
/// <summary>
/// 工作单元实现:基于 FreeSql 的 UnitOfWorkManager,事务内 Orm 通过 AsyncLocal 暴露给仓储。
/// 仓储通过 UnitOfWork.CurrentOrm 获取事务感知的 Orm。
/// </summary>
public class UnitOfWork_F9 : IUnitOfWork_F9
{
private readonly IFreeSql _fsql;
private readonly UnitOfWorkManager _uowManager;
private static readonly AsyncLocal<IFreeSql> _currentOrm = new();
/// <summary>当前事务内的 Orm(非事务上下文返回 null)</summary>
public static IFreeSql CurrentOrm => _currentOrm.Value;
public UnitOfWork_F9(IFreeSql fsql)
{
_fsql = fsql;
_uowManager = new UnitOfWorkManager(fsql);
}
public async Task<bool> ExecuteWithUow(Func<Task<bool>> func)
{
using var uow = _uowManager.Begin();
_currentOrm.Value = uow.Orm;
try
{
var result = await func();
if (result) uow.Commit();
else uow.Rollback();
return result;
}
catch
{
uow.Rollback();
throw;
}
finally
{
_currentOrm.Value = null;
}
}
}
@@ -0,0 +1,15 @@
using FreeSql;
using F9WebAiTest.Infrastructure.FreeSqls.Persistence;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories;
/// <summary>F9 风格仓储基类:提供事务感知的 Orm(事务内用事务 Orm,事务外用外部 Orm)</summary>
public abstract class F9RepositoryBase
{
protected readonly IFreeSql _fsql;
protected F9RepositoryBase(IFreeSql fsql) => _fsql = fsql;
/// <summary>事务感知的 Orm</summary>
protected IFreeSql Orm => UnitOfWork_F9.CurrentOrm ?? _fsql;
}
@@ -0,0 +1,17 @@
using FreeSql;
using F9WebAiTest.Application.Dtos.SYS.Departments;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Departments;
using F9WebAiTest.Domain.Entities.SYS.Departments;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Departments;
internal class DepartmentReadRepository : F9RepositoryBase, IDepartmentReadRepository
{
public DepartmentReadRepository(IFreeSql fsql) : base(fsql) { }
public async Task<List<DepartmentReadDto_View>> GetAllAsync()
=> await Orm.Select<Department>()
.Where(x => x.DeletedAt == null)
.OrderBy(x => x.SortOrder)
.ToListAsync(x => new DepartmentReadDto_View(x.Id, x.Code, x.Name, x.ParentId, x.ManageType, x.DutyType, x.Description, x.IsActive, x.SortOrder));
}
@@ -0,0 +1,21 @@
using FreeSql;
using F9WebAiTest.Domain.Entities.SYS.Departments;
using F9WebAiTest.Domain.Interfaces.SYS.Departments;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Departments;
internal class DepartmentUserRepository : F9RepositoryBase, IDepartmentUserRepository
{
public DepartmentUserRepository(IFreeSql fsql) : base(fsql) { }
public async Task<bool> AssignAsync(DepartmentUser depUser)
{
depUser.CreatedAt = DateTime.Now;
return await Orm.Insert(depUser).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> RemoveAsync(long depId, long userId)
=> await Orm.Delete<DepartmentUser>()
.Where(x => x.DepartmentId == depId && x.UserId == userId)
.ExecuteAffrowsAsync() > 0;
}
@@ -0,0 +1,38 @@
using FreeSql;
using F9WebAiTest.Domain.Entities.SYS.Departments;
using F9WebAiTest.Domain.Interfaces.SYS.Departments;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Departments;
internal class DepartmentWriteRepository : F9RepositoryBase, IDepartmentWriteRepository
{
public DepartmentWriteRepository(IFreeSql fsql) : base(fsql) { }
public async Task<Department> GetByIdAsync(long id, CancellationToken cancellationToken)
=> await Orm.Select<Department>().Where(x => x.Id == id ).FirstAsync(cancellationToken);
public async Task<bool> AddAsync(Department dep)
{
dep.CreatedAt = DateTime.Now;
return await Orm.Insert(dep).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> UpdateAsync(Department dep)
{
dep.UpdatedAt = DateTime.Now;
return await Orm.Update<Department>().SetSource(dep).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> SoftDeleteAsync(long id)
=> await Orm.Update<Department>()
.Set(x => x.IsDeleted, true)
.Where(x => x.Id == id )
.ExecuteAffrowsAsync() > 0;
public async Task<bool> ActiveOrDeactive(long id, bool active)
=> await Orm.Update<Department>()
.Set(x => x.IsActive, active)
.Set(x => x.UpdatedAt, DateTime.Now)
.Where(x => x.Id == id)
.ExecuteAffrowsAsync() > 0;
}
@@ -0,0 +1,61 @@
using FreeSql;
using F9WebAiTest.Application.Dtos.SYS.RBAC.Menus;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Menus;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Menus;
internal class MenuReadRepository : F9RepositoryBase, IMenuReadRepository
{
public MenuReadRepository(IFreeSql fsql) : base(fsql) { }
public async Task<MenuReadDto_Menu_Edit?> GetMenuByIdForEditAsync(long id)
=> await Orm.Select<Menu>()
.Where(x => x.Id == id && x.DeletedAt == null)
.FirstAsync(x => new MenuReadDto_Menu_Edit(x.Id, x.Code, x.Name, x.IconSvg, x.MenuType, x.AssemblyName, x.TypeFullName, x.IsActive, x.ParentId, x.SortOrder));
public async Task<bool> ExistMenuByTypeFullnameExcludeIdAsync(string typeFullname, long? excludeId)
{
var query = Orm.Select<Menu>().Where(x => x.TypeFullName == typeFullname && x.DeletedAt == null);
if (excludeId.HasValue)
query = query.Where(x => x.Id != excludeId.Value);
return await query.AnyAsync();
}
public async Task<List<MenuReadDto_Menu_Table>> GetAllMenuAsync(string like, long? parentId)
{
var all = await Orm.Select<Menu>().Where(x => x.DeletedAt == null).ToListAsync();
var dict = all.ToDictionary(x => x.Id, x => x.Name);
var list = all.AsEnumerable();
if (parentId.HasValue)
list = list.Where(x => x.ParentId == parentId.Value);
if (!string.IsNullOrWhiteSpace(like))
list = list.Where(x => x.Name.Contains(like) || x.Code.Contains(like));
return list.OrderBy(x => x.SortOrder).Select(x => ToTable(x, dict)).ToList();
}
public async Task<List<MenuReadDto_Menu_Table>> GetTreeMenuAsync()
{
var all = await Orm.Select<Menu>().Where(x => x.DeletedAt == null).OrderBy(x => x.SortOrder).ToListAsync();
var dict = all.ToDictionary(x => x.Id, x => x.Name);
return BuildTree(all, 0, dict);
}
private static MenuReadDto_Menu_Table ToTable(Menu x, Dictionary<long, string> dict)
=> new(x.Id, x.ParentId, x.Code, x.Name, x.IconSvg, x.MenuType.ToString(),
x.AssemblyName, x.TypeFullName, dict.GetValueOrDefault(x.ParentId, ""), x.Level,
x.IsActive ? "启用" : "禁用", x.SortOrder);
private static List<MenuReadDto_Menu_Table> BuildTree(List<Menu> all, long parentId, Dictionary<long, string> dict)
=> all.Where(x => x.ParentId == parentId).OrderBy(x => x.SortOrder).Select(x =>
{
var children = BuildTree(all, x.Id, dict);
return new MenuReadDto_Menu_Table(
x.Id, x.ParentId, x.Code, x.Name, x.IconSvg, x.MenuType.ToString(),
x.AssemblyName, x.TypeFullName, dict.GetValueOrDefault(x.ParentId, ""), x.Level,
x.IsActive ? "启用" : "禁用", x.SortOrder, false,
children.Count > 0 ? children.ToArray() : default);
}).ToList();
}
@@ -0,0 +1,38 @@
using FreeSql;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Menus;
internal class MenuWriteRepository : F9RepositoryBase, IMenuWriteRepository
{
public MenuWriteRepository(IFreeSql fsql) : base(fsql) { }
public async Task<Menu> GetByIdAsync(long id)
=> await Orm.Select<Menu>().Where(x => x.Id == id ).FirstAsync();
public async Task<bool> AddAsync(Menu menu)
{
menu.CreatedAt = DateTime.Now;
return await Orm.Insert(menu).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> UpdateAsync(Menu menu)
{
menu.UpdatedAt = DateTime.Now;
return await Orm.Update<Menu>().SetSource(menu).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> SoftDeleteAsync(long id)
=> await Orm.Update<Menu>()
.Set(x => x.IsDeleted, true)
.Where(x => x.Id == id )
.ExecuteAffrowsAsync() > 0;
public async Task<bool> ActiveOrDeactive(long id, bool active)
=> await Orm.Update<Menu>()
.Set(x => x.IsActive, active)
.Set(x => x.UpdatedAt, DateTime.Now)
.Where(x => x.Id == id)
.ExecuteAffrowsAsync() > 0;
}
@@ -0,0 +1,27 @@
using FreeSql;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Permissions;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Permissions;
internal class PermissionWriteRepository : F9RepositoryBase, IPermissionWriteRepository
{
public PermissionWriteRepository(IFreeSql fsql) : base(fsql) { }
public async Task<Permission> GetByIdAsync(Permission_Type_Enum permissionType, Resource_Type_Enum resourceType, long resourceId)
=> await Orm.Select<Permission>()
.Where(x => x.PermissionType == permissionType && x.ResourceType == resourceType && x.ResourceId == resourceId )
.FirstAsync();
public async Task<bool> AddRangeAsync(Permission[] entites)
=> await Orm.Insert(entites).ExecuteAffrowsAsync() > 0;
public async Task<bool> DeleteByResourceIdAndTypeAsync(Permission_Type_Enum permissionType, Resource_Type_Enum resourceType, long resourceId)
=> await Orm.Delete<Permission>()
.Where(x => x.PermissionType == permissionType && x.ResourceType == resourceType && x.ResourceId == resourceId)
.ExecuteAffrowsAsync() > 0;
public async Task<bool> DeleteByResourceIdAsync(long resourceId)
=> await Orm.Delete<Permission>().Where(x => x.ResourceId == resourceId).ExecuteAffrowsAsync() > 0;
}
@@ -0,0 +1,22 @@
using FreeSql;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Roles;
internal class RolePermissionWriteRepository : F9RepositoryBase, IRolePermissionWriteRepository
{
public RolePermissionWriteRepository(IFreeSql fsql) : base(fsql) { }
public async Task<RolePermission> GetByIdAsync(long id)
=> await Orm.Select<RolePermission>().Where(x => x.Id == id).FirstAsync();
public async Task<bool> AssignAsync(RolePermission rolePermission)
{
rolePermission.CreatedAt = DateTime.Now;
return await Orm.Insert(rolePermission).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> RemoveAsync(long id)
=> await Orm.Delete<RolePermission>().Where(x => x.Id == id).ExecuteAffrowsAsync() > 0;
}
@@ -0,0 +1,66 @@
using FreeSql;
using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Roles;
internal class RoleReadRepository : F9RepositoryBase, IRoleReadRepository
{
public RoleReadRepository(IFreeSql fsql) : base(fsql) { }
public async Task<RoleReadDto_Edit> GetByIdAsync(long id)
=> await Orm.Select<Role>()
.Where(x => x.Id == id && x.DeletedAt == null)
.FirstAsync(x => new RoleReadDto_Edit(x.Id, x.Name, x.Code, x.SortOrder, x.Description));
public async Task<List<RoleReadDto_View>> GetAllAsync()
{
var roles = await Orm.Select<Role>()
.Where(x => x.DeletedAt == null)
.OrderBy(x => x.SortOrder)
.ToListAsync(x => new { x.Id, x.Name, x.Code, x.SortOrder, x.IsActive });
var roleIds = roles.Select(x => x.Id).ToList();
var permCounts = await GetPermissionCountsAsync(roleIds);
return roles.Select(x =>
{
var c = permCounts.GetValueOrDefault(x.Id);
return new RoleReadDto_View(x.Id, x.Name, x.Code, x.SortOrder, c.Menu, c.Report, c.Dep, x.IsActive);
}).ToList();
}
public async Task<List<RoleReadDto_Cache>> GetAllForCacheAsync()
=> await Orm.Select<Role>()
.Where(x => x.DeletedAt == null)
.OrderBy(x => x.SortOrder)
.ToListAsync(x => new RoleReadDto_Cache(x.Id, x.Name, x.Code, x.SortOrder));
public async Task<List<RolePermissionReadDto_View>> GetRolePermissionsByResourceType(long roleId, Resource_Type_Enum resourceType)
=> await Orm.Select<RolePermission, Permission>()
.InnerJoin((rp, p) => rp.PermissionId == p.Id && p.DeletedAt == null)
.Where((rp, p) => rp.RoleId == roleId && p.ResourceType == resourceType)
.ToListAsync((rp, p) => new RolePermissionReadDto_View(rp.Id, rp.RoleId, rp.PermissionId, p.ResourceId, p.PermissionType));
private async Task<Dictionary<long, (int Menu, int Report, int Dep)>> GetPermissionCountsAsync(List<long> roleIds)
{
var result = new Dictionary<long, (int, int, int)>();
if (roleIds.Count == 0) return result;
var perms = await Orm.Select<RolePermission, Permission>()
.InnerJoin((rp, p) => rp.PermissionId == p.Id && p.DeletedAt == null)
.Where((rp, p) => roleIds.Contains(rp.RoleId))
.ToListAsync((rp, p) => new { rp.RoleId, p.ResourceType });
foreach (var g in perms.GroupBy(x => x.RoleId))
{
result[g.Key] = (
g.Count(x => x.ResourceType == Resource_Type_Enum.菜单),
g.Count(x => x.ResourceType == Resource_Type_Enum.报表),
g.Count(x => x.ResourceType == Resource_Type_Enum.部门));
}
return result;
}
}
@@ -0,0 +1,38 @@
using FreeSql;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Roles;
internal class RoleWriteRepository : F9RepositoryBase, IRoleWriteRepository
{
public RoleWriteRepository(IFreeSql fsql) : base(fsql) { }
public async Task<Role> GetByIdAsync(long id)
=> await Orm.Select<Role>().Where(x => x.Id == id ).FirstAsync();
public async Task<bool> AddAsync(Role role)
{
role.CreatedAt = DateTime.Now;
return await Orm.Insert(role).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> UpdateAsync(Role role)
{
role.UpdatedAt = DateTime.Now;
return await Orm.Update<Role>().SetSource(role).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> SoftDeleteAsync(long id)
=> await Orm.Update<Role>()
.Set(x => x.IsDeleted, true)
.Where(x => x.Id == id )
.ExecuteAffrowsAsync() > 0;
public async Task<bool> ActiveOrDeactive(long id, bool active)
=> await Orm.Update<Role>()
.Set(x => x.IsActive, active)
.Set(x => x.UpdatedAt, DateTime.Now)
.Where(x => x.Id == id)
.ExecuteAffrowsAsync() > 0;
}
@@ -0,0 +1,96 @@
using FreeSql;
using F9WebAiTest.Application.Dtos.Common;
using F9WebAiTest.Application.Dtos.SYS.RBAC.Users;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users;
using F9WebAiTest.Domain.Entities.SYS.Departments;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Users;
internal class UserReadRepository : F9RepositoryBase, IUserReadRepository
{
public UserReadRepository(IFreeSql fsql) : base(fsql) { }
public async Task<UserReadDto_Login> GetByLoginNameAsync(string loginName)
=> await Orm.Select<User>()
.Where(x => x.LoginName == loginName && x.DeletedAt == null)
.FirstAsync(x => new UserReadDto_Login(x.Id, x.UserName, x.LoginName, x.PasswordHash, x.DepartmentId));
public async Task<UserReadDto_Login> GetByPhoneAsync(string phone)
=> await Orm.Select<User>()
.Where(x => x.PhoneNumber == phone && x.DeletedAt == null)
.FirstAsync(x => new UserReadDto_Login(x.Id, x.UserName, x.LoginName, x.PasswordHash, x.DepartmentId));
public async Task<UserReadDto_Edit> GetByIdAsync(long id)
=> await Orm.Select<User>()
.Where(x => x.Id == id && x.DeletedAt == null)
.FirstAsync(x => new UserReadDto_Edit(x.Id, x.UserName, x.PhoneNumber, x.LoginName, x.DepartmentId, x.SortOrder));
public async Task<List<UserReadDto_Edit>> GetByIdsAsync(List<long> ids)
=> await Orm.Select<User>()
.Where(x => ids.Contains(x.Id) && x.DeletedAt == null)
.ToListAsync(x => new UserReadDto_Edit(x.Id, x.UserName, x.PhoneNumber, x.LoginName, x.DepartmentId, x.SortOrder));
public async Task<PagedResult<UserReadDto_View>> GetPagedAsync(long departmentId, string like, int page = 1, int pageSize = 10)
{
var query = Orm.Select<User, Department>()
.LeftJoin((u, d) => u.DepartmentId == d.Id)
.Where((u, d) => u.DeletedAt == null);
if (departmentId > 0)
query = query.Where((u, d) => u.DepartmentId == departmentId);
if (!string.IsNullOrWhiteSpace(like))
query = query.Where((u, d) => u.UserName.Contains(like) || u.LoginName.Contains(like) || u.PhoneNumber.Contains(like));
var total = await query.CountAsync();
var list = await query
.OrderBy((u, d) => u.SortOrder)
.Page(page, pageSize)
.ToListAsync((u, d) => new
{
u.Id, u.UserName, u.PhoneNumber, u.LoginName,
DepartmentName = d.Name,
u.LastLoginAt, u.SortOrder, u.IsActive
});
// 批量查角色名,避免 N+1
var userIds = list.Select(x => x.Id).ToList();
var roleDict = await GetRoleNameDictAsync(userIds);
var items = list.Select(x => new UserReadDto_View(
x.Id, x.UserName, x.PhoneNumber, x.LoginName,
x.DepartmentName,
roleDict.TryGetValue(x.Id, out var roleName) ? roleName : "",
x.LastLoginAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "",
x.SortOrder, x.IsActive)).ToList();
return new PagedResult<UserReadDto_View>(items, page, pageSize, total);
}
public async Task<List<UserRoleReadDto_Cache>> GetUserRolesByUserId(long userId)
=> await Orm.Select<UserRole, Role>()
.InnerJoin((x, y) => x.RoleId == y.Id && y.DeletedAt == null)
.Where((x, y) => x.UserId == userId)
.ToListAsync((x, y) => new UserRoleReadDto_Cache(x.Id, x.UserId, x.RoleId, y.Name));
public async Task<List<UserReadDto_Edit>> GetByRoleIdsAsync(List<long> roleIds)
=> await Orm.Select<User, UserRole, Role>()
.InnerJoin((u, x, y) => u.Id == x.UserId)
.InnerJoin((u, x, y) => x.RoleId == y.Id && y.DeletedAt == null)
.Where((u, x, y) => roleIds.Contains(x.RoleId) && u.DeletedAt == null)
.ToListAsync((u, x, y) => new UserReadDto_Edit(u.Id, u.UserName, u.PhoneNumber, u.LoginName, u.DepartmentId, u.SortOrder));
private async Task<Dictionary<long, string>> GetRoleNameDictAsync(List<long> userIds)
{
if (userIds.Count == 0) return new Dictionary<long, string>();
var list = await Orm.Select<UserRole, Role>()
.InnerJoin((x, y) => x.RoleId == y.Id && y.DeletedAt == null)
.Where((x, y) => userIds.Contains(x.UserId))
.ToListAsync((x, y) => new { x.UserId, y.Name });
return list.GroupBy(x => x.UserId)
.ToDictionary(g => g.Key, g => string.Join(",", g.Select(v => v.Name)));
}
}
@@ -0,0 +1,21 @@
using FreeSql;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Users;
internal class UserRoleWriteRepository : F9RepositoryBase, IUserRoleWriteRepository
{
public UserRoleWriteRepository(IFreeSql fsql) : base(fsql) { }
public async Task<bool> AssignAsync(UserRole userRole)
{
userRole.CreatedAt = DateTime.Now;
return await Orm.Insert(userRole).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> RemoveAsync(long userid, long roleId)
=> await Orm.Delete<UserRole>()
.Where(x => x.UserId == userid && x.RoleId == roleId)
.ExecuteAffrowsAsync() > 0;
}
@@ -0,0 +1,51 @@
using FreeSql;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users;
namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Users;
internal class UserWriteRepository : F9RepositoryBase, IUserWriteRepository
{
public UserWriteRepository(IFreeSql fsql) : base(fsql) { }
public async Task<User> GetByIdAsync(long id)
=> await Orm.Select<User>().Where(x => x.Id == id ).FirstAsync();
public async Task<bool> AddAsync(User user)
{
user.CreatedAt = DateTime.Now;
return await Orm.Insert(user).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> UpdateAsync(User user)
{
user.UpdatedAt = DateTime.Now;
return await Orm.Update<User>().SetSource(user).ExecuteAffrowsAsync() > 0;
}
public async Task<bool> SoftDeleteAsync(long id)
=> await Orm.Update<User>()
.Set(x => x.IsDeleted, true)
.Where(x => x.Id == id )
.ExecuteAffrowsAsync() > 0;
public async Task<bool> ActiveOrDeactive(long id, bool active)
=> await Orm.Update<User>()
.Set(x => x.IsActive, active)
.Set(x => x.UpdatedAt, DateTime.Now)
.Where(x => x.Id == id)
.ExecuteAffrowsAsync() > 0;
public async Task<bool> UpdatePassworkdAsync(long id, string passwordHash)
=> await Orm.Update<User>()
.Set(x => x.PasswordHash, passwordHash)
.Set(x => x.UpdatedAt, DateTime.Now)
.Where(x => x.Id == id)
.ExecuteAffrowsAsync() > 0;
public async Task<bool> UpdateLastLoginAtAsync(long id, DateTime dt)
=> await Orm.Update<User>()
.Set(x => x.LastLoginAt, dt)
.Where(x => x.Id == id)
.ExecuteAffrowsAsync() > 0;
}
@@ -0,0 +1,27 @@
using System.Security.Cryptography;
using System.Text;
using F9WebAiTest.Application.Interfaces.Security;
namespace F9WebAiTest.Infrastructure.InterfaceImplements.Security;
/// <summary>密码哈希服务实现(BCrypt)</summary>
public class PasswordHasher : IPasswordHasher
{
public string Hash256(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input ?? ""));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
public string GetMd5Hash(string input)
{
var bytes = MD5.HashData(Encoding.UTF8.GetBytes(input ?? ""));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
public string HashPassword(string password)
=> BCrypt.Net.BCrypt.HashPassword(password);
public bool VerifyPassword(string password, string hashedPassword)
=> BCrypt.Net.BCrypt.Verify(password, hashedPassword);
}
@@ -0,0 +1,59 @@
using F9WebAiTest.Application.Interfaces.SnowflakeID;
namespace F9WebAiTest.Infrastructure.InterfaceImplements.SnowflakeID;
/// <summary>
/// 雪花 ID 生成器(自实现,41 时间戳 + 10 worker + 12 序列)。
/// 参考 BBIT.F9.Infrastructure.InterfaceImplements.SnowflakeID.SnowflakeIdGenerator
/// </summary>
public class SnowflakeIdGenerator : ISnowflakeIdGenerator
{
private readonly object _lock = new();
private readonly long _workerId;
private readonly long _epoch;
private long _lastTimestamp = -1L;
private long _sequence;
private const long WorkerIdBits = 10L;
private const long SequenceBits = 12L;
private const long WorkerIdShift = SequenceBits;
private const long TimestampShift = SequenceBits + WorkerIdBits;
private const long SequenceMask = -1L ^ (-1L << (int)SequenceBits);
public SnowflakeIdGenerator()
{
_workerId = 1; // 单节点默认 workerId = 1
_epoch = new DateTimeOffset(2025, 10, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeMilliseconds();
}
public long CreateId()
{
lock (_lock)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - _epoch;
if (timestamp < _lastTimestamp)
throw new InvalidOperationException("时钟回拨,拒绝生成 ID");
if (timestamp == _lastTimestamp)
{
_sequence = (_sequence + 1) & SequenceMask;
if (_sequence == 0)
{
// 序列溢出,等待下一毫秒
while (timestamp <= _lastTimestamp)
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - _epoch;
}
}
else
{
_sequence = 0;
}
_lastTimestamp = timestamp;
return (timestamp << (int)TimestampShift)
| (_workerId << (int)WorkerIdShift)
| _sequence;
}
}
}
@@ -0,0 +1,88 @@
using System.Linq.Expressions;
using FreeSql;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Infrastructure.Repositories;
/// <summary>通用仓储实现(基于 FreeSql)</summary>
public class Repository<T> : IRepository<T> where T : class, IEntity
{
protected readonly IFreeSql _fsql;
public Repository(IFreeSql fsql)
{
_fsql = fsql;
}
public virtual async Task<T> GetByIdAsync(long id)
=> await _fsql.Select<T>().Where(e => e.Id == id).FirstAsync();
public virtual async Task<List<T>> GetAllAsync()
=> await _fsql.Select<T>().ToListAsync();
public virtual async Task<List<T>> FindAsync(Expression<Func<T, bool>> predicate)
=> await _fsql.Select<T>().Where(predicate).ToListAsync();
public virtual async Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate)
=> await _fsql.Select<T>().Where(predicate).FirstAsync();
public virtual async Task<bool> AnyAsync(Expression<Func<T, bool>> predicate)
=> await _fsql.Select<T>().Where(predicate).AnyAsync();
public virtual async Task<long> CountAsync(Expression<Func<T, bool>> predicate)
=> await _fsql.Select<T>().Where(predicate).CountAsync();
public virtual async Task<T> AddAsync(T entity)
{
entity.CreatedAt = DateTime.Now;
await _fsql.Insert(entity).ExecuteAffrowsAsync();
// FreeSql 在 SQLite 下不回填自增主键,手动用 last_insert_rowid 获取并回填
if (entity.Id == 0)
{
entity.Id = Convert.ToInt64(await _fsql.Ado.ExecuteScalarAsync("SELECT last_insert_rowid();"));
}
return entity;
}
public virtual async Task AddRangeAsync(IEnumerable<T> entities)
{
foreach (var e in entities) e.CreatedAt = DateTime.Now;
await _fsql.Insert(entities).ExecuteAffrowsAsync();
}
public virtual async Task UpdateAsync(T entity)
{
entity.UpdatedAt = DateTime.Now;
await _fsql.Update<T>().SetSource(entity).ExecuteAffrowsAsync();
}
public virtual async Task DeleteAsync(T entity)
{
entity.IsDeleted = true;
entity.UpdatedAt = DateTime.Now;
await _fsql.Update<T>().SetSource(entity).ExecuteAffrowsAsync();
}
public virtual async Task DeleteRangeAsync(IEnumerable<T> entities)
{
foreach (var e in entities)
{
e.IsDeleted = true;
e.UpdatedAt = DateTime.Now;
}
await _fsql.Update<T>().SetSource(entities).ExecuteAffrowsAsync();
}
public virtual async Task HardDeleteAsync(T entity)
=> await _fsql.Delete<T>().Where(e => e.Id == entity.Id).ExecuteAffrowsAsync();
public virtual async Task HardDeleteRangeAsync(IEnumerable<T> entities)
{
var ids = entities.Select(e => e.Id).ToList();
if (ids.Count > 0)
await _fsql.Delete<T>().Where(e => ids.Contains(e.Id)).ExecuteAffrowsAsync();
}
public virtual Task<int> SaveChangesAsync()
=> Task.FromResult(1); // FreeSql 即时提交,无 SaveChanges 语义,保持接口兼容
}