Skip to content
bitzorcas
中EN

Guide

Reporting 审计活动日报与聚合任务

说明 Rpt_AuditActivityDaily 的物理粒度、覆盖式 Upsert、QUERY 契约、缺失的生产者,以及聚合、checkpoint、回补与隐私要求。

Last updated

Rpt_AuditActivityDaily 设计为按 (TenantId, ActivityDate, UserId, ActionType) 汇总的宽表,但当前只有模型、store 和查询端点,没有任何生产任务。文档必须把“数据结构可写”与“审计活动已经自动聚合”分开。

1. 当前物理模型

字段数据库约束业务含义
TenantIdBizEntityBase + tenant table聚合所属租户
ActivityDaterequired DateTime注释称 UTC 活动日期,但未强制 date-only
UserIdrequired,长度 36系统活动也要求稳定用户标识
ActionTyperequired,长度 50动作维度
ActivityCountrequired int当前维度的完整计数快照
LastUpdatedAtrequired DateTimeOffset聚合写入时间

唯一索引包含四个维度;另有 ActivityDate 索引。表不是软删除,模块没有 retention/delete/archive API。

2. Upsert 是覆盖,不是累加

当前审计日报 Upsert 语义
// 同一四维键命中后,store 直接用调用方给出的完整快照覆盖。
var existing = await rows.FirstOrDefaultAsync(
x => x.TenantId == input.TenantId
&& x.ActivityDate == input.ActivityDate
&& x.UserId == input.UserId
&& x.ActionType == input.ActionType,
cancellationToken);
if (existing is null)
{
await rows.AddAsync(ToPersistenceRecord(input), cancellationToken);
return Result.Success();
}
// 这里不是 ActivityCount += delta;重放 delta 会丢计数或重复计数。
existing.ActivityCount = input.ActivityCount;
existing.LastUpdatedAt = input.LastUpdatedAt;
await rows.UpdateAsync(existing, cancellationToken);
return Result.Success();

因此未来 writer 必须提交“截至 checkpoint 的完整 bucket count”。若要消费逐条审计事件做增量,应在数据库里用事件 inbox + 原子 increment,并处理重复;不能把 delta 填进现有覆盖接口。

3. 当前没有生产者

全仓库 UpsertAuditActivityDailyAsync 的引用只有:

  • IReportingMartStore 声明;
  • ReportingMartStore 实现。

没有 Auditing consumer、CAP subscription、Quartz/JobHost executor、SQL group-by service、后台计划、管理回补命令或 seed。ReportingQueryHandlerTests 用 substitute store 返回一条手工记录,只验证 Handler 映射。

4. 当前查询契约

审计日报通过 QUERY /api/reports/activities/daily 查询;不支持 QUERY 的客户端使用 POST /api/reports/activities/daily/_query,没有 GET 路由。请求体包含必填 From/To 和默认 PageIndex=1、PageSize=20。

Handler 会在访问 Store 前执行以下检查:

  • From 不得晚于 To,时间跨度不得超过 366 天;
  • PageIndex 必须 ≥1,PageSize 必须在 1..1000;
  • 调用方必须是带有效 TenantId 和正 UserId 的 User/Delegated 身份;
  • 必须拥有 reporting.activity.read,且最终 DataScope 必须精确为 Tenant。

返回值是完整 PagedResult<AuditActivityDailyRow>。Store 使用 inclusive 的 ActivityDate 范围,默认排序为 (ActivityDate desc, UserId desc, ActionType desc);在固定 TenantId 下,该排序覆盖唯一键剩余维度。公开 DTO 只有 ActivityDate、UserId、ActionType、ActivityCount,不包含 LastUpdatedAt 或聚合 watermark。

5. 日期与时区不变量

注释说 UTC 按日聚合,但类型是 DateTime,Upsert 不归一化:

  • 2026-07-15 00:00Z 与 2026-07-15 08:00Z 是两个唯一键;
  • Kind=Local/Unspecified 的 provider 行为可能不同;
  • 若产品要按租户本地日,UTC midnight 又不满足业务语义;
  • DST 会产生 23/25 小时本地日。

必须先选择:UTC calendar day,还是 tenant timezone day。建议存 ActivityDate 为 DateOnly 业务键,并保存 TimeZoneId/bucket start/end UTC 或聚合策略版本,避免租户时区变更后历史被重解释。

按租户时区生成稳定日桶
// 先把事件时刻映射到租户配置的 IANA 时区,再得到业务 DateOnly。
var zone = DateTimeZoneProviders.Tzdb[tenant.TimeZoneId];
var instant = Instant.FromDateTimeOffset(audit.OccurredAt);
var activityDate = instant.InZone(zone).Date;
// 桶边界保留 UTC,可用于增量扫描和 DST 正确性。
var start = activityDate.AtStartOfDayInZone(zone).ToInstant();
var end = activityDate.PlusDays(1).AtStartOfDayInZone(zone).ToInstant();
// 完整快照与 checkpoint 一起提交;不要用处理时间决定归属日。
await aggregator.UpsertSnapshotAsync(
tenant.Id, activityDate, audit.UserId, audit.ActionType,
count, sourceCheckpoint, start, end, cancellationToken);

