增强筛选

This commit is contained in:
biss committed 2026-08-11 16:34:52 +08:00
1 parent b08b4cc4df
commit eb20211f0e
5 files changed
+253 -18

No files matched your search

@@ -399,8 +399,22 @@ public sealed class CourseSelectionsController(
[HttpGet("offerings/{id:guid}/roster")] [HttpGet("offerings/{id:guid}/roster")]
[Authorize(Roles = RosterReaders)] [Authorize(Roles = RosterReaders)]
public async Task<ActionResult> GetRoster(Guid id, CancellationToken cancellationToken) public async Task<ActionResult> GetRoster(
Guid id,
string? keyword = null,
int? grade = null,
Guid? majorId = null,
Guid? administrativeClassId = null,
int studentPage = 1,
int studentPageSize = 20,
int waitlistPage = 1,
int waitlistPageSize = 20,
CancellationToken cancellationToken = default)
{ {
studentPage = Math.Max(1, studentPage);
studentPageSize = Math.Clamp(studentPageSize, 10, 100);
waitlistPage = Math.Max(1, waitlistPage);
waitlistPageSize = Math.Clamp(waitlistPageSize, 10, 100);
var offering = await db.CourseSelectionOfferings.AsNoTracking() var offering = await db.CourseSelectionOfferings.AsNoTracking()
.Where(x => x.Id == id) .Where(x => x.Id == id)
.Select(x => new .Select(x => new
@@ -428,11 +442,64 @@ public sealed class CourseSelectionsController(
if (!isAssignedTeacher && !scope.CanAccessCollege(offering.CollegeId)) if (!isAssignedTeacher && !scope.CanAccessCollege(offering.CollegeId))
return Forbid(); return Forbid();
var students = await db.CourseEnrollments.AsNoTracking() var enrolledSource = db.CourseEnrollments.AsNoTracking()
.Where(x => .Where(x =>
x.CourseSelectionOfferingId == id && x.CourseSelectionOfferingId == id &&
x.Status == CourseEnrollmentStatus.Enrolled) x.Status == CourseEnrollmentStatus.Enrolled);
var waitlistSource = db.CourseEnrollments.AsNoTracking()
.Where(x =>
x.CourseSelectionOfferingId == id &&
x.Status == CourseEnrollmentStatus.Waitlisted);
var enrolledCount = await enrolledSource.CountAsync(cancellationToken);
var waitlistedCount = await waitlistSource.CountAsync(cancellationToken);
var rosterSource = db.CourseEnrollments.AsNoTracking().Where(x =>
x.CourseSelectionOfferingId == id &&
(x.Status == CourseEnrollmentStatus.Enrolled ||
x.Status == CourseEnrollmentStatus.Waitlisted));
var filterOptions = await rosterSource
.Select(x => new
{
Grade = x.Student!.AdministrativeClass!.Grade,
MajorId = x.Student.AdministrativeClass.MajorId,
MajorName = x.Student.AdministrativeClass.Major!.Name,
ClassId = x.Student.AdministrativeClassId,
ClassName = x.Student.AdministrativeClass.Name
})
.Distinct()
.ToListAsync(cancellationToken);
if (grade.HasValue)
{
enrolledSource = enrolledSource.Where(x => x.Student!.AdministrativeClass!.Grade == grade.Value);
waitlistSource = waitlistSource.Where(x => x.Student!.AdministrativeClass!.Grade == grade.Value);
}
if (majorId.HasValue)
{
enrolledSource = enrolledSource.Where(x => x.Student!.AdministrativeClass!.MajorId == majorId.Value);
waitlistSource = waitlistSource.Where(x => x.Student!.AdministrativeClass!.MajorId == majorId.Value);
}
if (administrativeClassId.HasValue)
{
enrolledSource = enrolledSource.Where(x => x.Student!.AdministrativeClassId == administrativeClassId.Value);
waitlistSource = waitlistSource.Where(x => x.Student!.AdministrativeClassId == administrativeClassId.Value);
}
if (!string.IsNullOrWhiteSpace(keyword))
{
keyword = keyword.Trim();
enrolledSource = enrolledSource.Where(x =>
x.Student!.StudentNumber.Contains(keyword) ||
x.Student.Name.Contains(keyword) ||
x.Student.AdministrativeClass!.Name.Contains(keyword));
waitlistSource = waitlistSource.Where(x =>
x.Student!.StudentNumber.Contains(keyword) ||
x.Student.Name.Contains(keyword) ||
x.Student.AdministrativeClass!.Name.Contains(keyword));
}
var enrolledTotal = await enrolledSource.CountAsync(cancellationToken);
var waitlistedTotal = await waitlistSource.CountAsync(cancellationToken);
var students = await enrolledSource
.OrderBy(x => x.Student!.StudentNumber) .OrderBy(x => x.Student!.StudentNumber)
.Skip((studentPage - 1) * studentPageSize)
.Take(studentPageSize)
.Select(x => new .Select(x => new
{ {
x.Id, x.Id,
@@ -445,12 +512,11 @@ public sealed class CourseSelectionsController(
x.EnrolledAt x.EnrolledAt
}) })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var waitlistedRows = await db.CourseEnrollments.AsNoTracking() var waitlistedRows = await waitlistSource
.Where(x =>
x.CourseSelectionOfferingId == id &&
x.Status == CourseEnrollmentStatus.Waitlisted)
.OrderBy(x => x.WaitlistedAt) .OrderBy(x => x.WaitlistedAt)
.ThenBy(x => x.CreatedAt) .ThenBy(x => x.CreatedAt)
.Skip((waitlistPage - 1) * waitlistPageSize)
.Take(waitlistPageSize)
.Select(x => new .Select(x => new
{ {
x.Id, x.Id,
@@ -474,7 +540,7 @@ public sealed class CourseSelectionsController(
item.MajorName, item.MajorName,
item.Grade, item.Grade,
item.WaitlistedAt, item.WaitlistedAt,
Position = index + 1 Position = (waitlistPage - 1) * waitlistPageSize + index + 1
}) })
.ToList(); .ToList();
return Ok(new return Ok(new
@@ -487,10 +553,24 @@ public sealed class CourseSelectionsController(
offering.CourseName, offering.CourseName,
offering.CourseNature, offering.CourseNature,
offering.Capacity, offering.Capacity,
EnrolledCount = students.Count, EnrolledCount = enrolledCount,
Students = students, Students = students,
WaitlistedCount = waitlist.Count, StudentPage = studentPage,
StudentPageSize = studentPageSize,
StudentTotal = enrolledTotal,
WaitlistedCount = waitlistedCount,
Waitlist = waitlist, Waitlist = waitlist,
WaitlistPage = waitlistPage,
WaitlistPageSize = waitlistPageSize,
WaitlistTotal = waitlistedTotal,
FilterOptions = new
{
Grades = filterOptions.Select(x => x.Grade).Distinct().OrderBy(x => x),
Majors = filterOptions.Select(x => new { x.MajorId, x.MajorName }).Distinct()
.OrderBy(x => x.MajorName),
Classes = filterOptions.Select(x => new { x.ClassId, x.ClassName, x.MajorId })
.Distinct().OrderBy(x => x.ClassName)
},
CanManageWaitlist = CanManageWaitlist =
offering.RoundStatus == CourseSelectionRoundStatus.Open && offering.RoundStatus == CourseSelectionRoundStatus.Open &&
(scope.IsInRole(SystemRoles.SuperAdmin) || (scope.IsInRole(SystemRoles.SuperAdmin) ||
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Auth;
@@ -20,10 +21,38 @@ public sealed class OtherExamsController(AppDbContext db, ICurrentUserDataScope
[HttpGet("batches")] [HttpGet("batches")]
[Authorize(Roles = Managers)] [Authorize(Roles = Managers)]
public async Task<ActionResult> GetBatches(CancellationToken ct) => Ok(await db.OtherExamBatches public async Task<ActionResult> GetBatches(
.AsNoTracking().OrderByDescending(x => x.ExamDate).ThenByDescending(x => x.CreatedAt) string? keyword = null,
OtherExamBatchStatus? status = null,
OtherExamMetricKind? metricKind = null,
DateOnly? examDateFrom = null,
DateOnly? examDateTo = null,
int page = 1,
int pageSize = 20,
CancellationToken ct = default)
{
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 10, 100);
var source = db.OtherExamBatches.AsNoTracking();
if (!string.IsNullOrWhiteSpace(keyword))
{
keyword = keyword.Trim();
source = source.Where(x =>
(x.ExamCode != null && x.ExamCode.Contains(keyword)) ||
x.Name.Contains(keyword) ||
(x.Organizer != null && x.Organizer.Contains(keyword)));
}
if (status.HasValue) source = source.Where(x => x.Status == status.Value);
if (metricKind.HasValue) source = source.Where(x => x.MetricKind == metricKind.Value);
if (examDateFrom.HasValue) source = source.Where(x => x.ExamDate >= examDateFrom.Value);
if (examDateTo.HasValue) source = source.Where(x => x.ExamDate <= examDateTo.Value);
var total = await source.CountAsync(ct);
var items = await source.OrderByDescending(x => x.ExamDate).ThenByDescending(x => x.CreatedAt)
.Skip((page - 1) * pageSize).Take(pageSize)
.Select(x => new { x.Id, x.ExamCode, x.Name, x.Organizer, x.ExamDate, x.MetricKind, x.MaxScore, x.LevelOptions, x.Status, x.PublicationCount, x.PublishedAt, ResultCount = x.Results.Count }) .Select(x => new { x.Id, x.ExamCode, x.Name, x.Organizer, x.ExamDate, x.MetricKind, x.MaxScore, x.LevelOptions, x.Status, x.PublicationCount, x.PublishedAt, ResultCount = x.Results.Count })
.ToListAsync(ct)); .ToListAsync(ct);
return Ok(new PagedResult<object>(items, total, page, pageSize));
}
[HttpPost("batches")] [HttpPost("batches")]
[Authorize(Roles = Managers)] [Authorize(Roles = Managers)]
@@ -1,4 +1,5 @@
using Jiaowu.Api.Controllers; using Jiaowu.Api.Controllers;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Auth;
@@ -11,6 +12,37 @@ namespace Jiaowu.Api.Tests;
public sealed class OtherExamsControllerTests public sealed class OtherExamsControllerTests
{ {
[Fact]
public async Task Batches_AreFilteredAndReturnedByPage()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
await using var db = new AppDbContext(
new DbContextOptionsBuilder<AppDbContext>().UseSqlite(connection).Options);
await db.Database.EnsureCreatedAsync();
db.OtherExamBatches.AddRange(Enumerable.Range(1, 11).Select(number =>
new OtherExamBatch
{
ExamCode = $"CET-{number:00}",
Name = $"英语等级考试 {number}",
ExamDate = new DateOnly(2026, 6, number),
MetricKind = OtherExamMetricKind.Score,
MaxScore = 100,
Status = number == 11
? OtherExamBatchStatus.Published
: OtherExamBatchStatus.Draft
}));
await db.SaveChangesAsync();
var controller = new OtherExamsController(db, new TestDataScope(Guid.NewGuid()));
var result = Assert.IsType<OkObjectResult>(await controller.GetBatches(
keyword: "英语", status: OtherExamBatchStatus.Draft, page: 1,
pageSize: 10, ct: CancellationToken.None));
var page = Assert.IsType<PagedResult<object>>(result.Value);
Assert.Equal(10, page.Total);
Assert.Equal(10, page.Items.Count);
}
[Fact] [Fact]
public async Task ReplaceResults_ReplacesExistingResultsInsideTransaction() public async Task ReplaceResults_ReplacesExistingResultsInsideTransaction()
{ {
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff. Show more