调课
This commit is contained in:
@@ -3,6 +3,8 @@ using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Scheduling;
|
||||
using Jiaowu.Api.Infrastructure.Teaching;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -58,6 +60,114 @@ public sealed class CourseAdjustmentsController(
|
||||
.ToListAsync(cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("tasks/{teachingTaskId:guid}/session-options")]
|
||||
[Authorize(Roles = Applicants)]
|
||||
public async Task<ActionResult> GetSessionOptions(
|
||||
Guid teachingTaskId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await AccessibleTeachingTasks().AsNoTracking()
|
||||
.Where(x =>
|
||||
x.Id == teachingTaskId &&
|
||||
x.Status == TeachingTaskStatus.Published)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.TaskNumber,
|
||||
CourseName = x.Course!.Name,
|
||||
x.AcademicTermId,
|
||||
TermStartDate = x.AcademicTerm!.StartDate,
|
||||
TermEndDate = x.AcademicTerm.EndDate
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (task is null) return NotFound();
|
||||
|
||||
var entries = await db.ScheduleEntries.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.TeachingTaskId == teachingTaskId &&
|
||||
x.SchedulePlan!.Status == SchedulePlanStatus.Published)
|
||||
.OrderBy(x => x.DayOfWeek)
|
||||
.ThenBy(x => x.StartPeriod)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.DayOfWeek,
|
||||
x.StartPeriod,
|
||||
x.PeriodCount,
|
||||
x.StartWeek,
|
||||
x.EndWeek,
|
||||
x.WeekPattern,
|
||||
x.ClassroomId,
|
||||
ClassroomName = x.Classroom != null ? x.Classroom.Name : null,
|
||||
BuildingName = x.Classroom != null ? x.Classroom.Building!.Name : null
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var occupied = await db.CourseAdjustments.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.TeachingTaskId == teachingTaskId &&
|
||||
x.SourceScheduleEntryId != null &&
|
||||
x.SourceWeek != null &&
|
||||
(x.Status == CourseAdjustmentStatus.Submitted ||
|
||||
x.Status == CourseAdjustmentStatus.Approved))
|
||||
.Select(x => new { x.SourceScheduleEntryId, x.SourceWeek })
|
||||
.ToListAsync(cancellationToken);
|
||||
var occupiedOccurrences = occupied
|
||||
.Select(x => (x.SourceScheduleEntryId!.Value, x.SourceWeek!.Value))
|
||||
.ToHashSet();
|
||||
|
||||
var sessions = entries
|
||||
.SelectMany(entry => Enumerable.Range(
|
||||
entry.StartWeek,
|
||||
entry.EndWeek - entry.StartWeek + 1)
|
||||
.Where(week => IncludesWeek(entry.WeekPattern, week))
|
||||
.Select(week => new
|
||||
{
|
||||
ScheduleEntryId = entry.Id,
|
||||
Week = week,
|
||||
Date = ResolveDate(task.TermStartDate, week, entry.DayOfWeek),
|
||||
entry.DayOfWeek,
|
||||
entry.StartPeriod,
|
||||
entry.PeriodCount,
|
||||
entry.ClassroomId,
|
||||
entry.ClassroomName,
|
||||
entry.BuildingName,
|
||||
HasExistingAdjustment = occupiedOccurrences.Contains((entry.Id, week))
|
||||
}))
|
||||
.Where(x => x.Date >= task.TermStartDate && x.Date <= task.TermEndDate)
|
||||
.OrderBy(x => x.Date)
|
||||
.ThenBy(x => x.StartPeriod)
|
||||
.ToList();
|
||||
|
||||
var classrooms = await db.Classrooms.AsNoTracking()
|
||||
.Where(x => x.IsEnabled)
|
||||
.OrderBy(x => x.Building!.Campus!.SortOrder)
|
||||
.ThenBy(x => x.Building!.SortOrder)
|
||||
.ThenBy(x => x.SortOrder)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.Name,
|
||||
BuildingName = x.Building!.Name,
|
||||
CampusName = x.Building.Campus!.Name,
|
||||
x.Capacity
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var periods = await db.ScheduleTimeSlots.AsNoTracking()
|
||||
.Where(x => x.AcademicTermId == task.AcademicTermId && x.IsEnabled)
|
||||
.OrderBy(x => x.PeriodNumber)
|
||||
.Select(x => new
|
||||
{
|
||||
x.PeriodNumber,
|
||||
x.Name,
|
||||
StartsAt = x.StartsAt.ToString("HH:mm"),
|
||||
EndsAt = x.EndsAt.ToString("HH:mm")
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(new { Task = task, Sessions = sessions, Classrooms = classrooms, Periods = periods });
|
||||
}
|
||||
|
||||
// ═══════════════ My adjustments ═══════════════
|
||||
|
||||
[HttpGet("mine")]
|
||||
@@ -131,20 +241,41 @@ public sealed class CourseAdjustmentsController(
|
||||
var validation = await ValidateRequestAsync(request, cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
|
||||
var source = RequiresSourceOccurrence(request.Type)
|
||||
? await LoadSourceOccurrenceAsync(
|
||||
request.TeachingTaskId,
|
||||
request.SourceScheduleEntryId,
|
||||
request.SourceWeek,
|
||||
cancellationToken)
|
||||
: null;
|
||||
var targetDayOfWeek = request.TargetDate.HasValue
|
||||
? ToIsoDayOfWeek(request.TargetDate.Value.DayOfWeek)
|
||||
: request.DayOfWeek;
|
||||
|
||||
var adj = new CourseAdjustment
|
||||
{
|
||||
TeachingTaskId = request.TeachingTaskId,
|
||||
Type = request.Type,
|
||||
ApplicantUserId = userId,
|
||||
Reason = request.Reason.Trim(),
|
||||
SourceScheduleEntryId = source?.Entry.Id,
|
||||
SourceWeek = source?.Week,
|
||||
SourceDate = source?.Date,
|
||||
SourceStartPeriod = source?.Entry.StartPeriod,
|
||||
SourcePeriodCount = source?.Entry.PeriodCount,
|
||||
SourceClassroomId = source?.Entry.ClassroomId,
|
||||
TargetDate = request.TargetDate,
|
||||
DayOfWeek = request.DayOfWeek,
|
||||
DayOfWeek = targetDayOfWeek,
|
||||
StartPeriod = request.StartPeriod,
|
||||
PeriodCount = request.PeriodCount,
|
||||
PeriodCount = source?.Entry.PeriodCount ?? request.PeriodCount,
|
||||
ClassroomId = request.ClassroomId,
|
||||
SubstituteTeacherId = request.SubstituteTeacherId,
|
||||
CancelWeek = request.CancelWeek,
|
||||
CancelDate = request.CancelDate
|
||||
CancelWeek = request.Type == CourseAdjustmentType.Cancel
|
||||
? source?.Week
|
||||
: request.CancelWeek,
|
||||
CancelDate = request.Type == CourseAdjustmentType.Cancel
|
||||
? source?.Date
|
||||
: request.CancelDate
|
||||
};
|
||||
|
||||
if (request.Submit)
|
||||
@@ -193,6 +324,25 @@ public sealed class CourseAdjustmentsController(
|
||||
if (adj.Status != CourseAdjustmentStatus.Draft)
|
||||
return ConflictProblem("只有草稿可以提交。");
|
||||
|
||||
var validation = await ValidateRequestAsync(
|
||||
new CourseAdjustmentRequest(
|
||||
adj.TeachingTaskId,
|
||||
adj.Type,
|
||||
adj.Reason,
|
||||
true,
|
||||
adj.TargetDate,
|
||||
adj.DayOfWeek,
|
||||
adj.StartPeriod,
|
||||
adj.PeriodCount,
|
||||
adj.ClassroomId,
|
||||
adj.SubstituteTeacherId,
|
||||
adj.CancelWeek,
|
||||
adj.CancelDate,
|
||||
adj.SourceScheduleEntryId,
|
||||
adj.SourceWeek),
|
||||
cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
|
||||
adj.Status = CourseAdjustmentStatus.Submitted;
|
||||
adj.SubmittedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
@@ -224,6 +374,12 @@ public sealed class CourseAdjustmentsController(
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Course)
|
||||
.ThenInclude(x => x!.College)
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Teachers)
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Classes)
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.AcademicTerm)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (adj is null) return NotFound();
|
||||
|
||||
@@ -234,6 +390,12 @@ public sealed class CourseAdjustmentsController(
|
||||
if (adj.Status != CourseAdjustmentStatus.Submitted)
|
||||
return ConflictProblem("只有待审核的申请可以审批。");
|
||||
|
||||
var scheduleProblem = await ValidateApprovalScheduleAsync(
|
||||
adj,
|
||||
cancellationToken);
|
||||
if (scheduleProblem is not null)
|
||||
return ConflictProblem(scheduleProblem);
|
||||
|
||||
adj.Status = CourseAdjustmentStatus.Approved;
|
||||
adj.ReviewedAt = DateTime.UtcNow;
|
||||
adj.ReviewedByUserId = currentUserDataScope.Current.UserId;
|
||||
@@ -251,11 +413,9 @@ public sealed class CourseAdjustmentsController(
|
||||
NotificationCategory.Schedule);
|
||||
|
||||
// Notify affected students
|
||||
var studentUserIds = await db.CourseEnrollments
|
||||
.Where(x =>
|
||||
x.CourseSelectionOffering!.TeachingTaskId == adj.TeachingTaskId &&
|
||||
x.Status == CourseEnrollmentStatus.Enrolled)
|
||||
.Select(x => x.Student!.UserId)
|
||||
var studentUserIds = await TeachingTaskRosterQuery
|
||||
.ForTask(db, adj.TeachingTaskId)
|
||||
.Select(x => x.UserId)
|
||||
.Where(uid => uid != null)
|
||||
.Select(uid => uid!.Value)
|
||||
.Distinct()
|
||||
@@ -278,81 +438,19 @@ public sealed class CourseAdjustmentsController(
|
||||
switch (adj.Type)
|
||||
{
|
||||
case CourseAdjustmentType.Reschedule:
|
||||
// Update existing schedule entries for this teaching task
|
||||
if (adj.DayOfWeek.HasValue && adj.StartPeriod.HasValue)
|
||||
{
|
||||
var entries = await db.ScheduleEntries
|
||||
.Where(x => x.TeachingTaskId == adj.TeachingTaskId)
|
||||
.ToListAsync(ct);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
entry.DayOfWeek = adj.DayOfWeek.Value;
|
||||
entry.StartPeriod = adj.StartPeriod.Value;
|
||||
entry.PeriodCount = adj.PeriodCount ?? entry.PeriodCount;
|
||||
}
|
||||
if (adj.ClassroomId.HasValue)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
entry.ClassroomId = adj.ClassroomId.Value;
|
||||
}
|
||||
}
|
||||
var rescheduleSource = await LoadSourceEntryAsync(adj, ct);
|
||||
ExcludeWeek(rescheduleSource, adj.SourceWeek!.Value);
|
||||
db.ScheduleEntries.Add(CreateTargetEntry(adj, rescheduleSource.SchedulePlanId));
|
||||
break;
|
||||
|
||||
case CourseAdjustmentType.Cancel:
|
||||
// Cancel: remove schedule entries for the specified week
|
||||
if (adj.CancelWeek.HasValue)
|
||||
{
|
||||
var entries = await db.ScheduleEntries
|
||||
.Where(x => x.TeachingTaskId == adj.TeachingTaskId &&
|
||||
x.StartWeek <= adj.CancelWeek.Value &&
|
||||
x.EndWeek >= adj.CancelWeek.Value)
|
||||
.ToListAsync(ct);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
// Split the entry to exclude the cancelled week
|
||||
if (entry.StartWeek == adj.CancelWeek.Value &&
|
||||
entry.EndWeek == adj.CancelWeek.Value)
|
||||
{
|
||||
db.ScheduleEntries.Remove(entry);
|
||||
}
|
||||
else if (entry.StartWeek == adj.CancelWeek.Value)
|
||||
{
|
||||
entry.StartWeek = adj.CancelWeek.Value + 1;
|
||||
}
|
||||
else if (entry.EndWeek == adj.CancelWeek.Value)
|
||||
{
|
||||
entry.EndWeek = adj.CancelWeek.Value - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
var cancelSource = await LoadSourceEntryAsync(adj, ct);
|
||||
ExcludeWeek(cancelSource, adj.SourceWeek!.Value);
|
||||
break;
|
||||
|
||||
case CourseAdjustmentType.Makeup:
|
||||
// Makeup: add a temp schedule entry for the makeup date
|
||||
if (adj.TargetDate.HasValue && adj.DayOfWeek.HasValue &&
|
||||
adj.StartPeriod.HasValue)
|
||||
{
|
||||
var publishedPlan = await db.SchedulePlans
|
||||
.Where(x => x.AcademicTermId == adj.TeachingTask!.AcademicTermId &&
|
||||
x.Status == SchedulePlanStatus.Published)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (publishedPlan is not null)
|
||||
{
|
||||
db.ScheduleEntries.Add(new ScheduleEntry
|
||||
{
|
||||
SchedulePlanId = publishedPlan.Id,
|
||||
TeachingTaskId = adj.TeachingTaskId,
|
||||
ClassroomId = adj.ClassroomId,
|
||||
DayOfWeek = adj.DayOfWeek.Value,
|
||||
StartPeriod = adj.StartPeriod.Value,
|
||||
PeriodCount = adj.PeriodCount ?? 2,
|
||||
StartWeek = 1,
|
||||
EndWeek = 1,
|
||||
WeekPattern = WeekPattern.All,
|
||||
Notes = $"补课(原申请 {adj.CreatedAt:yyyy-MM-dd})"
|
||||
});
|
||||
}
|
||||
}
|
||||
var makeupSource = await LoadSourceEntryAsync(adj, ct);
|
||||
db.ScheduleEntries.Add(CreateTargetEntry(adj, makeupSource.SchedulePlanId));
|
||||
break;
|
||||
|
||||
case CourseAdjustmentType.Substitute:
|
||||
@@ -442,15 +540,13 @@ public sealed class CourseAdjustmentsController(
|
||||
{
|
||||
case CourseAdjustmentType.Reschedule:
|
||||
case CourseAdjustmentType.Makeup:
|
||||
if (!request.DayOfWeek.HasValue || request.DayOfWeek is < 1 or > 7)
|
||||
return ValidationProblem("请选择有效的上课日。");
|
||||
if (!request.TargetDate.HasValue)
|
||||
return ValidationProblem(
|
||||
request.Type == CourseAdjustmentType.Reschedule
|
||||
? "请选择调课后的日期。"
|
||||
: "请选择补课日期。");
|
||||
if (!request.StartPeriod.HasValue || request.StartPeriod < 1)
|
||||
return ValidationProblem("请选择起始节次。");
|
||||
if (!request.PeriodCount.HasValue || request.PeriodCount < 1)
|
||||
return ValidationProblem("请选择持续节数。");
|
||||
if (request.Type == CourseAdjustmentType.Makeup &&
|
||||
!request.TargetDate.HasValue)
|
||||
return ValidationProblem("补课必须指定日期。");
|
||||
break;
|
||||
case CourseAdjustmentType.Substitute:
|
||||
if (!request.SubstituteTeacherId.HasValue)
|
||||
@@ -461,14 +557,226 @@ public sealed class CourseAdjustmentsController(
|
||||
return ValidationProblem("代课教师不存在或已离职。");
|
||||
break;
|
||||
case CourseAdjustmentType.Cancel:
|
||||
if (!request.CancelWeek.HasValue && !request.CancelDate.HasValue)
|
||||
return ValidationProblem("停课需指定周次或日期。");
|
||||
break;
|
||||
}
|
||||
|
||||
if (RequiresSourceOccurrence(request.Type))
|
||||
{
|
||||
var source = await LoadSourceOccurrenceAsync(
|
||||
request.TeachingTaskId,
|
||||
request.SourceScheduleEntryId,
|
||||
request.SourceWeek,
|
||||
cancellationToken);
|
||||
if (source is null)
|
||||
return ValidationProblem("请选择当前已发布课表中的具体原课次。");
|
||||
|
||||
var duplicate = await db.CourseAdjustments.AsNoTracking().AnyAsync(x =>
|
||||
x.SourceScheduleEntryId == source.Entry.Id &&
|
||||
x.SourceWeek == source.Week &&
|
||||
(x.Status == CourseAdjustmentStatus.Submitted ||
|
||||
x.Status == CourseAdjustmentStatus.Approved),
|
||||
cancellationToken);
|
||||
if (duplicate)
|
||||
return ConflictProblem("该课次已有待审核或已通过的调停课记录。");
|
||||
|
||||
if (request.TargetDate.HasValue &&
|
||||
(request.TargetDate < source.Term.StartDate ||
|
||||
request.TargetDate > source.Term.EndDate))
|
||||
return ValidationProblem("目标日期必须在本学期起止日期内。");
|
||||
}
|
||||
|
||||
if (request.ClassroomId.HasValue &&
|
||||
!await db.Classrooms.AsNoTracking().AnyAsync(x =>
|
||||
x.Id == request.ClassroomId && x.IsEnabled,
|
||||
cancellationToken))
|
||||
return ValidationProblem("目标教室不存在或已停用。");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string?> ValidateApprovalScheduleAsync(
|
||||
CourseAdjustment adjustment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!RequiresSourceOccurrence(adjustment.Type)) return null;
|
||||
|
||||
var source = await LoadSourceOccurrenceAsync(
|
||||
adjustment.TeachingTaskId,
|
||||
adjustment.SourceScheduleEntryId,
|
||||
adjustment.SourceWeek,
|
||||
cancellationToken);
|
||||
if (source is null)
|
||||
return "原课次已不在当前发布课表中,请退回后重新选择。";
|
||||
if (adjustment.Type == CourseAdjustmentType.Cancel) return null;
|
||||
if (!adjustment.TargetDate.HasValue || !adjustment.StartPeriod.HasValue)
|
||||
return "目标上课日期或节次不完整。";
|
||||
|
||||
var targetWeek = ResolveWeek(source.Term.StartDate, adjustment.TargetDate.Value);
|
||||
var targetDay = ToIsoDayOfWeek(adjustment.TargetDate.Value.DayOfWeek);
|
||||
var target = new ScheduleEntry
|
||||
{
|
||||
TeachingTaskId = adjustment.TeachingTaskId,
|
||||
TeachingTask = adjustment.TeachingTask,
|
||||
ClassroomId = adjustment.ClassroomId ?? adjustment.SourceClassroomId,
|
||||
DayOfWeek = targetDay,
|
||||
StartPeriod = adjustment.StartPeriod.Value,
|
||||
PeriodCount = adjustment.SourcePeriodCount ?? source.Entry.PeriodCount,
|
||||
StartWeek = targetWeek,
|
||||
EndWeek = targetWeek,
|
||||
WeekPattern = WeekPattern.All
|
||||
};
|
||||
var possibleConflicts = await db.ScheduleEntries.AsNoTracking()
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Teachers)
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Classes)
|
||||
.Where(x =>
|
||||
x.SchedulePlan!.Status == SchedulePlanStatus.Published &&
|
||||
x.Id != adjustment.SourceScheduleEntryId &&
|
||||
x.DayOfWeek == targetDay &&
|
||||
x.StartWeek <= targetWeek &&
|
||||
x.EndWeek >= targetWeek &&
|
||||
x.StartPeriod < target.StartPeriod + target.PeriodCount &&
|
||||
target.StartPeriod < x.StartPeriod + x.PeriodCount)
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var existing in possibleConflicts.Where(x =>
|
||||
IncludesWeek(x.WeekPattern, targetWeek)))
|
||||
{
|
||||
var reason = ScheduleConflictDetector.ConflictReason(target, existing);
|
||||
if (reason is not null)
|
||||
return $"目标时间存在{reason}冲突:{existing.TeachingTask!.Name}。";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<SourceOccurrence?> LoadSourceOccurrenceAsync(
|
||||
Guid teachingTaskId,
|
||||
Guid? scheduleEntryId,
|
||||
int? week,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!scheduleEntryId.HasValue || !week.HasValue) return null;
|
||||
var entry = await db.ScheduleEntries.AsNoTracking()
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.AcademicTerm)
|
||||
.FirstOrDefaultAsync(x =>
|
||||
x.Id == scheduleEntryId.Value &&
|
||||
x.TeachingTaskId == teachingTaskId &&
|
||||
x.SchedulePlan!.Status == SchedulePlanStatus.Published,
|
||||
cancellationToken);
|
||||
if (entry?.TeachingTask?.AcademicTerm is null ||
|
||||
week < entry.StartWeek || week > entry.EndWeek ||
|
||||
!IncludesWeek(entry.WeekPattern, week.Value))
|
||||
return null;
|
||||
var date = ResolveDate(
|
||||
entry.TeachingTask.AcademicTerm.StartDate,
|
||||
week.Value,
|
||||
entry.DayOfWeek);
|
||||
if (date < entry.TeachingTask.AcademicTerm.StartDate ||
|
||||
date > entry.TeachingTask.AcademicTerm.EndDate)
|
||||
return null;
|
||||
return new SourceOccurrence(
|
||||
entry,
|
||||
entry.TeachingTask.AcademicTerm,
|
||||
week.Value,
|
||||
date);
|
||||
}
|
||||
|
||||
private Task<ScheduleEntry> LoadSourceEntryAsync(
|
||||
CourseAdjustment adjustment,
|
||||
CancellationToken cancellationToken) =>
|
||||
db.ScheduleEntries.SingleAsync(x =>
|
||||
x.Id == adjustment.SourceScheduleEntryId &&
|
||||
x.TeachingTaskId == adjustment.TeachingTaskId &&
|
||||
x.SchedulePlan!.Status == SchedulePlanStatus.Published,
|
||||
cancellationToken);
|
||||
|
||||
private void ExcludeWeek(ScheduleEntry entry, int week)
|
||||
{
|
||||
db.ScheduleEntries.Remove(entry);
|
||||
if (entry.StartWeek <= week - 1 &&
|
||||
HasIncludedWeek(entry.WeekPattern, entry.StartWeek, week - 1))
|
||||
db.ScheduleEntries.Add(CloneEntry(entry, entry.StartWeek, week - 1));
|
||||
if (week + 1 <= entry.EndWeek &&
|
||||
HasIncludedWeek(entry.WeekPattern, week + 1, entry.EndWeek))
|
||||
db.ScheduleEntries.Add(CloneEntry(entry, week + 1, entry.EndWeek));
|
||||
}
|
||||
|
||||
private ScheduleEntry CreateTargetEntry(
|
||||
CourseAdjustment adjustment,
|
||||
Guid schedulePlanId)
|
||||
{
|
||||
var targetWeek = ResolveWeek(
|
||||
adjustment.TeachingTask!.AcademicTerm!.StartDate,
|
||||
adjustment.TargetDate!.Value);
|
||||
return new ScheduleEntry
|
||||
{
|
||||
SchedulePlanId = schedulePlanId,
|
||||
TeachingTaskId = adjustment.TeachingTaskId,
|
||||
ClassroomId = adjustment.ClassroomId ?? adjustment.SourceClassroomId,
|
||||
DayOfWeek = ToIsoDayOfWeek(adjustment.TargetDate.Value.DayOfWeek),
|
||||
StartPeriod = adjustment.StartPeriod!.Value,
|
||||
PeriodCount = adjustment.SourcePeriodCount!.Value,
|
||||
StartWeek = targetWeek,
|
||||
EndWeek = targetWeek,
|
||||
WeekPattern = WeekPattern.All,
|
||||
Notes = adjustment.Type == CourseAdjustmentType.Reschedule
|
||||
? $"调课(原第 {adjustment.SourceWeek} 周)"
|
||||
: $"补课(对应原第 {adjustment.SourceWeek} 周课次)"
|
||||
};
|
||||
}
|
||||
|
||||
private static ScheduleEntry CloneEntry(
|
||||
ScheduleEntry source,
|
||||
int startWeek,
|
||||
int endWeek) => new()
|
||||
{
|
||||
SchedulePlanId = source.SchedulePlanId,
|
||||
TeachingTaskId = source.TeachingTaskId,
|
||||
ClassroomId = source.ClassroomId,
|
||||
DayOfWeek = source.DayOfWeek,
|
||||
StartPeriod = source.StartPeriod,
|
||||
PeriodCount = source.PeriodCount,
|
||||
StartWeek = startWeek,
|
||||
EndWeek = endWeek,
|
||||
WeekPattern = source.WeekPattern,
|
||||
Notes = source.Notes
|
||||
};
|
||||
|
||||
private static bool RequiresSourceOccurrence(CourseAdjustmentType type) =>
|
||||
type is CourseAdjustmentType.Reschedule or
|
||||
CourseAdjustmentType.Cancel or
|
||||
CourseAdjustmentType.Makeup;
|
||||
|
||||
private static bool IncludesWeek(WeekPattern pattern, int week) =>
|
||||
pattern == WeekPattern.All ||
|
||||
pattern == WeekPattern.Odd && week % 2 == 1 ||
|
||||
pattern == WeekPattern.Even && week % 2 == 0;
|
||||
|
||||
private static bool HasIncludedWeek(WeekPattern pattern, int startWeek, int endWeek) =>
|
||||
Enumerable.Range(startWeek, endWeek - startWeek + 1)
|
||||
.Any(week => IncludesWeek(pattern, week));
|
||||
|
||||
private static DateOnly ResolveDate(DateOnly termStartDate, int week, int dayOfWeek)
|
||||
{
|
||||
var startDay = (int)termStartDate.DayOfWeek;
|
||||
var daysSinceMonday = (startDay + 6) % 7;
|
||||
var firstWeekMonday = termStartDate.AddDays(-daysSinceMonday);
|
||||
return firstWeekMonday.AddDays((week - 1) * 7 + dayOfWeek - 1);
|
||||
}
|
||||
|
||||
private static int ResolveWeek(DateOnly termStartDate, DateOnly date)
|
||||
{
|
||||
var startDay = (int)termStartDate.DayOfWeek;
|
||||
var daysSinceMonday = (startDay + 6) % 7;
|
||||
var firstWeekMonday = termStartDate.AddDays(-daysSinceMonday);
|
||||
return (date.DayNumber - firstWeekMonday.DayNumber) / 7 + 1;
|
||||
}
|
||||
|
||||
private static int ToIsoDayOfWeek(DayOfWeek dayOfWeek) =>
|
||||
dayOfWeek == System.DayOfWeek.Sunday ? 7 : (int)dayOfWeek;
|
||||
|
||||
private IQueryable<TeachingTask> AccessibleTeachingTasks()
|
||||
{
|
||||
var source = db.TeachingTasks.AsQueryable();
|
||||
@@ -483,7 +791,7 @@ public sealed class CourseAdjustmentsController(
|
||||
return source.Where(_ => false);
|
||||
}
|
||||
|
||||
private static System.Linq.Expressions.Expression<
|
||||
private System.Linq.Expressions.Expression<
|
||||
Func<CourseAdjustment, object>> AdjustmentProjection() => x => new
|
||||
{
|
||||
x.Id,
|
||||
@@ -500,6 +808,17 @@ public sealed class CourseAdjustmentsController(
|
||||
x.Status,
|
||||
x.Reason,
|
||||
x.ReviewComment,
|
||||
x.SourceScheduleEntryId,
|
||||
x.SourceWeek,
|
||||
x.SourceDate,
|
||||
x.SourceStartPeriod,
|
||||
x.SourcePeriodCount,
|
||||
SourceClassroomName = x.SourceClassroomId == null
|
||||
? null
|
||||
: db.Classrooms
|
||||
.Where(room => room.Id == x.SourceClassroomId)
|
||||
.Select(room => room.Building!.Name + " " + room.Name)
|
||||
.FirstOrDefault(),
|
||||
x.TargetDate,
|
||||
x.DayOfWeek,
|
||||
x.StartPeriod,
|
||||
@@ -515,6 +834,12 @@ public sealed class CourseAdjustmentsController(
|
||||
x.CreatedAt
|
||||
};
|
||||
|
||||
private sealed record SourceOccurrence(
|
||||
ScheduleEntry Entry,
|
||||
AcademicTerm Term,
|
||||
int Week,
|
||||
DateOnly Date);
|
||||
|
||||
private ActionResult ConflictProblem(string detail) =>
|
||||
Conflict(new ProblemDetails
|
||||
{
|
||||
@@ -538,7 +863,9 @@ public sealed record CourseAdjustmentRequest(
|
||||
Guid? ClassroomId,
|
||||
Guid? SubstituteTeacherId,
|
||||
int? CancelWeek,
|
||||
DateOnly? CancelDate);
|
||||
DateOnly? CancelDate,
|
||||
Guid? SourceScheduleEntryId = null,
|
||||
int? SourceWeek = null);
|
||||
|
||||
public sealed record RejectionRequest(
|
||||
[MaxLength(500)] string? Comment);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using Jiaowu.Api.Contracts;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
@@ -41,18 +42,38 @@ public sealed class GradesController(
|
||||
public async Task<ActionResult> GetSheets(
|
||||
Guid? academicTermId,
|
||||
GradeSheetStatus? status,
|
||||
CancellationToken cancellationToken)
|
||||
string? keyword = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 10, 50);
|
||||
var source = AccessibleTasks().AsNoTracking()
|
||||
.Where(x =>
|
||||
x.Status == TeachingTaskStatus.Published ||
|
||||
x.Status == TeachingTaskStatus.Closed);
|
||||
if (academicTermId.HasValue)
|
||||
source = source.Where(x => x.AcademicTermId == academicTermId);
|
||||
if (status.HasValue)
|
||||
source = source.Where(x => db.GradeSheets.Any(sheet =>
|
||||
sheet.TeachingTaskId == x.Id && sheet.Status == status.Value));
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
keyword = keyword.Trim();
|
||||
source = source.Where(x =>
|
||||
x.TaskNumber.Contains(keyword) ||
|
||||
x.Name.Contains(keyword) ||
|
||||
x.Course!.Code.Contains(keyword) ||
|
||||
x.Course.Name.Contains(keyword));
|
||||
}
|
||||
|
||||
var total = await source.CountAsync(cancellationToken);
|
||||
var items = await source
|
||||
.OrderByDescending(x => x.AcademicTerm!.StartDate)
|
||||
.ThenBy(x => x.TaskNumber)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
@@ -96,10 +117,7 @@ public sealed class GradesController(
|
||||
.FirstOrDefault()
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (status.HasValue)
|
||||
items = items.Where(x => x.Sheet?.Status == status.Value).ToList();
|
||||
return Ok(items);
|
||||
return Ok(new PagedResult<object>(items, total, page, pageSize));
|
||||
}
|
||||
|
||||
[HttpPost("sheets")]
|
||||
@@ -164,8 +182,18 @@ public sealed class GradesController(
|
||||
|
||||
[HttpGet("sheets/{id:guid}")]
|
||||
[Authorize(Roles = SheetUsers)]
|
||||
public async Task<ActionResult> GetSheet(Guid id, CancellationToken cancellationToken)
|
||||
public async Task<ActionResult> GetSheet(
|
||||
Guid id,
|
||||
int recordPage = 1,
|
||||
int recordPageSize = 50,
|
||||
string? studentKeyword = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
recordPage = Math.Max(1, recordPage);
|
||||
recordPageSize = Math.Clamp(recordPageSize, 10, 100);
|
||||
studentKeyword = string.IsNullOrWhiteSpace(studentKeyword)
|
||||
? null
|
||||
: studentKeyword.Trim();
|
||||
var sheet = await AccessibleSheets().AsNoTracking()
|
||||
.Where(x => x.Id == id)
|
||||
.Select(x => new
|
||||
@@ -199,37 +227,49 @@ public sealed class GradesController(
|
||||
x.SubmittedAt,
|
||||
x.ReviewedAt,
|
||||
x.PublishedAt,
|
||||
Records = x.Records
|
||||
.OrderBy(record => record.Student!.StudentNumber)
|
||||
.Select(record => new
|
||||
{
|
||||
record.Id,
|
||||
record.StudentId,
|
||||
record.Student!.StudentNumber,
|
||||
record.Student.Name,
|
||||
ClassName = record.Student.AdministrativeClass!.Name,
|
||||
record.RegularScore,
|
||||
record.FinalScore,
|
||||
ItemScores = record.ItemScores
|
||||
.OrderBy(itemScore => itemScore.GradeItem!.SortOrder)
|
||||
.Select(itemScore => new
|
||||
{
|
||||
itemScore.GradeItemId,
|
||||
itemScore.GradeItem!.Name,
|
||||
itemScore.Score
|
||||
}),
|
||||
record.TotalScore,
|
||||
record.GradePoint,
|
||||
record.ExamStatus,
|
||||
record.Notes,
|
||||
record.UpdatedAt
|
||||
}),
|
||||
x.CreatedAt,
|
||||
x.UpdatedAt
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
|
||||
var recordsSource = db.GradeRecords.AsNoTracking()
|
||||
.Where(record => record.GradeSheetId == id);
|
||||
if (studentKeyword is not null)
|
||||
recordsSource = recordsSource.Where(record =>
|
||||
record.Student!.StudentNumber.Contains(studentKeyword) ||
|
||||
record.Student.Name.Contains(studentKeyword) ||
|
||||
record.Student.AdministrativeClass!.Name.Contains(studentKeyword));
|
||||
var recordTotal = await recordsSource.CountAsync(cancellationToken);
|
||||
var records = await recordsSource
|
||||
.OrderBy(record => record.Student!.StudentNumber)
|
||||
.Skip((recordPage - 1) * recordPageSize)
|
||||
.Take(recordPageSize)
|
||||
.Select(record => new
|
||||
{
|
||||
record.Id,
|
||||
record.StudentId,
|
||||
record.Student!.StudentNumber,
|
||||
record.Student.Name,
|
||||
ClassName = record.Student.AdministrativeClass!.Name,
|
||||
record.RegularScore,
|
||||
record.FinalScore,
|
||||
ItemScores = record.ItemScores
|
||||
.OrderBy(itemScore => itemScore.GradeItem!.SortOrder)
|
||||
.Select(itemScore => new
|
||||
{
|
||||
itemScore.GradeItemId,
|
||||
itemScore.GradeItem!.Name,
|
||||
itemScore.Score
|
||||
}),
|
||||
record.TotalScore,
|
||||
record.GradePoint,
|
||||
record.ExamStatus,
|
||||
record.Notes,
|
||||
record.UpdatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var task = await db.TeachingTasks.AsNoTracking()
|
||||
.Include(x => x.Teachers)
|
||||
.ThenInclude(x => x.Teacher)
|
||||
@@ -243,7 +283,36 @@ public sealed class GradesController(
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Sheet = sheet,
|
||||
Sheet = new
|
||||
{
|
||||
sheet.Id,
|
||||
sheet.TeachingTaskId,
|
||||
sheet.TaskNumber,
|
||||
sheet.TaskName,
|
||||
sheet.AcademicTermId,
|
||||
sheet.TermName,
|
||||
sheet.CourseCode,
|
||||
sheet.CourseName,
|
||||
sheet.CourseCollegeId,
|
||||
sheet.CourseCollegeName,
|
||||
sheet.Credits,
|
||||
sheet.TeacherNames,
|
||||
sheet.ClassNames,
|
||||
sheet.RegularWeight,
|
||||
sheet.FinalWeight,
|
||||
sheet.Items,
|
||||
sheet.Status,
|
||||
sheet.ReviewComment,
|
||||
sheet.SubmittedAt,
|
||||
sheet.ReviewedAt,
|
||||
sheet.PublishedAt,
|
||||
RecordTotal = recordTotal,
|
||||
RecordPage = recordPage,
|
||||
RecordPageSize = recordPageSize,
|
||||
Records = records,
|
||||
sheet.CreatedAt,
|
||||
sheet.UpdatedAt
|
||||
},
|
||||
CanEdit = CanEditScores(task) &&
|
||||
sheet.Status is GradeSheetStatus.Draft or GradeSheetStatus.Returned,
|
||||
CanReview = isCourseCollegeReviewer && sheet.Status == GradeSheetStatus.Submitted,
|
||||
|
||||
@@ -10,7 +10,15 @@ public sealed class CourseAdjustment : EntityBase
|
||||
public CourseAdjustmentStatus Status { get; set; } = CourseAdjustmentStatus.Draft;
|
||||
public Guid ApplicantUserId { get; set; }
|
||||
|
||||
// For Reschedule / Makeup
|
||||
// The concrete published timetable occurrence being adjusted.
|
||||
public Guid? SourceScheduleEntryId { get; set; }
|
||||
public int? SourceWeek { get; set; }
|
||||
public DateOnly? SourceDate { get; set; }
|
||||
public int? SourceStartPeriod { get; set; }
|
||||
public int? SourcePeriodCount { get; set; }
|
||||
public Guid? SourceClassroomId { get; set; }
|
||||
|
||||
// Target occurrence for Reschedule / Makeup
|
||||
public DateOnly? TargetDate { get; set; }
|
||||
public int? DayOfWeek { get; set; }
|
||||
public int? StartPeriod { get; set; }
|
||||
|
||||
@@ -953,6 +953,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
entity.HasIndex(x => new { x.TeachingTaskId, x.Status });
|
||||
entity.HasIndex(x => x.ApplicantUserId);
|
||||
entity.HasIndex(x => new { x.Status, x.CreatedAt });
|
||||
entity.HasIndex(x => new { x.SourceScheduleEntryId, x.SourceWeek });
|
||||
entity.HasOne(x => x.TeachingTask).WithMany()
|
||||
.HasForeignKey(x => x.TeachingTaskId).OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(x => x.Classroom).WithMany()
|
||||
|
||||
@@ -60,6 +60,8 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"20260726_32_classroom_reservations";
|
||||
private const string BackgroundJobOutboxMigration =
|
||||
"20260726_33_background_job_outbox";
|
||||
private const string CourseAdjustmentOccurrencesMigration =
|
||||
"20260727_34_course_adjustment_occurrences";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -278,6 +280,21 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
courseAdjustmentsExist ? [] : CourseAdjustmentsStatements,
|
||||
cancellationToken);
|
||||
|
||||
var courseAdjustmentOccurrencesExist = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"""
|
||||
SELECT COUNT(*) AS "Value"
|
||||
FROM pragma_table_info('CourseAdjustments')
|
||||
WHERE name = 'SourceScheduleEntryId'
|
||||
""")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
CourseAdjustmentOccurrencesMigration,
|
||||
courseAdjustmentOccurrencesExist
|
||||
? []
|
||||
: CourseAdjustmentOccurrencesStatements,
|
||||
cancellationToken);
|
||||
|
||||
var attendanceAppealExists = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"""
|
||||
@@ -1772,6 +1789,12 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"Type" INTEGER NOT NULL,
|
||||
"Status" INTEGER NOT NULL,
|
||||
"ApplicantUserId" TEXT NOT NULL,
|
||||
"SourceScheduleEntryId" TEXT NULL,
|
||||
"SourceWeek" INTEGER NULL,
|
||||
"SourceDate" TEXT NULL,
|
||||
"SourceStartPeriod" INTEGER NULL,
|
||||
"SourcePeriodCount" INTEGER NULL,
|
||||
"SourceClassroomId" TEXT NULL,
|
||||
"TargetDate" TEXT NULL,
|
||||
"DayOfWeek" INTEGER NULL,
|
||||
"StartPeriod" INTEGER NULL,
|
||||
@@ -1817,6 +1840,17 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"""CREATE INDEX "IX_Notifications_CreatedAt" ON "Notifications" ("CreatedAt");"""
|
||||
];
|
||||
|
||||
private static readonly string[] CourseAdjustmentOccurrencesStatements =
|
||||
[
|
||||
"""ALTER TABLE "CourseAdjustments" ADD "SourceScheduleEntryId" TEXT NULL;""",
|
||||
"""ALTER TABLE "CourseAdjustments" ADD "SourceWeek" INTEGER NULL;""",
|
||||
"""ALTER TABLE "CourseAdjustments" ADD "SourceDate" TEXT NULL;""",
|
||||
"""ALTER TABLE "CourseAdjustments" ADD "SourceStartPeriod" INTEGER NULL;""",
|
||||
"""ALTER TABLE "CourseAdjustments" ADD "SourcePeriodCount" INTEGER NULL;""",
|
||||
"""ALTER TABLE "CourseAdjustments" ADD "SourceClassroomId" TEXT NULL;""",
|
||||
"""CREATE INDEX "IX_CourseAdjustments_SourceScheduleEntryId_SourceWeek" ON "CourseAdjustments" ("SourceScheduleEntryId", "SourceWeek");"""
|
||||
];
|
||||
|
||||
private static readonly string[] AttendanceAppealStatements =
|
||||
[
|
||||
"""ALTER TABLE "AttendanceRecords" ADD COLUMN "AppealStatus" INTEGER NOT NULL DEFAULT 0;""",
|
||||
|
||||
+4867
File diff suppressed because it is too large
Load Diff
+88
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CourseAdjustmentOccurrences : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "SourceClassroomId",
|
||||
table: "CourseAdjustments",
|
||||
type: "char(36)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "SourceDate",
|
||||
table: "CourseAdjustments",
|
||||
type: "date",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SourcePeriodCount",
|
||||
table: "CourseAdjustments",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "SourceScheduleEntryId",
|
||||
table: "CourseAdjustments",
|
||||
type: "char(36)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SourceStartPeriod",
|
||||
table: "CourseAdjustments",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SourceWeek",
|
||||
table: "CourseAdjustments",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CourseAdjustments_SourceScheduleEntryId_SourceWeek",
|
||||
table: "CourseAdjustments",
|
||||
columns: new[] { "SourceScheduleEntryId", "SourceWeek" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_CourseAdjustments_SourceScheduleEntryId_SourceWeek",
|
||||
table: "CourseAdjustments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceClassroomId",
|
||||
table: "CourseAdjustments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceDate",
|
||||
table: "CourseAdjustments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourcePeriodCount",
|
||||
table: "CourseAdjustments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceScheduleEntryId",
|
||||
table: "CourseAdjustments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceStartPeriod",
|
||||
table: "CourseAdjustments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SourceWeek",
|
||||
table: "CourseAdjustments");
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-2
@@ -714,6 +714,24 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Property<Guid?>("ReviewedByUserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("SourceClassroomId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("SourceDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<int?>("SourcePeriodCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("SourceScheduleEntryId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int?>("SourceStartPeriod")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("SourceWeek")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("StartPeriod")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -746,6 +764,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
|
||||
b.HasIndex("SubstituteTeacherId");
|
||||
|
||||
b.HasIndex("SourceScheduleEntryId", "SourceWeek");
|
||||
|
||||
b.HasIndex("Status", "CreatedAt");
|
||||
|
||||
b.HasIndex("TeachingTaskId", "Status");
|
||||
@@ -3473,10 +3493,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.HasIndex("JobKind", "JobId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("State", "CreatedAt");
|
||||
|
||||
b.HasIndex("State", "CompletedAt");
|
||||
|
||||
b.HasIndex("State", "CreatedAt");
|
||||
|
||||
b.ToTable("BackgroundJobOutboxMessages");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user