using System.ComponentModel.DataAnnotations; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Controllers; [ApiController] [Authorize(Roles = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin)] [Route("api/venue-display-announcements")] public sealed class VenueDisplayAnnouncementsController(AppDbContext db) : ControllerBase { [HttpGet] public async Task Get(CancellationToken cancellationToken) => Ok(await db.VenueDisplayAnnouncements.AsNoTracking() .OrderByDescending(x => x.IsPinned).ThenByDescending(x => x.IsUrgent).ThenByDescending(x => x.Priority).ThenByDescending(x => x.UpdatedAt) .Select(x => new { x.Id, x.Title, x.Content, x.Scope, x.TargetId, x.IsPublished, x.IsPinned, x.IsUrgent, x.Priority, x.StartsAt, x.EndsAt, x.UpdatedAt }) .ToListAsync(cancellationToken)); [HttpGet("options")] public async Task Options(CancellationToken cancellationToken) => Ok(new { Campuses = await db.Campuses.AsNoTracking().Where(x => x.IsEnabled).OrderBy(x => x.Name).Select(x => new { x.Id, x.Name }).ToListAsync(cancellationToken), Buildings = await db.Buildings.AsNoTracking().Where(x => x.IsEnabled).OrderBy(x => x.Name).Select(x => new { x.Id, x.Name, x.CampusId }).ToListAsync(cancellationToken), Classrooms = await db.Classrooms.AsNoTracking().Where(x => x.IsEnabled).OrderBy(x => x.Building!.Name).ThenBy(x => x.Code).Select(x => new { x.Id, x.Name, x.BuildingId, BuildingName = x.Building!.Name }).ToListAsync(cancellationToken) }); [HttpPost] public async Task Create(UpsertVenueDisplayAnnouncementRequest request, CancellationToken cancellationToken) { var announcement = new VenueDisplayAnnouncement { Title = request.Title.Trim(), Content = request.Content.Trim() }; var error = Validate(announcement, request); if (error is not null) return ValidationProblem(error); if (!await TargetExistsAsync(request.Scope, request.TargetId, cancellationToken)) return ValidationProblem("公告投放目标不存在或已停用。"); Apply(announcement, request); db.VenueDisplayAnnouncements.Add(announcement); await db.SaveChangesAsync(cancellationToken); return CreatedAtAction(nameof(Get), new { announcement.Id }, new { announcement.Id }); } [HttpPut("{id:guid}")] public async Task Update(Guid id, UpsertVenueDisplayAnnouncementRequest request, CancellationToken cancellationToken) { var announcement = await db.VenueDisplayAnnouncements.FindAsync([id], cancellationToken); if (announcement is null) return NotFound(); announcement.Title = request.Title.Trim(); announcement.Content = request.Content.Trim(); var error = Validate(announcement, request); if (error is not null) return ValidationProblem(error); if (!await TargetExistsAsync(request.Scope, request.TargetId, cancellationToken)) return ValidationProblem("公告投放目标不存在或已停用。"); Apply(announcement, request); await db.SaveChangesAsync(cancellationToken); return NoContent(); } [HttpDelete("{id:guid}")] public async Task Delete(Guid id, CancellationToken cancellationToken) { var announcement = await db.VenueDisplayAnnouncements.FindAsync([id], cancellationToken); if (announcement is null) return NotFound(); db.VenueDisplayAnnouncements.Remove(announcement); await db.SaveChangesAsync(cancellationToken); return NoContent(); } private static string? Validate(VenueDisplayAnnouncement announcement, UpsertVenueDisplayAnnouncementRequest request) { if (announcement.Title.Length is < 1 or > 120) return "公告标题需为 1 至 120 个字符。"; if (announcement.Content.Length is < 1 or > 1000) return "公告内容需为 1 至 1000 个字符。"; if (request.Scope != VenueDisplayAnnouncementScope.School && !request.TargetId.HasValue) return "请选择公告投放目标。"; if (request.Scope == VenueDisplayAnnouncementScope.School && request.TargetId.HasValue) return "全校公告不应指定目标。"; if (request.EndsAt.HasValue && request.StartsAt.HasValue && request.EndsAt <= request.StartsAt) return "结束时间必须晚于开始时间。"; return null; } private static void Apply(VenueDisplayAnnouncement announcement, UpsertVenueDisplayAnnouncementRequest request) { announcement.Scope = request.Scope; announcement.TargetId = request.Scope == VenueDisplayAnnouncementScope.School ? null : request.TargetId; announcement.IsPublished = request.IsPublished; announcement.IsPinned = request.IsPinned; announcement.IsUrgent = request.IsUrgent; announcement.Priority = request.Priority; announcement.StartsAt = request.StartsAt; announcement.EndsAt = request.EndsAt; announcement.UpdatedAt = DateTime.UtcNow; } private Task TargetExistsAsync(VenueDisplayAnnouncementScope scope, Guid? targetId, CancellationToken cancellationToken) => scope switch { VenueDisplayAnnouncementScope.School => Task.FromResult(true), VenueDisplayAnnouncementScope.Campus => db.Campuses.AnyAsync(x => x.Id == targetId && x.IsEnabled, cancellationToken), VenueDisplayAnnouncementScope.Building => db.Buildings.AnyAsync(x => x.Id == targetId && x.IsEnabled, cancellationToken), VenueDisplayAnnouncementScope.Classroom => db.Classrooms.AnyAsync(x => x.Id == targetId && x.IsEnabled, cancellationToken), _ => Task.FromResult(false) }; } public sealed record UpsertVenueDisplayAnnouncementRequest( [param: Required, StringLength(120)] string Title, [param: Required, StringLength(1000)] string Content, VenueDisplayAnnouncementScope Scope, Guid? TargetId, bool IsPublished, bool IsPinned, bool IsUrgent, [param: Range(0, 100)] int Priority, DateTime? StartsAt, DateTime? EndsAt);