195 lines
6.5 KiB
C#
195 lines
6.5 KiB
C#
using Jiaowu.Api.Domain.Academic;
|
||
using Jiaowu.Api.Domain.Identity;
|
||
using Microsoft.AspNetCore.Identity;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace Jiaowu.Api.Infrastructure.Persistence;
|
||
|
||
public sealed class DatabaseInitializer(
|
||
AppDbContext db,
|
||
RoleManager<ApplicationRole> roleManager,
|
||
UserManager<ApplicationUser> userManager,
|
||
DevelopmentSqliteMigrator sqliteMigrator,
|
||
DatabaseOptions databaseOptions,
|
||
IConfiguration configuration,
|
||
IHostEnvironment environment,
|
||
ILogger<DatabaseInitializer> logger)
|
||
{
|
||
public async Task InitializeAsync(bool migrateOnly = false)
|
||
{
|
||
if (environment.IsDevelopment())
|
||
{
|
||
if (migrateOnly)
|
||
{
|
||
throw new InvalidOperationException(
|
||
"--migrate-only 仅用于非 Development 环境的 MySQL 数据库。");
|
||
}
|
||
|
||
await db.Database.EnsureCreatedAsync();
|
||
await sqliteMigrator.MigrateAsync();
|
||
}
|
||
else if (migrateOnly || databaseOptions.ApplyMigrationsOnStartup)
|
||
{
|
||
await db.Database.MigrateAsync();
|
||
}
|
||
else
|
||
{
|
||
var pendingMigrations = (await db.Database.GetPendingMigrationsAsync()).ToArray();
|
||
if (pendingMigrations.Length > 0)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"数据库还有 {pendingMigrations.Length} 个待执行迁移。请在发布前运行 " +
|
||
"'Jiaowu.Api --migrate-only',或显式配置 " +
|
||
"Database:ApplyMigrationsOnStartup=true。");
|
||
}
|
||
}
|
||
|
||
if (migrateOnly)
|
||
{
|
||
logger.LogInformation("数据库迁移已完成。");
|
||
return;
|
||
}
|
||
|
||
await SeedRolesAsync();
|
||
await SeedAdministratorAsync();
|
||
await SeedCourseCategoriesAsync();
|
||
}
|
||
|
||
private async Task SeedRolesAsync()
|
||
{
|
||
var roleDefinitions = new Dictionary<string, (string Description, DataScope Scope)>
|
||
{
|
||
[SystemRoles.SuperAdmin] = ("系统配置与全部数据管理", DataScope.All),
|
||
[SystemRoles.AcademicAdmin] = ("校级教务管理", DataScope.All),
|
||
[SystemRoles.CollegeAdmin] = ("院系教务管理", DataScope.College),
|
||
[SystemRoles.Teacher] = ("教师教学工作台", DataScope.Self),
|
||
[SystemRoles.Counselor] = ("辅导员与班级管理", DataScope.Class),
|
||
[SystemRoles.Student] = ("学生自助服务", DataScope.Self),
|
||
[SystemRoles.Leader] = ("校级统计查看", DataScope.All)
|
||
};
|
||
|
||
foreach (var (name, definition) in roleDefinitions)
|
||
{
|
||
if (await roleManager.RoleExistsAsync(name))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var result = await roleManager.CreateAsync(new ApplicationRole
|
||
{
|
||
Name = name,
|
||
Description = definition.Description,
|
||
DataScope = definition.Scope
|
||
});
|
||
|
||
EnsureSucceeded(result, $"创建角色 {name}");
|
||
}
|
||
}
|
||
|
||
private async Task SeedAdministratorAsync()
|
||
{
|
||
var userName = configuration["SeedAdmin:UserName"];
|
||
var password = configuration["SeedAdmin:Password"];
|
||
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password))
|
||
{
|
||
if (!environment.IsDevelopment())
|
||
{
|
||
logger.LogWarning("未配置 SeedAdmin,生产环境不会创建默认管理员。");
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
var user = await userManager.FindByNameAsync(userName);
|
||
if (user is null)
|
||
{
|
||
user = new ApplicationUser
|
||
{
|
||
UserName = userName,
|
||
DisplayName = configuration["SeedAdmin:DisplayName"] ?? "系统管理员",
|
||
LockoutEnabled = true,
|
||
IsEnabled = true
|
||
};
|
||
|
||
EnsureSucceeded(await userManager.CreateAsync(user, password), "创建初始管理员");
|
||
}
|
||
|
||
if (!user.LockoutEnabled)
|
||
{
|
||
user.LockoutEnabled = true;
|
||
EnsureSucceeded(await userManager.UpdateAsync(user), "启用管理员登录保护");
|
||
}
|
||
|
||
if (!await userManager.IsInRoleAsync(user, SystemRoles.SuperAdmin))
|
||
{
|
||
EnsureSucceeded(
|
||
await userManager.AddToRoleAsync(user, SystemRoles.SuperAdmin),
|
||
"授予超级管理员角色");
|
||
}
|
||
}
|
||
|
||
private async Task SeedCourseCategoriesAsync()
|
||
{
|
||
var defaults = new[]
|
||
{
|
||
("BASIC", "基础课程", 10),
|
||
("MORAL", "德育课程", 20),
|
||
("AESTHETIC", "美育课程", 30),
|
||
("LABOR", "劳动教育", 40),
|
||
("INNOVATION", "创新创业", 50),
|
||
("ENGLISH", "大学英语", 60),
|
||
("SPORTS", "大学体育", 70),
|
||
("MILITARY", "国防教育", 80),
|
||
("MAJOR", "专业教育", 90),
|
||
("PRACTICE", "实践教学", 100)
|
||
};
|
||
var existingCodes = (await db.CourseCategories
|
||
.Select(x => x.Code)
|
||
.ToListAsync())
|
||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var (code, name, sortOrder) in defaults)
|
||
{
|
||
if (existingCodes.Contains(code))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
db.CourseCategories.Add(new CourseCategory
|
||
{
|
||
Code = code,
|
||
Name = name,
|
||
SortOrder = sortOrder
|
||
});
|
||
}
|
||
await db.SaveChangesAsync();
|
||
|
||
var categories = await db.CourseCategories
|
||
.ToDictionaryAsync(x => x.Code, StringComparer.OrdinalIgnoreCase);
|
||
var uncategorizedCourses = await db.Courses
|
||
.Where(x => x.CourseCategoryId == null)
|
||
.ToListAsync();
|
||
foreach (var course in uncategorizedCourses)
|
||
{
|
||
var categoryCode = course.Nature switch
|
||
{
|
||
CourseNature.Practice => "PRACTICE",
|
||
CourseNature.MajorRequired or CourseNature.MajorElective => "MAJOR",
|
||
_ => "BASIC"
|
||
};
|
||
course.CourseCategoryId = categories[categoryCode].Id;
|
||
}
|
||
await db.SaveChangesAsync();
|
||
}
|
||
|
||
private static void EnsureSucceeded(IdentityResult result, string action)
|
||
{
|
||
if (result.Succeeded)
|
||
{
|
||
return;
|
||
}
|
||
|
||
throw new InvalidOperationException(
|
||
$"{action}失败:{string.Join(";", result.Errors.Select(x => x.Description))}");
|
||
}
|
||
}
|