草稿计划支持选择场次后“批量移除已选场次”,一次最多 100 个。

批量操作会完整校验:包含不存在、其他计划或已发布计划的场次时整批拒绝,不会部分删除。[ExamsController.cs (line 384)](E:/jiaowu/src/Jiaowu.Api/Controllers/ExamsController.cs:384)
草稿计划新增“删除草稿”按钮,删除前显示计划名称及场次数量确认。[ExamsView.vue (line 292)](E:/jiaowu/web/src/views/ExamsView.vue:292)
删除草稿计划会级联清理场次和监考关联;已发布计划不能删除。[ExamsController.cs (line 81)](E:/jiaowu/src/Jiaowu.Api/Controllers/ExamsController.cs:81)
删除后会自动切换到其他计划;没有剩余计划时正确显示空状态。
新增草稿、已发布、跨计划混选及级联删除测试。[ExamDeletionControllerTests.cs (line 17)](E:/jiaowu/tests/Jiaowu.Api.Tests/ExamDeletionControllerTests.cs:17)
This commit is contained in:
2026-07-27 18:31:52 +08:00 Unverified
parent fb2acc6751
commit 64decdab7e
3 changed files with 422 additions and 1 deletions
@@ -76,6 +76,33 @@ public sealed class ExamsController(
return await SaveAsync(plan.Id, true, cancellationToken);
}
[HttpDelete("plans/{id:guid}")]
[Authorize(Roles = Managers)]
public async Task<ActionResult> DeletePlan(
Guid id,
CancellationToken cancellationToken)
{
var plan = await db.ExamPlans
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (plan is null) return NotFound();
if (plan.Status != ExamPlanStatus.Draft)
return ConflictProblem("只有草稿考试计划可以删除。");
db.ExamPlans.Remove(plan);
try
{
await db.SaveChangesAsync(cancellationToken);
await cache.RemoveByTagAsync(
AppCacheTags.Timetables,
cancellationToken);
return NoContent();
}
catch (DbUpdateException)
{
return ConflictProblem("删除考试计划失败,关联数据可能已发生变化。");
}
}
[HttpGet("plans/{id:guid}")]
public async Task<ActionResult> GetPlan(Guid id, CancellationToken cancellationToken)
{
@@ -354,6 +381,48 @@ public sealed class ExamsController(
return await SaveAsync(id, false, cancellationToken);
}
[HttpPost("plans/{planId:guid}/sessions/batch-remove")]
[Authorize(Roles = Managers)]
public async Task<ActionResult> RemoveSessionsBatch(
Guid planId,
RemoveExamSessionsBatchRequest request,
CancellationToken cancellationToken)
{
var sessionIds = request.SessionIds.Distinct().ToArray();
if (sessionIds.Length == 0)
return ValidationProblem("请至少选择一个考试场次。");
if (sessionIds.Length > 100)
return ValidationProblem("一次最多移除100个考试场次。");
var plan = await db.ExamPlans.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == planId, cancellationToken);
if (plan is null) return NotFound();
if (plan.Status != ExamPlanStatus.Draft)
return ConflictProblem("已发布的考试计划不能调整场次。");
var sessions = await db.ExamSessions
.Where(x => x.ExamPlanId == planId)
.WhereIn(sessionIds, x => x.Id)
.ToListAsync(cancellationToken);
if (sessions.Count != sessionIds.Length)
return ConflictProblem(
"所选考试场次包含不存在或不属于当前计划的记录,请刷新后重新选择。");
db.ExamSessions.RemoveRange(sessions);
try
{
await db.SaveChangesAsync(cancellationToken);
await cache.RemoveByTagAsync(
AppCacheTags.Timetables,
cancellationToken);
return Ok(new { removedCount = sessions.Count });
}
catch (DbUpdateException)
{
return ConflictProblem("批量移除考试场次失败,关联数据可能已发生变化。");
}
}
// ═══════════════════════════════════════════
// Auto-arrange
// ═══════════════════════════════════════════
@@ -958,6 +1027,9 @@ public sealed record CreateExamSessionsBatchRequest(
[Range(1, 10)] int RequiredInvigilatorCount,
[MaxLength(500)] string? Notes);
public sealed record RemoveExamSessionsBatchRequest(
[Required] IReadOnlyCollection<Guid> SessionIds);
public sealed record ExamAutoArrangeRequest(
IReadOnlyCollection<Guid>? SessionIds = null,
bool AssignClassrooms = true,