草稿计划支持选择场次后“批量移除已选场次”,一次最多 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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user