体测系统
This commit is contained in:
1 parent
33b875b21a
commit
c74c1e3914
19 files changed
+10507
-7
No files matched your search
@@ -0,0 +1,150 @@
|
||||
using Jiaowu.Api.Controllers;
|
||||
using Jiaowu.Api.Contracts;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class PhysicalFitnessControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task UpsertRecords_CalculatesAndPersistsItemSnapshot()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync();
|
||||
var controller = new PhysicalFitnessController(fixture.Db, fixture.AllScope);
|
||||
|
||||
var action = await controller.UpsertRecords(fixture.Batch.Id,
|
||||
new UpsertPhysicalTestRecordsRequest([
|
||||
new PhysicalTestRecordRequest(
|
||||
fixture.Student.StudentNumber, PhysicalTestRecordStatus.Normal, 1,
|
||||
175, 70, 5040, 6.7m, 24.9m, 273, 19, 197, null, "已复核")
|
||||
]), CancellationToken.None);
|
||||
|
||||
Assert.IsType<OkObjectResult>(action);
|
||||
fixture.Db.ChangeTracker.Clear();
|
||||
var saved = await fixture.Db.PhysicalTestRecords.Include(x => x.Items).SingleAsync();
|
||||
Assert.Equal(100m, saved.TotalScore);
|
||||
Assert.Equal(PhysicalTestGrade.Excellent, saved.Grade);
|
||||
Assert.Equal(9, saved.Items.Count);
|
||||
Assert.Equal("已复核", saved.Notes);
|
||||
Assert.Equal(PhysicalTestBatchStatus.Draft, (await fixture.Db.PhysicalTestBatches.FindAsync(fixture.Batch.Id))!.Status);
|
||||
|
||||
fixture.Db.ChangeTracker.Clear();
|
||||
var secondController = new PhysicalFitnessController(fixture.Db, fixture.AllScope);
|
||||
var updated = await secondController.UpsertRecords(fixture.Batch.Id,
|
||||
new UpsertPhysicalTestRecordsRequest([
|
||||
new PhysicalTestRecordRequest(
|
||||
fixture.Student.StudentNumber, PhysicalTestRecordStatus.Normal, 1,
|
||||
175, 70, 4920, 6.8m, 23.1m, 268, 18, 202, null, "二次复核")
|
||||
]), CancellationToken.None);
|
||||
Assert.IsType<OkObjectResult>(updated);
|
||||
fixture.Db.ChangeTracker.Clear();
|
||||
var replacement = await fixture.Db.PhysicalTestRecords.Include(x => x.Items).SingleAsync();
|
||||
Assert.Equal(9, replacement.Items.Count);
|
||||
Assert.Equal("二次复核", replacement.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CollegeViewer_OnlySeesRecordsInsideCollegeScope()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync(includeSecondCollege: true);
|
||||
fixture.Db.PhysicalTestRecords.AddRange(
|
||||
new PhysicalTestRecord { PhysicalTestBatchId = fixture.Batch.Id, StudentId = fixture.Student.Id, GradeLevel = 1, Status = PhysicalTestRecordStatus.Exempt, SpecialReason = "测试" },
|
||||
new PhysicalTestRecord { PhysicalTestBatchId = fixture.Batch.Id, StudentId = fixture.OtherStudent!.Id, GradeLevel = 1, Status = PhysicalTestRecordStatus.Exempt, SpecialReason = "测试" });
|
||||
await fixture.Db.SaveChangesAsync();
|
||||
var controller = new PhysicalFitnessController(fixture.Db, fixture.CollegeScope);
|
||||
|
||||
var action = Assert.IsType<OkObjectResult>(await controller.GetRecords(
|
||||
fixture.Batch.Id, pageSize: 30, ct: CancellationToken.None));
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(action.Value);
|
||||
|
||||
Assert.Contains(fixture.Student.StudentNumber, json);
|
||||
Assert.DoesNotContain(fixture.OtherStudent.StudentNumber, json);
|
||||
|
||||
var batches = Assert.IsType<OkObjectResult>(await controller.GetBatches(
|
||||
pageSize: 20, ct: CancellationToken.None));
|
||||
var page = Assert.IsType<CursorPagedResult<object>>(batches.Value);
|
||||
var visibleCount = page.Items.Single().GetType().GetProperty("RecordCount")!.GetValue(page.Items.Single());
|
||||
Assert.Equal(1, visibleCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Publish_RejectsEmptyBatch()
|
||||
{
|
||||
await using var fixture = await Fixture.CreateAsync();
|
||||
var controller = new PhysicalFitnessController(fixture.Db, fixture.AllScope);
|
||||
|
||||
var action = await controller.Publish(fixture.Batch.Id, CancellationToken.None);
|
||||
|
||||
var conflict = Assert.IsType<ConflictObjectResult>(action);
|
||||
Assert.Contains("没有体测记录", Assert.IsType<ProblemDetails>(conflict.Value).Detail);
|
||||
}
|
||||
|
||||
private sealed class Fixture : IAsyncDisposable
|
||||
{
|
||||
private readonly SqliteConnection connection;
|
||||
public AppDbContext Db { get; }
|
||||
public College College { get; }
|
||||
public Student Student { get; }
|
||||
public Student? OtherStudent { get; }
|
||||
public PhysicalTestBatch Batch { get; }
|
||||
public ICurrentUserDataScope AllScope { get; }
|
||||
public ICurrentUserDataScope CollegeScope { get; }
|
||||
|
||||
private Fixture(SqliteConnection connection, AppDbContext db, College college,
|
||||
Student student, Student? otherStudent, PhysicalTestBatch batch)
|
||||
{
|
||||
this.connection = connection;
|
||||
Db = db;
|
||||
College = college;
|
||||
Student = student;
|
||||
OtherStudent = otherStudent;
|
||||
Batch = batch;
|
||||
AllScope = new TestScope(null, DataScope.All, SystemRoles.PhysicalEducationAdmin);
|
||||
CollegeScope = new TestScope(college.Id, DataScope.College, SystemRoles.CollegeAdmin);
|
||||
}
|
||||
|
||||
public static async Task<Fixture> CreateAsync(bool includeSecondCollege = false)
|
||||
{
|
||||
var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
var db = new AppDbContext(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(connection).Options);
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||
var major = new Major { Code = "SE", Name = "软件工程", CollegeId = college.Id, DegreeType = "工学" };
|
||||
var administrativeClass = new AdministrativeClass { Code = "SE202601", Name = "软件工程 2026 级 1 班", MajorId = major.Id, Grade = 2026 };
|
||||
var student = new Student { StudentNumber = "202600001", Name = "学生甲", Gender = Gender.Male, AdministrativeClassId = administrativeClass.Id, EnrollmentYear = 2026, EnrollmentDate = new DateOnly(2026, 9, 1) };
|
||||
Student? otherStudent = null;
|
||||
db.AddRange(college, major, administrativeClass, student);
|
||||
if (includeSecondCollege)
|
||||
{
|
||||
var otherCollege = new College { Code = "ART", Name = "艺术学院" };
|
||||
var otherMajor = new Major { Code = "ART", Name = "艺术学", CollegeId = otherCollege.Id, DegreeType = "艺术学" };
|
||||
var otherClass = new AdministrativeClass { Code = "ART202601", Name = "艺术 2026 级 1 班", MajorId = otherMajor.Id, Grade = 2026 };
|
||||
otherStudent = new Student { StudentNumber = "202600002", Name = "学生乙", Gender = Gender.Female, AdministrativeClassId = otherClass.Id, EnrollmentYear = 2026, EnrollmentDate = new DateOnly(2026, 9, 1) };
|
||||
db.AddRange(otherCollege, otherMajor, otherClass, otherStudent);
|
||||
}
|
||||
var batch = new PhysicalTestBatch { Name = "秋季体测", AcademicYear = "2026-2027", TestDate = new DateOnly(2026, 10, 15), StandardVersion = "国家学生体质健康标准(2014年修订)" };
|
||||
db.Add(batch);
|
||||
await db.SaveChangesAsync();
|
||||
return new Fixture(connection, db, college, student, otherStudent, batch);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Db.DisposeAsync();
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestScope(Guid? collegeId, DataScope dataScope, string role) : ICurrentUserDataScope
|
||||
{
|
||||
public CurrentUserScope Current { get; } = new(
|
||||
Guid.NewGuid(), "测试用户", collegeId, dataScope, new HashSet<string>([role]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Academic;
|
||||
|
||||
namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class PhysicalFitnessScoringTests
|
||||
{
|
||||
[Fact]
|
||||
public void MaleFreshman_AtAllPerfectThresholds_ScoresOneHundred()
|
||||
{
|
||||
var result = PhysicalFitnessScoringService.Calculate(
|
||||
Gender.Male,
|
||||
1,
|
||||
new PhysicalFitnessMeasurements(175, 70, 5040, 6.7m, 24.9m, 273, 19, 197));
|
||||
|
||||
Assert.Equal(100m, result.StandardScore);
|
||||
Assert.Equal(0m, result.BonusScore);
|
||||
Assert.Equal(100m, result.TotalScore);
|
||||
Assert.Equal(PhysicalTestGrade.Excellent, result.Grade);
|
||||
Assert.Equal(9, result.Items.Count);
|
||||
Assert.Equal(22.9m, result.Items.Single(x => x.ItemCode == PhysicalTestItemCode.Bmi).Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FemaleSenior_UsesSeniorFemaleThresholdsAndBonus()
|
||||
{
|
||||
var result = PhysicalFitnessScoringService.Calculate(
|
||||
Gender.Female,
|
||||
4,
|
||||
new PhysicalFitnessMeasurements(165, 55, 3450, 7.4m, 26.3m, 208, 70, 146));
|
||||
|
||||
Assert.Equal(100m, result.StandardScore);
|
||||
Assert.Equal(20m, result.BonusScore);
|
||||
Assert.Equal(120m, result.TotalScore);
|
||||
Assert.Equal(10m, result.Items.Single(x => x.ItemCode == PhysicalTestItemCode.Strength).BonusScore);
|
||||
Assert.Equal(10m, result.Items.Single(x => x.ItemCode == PhysicalTestItemCode.EnduranceRun).BonusScore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownGender_IsRejected()
|
||||
{
|
||||
var error = Assert.Throws<ArgumentException>(() => PhysicalFitnessScoringService.Calculate(
|
||||
Gender.Unknown,
|
||||
1,
|
||||
new PhysicalFitnessMeasurements(170, 60, 3000, 8, 10, 200, 10, 240)));
|
||||
|
||||
Assert.Contains("性别", error.Message);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user