# 新增需求 04：全局多视角审计方案

> 状态：已合并至标准评审稿，本文保留为实施明细。
> 评审主文档：`../../7月迭代技术方案-标准评审稿.md`
> 范围：全系统业务写操作、安全操作、外部集成和审计查询前端。

## 一、目标

审计系统必须能够从不同视角回答：

```text
谁做了什么？
某个资源经历了什么？
一次请求引发了哪些跨模块变化？
哪些操作失败、被拒绝或存在风险？
外部 Gateway、运营商和企业微信交互发生了什么？
资金变化对应哪个业务动作和审批结果？
```

不是把所有日志塞进一张表，而是建立统一审计事件，并通过 Query 组合访问日志、领域流水和外部集成记录。

## 二、当前代码问题

现有实现已经有账号审计和资产审计，但没有形成全系统能力：

- `tb_account_operation_log` 和 `tb_asset_operation_log` 字段、操作类型和查询入口不一致。
- 账号、资产审计通过 goroutine Best Effort 写入，进程退出或数据库短暂失败时会丢失。
- 部分调用方又在审计服务外增加一层 goroutine，形成双重异步。
- 退款审批等资金操作没有统一业务审计。
- 线下充值借用账号操作日志，资源类型和关联关系不准确。
- 访问日志只脱敏请求体，响应体仍按原文记录，可能泄露 Token、个人信息和敏感配置。
- 现有审计表只能从单个账号或单个资产查看，无法按请求、关联业务、异常等级和外部交互串联。

## 三、四类记录边界

| 类型 | 权威性 | 存储 | 用途 |
|---|---|---|---|
| Access Log | 非业务权威 | 文件/日志平台 | HTTP 调试、性能和请求追踪 |
| Audit Event | 业务审计权威 | PostgreSQL | 谁对什么做了什么、结果如何 |
| Domain Ledger | 领域事实权威 | 现有业务表 | 钱包流水、退款单、审批实例、订单状态 |
| Integration Log | 外部交互权威 | PostgreSQL | Gateway、运营商回调、企微 API、回调和同步尝试 |

规则：

- Audit Event 不替代钱包流水、退款记录和企微审批详情。
- Access Log 不作为业务审计依据。
- 数据同步的轮询、事件阶梯尝试、手动刷新和运营商回调统一进入 `tb_integration_log`，不再新建 `tb_card_sync_execution`。
- 高频轮询只有在状态变化、人工强制触发、连续失败或高风险异常时生成 Audit Event。
- Audit Query 可以把四类记录组合成时间线，但必须标明数据来源。

## 四、审计事件模型

### 4.1 主表

```sql
CREATE TABLE tb_audit_event (
    id                    BIGSERIAL PRIMARY KEY,
    event_id              VARCHAR(64) NOT NULL UNIQUE,
    occurred_at           TIMESTAMPTZ NOT NULL,
    category              VARCHAR(32) NOT NULL,
    action                VARCHAR(128) NOT NULL,
    action_name           VARCHAR(255) NOT NULL,

    actor_kind            VARCHAR(32) NOT NULL,
    actor_id              BIGINT,
    actor_name            VARCHAR(255) NOT NULL DEFAULT '',
    actor_user_type       INT,

    source                VARCHAR(64) NOT NULL,
    tenant_type           VARCHAR(32),
    tenant_id             BIGINT,
    shop_id               BIGINT,
    enterprise_id         BIGINT,

    result                VARCHAR(16) NOT NULL,
    risk_level            VARCHAR(16) NOT NULL DEFAULT 'normal',
    summary               TEXT NOT NULL,
    error_code            VARCHAR(64),
    error_message         TEXT,

    before_data           JSONB,
    after_data            JSONB,
    metadata              JSONB,

    request_id            VARCHAR(64),
    correlation_id        VARCHAR(64),
    parent_event_id       VARCHAR(64),

    ip_address            VARCHAR(64),
    user_agent            TEXT,
    request_path          VARCHAR(255),
    request_method        VARCHAR(16),

    content_hash          VARCHAR(64) NOT NULL,
    created_at            TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_audit_event_time
    ON tb_audit_event (occurred_at DESC, id DESC);
CREATE INDEX idx_audit_event_actor
    ON tb_audit_event (actor_kind, actor_id, occurred_at DESC);
CREATE INDEX idx_audit_event_action
    ON tb_audit_event (category, action, occurred_at DESC);
CREATE INDEX idx_audit_event_result
    ON tb_audit_event (risk_level, result, occurred_at DESC);
CREATE INDEX idx_audit_event_request
    ON tb_audit_event (request_id, occurred_at ASC);
CREATE INDEX idx_audit_event_correlation
    ON tb_audit_event (correlation_id, occurred_at ASC);
```

