服务器端分页优化

This commit is contained in:
2026-08-11 11:46:26 +08:00 Unverified
parent 292d029459
commit 83103eebb8
7 changed files with 287 additions and 50 deletions
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System;
@@ -23,43 +24,73 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
// ═══════════════ Aggregated pending ═══════════════
[HttpGet("pending")]
[Authorize(Roles = Managers)]
public async Task<ActionResult> GetPending(CancellationToken ct)
public async Task<ActionResult<PagedResult<ApprovalItem>>> GetPending(
int page = 1,
int pageSize = 20,
CancellationToken ct = default)
{
var items = new List<ApprovalItem>();
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("page 必须大于 0pageSize 必须在 1 到 100 之间。");
var take = checked(page * pageSize);
var total = 0;
var items = new List<ApprovalItem>(take * 8);
items.AddRange(await Scoped<CourseExemption>(x => x.Status == ApprovalStatus.Submitted)
.OrderByDescending(x => x.SubmittedAt).Take(take)
.Select(x => new ApprovalItem(x.Id, "CourseExemption", "免修", $"{x.Student!.Name} — 《{x.TeachingTask!.Course!.Name}》", x.Reason, x.SubmittedAt, x.TeachingTask.Course!.College!.Name))
.ToListAsync(ct));
total += await Scoped<CourseExemption>(x => x.Status == ApprovalStatus.Submitted).CountAsync(ct);
items.AddRange(await Scoped<DeferredExam>(x => x.Status == ApprovalStatus.Submitted)
.OrderByDescending(x => x.SubmittedAt).Take(take)
.Select(x => new ApprovalItem(x.Id, "DeferredExam", "缓考", $"{x.Student!.Name} — 《{x.TeachingTask!.Course!.Name}》", x.Reason, x.SubmittedAt, x.TeachingTask.Course!.College!.Name))
.ToListAsync(ct));
total += await Scoped<DeferredExam>(x => x.Status == ApprovalStatus.Submitted).CountAsync(ct);
items.AddRange(await Scoped<GradeModification>(x => x.Status == GradeModificationStatus.TeacherSubmitted || x.Status == GradeModificationStatus.CollegeApproved)
.OrderByDescending(x => x.SubmittedAt).Take(take)
.Select(x => new ApprovalItem(x.Id, "GradeModification", "成绩修改", $"{x.GradeRecord!.Student!.Name} — 《{x.GradeRecord.GradeSheet!.TeachingTask!.Course!.Name}》", $"{x.CurrentScore} → {x.RequestedScore}{x.Reason}", x.SubmittedAt, x.GradeRecord.GradeSheet.TeachingTask.Course!.College!.Name))
.ToListAsync(ct));
total += await Scoped<GradeModification>(x => x.Status == GradeModificationStatus.TeacherSubmitted || x.Status == GradeModificationStatus.CollegeApproved).CountAsync(ct);
items.AddRange(await Scoped<CourseSubstitution>(x => x.Status == ApprovalStatus.Submitted)
.OrderByDescending(x => x.SubmittedAt).Take(take)
.Select(x => new ApprovalItem(x.Id, "CourseSubstitution", "课程替代", $"{x.Student!.Name}{x.SubstituteCourse!.Name} → {x.OriginalCourse!.Name}", x.Reason, x.SubmittedAt, x.Student.AdministrativeClass!.Major!.College!.Name))
.ToListAsync(ct));
total += await Scoped<CourseSubstitution>(x => x.Status == ApprovalStatus.Submitted).CountAsync(ct);
items.AddRange(await Scoped<StudentStatusChange>(x => x.State != StudentStatusChangeState.Approved && x.State != StudentStatusChangeState.Rejected && x.State != StudentStatusChangeState.Cancelled)
.OrderByDescending(x => x.SubmittedAt).Take(take)
.Select(x => new ApprovalItem(x.Id, "StudentStatusChange", "学籍异动", $"{x.Student!.Name} — {SSCLabel(x.Type)}", x.Reason, x.SubmittedAt, x.Student.AdministrativeClass!.Major!.College!.Name))
.ToListAsync(ct));
total += await Scoped<StudentStatusChange>(x => x.State != StudentStatusChangeState.Approved && x.State != StudentStatusChangeState.Rejected && x.State != StudentStatusChangeState.Cancelled).CountAsync(ct);
items.AddRange(await Scoped<CourseAdjustment>(x => x.Status == CourseAdjustmentStatus.Submitted)
.OrderByDescending(x => x.SubmittedAt).Take(take)
.Select(x => new ApprovalItem(x.Id, "CourseAdjustment", CALabel(x.Type), $"《{x.TeachingTask!.Course!.Name}》", x.Reason, x.SubmittedAt!.Value, x.TeachingTask.Course.College!.Name))
.ToListAsync(ct));
total += await Scoped<CourseAdjustment>(x => x.Status == CourseAdjustmentStatus.Submitted).CountAsync(ct);
items.AddRange(await Scoped<GradeSheet>(x => x.Status == GradeSheetStatus.Submitted)
.OrderByDescending(x => x.SubmittedAt).Take(take)
.Select(x => new ApprovalItem(x.Id, "GradeSheet", "成绩审核", $"《{x.TeachingTask!.Course!.Name}》— {x.Records.Count}人", "教师已提交成绩", x.SubmittedAt!.Value, x.TeachingTask.Course.College!.Name))
.ToListAsync(ct));
total += await Scoped<GradeSheet>(x => x.Status == GradeSheetStatus.Submitted).CountAsync(ct);
items.AddRange(await Scoped<AttendanceRecord>(x => x.AppealStatus == AttendanceAppealStatus.Pending)
.OrderByDescending(x => x.AppealSubmittedAt).Take(take)
.Select(x => new ApprovalItem(x.StudentId, "AttendanceAppeal", "考勤申诉", $"{x.Student!.Name} — 《{x.AttendanceSheet!.TeachingTask!.Course!.Name}》", x.AppealReason ?? "", x.AppealSubmittedAt!.Value, x.Student.AdministrativeClass!.Major!.College!.Name))
.ToListAsync(ct));
total += await Scoped<AttendanceRecord>(x => x.AppealStatus == AttendanceAppealStatus.Pending).CountAsync(ct);
return Ok(items.OrderByDescending(x => x.Time).ToList());
var pageItems = items
.OrderByDescending(x => x.Time)
.ThenByDescending(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToArray();
return Ok(new PagedResult<ApprovalItem>(pageItems, total, page, pageSize));
}
// ═══════════════ Course Exemption ═══════════════
+66 -22
View File
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Persistence;
@@ -19,10 +20,35 @@ public sealed class UsersController(
RoleManager<ApplicationRole> roleManager) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<object>> GetUsers(CancellationToken cancellationToken)
public async Task<ActionResult<PagedResult<UserListItem>>> GetUsers(
int page = 1,
int pageSize = 20,
string? keyword = null,
CancellationToken cancellationToken = default)
{
var users = await userManager.Users.AsNoTracking()
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
var query = userManager.Users.AsNoTracking();
if (!string.IsNullOrWhiteSpace(keyword))
{
keyword = keyword.Trim();
query = query.Where(user =>
user.UserName!.Contains(keyword) ||
user.DisplayName.Contains(keyword) ||
(user.StaffNumber != null && user.StaffNumber.Contains(keyword)) ||
(from userRole in db.UserRoles
join role in db.Roles on userRole.RoleId equals role.Id
where userRole.UserId == user.Id && role.Name!.Contains(keyword)
select role.Id).Any());
}
var total = await query.CountAsync(cancellationToken);
var users = await query
.OrderBy(x => x.UserName)
.ThenBy(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(x => new
{
x.Id,
@@ -36,26 +62,34 @@ public sealed class UsersController(
})
.ToListAsync(cancellationToken);
var result = new List<object>();
foreach (var user in users)
{
var identityUser = await userManager.FindByIdAsync(user.Id.ToString());
result.Add(new
{
user.Id,
user.UserName,
user.DisplayName,
user.StaffNumber,
user.CollegeId,
user.IsEnabled,
user.LastLoginAt,
user.CreatedAt,
Roles = identityUser is null
? []
: await userManager.GetRolesAsync(identityUser)
});
}
return Ok(result);
var userIds = users.Select(x => x.Id).ToArray();
var roleRows = await (
from userRole in db.UserRoles.AsNoTracking()
join role in db.Roles.AsNoTracking() on userRole.RoleId equals role.Id
where userIds.Contains(userRole.UserId)
select new { userRole.UserId, RoleName = role.Name! })
.ToListAsync(cancellationToken);
var rolesByUser = roleRows
.GroupBy(x => x.UserId)
.ToDictionary(
group => group.Key,
group => (IReadOnlyCollection<string>)group
.Select(x => x.RoleName)
.OrderBy(x => x)
.ToArray());
var items = users.Select(user => new UserListItem(
user.Id,
user.UserName ?? string.Empty,
user.DisplayName,
user.StaffNumber,
user.CollegeId,
user.IsEnabled,
user.LastLoginAt,
user.CreatedAt,
rolesByUser.GetValueOrDefault(user.Id, Array.Empty<string>())))
.ToArray();
return Ok(new PagedResult<UserListItem>(items, total, page, pageSize));
}
[HttpGet("roles")]
@@ -281,6 +315,16 @@ public sealed record CreateUserRequest(
[MinLength(1)] string[] Roles);
public sealed record SetUserStatusRequest(bool IsEnabled);
public sealed record UserListItem(
Guid Id,
string UserName,
string DisplayName,
string? StaffNumber,
Guid? CollegeId,
bool IsEnabled,
DateTime? LastLoginAt,
DateTime CreatedAt,
IReadOnlyCollection<string> Roles);
public sealed record SetRolesRequest(
[MaxLength(30)] string? StaffNumber,
Guid? CollegeId,
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
@@ -120,8 +121,16 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
[HttpGet("records")]
[Authorize(Roles = Managers + "," + SystemRoles.Counselor)]
public async Task<ActionResult> GetRecords(Guid? academicTermId, WarningType? type, CancellationToken ct)
public async Task<ActionResult<PagedResult<WarningRecordListItem>>> GetRecords(
Guid? academicTermId,
WarningType? type,
int page = 1,
int pageSize = 20,
CancellationToken ct = default)
{
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
var q = db.WarningRecords.AsNoTracking().AsQueryable();
if (scope.Current.Scope == DataScope.College || scope.Current.IsInRole(SystemRoles.Counselor))
{
@@ -133,7 +142,28 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
}
if (academicTermId.HasValue) q = q.Where(x => x.AcademicTermId == academicTermId);
if (type.HasValue) q = q.Where(x => x.Type == type);
return Ok(await q.OrderByDescending(x => x.CreatedAt).Select(x => new { x.Id, x.StudentId, StudentName = x.Student!.Name, StudentNumber = x.Student.StudentNumber, ClassName = x.Student.AdministrativeClass!.Name, Type = (int)x.Type, Status = (int)x.Status, x.TriggerValue, x.Detail, x.AcknowledgedAt, x.AcknowledgeComment, x.CreatedAt }).ToListAsync(ct));
var total = await q.CountAsync(ct);
var items = await q
.OrderByDescending(x => x.CreatedAt)
.ThenByDescending(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(x => new WarningRecordListItem(
x.Id,
x.StudentId,
x.Student!.Name,
x.Student.StudentNumber,
x.Student.AdministrativeClass!.Name,
(int)x.Type,
(int)x.Status,
x.TriggerValue,
x.Detail,
x.AcknowledgedAt,
x.AcknowledgeComment,
x.CreatedAt))
.ToListAsync(ct);
return Ok(new PagedResult<WarningRecordListItem>(items, total, page, pageSize));
}
// ═══════════ Student ═══════════
@@ -233,5 +263,18 @@ public sealed class WarningsController(AppDbContext db, ICurrentUserDataScope sc
}
public sealed record StudentInfo(Guid Id, Guid? UserId, string Name, Guid ClassId);
public sealed record WarningRecordListItem(
Guid Id,
Guid StudentId,
string StudentName,
string StudentNumber,
string ClassName,
int Type,
int Status,
decimal TriggerValue,
string Detail,
DateTime? AcknowledgedAt,
string? AcknowledgeComment,
DateTime CreatedAt);
public sealed record WarningRuleDto(WarningType Type, [MaxLength(100)] string Name, decimal Threshold, bool IsEnabled, bool NotifyStudent, bool NotifyCounselor, [MaxLength(300)] string? Description, bool AutoCheckEnabled, int? CheckDayOfWeek, int CheckHour, int CheckMinute);
public sealed record AckBody([MaxLength(300)] string? Comment);