# PRD：TECH 七月迭代公共开发基础

Status: completed

## Problem Statement

七月迭代同时包含企微审批、支付与充值、钱包入账、站内通知、卡状态事件、批量订购、设备批量分配、导出和低余额预警等需求。这些需求都需要可靠事件投递、重复请求防护、异步任务状态、动态配置、增量迁移和日志脱敏，但它们不应各自实现一套互不兼容的基础设施。

当前仓库已经具备可复用的基础：GORM 显式事务、PostgreSQL 唯一约束和条件更新、钱包 `version` 乐观锁、Fiber `request_id`、Redis、Asynq 客户端与 Handler、部分任务的五态常量及进度字段，以及请求 JSON 的递归脱敏。与此同时，公共 `tb_outbox_event`、受控 `tb_system_config` 尚未落地；已有异步任务状态并不完全一致；Redis 防重、状态条件更新、乐观锁和 Worker 抢占的职责没有形成统一契约；Access Log 的响应体仍可能原样记录敏感字段，非 JSON 和敏感接口也缺少明确策略。

如果没有先冻结公共责任边界，将产生以下风险：

- 同一业务事务可能只写业务事实却丢失审计或异步事件，或者在事务内直接调用外部系统，造成不可恢复的不一致。
- 各业务建立不同 Outbox 表、Relay、重试状态和 Asynq 载荷，重复投递时缺乏稳定 `event_id`，消费者无法可靠幂等。
- 将 Redis 锁、`request_id`、状态条件更新、钱包版本号和 Worker 租约误当成同一种幂等机制，甚至把 Redis 当作最终业务事实。
- 批量订购、设备分配和导出各自创造不同的“部分成功”状态、错误结构和轮询语义，前端无法形成统一交互。
- 动态配置演变成任意 Key-Value 数据库，未经注册的 Key 可被写入，类型、值域、权限、缓存失效和审计无法保证。
- Access Log 在登录、Token、支付、企微回调和文件接口中泄露凭证、签名、支付链接、回调原文或文件内容。
- 公共基础设施需求侵入 Audit Event、Integration Log、站内通知或各业务领域，最终重新合成一个无法独立交付的巨型需求。

本 PRD 的目标不是重新探索业务需求，也不是统一重构全仓库，而是把已评审通过的跨需求约定归拢成一个可先行交付、可被下游复用、所有权清晰的公共基础设施边界。

## Solution

交付一个边界明确的 `tech-public-foundation`，覆盖以下八类已确认能力：

1. 公共表、索引和约束的增量迁移护栏，以及上线前检查、失败退出和数据安全回滚边界。
2. 基于现有 GORM 用法的显式事务契约，使业务事实与契约要求的 Audit Event 或 Outbox 在同一事务提交。
3. 权威公共 `tb_outbox_event`、Relay、领取租约、重试、监控、恢复及结构化 Asynq 投递契约。
4. `request_id + 请求指纹`、PostgreSQL 唯一约束、状态条件更新、钱包 `version`、Worker 租约和 Redis 防并发的公共幂等原语与选择规则。
5. 面向业务任务和前端的统一五态、结果计数、错误摘要、失败明细、恢复与轮询语义，但不建设万能任务表。
6. 受控系统配置壳层，包括 Key 注册、校验、缓存、权限、API 和统一审计接缝。
7. Access Log 请求/响应递归脱敏和敏感接口安全摘要。
8. 加载、空态、权限不足、失败重试和异步任务恢复的前端公共交互契约；当前仓库不实现前端代码。

责任边界固定如下：

| 责任方 | 拥有内容 | 复用但不拥有 |
|---|---|---|
| `tech-public-foundation` | 公共 Outbox 与 Relay、公共幂等原语、系统配置壳层、统一异步状态、迁移护栏、Access Log 脱敏 | Audit Event、Integration Log、通知和业务消费者 |
| `tech-global-audit` | Audit Event、Integration Log、多视角 Query、审计前端、旧审计 Writer 一次性切换 | 公共事务接缝、迁移护栏和共享脱敏策略 |
| `tech-inapp-notifications` | 通知表、模板、接收人解析、Notification Worker、通知 API 和前端通知中心 | 公共 Outbox、Relay、Worker 租约和统一审计 |
| 各业务 PRD | 业务事件定义、业务状态机、业务唯一键、业务任务表、失败明细和消费者行为 | 公共 Outbox、幂等选择规则、五态任务契约和系统配置壳层 |

以上三项公共需求保持独立，不把公共基础、全局审计和站内通知重新合并为一个巨型基础设施需求。

