课程库 Excel 导入导出已完成:
支持下载标准导入模板。 导出严格沿用当前关键词、学院、分类、课程性质和状态筛选。 以课程编码为唯一键:存在则更新,不存在则新增。 校验学院编码、课程分类、课程性质、学分学时及考核方式。 整批原子导入,任一行错误则全部不写入,并提示具体行号。 学院管理员仍只能维护本学院的专业课和实践课,无法借 Excel 越权。 界面入口沿用现有课程库工具栏样式。
This commit is contained in:
@@ -19,6 +19,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
public DbSet<AcademicTerm> AcademicTerms => Set<AcademicTerm>();
|
||||
public DbSet<Teacher> Teachers => Set<Teacher>();
|
||||
public DbSet<Student> Students => Set<Student>();
|
||||
public DbSet<CourseCategory> CourseCategories => Set<CourseCategory>();
|
||||
public DbSet<Course> Courses => Set<Course>();
|
||||
public DbSet<CurriculumPlan> CurriculumPlans => Set<CurriculumPlan>();
|
||||
public DbSet<CurriculumModule> CurriculumModules => Set<CurriculumModule>();
|
||||
@@ -78,6 +79,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
ConfigureCatalog<Building>(builder);
|
||||
ConfigureCatalog<Classroom>(builder);
|
||||
ConfigureCatalog<AcademicTerm>(builder);
|
||||
ConfigureCatalog<CourseCategory>(builder);
|
||||
ConfigureCatalog<Course>(builder);
|
||||
|
||||
builder.Entity<College>()
|
||||
@@ -167,10 +169,15 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
entity.Property(x => x.Credits).HasPrecision(5, 2);
|
||||
entity.Property(x => x.Description).HasMaxLength(1000);
|
||||
entity.HasIndex(x => new { x.CollegeId, x.Nature });
|
||||
entity.HasIndex(x => x.CourseCategoryId);
|
||||
entity.HasOne(x => x.College)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CollegeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(x => x.CourseCategory)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CourseCategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<CurriculumPlan>(entity =>
|
||||
|
||||
@@ -28,6 +28,7 @@ public sealed class DatabaseInitializer(
|
||||
|
||||
await SeedRolesAsync();
|
||||
await SeedAdministratorAsync();
|
||||
await SeedCourseCategoriesAsync();
|
||||
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
@@ -108,6 +109,55 @@ public sealed class DatabaseInitializer(
|
||||
}
|
||||
}
|
||||
|
||||
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 async Task SeedDevelopmentDataAsync()
|
||||
{
|
||||
if (!await db.Campuses.AnyAsync())
|
||||
@@ -242,6 +292,9 @@ public sealed class DatabaseInitializer(
|
||||
});
|
||||
}
|
||||
|
||||
var courseCategories = await db.CourseCategories
|
||||
.ToDictionaryAsync(x => x.Code, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!await db.Courses.AnyAsync())
|
||||
{
|
||||
db.Courses.AddRange(
|
||||
@@ -251,6 +304,7 @@ public sealed class DatabaseInitializer(
|
||||
Name = "程序设计基础",
|
||||
EnglishName = "Fundamentals of Programming",
|
||||
CollegeId = computerCollege.Id,
|
||||
CourseCategoryId = courseCategories["BASIC"].Id,
|
||||
Credits = 4,
|
||||
TotalHours = 64,
|
||||
LectureHours = 40,
|
||||
@@ -265,6 +319,7 @@ public sealed class DatabaseInitializer(
|
||||
Name = "数据结构",
|
||||
EnglishName = "Data Structures",
|
||||
CollegeId = computerCollege.Id,
|
||||
CourseCategoryId = courseCategories["BASIC"].Id,
|
||||
Credits = 3.5m,
|
||||
TotalHours = 56,
|
||||
LectureHours = 40,
|
||||
@@ -278,6 +333,7 @@ public sealed class DatabaseInitializer(
|
||||
Name = "软件工程实践",
|
||||
EnglishName = "Software Engineering Practice",
|
||||
CollegeId = computerCollege.Id,
|
||||
CourseCategoryId = courseCategories["PRACTICE"].Id,
|
||||
Credits = 2,
|
||||
TotalHours = 48,
|
||||
LectureHours = 8,
|
||||
@@ -286,7 +342,6 @@ public sealed class DatabaseInitializer(
|
||||
AssessmentMethod = AssessmentMethod.Assessment
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
if (!await db.CurriculumPlans.AnyAsync())
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
private const string GraduationAuditsMigration = "20260724_10_graduation_audits";
|
||||
private const string DegreeAwardsMigration = "20260724_11_degree_awards";
|
||||
private const string GraduationClearanceMigration = "20260724_12_graduation_clearance";
|
||||
private const string CourseCategoriesMigration = "20260724_13_course_categories";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -90,6 +91,22 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
GraduationClearanceMigration,
|
||||
GraduationClearanceStatements,
|
||||
cancellationToken);
|
||||
var courseCategoryColumnExists = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"""
|
||||
SELECT COUNT(*) AS "Value"
|
||||
FROM pragma_table_info('Courses')
|
||||
WHERE name = 'CourseCategoryId'
|
||||
""")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
CourseCategoriesMigration,
|
||||
courseCategoryColumnExists
|
||||
? CourseCategoriesStatements.Where(
|
||||
statement => !statement.StartsWith(
|
||||
"ALTER TABLE", StringComparison.OrdinalIgnoreCase))
|
||||
: CourseCategoriesStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationAsync(
|
||||
@@ -767,4 +784,34 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"""CREATE UNIQUE INDEX IF NOT EXISTS "IX_GraduationClearanceRecords_GraduationClearanceItemId_StudentId" ON "GraduationClearanceRecords" ("GraduationClearanceItemId", "StudentId");""",
|
||||
"""CREATE INDEX IF NOT EXISTS "IX_GraduationClearanceRecords_StudentId_Status" ON "GraduationClearanceRecords" ("StudentId", "Status");"""
|
||||
];
|
||||
|
||||
private static readonly string[] CourseCategoriesStatements =
|
||||
[
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "CourseCategories" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_CourseCategories" PRIMARY KEY,
|
||||
"Code" TEXT NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"SortOrder" INTEGER NOT NULL,
|
||||
"IsEnabled" INTEGER NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IX_CourseCategories_Code"
|
||||
ON "CourseCategories" ("Code");
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS "IX_CourseCategories_IsEnabled_SortOrder"
|
||||
ON "CourseCategories" ("IsEnabled", "SortOrder");
|
||||
""",
|
||||
"""
|
||||
ALTER TABLE "Courses" ADD COLUMN "CourseCategoryId" TEXT NULL;
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS "IX_Courses_CourseCategoryId"
|
||||
ON "Courses" ("CourseCategoryId");
|
||||
"""
|
||||
];
|
||||
}
|
||||
|
||||
+2541
File diff suppressed because it is too large
Load Diff
+82
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CourseCategories : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "CourseCategoryId",
|
||||
table: "Courses",
|
||||
type: "char(36)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CourseCategories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
Code = table.Column<string>(type: "varchar(40)", maxLength: 40, nullable: false),
|
||||
Name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false),
|
||||
IsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CourseCategories", x => x.Id);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Courses_CourseCategoryId",
|
||||
table: "Courses",
|
||||
column: "CourseCategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CourseCategories_Code",
|
||||
table: "CourseCategories",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CourseCategories_IsEnabled_SortOrder",
|
||||
table: "CourseCategories",
|
||||
columns: new[] { "IsEnabled", "SortOrder" });
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Courses_CourseCategories_CourseCategoryId",
|
||||
table: "Courses",
|
||||
column: "CourseCategoryId",
|
||||
principalTable: "CourseCategories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Courses_CourseCategories_CourseCategoryId",
|
||||
table: "Courses");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CourseCategories");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Courses_CourseCategoryId",
|
||||
table: "Courses");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CourseCategoryId",
|
||||
table: "Courses");
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -329,6 +329,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Property<Guid>("CollegeId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("CourseCategoryId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
@@ -375,6 +378,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CourseCategoryId");
|
||||
|
||||
b.HasIndex("CollegeId", "Nature");
|
||||
|
||||
b.HasIndex("IsEnabled", "SortOrder");
|
||||
@@ -382,6 +387,44 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("Courses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("IsEnabled", "SortOrder");
|
||||
|
||||
b.ToTable("CourseCategories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseEnrollment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -1948,7 +1991,14 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.CourseCategory", "CourseCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("CourseCategoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("College");
|
||||
|
||||
b.Navigation("CourseCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseEnrollment", b =>
|
||||
|
||||
Reference in New Issue
Block a user