This commit is contained in:
2026-07-25 15:30:46 +08:00 Unverified
parent 4e7da6709e
commit 9685435da9
7 changed files with 451 additions and 49 deletions
@@ -26,4 +26,46 @@ public static class CourseSelectionRules
candidateEntries.Any(candidate =>
selectedEntries.Any(selected =>
ScheduleConflictDetector.TimeOverlaps(candidate, selected)));
/// <summary>
/// Returns the overlap percentage of candidate schedule entries with selected entries.
/// 0 = no conflict, 100 = fully overlapping.
/// Used for retake enrollment where ≤50% overlap is allowed.
/// </summary>
public static double CalculateScheduleOverlap(
IReadOnlyCollection<ScheduleEntry> candidateEntries,
IReadOnlyCollection<ScheduleEntry> selectedEntries)
{
if (candidateEntries.Count == 0 || selectedEntries.Count == 0)
return 0;
int totalCandidatePeriods = 0;
int overlappedPeriods = 0;
foreach (var candidate in candidateEntries)
{
totalCandidatePeriods += candidate.PeriodCount;
foreach (var selected in selectedEntries)
{
if (!ScheduleConflictDetector.TimeOverlaps(candidate, selected))
continue;
// Calculate overlapping periods
var overlapStart = Math.Max(
candidate.StartPeriod, selected.StartPeriod);
var overlapEnd = Math.Min(
candidate.StartPeriod + candidate.PeriodCount,
selected.StartPeriod + selected.PeriodCount);
if (overlapEnd > overlapStart)
overlappedPeriods += overlapEnd - overlapStart;
}
}
return totalCandidatePeriods == 0
? 0
: (double)overlappedPeriods / totalCandidatePeriods * 100;
}
/// <summary>Retake expanded capacity: ceiling(original * 1.15).</summary>
public static int RetakeCapacity(int originalCapacity) =>
(int)Math.Ceiling(originalCapacity * 1.15);
}
@@ -34,6 +34,8 @@ public sealed class DevelopmentSqliteMigrator(
"20260725_19_flexible_grades";
private const string ExamSchedulingOptimizationMigration =
"20260725_20_exam_scheduling_optimization";
private const string RetakeEnrollmentMigration =
"20260725_21_retake_enrollment";
public async Task MigrateAsync(CancellationToken cancellationToken = default)
{
@@ -225,6 +227,19 @@ public sealed class DevelopmentSqliteMigrator(
? []
: ExamSchedulingOptimizationStatements,
cancellationToken);
var retakeEnrollmentExists = await db.Database
.SqlQueryRaw<int>(
"""
SELECT COUNT(*) AS "Value"
FROM pragma_table_info('CourseEnrollments')
WHERE name = 'EnrollmentType'
""")
.AnyAsync(value => value > 0, cancellationToken);
await ApplyMigrationAsync(
RetakeEnrollmentMigration,
retakeEnrollmentExists ? [] : RetakeEnrollmentStatements,
cancellationToken);
}
private async Task ApplyMigrationAsync(
@@ -1461,4 +1476,12 @@ public sealed class DevelopmentSqliteMigrator(
ON "ExamSessions" ("RequiredBuildingId");
"""
];
private static readonly string[] RetakeEnrollmentStatements =
[
"""
ALTER TABLE "CourseEnrollments"
ADD COLUMN "EnrollmentType" INTEGER NOT NULL DEFAULT 1;
"""
];
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class RetakeEnrollment : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "EnrollmentType",
table: "CourseEnrollments",
type: "int",
nullable: false,
defaultValue: 1);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EnrollmentType",
table: "CourseEnrollments");
}
}
}