## User Stories

1. 作为发布负责人，我希望每张公共表、每个公共索引和约束都有唯一迁移所有者，以便避免多个下游 PRD 重复创建或互相回滚。
2. 作为发布负责人，我希望迁移前检查能发现重复业务键、非法状态、空值、类型不兼容和未完成任务，并在不满足前置条件时明确失败退出，而不是带病上线。
3. 作为运维人员，我希望数据迁移可重入、可观测，并能区分“可安全回滚结构”与“只能停止生产者后向前修复的数据事实”。
4. 作为业务开发者，我希望继续使用现有 GORM 显式事务，在一个清晰边界内提交业务事实、Audit Event 或 Outbox，而不必引入 UnitOfWork、工厂层或迁移未触碰模块。
5. 作为业务开发者，我希望通过一个权威 Outbox 模型发布事件，稳定携带 `event_id`、`event_type`、聚合/资源定位、`request_id`、`correlation_id` 和结构化载荷。
6. 作为业务开发者，我希望 Outbox 写入失败会令业务事务整体回滚，事务成功后即使 Asynq 暂时不可用，事件仍可恢复投递。
7. 作为 Relay 运维人员，我希望多个 Worker 能并发领取事件但不会长期重复处理同一行，Worker 崩溃后过期租约可以自动恢复。
8. 作为 Relay 运维人员，我希望看到待投递量、最老积压时长、投递速率、重试次数、租约过期数和最终失败数，并能按受控流程恢复失败事件。
9. 作为事件消费者，我希望重复投递始终携带同一个 `event_id` 和业务关联标识，以便用业务状态、唯一约束或消费记录实现自己的幂等。
10. 作为 API 调用方，我希望同一作用域内相同 `request_id` 和相同请求指纹返回原结果，而同一 `request_id` 携带不同业务内容时获得明确冲突。
11. 作为资金业务开发者，我希望钱包 `version`、唯一流水和事务边界继续作为资金正确性来源，公共幂等能力不会用 Redis 锁替代资金约束。
12. 作为状态机业务开发者，我希望状态条件更新负责状态流转幂等，更新未命中时按当前状态判断“已处理、冲突或资源不可见”。
13. 作为批量任务开发者，我希望继续拥有本业务的任务表和逐项失败明细，同时复用固定的五态、结果计数、租约恢复和轮询语义。
14. 作为前端用户，我希望任务出现部分成功时仍显示“已完成”，并通过总数、成功数、失败数和失败明细了解结果，而不是看到一个新的“部分成功”状态。
15. 作为前端用户，我希望刷新页面后可以通过 `task_id` 恢复任务进度，页面隐藏时停止轮询，重新可见时立即刷新。
16. 作为超级管理员，我希望查询按模块组织的受控系统配置，并通过与类型匹配的控件更新已注册且允许修改的 Key。
17. 作为安全负责人，我希望未注册配置默认不可写，错误类型、越界值和只读配置更新均被拒绝并留下统一审计。
18. 作为业务模块开发者，我希望只注册本模块的配置 Key、类型和值域；公共基础负责存储、缓存、权限和审计，但不接管具体业务校验。
19. 作为运维人员，我希望系统配置缓存失效失败会告警，Redis 故障时读取可回退 PostgreSQL，而不会把缓存当作唯一事实。
20. 作为安全负责人，我希望 Access Log 对请求和响应进行同一套递归脱敏，且敏感接口无法因解析失败而回退记录原文。
21. 作为排障人员，我希望脱敏后仍保留 `request_id`、方法、路径、安全查询参数、状态码、耗时、用户与终端信息及截断标记，以便关联问题。
22. 作为前端用户，我希望公共页面都有一致的加载、空态、权限不足、失败和重试反馈，不把权限不足伪装成空数据。
23. 作为测试人员，我希望通过真实 Fiber、GORM、PostgreSQL、Redis、Relay 和 Asynq Handler 验证公共外部行为，而不是依赖私有函数或目录结构。
24. 作为下游需求负责人，我希望清楚知道公共基础提供什么、不提供什么，以及哪些契约必须先冻结，避免为赶进度复制临时基础设施。

## Implementation Decisions

### 1. 架构与责任边界