### 4.2 资源关系表

一次退款审批通过会同时影响退款单、订单、钱包、钱包流水、套餐和资产，只在主表保存一个 `resource_id` 无法完整查询。

```sql
CREATE TABLE tb_audit_event_resource (
    id                 BIGSERIAL PRIMARY KEY,
    audit_event_id     BIGINT NOT NULL,
    resource_type      VARCHAR(64) NOT NULL,
    resource_id        BIGINT,
    resource_key       VARCHAR(128) NOT NULL DEFAULT '',
    relation           VARCHAR(20) NOT NULL DEFAULT 'affected',
    created_at         TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX uq_audit_event_resource
    ON tb_audit_event_resource (
        audit_event_id,
        resource_type,
        COALESCE(resource_id, 0),
        resource_key,
        relation
    );

CREATE INDEX idx_audit_resource_timeline
    ON tb_audit_event_resource (resource_type, resource_id, audit_event_id DESC);
CREATE INDEX idx_audit_resource_key
    ON tb_audit_event_resource (resource_type, resource_key, audit_event_id DESC);
```

不建立数据库外键。`relation`：

| relation | 含义 |
|---|---|
| `primary` | 本次操作的主要对象 |
| `affected` | 被本次操作修改的对象 |
| `reference` | 只用于解释上下文的关联对象 |

### 4.3 操作者

`actor_kind`：

```text
admin_user
agent_user
enterprise_user
personal_customer
open_api_account
system_task
carrier_callback
wecom_callback
```

系统和外部回调允许 `actor_id=NULL`，但必须保存清晰的 `actor_name`，例如“移动实名回调”“企微审批状态同步”“套餐到期任务”。

### 4.4 来源

`source` 表示入口，不表示操作者：

```text
admin_api
personal_api
open_api
asynq
scheduled_job
gateway
carrier_callback.cmcc
carrier_callback.cucc
carrier_callback.ctcc
wecom_callback
wecom_polling
data_migration
```

### 4.5 结果与风险

`result`：`success / failed / denied / partial`。

`risk_level`：`normal / warning / high / critical`。

默认风险示例：

| 操作 | 风险等级 |
|---|---|
| 普通字段修改成功 | normal |
| 批量部分失败 | warning |
| 权限拒绝、重复回调冲突 | warning |
| 钱包余额、退款、角色权限、支付配置修改 | high |
| 企微通过后撤销但资金已执行、审计写入失败 | critical |

## 五、领域模型和代码结构

```text
internal/
├── domain/audit/
│   ├── event.go                不可变 AuditEvent 实体及不变量
│   ├── actor.go                操作者值对象
│   ├── resource.go             多资源关联
│   ├── sanitizer.go            审计数据脱敏规则
│   ├── action_registry.go      操作编码、名称、分类和默认风险
│   └── repository.go
├── application/audit/
│   ├── append_event.go         成功/失败审计写入
│   ├── append_with_tx.go       关键业务事务内写入
│   └── append_system_event.go
├── query/audit/
│   ├── event_list.go
│   ├── actor_view.go
│   ├── resource_view.go
│   ├── request_view.go
│   ├── correlation_view.go
│   ├── risk_view.go
│   ├── integration_view.go
│   └── finance_view.go
└── infrastructure/persistence/
    └── audit_repository.go
```

Audit Event 具有独立 `event_id`，因此是不可变领域实体，不是值对象；Actor、Resource 和字段差异是值对象。事件创建后不提供 Update/Delete Repository 方法。

## 六、操作注册表

操作编码禁止在各 Service 中自由拼字符串：

```go
type ActionDefinition struct {
    Code             string
    Name             string
    Category         string
    DefaultRiskLevel string
    SensitiveFields  []string
}

var ActionRegistry = map[string]ActionDefinition{
    "account.role.assign": {
        Name: "分配账号角色", Category: "security", DefaultRiskLevel: "high",
    },
    "refund.wecom.approved": {
        Name: "企微审批通过并确认人工退款", Category: "finance", DefaultRiskLevel: "high",
    },
    "recharge.wecom.approved": {
        Name: "企微审批通过并完成线下充值", Category: "finance", DefaultRiskLevel: "high",
    },
    "iot_card.realname.callback": {
        Name: "运营商回调更新实名状态", Category: "asset", DefaultRiskLevel: "normal",
    },
}
```

