修复其他考试提交

This commit is contained in:
2026-08-08 21:27:52 +08:00 Unverified
parent 87e0b208ef
commit 5f30dd6766
7 changed files with 182 additions and 48 deletions
+63 -5
View File
@@ -699,7 +699,7 @@ public sealed class GradesController(
var itemNames = sheet.Items.Select(i => i.Name).ToList();
var headers = new List<string> { "学号", "姓名", "班级", "平时成绩" };
headers.AddRange(itemNames);
headers.AddRange(["期末成绩", "考试状态", "备注"]);
headers.AddRange(["总分(自动计算)", "期末成绩", "考试状态", "备注"]);
var rows = sheet.Records.Select(record =>
{
@@ -716,6 +716,7 @@ public sealed class GradesController(
.FirstOrDefault(s => s.GradeItemId == item.Id)?.Score;
values.Add(score);
}
values.Add(null);
values.Add(record.FinalScore);
values.Add(record.ExamStatus == GradeExamStatus.Normal ? "正常" :
record.ExamStatus == GradeExamStatus.Absent ? "缺考" :
@@ -729,13 +730,56 @@ public sealed class GradesController(
{
"请勿修改第一行列名;学号、姓名、班级列请勿修改,用于匹配学生。",
"成绩列填写 0—100 的数值,留空表示暂未录入。",
"总分(自动计算)列由 Excel 按各部分比例自动计算,仅供填写时预览;上传时系统不会采用该列结果。",
"考试状态填写:正常、缺考、缓考 或 免修,留空默认为正常。",
$"本成绩单共 {sheet.Items.Count} 个分项:{string.Join("", itemNames)}。",
"导入后会自动重新计算总评成绩和绩点。"
};
var regularColumn = 4;
var itemColumns = Enumerable.Range(5, sheet.Items.Count).ToArray();
var totalColumn = regularColumn + itemColumns.Length + 1;
var finalColumn = totalColumn + 1;
var statusColumn = finalColumn + 1;
var weightedColumns = new List<(int Column, decimal Weight)> { (regularColumn, sheet.RegularWeight) };
weightedColumns.AddRange(sheet.Items.Select((item, index) =>
(itemColumns[index], item.Weight)));
weightedColumns.Add((finalColumn, sheet.FinalWeight));
var requiredColumns = weightedColumns.Where(x => x.Weight > 0).ToArray();
var scoreColumns = weightedColumns.Select(x => x.Column)
.Append(totalColumn)
.ToArray();
var bytes = ExcelWorkbookHelper.Create(
"成绩导入", headers, rows, instructions);
"成绩导入", headers, rows, instructions,
(worksheet, rowNumber) =>
{
var componentReferences = requiredColumns
.Select(x => $"{ColumnLetter(x.Column)}{rowNumber}")
.ToArray();
var weightedExpression = string.Join("+", weightedColumns.Select(x =>
$"{ColumnLetter(x.Column)}{rowNumber}*{x.Weight.ToString(CultureInfo.InvariantCulture)}/100"));
var statusReference = $"{ColumnLetter(statusColumn)}{rowNumber}";
var formula =
$"=IF(OR({statusReference}=\"\",{statusReference}=\"缓考\",{statusReference}=\"免修\"),\"\",IF(COUNT({string.Join(",", componentReferences)})={requiredColumns.Length},ROUND({weightedExpression},1),\"\"))";
var cell = worksheet.Cell(rowNumber, totalColumn);
cell.FormulaA1 = formula;
cell.Style.NumberFormat.Format = "0.0";
cell.Style.Font.Bold = true;
cell.Style.Fill.BackgroundColor = ClosedXML.Excel.XLColor.FromHtml("#E8F1FB");
foreach (var scoreColumn in scoreColumns)
{
var conditionalFormat = worksheet
.Range(rowNumber, scoreColumn, rowNumber, scoreColumn)
.AddConditionalFormat();
var failingScoreFormat = conditionalFormat.WhenLessThan(60);
failingScoreFormat.Fill.BackgroundColor =
ClosedXML.Excel.XLColor.FromHtml("#FDECEC");
failingScoreFormat.Font.FontColor =
ClosedXML.Excel.XLColor.FromHtml("#B42318");
}
});
var taskName = sheet.TeachingTask!.Name;
return File(bytes, ExcelWorkbookHelper.ContentType,
$"成绩导入模板-{taskName}.xlsx");
@@ -768,13 +812,15 @@ public sealed class GradesController(
var itemNames = sheet.Items.Select(i => i.Name).ToList();
var headers = new List<string> { "学号", "姓名", "班级", "平时成绩" };
headers.AddRange(itemNames);
headers.AddRange(["期末成绩", "考试状态", "备注"]);
headers.AddRange(["总分(自动计算)", "期末成绩", "考试状态", "备注"]);
IReadOnlyList<ExcelRow> rows;
try
{
rows = await ExcelWorkbookHelper.ReadAsync(
file, headers, cancellationToken);
file,
headers.Where(x => x != "总分(自动计算)").ToArray(),
cancellationToken);
}
catch (InvalidDataException exception)
{
@@ -810,7 +856,7 @@ public sealed class GradesController(
var regularScore = ParseOptionalDecimal(row, "平时成绩", 0, 100, errors);
if (errors.Count > 0 && errors[^1].Contains(row.RowNumber.ToString())) continue;
// Parse final score
// Parse final score. The formula-driven total column is intentionally ignored.
var finalScore = ParseOptionalDecimal(row, "期末成绩", 0, 100, errors);
if (errors.Count > 0 && errors[^1].Contains(row.RowNumber.ToString())) continue;
@@ -1041,6 +1087,18 @@ public sealed class GradesController(
private static string? Normalize(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private static string ColumnLetter(int column)
{
var result = string.Empty;
while (column > 0)
{
column--;
result = (char)('A' + column % 26) + result;
column /= 26;
}
return result;
}
}
public sealed record GradeSheetRequest(
@@ -146,7 +146,10 @@ public sealed class OtherExamsController(AppDbContext db, ICurrentUserDataScope
{
var numbers = inputs.Select(x => x.StudentNumber.Trim()).ToList();
if (numbers.Count != numbers.Distinct(StringComparer.OrdinalIgnoreCase).Count()) return ValidationProblem("同一考试批次中学生不能重复出现。");
var students = await db.Students.Where(x => numbers.Contains(x.StudentNumber)).ToDictionaryAsync(x => x.StudentNumber, StringComparer.OrdinalIgnoreCase, ct);
var studentRows = await db.Students
.Where(x => numbers.Contains(x.StudentNumber))
.ToListAsync(ct);
var students = studentRows.ToDictionary(x => x.StudentNumber, StringComparer.OrdinalIgnoreCase);
if (students.Count != numbers.Count) return ValidationProblem("存在不存在的学号,请先检查学生档案。");
foreach (var item in inputs)
{
@@ -157,11 +160,30 @@ public sealed class OtherExamsController(AppDbContext db, ICurrentUserDataScope
var beforeCount = await db.OtherExamResults.AsNoTracking()
.Where(x => x.OtherExamBatchId != batch.Id && studentIds.Contains(x.StudentId) && (x.OtherExamBatch!.ExamCode == batch.ExamCode || (x.OtherExamBatch.ExamCode == null && batch.ExamCode == null && x.OtherExamBatch.Name == batch.Name)) && (x.OtherExamBatch.ExamDate < batch.ExamDate || (x.OtherExamBatch.ExamDate == batch.ExamDate && x.OtherExamBatch.CreatedAt < batch.CreatedAt)))
.GroupBy(x => x.StudentId).Select(x => new { StudentId = x.Key, Count = x.Count() }).ToDictionaryAsync(x => x.StudentId, x => x.Count, ct);
db.OtherExamResults.RemoveRange(batch.Results);
batch.Status = OtherExamBatchStatus.Draft;
batch.Results = inputs.Select(x => { var student = students[x.StudentNumber.Trim()]; return new OtherExamResult { OtherExamBatchId = batch.Id, StudentId = student.Id, AttemptNumber = (beforeCount.GetValueOrDefault(student.Id) + 1), Score = x.Score, Level = Normalize(x.Level), IsPassed = x.IsPassed, Notes = Normalize(x.Notes) }; }).ToList();
await db.SaveChangesAsync(ct);
return null;
return await db.ExecuteInRetriableTransactionAsync<ActionResult?>(async transaction =>
{
db.OtherExamResults.RemoveRange(batch.Results);
batch.Status = OtherExamBatchStatus.Draft;
await db.SaveChangesAsync(ct);
batch.Results = inputs.Select(x =>
{
var student = students[x.StudentNumber.Trim()];
return new OtherExamResult
{
OtherExamBatchId = batch.Id,
StudentId = student.Id,
AttemptNumber = beforeCount.GetValueOrDefault(student.Id) + 1,
Score = x.Score,
Level = Normalize(x.Level),
IsPassed = x.IsPassed,
Notes = Normalize(x.Notes)
};
}).ToList();
await db.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
return null;
}, ct);
}
private static string[] HeadersFor(OtherExamBatch batch) => batch.MetricKind switch
@@ -11,7 +11,8 @@ public static class ExcelWorkbookHelper
string sheetName,
IReadOnlyList<string> headers,
IEnumerable<IReadOnlyList<object?>> rows,
IReadOnlyList<string>? instructions = null)
IReadOnlyList<string>? instructions = null,
Action<IXLWorksheet, int>? configureRow = null)
{
using var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add(sheetName);
@@ -33,6 +34,7 @@ public static class ExcelWorkbookHelper
{
SetCellValue(sheet.Cell(rowNumber, column + 1), row[column]);
}
configureRow?.Invoke(sheet, rowNumber);
rowNumber++;
}
+2 -2
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Version>2.3.0-rc3</Version>
<Version>2.3.1-beta.1</Version>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<SpaRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../../web'))</SpaRoot>
@@ -32,7 +32,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageReference Include="MySql.EntityFrameworkCore" Version="10.0.7" />
<PackageReference Include="MySql.EntityFrameworkCore" Version="10.0.9" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />