学年学期页面新增“设为当前”“归档”“撤销归档”,当前学期不能归档、删除或直接取消;已归档学期需先撤销才能重新设为当前。[BaseDataController.cs (line 286)](/E:/jiaowu/src/Jiaowu.Api/Controllers/BaseDataController.cs:286)
切换后,其他学期自动变为历史态;已归档学期进一步弱化。所有主要学期选择器统一默认当前,并显示“当前 / 历史 / 已归档”。[academicTerms.ts (line 1)](/E:/jiaowu/web/src/utils/academicTerms.ts:1) 桌面使用分层表格,390px 手机端改为独立学期卡片,操作不会再挤压表格。[BaseDataView.vue (line 211)](/E:/jiaowu/web/src/views/BaseDataView.vue:211) 归档定义为“历史显示状态”,不是成绩冻结:补考成绩仍可录入并回写,已发布成绩仍可走成绩更正审批。 已加入 SQLite 开发迁移、MySQL 正式迁移和切换/归档/撤销测试。[20260726143000_AcademicTermArchiving.cs (line 1)](/E:/jiaowu/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726143000_AcademicTermArchiving.cs:1)
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
using Jiaowu.Api.Controllers;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class BaseDataControllerTests : IAsyncDisposable
|
||||
{
|
||||
private readonly SqliteConnection connection = new("Data Source=:memory:");
|
||||
private readonly AppDbContext db;
|
||||
private readonly BaseDataController controller;
|
||||
|
||||
public BaseDataControllerTests()
|
||||
{
|
||||
connection.Open();
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
db = new AppDbContext(options);
|
||||
db.Database.EnsureCreated();
|
||||
controller = new BaseDataController(db);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetCurrentTerm_clears_previous_current_in_one_save()
|
||||
{
|
||||
var previous = CreateTerm("2025-2026-2", true);
|
||||
var next = CreateTerm("2026-2027-1", false);
|
||||
db.AcademicTerms.AddRange(previous, next);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var result = await controller.SetCurrentTerm(next.Id, CancellationToken.None);
|
||||
|
||||
Assert.Null(result.Result);
|
||||
db.ChangeTracker.Clear();
|
||||
Assert.False((await db.AcademicTerms.FindAsync(previous.Id))!.IsCurrent);
|
||||
Assert.True((await db.AcademicTerms.FindAsync(next.Id))!.IsCurrent);
|
||||
Assert.Equal(1, await db.AcademicTerms.CountAsync(x => x.IsCurrent));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Archive_requires_switching_current_and_is_reversible()
|
||||
{
|
||||
var current = CreateTerm("2026-2027-1", true);
|
||||
var history = CreateTerm("2025-2026-2", false);
|
||||
db.AcademicTerms.AddRange(current, history);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var currentResult = await controller.ArchiveTerm(
|
||||
current.Id, CancellationToken.None);
|
||||
Assert.IsType<ConflictObjectResult>(currentResult.Result);
|
||||
|
||||
var archiveResult = await controller.ArchiveTerm(
|
||||
history.Id, CancellationToken.None);
|
||||
Assert.Null(archiveResult.Result);
|
||||
Assert.True(history.IsArchived);
|
||||
Assert.NotNull(history.ArchivedAt);
|
||||
Assert.True(history.IsEnabled);
|
||||
|
||||
var unarchiveResult = await controller.UnarchiveTerm(
|
||||
history.Id, CancellationToken.None);
|
||||
Assert.Null(unarchiveResult.Result);
|
||||
Assert.False(history.IsArchived);
|
||||
Assert.Null(history.ArchivedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Archived_term_must_be_unarchived_before_becoming_current()
|
||||
{
|
||||
var current = CreateTerm("2026-2027-1", true);
|
||||
var archived = CreateTerm("2025-2026-2", false);
|
||||
archived.IsArchived = true;
|
||||
archived.ArchivedAt = DateTime.UtcNow;
|
||||
db.AcademicTerms.AddRange(current, archived);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var result = await controller.SetCurrentTerm(
|
||||
archived.Id, CancellationToken.None);
|
||||
|
||||
Assert.IsType<ConflictObjectResult>(result.Result);
|
||||
Assert.True(current.IsCurrent);
|
||||
Assert.False(archived.IsCurrent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Current_term_cannot_be_cleared_without_a_replacement()
|
||||
{
|
||||
var current = CreateTerm("2026-2027-1", true);
|
||||
db.AcademicTerms.Add(current);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var request = new TermRequest(
|
||||
current.Code,
|
||||
current.Name,
|
||||
true,
|
||||
current.AcademicYear,
|
||||
current.Season,
|
||||
current.StartDate,
|
||||
current.EndDate,
|
||||
false);
|
||||
var result = await controller.UpdateTerm(
|
||||
current.Id, request, CancellationToken.None);
|
||||
|
||||
Assert.IsType<ConflictObjectResult>(result.Result);
|
||||
Assert.True(current.IsCurrent);
|
||||
}
|
||||
|
||||
private static AcademicTerm CreateTerm(string code, bool isCurrent) =>
|
||||
new()
|
||||
{
|
||||
Code = code,
|
||||
Name = code,
|
||||
AcademicYear = code[..9],
|
||||
Season = TermSeason.Autumn,
|
||||
StartDate = new DateOnly(2026, 9, 1),
|
||||
EndDate = new DateOnly(2027, 1, 20),
|
||||
IsCurrent = isCurrent,
|
||||
IsEnabled = true
|
||||
};
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await db.DisposeAsync();
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user