DTO `description`、前端中文名称和筛选项都从同一注册表生成，避免操作编码与展示名称不一致。

## 七、写入可靠性

### 7.1 关键成功事件

以下事件必须与业务变更同事务写入：

- 钱包余额变化。
- 人工退款结果确认和代理钱包回溯。
- 线下充值入账。
- 账号角色和权限变化。
- 支付、企微和系统关键配置变化。
- 卡实名、网络状态被人工修改。
- 手工绑定企微 `sp_no`。

Application 在事务内调用：

```go
auditWriter.AppendWithTx(ctx, tx, event)
```

审计写入失败时事务回滚，禁止业务成功但关键审计缺失。

### 7.2 异步系统事件

轮询、事件阶梯同步、运营商回调和企微轮询等外部交互统一进入 Integration Log。只有以下情况同时写 Audit Event：

- 状态发生业务变化。
- 连续失败达到阈值。
- 人工强制触发。
- 出现高风险异常。

系统事件通过同一业务事务或 Outbox 可靠写入，不启动裸 goroutine。

### 7.3 失败和拒绝事件

业务事务已经回滚时，使用独立短事务写 `failed/denied` 审计。审计写入失败不能把原业务错误改成成功，但必须记录 `critical` 应用日志和监控指标。

## 八、关联链路

### 8.1 request_id

表示一次 HTTP 请求，由现有 Request ID 中间件产生。一次请求触发的所有审计事件使用同一个 `request_id`。

### 8.2 correlation_id

表示跨请求、跨任务的完整业务链路：

```text
退款申请创建
→ 企微审批提交
→ 企微回调
→ 人工退款结果入账
→ 代理钱包退款流水（仅代理钱包支付订单）
→ 佣金回扣
→ 套餐失效和停机
```

以上事件共享退款申请创建时生成的 `correlation_id`。Asynq、Outbox、企微实例和同步任务载荷都必须传递该值。

### 8.3 parent_event_id

表示直接因果关系。例如“套餐失效”事件的父事件是“退款终态处理成功”，用于前端画出树状链路。

## 九、数据脱敏

### 9.1 永不进入审计的数据

```text
password
operation_password
access_token / refresh_token
agent_secret / app_secret
callback_token / encoding_aes_key
支付私钥、公钥原文
短信验证码
完整身份证号
对象存储签名 URL
企微 media_id
```

这些字段直接删除，不保存为 `******`，避免字段存在本身造成误用。

### 9.2 按权限展示的数据

手机、IP、ICCID、钱包余额和第三方交易号可以存储受控快照，但 Query 根据权限返回：

- 普通审计权限：脱敏展示。
- 敏感审计权限：展示完整值。
- 导出权限单独控制，不能因为可查看页面就默认可导出完整值。

### 9.3 大字段

`before_data`、`after_data` 和 `metadata` 分别限制 16KB。超过后保存：

```json
{
  "_truncated": true,
  "_original_bytes": 38210,
  "_summary": "批量修改500条资产",
  "_artifact_ref": "batch_task:123"
}
```

批量明细保存在对应任务结果表或对象存储，不塞入审计 JSON。

## 十、外部集成日志

企业微信、运营商回调、Gateway 请求和事件同步尝试使用通用 Integration Log。同步尝试即使在真正发送 HTTP 前因合并、限流或预期状态已满足而结束，也写一条结果记录，保证能够解释“为什么没有请求上游”。