- 采用触碰式渐进迁移。公共能力放在可复用的 Application、Persistence、Queue 和 Middleware 接缝中，但不主动迁移未被七月需求触碰的旧 Service。
- 复杂写操作仍由各业务 UseCase 和 Domain 收口业务不变量；简单写操作可使用 Application 事务脚本；读取继续由 Query 负责。公共基础不创建新的业务聚合。
- 不引入 Java 风格 UnitOfWork、事务工厂、Repository 工厂或全仓事务抽象。现有 GORM `Transaction` 用法和显式传递事务句柄是权威基础。
- 公共基础提供契约、模型和运行机制；事件含义、业务状态、消费者副作用、业务唯一键和失败明细始终由业务 PRD 所有。
- 数据库关联使用 ID 显式维护，不建立外键约束，不通过 GORM 关联标签扩大耦合。

### 2. 增量迁移与发布基础

- `tech-public-foundation` 是 `tb_outbox_event`、`tb_system_config` 及其公共索引、唯一约束和必要初始化数据的唯一迁移所有者。
- `tech-global-audit`、`tech-inapp-notifications` 和各业务 PRD 分别拥有自己的表、字段、业务索引、业务唯一约束和数据迁移。公共基础不得接管所有业务迁移。
- 发布清单必须维护数据库对象所有权。同一表、字段、索引或约束只能在一个迁移中创建或修改；下游只能声明依赖，不得复制公共 DDL。
- 每个增量迁移包含正向和回滚边界。结构创建、兼容字段和未产生业务数据的初始化可按验证结果回滚；已经产生的业务事实、Audit Event、Outbox、通知和任务结果不得通过降级删除。
- 上线前检查至少覆盖：目标对象是否存在且定义一致、唯一键冲突、必填字段空值、枚举非法值、待处理/处理中任务、未投递 Outbox、长租约和依赖版本。任何破坏正确性的异常都必须非零退出并阻断发布。
- 数据回填按稳定主键分批、可重复执行并记录进度；重复执行不能生成重复事实。`IF NOT EXISTS` 只能用于安全重入，不能掩盖已有对象定义不一致。
- 正向迁移完成后执行后置校验，包括约束生效、行数守恒、异常计数归零、关键索引可用和读写冒烟。检查输出只包含计数与安全标识，不泄露敏感数据。
- 推荐发布顺序为：迁移及前后置检查、兼容 API、Outbox Relay/Worker、依赖消费者、前端。生产者不得早于消费者和监控就绪而开始制造不可见积压。
- 回滚优先顺序为：停止新写入和 Relay 领取、保留已有事实与 Outbox、回滚无数据风险的应用版本、修复后向前恢复。公共表已有生产数据后，不允许通过删除表完成回滚。
- 发布必须设置停止条件：迁移异常、Outbox 持续积压或租约大量过期、系统配置读写不一致、Access Log 脱敏回归失败、关键任务无法恢复时停止继续放量。

### 3. GORM 事务边界

- 事务由 Application UseCase 或简单写事务脚本开启，Handler 不拼接事务逻辑，Domain 不依赖 GORM。
- 一次事务只包含需要原子提交的 PostgreSQL 写入。业务事实与契约要求的 Audit Event、Outbox 必须使用同一事务句柄；任一写入失败，整体回滚。
- 是否写 Audit Event、Outbox 或两者，由业务契约决定：需要同步查询的不可变审计事实写 Audit Event；需要跨进程消费的可靠事件写 Outbox；同一用例同时需要时两者同事务写入。Audit Event 的模型和 Writer 仍归 `tech-global-audit`。
- Redis、Asynq、HTTP、企微、支付、Gateway、运营商和对象存储调用不得放进数据库事务。事务提交后由 Relay 或后置动作执行；缓存失效在提交成功后发生。
- 事务内生成并持久化稳定 `event_id`、业务唯一键和必要快照，禁止由 Relay 或消费者在重试时重新生成身份标识。
- 事务应短小，避免在事务内解析大文件、渲染报表或执行慢查询。并发正确性由唯一约束、条件更新、行锁或版本号承担，而不是扩大事务范围。

### 4. 公共 Outbox

#### 权威模型

- 全仓只使用公共 `tb_outbox_event` 表承载需要跨进程可靠投递的领域/应用事件。业务模块不得再创建自己的 Outbox 表或 Relay。
- 权威字段语义至少包括：内部主键；全局稳定且唯一的 `event_id`；稳定的 `event_type`；载荷版本；来源聚合类型与标识；主要资源类型、标识和可选业务键；`request_id`；`correlation_id`；结构化 JSON payload；投递状态；重试次数；下次可领取时间；租约所有者和过期时间；最后错误码与脱敏摘要；创建、更新和成功投递时间。
- Outbox 状态是 Relay 内部生命周期，使用整数：`1=待投递、2=投递中、3=已投递、4=投递失败`。它不等同于面向用户的五态业务任务，也不产生“部分成功”状态。
- `event_id` 建立最终唯一约束；领取路径按状态、下次可领取时间和租约到期时间建立索引；聚合/资源和关联标识建立满足排障与恢复的查询索引。所有索引归公共基础所有。
- `event_type` 和 payload schema 归发布事件的业务 PRD 所有；公共基础只要求事件类型稳定、payload 带版本且能够向后兼容。禁止把任意业务对象完整序列化后无约束写入。
- `request_id` 表示本次入口请求或命令标识；`correlation_id` 表示跨事务、跨队列和外部交互的业务链路。没有 HTTP 请求的定时任务使用稳定命令标识，并以其或 `event_id` 建立关联链路。Relay 和消费者必须原样传播这些标识。

