Reporting 对外提供两条分页查询和一条异步导出提交端点。查询统一采用 RFC 10008 QUERY 主入口,并为不支持 QUERY 的客户端生成 POST .../_query 降级入口;不存在 GET 别名。三条入口都从认证上下文取得当前租户,不接受客户端 TenantId。
1. 端点总表
| 能力 | 主入口 | 兼容入口 | 权限 |
|---|---|---|---|
| 工单汇总 | QUERY /api/reports/tickets/summary | POST /api/reports/tickets/summary/_query | reporting.ticket-summary.read |
| 审计活动日报 | QUERY /api/reports/activities/daily | POST /api/reports/activities/daily/_query | reporting.activity.read |
| 工单汇总导出 | POST /api/reports/tickets/summary/export | 无 | reporting.ticket-summary.export |
QUERY 与 POST 降级入口接收同一 JSON 正文。OpenAPI 3.1 只发布 POST 降级操作,并用 x-http-query-method、x-http-query-path 等扩展声明首选 QUERY;生成 SDK 应读取这些扩展,而不是把操作降格为普通 POST 语义。
2. 工单汇总查询
QUERY /api/reports/tickets/summary HTTP/1.1Authorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/jsonAccept: application/json
{ "status": "Closed", "from": "2026-07-01T00:00:00+08:00", "to": "2026-07-31T23:59:59+08:00", "pageIndex": 1, "pageSize": 20}| 字段 | 类型/默认值 | 当前处理 |
|---|---|---|
status | string? | null/空白不筛选;其他值 trim 后精确比较 StatusName;最长 64,禁止控制字符;不校验枚举 |
from | DateTimeOffset? | OpenedAt >= from,闭区间下界 |
to | DateTimeOffset? | OpenedAt <= to,闭区间上界 |
pageIndex | int = 1 | 必须大于等于 1 |
pageSize | int = 20 | 必须为 1–1000 |
From 晚于 To 或跨度超过 366 天时返回 Reporting.ReportingQuery.InvalidDateRange。时区偏移由 DateTimeOffset 保留;规则不会主动把边界改写成 UTC。若按自然月查询,应明确构造最后一个有效时刻或由业务层转换边界,不要误把次月零点当成排他上界。
{ "items": [ { "ticketId": "019c48cb7ba47000953d5af574ddc531", "requesterId": "100", "assigneeId": "200", "subject": "无法登录生产租户", "priorityName": "High", "statusName": "Closed", "openedAt": "2026-07-03T08:30:00Z", "closedAt": "2026-07-03T10:15:00Z" } ], "totalCount": 1, "pageIndex": 1, "pageSize": 20}公开行不含 Mart 的 LastEventId 和 LastUpdatedAt,不能作为投影 watermark 使用。
3. 审计活动日报查询
POST /api/reports/activities/daily/_query HTTP/1.1Authorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/json
{ "from": "2026-07-01T00:00:00Z", "to": "2026-07-31T00:00:00Z", "pageIndex": 1, "pageSize": 50}from 和 to 是必填 DateTime,同样使用闭区间,要求顺序正确且跨度不超过 366 天。分页范围与工单查询一致。响应也是完整 PagedResult:
{ "items": [ { "activityDate": "2026-07-15T00:00:00", "userId": "42", "actionType": "ticket.view", "activityCount": 17 } ], "totalCount": 1, "pageIndex": 1, "pageSize": 50}协议仍未规定 DateTime Kind、租户时区或 date-only 归一方式;生产 writer 也尚不存在。详见审计活动日报与聚合任务。
4. 输入错误目录
| 条件 | 稳定错误码 |
|---|---|
| 页码小于 1,或页大小不在 1–1000 | Reporting.ReportingQuery.InvalidPage |
| status 超过 64 字符或含控制字符 | Reporting.ReportingQuery.InvalidStatus |
| 日期逆序或跨度超过 366 天 | Reporting.ReportingQuery.InvalidDateRange |
| 非真实/委托用户、UserId 非正数或租户非法 | Reporting.User.Required |
| 授权不是 Tenant DataScope | Reporting.DataScope.TenantRequired |
| 导出格式不是 csv/excel | Reporting.Export.InvalidFormat |
| 幂等键不满足 8–128 个无控制字符 | Reporting.Export.InvalidIdempotencyKey |
5. 精确权限已对齐 Read
两个 Query 都实现 IAuthorizedRequest 并声明 AuthorizationAction.Read:
public ResourceDescriptor Resource { get; } = new(ReportingPermissions.Module, ReportingPermissions.TicketResource);
public AuthorizationAction Action { get; } = AuthorizationAction.Read;// => reporting.ticket-summary.read治理目录和授权种子包含:
reporting.ticket-summary.read;reporting.ticket-summary.export;reporting.activity.read;reporting.export-job.read。
旧版文档所述 .view/.read 漂移已经修复。不要为兼容旧文档重新授予 .view;应以当前生成权限和种子为准。
6. Feature 运行时事实
FeaturePolicyEvaluator.ModuleFeatureMap 包含:
["reporting"] = "platform.reporting"platform.reporting 在中央 Feature seed 中默认 Disabled,并标记为商业能力;租户 override 可显式启用。禁用时 evaluator 返回 Deny,不是 Neutral。
Reporting 模块本身还通过 [FeatureDefinition] 贡献 reporting.mart,默认也是关闭,但授权管道不会根据资源模块 reporting 查询它。当前存在两个表达同一产品能力的代码:
| 代码 | 当前用途 |
|---|---|
platform.reporting | 运行时授权 Feature gate、许可证示例和中央 seed |
reporting.mart | Reporting owner 治理 catalog 定义 |
在两者合并或建立显式别名前,运行配置必须启用 platform.reporting;只启用 reporting.mart 不会打开查询端点。
7. 当前用户与 Tenant DataScope
Endpoint 授权通过后,Handler 仍执行以下约束:
// 资源 TenantId 必须由认证上下文覆盖,不能信任请求体。var decision = await authorization.EvaluateAsync( user, resource with { TenantId = user.TenantId }, action, cancellationToken);
// Reporting 只接受精确的租户级范围;User/Department/All 都不能替代。if (!decision.IsAllowed || decision.DataScope != DataScope.Tenant) return ReportingErrors.TenantDataScopeRequired;当前用户必须满足:
IsAuthenticated=true;- CallerType 为
User或Delegated; - UserId 存在且大于 0;
- TenantId 通过
TenancyDefaults.IsValid。
Store 的每个 Count、Page、Find、Upsert 和导出批读谓词也包含 TenantId,唯一索引同样包含 TenantId。调用者不能在正文中选择别的租户。
Own、Department、Office 等数据范围不会自动收窄 Mart 查询,而是直接被拒绝。原因是当前汇总表没有足够维度安全表达这些 scope。若未来支持更细范围,应先把 owner 规则转成数据库谓词或安全候选集,不能先取全租户数据再内存过滤。
8. 导出安全契约
导出请求:
{ "status": "Closed", "from": "2026-07-01T00:00:00Z", "to": "2026-07-31T23:59:59Z", "format": "excel", "idempotencyKey": "reporting-closed-202607-v1"}安全边界不是“复用查询后下载”:
- 提交要求精确
.export权限和 Tenant DataScope; - Builder Key、列和文件基础名由服务端固定;
- status/from/to/format 进入幂等指纹;
- 任务只允许
ExportScope.All,不接受 CheckedIds; - 执行和重试恢复原请求租户与用户,并重新验证
.export; - 每条 Mart 行还通过 Tickets owner 的
EnsureSubjectCanViewAsync实时复核;AccessDenied/NotFound 行被跳过,基础设施错误会使任务失败; - 输出固定八列:ticketId、subject、statusName、priorityName、requesterId、assigneeId、openedAt、closedAt。
这使导出比交互式查询多一层逐工单 owner 授权。查询本身仍要求租户级 scope,并直接返回全租户 Mart 行。
9. 字段敏感性
工单行的 Subject 是自由文本,RequesterId/AssigneeId 是人员标识;审计行的 UserId、ActionType 和计数可形成行为画像。当前没有字段安全 contributor 或响应 masker。前端隐藏列不是安全控制。
上线前至少应明确:
- 哪些角色可读取逐工单 Subject 和人员 ID;
- 哪些角色只能看聚合计数;
- 导出是否需要 watermark、水印、下载过期和审计;
- 审计日报何时聚合到组织层级或对 UserId 做假名化;
- 删除、保留和合法保留如何同步到派生 Mart。
10. 契约测试清单
- QUERY 与 POST 降级使用相同正文和响应;GET 返回 405/未映射。
- OpenAPI 扩展正确声明 QUERY 主路径与降级路径。
.read、.export与缺失权限分别得到固定结果。platform.reporting启用/禁用和 unavailable adapter 均失败方式明确。- User、Delegated、System、匿名、无效 UserId/TenantId 的边界。
- Tenant、Own、Department、Office DataScope 的允许/拒绝矩阵。
- null/空白/64 字符/控制字符 status。
- 日期闭区间、偏移、Kind、逆序和 366/367 天边界。
- pageIndex 和 pageSize 的 1、20、1000、1001 边界。
- 导出格式、幂等键、指纹、重试复核和中途撤权。
- Subject/UserId 的字段策略与导出审计。
- 稳定 Problem Details 错误码和描述一致性。
11. 审查命令
# 路由、动作和请求规则。rg -n "HttpRoute\.(Query|Post)|AuthorizationAction\.(Read|Export)|Validate(Page|DateRange|Idempotency)" \ src/Platform/Reporting -g '*.cs'
# 运行时 Feature 与 owner catalog 代码要同时审查。rg -n "platform\.reporting|reporting\.mart|\[\"reporting\"\]" src tests -g '*.cs'
# Tenant DataScope 与逐工单导出复核。rg -n "RequireTenantDataScopeAsync|DataScope\.Tenant|EnsureSubjectCanViewAsync" \ src/Platform/Reporting src/Platform/Tickets -g '*.cs'