Files
junhong_cmp_fiber/.scratch/tech-public-foundation/PRD.md
2026-07-22 12:37:05 +09:00

292 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PRDTECH 七月迭代公共开发基础
Status: ready-for-agent
## 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 测试和业务前端验收仍是各自发布门槛,不能用公共基础测试替代。