本页说明列表用例从 HTTP 请求到 QueryShape,再到分页结果和导出的类型边界。目标形状是 Fields + Paging + Sort;传输仍使用 QUERY,并由 Platform SDK 在链路不支持时降级到 POST /_query。
1. 标准列表信封
新的或已迁移的 HTTP 列表 Query 使用以下结构:
// HTTP 信封只组合可序列化查询值;可信租户和当前用户不进入请求模型。public sealed record ListResourcesQuery( ResourceListFields Fields, PaginationParams Paging, SortRequest? Sort = null) : IQuery<Result<PagedResult<ResourceSummaryDto>>>, IPaginatedRequest;
// 业务筛选集中在 Fields,新增范围时不扩大顶层信封。public sealed record ResourceListFields( string? SearchText = null, DateRange? CreatedAt = null, AmountRange? Amount = null, EnumFilterSet<ResourceStatus>? Statuses = null);| 成员 | 职责 | 约束 |
|---|---|---|
Fields | 业务筛选条件 | 字段名使用业务语言;可包含多个命名范围,不声明顶层通用 Period |
Paging | HTTP offset 分页 | 使用 PaginationParams;IPaginatedRequest 只用于门禁识别,不提供数据继承 |
Sort | 单项标准排序 | SortRequest(Field, Descending);字段仍须由 QueryShape 白名单验证 |
| 返回值 | 列表结果 | 使用 Result<PagedResult<TSummaryDto>>,不新建模块级 *Page 壳 |
请求中的租户、当前用户和授权范围不是筛选字段。Handler 从可信上下文取得这些值,再把它们交给 Store 或基础谓词;不得允许 JSON 正文覆盖。
2. 分页规范化
PaginationParams 的页码从 1 开始,默认值为 PageIndex=1、PageSize=20。Normalize() 复用 PagingLimits,行为固定如下:
| 输入 | 规范化结果 |
|---|---|
PageIndex < 1 | 1 |
PageSize <= 0 | 20 |
PageSize > 1000 | 1000 |
| 其他合法值 | 原值 |
var paging = (request.Paging ?? new PaginationParams()).Normalize();var pageRequest = paging.ToPageRequest();var window = paging.ToWindow();ToWindow() 还使用 PageWindow 的深分页保护。默认最大 offset 为 100_000;超过窗口时 CanRead=false,调用方应返回空页或改用游标/键集分页,不能继续构造可能溢出的数据库 offset。
平台硬边界不是同一个数字
GlobalPlatformConstants 只收纳跨模块、不可按租户调整的稳定上限:
| 常量 | 当前值 | 使用范围 |
|---|---|---|
Pagination.DefaultPageSize | 20 | 在线列表别名,来源仍是 PagingLimits |
Pagination.MaxPageSize | 1000 | 在线列表别名,来源仍是 PagingLimits |
Batch.PageSize | 2000 | SAR、归档等 worker 的后台批读,不是 HTTP 页大小 |
Text.IdentifierMaxLength | 128 | 跨模块稳定标识 |
Text.DescriptionMaxLength | 1000 | 短描述 |
Text.RemarkMaxLength | 2000 | 备注与审计原因 |
远程选项 50、Workflow QueryStore 200 和 SCIM 200 是具名协议或引擎边界,不并入在线分页常量。
3. 筛选值对象
DateRange
DateRange 表示可单侧无界的时间闭区间,JSON 固定为对象:
{ "from": "2026-08-01T00:00:00+08:00", "to": "2026-08-13T00:00:00+08:00"}创建和反序列化都经过 DateRange.Create。下界晚于上界时返回 DateRange.Invalid;JSON 入口把该失败转换为 JsonException,不能绕过领域校验。
上界有两种语义:
| 输入上界 | To | 查询展开 |
|---|---|---|
当日零点,例如 2026-08-13T00:00:00+08:00 | 补齐到 8 月 13 日最后一个 tick | QueryToExclusive 为 8 月 14 日零点 |
显式时刻,例如 2026-08-13T15:30:45+08:00 | 保持原值 | 展开点使用 To.AddTicks(1) 取得排他上界 |
null | 该侧无界 | 不生成上界谓词 |
Contains 和展示使用闭区间的 To。数据库谓词统一使用排他上界和 LessThan,不能对日界值使用 LessThanOrEqual。
金额、数值和枚举集合
| 类型 | JSON/领域语义 | 失败与相等性 |
|---|---|---|
AmountRange | decimal? Min/Max 的金额闭区间,可单侧无界 | Min > Max 返回 NumberRange.Invalid;包含两个端点 |
NumberRange<TNumber> | 数量、评分、百分比等可比较值类型的闭区间 | Min > Max 返回 NumberRange.Invalid |
EnumFilterSet<TEnum> | 对输入去重、排序并冻结,只暴露 IReadOnlySet<TEnum> | 相等性忽略调用方顺序和重复项;null 按空集合处理 |
这些类型属于 HTTP/Application 的 TFields。它们表达请求语义,不直接成为生成式数据库字段。
4. QueryShape 的标量展开边界
QueryShape 生成器仍以标量为声明输入。*ListInput.From 或 MapFrom 是组合值对象的唯一展开点:
public sealed partial class ResourceListInput : IDeclareQueryShape{ // BQRY005 要求分页标量成对出现,且不能标为 QueryField。 public int PageIndex { get; init; } public int PageSize { get; init; }
[QueryField(DefaultOperator = FilterOperator.GreaterThanOrEqual)] public DateTimeOffset? CreatedAtFrom { get; init; }
[QueryField(DefaultOperator = FilterOperator.LessThan)] public DateTimeOffset? CreatedAtTo { get; init; }
public static ResourceListInput From( ResourceListFields fields, PaginationParams paging) { var normalized = paging.Normalize(); return new ResourceListInput { // 日期范围只在此处展开;上界统一转换成 LessThan 所需的排他值。 CreatedAtFrom = fields.CreatedAt?.From, CreatedAtTo = fields.CreatedAt?.QueryToExclusive ?? fields.CreatedAt?.To?.AddTicks(1), PageIndex = normalized.PageIndex, PageSize = normalized.PageSize, }; }}生成器规则 BQRY005 要求 PageIndex 与 PageSize 成对出现,类型只能是 int 或 int?,且两者都不能带 [QueryField]。DateRange、PaginationParams 以及其他组合筛选值对象也不能直接标为 [QueryField];生成器无法替模块猜测字段、运算符或日期上界语义。
主路径如下:
5. 远程选项与关联回填
QueryOptionItem
所有远程选择器返回同一展示形状:
new QueryOptionItem( Value: "user-42", Label: "张三", // Selected 只恢复界面状态,不授予访问权限。 Description: "主办律师", Selected: true, // Mapping 只能携带服务端登记并经过字段安全处理的展示值。 Mapping: new Dictionary<string, string?> { ["department"] = "诉讼部", });Value是提交给筛选字段的稳定标识;Selected只用于恢复或最近使用状态,不是授权结果;Mapping只能包含选择器目录登记、经过字段安全处理的白名单字段;QueryOptionRequest.MaximumPageSize固定为50,批量解析标识也最多保留 50 个。
RelatedEntitySnapshot
详情或编辑回填模型可以携带 RelatedEntitySnapshot(Relation, Id, Label, ResourceType, Mapping),以免客户端为了关联名称再次请求详情。它不是聚合关系,也不能携带对方聚合、授权事实或未脱敏字段。列表行默认不携带该快照;需要显示关联信息时,应由 QueryShape 投影或选项 Mapping 提供最小字段。
6. 导出范围
导出不能靠把在线 PageSize 调大实现。ExportScope 的数值已经进入持久化请求快照,不得重排:
| 名称 | 数值 | 所需范围参数 |
|---|---|---|
CurrentPage | 0 | 复用当前列表筛选、排序与页大小 |
Selected | 1 | 非空、无重复的 CheckedIds;最多 1000 个 |
All | 2 | 不携带 CheckedIds、PageFrom/PageTo 或 Take |
PageRange | 3 | PageFrom > 0、PageTo > 0 且 PageFrom <= PageTo |
Quantity | 4 | Take > 0;最大值由导出策略治理,不复用 PagingLimits |
ExportRange 用同一组字段表达范围;持久化的 ExportRequest 目前把 Scope、CheckedIds、PageFrom、PageTo 和 Take 作为扁平属性保存。非对应范围夹带参数会返回 ApplicationErrors.Export.InvalidRequest。
// TenantId 与 RequestedBy 必须来自可信执行上下文,而不是未经验证的页面字段。var request = new ExportRequest( ExportBuilderKey: "billing-invoices", Format: ExportFormat.Csv, TenantId: tenantId, RequestedBy: userId, Parameters: serializedListFields){ // PageRange 必须同时提供两个正页码;反向范围由请求校验器拒绝。 Scope = ExportScope.PageRange, PageFrom = 2, PageTo = 4,};IExportBuilder 的旧接口只接收 Parameters,因此只允许 All。支持 CurrentPage、Selected、PageRange 或 Quantity 的构建器必须实现 IScopedExportBuilder,从完整且已验证的 ExportRequest 估算行数并分批读取。否则调度器拒绝请求,不能静默扩大为全量导出。
公开的 ExportRequestDisplaySnapshot 会保留 PageFrom、PageTo 和 Take,但不保留选中行标识、租户、用户或原始参数。业务构建器只能通过服务端白名单字段补充可展示条件。
7. 已登记的稳定例外
| 表面 | 例外 | 边界 |
|---|---|---|
| Chat 消息 | CursorPage<T> 游标分页 | 保持游标,不迁为 PaginationParams |
| 远程选择器 | 最大 50 条 | 选择器协议边界,不复制到普通列表 |
| Workflow QueryStore | 最大 200 条 | 引擎内部护栏,不是 HTTP 列表上限 |
| SCIM 列表 | 最大 200 条 | 遵循 SCIM 分页协议 |
| 有界字典 | 可一次读取小枚举、事件类型或小目录 | 超过 200 条或需要交互筛选时迁为标准分页 |
| Operations 租户列表 | 可跨租户读取 | 仅可信运维授权面;请求不接受环境 TenantId |
| 单租户详情 | GetTenantQuery(string TenantId) | 路由资源标识的单资源 GET,不是列表字段 |
新增例外必须记录 owner、边界和删除条件,不能用一次性迁移进度代替稳定契约。
8. 迁移与验证
维护现有列表时按以下顺序处理:
- 先导出当前 OpenAPI,确认线上请求仍是扁平形状还是已经使用
Fields/Paging/Sort。 - 把业务筛选收进
{Resource}ListFields;时间范围使用业务字段名,不声明顶层Period。 - 用
PaginationParams和IPaginatedRequest替换 HTTP 层平铺分页。 - 在
*ListInput.From/MapFrom一次性展开值对象并规范化分页。 - 保持 QueryShape 输入为标量,日期上界使用
LessThan。 - 同步更新 OpenAPI artifact、Platform SDK 类型和页面调用。
- 运行 Domain 单元测试、模块 Query 测试、生成器测试和架构测试。
当前直接证据包括:
PaginationParamsTests:默认值、1000 上限和PageWindow转换;DateRangeTests:无界、反向失败、日界补齐、显式时刻和 JSON 校验;RangeValueObjectTests:金额/数值闭区间与枚举集合相等性;ExportRangeContractTests:范围参数校验、枚举数值和快照往返;ListQueryShapeConvergenceArchitectureTests:W6 启用的存量收敛扫描,目前仍为Skip。