```sql
CREATE TABLE tb_integration_log (
    id                 BIGSERIAL PRIMARY KEY,
    integration_id     VARCHAR(64) NOT NULL UNIQUE,
    provider           VARCHAR(32) NOT NULL,
    direction          VARCHAR(16) NOT NULL,
    operation          VARCHAR(64) NOT NULL,
    external_id        VARCHAR(128),

    resource_type      VARCHAR(64),
    resource_id        BIGINT,
    resource_key       VARCHAR(128),

    trigger_source     VARCHAR(32),
    trigger_scene      VARCHAR(128),
    trigger_series     VARCHAR(64),
    scheduled_at       TIMESTAMPTZ,
    started_at         TIMESTAMPTZ,
    attempt            INT NOT NULL DEFAULT 1,

    result             VARCHAR(20) NOT NULL,
    http_status        INT,
    provider_code      VARCHAR(64),
    provider_message   TEXT,
    request_summary    JSONB,
    response_summary   JSONB,
    duration_ms        BIGINT NOT NULL DEFAULT 0,
    state_changed      BOOLEAN NOT NULL DEFAULT FALSE,
    metadata           JSONB,

    request_id         VARCHAR(64),
    correlation_id     VARCHAR(64),
    audit_event_id     BIGINT,
    created_at         TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_integration_log_time
    ON tb_integration_log (created_at DESC, id DESC);
CREATE INDEX idx_integration_log_provider
    ON tb_integration_log (provider, operation, result, created_at DESC);
CREATE INDEX idx_integration_log_resource
    ON tb_integration_log (resource_type, resource_id, created_at DESC);
CREATE INDEX idx_integration_log_resource_key
    ON tb_integration_log (resource_type, resource_key, created_at DESC);
CREATE INDEX idx_integration_log_trigger
    ON tb_integration_log (trigger_series, attempt);
CREATE INDEX idx_integration_log_correlation
    ON tb_integration_log (correlation_id, created_at ASC);
```

`direction`：`inbound / outbound`。

`result` 至少支持：

```text
success
failed
not_found
invalid_payload
ignored
merged
rate_limited
completed
cancelled
```

数据同步字段约定：

- `provider=gateway`：实名、流量、卡状态和设备信息查询。
- `provider=cmcc/cucc/ctcc` 且 `direction=inbound`：运营商实名或解除实名回调。
- `trigger_source`：`polling/business_event/open_api/manual_refresh/carrier_callback`。
- `trigger_scene`：触发埋点场景，例如 `client_asset_info`、`card_resume`、`device_switch_card`。
- `trigger_series + attempt`：关联一次 `0/3/5` 阶梯同步。
- `result=merged/rate_limited/completed` 时允许没有 HTTP 状态和响应摘要，因为没有真正请求上游。
- `state_changed=true` 时通过 `audit_event_id` 关联对应业务状态变化事件。

普通高频轮询成功也写 Integration Log，但不生成 Audit Event。查询和清理必须使用时间索引及分批删除，不能在业务请求中同步聚合全量历史。

外部交互正文只保存脱敏摘要。企微加密报文、运营商原始回调和 Gateway 加密请求不进入普通审计详情。

## 十一、审计范围

### 11.1 必须审计的写操作

| 模块 | 操作 |
|---|---|
| 账号权限 | 创建、禁用、删除、角色分配、密码重置 |
| 资产 | 分配、回收、绑定、解绑、停复机、实名策略、手工实名、限速 |
| 套餐 | 创建、修改、上下架、分配、价格、已用量和到期时间调整 |
| 钱包资金 | 充值、扣款、退款、信用变化、人工调整 |
| 订单退款 | 创建退款、企微终态、人工退款确认、代理钱包回溯、资产后处理 |
| 线下充值 | 创建、企微终态、入账、通过后撤销异常 |
| 系统配置 | 支付配置、企微配置状态、模板版本发布、导出字段权限 |
| 数据同步 | 人工强制同步、回调更新业务状态、连续失败告警 |
| 导入导出 | 创建任务、下载敏感导出、批量任务结果 |
| 登录安全 | 登录成功/失败、Token 撤销、开放接口签名失败和重放拒绝 |

### 11.2 不进入业务审计的普通读操作

- 普通列表、详情和未读数查询只进入 Access Log。
- 下载敏感附件、导出、查看完整密钥状态、查看完整个人信息属于敏感读取，需要 Audit Event。

## 十二、多视角 API

### 12.1 全局事件视角

```http
GET /api/admin/audit/events
    ?category=finance
    &action=
    &result=success
    &risk_level=high
    &source=wecom_callback
    &start_at=
    &end_at=
    &keyword=
    &page=1&page_size=50
```

`keyword` 只匹配事件摘要、资源编号、操作者名称和 request_id，不对 JSONB 做无索引模糊扫描。

### 12.2 事件详情

```http
GET /api/admin/audit/events/{event_id}
```

返回：

- 操作者快照。
- 入口、租户和请求信息。
- 主要资源、影响资源和引用资源。
- 前后数据差异。
- 错误信息和风险等级。
- request/correlation/parent 关联。
- 可访问的领域流水和 Integration Log 链接。

### 12.3 操作者视角

```http
GET /api/admin/audit/actors
    ?actor_kind=admin_user
    &keyword=
    &range=30d
    &page=1&page_size=20

GET /api/admin/audit/actors/{actor_kind}/{actor_id}/summary?range=30d
GET /api/admin/audit/actors/{actor_kind}/{actor_id}/events?page=1&page_size=50
```

人员列表返回操作者 ID、名称、类型、最近操作时间、操作量、失败量和最高风险等级；`keyword` 只做 ID、账号和名称的前缀匹配，不扫描审计 JSON。Summary 返回操作量、失败量、拒绝量、高风险量、常用操作、常操作资源和最近登录 IP。

### 12.4 资源视角

```http
GET /api/admin/audit/resources/search
    ?resource_type=
    &keyword=RF202607150001
    &page=1&page_size=20

GET /api/admin/audit/resources/{resource_type}/{resource_id}/timeline
    ?include_related=true
    &page=1&page_size=50
```

示例资源：`iot_card / device / refund / order / agent_wallet / wecom_approval / account / system_config`。

资源搜索属于 Query 模块，允许直接查询退款、订单、充值、账号和资产等读模型，不要求加载领域聚合。返回候选项的 `resource_type/resource_id/resource_key/display_name`，解决同一关键词命中多个资源的问题。静态 `resources/search` 路由必须先于动态资源路由注册。

`include_related=true` 时通过资源关系表返回影响该资源的跨模块事件，但不无限递归加载关联资源。

### 12.5 请求视角

```http
GET /api/admin/audit/requests/{request_id}/timeline
```

按时间展示一次 HTTP 请求产生的 Access Log 摘要、Audit Event、Outbox 和 Integration Log。

### 12.6 业务链路视角

```http
GET /api/admin/audit/correlations/{correlation_id}/timeline
```

用于查看退款、企微审批、钱包和资产处理等跨请求链路。返回节点包含 `parent_event_id`，前端可展示因果树。

### 12.7 风险视角

```http
GET /api/admin/audit/risks/overview?range=24h
GET /api/admin/audit/risks/events?risk_level=critical&page=1&page_size=50
```

Overview：

- 高风险和严重事件数量。
- 权限拒绝、开放接口重放、资金冲突和外部回调异常趋势。
- 按操作人、来源和资源类型聚合。

### 12.8 外部集成视角

```http
GET /api/admin/audit/integrations
    ?provider=wecom
    &direction=inbound
    &operation=getapprovaldetail
    &result=failed
    &external_id=
    &resource_type=
    &resource_key=
    &trigger_source=
    &trigger_scene=
    &trigger_series=
    &page=1&page_size=50

GET /api/admin/audit/integrations/{integration_log_id}
```

列表查询 `tb_integration_log`，并返回关联审计事件和业务链路 ID；详情返回脱敏后的请求摘要、响应摘要、耗时、错误、尝试次数、计划时间、触发场景和关联资源，不返回密钥、完整回调正文或附件内容。

当 `trigger_series` 有值时，详情同时返回该序列的全部尝试，前端按“立即、3 分钟、5 分钟”展示，不要求用户逐条搜索。同步记录不再通过独立 `/card-sync/executions` 接口查询。

### 12.9 资金视角

```http
GET /api/admin/audit/finance/timeline
    ?business_no=
    &wallet_id=
    &transaction_no=
    &page=1&page_size=50
```

Finance Query 组合：

- Audit Event。
- 代理钱包和资产钱包流水。
- 退款单、充值单和订单。
- 企微审批实例。

每条记录明确 `record_source`，钱包流水仍是金额变化的权威数据。

### 12.10 导出

```http
POST /api/admin/audit/exports
```

复用现有导出任务系统。导出字段按角色配置，默认不允许导出完整 IP、手机号、ICCID、前后 JSON 和外部响应摘要。

## 十三、权限

建议权限码：

```text
audit:global:view
audit:actor:view
audit:resource:view
audit:request:view
audit:risk:view
audit:integration:view
audit:finance:view
audit:sensitive:view
audit:export
```

第一版审计中心只开放给超级管理员和具有对应权限的平台角色。

