# PRD:TECH 七月迭代公共开发基础 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 测试和业务前端验收仍是各自发布门槛,不能用公共基础测试替代。