#### 写入、Relay 与至少一次投递

- 业务 UseCase 在保存业务事实的同一 GORM 事务内插入 Outbox。不能先提交业务再补写 Outbox，也不能在事务内直接入 Asynq。
- Relay 按小批量领取到期的待投递或可重试事件，通过数据库条件更新/跳锁机制取得有期限的处理权。领取、续租、完成和失败都必须校验当前状态与租约所有者。
- Relay 将公共事件信封作为 struct 或 map 调用项目 `EnqueueTask`；禁止传入预序列化 `[]byte`，避免二次序列化成为 Base64 字符串。直接使用 Asynq 原生任务构造器的既有代码不属于该调用方式，必须自行且只序列化一次。
- Asynq 入队成功后将 Outbox 标记为已投递。若进程在“入队成功、数据库标记前”崩溃，同一事件会再次投递，这是被接受的至少一次语义。
- 入队失败记录安全错误码与摘要，按有上限的指数退避设置下次领取时间；达到最大重试或判定永久错误后保持失败并告警，不删除记录。
- 处理中 Worker 超过租约未完成时，可由恢复扫描重新置为可领取；恢复保留原 `event_id`、payload 和关联标识，不生成新事件。
- 受控重放只能作用于明确选择的失败/滞留事件，记录操作者、原因和批次，不修改已投递事件内容。重放仍使用原 `event_id`，因此消费者必须幂等。
- 监控至少提供待投递量、最老待投递年龄、处理中与过期租约数、投递成功率、重试分布、最终失败数和按 `event_type` 的积压。阈值越界进入现有告警通道。
- 公共 Outbox 只保证“事件最终至少被送达队列”，不保证业务副作用只发生一次。消费者仍需用业务状态、PostgreSQL 唯一约束、消费记录或稳定业务键实现自己的幂等。

### 5. 公共幂等原语

- 公共基础提供选择规则和可复用构件，不建设要求所有请求进入同一张表的万能幂等平台。
- 创建类命令采用调用方稳定提供的 `request_id`，作用域至少包含调用主体与操作类型。请求指纹由会影响业务结果的规范化字段计算，排除时间戳、签名、Token 等易变传输字段，并带算法版本。
- 同一作用域内，`request_id + 相同指纹` 返回已存在结果；`request_id + 不同指纹` 返回幂等冲突，不覆盖原事实；并发首次写入由 PostgreSQL 唯一约束裁决。
- PostgreSQL 唯一约束、业务状态和账务流水是最终正确性来源。应用层预查只用于友好返回，不能替代数据库约束。
- 状态流转使用 `WHERE 当前状态=预期状态` 的条件更新；影响行数为零时重新读取当前状态，区分已完成、非法转换和统一资源不可见语义。
- 钱包余额、冻结金额和其他并发数值写使用 `version` 乐观锁或业务明确要求的行锁，并与唯一流水、业务事实、Audit Event/Outbox 同事务。公共基础不拥有钱包规则。
- Worker 租约只解决“谁在这一时刻处理”，不证明业务副作用未发生。领取条件、租约所有者、过期时间和完成条件必须落在 PostgreSQL；消费者仍执行自己的业务幂等检查。
- Redis `SETNX`、分布式锁和短期防重键只用于减少重复并发和热点压力。Redis 缺失、过期、故障或主从切换不能造成重复业务事实；锁必须设置过期并确保释放。
- 稳定 `event_id` 解决事件身份，稳定业务唯一键解决副作用身份，`request_id` 解决入口命令身份，三者不得混为一个万能键。

### 6. 统一异步任务契约

