diff --git a/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs b/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs index 4f4f2fd..0510b80 100644 --- a/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs +++ b/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs @@ -568,21 +568,39 @@ public sealed class ExperimentGradesController( sheet.ContributionWeight = request.ContributionWeight; sheet.PassScore = request.PassScore; - db.ExperimentGradeItems.RemoveRange(sheet.Items); + var existingItems = sheet.Items.ToList(); + var existingItemScores = sheet.Records + .SelectMany(record => record.ItemScores) + .ToList(); + + // MySQL must see each replacement item before its score rows. Do not + // rely on a single mixed insert/delete batch to infer that order. + await using var transaction = + await db.Database.BeginTransactionAsync(cancellationToken); + db.ExperimentGradeItemScores.RemoveRange(existingItemScores); + db.ExperimentGradeItems.RemoveRange(existingItems); foreach (var record in sheet.Records) { - record.ItemScores.Clear(); record.TotalScore = null; record.IsPassed = null; } - sheet.Items = replacementItems; - foreach (var record in sheet.Records) - foreach (var item in replacementItems) - record.ItemScores.Add(new ExperimentGradeItemScore - { - ExperimentGradeItemId = item.Id - }); + await db.SaveChangesAsync(cancellationToken); + + foreach (var item in replacementItems) + item.ExperimentGradeSheetId = sheet.Id; + db.ExperimentGradeItems.AddRange(replacementItems); + await db.SaveChangesAsync(cancellationToken); + + db.ExperimentGradeItemScores.AddRange( + sheet.Records.SelectMany(record => replacementItems.Select(item => + new ExperimentGradeItemScore + { + ExperimentGradeRecordId = record.Id, + ExperimentGradeItemId = item.Id + }))); + await db.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); return NoContent(); } diff --git a/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs b/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs index 489f905..bf696f9 100644 --- a/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs @@ -14,6 +14,74 @@ namespace Jiaowu.Api.Tests; public sealed class ExperimentGradesControllerTests { + [Fact] + public async Task UpdateSettings_ReplacesItemsAndRecreatesScores() + { + await using var fixture = await ExperimentGradeFixture.CreateAsync(); + var project = new ExperimentProject + { + TeachingTaskId = fixture.Task.Id, + Code = "LAB-SETTINGS", + Name = "评分方案更新实验", + ArrangementMode = ExperimentArrangementMode.Centralized, + StartDate = fixture.Term.StartDate, + EndDate = fixture.Term.EndDate, + Status = ExperimentProjectStatus.Published, + PublishedAt = DateTime.UtcNow + }; + fixture.Db.ExperimentProjects.Add(project); + await fixture.Db.SaveChangesAsync(); + + var administrator = fixture.ExperimentGrades(fixture.AdminScope); + await administrator.CreateSheet( + new ExperimentGradeSheetRequest( + project.Id, + 1, + 60, + [new ExperimentGradeItemRequest( + "旧评分项", + ExperimentGradeItemKind.Operation, + 100)]), + CancellationToken.None); + var sheetId = await fixture.Db.ExperimentGradeSheets + .Select(sheet => sheet.Id) + .SingleAsync(); + fixture.Db.ChangeTracker.Clear(); + + Assert.IsType(await administrator.UpdateSettings( + sheetId, + new ExperimentGradeSettingsRequest( + 1, + 60, + [ + new ExperimentGradeItemRequest( + "操作", + ExperimentGradeItemKind.Operation, + 60), + new ExperimentGradeItemRequest( + "报告", + ExperimentGradeItemKind.Report, + 40) + ]), + CancellationToken.None)); + + fixture.Db.ChangeTracker.Clear(); + var updatedSheet = await fixture.Db.ExperimentGradeSheets + .Include(sheet => sheet.Items) + .Include(sheet => sheet.Records) + .ThenInclude(record => record.ItemScores) + .SingleAsync(sheet => sheet.Id == sheetId); + Assert.Equal(["操作", "报告"], updatedSheet.Items + .OrderBy(item => item.SortOrder) + .Select(item => item.Name)); + var record = Assert.Single(updatedSheet.Records); + Assert.Equal(2, record.ItemScores.Count); + Assert.All(record.ItemScores, score => Assert.Null(score.Score)); + Assert.All(record.ItemScores, score => Assert.Contains( + updatedSheet.Items, + item => item.Id == score.ExperimentGradeItemId)); + } + [Fact] public async Task CollegeAdmin_CanPublishApprovedExperimentGradeSheet() {