课表优化

This commit is contained in:
biss committed 2026-08-11 16:56:23 +08:00
1 parent e8482b26f2
commit cd4ffa1d85
7 files changed
+569 -34

No files matched your search

@@ -171,6 +171,77 @@ public sealed class TimetableManagementController(
return File(bytes, ExcelWorkbookHelper.ContentType, fileName); return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
} }
[HttpPost("export/batch.xlsx")]
public async Task<ActionResult> ExportBatch(
TimetableBatchExportRequest request,
CancellationToken cancellationToken)
{
var resourceIds = request.ResourceIds.Distinct().ToArray();
if (resourceIds.Length is 0 or > 100)
return ValidationProblem("请选择 1 至 100 个课表对象进行导出。", statusCode: StatusCodes.Status400BadRequest);
var timetables = new List<TimetableData>(resourceIds.Length);
foreach (var resourceId in resourceIds)
{
var result = await LoadAuthorizedAsync(
request.ResourceType,
resourceId,
request.AcademicTermId,
request.SchedulePlanId,
cancellationToken);
if (result.Result is null) return result.Error!;
timetables.Add(result.Result);
}
var bytes = TimetableExcelExporter.Create(timetables);
var fileName = $"课表批量导出-{FileName(timetables[0].Term.Name)}.xlsx";
return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
}
[HttpGet("export.pdf")]
public async Task<ActionResult> ExportPdf(
TimetableResourceType resourceType,
Guid resourceId,
Guid academicTermId,
Guid? schedulePlanId,
CancellationToken cancellationToken)
{
var result = await LoadAuthorizedAsync(
resourceType,
resourceId,
academicTermId,
schedulePlanId,
cancellationToken);
if (result.Result is null) return result.Error!;
return PdfFile(result.Result);
}
[HttpPost("export/batch.pdf")]
public async Task<ActionResult> ExportBatchPdf(
TimetableBatchExportRequest request,
CancellationToken cancellationToken)
{
var resourceIds = request.ResourceIds.Distinct().ToArray();
if (resourceIds.Length is 0 or > 100)
return ValidationProblem("请选择 1 至 100 个课表对象进行导出。", statusCode: StatusCodes.Status400BadRequest);
var timetables = new List<TimetableData>(resourceIds.Length);
foreach (var resourceId in resourceIds)
{
var result = await LoadAuthorizedAsync(
request.ResourceType,
resourceId,
request.AcademicTermId,
request.SchedulePlanId,
cancellationToken);
if (result.Result is null) return result.Error!;
timetables.Add(result.Result);
}
var fileName = $"课表批量导出-{FileName(timetables[0].Term.Name)}.pdf";
return File(TimetablePdfExporter.Create(timetables), "application/pdf", fileName);
}
private async Task<(TimetableData? Result, ActionResult? Error)> LoadAuthorizedAsync( private async Task<(TimetableData? Result, ActionResult? Error)> LoadAuthorizedAsync(
TimetableResourceType resourceType, TimetableResourceType resourceType,
Guid resourceId, Guid resourceId,
@@ -205,8 +276,20 @@ public sealed class TimetableManagementController(
value = value.Replace(character, '-'); value = value.Replace(character, '-');
return value.Trim(); return value.Trim();
} }
private ActionResult PdfFile(TimetableData timetable) =>
File(
TimetablePdfExporter.Create(timetable),
"application/pdf",
$"{FileName(timetable.Subject.Name)}-{FileName(timetable.Term.Name)}-课表.pdf");
} }
public sealed record TimetableBatchExportRequest(
TimetableResourceType ResourceType,
[property: Required, MinLength(1), MaxLength(100)] IReadOnlyCollection<Guid> ResourceIds,
Guid AcademicTermId,
Guid? SchedulePlanId);
[ApiController] [ApiController]
[Route("api/timetables")] [Route("api/timetables")]
public sealed class FreeClassroomsController( public sealed class FreeClassroomsController(
@@ -100,6 +100,21 @@ public sealed class TimetablesController(
return ExcelFile(result); return ExcelFile(result);
} }
[HttpGet("teachers/{teacherId:guid}/export.pdf")]
[AllowAnonymous]
public async Task<ActionResult> ExportTeacherTimetablePdf(
Guid teacherId,
Guid? academicTermId,
CancellationToken cancellationToken)
{
var result = await GetPublishedTimetableAsync(
TimetableResourceType.Teacher,
teacherId,
academicTermId,
cancellationToken);
return result is null ? NotFound() : PdfFile(result);
}
[HttpGet("classes/{classId:guid}/export.xlsx")] [HttpGet("classes/{classId:guid}/export.xlsx")]
[AllowAnonymous] [AllowAnonymous]
public async Task<ActionResult> ExportClassTimetable( public async Task<ActionResult> ExportClassTimetable(
@@ -116,6 +131,21 @@ public sealed class TimetablesController(
return ExcelFile(result); return ExcelFile(result);
} }
[HttpGet("classes/{classId:guid}/export.pdf")]
[AllowAnonymous]
public async Task<ActionResult> ExportClassTimetablePdf(
Guid classId,
Guid? academicTermId,
CancellationToken cancellationToken)
{
var result = await GetPublishedTimetableAsync(
TimetableResourceType.Class,
classId,
academicTermId,
cancellationToken);
return result is null ? NotFound() : PdfFile(result);
}
[HttpGet("mine")] [HttpGet("mine")]
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)] [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
public async Task<ActionResult> GetMyTimetable( public async Task<ActionResult> GetMyTimetable(
@@ -227,6 +257,39 @@ public sealed class TimetablesController(
return NotFound(); return NotFound();
} }
[HttpGet("mine/export.pdf")]
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
public async Task<ActionResult> ExportMyTimetablePdf(
Guid? academicTermId,
CancellationToken cancellationToken)
{
if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
return Unauthorized();
var student = await db.Students.AsNoTracking()
.Where(x => x.UserId == userId)
.Select(x => new { x.Id, x.AdministrativeClassId, x.StudentNumber, x.Name })
.FirstOrDefaultAsync(cancellationToken);
if (student is not null)
{
var result = await timetableDataService.BuildAsync(
TimetableResourceType.Class, student.AdministrativeClassId, academicTermId, null,
false, student.Id, new TimetableStudentDto(student.StudentNumber, student.Name),
cancellationToken);
return result is null ? NotFound() : PdfFile(result);
}
var teacher = await db.Teachers.AsNoTracking()
.Where(x => x.UserId == userId && x.Status == TeacherStatus.Active)
.Select(x => new { x.Id })
.FirstOrDefaultAsync(cancellationToken);
if (teacher is null) return NotFound();
var teacherResult = await timetableDataService.BuildAsync(
TimetableResourceType.Teacher, teacher.Id, academicTermId, null, false, null, null,
cancellationToken);
return teacherResult is null ? NotFound() : PdfFile(teacherResult);
}
[HttpGet("mine/calendar-subscription")] [HttpGet("mine/calendar-subscription")]
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)] [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
public async Task<ActionResult> GetMyCalendarSubscription( public async Task<ActionResult> GetMyCalendarSubscription(
@@ -450,6 +513,13 @@ public sealed class TimetablesController(
return File(bytes, ExcelWorkbookHelper.ContentType, fileName); return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
} }
private ActionResult PdfFile(TimetableData result)
{
var fileName =
$"{SafeFileName(result.Subject.Name)}-{SafeFileName(result.Term.Name)}-课表.pdf";
return File(TimetablePdfExporter.Create(result), "application/pdf", fileName);
}
private static string SafeFileName(string value) private static string SafeFileName(string value)
{ {
foreach (var character in Path.GetInvalidFileNameChars()) foreach (var character in Path.GetInvalidFileNameChars())
@@ -10,7 +10,33 @@ public static class TimetableExcelExporter
public static byte[] Create(TimetableData timetable) public static byte[] Create(TimetableData timetable)
{ {
using var workbook = new XLWorkbook(); using var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add("课表"); CreateWorksheet(workbook, timetable, "课表");
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}
public static byte[] Create(IReadOnlyCollection<TimetableData> timetables)
{
if (timetables.Count == 0)
throw new ArgumentException("至少需要一张课表。", nameof(timetables));
using var workbook = new XLWorkbook();
var index = 1;
foreach (var timetable in timetables)
CreateWorksheet(workbook, timetable, WorksheetName(timetable.Subject.Name, index++));
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}
private static void CreateWorksheet(
XLWorkbook workbook,
TimetableData timetable,
string worksheetName)
{
var sheet = workbook.Worksheets.Add(worksheetName);
sheet.Style.Font.FontName = "Microsoft YaHei"; sheet.Style.Font.FontName = "Microsoft YaHei";
sheet.Range("A1:H1").Merge(); sheet.Range("A1:H1").Merge();
@@ -153,9 +179,17 @@ public static class TimetableExcelExporter
sheet.PageSetup.PaperSize = XLPaperSize.A4Paper; sheet.PageSetup.PaperSize = XLPaperSize.A4Paper;
sheet.PageSetup.FitToPages(1, 0); sheet.PageSetup.FitToPages(1, 0);
sheet.PageSetup.Margins.SetLeft(0.25).SetRight(0.25).SetTop(0.35).SetBottom(0.35); sheet.PageSetup.Margins.SetLeft(0.25).SetRight(0.25).SetTop(0.35).SetBottom(0.35);
using var stream = new MemoryStream(); }
workbook.SaveAs(stream);
return stream.ToArray(); private static string WorksheetName(string subjectName, int index)
{
var suffix = $"-{index}";
var name = string.Concat(subjectName.Select(character =>
"[]:*?/\\".Contains(character) ? '-' : character)).Trim();
if (string.IsNullOrWhiteSpace(name)) name = "课表";
return name.Length > 31 - suffix.Length
? name[..(31 - suffix.Length)] + suffix
: name + suffix;
} }
private static string EntryText(TimetableEntryDto entry) => private static string EntryText(TimetableEntryDto entry) =>
Loaded 3 of 7 files, more files were not shown because too many files have changed in this diff. Show more