- 面向业务和前端的任务状态固定为：`1=待处理、2=处理中、3=已完成、4=已失败、5=已取消`。响应同时提供对应中文状态名。
- “已完成”表示任务已到达处理终点，不表示每个业务项都成功。部分成功由 `total_count`、`success_count`、`failed_count` 表达，禁止增加“部分成功”状态。
- 业务数据逐项完成但存在业务校验失败时，任务状态为已完成；只有文件无法解析、任务无法建立、关键基础设施持续失败或整体执行无法到达业务终点时，任务状态才为已失败。
- 每类任务继续拥有自己的业务任务表、业务项表和失败明细。批量订购、设备批量分配和导出不得为了统一状态而迁移到一张万能任务表。
- 公共查询语义至少统一 `task_id`、状态与状态名、总数/成功数/失败数、进度、脱敏失败摘要、开始时间、完成时间和更新时间。失败明细由业务定义字段并分页或受限返回。
- 失败摘要使用稳定错误码与用户可见中文说明，不暴露 SQL、堆栈、外部密钥、回调原文或内部网络信息。可下载失败文件时只返回受控短期访问能力，不写永久公开地址。
- 待处理任务通过条件更新领取为处理中；处理中任务必须具有租约或等价的可恢复执行记录。Worker 崩溃、进程重启或队列重复投递后，过期任务可以重新领取并从业务事实恢复。
- 终态只能通过满足预期状态的条件更新进入。重复 Handler 看到已完成、已失败或已取消时不得重新制造业务副作用。
- 取消只适用于业务明确支持取消的任务；不支持取消的业务仍返回五态中的实际状态，不能把失败伪装成取消。
- Asynq 载荷使用最小结构化标识，通常只包含 `task_id`、必要分片标识和关联标识；调用 `EnqueueTask` 时必须传 struct 或 map，禁止传 `[]byte`。

### 7. 公共系统配置

- 新建公共 `tb_system_config`，至少保存唯一 `config_key`、字符串化 `config_value`、`value_type`、所属模块、中文说明、只读标记、创建/更新人与时间。`value_type` 限定为 `string`、`int`、`bool`、`json`。
- Key 使用稳定的 `module.group.name` 命名。代码中的受控 Key 注册表是可写配置的权威来源，定义 Key、模块、类型、值域/枚举、默认值、是否只读、是否敏感和前端控件提示。
- 未注册 Key 默认不可写；数据库中已存在但未注册的 Key 最多按只读、可诊断方式展示。禁止通过 API 创建任意 Key，禁止提供原始 JSON 自由编辑器把它扩展成通用 Key-Value 配置中心。
- 注册表重复 Key、类型冲突或不合法默认值必须在启动或验证阶段失败，不能以后注册者静默覆盖前者。
- 查询 API 为已认证超级管理员提供按模块过滤的配置列表，返回脱敏后的值、类型、值域、说明、只读状态和更新时间。更新 API 按 Key 修改单项配置，不提供无约束批量覆盖。
- 更新流程依次执行认证与超级管理员授权、Key 注册检查、类型解析、值域/业务边界校验、GORM 事务更新和统一 Audit Event。审计模型与 Writer 复用 `tech-global-audit`，公共基础不另建配置审计表。
- Redis Key 固定按配置 Key 生成，默认缓存五分钟。PostgreSQL 是唯一事实来源；读取缓存未命中或 Redis 不可用时查询数据库并尝试回填。
- 配置更新提交成功后立即失效对应 Redis 缓存。失效失败不回滚已提交事实，但必须告警；短 TTL 限制旧值持续时间，后续读取可按版本/更新时间避免回填旧值。
- 数据库值无法解析、越界或读取失败时不得静默使用错误值。按注册策略使用最后一个已验证值或代码安全默认值，并产生可定位告警。
- UR#48 负责注册具体支付方式 Key、支付业务值域和启停校验；本 PRD 只负责存储、注册、权限、缓存、API 和审计壳层。

### 8. Access Log 脱敏

- Access Log 对 query、请求体和响应体执行同一套递归脱敏，覆盖嵌套对象与数组。字段匹配大小写不敏感，并支持公共敏感字段注册表与路由级策略。
- 通用敏感字段至少覆盖密码/口令、Token、Authorization、Cookie、密钥/Secret、签名、Nonce、验证码、支付凭证和私密 URL。脱敏值不可逆，不允许只遮盖中间几位后保留可复用凭证。
- 登录和 Token 接口：请求中的密码、验证码全部替换；响应中的访问令牌、刷新令牌、会话标识全部替换，只保留成功状态和必要主体标识。
- 支付接口：不记录支付凭证、银行卡敏感信息、二维码原文、支付跳转链接、渠道密钥和完整签名；保留安全订单号、渠道类型、结果码和金额等排障摘要。
- 企微回调：不记录原始加密包、解密正文、签名、Nonce、通讯录敏感字段或完整回调响应；保留事件类型、安全资源标识、载荷大小、摘要哈希和处理结果。
- 文件上传、下载和导出接口：不记录 multipart/binary、Base64 内容、文件字节、临时凭证和签名下载地址；只记录脱敏文件名、类型、大小、数量、任务标识和结果。
- 敏感路由的 JSON/XML/表单解析失败时采用“字段存在性、长度、内容类型、安全哈希和截断标记”的摘要策略，禁止回退记录原始 body。普通非敏感文本接口也必须经过路由策略后才可记录。
- 保留方法、路径、脱敏 query、状态码、耗时、`request_id`、IP、User-Agent、用户标识以及请求/响应摘要。请求体和响应体分别遵守现有 50KB 上限，先脱敏再截断，并明确记录截断状态。
- 脱敏器是公共可复用能力；Access Log 中间件归本 PRD。Audit Event 和 Integration Log 的模型、Writer、查询、保留策略及其字段级脱敏仍归 `tech-global-audit`，本 PRD 不重复实现。

### 9. 前端公共交互契约

- 当前仓库没有前端源码，本 PRD 只冻结跨仓 API 与交互验收契约，不虚构前端目录、组件名或状态管理实现。
- 加载态：首次加载展示明确占位并阻止重复提交；已有数据刷新时保留可辨识的旧内容和刷新提示，不闪回空白或零值。
- 空态：只有成功请求且确实无数据时展示空态；筛选无结果与系统暂无数据使用不同文案，并提供清除筛选或返回入口。
- 权限不足：按统一 403 语义展示无权限，不展示重试按钮，不用 404/空列表泄露资源是否存在。
- 失败与重试：瞬时网络或服务错误保留用户输入和已有结果，展示安全错误摘要与显式重试；参数错误定位可修正字段，不自动无限重试。
- 创建异步任务成功后，前端保存 `task_id` 到刷新后可恢复的页面状态，不能只存在内存。重新进入页面时通过 `task_id` 或业务任务列表恢复最新状态。
- 轮询状态为待处理或处理中时继续；建议间隔按 2 秒、3 秒、5 秒逐步退避，最长 10 秒。到达已完成、已失败或已取消后停止。
- 页面进入隐藏状态时暂停轮询；恢复可见时立即刷新一次，再按当前状态恢复退避。网络恢复或页面刷新不能创建重复任务。
- 已完成且 `failed_count > 0` 时展示部分成功摘要和失败明细入口；已失败展示失败摘要与业务允许的重试/重建入口；已取消展示取消原因且不自动重建。

### 10. 可观测性与故障恢复

- 公共日志和指标使用 `request_id`、`correlation_id`、`event_id`、`task_id` 和安全资源标识串联，但不得把完整 payload 或敏感配置值作为标签或日志字段。
- Outbox、系统配置缓存和 Access Log 脱敏失败均需提供中文、可操作告警；告警内容包含组件、错误码、时间窗口和安全标识。
- Worker 和 Relay 重启后以 PostgreSQL 状态和租约恢复，不依赖进程内内存或 Redis 锁推断任务是否完成。
- 恢复工具只暴露受控的查询、重试和租约释放能力，所有人工操作写统一 Audit Event；禁止直接删除 Outbox、篡改业务终态或跳过消费者幂等检查。

## Testing Decisions

### 1. 测试层级与真实接缝

- 系统配置通过真实 Fiber 路由、认证、Application、GORM 和 Redis 验证，覆盖查询、更新、超级管理员权限、未注册 Key、只读 Key、类型/值域错误、事务回滚、缓存命中与更新后失效。
- Outbox 通过“业务事务写入 → Relay → Asynq Handler → 可观察消费结果”的完整链路验证。断言基于数据库事实、队列可观察结果和消费者公开结果，不直接调用 Relay 私有函数完成测试。
- PostgreSQL 集成测试验证事务回滚、`event_id`/业务键唯一约束、状态条件领取、并发领取、租约恢复、重试调度和重复投递。
- Access Log 使用真实 Fiber 测试请求和响应，捕获最终 JSON 日志，检查递归脱敏、敏感接口特殊策略、解析失败安全降级、50KB 截断及 `request_id`/状态/耗时保留。
- Redis 和 PostgreSQL 使用隔离测试数据、唯一前缀或独立测试空间，测试只清理自己创建的数据，不执行全库/全缓存清空。
- 不依赖真实支付、企微、Gateway 或运营商网络；外部边界使用可观察的测试 Adapter。公共链路仍使用真实 PostgreSQL、Redis 和 Asynq 组件。
- 只测试公共外部行为和稳定契约，不绑定私有函数、未导出类型或内部文件组织。

### 2. 迁移与事务测试

- 在空数据库和带兼容存量数据的数据库分别执行正向迁移、后置校验和允许的回滚，验证公共对象只创建一次、定义一致且迁移版本可继续前进。
- 构造重复 `event_id`、非法配置、唯一键冲突、处理中任务和未投递事件，验证前置检查明确失败退出且不执行破坏性写入。
- 数据回填中断后重复运行，验证已完成批次不重复、未完成批次继续、行数守恒且错误报告不含敏感值。
- 在业务事实、Audit Event、Outbox 任一步注入失败，验证同一 GORM 事务完全回滚；提交成功后再模拟 Redis、Asynq 或外部 Adapter 失败，验证业务事实不回滚且可恢复。

### 3. Outbox 与幂等测试

- 验证业务提交成功时 Outbox 与业务事实同时可见，业务回滚时二者均不可见；Relay 不读取未提交事务中的事件。
- 并发启动多个 Relay 领取同一批事件，验证同一时刻只有租约所有者可完成该行；终止 Worker 后等待租约过期，验证其他 Worker 使用原 `event_id` 恢复。
- 模拟入队成功但 Outbox 未标记成功，验证再次投递同一 `event_id`，消费者通过业务唯一约束或状态检查只产生一次业务副作用。
- 覆盖瞬时失败退避、最大重试、永久失败告警、受控重放、积压年龄和不同 `event_type` 的监控聚合。
- 直接向 `EnqueueTask` 传 `[]byte` 必须失败；传 struct/map 能由公开 Handler 正确解析。重复 Marshal 造成 Base64 的回归必须被测试阻止。
- 对 `request_id + 指纹` 覆盖相同请求重放、同 ID 不同内容冲突、不同主体同 ID、并发首次提交和 Redis 不可用，验证 PostgreSQL 约束始终裁决最终结果。
- 分别验证状态条件更新、钱包版本冲突和 Worker 租约，证明三者只承担各自并发职责，不能相互替代。

### 4. 异步任务契约测试

- 对所有新接入的批量订购、设备分配和导出公开 DTO 做契约测试，状态只能为 1 至 5 且状态名一致。
- 覆盖全成功、部分成功、全部业务项失败、整体执行失败和取消：部分/全部业务项处理完均为已完成并由计数表达；整体无法执行才为已失败。
- 验证 `total_count = success_count + failed_count` 的业务终态守恒；若业务存在明确跳过项，必须在业务 PRD 中定义其归类，不能由公共层凭空新增状态。
- 验证失败摘要不泄露内部错误，失败明细分页/受限，终态重复消费不改变计数，过期处理中任务能够恢复。
- 前端契约验收覆盖加载、真实空态、筛选空态、权限不足、失败重试、2/3/5 秒退避、页面隐藏暂停、恢复立即刷新和通过 `task_id` 恢复。

### 5. 系统配置与脱敏测试

- 系统配置测试覆盖 `string/int/bool/json` 类型、枚举/范围校验、未注册和只读 Key、超级管理员与非授权用户、并发更新、审计事实和安全默认值。
- 模拟 Redis 未命中、超时、写入失败和失效失败，验证 PostgreSQL 仍为事实来源、更新不丢失、旧缓存有期限且告警可观察。
- Access Log 使用嵌套对象、数组、大小写变体、query、JSON/XML/表单、二进制、超长 body 和无法解析内容建立测试矩阵。
- 登录/Token、支付、企微回调和文件接口分别有固定回归样例；日志中不得出现测试密码、Token、签名、Nonce、支付链接、回调原文、文件字节或临时凭证。
- 验证脱敏后仍保留方法、路径、安全 query、状态、耗时、`request_id`、用户标识、body 摘要和截断标记，可用于从 Access Log 关联到 Audit/Integration 记录。

## Out of Scope

- Audit Event 和 Integration Log 的具体模型、Writer、查询、前端、保留策略及旧审计 Writer 一次性切换；这些归 `tech-global-audit`。
- 站内通知业务，包括通知表、模板、接收人、Notification Worker、通知 API 和前端通知中心；这些归 `tech-inapp-notifications`。
- 企微、支付、Gateway 和运营商 Adapter，以及其签名、协议映射、回调业务和外部补偿逻辑。
- 钱包、退款、充值、支付、套餐订购、卡状态、低余额预警等领域规则、状态机、金额校验和业务消费者行为。
- 各业务表、业务索引、业务唯一键和业务数据迁移；它们继续由对应业务 PRD 所有。
- 所有旧模块的全仓事务重构、DDD 重构、Repository 重写或异步任务迁移。
- 万能任务表、万能幂等表、全请求幂等平台和任意 Key-Value 配置中心。
- 强制所有任务使用同一个业务任务模型，或把批量订购、设备分配、导出迁移到公共表。
- 精确一次投递承诺。公共 Outbox 提供至少一次投递，消费者幂等由业务负责。
- 在当前仓库实现前端组件或虚构前端目录；这里只冻结跨仓交互和验收契约。
- 真实支付、企微、Gateway、运营商网络或生产数据上的集成测试。

## Further Notes

### 已确认的仓库基础

- GORM 已关闭默认事务并普遍使用显式事务；公共实现应沿用这一方式，不新增平行事务框架。
- 项目 Asynq 客户端已经统一使用 Redis 配置，并拒绝 `EnqueueTask` 接收 `[]byte`；仍存在直接使用 Asynq 原生客户端并自行序列化的业务代码，迁移时需区分两种调用契约。
- Fiber 已生成并传播 `request_id`；仓库已有 PostgreSQL 条件更新、钱包 `version`、Redis 防并发和导出任务五态的可复用实践，但尚未形成完整公共契约。
- 当前没有公共 Outbox 和受控系统配置实现。Access Log 已对 JSON 请求递归脱敏并限制 50KB，但响应体仍直接截断记录，敏感非 JSON/文件接口也缺少安全降级策略。
- 当前仓库没有前端源码，前端交付由对应前端仓库按本 PRD 的公开契约验收。

### 下游依赖与阻塞关系

本 PRD 是公共契约和发布顺序的前置项，但只阻塞下游对公共接缝的集成与上线，不接管下游业务设计：

| 下游 PRD | 被本 PRD 阻塞的公共接缝 | 下游仍自行负责 |
|---|---|---|
| `tech-global-audit` | 公共迁移所有权、同事务写入接缝、关联标识和共享脱敏策略 | Audit Event、Integration Log、Query、前端和旧 Writer 切换 |
| `tech-inapp-notifications` | `tb_outbox_event`、Relay、结构化载荷和租约恢复 | 通知模型、模板、接收人、Worker、API 和前端 |
| UR#37 企微审批基础 | Outbox、至少一次投递、Worker 租约、关联标识 | 企微场景、实例状态机、扫码绑定和 Adapter |
| UR#34 代理充值 | `request_id + 指纹`、Outbox、两阶段投递和任务恢复 | 支付事实、充值状态机、钱包入账和唯一流水 |
| UR#94 卡状态事件与回调 | 公共事件信封、Outbox、结构化 Asynq 载荷 | 卡状态规则、事件类型、消费者和外部回调行为 |
| UR#97 钱包低余额预警 | Outbox 与可靠消费接缝 | 阈值规则、钱包变更事件、接收人和通知内容 |
| UR#36 批量订购 | 五态任务、计数、失败语义、幂等和租约契约 | 批量任务/明细表、逐行校验、订购行为 |
| UR#42 统一导出字段权限 | 五态、结构化任务载荷、轮询和恢复语义 | 导出 DataSource、字段权限、文件生成和业务任务表 |
| UR#49 设备批量分配 CSV | 五态、部分成功、失败明细和租约契约 | 设备分配规则、CSV 校验和业务任务表 |
| UR#38 代理主钱包授信 | GORM 事务、PostgreSQL 最终幂等和 `version` 职责边界 | 钱包规则、授信/扣款、流水与领域事件 |
| UR#48 支付方式配置 | `tb_system_config`、受控 Key 注册、缓存、权限、API 和审计壳层 | 具体支付 Key、值域、支付业务校验和生效规则 |

上述下游可以在公共契约稳定后并行开发自己的领域部分；在公共 Outbox、任务状态或系统配置接缝尚未可用时，不得复制临时 Outbox、万能任务表、幂等表、通知表或配置中心作为替代。

### 交付判定

- 公共能力的公开契约、迁移所有权、故障恢复和可观测性全部通过本 PRD 的真实接缝测试后，才可认为公共基础就绪。
- `tech-global-audit` 与 `tech-inapp-notifications` 保持独立交付；公共基础就绪不等于审计或通知业务已经完成。
- 下游业务 PRD 的领域测试、外部 Adapter 测试和业务前端验收仍是各自发布门槛，不能用公共基础测试替代。
