已把选课的“年级划分”和限制规则补成完整闭环:

选课批次可指定多个适用年级;留空兼容原有“全部年级”。
新增可选的“最多课程门数”,与原有学分上限并行。
年级、门数限制统一作用于学生可见批次、自主选课、候补、管理员代选和自动递补。
普通候选名单遵守年级及行政班范围;强制选课可绕过业务限制,但仍严格遵守管理员学院权限。
管理页面会展示“2026 级 · 最多 6 门 · 最多 30 学分”,名单和候补列表也增加年级列。
新增 MySQL 迁移:[CourseSelectionGradeLimits.cs](E:/jiaowu/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726180000_CourseSelectionGradeLimits.cs)。
This commit is contained in:
2026-07-26 16:12:21 +08:00 Unverified
parent 56b13b3adb
commit f62cd21319
11 changed files with 4920 additions and 25 deletions
@@ -21,6 +21,16 @@ public static class CourseSelectionRules
public static bool SupportsProxyEnrollment(CourseNature nature) =>
nature == CourseNature.GeneralRequired;
public static bool IsGradeEligible(
IEnumerable<int> eligibleGrades,
int studentGrade) =>
!eligibleGrades.Any() || eligibleGrades.Contains(studentGrade);
public static bool HasReachedCourseLimit(
int? maxCourseCount,
int selectedCourseCount) =>
maxCourseCount.HasValue && selectedCourseCount >= maxCourseCount.Value;
public static bool RequiresPublishedSchedule(TeachingTaskSchedulingMode schedulingMode) =>
schedulingMode == TeachingTaskSchedulingMode.Standard;
@@ -43,6 +43,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
Set<SchedulePublishJob>();
public DbSet<CourseSelectionRound> CourseSelectionRounds =>
Set<CourseSelectionRound>();
public DbSet<CourseSelectionRoundGrade> CourseSelectionRoundGrades =>
Set<CourseSelectionRoundGrade>();
public DbSet<CourseSelectionOffering> CourseSelectionOfferings =>
Set<CourseSelectionOffering>();
public DbSet<CourseEnrollment> CourseEnrollments => Set<CourseEnrollment>();
@@ -477,6 +479,17 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<CourseSelectionRoundGrade>(entity =>
{
entity.HasIndex(x => new { x.CourseSelectionRoundId, x.Grade })
.IsUnique();
entity.HasIndex(x => x.Grade);
entity.HasOne(x => x.CourseSelectionRound)
.WithMany(x => x.EligibleGrades)
.HasForeignKey(x => x.CourseSelectionRoundId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<CourseSelectionOffering>(entity =>
{
entity.Property(x => x.Notes).HasMaxLength(500);
@@ -0,0 +1,65 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class CourseSelectionGradeLimits : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "MaxCourseCount",
table: "CourseSelectionRounds",
type: "int",
nullable: true);
migrationBuilder.CreateTable(
name: "CourseSelectionRoundGrades",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CourseSelectionRoundId = table.Column<Guid>(type: "char(36)", nullable: false),
Grade = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CourseSelectionRoundGrades", x => x.Id);
table.ForeignKey(
name: "FK_CourseSelectionRoundGrades_CourseSelectionRounds_CourseSelec~",
column: x => x.CourseSelectionRoundId,
principalTable: "CourseSelectionRounds",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_CourseSelectionRoundGrades_CourseSelectionRoundId_Grade",
table: "CourseSelectionRoundGrades",
columns: new[] { "CourseSelectionRoundId", "Grade" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_CourseSelectionRoundGrades_Grade",
table: "CourseSelectionRoundGrades",
column: "Grade");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CourseSelectionRoundGrades");
migrationBuilder.DropColumn(
name: "MaxCourseCount",
table: "CourseSelectionRounds");
}
}
}
@@ -40,13 +40,13 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateOnly>("EndDate")
b.Property<DateTime>("EndDate")
.HasColumnType("date");
b.Property<bool>("IsCurrent")
b.Property<bool>("IsArchived")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsArchived")
b.Property<bool>("IsCurrent")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsEnabled")
@@ -63,7 +63,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<DateOnly>("StartDate")
b.Property<DateTime>("StartDate")
.HasColumnType("date");
b.Property<DateTime>("UpdatedAt")
@@ -74,10 +74,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.HasIndex("Code")
.IsUnique();
b.HasIndex("IsCurrent");
b.HasIndex("IsArchived");
b.HasIndex("IsCurrent");
b.HasIndex("IsEnabled", "SortOrder");
b.ToTable("AcademicTerms");
@@ -595,7 +595,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<Guid>("ApplicantUserId")
.HasColumnType("char(36)");
b.Property<DateOnly?>("CancelDate")
b.Property<DateTime?>("CancelDate")
.HasColumnType("date");
b.Property<int?>("CancelWeek")
@@ -640,7 +640,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<Guid?>("SubstituteTeacherId")
.HasColumnType("char(36)");
b.Property<DateOnly?>("TargetDate")
b.Property<DateTime?>("TargetDate")
.HasColumnType("date");
b.Property<Guid>("TeachingTaskId")
@@ -743,10 +743,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.HasIndex("CourseSelectionOfferingId", "StudentId")
.IsUnique();
b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt");
b.HasIndex("StudentId", "Status");
b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt");
b.ToTable("CourseEnrollments");
});
@@ -854,6 +854,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<DateTime>("EndsAt")
.HasColumnType("datetime(6)");
b.Property<int?>("MaxCourseCount")
.HasColumnType("int");
b.Property<decimal>("MaxCredits")
.HasPrecision(6, 1)
.HasColumnType("decimal(6,1)");
@@ -886,6 +889,34 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.ToTable("CourseSelectionRounds");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRoundGrade", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("CourseSelectionRoundId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("Grade")
.HasColumnType("int");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("Grade");
b.HasIndex("CourseSelectionRoundId", "Grade")
.IsUnique();
b.ToTable("CourseSelectionRoundGrades");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSubstitution", b =>
{
b.Property<Guid>("Id")
@@ -1407,7 +1438,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<DateTime>("EndsAt")
.HasColumnType("datetime(6)");
b.Property<DateOnly>("ExamDate")
b.Property<DateTime>("ExamDate")
.HasColumnType("date");
b.Property<Guid>("ExamPlanId")
@@ -2117,7 +2148,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<DateTime>("EndsAt")
.HasColumnType("datetime(6)");
b.Property<DateOnly>("ExamDate")
b.Property<DateTime>("ExamDate")
.HasColumnType("date");
b.Property<Guid>("MakeupExamPlanId")
@@ -2391,7 +2422,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<TimeOnly>("EndsAt")
b.Property<TimeSpan>("EndsAt")
.HasColumnType("time");
b.Property<bool>("IsEnabled")
@@ -2405,7 +2436,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<int>("PeriodNumber")
.HasColumnType("int");
b.Property<TimeOnly>("StartsAt")
b.Property<TimeSpan>("StartsAt")
.HasColumnType("time");
b.Property<DateTime>("UpdatedAt")
@@ -2431,14 +2462,14 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateOnly?>("DateOfBirth")
b.Property<DateTime?>("DateOfBirth")
.HasColumnType("date");
b.Property<string>("Email")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<DateOnly>("EnrollmentDate")
b.Property<DateTime>("EnrollmentDate")
.HasColumnType("date");
b.Property<int>("EnrollmentYear")
@@ -2559,7 +2590,7 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<int>("Gender")
.HasColumnType("int");
b.Property<DateOnly?>("HireDate")
b.Property<DateTime?>("HireDate")
.HasColumnType("date");
b.Property<bool>("IsExternal")
@@ -3412,6 +3443,17 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Navigation("AcademicTerm");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRoundGrade", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.CourseSelectionRound", "CourseSelectionRound")
.WithMany("EligibleGrades")
.HasForeignKey("CourseSelectionRoundId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CourseSelectionRound");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSubstitution", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.Course", "OriginalCourse")
@@ -4232,6 +4274,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.CourseSelectionRound", b =>
{
b.Navigation("EligibleGrades");
b.Navigation("Offerings");
});