主要变更:
新增统一缓存封装:[AppCache.cs (line 12)](/E:/jiaowu/src/Jiaowu.Api/Infrastructure/Caching/AppCache.cs:12) 接入 HybridCache 和可选 Redis:[Program.cs (line 153)](/E:/jiaowu/src/Jiaowu.Api/Program.cs:153) 缓存学生激活选项、基础数据、公开课表和课表选项。 个人课表、选课容量、成绩、权限、通知和任务状态保持实时查询。 基础数据、课程、教师、教学任务、作息、考试和课表发布后自动失效相关缓存。 新增 /health/cache,Redis 故障不影响 /health/ready。 Compose 增加 256MB、allkeys-lfu、无持久化的 redis:8.8-alpine 服务;该镜像标签已由 Docker 官方镜像仓库核对。 更新 [.env.example (line 1)](/E:/jiaowu/.env.example:1) 和 [README.md (line 190)](/E:/jiaowu/README.md:190) 部署说明。
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Caching;
|
||||
using Jiaowu.Api.Infrastructure.Excel;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Timetables;
|
||||
@@ -14,74 +15,24 @@ namespace Jiaowu.Api.Controllers;
|
||||
[Route("api/timetables")]
|
||||
public sealed class TimetablesController(
|
||||
AppDbContext db,
|
||||
TimetableDataService timetableDataService) : ControllerBase
|
||||
TimetableDataService timetableDataService,
|
||||
IAppCache cache) : ControllerBase
|
||||
{
|
||||
[HttpGet("options")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetOptions(CancellationToken cancellationToken)
|
||||
{
|
||||
var terms = await db.AcademicTerms.AsNoTracking()
|
||||
.Where(x => x.IsEnabled)
|
||||
.OrderByDescending(x => x.StartDate)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.Name,
|
||||
x.AcademicYear,
|
||||
x.Season,
|
||||
x.StartDate,
|
||||
x.EndDate,
|
||||
x.IsCurrent,
|
||||
x.IsArchived,
|
||||
HasPublishedTimetable = db.SchedulePlans.Any(plan =>
|
||||
plan.AcademicTermId == x.Id &&
|
||||
plan.Status == SchedulePlanStatus.Published) ||
|
||||
db.TeachingTasks.Any(task =>
|
||||
task.AcademicTermId == x.Id &&
|
||||
task.Status == TeachingTaskStatus.Published &&
|
||||
task.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var defaultTermId = terms.FirstOrDefault(x => x.IsCurrent)?.Id
|
||||
?? terms.FirstOrDefault(x => x.HasPublishedTimetable)?.Id;
|
||||
var classes = await db.AdministrativeClasses.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.Major!.IsEnabled && x.Major.College!.IsEnabled)
|
||||
.OrderByDescending(x => x.Grade)
|
||||
.ThenBy(x => x.Code)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.Code,
|
||||
x.Name,
|
||||
x.Grade,
|
||||
x.MajorId,
|
||||
MajorName = x.Major!.Name,
|
||||
CollegeId = x.Major.CollegeId,
|
||||
CollegeName = x.Major.College!.Name,
|
||||
HasPublishedTimetable = defaultTermId.HasValue &&
|
||||
(db.ScheduleEntries.Any(entry =>
|
||||
entry.SchedulePlan!.AcademicTermId == defaultTermId.Value &&
|
||||
entry.SchedulePlan.Status == SchedulePlanStatus.Published &&
|
||||
entry.TeachingTask!.Classes.Any(item =>
|
||||
item.AdministrativeClassId == x.Id)) ||
|
||||
db.TeachingTasks.Any(task =>
|
||||
task.AcademicTermId == defaultTermId.Value &&
|
||||
task.Status == TeachingTaskStatus.Published &&
|
||||
task.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
|
||||
task.Classes.Any(item => item.AdministrativeClassId == x.Id)))
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var colleges = await db.Colleges.AsNoTracking()
|
||||
.Where(x => x.IsEnabled)
|
||||
.OrderBy(x => x.Code)
|
||||
.Select(x => new { x.Id, x.Code, x.Name })
|
||||
.ToListAsync(cancellationToken);
|
||||
var majors = await db.Majors.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.College!.IsEnabled)
|
||||
.OrderBy(x => x.Code)
|
||||
.Select(x => new { x.Id, x.Code, x.Name, x.CollegeId })
|
||||
.ToListAsync(cancellationToken);
|
||||
return Ok(new { Terms = terms, Colleges = colleges, Majors = majors, Classes = classes });
|
||||
var result = await cache.GetOrCreateAsync(
|
||||
AppCacheKeys.TimetableOptions,
|
||||
LoadOptionsAsync,
|
||||
AppCacheProfile.ReferenceData,
|
||||
[
|
||||
AppCacheTags.BaseData,
|
||||
AppCacheTags.Timetables,
|
||||
AppCacheTags.TimetableOptions
|
||||
],
|
||||
cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("classes/{classId:guid}")]
|
||||
@@ -99,14 +50,10 @@ public sealed class TimetablesController(
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
var result = await GetPublishedTimetableAsync(
|
||||
TimetableResourceType.Teacher,
|
||||
teacherId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
cancellationToken);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
@@ -118,14 +65,10 @@ public sealed class TimetablesController(
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
var result = await GetPublishedTimetableAsync(
|
||||
TimetableResourceType.Teacher,
|
||||
teacherId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
cancellationToken);
|
||||
if (result is null) return NotFound();
|
||||
return ExcelFile(result);
|
||||
@@ -138,14 +81,10 @@ public sealed class TimetablesController(
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
var result = await GetPublishedTimetableAsync(
|
||||
TimetableResourceType.Class,
|
||||
classId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
cancellationToken);
|
||||
if (result is null) return NotFound();
|
||||
return ExcelFile(result);
|
||||
@@ -269,18 +208,111 @@ public sealed class TimetablesController(
|
||||
CancellationToken cancellationToken,
|
||||
TimetableStudentDto? student = null)
|
||||
{
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
TimetableResourceType.Class,
|
||||
classId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
studentId,
|
||||
student,
|
||||
cancellationToken);
|
||||
var result = studentId.HasValue || student is not null
|
||||
? await timetableDataService.BuildAsync(
|
||||
TimetableResourceType.Class,
|
||||
classId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
studentId,
|
||||
student,
|
||||
cancellationToken)
|
||||
: await GetPublishedTimetableAsync(
|
||||
TimetableResourceType.Class,
|
||||
classId,
|
||||
academicTermId,
|
||||
cancellationToken);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
private Task<TimetableData?> GetPublishedTimetableAsync(
|
||||
TimetableResourceType resourceType,
|
||||
Guid resourceId,
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken) =>
|
||||
cache.GetOrCreateAsync(
|
||||
AppCacheKeys.PublishedTimetable(
|
||||
resourceType.ToString().ToLowerInvariant(),
|
||||
resourceId,
|
||||
academicTermId),
|
||||
token => timetableDataService.BuildAsync(
|
||||
resourceType,
|
||||
resourceId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
token),
|
||||
AppCacheProfile.PublishedTimetable,
|
||||
[AppCacheTags.BaseData, AppCacheTags.Timetables],
|
||||
cancellationToken);
|
||||
|
||||
private async Task<TimetableOptionsResponse> LoadOptionsAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var terms = await db.AcademicTerms.AsNoTracking()
|
||||
.Where(x => x.IsEnabled)
|
||||
.OrderByDescending(x => x.StartDate)
|
||||
.Select(x => new TimetableTermOption(
|
||||
x.Id,
|
||||
x.Name,
|
||||
x.AcademicYear,
|
||||
x.Season,
|
||||
x.StartDate,
|
||||
x.EndDate,
|
||||
x.IsCurrent,
|
||||
x.IsArchived,
|
||||
db.SchedulePlans.Any(plan =>
|
||||
plan.AcademicTermId == x.Id &&
|
||||
plan.Status == SchedulePlanStatus.Published) ||
|
||||
db.TeachingTasks.Any(task =>
|
||||
task.AcademicTermId == x.Id &&
|
||||
task.Status == TeachingTaskStatus.Published &&
|
||||
task.SchedulingMode == TeachingTaskSchedulingMode.Flexible)))
|
||||
.ToListAsync(cancellationToken);
|
||||
var defaultTermId = terms.FirstOrDefault(x => x.IsCurrent)?.Id
|
||||
?? terms.FirstOrDefault(x => x.HasPublishedTimetable)?.Id;
|
||||
var classes = await db.AdministrativeClasses.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.Major!.IsEnabled && x.Major.College!.IsEnabled)
|
||||
.OrderByDescending(x => x.Grade)
|
||||
.ThenBy(x => x.Code)
|
||||
.Select(x => new TimetableClassOption(
|
||||
x.Id,
|
||||
x.Code,
|
||||
x.Name,
|
||||
x.Grade,
|
||||
x.MajorId,
|
||||
x.Major!.Name,
|
||||
x.Major.CollegeId,
|
||||
x.Major.College!.Name,
|
||||
defaultTermId.HasValue &&
|
||||
(db.ScheduleEntries.Any(entry =>
|
||||
entry.SchedulePlan!.AcademicTermId == defaultTermId.Value &&
|
||||
entry.SchedulePlan.Status == SchedulePlanStatus.Published &&
|
||||
entry.TeachingTask!.Classes.Any(item =>
|
||||
item.AdministrativeClassId == x.Id)) ||
|
||||
db.TeachingTasks.Any(task =>
|
||||
task.AcademicTermId == defaultTermId.Value &&
|
||||
task.Status == TeachingTaskStatus.Published &&
|
||||
task.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
|
||||
task.Classes.Any(item => item.AdministrativeClassId == x.Id)))))
|
||||
.ToListAsync(cancellationToken);
|
||||
var colleges = await db.Colleges.AsNoTracking()
|
||||
.Where(x => x.IsEnabled)
|
||||
.OrderBy(x => x.Code)
|
||||
.Select(x => new TimetableCollegeOption(x.Id, x.Code, x.Name))
|
||||
.ToListAsync(cancellationToken);
|
||||
var majors = await db.Majors.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.College!.IsEnabled)
|
||||
.OrderBy(x => x.Code)
|
||||
.Select(x => new TimetableMajorOption(
|
||||
x.Id, x.Code, x.Name, x.CollegeId))
|
||||
.ToListAsync(cancellationToken);
|
||||
return new TimetableOptionsResponse(terms, colleges, majors, classes);
|
||||
}
|
||||
|
||||
private ActionResult ExcelFile(TimetableData result)
|
||||
{
|
||||
var bytes = TimetableExcelExporter.Create(result);
|
||||
@@ -296,3 +328,39 @@ public sealed class TimetablesController(
|
||||
return value.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record TimetableOptionsResponse(
|
||||
IReadOnlyList<TimetableTermOption> Terms,
|
||||
IReadOnlyList<TimetableCollegeOption> Colleges,
|
||||
IReadOnlyList<TimetableMajorOption> Majors,
|
||||
IReadOnlyList<TimetableClassOption> Classes);
|
||||
|
||||
public sealed record TimetableTermOption(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string AcademicYear,
|
||||
TermSeason Season,
|
||||
DateOnly StartDate,
|
||||
DateOnly EndDate,
|
||||
bool IsCurrent,
|
||||
bool IsArchived,
|
||||
bool HasPublishedTimetable);
|
||||
|
||||
public sealed record TimetableCollegeOption(Guid Id, string Code, string Name);
|
||||
|
||||
public sealed record TimetableMajorOption(
|
||||
Guid Id,
|
||||
string Code,
|
||||
string Name,
|
||||
Guid CollegeId);
|
||||
|
||||
public sealed record TimetableClassOption(
|
||||
Guid Id,
|
||||
string Code,
|
||||
string Name,
|
||||
int Grade,
|
||||
Guid MajorId,
|
||||
string MajorName,
|
||||
Guid CollegeId,
|
||||
string CollegeName,
|
||||
bool HasPublishedTimetable);
|
||||
|
||||
Reference in New Issue
Block a user