当前表没有 TimeZoneId、bucket boundaries 或 policy version,若选择本地日需要 schema 演进。

6. ActionType 契约

ActionType 长度 50,只是自由字符串。没有 catalog、大小写/空白规范、版本或未知动作策略。Auditing 的不同 sink 可能用 activity name、permission、HTTP route 或事件类型,若直接混入会形成不可聚合的高基数维度。

应定义稳定动作 taxonomy,例如 tickets.ticket.view、identity.login.failed,并把 resource/operation/result 拆维度。不要把 URL、TicketId、错误消息、用户输入拼进 ActionType。

7. UserId 与系统活动

UserId required 且最长 36。审计源可能包含:

  • 数字用户 ID;
  • API Client;
  • BackgroundJob/System;
  • 未认证失败登录;
  • impersonator 与 effective actor。

把所有类型塞进 UserId 会丢 caller type。建议 ActorType + ActorId,并单独保留 EffectiveUser/Impersonator 聚合策略。未知/系统活动使用稳定保留值,不要空字符串。

8. 推荐的批量增量聚合

Audit source
immutable id + occurredAt

per shard checkpoint

bounded source window

tenant/date/actor/action group

absolute counts + source high-watermark

shadow/current daily Mart

source vs Mart control totals

每批读取 (lastCheckpoint, newHighWatermark],但覆盖式快照需要知道该 bucket 的完整累计值。可维护专用 accumulator,或按可控日期窗口从 immutable audit source 重算。Late event 应重开对应日桶;超过 lateness window 的事件进入 correction queue。

9. Checkpoint 与幂等

同一事务写:

  1. Mart snapshot;
  2. source high-watermark;
  3. aggregation run id/status/control totals。

若 source 是分区/分片,checkpoint 必须按 partition。重试同 run 产生相同 absolute counts。不要只保存 LastUpdatedAt:它是处理时间,不证明读到了哪个 source offset。

10. 回补与策略变更

Action taxonomy、时区、过滤规则或隐私策略改变时,要以 ProjectionVersion 重建 shadow 表。步骤:冻结定义 → 记录 source range → 分批 rebuild → control-total 校对 → 追平增量 → 原子切换 → 保留回滚窗口。

当前没有 rebuild API。直接 DELETE+重算会让查询看到部分数据,且无法区分空数据与重建中。

11. 隐私与保留

即使是计数,UserId+ActionType+日期也可形成行为画像。需要:

  • 最小访问范围和独立审计报表权限;
  • 小样本 suppression/k-anonymity(对跨人群分析);
  • UserId pseudonymization 或组织级聚合;
  • 与原始审计不同的 Mart retention;
  • GDPR erasure/合法保留冲突策略;
  • 导出 watermark 与下载过期。

当前表 IsSoftDelete=false 且没有 data lifecycle policy。不能假设审计源清理会自动清理 Mart 派生数据。

12. 必测聚合矩阵

  1. UTC 日、本地日、DST、时区修改;
  2. 同一事件重复、批次重跑、checkpoint 崩溃;
  3. late event、跨日边界、未来时间和错误 Kind;
  4. 多 partition checkpoint 与乱序;
  5. ActionType normalization/未知/高基数;
  6. User/API Client/System/anonymous/impersonation;
  7. int overflow、负数、空维度和列长;
  8. shadow rebuild 与 live catch-up;
  9. source/Mart control totals 与抽样明细;
  10. retention、erasure、访问和导出审计。

13. 审查命令

Terminal window
# 当前生产源码只有接口和 Store;出现第三个调用点后必须同步本章与契约测试。
rg -n "UpsertAuditActivityDailyAsync" src -g '*.cs'
# 暴露尚不存在的 scheduler/checkpoint/rebuild。
rg -n "Reporting.*(Job|Checkpoint|Backfill|Rebuild|Reconcile)|AuditActivity.*Consumer" \
src/Hosts src/Platform/Reporting -g '*.cs'
# 日期、用户、动作维度的物理约束必须与聚合器一致。
rg -n "ActivityDate|UserId|ActionType|ActivityCount|LastUpdatedAt" \
src/Platform/Reporting -g '*.cs'
# 查询路由、Read 权限、输入边界和三字段稳定排序。
rg -n "HttpRoute.Query|AuthorizationAction.Read|ValidateDateRange|ValidatePage|ReadModelSort" \
src/Platform/Reporting -g '*.cs'

返回 Reporting 总览 · Auditing 模块 · Mart 与分页

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%