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