游标分页优化

This commit is contained in:
biss committed 2026-08-23 10:03:32 +08:00
1 parent f5db2f6e77
commit 0a0713e65b
17 files changed
+448 -156

No files matched your search

@@ -113,15 +113,17 @@ public sealed partial class AppUpdatesController(AppDbContext db) : ControllerBa
[Authorize(Roles = SystemRoles.SuperAdmin)]
[HttpGet("releases")]
public async Task<ActionResult<PagedResult<AppUpdateReleaseItem>>> GetReleases(
[FromQuery] int page = 1,
public async Task<ActionResult<CursorPagedResult<AppUpdateReleaseItem>>> GetReleases(
[FromQuery] string? cursor = null,
[FromQuery] int pageSize = 20,
[FromQuery] string? platform = null,
[FromQuery] string? channel = null,
CancellationToken cancellationToken = default)
{
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于零,每页数量必须在 1 到 100 之间。");
if (pageSize is < 1 or > 100)
return ValidationProblem("每批数量必须在 1 到 100 之间。");
if (!CursorToken.TryDecode<ReleaseCursor>(cursor, out var after))
return BadRequest("更新包游标无效。");
var query = db.AppUpdateReleases.AsNoTracking().AsQueryable();
if (!string.IsNullOrWhiteSpace(platform))
@@ -137,11 +139,21 @@ public sealed partial class AppUpdatesController(AppDbContext db) : ControllerBa
query = query.Where(x => x.Channel == parsedChannel);
}
var total = await query.CountAsync(cancellationToken);
var rows = await query
if (after is not null)
{
query = query.Where(x => (x.PublishedAt ?? x.CreatedAt) < after.SortAt ||
(x.PublishedAt ?? x.CreatedAt) == after.SortAt && x.Id.CompareTo(after.Id) < 0);
}
var keys = await query
.OrderByDescending(x => x.PublishedAt ?? x.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ThenByDescending(x => x.Id)
.Take(pageSize + 1)
.Select(x => new { x.Id, SortAt = x.PublishedAt ?? x.CreatedAt })
.ToArrayAsync(cancellationToken);
var hasMore = keys.Length > pageSize;
var pageIds = keys.Take(pageSize).Select(x => x.Id).ToArray();
var rows = await query.WhereIn(pageIds, x => x.Id)
.OrderByDescending(x => x.PublishedAt ?? x.CreatedAt).ThenByDescending(x => x.Id)
.Select(x => new AppUpdateReleaseItem(
x.Id,
x.Platform,
@@ -158,13 +170,16 @@ public sealed partial class AppUpdatesController(AppDbContext db) : ControllerBa
x.PublishedByUserName,
x.PublishedAt))
.ToArrayAsync(cancellationToken);
return Ok(new PagedResult<AppUpdateReleaseItem>(
var last = keys.Take(pageSize).LastOrDefault();
return Ok(new CursorPagedResult<AppUpdateReleaseItem>(
rows,
total,
page,
hasMore && last is not null ? CursorToken.Encode(new ReleaseCursor(last.SortAt, last.Id)) : null,
hasMore,
pageSize));
}
private sealed record ReleaseCursor(DateTime SortAt, Guid Id);
[Authorize(Roles = SystemRoles.SuperAdmin)]
[Consumes("multipart/form-data")]
[RequestSizeLimit(MaximumBundleBytes + 1024 * 1024)]
@@ -177,12 +177,14 @@ public sealed class CourseAdjustmentsController(
public async Task<ActionResult> GetMine(
Guid? academicTermId,
CourseAdjustmentStatus? status,
int page = 1,
string? cursor = null,
int pageSize = 20,
CancellationToken cancellationToken = default)
{
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
if (pageSize is < 1 or > 100)
return ValidationProblem("每批条数应在 1 至 100 之间。");
if (!CursorToken.TryDecode<CreatedCursor>(cursor, out var after))
return BadRequest("调停课申请游标无效。");
var userId = currentUserDataScope.Current.UserId;
var source = db.CourseAdjustments.AsNoTracking()
@@ -193,15 +195,27 @@ public sealed class CourseAdjustmentsController(
if (status.HasValue)
source = source.Where(x => x.Status == status);
var total = await source.CountAsync(cancellationToken);
var items = await source
if (after is not null)
{
source = source.Where(x => x.CreatedAt < after.CreatedAt ||
x.CreatedAt == after.CreatedAt && x.Id.CompareTo(after.Id) < 0);
}
var keys = await source
.OrderByDescending(x => x.CreatedAt)
.ThenByDescending(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(AdjustmentProjection())
.Take(pageSize + 1)
.Select(x => new { x.Id, x.CreatedAt })
.ToListAsync(cancellationToken);
return Ok(new PagedResult<object>(items, total, page, pageSize));
var hasMore = keys.Count > pageSize;
var pageIds = keys.Take(pageSize).Select(x => x.Id).ToArray();
var items = await source.WhereIn(pageIds, x => x.Id)
.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)
.Select(AdjustmentProjection()).ToListAsync(cancellationToken);
var last = keys.Take(pageSize).LastOrDefault();
return Ok(new CursorPagedResult<object>(items,
hasMore && last is not null
? CursorToken.Encode(new CreatedCursor(last.CreatedAt, last.Id)) : null,
hasMore, pageSize));
}
// ═══════════════ Pending reviews ═══════════════
@@ -210,13 +224,15 @@ public sealed class CourseAdjustmentsController(
[Authorize(Roles = Reviewers)]
public async Task<ActionResult> GetPendingReviews(
Guid? academicTermId,
int page = 1,
string? cursor = null,
int pageSize = 20,
CancellationToken cancellationToken = default,
Guid? adjustmentId = null)
{
if (page < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
if (pageSize is < 1 or > 100)
return ValidationProblem("每批条数应在 1 至 100 之间。");
if (!CursorToken.TryDecode<SubmittedCursor>(cursor, out var after))
return BadRequest("待审核调停课游标无效。");
var scope = currentUserDataScope.Current;
var source = db.CourseAdjustments.AsNoTracking()
@@ -230,17 +246,32 @@ public sealed class CourseAdjustmentsController(
source = source.Where(x =>
x.TeachingTask!.AcademicTermId == academicTermId);
var total = await source.CountAsync(cancellationToken);
var items = await source
if (after is not null)
{
source = source.Where(x => x.SubmittedAt!.Value < after.SubmittedAt ||
x.SubmittedAt == after.SubmittedAt && x.Id.CompareTo(after.Id) < 0);
}
var keys = await source
.OrderByDescending(x => x.SubmittedAt)
.ThenByDescending(x => x.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(AdjustmentProjection())
.Take(pageSize + 1)
.Select(x => new { x.Id, SubmittedAt = x.SubmittedAt!.Value })
.ToListAsync(cancellationToken);
return Ok(new PagedResult<object>(items, total, page, pageSize));
var hasMore = keys.Count > pageSize;
var pageIds = keys.Take(pageSize).Select(x => x.Id).ToArray();
var items = await source.WhereIn(pageIds, x => x.Id)
.OrderByDescending(x => x.SubmittedAt).ThenByDescending(x => x.Id)
.Select(AdjustmentProjection()).ToListAsync(cancellationToken);
var last = keys.Take(pageSize).LastOrDefault();
return Ok(new CursorPagedResult<object>(items,
hasMore && last is not null
? CursorToken.Encode(new SubmittedCursor(last.SubmittedAt, last.Id)) : null,
hasMore, pageSize));
}
private sealed record CreatedCursor(DateTime CreatedAt, Guid Id);
private sealed record SubmittedCursor(DateTime SubmittedAt, Guid Id);
// ═══════════════ Detail ═══════════════
[HttpGet("{id:guid}")]
@@ -25,8 +25,8 @@ public sealed class CoursesController(
SystemRoles.CollegeAdmin;
[HttpGet]
public async Task<ActionResult<PagedResult<object>>> Get(
int page = 1,
public async Task<ActionResult<CursorPagedResult<object>>> Get(
string? cursor = null,
int pageSize = 20,
string? keyword = null,
Guid? collegeId = null,
@@ -35,8 +35,9 @@ public sealed class CoursesController(
bool? isEnabled = null,
CancellationToken cancellationToken = default)
{
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 10, 100);
if (!CursorToken.TryDecode<CourseCursor>(cursor, out var after))
return BadRequest("课程游标无效。");
var scope = currentUserDataScope.Current;
var canManageAll = scope.IsInRole(SystemRoles.SuperAdmin) ||
scope.IsInRole(SystemRoles.AcademicAdmin);
@@ -61,11 +62,18 @@ public sealed class CoursesController(
}
var total = await source.CountAsync(cancellationToken);
var items = await source
if (after is not null)
{
source = source.Where(x => x.SortOrder > after.SortOrder ||
x.SortOrder == after.SortOrder &&
(x.Code.CompareTo(after.Code) > 0 ||
x.Code == after.Code && x.Id.CompareTo(after.Id) > 0));
}
var rows = await source
.OrderBy(x => x.SortOrder)
.ThenBy(x => x.Code)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ThenBy(x => x.Id)
.Take(pageSize + 1)
.Select(x => new
{
x.Id,
@@ -106,10 +114,17 @@ public sealed class CoursesController(
x.Nature == CourseNature.Practice)
})
.ToListAsync(cancellationToken);
return Ok(new PagedResult<object>(items, total, page, pageSize));
var hasMore = rows.Count > pageSize;
var items = rows.Take(pageSize).ToArray();
var last = items.LastOrDefault();
return Ok(new CursorPagedResult<object>(items.Cast<object>().ToArray(),
hasMore && last is not null
? CursorToken.Encode(new CourseCursor(last.SortOrder, last.Code, last.Id)) : null,
hasMore, pageSize, total));
}
private sealed record CourseCursor(int SortOrder, string Code, Guid Id);
[HttpGet("options")]
public async Task<ActionResult<object>> GetOptions(
Guid? collegeId = null,
Loaded 3 of 17 files, more files were not shown because too many files have changed in this diff. Show more