代理、企业账号不进入全局审计中心；它们在资产、订单、充值等业务详情页只能查看自己有权限资源的资源时间线，且返回字段经过脱敏。

Service/Query 必须重新应用资源权限，前端隐藏 Tab 不能替代后端授权。

## 十四、前端多视角界面

### 14.1 审计中心

路由：`/operations/audit`

使用工作台式紧凑布局，不做营销式卡片页面。顶部为时间范围、关键词和常用过滤器，下方使用 Tab：

1. **全局事件**：按时间倒序的审计事件表。
2. **人员行为**：操作者列表、风险统计和行为时间线。
3. **资源轨迹**：输入资源类型和标识，展示资源生命周期。
4. **请求链路**：按 request_id/correlation_id 展示跨模块时间线。
5. **资金审计**：订单、审批、钱包流水和退款/充值组合视图。
6. **风险事件**：失败、拒绝、高风险和严重事件。
7. **外部集成**：Gateway、企微、运营商回调和事件同步阶梯尝试。

Tab 根据权限返回，前端不展示无权限入口。

外部集成 Tab 增加紧凑的来源切换：`全部 / Gateway 同步 / 运营商回调 / 企业微信`。选择 Gateway 同步后展示触发来源、触发场景、资源、序列、尝试、结果、耗时和状态是否变化；点击序列打开三次尝试时间线。

### 14.2 全局事件表

字段：

```text
时间 | 风险 | 操作者 | 操作 | 主要资源 | 来源 | 结果 | request_id
```

- 支持服务端分页和列筛选。
- 点击行打开右侧详情抽屉。
- 风险仅用图标和有限颜色表达，普通成功事件保持中性色。
- `request_id`、资源编号可点击进入对应视角。

### 14.3 事件详情抽屉

分区：

```text
事件摘要
操作者与入口
关联资源
字段变更对比
请求与业务链路
错误和外部交互
```

字段变更使用结构化键值对比，不直接把整段 JSON 原样堆在页面。未知字段可以折叠显示原始 JSON。

敏感字段默认脱敏；有 `audit:sensitive:view` 权限时提供“显示敏感数据”按钮，并对该次查看再写一条敏感读取审计。

### 14.4 人员行为视角

左侧为操作者搜索和筛选，右侧显示：

- 30 天操作量、失败率、高风险操作数。
- 常用来源 IP。
- 操作类型分布。
- 最近行为时间线。
- 涉及资源列表。

不做基于行为的自动封禁，只提供审计判断依据。
左侧人员列表调用 `/api/admin/audit/actors`，选择人员后再并行加载 Summary 和事件列表，避免前端从全局事件自行聚合。

### 14.5 资源轨迹视角

支持输入 ICCID、设备虚拟号、退款单号、订单号、充值单号、账号名称等，先调用 `/api/admin/audit/resources/search` 返回候选资源；只有一个候选时直接打开时间线，多个候选时由用户选择，前端不自行猜测资源类型。

时间线同时展示：

- 业务状态变化。
- 人工操作。
- 企微审批结果。
- 资金流水链接。
- 重要 Gateway/回调事件。

普通高频轮询成功不进入资源审计时间线，避免有效信息被淹没。

### 14.6 请求和业务链路视角

使用纵向时间线展示节点，节点固定包含时间、来源、动作、结果和耗时。父子事件使用缩进和连接线表达，不使用复杂自由拖拽图编辑器。

### 14.7 风险视角

顶部显示 24 小时趋势和风险分类，下方是风险事件表。支持一键跳转操作者、资源和 correlation 链路，但第一版不建设风险处置工单。

## 十五、Access Log 修正

当前访问日志的响应体需要使用与请求体相同的递归脱敏逻辑：

```go
responseBody := sanitizeBody(c.Response().Body())
```

并增加路由级策略：

| 路由 | Body 记录策略 |
|---|---|
| 登录、Token、支付配置 | 只记录字段名和长度，不记录值 |
| 企微/支付/运营商回调 | 记录摘要和哈希，不记录完整正文 |
| 文件上传下载 | 不记录文件内容和签名 URL |
| 普通 JSON API | 脱敏后最多 50KB |

Access Log 建议保留 30 天；不得因为 Access Log 已存在而省略关键业务 Audit Event。

## 十六、保留策略

