实验课表
This commit is contained in:
1 parent
ec979f640d
commit
51c3b81e27
7 files changed
+686
-27
No files matched your search
@@ -115,6 +115,7 @@ public sealed class PersonalCalendarService(
|
|||||||
|
|
||||||
AddScheduledCourses(builder, timetable);
|
AddScheduledCourses(builder, timetable);
|
||||||
AddExamEntries(builder, timetable, teacher is not null);
|
AddExamEntries(builder, timetable, teacher is not null);
|
||||||
|
AddExperimentEntries(builder, timetable);
|
||||||
AddFlexibleCourseReminders(builder, timetable);
|
AddFlexibleCourseReminders(builder, timetable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +247,46 @@ public sealed class PersonalCalendarService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddExperimentEntries(
|
||||||
|
IcsBuilder builder,
|
||||||
|
TimetableData timetable)
|
||||||
|
{
|
||||||
|
var slots = timetable.Slots.ToDictionary(x => x.PeriodNumber);
|
||||||
|
foreach (var entry in timetable.ExperimentEntries)
|
||||||
|
{
|
||||||
|
if (entry.ExperimentDate is not DateOnly experimentDate ||
|
||||||
|
!slots.TryGetValue(entry.StartPeriod, out var startSlot) ||
|
||||||
|
!slots.TryGetValue(
|
||||||
|
entry.StartPeriod + entry.PeriodCount - 1,
|
||||||
|
out var endSlot))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var arrangement = entry.ExperimentArrangementMode switch
|
||||||
|
{
|
||||||
|
ExperimentArrangementMode.Centralized => "集中安排",
|
||||||
|
ExperimentArrangementMode.SelfScheduled => "自主预约",
|
||||||
|
_ => "实验安排"
|
||||||
|
};
|
||||||
|
builder.AddTimedEvent(
|
||||||
|
$"experiment-{entry.Id:N}@jiaowu",
|
||||||
|
$"[实验] {entry.ExperimentProjectName ?? entry.CourseName}",
|
||||||
|
ToUtc(experimentDate, startSlot.StartsAt),
|
||||||
|
ToUtc(experimentDate, endSlot.EndsAt),
|
||||||
|
JoinLocation(
|
||||||
|
entry.CampusName,
|
||||||
|
entry.BuildingName,
|
||||||
|
entry.ClassroomName),
|
||||||
|
JoinDescription(
|
||||||
|
$"{entry.ExperimentProjectCode} · {arrangement}",
|
||||||
|
entry.CourseName,
|
||||||
|
entry.TaskName,
|
||||||
|
$"教师:{string.Join('、', entry.TeacherNames)}",
|
||||||
|
entry.Notes),
|
||||||
|
"实验",
|
||||||
|
entry.UpdatedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<List<PersonalMakeupSession>> LoadMakeupSessionsAsync(
|
private async Task<List<PersonalMakeupSession>> LoadMakeupSessionsAsync(
|
||||||
Guid? studentId,
|
Guid? studentId,
|
||||||
Guid? teacherId,
|
Guid? teacherId,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Jiaowu.Api.Infrastructure.Teaching;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Jiaowu.Api.Infrastructure.Timetables;
|
namespace Jiaowu.Api.Infrastructure.Timetables;
|
||||||
@@ -117,6 +118,8 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
// Load exam sessions for student/teacher timetables
|
// Load exam sessions for student/teacher timetables
|
||||||
var examEntries = await LoadExamEntriesAsync(
|
var examEntries = await LoadExamEntriesAsync(
|
||||||
resourceType, resourceId, term.Id, studentId, slots, cancellationToken);
|
resourceType, resourceId, term.Id, studentId, slots, cancellationToken);
|
||||||
|
var experimentEntries = await LoadExperimentEntriesAsync(
|
||||||
|
resourceType, term, studentId, cancellationToken);
|
||||||
|
|
||||||
return new TimetableData(
|
return new TimetableData(
|
||||||
term,
|
term,
|
||||||
@@ -127,7 +130,10 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
slots,
|
slots,
|
||||||
entries,
|
entries,
|
||||||
flexibleCourses,
|
flexibleCourses,
|
||||||
examEntries);
|
examEntries)
|
||||||
|
{
|
||||||
|
ExperimentEntries = experimentEntries
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TimetableSubjectDto?> LoadSubjectAsync(
|
private async Task<TimetableSubjectDto?> LoadSubjectAsync(
|
||||||
@@ -420,6 +426,115 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<List<TimetableEntryDto>> LoadExperimentEntriesAsync(
|
||||||
|
TimetableResourceType resourceType,
|
||||||
|
TimetableTermDto term,
|
||||||
|
Guid? studentId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (resourceType != TimetableResourceType.Class || !studentId.HasValue)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var rosterTaskIds = TeachingTaskRosterQuery.TaskIdsForStudent(
|
||||||
|
db,
|
||||||
|
studentId.Value);
|
||||||
|
var sessions = await db.ExperimentSessions.AsNoTracking()
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Where(x =>
|
||||||
|
x.ExperimentProject!.TeachingTask!.AcademicTermId == term.Id &&
|
||||||
|
(x.ExperimentProject.Status ==
|
||||||
|
ExperimentProjectStatus.Published ||
|
||||||
|
x.ExperimentProject.Status ==
|
||||||
|
ExperimentProjectStatus.Closed) &&
|
||||||
|
x.Status == ExperimentSessionStatus.Scheduled)
|
||||||
|
.Where(x =>
|
||||||
|
(x.ExperimentProject!.ArrangementMode ==
|
||||||
|
ExperimentArrangementMode.Centralized &&
|
||||||
|
rosterTaskIds.Contains(
|
||||||
|
x.ExperimentProject.TeachingTaskId)) ||
|
||||||
|
(x.ExperimentProject.ArrangementMode ==
|
||||||
|
ExperimentArrangementMode.SelfScheduled &&
|
||||||
|
x.Bookings.Any(booking =>
|
||||||
|
booking.StudentId == studentId.Value &&
|
||||||
|
booking.Status == ExperimentBookingStatus.Booked)))
|
||||||
|
.OrderBy(x => x.SessionDate)
|
||||||
|
.ThenBy(x => x.StartPeriod)
|
||||||
|
.ThenBy(x => x.ExperimentProject!.Code)
|
||||||
|
.Select(x => new ExperimentSessionProjection(
|
||||||
|
x.Id,
|
||||||
|
x.ExperimentProjectId,
|
||||||
|
x.ExperimentProject!.Code,
|
||||||
|
x.ExperimentProject.Name,
|
||||||
|
x.ExperimentProject.ArrangementMode,
|
||||||
|
x.ExperimentProject.TeachingTaskId,
|
||||||
|
x.ExperimentProject.TeachingTask!.TaskNumber,
|
||||||
|
x.ExperimentProject.TeachingTask.Name,
|
||||||
|
x.ExperimentProject.TeachingTask.Course!.Code,
|
||||||
|
x.ExperimentProject.TeachingTask.Course.Name,
|
||||||
|
x.ExperimentProject.TeachingTask.Teachers
|
||||||
|
.OrderByDescending(item => item.IsPrimary)
|
||||||
|
.Select(item => item.Teacher!.Name)
|
||||||
|
.ToList(),
|
||||||
|
x.ExperimentProject.TeachingTask.Classes
|
||||||
|
.Select(item => item.AdministrativeClass!.Name)
|
||||||
|
.ToList(),
|
||||||
|
x.Classroom!.Name,
|
||||||
|
x.Classroom.Building!.Name,
|
||||||
|
x.Classroom.Building.Campus!.Name,
|
||||||
|
x.SessionDate,
|
||||||
|
x.StartPeriod,
|
||||||
|
x.PeriodCount,
|
||||||
|
x.Notes,
|
||||||
|
x.UpdatedAt))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var termMonday = StartOfWeek(term.StartDate);
|
||||||
|
return sessions.Select(x =>
|
||||||
|
{
|
||||||
|
var week = (x.SessionDate.DayNumber - termMonday.DayNumber) / 7 + 1;
|
||||||
|
var dayOfWeek = x.SessionDate.DayOfWeek == DayOfWeek.Sunday
|
||||||
|
? 7
|
||||||
|
: (int)x.SessionDate.DayOfWeek;
|
||||||
|
return new TimetableEntryDto(
|
||||||
|
x.Id,
|
||||||
|
x.TeachingTaskId,
|
||||||
|
x.TaskNumber,
|
||||||
|
x.TaskName,
|
||||||
|
x.CourseCode,
|
||||||
|
x.CourseName,
|
||||||
|
x.TeacherNames,
|
||||||
|
x.ClassNames,
|
||||||
|
x.ClassroomName,
|
||||||
|
x.BuildingName,
|
||||||
|
x.CampusName,
|
||||||
|
dayOfWeek,
|
||||||
|
x.StartPeriod,
|
||||||
|
x.PeriodCount,
|
||||||
|
week,
|
||||||
|
week,
|
||||||
|
WeekPattern.All,
|
||||||
|
x.Notes,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
x.UpdatedAt,
|
||||||
|
true,
|
||||||
|
x.ExperimentProjectId,
|
||||||
|
x.ProjectCode,
|
||||||
|
x.ProjectName,
|
||||||
|
x.SessionDate,
|
||||||
|
x.ArrangementMode);
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateOnly StartOfWeek(DateOnly date)
|
||||||
|
{
|
||||||
|
var dayOfWeek = date.DayOfWeek == DayOfWeek.Sunday
|
||||||
|
? 7
|
||||||
|
: (int)date.DayOfWeek;
|
||||||
|
return date.AddDays(1 - dayOfWeek);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool MatchesMixedResource(
|
private static bool MatchesMixedResource(
|
||||||
ExamRoomAssignment room,
|
ExamRoomAssignment room,
|
||||||
ExamSession session,
|
ExamSession session,
|
||||||
@@ -535,6 +650,28 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
List<string> InvigilatorNames,
|
List<string> InvigilatorNames,
|
||||||
string? Notes,
|
string? Notes,
|
||||||
DateTime UpdatedAt);
|
DateTime UpdatedAt);
|
||||||
|
|
||||||
|
private sealed record ExperimentSessionProjection(
|
||||||
|
Guid Id,
|
||||||
|
Guid ExperimentProjectId,
|
||||||
|
string ProjectCode,
|
||||||
|
string ProjectName,
|
||||||
|
ExperimentArrangementMode ArrangementMode,
|
||||||
|
Guid TeachingTaskId,
|
||||||
|
string TaskNumber,
|
||||||
|
string TaskName,
|
||||||
|
string CourseCode,
|
||||||
|
string CourseName,
|
||||||
|
List<string> TeacherNames,
|
||||||
|
List<string> ClassNames,
|
||||||
|
string ClassroomName,
|
||||||
|
string BuildingName,
|
||||||
|
string CampusName,
|
||||||
|
DateOnly SessionDate,
|
||||||
|
int StartPeriod,
|
||||||
|
int PeriodCount,
|
||||||
|
string? Notes,
|
||||||
|
DateTime UpdatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum TimetableResourceType
|
public enum TimetableResourceType
|
||||||
@@ -553,7 +690,10 @@ public sealed record TimetableData(
|
|||||||
IReadOnlyList<TimetableSlotDto> Slots,
|
IReadOnlyList<TimetableSlotDto> Slots,
|
||||||
IReadOnlyList<TimetableEntryDto> Entries,
|
IReadOnlyList<TimetableEntryDto> Entries,
|
||||||
IReadOnlyList<FlexibleCourseDto> FlexibleCourses,
|
IReadOnlyList<FlexibleCourseDto> FlexibleCourses,
|
||||||
IReadOnlyList<TimetableEntryDto> ExamEntries);
|
IReadOnlyList<TimetableEntryDto> ExamEntries)
|
||||||
|
{
|
||||||
|
public IReadOnlyList<TimetableEntryDto> ExperimentEntries { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
public sealed record TimetableTermDto(
|
public sealed record TimetableTermDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
@@ -616,7 +756,13 @@ public sealed record TimetableEntryDto(
|
|||||||
bool IsExam = false,
|
bool IsExam = false,
|
||||||
string? ExamPlanName = null,
|
string? ExamPlanName = null,
|
||||||
DateOnly? ExamDate = null,
|
DateOnly? ExamDate = null,
|
||||||
DateTime UpdatedAt = default);
|
DateTime UpdatedAt = default,
|
||||||
|
bool IsExperiment = false,
|
||||||
|
Guid? ExperimentProjectId = null,
|
||||||
|
string? ExperimentProjectCode = null,
|
||||||
|
string? ExperimentProjectName = null,
|
||||||
|
DateOnly? ExperimentDate = null,
|
||||||
|
ExperimentArrangementMode? ExperimentArrangementMode = null);
|
||||||
|
|
||||||
public sealed record FlexibleCourseDto(
|
public sealed record FlexibleCourseDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
|
|||||||
@@ -25,14 +25,21 @@ public static class TimetableExcelExporter
|
|||||||
sheet.Row(1).Height = 34;
|
sheet.Row(1).Height = 34;
|
||||||
|
|
||||||
sheet.Range("A2:H2").Merge();
|
sheet.Range("A2:H2").Merge();
|
||||||
var examSummary = timetable.ExamEntries.Count > 0
|
var activitySummary = string.Concat(
|
||||||
|
timetable.ExamEntries.Count > 0
|
||||||
? $" · 已融入 {timetable.ExamEntries.Count} 场考试"
|
? $" · 已融入 {timetable.ExamEntries.Count} 场考试"
|
||||||
: "";
|
: "",
|
||||||
|
timetable.ExperimentEntries.Count > 0
|
||||||
|
? $" · 已融入 {timetable.ExperimentEntries.Count} 场实验"
|
||||||
|
: "");
|
||||||
|
var hasOneOffActivities =
|
||||||
|
timetable.ExamEntries.Count > 0 ||
|
||||||
|
timetable.ExperimentEntries.Count > 0;
|
||||||
sheet.Cell("A2").Value = timetable.Plan is null
|
sheet.Cell("A2").Value = timetable.Plan is null
|
||||||
? timetable.ExamEntries.Count > 0
|
? hasOneOffActivities
|
||||||
? $"固定课表尚未发布{examSummary} · 导出时间:{DateTime.Now:yyyy-MM-dd HH:mm}"
|
? $"固定课表尚未发布{activitySummary} · 导出时间:{DateTime.Now:yyyy-MM-dd HH:mm}"
|
||||||
: "本学期暂无可用课表版本"
|
: "本学期暂无可用课表版本"
|
||||||
: $"版本:{timetable.Plan.Version} · 状态:{PlanStatus(timetable.Plan.Status)}{examSummary} · 导出时间:{DateTime.Now:yyyy-MM-dd HH:mm}";
|
: $"版本:{timetable.Plan.Version} · 状态:{PlanStatus(timetable.Plan.Status)}{activitySummary} · 导出时间:{DateTime.Now:yyyy-MM-dd HH:mm}";
|
||||||
sheet.Cell("A2").Style
|
sheet.Cell("A2").Style
|
||||||
.Font.SetFontColor(XLColor.FromHtml("#52677A"))
|
.Font.SetFontColor(XLColor.FromHtml("#52677A"))
|
||||||
.Fill.SetBackgroundColor(XLColor.FromHtml("#EFF3F6"));
|
.Fill.SetBackgroundColor(XLColor.FromHtml("#EFF3F6"));
|
||||||
@@ -55,6 +62,7 @@ public static class TimetableExcelExporter
|
|||||||
|
|
||||||
var timedEntries = timetable.Entries
|
var timedEntries = timetable.Entries
|
||||||
.Concat(timetable.ExamEntries)
|
.Concat(timetable.ExamEntries)
|
||||||
|
.Concat(timetable.ExperimentEntries)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var periods = timetable.Slots.Select(x => x.PeriodNumber)
|
var periods = timetable.Slots.Select(x => x.PeriodNumber)
|
||||||
.Concat(timedEntries.SelectMany(x =>
|
.Concat(timedEntries.SelectMany(x =>
|
||||||
@@ -95,7 +103,11 @@ public static class TimetableExcelExporter
|
|||||||
{
|
{
|
||||||
cell.Style
|
cell.Style
|
||||||
.Fill.SetBackgroundColor(XLColor.FromHtml(
|
.Fill.SetBackgroundColor(XLColor.FromHtml(
|
||||||
entries.Any(x => x.IsExam) ? "#FFF1E8" : "#E8F3F2"))
|
entries.Any(x => x.IsExam)
|
||||||
|
? "#FFF1E8"
|
||||||
|
: entries.Any(x => x.IsExperiment)
|
||||||
|
? "#E7F5F1"
|
||||||
|
: "#E8F3F2"))
|
||||||
.Font.SetFontColor(XLColor.FromHtml("#173F4C"));
|
.Font.SetFontColor(XLColor.FromHtml("#173F4C"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +159,14 @@ public static class TimetableExcelExporter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string EntryText(TimetableEntryDto entry) =>
|
private static string EntryText(TimetableEntryDto entry) =>
|
||||||
entry.IsExam
|
entry.IsExperiment
|
||||||
|
? $"【实验】{entry.ExperimentProjectName}\n" +
|
||||||
|
$"{entry.CourseName} · {ArrangementMode(entry)}\n" +
|
||||||
|
$"{string.Join('、', entry.TeacherNames)}\n" +
|
||||||
|
$"{Location(entry)}\n" +
|
||||||
|
$"{entry.ExperimentDate:yyyy-MM-dd} · 第 {entry.StartPeriod}-" +
|
||||||
|
$"{entry.StartPeriod + entry.PeriodCount - 1} 节"
|
||||||
|
: entry.IsExam
|
||||||
? $"【考试】{entry.CourseName}\n" +
|
? $"【考试】{entry.CourseName}\n" +
|
||||||
$"{entry.ExamPlanName}\n" +
|
$"{entry.ExamPlanName}\n" +
|
||||||
$"{string.Join('、', entry.TeacherNames)}\n" +
|
$"{string.Join('、', entry.TeacherNames)}\n" +
|
||||||
@@ -159,6 +178,14 @@ public static class TimetableExcelExporter
|
|||||||
$"{Location(entry)}\n" +
|
$"{Location(entry)}\n" +
|
||||||
$"{entry.StartWeek}-{entry.EndWeek} 周";
|
$"{entry.StartWeek}-{entry.EndWeek} 周";
|
||||||
|
|
||||||
|
private static string ArrangementMode(TimetableEntryDto entry) =>
|
||||||
|
entry.ExperimentArrangementMode switch
|
||||||
|
{
|
||||||
|
Domain.Academic.ExperimentArrangementMode.Centralized => "集中安排",
|
||||||
|
Domain.Academic.ExperimentArrangementMode.SelfScheduled => "自主预约",
|
||||||
|
_ => "实验安排"
|
||||||
|
};
|
||||||
|
|
||||||
private static string Location(TimetableEntryDto entry) =>
|
private static string Location(TimetableEntryDto entry) =>
|
||||||
string.Join(" · ", new[]
|
string.Join(" · ", new[]
|
||||||
{
|
{
|
||||||
|
|||||||
Loaded 3 of 7 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user