Files
Academic-Affairs-System/src/Jiaowu.Api/Infrastructure/CourseSelection/CourseSelectionRules.cs
T
biss 56b13b3adb 已完成“选课候补与自动递补”完整流程。
学生端:满员后可加入/取消候补,展示候补顺位和当前候补人数。
自动递补:退课释放名额后,按候补时间顺序重新校验学籍、范围、学分及时间冲突;不合格者自动失效并继续下一位。
管理端:教学班显示候补人数,名单抽屉提供候补队列及移出操作。
状态通知:递补成功、资格失效、批次关闭、管理员移出均会通知学生。
并发安全:退课和递补放在 Serializable 事务中执行。
数据库:已补充 SQLite 开发迁移和 MySQL 正式迁移。
2026-07-26 15:39:50 +08:00

79 lines
3.1 KiB
C#

using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Scheduling;
namespace Jiaowu.Api.Infrastructure.CourseSelection;
public static class CourseSelectionRules
{
public static bool IsSelectionOpen(CourseSelectionRound round, DateTime nowUtc) =>
round.Status == CourseSelectionRoundStatus.Open &&
nowUtc >= round.StartsAt &&
nowUtc <= round.EndsAt;
public static bool CanWithdraw(CourseSelectionRound round, DateTime nowUtc) =>
round.Status == CourseSelectionRoundStatus.Open &&
nowUtc <= round.WithdrawalEndsAt;
public static bool CanPromoteWaitlist(CourseSelectionRound round, DateTime nowUtc) =>
round.Status == CourseSelectionRoundStatus.Open &&
nowUtc <= round.WithdrawalEndsAt;
public static bool SupportsProxyEnrollment(CourseNature nature) =>
nature == CourseNature.GeneralRequired;
public static bool RequiresPublishedSchedule(TeachingTaskSchedulingMode schedulingMode) =>
schedulingMode == TeachingTaskSchedulingMode.Standard;
public static bool HasScheduleConflict(
IEnumerable<ScheduleEntry> candidateEntries,
IEnumerable<ScheduleEntry> selectedEntries) =>
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);
public static int EffectiveCapacity(int originalCapacity, bool isRetake) =>
isRetake ? RetakeCapacity(originalCapacity) : originalCapacity;
}