游标分页初步
This commit is contained in:
1 parent
14af0910e7
commit
bdb2e54521
5 files changed
+99
-20
No files matched your search
@@ -0,0 +1,29 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Jiaowu.Api.Contracts;
|
||||
|
||||
/// <summary>Encodes endpoint-specific keyset values without exposing a query offset.</summary>
|
||||
public static class CursorToken
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public static string Encode<T>(T value) =>
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value, Options)))
|
||||
.TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||
|
||||
public static bool TryDecode<T>(string? token, out T? value)
|
||||
{
|
||||
value = default;
|
||||
if (string.IsNullOrWhiteSpace(token)) return true;
|
||||
try
|
||||
{
|
||||
var normalized = token.Replace('-', '+').Replace('_', '/');
|
||||
normalized = normalized.PadRight(normalized.Length + (4 - normalized.Length % 4) % 4, '=');
|
||||
value = JsonSerializer.Deserialize<T>(Encoding.UTF8.GetString(Convert.FromBase64String(normalized)), Options);
|
||||
return value is not null;
|
||||
}
|
||||
catch (JsonException) { return false; }
|
||||
catch (FormatException) { return false; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
namespace Jiaowu.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// A forward-only keyset page. <see cref="NextCursor"/> is opaque to clients and
|
||||
/// must be sent back unchanged as the <c>cursor</c> query parameter.
|
||||
/// </summary>
|
||||
public sealed record CursorPagedResult<T>(
|
||||
IReadOnlyCollection<T> Items,
|
||||
string? NextCursor,
|
||||
bool HasMore,
|
||||
int PageSize);
|
||||
|
||||
/// <summary>
|
||||
/// Legacy offset-page response. New list endpoints must use
|
||||
/// <see cref="CursorPagedResult{T}"/> instead.
|
||||
/// </summary>
|
||||
public sealed record PagedResult<T>(
|
||||
IReadOnlyCollection<T> Items,
|
||||
int Total,
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Data;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using Jiaowu.Api.Contracts;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
@@ -34,12 +35,13 @@ public sealed class NotificationsController(
|
||||
bool? unreadOnly,
|
||||
NotificationCategory? category,
|
||||
string? keyword,
|
||||
int page = 1,
|
||||
string? cursor = null,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
if (!CursorToken.TryDecode<NotificationCursor>(cursor, out var after))
|
||||
return BadRequest("通知游标无效。");
|
||||
var userId = currentUserDataScope.Current.UserId;
|
||||
var source = db.Notifications.AsNoTracking()
|
||||
.Where(x => x.UserId == userId);
|
||||
@@ -55,12 +57,16 @@ public sealed class NotificationsController(
|
||||
x.Content.Contains(value));
|
||||
}
|
||||
|
||||
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 rows = await source
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.ThenByDescending(x => x.Id)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Take(pageSize + 1)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
@@ -79,6 +85,9 @@ public sealed class NotificationsController(
|
||||
x.CreatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var hasMore = rows.Count > pageSize;
|
||||
var items = rows.Take(pageSize).ToArray();
|
||||
var last = items.LastOrDefault();
|
||||
|
||||
var unreadCount = await db.Notifications
|
||||
.CountAsync(x => x.UserId == userId && !x.IsRead, cancellationToken);
|
||||
@@ -91,12 +100,18 @@ public sealed class NotificationsController(
|
||||
return Ok(new
|
||||
{
|
||||
Items = items,
|
||||
Total = total,
|
||||
HasMore = hasMore,
|
||||
NextCursor = hasMore && last is not null
|
||||
? CursorToken.Encode(new NotificationCursor(last.CreatedAt, last.Id))
|
||||
: null,
|
||||
PageSize = pageSize,
|
||||
UnreadCount = unreadCount,
|
||||
UnreadByCategory = unreadByCategory
|
||||
});
|
||||
}
|
||||
|
||||
private sealed record NotificationCursor(DateTime CreatedAt, Guid Id);
|
||||
|
||||
[HttpGet("unread-count")]
|
||||
public async Task<ActionResult> GetUnreadCount(CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user