Implement requested Jiaowu workflow updates
This commit is contained in:
1 parent
2257ce7674
commit
3450d882cf
20 files changed
+16416
-7
No files matched your search
@@ -0,0 +1,164 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.AcademicCalendar;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/academic-calendar")]
|
||||
public sealed class AcademicCalendarController(AppDbContext db, HolidayCnClient holidayCnClient) : ControllerBase
|
||||
{
|
||||
private const string Managers = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> GetMyCalendar(Guid? academicTermId, CancellationToken cancellationToken)
|
||||
{
|
||||
var roles = User.FindAll(ClaimTypes.Role)
|
||||
.Select(claim => claim.Value)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var events = db.AcademicCalendarEvents.AsNoTracking().Where(x => x.IsEnabled);
|
||||
if (academicTermId.HasValue)
|
||||
events = events.Where(x => x.AcademicTermId == academicTermId.Value);
|
||||
else
|
||||
events = events.Where(x => x.AcademicTerm!.IsCurrent);
|
||||
|
||||
var items = await events
|
||||
.Include(x => x.AcademicTerm)
|
||||
.OrderBy(x => x.StartsOn)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(items.Where(item => IsForRoles(item.AudienceRoles, roles))
|
||||
.Select(ToResponse));
|
||||
}
|
||||
|
||||
[HttpGet("manage")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> GetAll(Guid? academicTermId, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = db.AcademicCalendarEvents.AsNoTracking().Include(x => x.AcademicTerm).AsQueryable();
|
||||
if (academicTermId.HasValue)
|
||||
query = query.Where(x => x.AcademicTermId == academicTermId.Value);
|
||||
return Ok((await query.OrderBy(x => x.StartsOn).ToListAsync(cancellationToken)).Select(ToResponse));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> Create(AcademicCalendarEventRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = await ValidateAsync(request, cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
var item = new AcademicCalendarEvent { AcademicTermId = request.AcademicTermId, Title = request.Title.Trim(), Kind = request.Kind, StartsOn = request.StartsOn, EndsOn = request.EndsOn, ReminderDays = request.ReminderDays, AudienceRoles = SerializeRoles(request.AudienceRoles), Description = request.Description?.Trim(), IsEnabled = request.IsEnabled };
|
||||
db.AcademicCalendarEvents.Add(item);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await db.Entry(item).Reference(x => x.AcademicTerm).LoadAsync(cancellationToken);
|
||||
return CreatedAtAction(nameof(GetAll), new { academicTermId = item.AcademicTermId }, ToResponse(item));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> Update(Guid id, AcademicCalendarEventRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = await ValidateAsync(request, cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
var item = await db.AcademicCalendarEvents.Include(x => x.AcademicTerm).FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (item is null) return NotFound();
|
||||
item.AcademicTermId = request.AcademicTermId; item.Title = request.Title.Trim(); item.Kind = request.Kind; item.StartsOn = request.StartsOn; item.EndsOn = request.EndsOn; item.ReminderDays = request.ReminderDays; item.AudienceRoles = SerializeRoles(request.AudienceRoles); item.Description = request.Description?.Trim(); item.IsEnabled = request.IsEnabled;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
item.AcademicTerm = await db.AcademicTerms.AsNoTracking()
|
||||
.FirstAsync(x => x.Id == item.AcademicTermId, cancellationToken);
|
||||
return Ok(ToResponse(item));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> Delete(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await db.AcademicCalendarEvents.FindAsync([id], cancellationToken);
|
||||
if (item is null) return NotFound();
|
||||
db.AcademicCalendarEvents.Remove(item);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("holidays/{year:int}")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> PreviewHolidays(int year, CancellationToken cancellationToken)
|
||||
{
|
||||
if (year is < 2000 or > 2100) return ValidationProblem("年份必须在 2000 至 2100 之间。");
|
||||
try
|
||||
{
|
||||
var result = await holidayCnClient.GetYearAsync(year, cancellationToken);
|
||||
return Ok(new { result.Year, result.Papers, Days = result.Days.Select(day => new { day.Name, day.Date, day.IsOffDay }) });
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return Problem("无法获取节假日数据,请检查网络后重试。", statusCode: StatusCodes.Status502BadGateway);
|
||||
}
|
||||
catch (InvalidOperationException exception)
|
||||
{
|
||||
return ValidationProblem(exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("holidays/sync")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> SyncHolidays(HolidaySyncRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Year is < 2000 or > 2100) return ValidationProblem("年份必须在 2000 至 2100 之间。");
|
||||
if (!await db.AcademicTerms.AnyAsync(item => item.Id == request.AcademicTermId && item.IsEnabled, cancellationToken)) return ValidationProblem("学期不存在或已停用。");
|
||||
HolidayCnYear source;
|
||||
try { source = await holidayCnClient.GetYearAsync(request.Year, cancellationToken); }
|
||||
catch (HttpRequestException) { return Problem("无法获取节假日数据,请检查网络后重试。", statusCode: StatusCodes.Status502BadGateway); }
|
||||
catch (InvalidOperationException exception) { return ValidationProblem(exception.Message); }
|
||||
|
||||
var existing = await db.AcademicCalendarEvents.Where(item => item.AcademicTermId == request.AcademicTermId && item.Source == AcademicCalendarEventSource.HolidayCn && item.SourceYear == request.Year).ToListAsync(cancellationToken);
|
||||
db.AcademicCalendarEvents.RemoveRange(existing);
|
||||
db.AcademicCalendarEvents.AddRange(source.Days.Select(day => new AcademicCalendarEvent
|
||||
{
|
||||
AcademicTermId = request.AcademicTermId,
|
||||
Title = day.Name,
|
||||
Kind = AcademicCalendarEventKind.PublicHoliday,
|
||||
StartsOn = day.Date,
|
||||
ReminderDays = day.IsOffDay ? 3 : 0,
|
||||
Description = day.IsOffDay ? "法定节假日或调休休息日" : "调休上班日",
|
||||
Source = AcademicCalendarEventSource.HolidayCn,
|
||||
SourceYear = request.Year,
|
||||
IsOffDay = day.IsOffDay
|
||||
}));
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return Ok(new { Year = request.Year, SyncedCount = source.Days.Count, ReplacedCount = existing.Count, source.Papers });
|
||||
}
|
||||
|
||||
private async Task<ActionResult?> ValidateAsync(AcademicCalendarEventRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.EndsOn.HasValue && request.EndsOn < request.StartsOn) return ValidationProblem("结束日期不能早于开始日期。");
|
||||
if (!await db.AcademicTerms.AnyAsync(x => x.Id == request.AcademicTermId && x.IsEnabled, cancellationToken)) return ValidationProblem("学期不存在或已停用。");
|
||||
if (request.AudienceRoles.Any(role => !KnownRoles.Contains(role))) return ValidationProblem("包含无效的目标角色。");
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> KnownRoles = [SystemRoles.SuperAdmin, SystemRoles.AcademicAdmin, SystemRoles.CollegeAdmin, SystemRoles.Leader, SystemRoles.Counselor, SystemRoles.Teacher, SystemRoles.Student, SystemRoles.MaintenanceAdmin, SystemRoles.MaintenanceStaff];
|
||||
private static bool IsForRoles(string? audienceRoles, HashSet<string> roles) => string.IsNullOrWhiteSpace(audienceRoles) || audienceRoles.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Any(roles.Contains);
|
||||
private static string? SerializeRoles(IReadOnlyCollection<string> roles) => roles.Count == 0 ? null : string.Join(',', roles.Distinct().Order());
|
||||
private static object ToResponse(AcademicCalendarEvent item) => new { item.Id, item.AcademicTermId, AcademicTermName = item.AcademicTerm?.Name, item.Title, item.Kind, item.StartsOn, item.EndsOn, item.ReminderDays, AudienceRoles = string.IsNullOrWhiteSpace(item.AudienceRoles) ? Array.Empty<string>() : item.AudienceRoles.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), item.Description, item.IsEnabled, item.Source, item.SourceYear, item.IsOffDay };
|
||||
}
|
||||
|
||||
public sealed record AcademicCalendarEventRequest(
|
||||
Guid AcademicTermId,
|
||||
[Required, MaxLength(120)] string Title,
|
||||
AcademicCalendarEventKind Kind,
|
||||
DateOnly StartsOn,
|
||||
DateOnly? EndsOn,
|
||||
[Range(0, 90)] int ReminderDays,
|
||||
IReadOnlyCollection<string> AudienceRoles,
|
||||
[MaxLength(1000)] string? Description,
|
||||
bool IsEnabled = true);
|
||||
|
||||
public sealed record HolidaySyncRequest(Guid AcademicTermId, int Year);
|
||||
Reference in new issue
Block a user