升级
This commit is contained in:
1 parent
cec56564b5
commit
ae075bda6d
32 files changed
+10052
-90
No files matched your search
@@ -0,0 +1,69 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Caching;
|
||||
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 + "," + SystemRoles.CollegeAdmin)]
|
||||
[Route("api/archives/{kind}")]
|
||||
public sealed class ArchivesController(AppDbContext db, ICurrentUserDataScope scope, IAppCache cache) : ControllerBase
|
||||
{
|
||||
[HttpPost("preview")]
|
||||
public async Task<ActionResult> Preview(string kind, ArchiveBatchRequest request, CancellationToken ct)
|
||||
{
|
||||
var service = new ArchiveService(db, scope);
|
||||
var results = new List<object>();
|
||||
foreach (var id in request.Ids.Distinct())
|
||||
{
|
||||
var item = await service.InspectAsync(kind, id, !request.Archived, ct);
|
||||
results.Add(item is null ? new { Id = id, Error = "记录不存在或无权操作。" } : (object)item.Value.Preview);
|
||||
}
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/history")]
|
||||
public async Task<ActionResult> History(string kind, Guid id, CancellationToken ct, int page = 1)
|
||||
{
|
||||
if (await new ArchiveService(db, scope).InspectAsync(kind, id, true, ct) is null) return NotFound();
|
||||
return Ok(await db.ArchiveEvents.AsNoTracking().Where(x => x.Kind == kind && x.EntityId == id)
|
||||
.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)
|
||||
.Skip((Math.Clamp(page, 1, 100000) - 1) * 20).Take(20).ToListAsync(ct));
|
||||
}
|
||||
|
||||
[HttpPost("apply")]
|
||||
public async Task<ActionResult> Apply(string kind, ArchiveBatchRequest request, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Reason)) return BadRequest("请填写归档或恢复原因。");
|
||||
var results = new List<object>();
|
||||
foreach (var id in request.Ids.Distinct())
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await new ArchiveService(db, scope).ChangeAsync(kind, id, request.Archived, request.Reason, ct);
|
||||
results.Add(result);
|
||||
}
|
||||
catch (Exception ex) when (ex is DbUpdateException or System.Data.Common.DbException)
|
||||
{
|
||||
results.Add(new ArchiveOperationResult(id, false, "记录已发生变化或保存失败,请刷新后重试。"));
|
||||
}
|
||||
finally { db.ChangeTracker.Clear(); }
|
||||
}
|
||||
await cache.RemoveByTagAsync(AppCacheTags.Timetables, ct);
|
||||
await cache.RemoveByTagAsync(AppCacheTags.Analytics, ct);
|
||||
await cache.RemoveByTagAsync(AppCacheTags.BaseData, ct);
|
||||
return Ok(results);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ArchiveBatchRequest(
|
||||
[property: Required, MinLength(1), MaxLength(100)] Guid[] Ids,
|
||||
bool Archived,
|
||||
[property: MaxLength(500)] string? Reason = null);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Jiaowu.Api.Infrastructure.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Jiaowu.Api.Contracts;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
@@ -14,7 +16,8 @@ namespace Jiaowu.Api.Controllers;
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/base-data")]
|
||||
public sealed class BaseDataController(AppDbContext db, IAppCache cache) : ControllerBase
|
||||
public sealed class BaseDataController(AppDbContext db, IAppCache cache,
|
||||
ICurrentUserDataScope? currentUserDataScope = null) : ControllerBase
|
||||
{
|
||||
private const string Administrators =
|
||||
$"{SystemRoles.SuperAdmin},{SystemRoles.AcademicAdmin}";
|
||||
@@ -435,8 +438,8 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
||||
return ValidationProblem("学期结束日期必须晚于开始日期。");
|
||||
if (entity.IsCurrent && !request.IsCurrent)
|
||||
return ConflictProblem("不能直接取消当前学期,请将另一个学期设为当前。");
|
||||
if (request.IsCurrent && entity.IsArchived)
|
||||
return ConflictProblem("已归档学期不能直接设为当前,请先撤销归档。");
|
||||
if (entity.IsArchived)
|
||||
return ConflictProblem("已归档学期请先恢复后再编辑。");
|
||||
if (request.IsCurrent && !request.IsEnabled)
|
||||
return ValidationProblem("当前学期必须保持启用。");
|
||||
if (request.IsCurrent)
|
||||
@@ -493,26 +496,28 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
||||
return ConflictProblem("当前学期不能归档,请先切换到新的当前学期。");
|
||||
if (entity.IsArchived) return entity;
|
||||
|
||||
entity.IsArchived = true;
|
||||
entity.ArchivedAt = DateTime.UtcNow;
|
||||
await SaveAndInvalidateAsync(cancellationToken);
|
||||
return entity;
|
||||
return await ChangeTermArchiveAsync(id, true, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("terms/{id:guid}/unarchive")]
|
||||
[Authorize(Roles = Administrators)]
|
||||
public async Task<ActionResult<AcademicTerm>> UnarchiveTerm(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.AcademicTerms.FindAsync([id], cancellationToken);
|
||||
if (entity is null) return NotFound();
|
||||
if (!entity.IsArchived) return entity;
|
||||
public Task<ActionResult<AcademicTerm>> UnarchiveTerm(Guid id, CancellationToken cancellationToken) =>
|
||||
ChangeTermArchiveAsync(id, false, cancellationToken);
|
||||
|
||||
entity.IsArchived = false;
|
||||
entity.ArchivedAt = null;
|
||||
await SaveAndInvalidateAsync(cancellationToken);
|
||||
return entity;
|
||||
private async Task<ActionResult<AcademicTerm>> ChangeTermArchiveAsync(Guid id, bool archived, CancellationToken ct)
|
||||
{
|
||||
if (!await db.AcademicTerms.AnyAsync(x => x.Id == id, ct)) return NotFound();
|
||||
var scope = currentUserDataScope ?? new CurrentUserDataScope(new HttpContextAccessor { HttpContext = HttpContext });
|
||||
var service = new ArchiveService(db, scope);
|
||||
try
|
||||
{
|
||||
var result = await service.ChangeAsync("terms", id, archived, archived ? "通过学期管理归档" : "通过学期管理恢复", ct);
|
||||
if (!result.Success) return ConflictProblem(result.Message);
|
||||
await cache.RemoveByTagAsync(AppCacheTags.BaseData, ct);
|
||||
await cache.RemoveByTagAsync(AppCacheTags.Timetables, ct);
|
||||
return (await db.AcademicTerms.FindAsync([id], ct))!;
|
||||
}
|
||||
catch (DbUpdateException) { return ConflictProblem("学期数据已变化,请刷新后重试。"); }
|
||||
}
|
||||
|
||||
[HttpGet("classrooms")]
|
||||
@@ -704,6 +709,8 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
||||
if (entity is null) return NotFound();
|
||||
if (entity is AcademicTerm { IsCurrent: true })
|
||||
return ConflictProblem("当前学期不能删除,请先切换到其他学期。");
|
||||
if (entity is AcademicTerm { IsArchived: true })
|
||||
return ConflictProblem("已归档学期不能删除,请先恢复。");
|
||||
db.Remove(entity);
|
||||
try
|
||||
{
|
||||
|
||||
@@ -51,10 +51,10 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
||||
}
|
||||
|
||||
[HttpGet("{kind}/export")]
|
||||
public async Task<IActionResult> Export(string kind, CancellationToken cancellationToken)
|
||||
public async Task<IActionResult> Export(string kind, CancellationToken cancellationToken, bool? isArchived = null, string? keyword = null)
|
||||
{
|
||||
if (!TryGetHeaders(kind, out var headers)) return NotFound();
|
||||
var rows = await GetExportRowsAsync(kind, cancellationToken);
|
||||
var rows = await GetExportRowsAsync(kind, cancellationToken, isArchived, keyword?.Trim());
|
||||
var bytes = ExcelWorkbookHelper.Create(KindName(kind), headers, rows);
|
||||
return File(bytes, ExcelWorkbookHelper.ContentType,
|
||||
DownloadFileName.Export(KindName(kind), ".xlsx"));
|
||||
@@ -140,7 +140,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
||||
|
||||
private async Task<IReadOnlyList<IReadOnlyList<object?>>> GetExportRowsAsync(
|
||||
string kind,
|
||||
CancellationToken cancellationToken) =>
|
||||
CancellationToken cancellationToken, bool? isArchived, string? keyword) =>
|
||||
kind.ToLowerInvariant() switch
|
||||
{
|
||||
"campuses" => (await db.Campuses.AsNoTracking().OrderBy(x => x.Code)
|
||||
@@ -158,7 +158,10 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
||||
.Include(x => x.CounselorUser).OrderBy(x => x.Code).ToListAsync(cancellationToken))
|
||||
.Select(x => Row(x.Code, x.Name, x.Major!.Code, x.Grade,
|
||||
x.CounselorUser?.StaffNumber, x.SortOrder, Status(x.IsEnabled))).ToList(),
|
||||
"terms" => (await db.AcademicTerms.AsNoTracking().OrderByDescending(x => x.StartDate)
|
||||
"terms" => (await db.AcademicTerms.AsNoTracking()
|
||||
.Where(x => !isArchived.HasValue || x.IsArchived == isArchived.Value)
|
||||
.Where(x => keyword == null || keyword == "" || x.Code.Contains(keyword) || x.Name.Contains(keyword))
|
||||
.OrderByDescending(x => x.StartDate)
|
||||
.ToListAsync(cancellationToken))
|
||||
.Select(x => Row(x.Code, x.Name, x.AcademicYear, SeasonName(x.Season),
|
||||
x.StartDate, x.EndDate, YesNo(x.IsCurrent), Status(x.IsEnabled))).ToList(),
|
||||
@@ -384,6 +387,11 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
||||
continue;
|
||||
}
|
||||
var entity = existing.GetValueOrDefault(code);
|
||||
if (entity?.IsArchived == true)
|
||||
{
|
||||
errors.Add($"第 {row.RowNumber} 行:学期已归档,请先恢复后再导入更新。");
|
||||
continue;
|
||||
}
|
||||
if (entity is null)
|
||||
{
|
||||
entity = new AcademicTerm
|
||||
|
||||
Loaded 3 of 32 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user