新增显式命令:--seed-demo-data --confirm-production-demo-data

仅允许非 Development 环境运行。
只允许写入没有业务数据的独立空库。
演示数据使用单个 InnoDB 事务,失败整体回滚;迁移 DDL 仍单独执行,因为 MySQL 8.4 的 DDL 会隐式提交。MySQL 事务文档、隐式提交说明
不包含通用测试密码或学生、教师登录账号。
包含校区、学期、30 间教室、16 个学院、52 个专业、104 个班级、128 名教师、3,640 名学生和148门课程。
This commit is contained in:
2026-07-25 20:58:51 +08:00 Unverified
parent 08b81eec42
commit 2ac873c347
4 changed files with 897 additions and 3 deletions
@@ -0,0 +1,696 @@
using System.Data;
using Jiaowu.Api.Domain.Academic;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Infrastructure.Persistence;
public sealed class DemoDataSeeder(
AppDbContext db,
ILogger<DemoDataSeeder> logger)
{
private const int Grade = 2026;
private const int ClassesPerMajor = 2;
private const int StudentsPerClass = 35;
private const int TeachersPerCollege = 8;
public async Task SeedAsync(CancellationToken cancellationToken = default)
{
var executionStrategy = db.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
db.ChangeTracker.Clear();
await using var transaction = await db.Database.BeginTransactionAsync(
IsolationLevel.Serializable,
cancellationToken);
await EnsureDatabaseIsEmptyAsync(cancellationToken);
var campus = new Campus
{
Code = "MAIN",
Name = "主校区",
Address = "大学路 1 号"
};
db.Campuses.Add(campus);
await db.SaveChangesAsync(cancellationToken);
await SeedAcademicTermAsync(cancellationToken);
await SeedClassroomsAsync(campus.Id, cancellationToken);
await SeedCollegesAsync(campus.Id, cancellationToken);
await SeedMajorsAsync(cancellationToken);
await SeedClassesAsync(cancellationToken);
await SeedTeachersAsync(cancellationToken);
await SeedStudentsAsync(cancellationToken);
await SeedCoursesAsync(cancellationToken);
await SeedTeacherCourseApplicationsAsync(cancellationToken);
await LogSummaryAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
});
}
private async Task EnsureDatabaseIsEmptyAsync(CancellationToken cancellationToken)
{
var occupiedAreas = new List<string>();
if (await db.Campuses.AnyAsync(cancellationToken)) occupiedAreas.Add("校区");
if (await db.Colleges.AnyAsync(cancellationToken)) occupiedAreas.Add("学院");
if (await db.Majors.AnyAsync(cancellationToken)) occupiedAreas.Add("专业");
if (await db.AdministrativeClasses.AnyAsync(cancellationToken)) occupiedAreas.Add("行政班");
if (await db.Teachers.AnyAsync(cancellationToken)) occupiedAreas.Add("教师");
if (await db.Students.AnyAsync(cancellationToken)) occupiedAreas.Add("学生");
if (await db.Courses.AnyAsync(cancellationToken)) occupiedAreas.Add("课程");
if (await db.AcademicTerms.AnyAsync(cancellationToken)) occupiedAreas.Add("学期");
if (await db.Buildings.AnyAsync(cancellationToken)) occupiedAreas.Add("教学楼");
if (await db.Classrooms.AnyAsync(cancellationToken)) occupiedAreas.Add("教室");
if (await db.TeachingTasks.AnyAsync(cancellationToken)) occupiedAreas.Add("教学任务");
if (occupiedAreas.Count > 0)
{
throw new InvalidOperationException(
"演示数据只允许写入没有业务数据的新数据库。检测到已有:" +
string.Join("、", occupiedAreas) +
"。请创建独立空库,不要清理或覆盖现有生产数据。");
}
}
private async Task SeedAcademicTermAsync(CancellationToken cancellationToken)
{
db.AcademicTerms.Add(new AcademicTerm
{
Code = "2026-2027-1",
Name = "2026—2027 学年第一学期",
AcademicYear = "2026-2027",
Season = TermSeason.Autumn,
StartDate = new DateOnly(2026, 9, 7),
EndDate = new DateOnly(2027, 1, 17),
IsCurrent = true
});
await db.SaveChangesAsync(cancellationToken);
}
private async Task SeedClassroomsAsync(
Guid campusId,
CancellationToken cancellationToken)
{
var building = new Building
{
Code = "J1",
Name = "第一教学楼",
CampusId = campusId
};
db.Buildings.Add(building);
for (var floor = 1; floor <= 5; floor++)
{
for (var room = 1; room <= 6; room++)
{
var roomNumber = floor * 100 + room;
db.Classrooms.Add(new Classroom
{
Code = $"J1-{roomNumber}",
Name = $"J1-{roomNumber}",
BuildingId = building.Id,
Capacity = room % 3 == 0 ? 120 : 60,
RoomType = room % 3 == 0 ? "多媒体阶梯教室" : "多媒体教室",
Equipment = "投影、扩音、网络"
});
}
}
await db.SaveChangesAsync(cancellationToken);
}
private async Task SeedCollegesAsync(Guid campusId, CancellationToken cancellationToken)
{
var existingCodes = (await db.Colleges
.Select(x => x.Code)
.ToListAsync(cancellationToken))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var additions = CollegeDefinitions
.Where(x => !existingCodes.Contains(x.Code))
.Select((x, index) => new College
{
Code = x.Code,
Name = x.Name,
ShortName = x.ShortName,
CampusId = campusId,
SortOrder = (index + 1) * 10
})
.ToList();
if (additions.Count == 0) return;
db.Colleges.AddRange(additions);
await db.SaveChangesAsync(cancellationToken);
}
private async Task SeedMajorsAsync(CancellationToken cancellationToken)
{
var colleges = await db.Colleges
.ToDictionaryAsync(x => x.Code, StringComparer.OrdinalIgnoreCase, cancellationToken);
var existingCodes = (await db.Majors
.Select(x => x.Code)
.ToListAsync(cancellationToken))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var additions = new List<Major>();
foreach (var collegeDefinition in CollegeDefinitions)
{
if (!colleges.TryGetValue(collegeDefinition.Code, out var college)) continue;
for (var index = 0; index < collegeDefinition.Majors.Count; index++)
{
var definition = collegeDefinition.Majors[index];
if (existingCodes.Contains(definition.Code)) continue;
additions.Add(new Major
{
Code = definition.Code,
Name = definition.Name,
CollegeId = college.Id,
DegreeType = definition.DegreeType,
SchoolingYears = definition.SchoolingYears,
SortOrder = (index + 1) * 10
});
}
}
if (additions.Count == 0) return;
db.Majors.AddRange(additions);
await db.SaveChangesAsync(cancellationToken);
}
private async Task SeedClassesAsync(CancellationToken cancellationToken)
{
var targetMajorCodes = CollegeDefinitions
.SelectMany(x => x.Majors)
.Select(x => x.Code)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var majors = await db.Majors
.Where(x => targetMajorCodes.Contains(x.Code))
.OrderBy(x => x.Code)
.ToListAsync(cancellationToken);
var existingClasses = await db.AdministrativeClasses
.Where(x => x.Grade == Grade && targetMajorCodes.Contains(x.Major!.Code))
.OrderBy(x => x.Code)
.ToListAsync(cancellationToken);
var existingCodes = (await db.AdministrativeClasses
.Select(x => x.Code)
.ToListAsync(cancellationToken))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var additions = new List<AdministrativeClass>();
foreach (var major in majors)
{
var currentCount = existingClasses.Count(x => x.MajorId == major.Id);
for (var section = currentCount + 1; section <= ClassesPerMajor; section++)
{
var code = CreateAvailableCode(
$"{major.Code}-{Grade}-{section:D2}",
existingCodes);
additions.Add(new AdministrativeClass
{
Code = code,
Name = $"{major.Name}{Grade}级{section}班",
MajorId = major.Id,
Grade = Grade,
CounselorName = $"{CounselorSurnames[(section + major.Code.Length) % CounselorSurnames.Length]}老师",
SortOrder = section * 10
});
}
}
if (additions.Count == 0) return;
db.AdministrativeClasses.AddRange(additions);
await db.SaveChangesAsync(cancellationToken);
}
private async Task SeedTeachersAsync(CancellationToken cancellationToken)
{
var collegeCodes = CollegeDefinitions
.Select(x => x.Code)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var colleges = await db.Colleges
.Where(x => collegeCodes.Contains(x.Code))
.OrderBy(x => x.Code)
.ToListAsync(cancellationToken);
var existingTeachers = await db.Teachers
.Where(x => colleges.Select(c => c.Id).Contains(x.CollegeId))
.ToListAsync(cancellationToken);
var existingNumbers = (await db.Teachers
.Select(x => x.TeacherNumber)
.ToListAsync(cancellationToken))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var additions = new List<Teacher>();
for (var collegeIndex = 0; collegeIndex < colleges.Count; collegeIndex++)
{
var college = colleges[collegeIndex];
var currentCount = existingTeachers.Count(x => x.CollegeId == college.Id);
for (var position = currentCount + 1; position <= TeachersPerCollege; position++)
{
var teacherNumber = CreateAvailableCode(
$"T26{collegeIndex + 1:D2}{position:D3}",
existingNumbers);
additions.Add(new Teacher
{
TeacherNumber = teacherNumber,
Name = BuildPersonName(collegeIndex, position),
Gender = position % 2 == 0 ? Gender.Female : Gender.Male,
CollegeId = college.Id,
Title = TeacherTitles[(position - 1) % TeacherTitles.Length],
Status = TeacherStatus.Active,
HireDate = new DateOnly(2012 + position, 7, 1),
Email = $"{teacherNumber.ToLowerInvariant()}@example.edu.cn",
Notes = "生产演示环境批量测试教师。"
});
}
}
if (additions.Count == 0) return;
db.Teachers.AddRange(additions);
await db.SaveChangesAsync(cancellationToken);
}
private async Task SeedStudentsAsync(CancellationToken cancellationToken)
{
var targetMajorCodes = CollegeDefinitions
.SelectMany(x => x.Majors)
.Select(x => x.Code)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var classes = await db.AdministrativeClasses
.Where(x => x.Grade == Grade && targetMajorCodes.Contains(x.Major!.Code))
.OrderBy(x => x.Code)
.ToListAsync(cancellationToken);
var classIds = classes.Select(x => x.Id).ToHashSet();
var existingStudents = await db.Students
.Where(x => classIds.Contains(x.AdministrativeClassId))
.ToListAsync(cancellationToken);
var existingNumbers = (await db.Students
.Select(x => x.StudentNumber)
.ToListAsync(cancellationToken))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var additions = new List<Student>();
for (var classIndex = 0; classIndex < classes.Count; classIndex++)
{
var administrativeClass = classes[classIndex];
var existingCount = existingStudents.Count(x =>
x.AdministrativeClassId == administrativeClass.Id);
var candidate = 1;
while (existingCount + additions.Count(x =>
x.AdministrativeClassId == administrativeClass.Id) < StudentsPerClass)
{
var studentNumber = $"{Grade}{classIndex + 1:D3}{candidate:D3}";
candidate++;
if (!existingNumbers.Add(studentNumber)) continue;
additions.Add(new Student
{
StudentNumber = studentNumber,
Name = BuildPersonName(classIndex, candidate + 5),
Gender = candidate % 2 == 0 ? Gender.Male : Gender.Female,
AdministrativeClassId = administrativeClass.Id,
EnrollmentYear = Grade,
EnrollmentDate = new DateOnly(Grade, 9, 7),
DateOfBirth = new DateOnly(2007 + candidate % 2, candidate % 12 + 1,
candidate % 27 + 1),
Status = StudentStatus.Active,
Email = $"{studentNumber}@student.example.edu.cn",
Notes = "生产演示环境批量测试学生。"
});
}
}
if (additions.Count == 0) return;
db.Students.AddRange(additions);
await db.SaveChangesAsync(cancellationToken);
}
private async Task SeedCoursesAsync(CancellationToken cancellationToken)
{
var colleges = await db.Colleges
.ToDictionaryAsync(x => x.Code, StringComparer.OrdinalIgnoreCase, cancellationToken);
var categories = await db.CourseCategories
.ToDictionaryAsync(x => x.Code, StringComparer.OrdinalIgnoreCase, cancellationToken);
var existingCodes = (await db.Courses
.Select(x => x.Code)
.ToListAsync(cancellationToken))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var additions = new List<Course>();
foreach (var definition in PublicCourseDefinitions)
{
if (existingCodes.Contains(definition.Code) ||
!colleges.TryGetValue(definition.CollegeCode, out var college) ||
!categories.TryGetValue(definition.CategoryCode, out var category))
continue;
additions.Add(new Course
{
Code = definition.Code,
Name = definition.Name,
CollegeId = college.Id,
CourseCategoryId = category.Id,
Credits = definition.Credits,
TotalHours = definition.TotalHours,
LectureHours = definition.LectureHours,
PracticeHours = definition.TotalHours - definition.LectureHours,
Nature = definition.Nature,
AssessmentMethod = definition.AssessmentMethod,
Description = "生产演示环境公共课程测试数据。"
});
}
foreach (var collegeDefinition in CollegeDefinitions)
{
if (!colleges.TryGetValue(collegeDefinition.Code, out var college)) continue;
for (var index = 0; index < collegeDefinition.ProfessionalCourses.Count; index++)
{
var code = $"{collegeDefinition.Code}D{index + 1:D2}";
if (existingCodes.Contains(code)) continue;
var nature = index switch
{
< 4 => CourseNature.MajorRequired,
< 6 => CourseNature.MajorElective,
_ => CourseNature.Practice
};
var categoryCode = nature == CourseNature.Practice ? "PRACTICE" : "MAJOR";
additions.Add(new Course
{
Code = code,
Name = collegeDefinition.ProfessionalCourses[index],
CollegeId = college.Id,
CourseCategoryId = categories[categoryCode].Id,
Credits = nature switch
{
CourseNature.MajorRequired => index % 2 == 0 ? 3m : 3.5m,
_ => 2m
},
TotalHours = nature switch
{
CourseNature.MajorRequired => index % 2 == 0 ? 48 : 56,
CourseNature.MajorElective => 32,
_ => 48
},
LectureHours = nature == CourseNature.Practice ? 8 :
nature == CourseNature.MajorElective ? 24 :
index % 2 == 0 ? 40 : 48,
PracticeHours = nature == CourseNature.Practice ? 40 :
nature == CourseNature.MajorElective ? 8 : 8,
Nature = nature,
AssessmentMethod = nature == CourseNature.Practice
? AssessmentMethod.Assessment
: AssessmentMethod.Examination,
Description = $"由{collegeDefinition.Name}开设的专业课程测试数据。"
});
}
}
if (additions.Count == 0) return;
db.Courses.AddRange(additions);
await db.SaveChangesAsync(cancellationToken);
}
private async Task SeedTeacherCourseApplicationsAsync(CancellationToken cancellationToken)
{
var term = await db.AcademicTerms
.OrderByDescending(x => x.IsCurrent)
.ThenByDescending(x => x.StartDate)
.FirstOrDefaultAsync(cancellationToken);
if (term is null) return;
var collegeCodes = CollegeDefinitions
.Select(x => x.Code)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var colleges = await db.Colleges
.Where(x => collegeCodes.Contains(x.Code))
.OrderBy(x => x.Code)
.ToListAsync(cancellationToken);
var collegeIds = colleges.Select(x => x.Id).ToHashSet();
var teachers = await db.Teachers
.Where(x => collegeIds.Contains(x.CollegeId) && x.Status == TeacherStatus.Active)
.OrderBy(x => x.TeacherNumber)
.ToListAsync(cancellationToken);
var courses = await db.Courses
.Where(x => collegeIds.Contains(x.CollegeId))
.OrderBy(x => x.Code)
.ToListAsync(cancellationToken);
var publicCourses = courses
.Where(x => x.Nature is CourseNature.GeneralRequired or CourseNature.GeneralElective)
.ToList();
if (publicCourses.Count == 0) return;
var existing = (await db.TeacherCourseApplications
.Where(x => x.AcademicTermId == term.Id)
.Select(x => new { x.TeacherId, x.CourseId })
.ToListAsync(cancellationToken))
.Select(x => (x.TeacherId, x.CourseId))
.ToHashSet();
var additions = new List<TeacherCourseApplication>();
for (var teacherIndex = 0; teacherIndex < teachers.Count; teacherIndex++)
{
var teacher = teachers[teacherIndex];
var professionalCourses = courses
.Where(x => x.CollegeId == teacher.CollegeId &&
x.Nature is CourseNature.MajorRequired or
CourseNature.MajorElective or CourseNature.Practice)
.Take(4);
var selectedPublicCourses = Enumerable.Range(0, 2)
.Select(offset => publicCourses[(teacherIndex + offset) % publicCourses.Count]);
foreach (var course in professionalCourses
.Concat(selectedPublicCourses)
.DistinctBy(x => x.Id))
{
if (!existing.Add((teacher.Id, course.Id))) continue;
additions.Add(new TeacherCourseApplication
{
AcademicTermId = term.Id,
TeacherId = teacher.Id,
CourseId = course.Id,
Status = TeacherCourseApplicationStatus.Approved,
Statement = "生产演示环境批量生成的授课意向。",
ReviewComment = "测试数据自动审核通过。",
SubmittedAt = DateTime.UtcNow.AddDays(-7),
ReviewedAt = DateTime.UtcNow.AddDays(-6)
});
}
}
if (additions.Count == 0) return;
db.TeacherCourseApplications.AddRange(additions);
await db.SaveChangesAsync(cancellationToken);
}
private async Task LogSummaryAsync(CancellationToken cancellationToken)
{
var courseCounts = await db.Courses
.GroupBy(x => x.Nature)
.Select(x => new { Nature = x.Key, Count = x.Count() })
.ToDictionaryAsync(x => x.Nature, x => x.Count, cancellationToken);
logger.LogInformation(
"生产演示数据已就绪:学院/教学单位 {CollegeCount},专业 {MajorCount}" +
"行政班 {ClassCount},教师 {TeacherCount},学生 {StudentCount},课程 {CourseCount}" +
"公共必修 {GeneralRequiredCount},公共选修 {GeneralElectiveCount}" +
"专业必修 {MajorRequiredCount},专业选修 {MajorElectiveCount},实践课程 {PracticeCount}" +
"已审核授课资格 {ApplicationCount}。",
await db.Colleges.CountAsync(cancellationToken),
await db.Majors.CountAsync(cancellationToken),
await db.AdministrativeClasses.CountAsync(cancellationToken),
await db.Teachers.CountAsync(cancellationToken),
await db.Students.CountAsync(cancellationToken),
await db.Courses.CountAsync(cancellationToken),
courseCounts.GetValueOrDefault(CourseNature.GeneralRequired),
courseCounts.GetValueOrDefault(CourseNature.GeneralElective),
courseCounts.GetValueOrDefault(CourseNature.MajorRequired),
courseCounts.GetValueOrDefault(CourseNature.MajorElective),
courseCounts.GetValueOrDefault(CourseNature.Practice),
await db.TeacherCourseApplications
.CountAsync(x => x.Status == TeacherCourseApplicationStatus.Approved,
cancellationToken));
}
private static string CreateAvailableCode(string preferredCode, ISet<string> existingCodes)
{
var candidate = preferredCode;
var suffix = 1;
while (!existingCodes.Add(candidate))
{
candidate = $"{preferredCode}-{suffix++}";
}
return candidate;
}
private static string BuildPersonName(int groupIndex, int position)
{
var surname = Surnames[(groupIndex + position) % Surnames.Length];
var givenName = GivenNames[
(groupIndex * 7 + position * 3) % GivenNames.Length];
return surname + givenName;
}
private static readonly string[] Surnames =
["王", "李", "张", "刘", "陈", "杨", "黄", "赵", "吴", "周", "徐", "孙", "马", "朱", "胡", "郭", "何", "高", "林", "罗"];
private static readonly string[] GivenNames =
["明远", "知夏", "嘉树", "雨桐", "思源", "若溪", "景行", "书雅", "子涵", "浩然", "清越", "语晨", "承宇", "欣怡", "博文", "婉宁", "俊逸", "安然", "泽楷", "可心"];
private static readonly string[] CounselorSurnames =
["陈", "林", "周", "王", "李", "张", "刘", "赵"];
private static readonly string[] TeacherTitles =
["教授", "副教授", "讲师", "讲师", "副教授", "实验师", "讲师", "教授"];
private static readonly CollegeSeed[] CollegeDefinitions =
[
new("CS", "计算机学院", "计算机学院",
[
new("080901", "计算机科学与技术", "工学学士"),
new("080902", "软件工程", "工学学士"),
new("080903", "网络工程", "工学学士"),
new("080910T", "数据科学与大数据技术", "工学学士"),
new("080717T", "人工智能", "工学学士")
],
["程序设计基础", "离散数学", "数据结构", "计算机组成原理", "操作系统", "数据库系统原理", "软件工程课程设计", "人工智能项目实践"]),
new("EIA", "电子信息与自动化学院", "电子信息学院",
[
new("080701", "电子信息工程", "工学学士"),
new("080703", "通信工程", "工学学士"),
new("080801", "自动化", "工学学士"),
new("080803T", "机器人工程", "工学学士")
],
["电路分析", "模拟电子技术", "数字电子技术", "信号与系统", "通信原理", "嵌入式系统", "电子系统设计", "综合电子实训"]),
new("ME", "机械与车辆工程学院", "机械学院",
[
new("080202", "机械设计制造及其自动化", "工学学士"),
new("080207", "车辆工程", "工学学士"),
new("080205", "工业设计", "工学学士"),
new("080213T", "智能制造工程", "工学学士")
],
["工程制图", "理论力学", "材料力学", "机械原理", "机械设计", "智能制造技术", "机械创新设计", "工程训练"]),
new("EE", "电气工程学院", "电气学院",
[
new("080601", "电气工程及其自动化", "工学学士"),
new("080604T", "电气工程与智能控制", "工学学士"),
new("080605T", "电机电器智能化", "工学学士")
],
["电路原理", "电机学", "电力电子技术", "自动控制原理", "电力系统分析", "继电保护", "电气控制实训", "电力系统综合设计"]),
new("CIVIL", "土木建筑工程学院", "土建学院",
[
new("081001", "土木工程", "工学学士"),
new("082801", "建筑学", "建筑学学士", 5),
new("120103", "工程管理", "管理学学士"),
new("081006T", "道路桥梁与渡河工程", "工学学士")
],
["工程制图与识图", "工程力学", "结构力学", "混凝土结构", "土力学与地基基础", "工程项目管理", "建筑设计基础", "工程测量实习"]),
new("ECON", "经济与管理学院", "经管学院",
[
new("120201K", "工商管理", "管理学学士"),
new("120203K", "会计学", "管理学学士"),
new("020301K", "金融学", "经济学学士"),
new("020401", "国际经济与贸易", "经济学学士"),
new("120202", "市场营销", "管理学学士")
],
["微观经济学", "宏观经济学", "管理学原理", "会计学原理", "统计学", "财务管理", "企业经营沙盘", "商务数据分析实践"]),
new("FOREIGN", "外国语学院", "外国语学院",
[
new("050201", "英语", "文学学士"),
new("050207", "日语", "文学学士"),
new("050262", "商务英语", "文学学士")
],
["综合英语", "英语听力", "英语口语", "英语写作", "翻译理论与实践", "跨文化交际", "商务英语实训", "口译实践"]),
new("MATH", "数学与统计学院", "数统学院",
[
new("070101", "数学与应用数学", "理学学士"),
new("071201", "统计学", "理学学士"),
new("020102", "经济统计学", "经济学学士")
],
["数学分析", "高等代数", "解析几何", "常微分方程", "实变函数", "数值分析", "数学建模", "统计软件实践"]),
new("PHYSICS", "物理与光电工程学院", "物电学院",
[
new("070201", "物理学", "理学学士"),
new("070202", "应用物理学", "理学学士"),
new("080705", "光电信息科学与工程", "工学学士")
],
["力学", "热学", "电磁学", "光学", "量子力学", "固体物理", "近代物理实验", "光电技术综合实验"]),
new("CHEM", "化学与环境工程学院", "化环学院",
[
new("070301", "化学", "理学学士"),
new("070302", "应用化学", "理学学士"),
new("081301", "化学工程与工艺", "工学学士"),
new("082502", "环境工程", "工学学士")
],
["无机化学", "有机化学", "分析化学", "物理化学", "化工原理", "仪器分析", "基础化学实验", "化工设计实践"]),
new("HUMANITIES", "人文与法学院", "人文法学院",
[
new("050101", "汉语言文学", "文学学士"),
new("030101K", "法学", "法学学士"),
new("120402", "行政管理", "管理学学士")
],
["中国古代文学", "中国现当代文学", "现代汉语", "古代汉语", "文学概论", "行政管理学", "新闻写作实训", "社会调查实践"]),
new("EDU", "教育科学学院", "教育学院",
[
new("040101", "教育学", "教育学学士"),
new("040107", "小学教育", "教育学学士"),
new("040106", "学前教育", "教育学学士")
],
["教育学原理", "普通心理学", "教育心理学", "课程与教学论", "教育研究方法", "班级管理", "微格教学", "教育见习"]),
new("ART", "艺术设计学院", "艺术学院",
[
new("130502", "视觉传达设计", "艺术学学士"),
new("130503", "环境设计", "艺术学学士"),
new("130202", "音乐学", "艺术学学士")
],
["设计素描", "色彩基础", "构成基础", "艺术概论", "数字媒体设计", "品牌视觉设计", "专业采风", "毕业创作实践"]),
new("PE", "体育学院", "体育学院",
[
new("040201", "体育教育", "教育学学士"),
new("040203", "社会体育指导与管理", "教育学学士")
],
["运动解剖学", "运动生理学", "学校体育学", "体育心理学", "运动训练学", "体育社会学", "田径专项训练", "球类专项训练"]),
new("LIFE", "生命科学与食品工程学院", "生食学院",
[
new("071001", "生物科学", "理学学士"),
new("071002", "生物技术", "理学学士"),
new("082701", "食品科学与工程", "工学学士")
],
["普通生物学", "生物化学", "细胞生物学", "遗传学", "微生物学", "食品化学", "分子生物学实验", "生物工程综合实践"]),
new("MARXISM", "马克思主义学院", "马克思主义学院", [], [])
];
private static readonly PublicCourseSeed[] PublicCourseDefinitions =
[
new("PUB001", "思想道德与法治", "MARXISM", "MORAL", 3, 48, 48, CourseNature.GeneralRequired),
new("PUB002", "中国近现代史纲要", "MARXISM", "MORAL", 3, 48, 48, CourseNature.GeneralRequired),
new("PUB003", "马克思主义基本原理", "MARXISM", "MORAL", 3, 48, 48, CourseNature.GeneralRequired),
new("PUB004", "毛泽东思想和中国特色社会主义理论体系概论", "MARXISM", "MORAL", 5, 80, 64, CourseNature.GeneralRequired),
new("PUB005", "习近平新时代中国特色社会主义思想概论", "MARXISM", "MORAL", 3, 48, 48, CourseNature.GeneralRequired),
new("PUB006", "形势与政策", "MARXISM", "MORAL", 2, 32, 32, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB007", "大学英语 I", "FOREIGN", "ENGLISH", 4, 64, 48, CourseNature.GeneralRequired),
new("PUB008", "大学英语 II", "FOREIGN", "ENGLISH", 4, 64, 48, CourseNature.GeneralRequired),
new("PUB009", "高等数学 A(上)", "MATH", "BASIC", 5, 80, 80, CourseNature.GeneralRequired),
new("PUB010", "高等数学 A(下)", "MATH", "BASIC", 5, 80, 80, CourseNature.GeneralRequired),
new("PUB011", "线性代数", "MATH", "BASIC", 3, 48, 48, CourseNature.GeneralRequired),
new("PUB012", "概率论与数理统计", "MATH", "BASIC", 3, 48, 48, CourseNature.GeneralRequired),
new("PUB013", "大学计算机基础", "CS", "BASIC", 2, 32, 16, CourseNature.GeneralRequired),
new("PUB014", "Python 程序设计", "CS", "BASIC", 3, 48, 24, CourseNature.GeneralRequired),
new("PUB015", "大学体育 I", "PE", "SPORTS", 1, 32, 4, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB016", "大学体育 II", "PE", "SPORTS", 1, 32, 4, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB017", "军事理论", "HUMANITIES", "MILITARY", 2, 36, 32, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB018", "大学生心理健康教育", "EDU", "BASIC", 2, 32, 24, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB019", "大学生职业发展与就业指导", "EDU", "BASIC", 2, 32, 24, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB020", "创新创业基础", "ECON", "INNOVATION", 2, 32, 20, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB021", "劳动教育", "EDU", "LABOR", 1, 32, 8, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB022", "国家安全教育", "HUMANITIES", "MORAL", 1, 16, 16, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB023", "文献检索与学术规范", "HUMANITIES", "BASIC", 1, 16, 12, CourseNature.GeneralRequired, AssessmentMethod.Assessment),
new("PUB024", "艺术鉴赏", "ART", "AESTHETIC", 2, 32, 24, CourseNature.GeneralElective, AssessmentMethod.Assessment),
new("PUB025", "中国传统文化", "HUMANITIES", "AESTHETIC", 2, 32, 32, CourseNature.GeneralElective, AssessmentMethod.Assessment),
new("PUB026", "生态文明导论", "LIFE", "BASIC", 2, 32, 24, CourseNature.GeneralElective, AssessmentMethod.Assessment),
new("PUB027", "人工智能导论", "CS", "INNOVATION", 2, 32, 20, CourseNature.GeneralElective, AssessmentMethod.Assessment),
new("PUB028", "经济学通识", "ECON", "BASIC", 2, 32, 32, CourseNature.GeneralElective, AssessmentMethod.Assessment)
];
private sealed record CollegeSeed(
string Code,
string Name,
string ShortName,
IReadOnlyList<MajorSeed> Majors,
IReadOnlyList<string> ProfessionalCourses);
private sealed record MajorSeed(
string Code,
string Name,
string DegreeType,
int SchoolingYears = 4);
private sealed record PublicCourseSeed(
string Code,
string Name,
string CollegeCode,
string CategoryCode,
decimal Credits,
int TotalHours,
int LectureHours,
CourseNature Nature,
AssessmentMethod AssessmentMethod = AssessmentMethod.Examination);
}
+47 -3
View File
@@ -15,8 +15,46 @@ using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Threading.RateLimiting;
var migrateOnly = args.Contains("--migrate-only", StringComparer.OrdinalIgnoreCase);
var seedDemoData = args.Contains("--seed-demo-data", StringComparer.OrdinalIgnoreCase);
var confirmProductionDemoData = args.Contains(
"--confirm-production-demo-data",
StringComparer.OrdinalIgnoreCase);
if (migrateOnly && seedDemoData)
{
Console.Error.WriteLine(
"--migrate-only 与 --seed-demo-data 不能同时使用。请先迁移,再单独插入演示数据。");
Environment.ExitCode = 2;
return;
}
if (seedDemoData && !confirmProductionDemoData)
{
Console.Error.WriteLine(
"插入生产演示数据必须同时传入 --confirm-production-demo-data。");
Environment.ExitCode = 2;
return;
}
if (confirmProductionDemoData && !seedDemoData)
{
Console.Error.WriteLine(
"--confirm-production-demo-data 只能与 --seed-demo-data 同时使用。");
Environment.ExitCode = 2;
return;
}
var builder = WebApplication.CreateBuilder(args);
if (seedDemoData && builder.Environment.IsDevelopment())
{
Console.Error.WriteLine(
"--seed-demo-data 仅用于独立的非 Development 演示环境数据库。");
Environment.ExitCode = 2;
return;
}
var databaseOptions = builder.Configuration
.GetSection(DatabaseOptions.SectionName)
.Get<DatabaseOptions>() ?? new DatabaseOptions();
@@ -114,6 +152,7 @@ builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.AddScoped<ICurrentUserDataScope, CurrentUserDataScope>();
builder.Services.AddScoped<DatabaseInitializer>();
builder.Services.AddScoped<DemoDataSeeder>();
builder.Services.AddScoped<DevelopmentSqliteMigrator>();
builder.Services.AddScoped<TimetableDataService>();
builder.Services.AddScoped<AutomaticScheduleGenerator>();
@@ -305,11 +344,16 @@ app.MapFallback(async context =>
using (var scope = app.Services.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<DatabaseInitializer>()
.InitializeAsync(
args.Contains("--migrate-only", StringComparer.OrdinalIgnoreCase));
.InitializeAsync(migrateOnly);
if (seedDemoData)
{
await scope.ServiceProvider.GetRequiredService<DemoDataSeeder>()
.SeedAsync();
}
}
if (args.Contains("--migrate-only", StringComparer.OrdinalIgnoreCase))
if (migrateOnly || seedDemoData)
{
return;
}