审计写侧使用完整的存储中性 AuditLogEntry,读侧只返回 16 字段 AuditEnvelope。查询 API 不暴露请求正文、响应正文、实体 Diff 或异常详情;需要展示的资源字段还会经过模块贡献的 IAuditEnvelopeProjector,例如 Identity 用户字段继续受 Field Security 约束。
1. SqlSugar 六组分表
| 记录类型 | 表模板 | 实际 SplitType | 逻辑类别 |
|---|---|---|---|
SysAuditLogRecord | SysAuditLog_{year}{month}{day} | Month | HTTP Activity |
SysActivityLogRecord | SysActivityLog_{year}{month}{day} | Year | 业务 Activity |
SysEntityPropertyChangesLogRecord | SysEntityPropertyChangesLogs_{year}{month}{day} | Year | EntityChange |
SysExternalRequestLogRecord | SysExternalRequestLogRecord_{year}{month}{day} | Month | ExternalRequest |
SysCommunicationLogRecord | SysCommunicationLog_{year}{month}{day} | Month | CapConsumer / BackgroundJob |
SysSpecialLogRecord | SysSpecialLog_{year}{month}{day} | Year | Security / Exception |
SqlSugarAuditTableInitializer 在 --init-schema 中创建当前时间桶;任何初始化异常都会向上传播,使命令失败。未来时间桶由 SqlSugar 分表写入路径按事件时间创建。
两个配置边界仍需留意:AutoCreateTables=false 当前没有被 SchemaInitializationCommand 检查;AuditShardingSchedule 会做合法性校验,但 GetShardingType 没有参与实体映射,物理粒度仍由上表的 [SplitTable] 决定。
2. SqlSugar 写入与类别区分
Store 先按类别分组,再在一个数据库事务中写六组表:
- Activity 以
HttpMethod且Module=Http区分请求与业务活动; - CapConsumer/BackgroundJob 共表,
RunPars写入稳定类别标记; - Security/Exception 共表,
Level写入Security或Exception; - Exception 的可公开错误摘要写入 Message,不会把任意堆栈直接投影给列表 API;
- 每组插入前按目标分表和稳定 ID 查重,同 ID 证据不同则失败。
这意味着“六张物理表族”不等于“六个逻辑类别”。查询与保留必须继续使用类别标记,不能只按表名判断 CAP、Job、Security 或 Exception。
3. Mongo 七集合与索引
Mongo 将七个 Category 分别存入:
audit_activity、audit_entity_change、audit_security、audit_exception、audit_external_request、audit_cap_consumer、audit_background_job。
读、写、保留首次访问前共享 MongoAuditIndexInitializer:
- 删除旧版
ttl_occurred_at全局 TTL 索引; - 为每个集合创建统一查询索引;
- 初始化取消或失败会把状态恢复为未完成,下次访问可重试;
- 旧
AuditTtlEnabled/AuditTtlDays只产生迁移 Warning,不再创建类别无关 TTL; - 连接或数据库配置不完整时失败关闭。
Audit: # 审计读、写、保留和耐久实体变更 Store 一并切换。 StoreProvider: MongoMongo: # 生产环境应从 Secret Provider 注入,不要把凭据提交进配置文件。 ConnectionString: "secret-ref:mongodb-audit" Database: "bitzorcas"Mongo 以 _id=AuditId 做 $setOnInsert,随后回读并比较完整文档;已有同 ID、不同内容不会被覆盖。
4. Dapper 读侧
Dapper 只替换 IAuditQueryPort,不承担通用写入或保留。它按共享表定义发现 SQL Server 分表,并有以下边界:
- 每个表前缀最多发现 512 张物理表;
- Tenant、时间、User、Module、TraceId、类别和游标条件尽可能下推到 SQL;
- 每表只读取当前
AuditPageWindow.FetchCount所需候选,不再把所有匹配行装入内存; - 每表结果和每类别结果都用同一稳定归并器;
- 表名来自受控前缀与系统目录,不接受客户端提供物理表名。
它仍使用 SQL Server 的系统目录与方言。选择 MySQL/SQLite 时不要把 Dapper 审计读侧当作跨数据库替代品。
5. AuditEnvelope 与投影
公开摘要字段为:AuditId、OccurredAt、Category、TenantId、CallerType、UserId、ClientId、CorrelationId、TraceId、Module、ResourceType、ResourceId、Action、Result、DurationMs 与 SensitiveFieldsMasked。
Store Mapper 会限制可能含正文的旧列,查询 Handler 随后串行应用所有匹配的 IAuditEnvelopeProjector。任一投影失败,整个查询返回失败,避免在字段安全无法判定时放行原值。
6. 查询与导出入口
| 方法与路由 | 用途 | 关键约束 |
|---|---|---|
GET /api/audit | 租户审计列表 | auditing.audit.view、userPolicy、HeavyRead |
GET /api/operations/audit | 运维兼容路由 | 同一 QueryAuditCommand |
POST /api/audit/export | CSV/JSON 异步导出 | 同一查看权限、租户所有权、幂等键 |
列表支持 Category、UserId、Module、TraceId、From、To、PageIndex、PageSize。Handler 将页码下限设为 1;PageSize 非法或大于 200 时回退为 20。公共列表不接受 TenantId,租户只能来自可信 CurrentUser,也不提供 Host 跨租户绕过。
导出复用同一查询端口与投影链,每批使用 OccurredAt + AuditId 游标读取;只允许 csv/json,要求非平台租户、认证用户和最长 128 字符的幂等键。下载授权、文件保留和 DLP 由通用 Export 子系统继续控制。
7. 全局分页合同
三个 Provider 都调用 AuditPageWindow 和 AuditCandidateWindow:
- 每个物理来源读取从全局开头算起的
skip + pageSize个候选; - 按
OccurredAt desc, AuditId desc线性归并,并持续裁剪到候选上限; - 所有来源合并后才做一次全局
Skip/Take; - 总数独立累加。
这修复了旧实现“每个类别先 skip 再合并”导致的第二页错序。Offset 模式最多物化 10,000 个候选;超出窗口时返回空 Items 但仍给出 TotalCount。连续导出或深翻页应使用内部游标合同:
OccurredAt < cursorTime OR (OccurredAt = cursorTime AND AuditId < cursorId)。
游标模式每来源只取 PageSize,内存保持有界,也不会因翻页期间插入新记录而重复跨过同一边界。
8. Provider 过滤差异
| 条件 | SqlSugar | Mongo | Dapper |
|---|---|---|---|
| Tenant | 所有表强制 | 所有集合强制 | 所有表强制 |
| Category | 七类均可达 | 七个独立集合 | 七类均可达 |
| User | 按各表对应列 | user_id | 按表列映射 |
| Module | 按各表语义列 | module | 按表列映射 |
| Time | 全部 | 全部 | 全部 |
| TraceId | 当前未应用 | trace_id 或 correlation_id | 按表 Trace/Correlation 列 |
| Security/Exception | 按 Level 分离 | 独立集合 | 按 Level 分离 |
| CAP/Job | 按 RunPars 分离 | 独立集合 | 按 RunPars 分离 |
| Cursor | 支持 | 支持 | 支持 |
GET /api/audit?traceId=... 在 SqlSugar 默认组合下目前不会缩小结果,这是已确认的源码缺口。修复前,调用方不能把该参数当作可靠调查过滤器;可先用时间、类别和模块缩小范围,再核对 CorrelationId。
9. 租户与模拟边界
普通列表、导出和保留策略读取认证主体的所有者 TenantId。即使用户正在 operate-as 另一个租户,这些管理面也不会自动切到 Effective Tenant。这一选择可以防止模拟会话借普通查看权限读取目标租户全部审计历史。
真正的 Host 跨租户调查应有独立命令、Purpose、审批、结果上限和自身审计,不能通过传 TenantId、清除 ORM Filter 或使用平台租户 0 偷渡实现。
10. 查询验收合同
调查请求应先用权威支持的时间、类别和模块过滤缩小候选,再通过响应中的 Cursor 继续翻页:
# traceId 在默认 SqlSugar 组合修复前不作为唯一过滤条件。curl -fsS --get -H "Authorization: Bearer <TOKEN>" \ --data-urlencode "category=Security" --data-urlencode "module=Authorization" \ --data-urlencode "from=2026-08-10T00:00:00Z" --data-urlencode "pageSize=100" \ https://<HOST>/api/audit- 三 Provider 对相同 Fixture 返回相同类别、结果和稳定顺序;
- Activity 双表、七类别、第 1/2/末页以及同时间戳 AuditId 排序;
- offset 10,000 边界、超界空页与游标连续读取;
- Security/Exception、CAP/Job 严格分离;
- Tenant、User、Module、From/To、TraceId 的逐 Provider 合同测试;
- From>To、非法租户/标识/时间/游标、取消与 Timeout;
- Identity Field Security 投影失败时 fail-closed;
- 导出重试、幂等、DLP、文件过期和下载授权;
- Mongo 索引初始化失败后可重试,Dapper 物理表上限生效。