实验课表
This commit is contained in:
@@ -115,6 +115,7 @@ public sealed class PersonalCalendarService(
|
||||
|
||||
AddScheduledCourses(builder, timetable);
|
||||
AddExamEntries(builder, timetable, teacher is not null);
|
||||
AddExperimentEntries(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(
|
||||
Guid? studentId,
|
||||
Guid? teacherId,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Teaching;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Timetables;
|
||||
@@ -117,6 +118,8 @@ public sealed class TimetableDataService(AppDbContext db)
|
||||
// Load exam sessions for student/teacher timetables
|
||||
var examEntries = await LoadExamEntriesAsync(
|
||||
resourceType, resourceId, term.Id, studentId, slots, cancellationToken);
|
||||
var experimentEntries = await LoadExperimentEntriesAsync(
|
||||
resourceType, term, studentId, cancellationToken);
|
||||
|
||||
return new TimetableData(
|
||||
term,
|
||||
@@ -127,7 +130,10 @@ public sealed class TimetableDataService(AppDbContext db)
|
||||
slots,
|
||||
entries,
|
||||
flexibleCourses,
|
||||
examEntries);
|
||||
examEntries)
|
||||
{
|
||||
ExperimentEntries = experimentEntries
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<TimetableSubjectDto?> LoadSubjectAsync(
|
||||
@@ -420,6 +426,115 @@ public sealed class TimetableDataService(AppDbContext db)
|
||||
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(
|
||||
ExamRoomAssignment room,
|
||||
ExamSession session,
|
||||
@@ -535,6 +650,28 @@ public sealed class TimetableDataService(AppDbContext db)
|
||||
List<string> InvigilatorNames,
|
||||
string? Notes,
|
||||
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
|
||||
@@ -553,7 +690,10 @@ public sealed record TimetableData(
|
||||
IReadOnlyList<TimetableSlotDto> Slots,
|
||||
IReadOnlyList<TimetableEntryDto> Entries,
|
||||
IReadOnlyList<FlexibleCourseDto> FlexibleCourses,
|
||||
IReadOnlyList<TimetableEntryDto> ExamEntries);
|
||||
IReadOnlyList<TimetableEntryDto> ExamEntries)
|
||||
{
|
||||
public IReadOnlyList<TimetableEntryDto> ExperimentEntries { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record TimetableTermDto(
|
||||
Guid Id,
|
||||
@@ -616,7 +756,13 @@ public sealed record TimetableEntryDto(
|
||||
bool IsExam = false,
|
||||
string? ExamPlanName = 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(
|
||||
Guid Id,
|
||||
|
||||
@@ -25,14 +25,21 @@ public static class TimetableExcelExporter
|
||||
sheet.Row(1).Height = 34;
|
||||
|
||||
sheet.Range("A2:H2").Merge();
|
||||
var examSummary = timetable.ExamEntries.Count > 0
|
||||
? $" · 已融入 {timetable.ExamEntries.Count} 场考试"
|
||||
: "";
|
||||
var activitySummary = string.Concat(
|
||||
timetable.ExamEntries.Count > 0
|
||||
? $" · 已融入 {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
|
||||
? timetable.ExamEntries.Count > 0
|
||||
? $"固定课表尚未发布{examSummary} · 导出时间:{DateTime.Now:yyyy-MM-dd HH:mm}"
|
||||
? hasOneOffActivities
|
||||
? $"固定课表尚未发布{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
|
||||
.Font.SetFontColor(XLColor.FromHtml("#52677A"))
|
||||
.Fill.SetBackgroundColor(XLColor.FromHtml("#EFF3F6"));
|
||||
@@ -55,6 +62,7 @@ public static class TimetableExcelExporter
|
||||
|
||||
var timedEntries = timetable.Entries
|
||||
.Concat(timetable.ExamEntries)
|
||||
.Concat(timetable.ExperimentEntries)
|
||||
.ToArray();
|
||||
var periods = timetable.Slots.Select(x => x.PeriodNumber)
|
||||
.Concat(timedEntries.SelectMany(x =>
|
||||
@@ -95,7 +103,11 @@ public static class TimetableExcelExporter
|
||||
{
|
||||
cell.Style
|
||||
.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"));
|
||||
}
|
||||
}
|
||||
@@ -147,7 +159,14 @@ public static class TimetableExcelExporter
|
||||
}
|
||||
|
||||
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.ExamPlanName}\n" +
|
||||
$"{string.Join('、', entry.TeacherNames)}\n" +
|
||||
@@ -159,6 +178,14 @@ public static class TimetableExcelExporter
|
||||
$"{Location(entry)}\n" +
|
||||
$"{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) =>
|
||||
string.Join(" · ", new[]
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user