| 数据 | 保留期限 |
|---|---:|
| 资金、权限、审批和关键配置 Audit Event | 5 年 |
| 普通资产和业务操作 Audit Event | 2 年 |
| Integration Log | 180 天 |
| Gateway 高频同步 Integration Log | 正常 30 天，异常或状态变化 180 天 |
| Access Log | 30 天 |

Audit Event 默认不在线删除。数据量达到单表维护阈值后按月分区，超期分区归档到低成本存储，再由专用维护流程删除；应用账号不具备 Update/Delete 审计表权限。

## 十七、一次性审计切换

本需求与普通 DDD 触碰式迁移不同：审计基础设施、写入入口、Query API 和前端多视角界面在同一次停机发布中全局切换，不能让新旧审计长期并行。

### 17.1 新写入

- 数据同步、企微审批、站内通知和后续新功能只写统一 `tb_audit_event` 与 `tb_integration_log`。
- 账号、角色、权限、资产、套餐、订单、退款、充值、钱包、系统配置、导入导出和登录安全等现有敏感操作同时切换到统一 Audit Writer。
- 卡同步不创建 `tb_card_sync_execution`；轮询、手动刷新、事件阶梯尝试和运营商回调统一写 Integration Log。
- 发布后旧 `tb_account_operation_log`、`tb_asset_operation_log`、手动轮询触发日志等表不再产生新记录。
- 禁止双写新旧审计表，避免同一操作产生两条不一致记录。

一次性切换审计不等于一次性把所有旧业务模块迁移为 DDD。未迁移的旧 Service 通过统一审计 Adapter 写入新模型；退款、资金、卡状态等复杂且本次触碰的用例按 DDD 规范迁入 Application/Domain。

### 17.2 历史数据

- 旧审计表保留只读，不在线回填到新表。
- 审计 Query 使用 `UNION ALL` 把旧账号、资产和手动触发日志标准化为历史投影，返回 `record_source=legacy_account/legacy_asset/legacy_polling`。
- 历史记录只用于查询，不允许更新或补写。
- 后续达到归档条件后离线迁移或归档旧表，不影响本次新写入一次性切换。

## 十八、代码迁移重点

- 删除 `account_audit.Service.LogOperation` 和 `asset_audit.Service.LogOperation` 内部裸 goroutine。
- 删除调用方外围 `go auditService.LogOperation(...)`。
- 为尚未迁入 DDD 的旧 Service 提供统一 Audit Writer Adapter，并在本次发布中替换全部敏感操作旧写入口。
- 退款、线下充值资金事务使用 `AppendWithTx`。
- 卡实名领域用例在状态变化事务内写审计。
- 轮询、事件同步、手动刷新和运营商回调统一写 Integration Log；只有状态变化、连续失败、手动操作和高风险异常追加 Audit Event。
- 企微模板发布、手工绑定 sp_no、通过后撤销写高风险事件。
- `pkg/logger/middleware.go` 对响应体和特殊路由实施脱敏策略。
- 现有账号、资产和业务日志详情接口统一改为调用 `query/audit` 的资源时间线或历史投影。

## 十九、人工验证

1. 关键资金事务在审计写入失败时整体回滚。
2. 同一事件不会因 Outbox/Asynq 重试生成重复审计。
3. 一次退款能够从退款单、订单、钱包和资产四个资源视角查到同一事件。
4. request_id 能串联一次 API 请求产生的多条审计记录。
5. correlation_id 能串联退款申请、企微审批、退款、佣金和资产处理。
6. 普通用户不能访问全局、人员、风险和集成视角。
7. 敏感字段默认脱敏，查看完整敏感字段的动作本身再次被审计。
8. Access Log 请求和响应均不泄露 Token、Secret、操作密码和签名 URL。
9. 高频轮询成功只进入外部集成视角，不会淹没业务资源时间线，状态变化和连续失败仍可见。
10. 企微通过后撤销且资金已执行时生成 critical 事件，不自动冲正。
11. 历史账号/资产日志可以通过统一审计页面查询，发布后的新写入不再双写旧表。
12. 审计导出字段受角色权限控制，页面可见不等于可导出完整敏感值。
13. 人员、资源和集成视角都有独立查询接口，前端不通过下载全局事件后自行聚合。
14. 同一次事件同步的立即、3 分钟、5 分钟尝试能通过 `trigger_series` 连续展示，合并、限流和提前完成都有可解释结果。
15. 发布后旧账号、资产和轮询日志表不再新增记录，新旧历史仍能在同一审计界面查询。
