创建相关issues

This commit is contained in:
2026-07-22 12:37:05 +09:00
parent 841ed1ceb0
commit 21702da413
18 changed files with 629 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
# 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 测试和业务前端验收仍是各自发布门槛,不能用公共基础测试替代。

View File

@@ -0,0 +1,22 @@
# 01 — 统一换货资产解析与权威快照
**What to build:** 运营人员继续使用接口已支持的任一资产标识发起或执行换货但系统在物流换货创建、直接换货创建和物流发货三个入口统一解析真实资产并保存权威快照。IoT 卡的新旧资产快照始终保存数据库中的完整 ICCID设备快照依次选择虚拟号、IMEI、SN 中首个非空标识,不保存请求原文。三个入口必须共享同一套解析与规范化规则,自动化测试、接口文档和中文功能说明同步证明该行为可以独立发布和验收。
**Blocked by:** None — can start immediately.
**Status:** ready-for-agent
**架构通道:** 主通道为复杂写,辅助通道为 Infrastructure Adapter。资产解析和权威标识选择作为换货写用例复用能力收口既有 Service 可以作为迁移门面调用该能力,但规则不得继续散落在多个流程分支。
**完整业务边界:** 本票收口资产标识解析、权威快照选择以及物流创建、直接创建、物流发货三个快照写入入口并包含对应自动化测试、OpenAPI 契约和中文发布说明。明确不迁移换货状态机、确认完成、取消、资料迁移、旧资产转新、客户绑定切换等旧逻辑;不回填或改写历史换货单,不依赖 UR#86 或 UR#98 的实现。
- [ ] 使用 ICCID、接入号或虚拟号定位旧 IoT 卡时,新建物流换货单的旧资产快照均为该卡数据库中的完整 ICCID。
- [ ] 直接换货的新旧资产和物流换货发货时的新资产均复用相同规范化能力IoT 卡快照不保存请求原文、接入号或虚拟号。
- [ ] 设备无论通过虚拟号、IMEI 或 SN 定位,快照都按“虚拟号 → IMEI → SN”的优先级选择首个非空稳定标识。
- [ ] 标识解析仍执行既有资产权限、资产类型、状态和并发校验,不扩大可操作资产范围,也不改变换货生命周期规则。
- [ ] 资产不存在、类型不匹配或数据库失败时返回既有统一错误体系中的脱敏错误,不向客户端透出底层错误。
- [ ] 自动化测试覆盖卡的三种输入标识、设备的三种输入标识及设备标识优先级,并分别验证物流创建、直接创建和物流发货的持久化快照。
- [ ] 端到端回归验证三个写入入口产生的快照可由现有换货详情或列表响应读取,且构造的历史非规范快照保持原值、不被自动回填。
- [ ] OpenAPI 中创建换货和物流发货的请求标识说明、响应快照语义与实际实现一致,并完成文档重新生成验证。
- [ ] UR#45 中文功能总结记录卡与设备快照规则、历史数据不回填策略、错误边界、发布与回滚注意事项README 增加对应索引。
- [ ] 所有新增或修改的导出符号、复杂逻辑注释和日志均使用中文,并通过相关 Go 测试与格式检查。

View File

@@ -0,0 +1,27 @@
# 02 — 提供新旧资产独立搜索的换货列表契约
**What to build:** 运营人员在换货列表中可以分别提交 `old_asset_keyword``new_asset_keyword`,通过 IoT 卡的 ICCID、接入号、虚拟号或设备的虚拟号、IMEI、SN 搜索对应一侧的换货资产。两个关键词同时提交时按 AND 组合,并继续与状态、流程类型、创建时间、分页和现有店铺数据范围共同生效;旧通用 `identifier` 不再属于新接口契约。该列表能力包含接口发布、真实 PostgreSQL 性能证据和前端同维护窗口切换所需的完整验收契约,可独立交付。
**Blocked by:** None — can start immediately.
**Status:** ready-for-agent
**架构通道:** 主通道为 Query。仅将本次明显复杂化的换货列表读取用例收口为查询能力可直接使用 GORM、子查询或固定次数批量查询完成候选解析、权限过滤和 DTO 投影;不得让列表读取经过聚合根或执行写操作。
**完整业务边界:** 本票收口列表请求校验、候选资产解析、新旧资产主键过滤、权限、分页计数、排序、响应投影、错误转换、OpenAPI 发布、性能验证和前端联调验收说明。明确不迁移换货详情及其他读取接口,不创建换货聚合根,不修改换货写侧状态规则,不保留通用 `identifier` 的第二套长期搜索语义;不实现 UR#86 资产前代/后代关系或 UR#98 店铺继承。当前仓库未包含可实施该页面的前端工程,因此前端工作以接口契约和人工验收清单交付,不虚构前端代码改动。
- [ ] 列表请求新增最长 100 字符的 `old_asset_keyword``new_asset_keyword`,并从新契约移除通用 `identifier`;空值不增加对应过滤条件。
- [ ] 仅提供旧资产关键词时只按 `old_asset_type + old_asset_id` 过滤,绝不因新资产命中而返回;仅提供新资产关键词时规则对称。
- [ ] 两个关键词同时提供时按 AND 组合,并与状态、流程类型、创建时间范围和分页条件按 AND 组合。
- [ ] IoT 卡候选支持对 ICCID、接入号和虚拟号做包含匹配设备候选支持对虚拟号、IMEI 和 SN 做包含匹配;候选资产必须排除软删除记录。
- [ ] 换货单通过资产类型和资产主键命中,因此历史非规范快照不回填、不改写,但仍能通过所关联资产的任一受支持标识搜索到。
- [ ] 无候选资产或无换货单命中时返回成功的空分页;候选查询或换货单查询发生数据库错误时返回脱敏 500不得降级为空结果。
- [ ] 最终换货单查询继续排除软删除记录并应用现有店铺数据范围;平台、超级管理员和代理账号的既有可见范围不被候选资产解析绕过。
- [ ] 候选解析和换货单过滤使用数据库子查询或固定次数批量查询,不按换货单逐行反查资产;`total``items` 使用完全相同的过滤条件,结果按创建时间倒序。
- [ ] Handler 对完整请求 DTO 执行校验;非法关键词长度、分页、状态、流程类型或时间参数统一返回 HTTP 400、`code=1001``msg=参数验证失败`,详细原因仅记录中文日志。
- [ ] HTTP 集成测试覆盖仅旧关键词、仅新关键词、双关键词 AND、状态与时间组合、空参数、无匹配、非法参数、历史快照、数据库故障和各类账号数据权限。
- [ ] 响应保持统一外层结构并继续分别返回新旧资产类型、ID、快照标识、状态及状态名称。
- [ ] OpenAPI 中的换货列表只公开 `old_asset_keyword``new_asset_keyword`包含长度限制、AND 语义和中文说明,不再公开通用 `identifier`;重新生成文档并验证请求、响应和错误契约与实现一致。
- [ ] 使用真实 PostgreSQL 和代表性大结果集验证查询次数固定、无逐行资产反查、`total` 与分页结果一致,并记录查询计划或等价证据;性能满足项目列表接口目标。
- [ ] UR#45 中文功能总结补充搜索契约、权限与脱敏错误边界、性能结果、发布回滚方式和前端联调注意事项README 中的 UR#45 索引可以定位该说明。
- [ ] 前端人工验收清单明确要求:将单一资产输入框拆为旧资产和新资产输入框;空值不提交;双条件按 AND 提交;表格不混列新旧资产;前端不解析标识、不本地过滤当前页;清空、分页、空态和失败反馈沿用现有交互。

View File

@@ -0,0 +1,21 @@
# 01 — 后台资产详情统一展示预计最终到期
**What to build:** 运营人员通过统一资产解析查看卡或设备详情时,可以看到全部有效主套餐按稳定队列连续使用后的“预计套餐到期时间”。系统以统一 Query 计算 `exact``waiting_activation``none``invalid_data` 四种状态,并返回预计日期、上海自然日剩余天数和临期派生结果;后台详情按状态展示明确文案,不再把当前套餐自身到期时间当作资产汇总口径。
**Blocked by:** `.scratch/ur55-package-expiry-base/issues/05-activate-and-queue-from-package-snapshots.md` — 05 — 套餐激活与队列接续只消费购买快照
**Status:** ready-for-agent
- [ ] 架构主通道为 QueryInfrastructure 仅负责 GORM 批量读取、历史套餐兜底和索引;完整收口预计最终到期的读取与投影边界,不迁移套餐激活、购买、退款或队列接续写逻辑。
- [ ] 建立可供单资产和批量资产入口共同调用的纯计算核心;两种入口对同一组使用记录返回完全一致的状态、预计日期、剩余自然日和临期标记,卡、设备及各端不得各自实现推算规则。
- [ ] 仅纳入未软删除、仍处于当前或排队生命周期且不属于加油包的主套餐;排除已过期、已失效和已退款记录,并按 `priority ASC, created_at ASC, id ASC` 稳定计算。
- [ ] 当前生效主套餐的真实到期时间作为游标起点,后续套餐从前一段结束后的下一时刻接续;自然月和按天时长统一使用项目套餐生命周期日期语义,并以 `Asia/Shanghai` 解释业务自然日。
- [ ] 排队记录优先使用各自购买时计时快照;仅允许 UR#55 上线前形成的历史缺失快照记录显式回退套餐当前值,非法队列、非法日期或无法安全解析的快照返回 `invalid_data`,不得伪造日期或降级为 `none`
- [ ] 结果完整区分 `exact``waiting_activation``none``invalid_data`;非 `exact` 时预计日期和剩余天数明确序列化为 `null`,不能省略字段表达状态。
- [ ] 剩余天数按上海时区日期差计算,允许普通详情返回负数;仅 `exact` 且剩余 015 个自然日时 `is_expiring=true`
- [ ] 后台统一资产解析接口为卡和设备返回预计日期、剩余天数、状态、状态中文名称及临期标记;查询失败向上返回统一错误,不得静默返回 `none`
- [ ] 后台卡和设备详情统一显示“预计套餐到期时间”:精确日期、待激活后起算、—、数据异常分别对应四种状态;当前套餐自身到期时间只保留在套餐明细中。
- [ ] 为未软删除主套餐的资产、状态和队列读取建立经开发库 `EXPLAIN ANALYZE` 验证的非唯一或部分索引;索引迁移包含中文注释、可逆向下迁移,代表性查询满足项目性能目标。
- [ ] 纯计算测试覆盖无套餐、仅当前套餐、多段队列、自然月、按天、月末、跨年、等待实名激活、历史回退、非法快照、重复优先级、退款、失效、软删除和加油包排除,并明确验证接续边界不多算或少算一天。
- [ ] 使用真实开发 PostgreSQL、Redis、JWT 和进程内 Fiber App 完成后台卡/设备详情集成验证;新增、退款或失效排队套餐后无需刷新快照,下一次查询立即返回新结果。
- [ ] 补充本功能中文总结文档并同步更新 README不实现 UR#33 临期列表、Dashboard、提醒不实现 UR#42 导出,也不维护资产级最终到期快照字段。

View File

@@ -0,0 +1,17 @@
# 02 — 卡列表批量展示预计最终到期
**What to build:** 运营人员和代理人员查看卡列表时,每张卡都能看到与资产详情完全一致的“预计套餐到期时间”、剩余自然日、推算状态和临期标记。列表按本页资产一次批量计算,不因新增字段产生逐卡查询,也不改变普通卡列表既有筛选、分页和排序。
**Blocked by:** `.scratch/ur46-estimated-final-expiry/issues/01-admin-asset-detail-estimated-final-expiry.md` — 01 — 后台资产详情统一展示预计最终到期
**Status:** ready-for-agent
- [ ] 架构主通道为 Query复用 01 已建立的批量入口和纯计算核心,完整收口普通卡列表的预计最终到期投影,不迁移卡管理写操作、套餐生命周期或未触碰的列表逻辑。
- [ ] 卡列表每项明确返回预计日期、剩余自然日、推算状态、状态中文名称和临期标记;非 `exact` 时 nullable 字段返回 `null`,查询错误遵循统一错误规范。
- [ ] 一次批量加载本页全部卡的参与使用记录和必要历史套餐兜底数据,再按资产分组计算;不得逐卡调用单资产 Query 或产生其他 N+1 查询。
- [ ] 同一张卡在列表批量入口和详情单资产入口得到完全一致的结果,包含历史快照回退、等待激活、异常数据和已过期负数天数场景。
- [ ] Count 与 Fetch 保持原有相同过滤条件,新增投影不改变总数;普通卡列表继续使用原有稳定排序,不按预计日期或临期程度重新排序。
- [ ] 后台和代理端卡列表统一显示“预计套餐到期时间”,四种状态分别展示精确日期、待激活后起算、—、数据异常;前端不叠加套餐时长或自行计算剩余天数。
- [ ] `is_expiring=true` 时仅应用约定临期颜色,不引入 UR#33 的独立临期列表、03 天置顶、Dashboard 或通知行为。
- [ ] HTTP 集成测试覆盖四种状态、nullable 字段、详情与列表一致性、原筛选和分页不变;使用每页 100 张卡的代表性数据验证固定查询数量并满足项目性能目标。
- [ ] 前端验收普通卡列表排序不变、翻页稳定,当前套餐自身到期时间不再作为资产摘要中的第二个汇总到期字段。

View File

@@ -0,0 +1,17 @@
# 03 — 设备列表批量展示预计最终到期
**What to build:** 运营人员和代理人员查看设备列表时,每台设备都能看到与资产详情完全一致的“预计套餐到期时间”、剩余自然日、推算状态和临期标记。列表复用统一批量 Query不按设备逐条读取套餐也不改变普通设备列表既有筛选、分页和排序。
**Blocked by:** `.scratch/ur46-estimated-final-expiry/issues/01-admin-asset-detail-estimated-final-expiry.md` — 01 — 后台资产详情统一展示预计最终到期
**Status:** ready-for-agent
- [ ] 架构主通道为 Query复用 01 已建立的批量入口和纯计算核心,完整收口普通设备列表的预计最终到期投影,不迁移设备绑定、设备管理写操作、套餐生命周期或未触碰的列表逻辑。
- [ ] 设备列表每项明确返回预计日期、剩余自然日、推算状态、状态中文名称和临期标记;非 `exact` 时 nullable 字段返回 `null`,查询错误遵循统一错误规范。
- [ ] 一次批量加载本页全部设备的参与使用记录和必要历史套餐兜底数据,再按资产分组计算;不得逐设备调用单资产 Query 或产生其他 N+1 查询。
- [ ] 同一台设备在列表批量入口和详情单资产入口得到完全一致的结果,包含历史快照回退、等待激活、异常数据和已过期负数天数场景。
- [ ] Count 与 Fetch 保持原有相同过滤条件,新增投影不改变总数;普通设备列表继续使用原有稳定排序,不按预计日期或临期程度重新排序。
- [ ] 后台和代理端设备列表统一显示“预计套餐到期时间”,四种状态分别展示精确日期、待激活后起算、—、数据异常;前端不叠加套餐时长或自行计算剩余天数。
- [ ] `is_expiring=true` 时仅应用约定临期颜色,不引入 UR#33 的独立临期列表、03 天置顶、Dashboard 或通知行为。
- [ ] HTTP 集成测试覆盖四种状态、nullable 字段、详情与列表一致性、原筛选和分页不变;使用每页 100 台设备的代表性数据验证固定查询数量并满足项目性能目标。
- [ ] 前端验收普通设备列表排序不变、翻页稳定,当前套餐自身到期时间不再作为资产摘要中的第二个汇总到期字段。

View File

@@ -0,0 +1,16 @@
# 04 — C 端资产信息统一展示预计最终到期
**What to build:** 客户在 C 端查看卡或设备资产信息时可以看到与后台一致的“预计套餐到期时间”、剩余自然日、推算状态和临期标记。C 端继续在套餐明细中保留当前套餐自身到期时间,但资产摘要只使用统一 Query 给出的最终到期口径,前端不自行推算。
**Blocked by:** `.scratch/ur46-estimated-final-expiry/issues/01-admin-asset-detail-estimated-final-expiry.md` — 01 — 后台资产详情统一展示预计最终到期
**Status:** ready-for-agent
- [ ] 架构主通道为 Query辅助通道为 C 端接口投影;复用 01 的单资产入口和纯计算核心,完整收口 C 端资产信息展示,不迁移购买、实名、续费、退款或套餐激活写逻辑。
- [ ] C 端卡和设备资产信息明确返回预计日期、剩余自然日、推算状态、状态中文名称和临期标记字段语义、RFC3339 序列化及 nullable 行为与后台完全一致。
- [ ] 同一资产、同一时刻和同一套餐队列下C 端与后台详情返回相同的预计结果;不得在 C 端 Service、Handler 或前端建立另一套套餐时长累加规则。
- [ ] C 端资产摘要统一显示“预计套餐到期时间”,四种状态分别展示精确日期、待激活后起算、—、数据异常;异常不得伪装成无套餐或空白日期。
- [ ] 套餐明细中的当前套餐到期时间继续保留用于解释当前周期,但不得与预计最终到期并列为两个资产汇总口径。
- [ ] `is_expiring=true` 时仅应用约定临期颜色;本票不增加 UR#33 的续费入口、临期列表、通知或其他临期业务行为。
- [ ] 使用真实开发 PostgreSQL、Redis、JWT 和进程内 Fiber App 完成 C 端卡、设备集成测试覆盖四种状态、nullable 字段、后台一致性以及套餐新增、退款或失效后下一次查询实时变化。
- [ ] 前端验收确认 C 端不叠加套餐时长、不自行计算剩余天数,并保持现有套餐明细信息可见。

View File

@@ -0,0 +1,17 @@
# 01 — 创建套餐分配时固化生效条件选择
**What to build:** 运营人员通过系列授权、后续追加授权或批量套餐分配创建代理套餐分配时,可以显式选择跟随套餐默认、购买即生效或实名即生效。系统为本次创建的每条分配保存相同选择,并在列表、详情和操作结果中返回套餐默认值、分配覆盖值及最终有效值的中文名称。任一记录校验或写入失败时,整次操作不留下部分分配。
**Blocked by:** None — can start immediately
**Status:** ready-for-agent
- [ ] 架构主通道为简单写,读取投影使用 Query 或现有只读门面;完整收口创建分配时解析和保存覆盖值的业务边界,不迁移套餐管理 CRUD、价格、佣金或历史购买记录。
- [ ] 数据库迁移为套餐分配增加可空的生效条件覆盖字段,`NULL` 明确表示跟随套餐当前默认值,并包含中文字段注释及可逆的向下迁移。
- [ ] 系列首次授权、系列后续追加套餐和批量套餐分配均要求显式提交同一个覆盖选择,并写入本次创建的每条分配;跟随默认必须提交 JSON `null`,不能依靠省略字段表达。
- [ ] 仅接受 `null``from_purchase``from_activation`非法值或字段缺失统一返回参数错误DTO 枚举说明与套餐常量原文一致。
- [ ] 分配列表、详情及创建结果返回默认值、覆盖值、最终有效值及其中文名称;最终有效值由后端计算,前端不得自行合并。
- [ ] 任一套餐不存在、跨系列、越权、不可授权或写入失败时整批回滚,不产生部分系列授权、套餐分配或成功副作用。
- [ ] 后台分配表单提供“跟随套餐默认、购买即生效、实名即生效”三个选项,并正确发送显式 `null`
- [ ] HTTP 集成测试覆盖全部创建入口、三个选择、整批一致性、字段缺失、非法枚举、越权及事务回滚;前端验收列表和详情的默认值、覆盖值、最终值显示一致。

View File

@@ -0,0 +1,18 @@
# 02 — 编辑已有分配的生效条件覆盖
**What to build:** 运营人员可以修改已有套餐分配的生效条件覆盖,或通过显式 JSON `null` 恢复跟随套餐默认。重复设置相同值按幂等成功处理;修改完成后重新读取并展示后端计算的有效值,同时明确告知该操作只影响后续购买,不改变任何已购买套餐。
**Blocked by:** `.scratch/ur55-package-expiry-base/issues/01-create-allocation-expiry-base-override.md` — 01 — 创建套餐分配时固化生效条件选择
**Status:** ready-for-agent
- [ ] 架构主通道为简单写,读取投影使用 Query 或现有只读门面;完整收口单条分配覆盖值修改,不扩展为批量修改或迁移套餐默认配置接口。
- [ ] 提供修改单条套餐分配生效条件覆盖的后台 PATCH 接口,请求必须包含覆盖字段,并能区分字段缺失与显式 JSON `null`
- [ ] 接口仅接受 `null``from_purchase``from_activation``null` 恢复跟随默认,非法值和字段缺失统一返回参数错误。
- [ ] 分配不存在、已软删除或越权时遵守统一防越权错误语义,不向调用方区分无权限与资源不存在。
- [ ] 重复提交当前值按幂等成功处理,不修改已有套餐使用记录,也不产生与实际变更不符的业务副作用。
- [ ] 修改成功后响应或重新查询结果包含默认值、覆盖值、最终有效值及其中文名称,最终值始终由后端计算。
- [ ] 敏感配置变更写入统一审计,记录操作人、变更前后覆盖值及分配标识,但不把底层错误或无权资源信息暴露给客户端。
- [ ] 前端编辑区域显示“仅影响后续新订单,不影响已购买套餐”,成功后重新拉取分配详情,不在本地推导最终有效值。
- [ ] HTTP 集成测试覆盖两个覆盖值、显式恢复默认、字段缺失、非法值、不存在、软删除、越权、重复 PATCH 幂等及已购买记录不变。

View File

@@ -0,0 +1,18 @@
# 03 — 同步购买链路创建不可变套餐计时快照
**What to build:** C 端购买、后台代购和代理囤货等同步订单路径,在创建正式主套餐或加油包使用记录时,通过同一套餐生命周期能力解析购买时有效的生效条件、周期类型和时长,并在创建事务中一次性保存完整快照。购买完成后再修改套餐或分配配置,不会改变这些使用记录承诺的计时规则。
**Blocked by:** `.scratch/ur55-package-expiry-base/issues/01-create-allocation-expiry-base-override.md` — 01 — 创建套餐分配时固化生效条件选择
**Status:** ready-for-agent
- [ ] 架构主通道为复杂写Infrastructure 负责持久化;完整收口同步购买形成套餐使用记录时的计时条款解析,不迁移自动购包、激活接续、最终到期 Query 或未触碰的订单用例。
- [ ] 套餐使用记录增加生效条件、周期类型、月数和天数四个购买快照字段;迁移包含中文注释和可逆向下迁移,不回填或猜测历史购买配置。
- [ ] 套餐生命周期 Domain 提供 `PackageTermsSnapshot` 值对象或等价单一能力,校验并返回有效生效条件、周期类型、月数和天数;领域规则不依赖 Fiber、GORM 或 Redis。
- [ ] 有分配覆盖时使用覆盖值,无覆盖时使用套餐默认值;解析结果必须是非空有效枚举,不能把 `NULL=跟随默认` 写成空快照。
- [ ] C 端购买、后台代购、代理囤货及仓库中其他同步生产入口都调用同一快照能力,正式主套餐和加油包均保存四个字段。
- [ ] 四个快照与套餐使用记录在同一数据库事务写入,不允许先创建空记录再异步补齐;解析、校验或持久化失败时不留下订单后处理产生的部分使用记录。
- [ ] 快照写入后不因套餐默认值、套餐周期时长、分配覆盖或系列授权变化而更新。
- [ ] 新同步购买记录若无法生成完整有效快照必须拒绝创建并记录中文上下文日志,不得静默退回读取套餐当前值。
- [ ] 领域测试覆盖无覆盖、两个覆盖值、非法值、自然月和按天时长;集成测试逐一覆盖所有同步创建路径、主套餐、加油包、事务回滚及购买后配置变化不修改快照。

View File

@@ -0,0 +1,17 @@
# 04 — 自动购包链路复用不可变套餐计时快照
**What to build:** 自动购包任务创建正式主套餐和加油包使用记录时,复用同步购买已经建立的套餐计时快照能力,在同一业务事务中保存购买时承诺。任务重试或重复消费不会重复创建使用记录,也不会产生缺少快照的新记录。
**Blocked by:** `.scratch/ur55-package-expiry-base/issues/03-snapshot-synchronous-package-purchases.md` — 03 — 同步购买链路创建不可变套餐计时快照
**Status:** ready-for-agent
- [ ] 架构主通道为复杂写Application 通过 Port/Adapter 编排自动任务和持久化;只迁移自动购包形成套餐使用记录的完整用例,不迁移其调度、定价、支付或其他订单规则。
- [ ] 自动购包创建主套餐和加油包时调用与同步购买相同的计时条款解析能力,不再自行读取套餐当前生效条件和时长决定使用记录语义。
- [ ] 自动任务能够解析对应代理分配的覆盖值;无覆盖时使用套餐默认值,并为每条新使用记录写入四个非空、有效的计时快照。
- [ ] 使用记录及其快照在同一业务事务提交,任一步骤失败均不留下部分订单、部分主套餐、部分加油包或空快照记录。
- [ ] 重复任务和并发消费依赖数据库业务幂等事实或状态条件,不以 Redis 锁代替权威幂等;同一业务只生成一组套餐使用记录。
- [ ] 队列载荷使用结构体或 map不向统一入队能力传入预序列化字节。
- [ ] 新自动购包记录无法生成完整快照时明确失败并记录中文上下文日志,不允许静默使用历史兼容回退。
- [ ] 自动任务集成测试覆盖两个生效条件、跟随默认、主套餐、加油包、重复消费、并发、事务中途失败和购买后配置变化不修改快照。

View File

@@ -0,0 +1,21 @@
# 05 — 套餐激活与队列接续只消费购买快照
**What to build:** 套餐首次激活、实名后激活、前一主套餐结束后的排队接续及退款后的下一套餐接续都只使用套餐使用记录保存的购买快照决定起算条件和周期时长。历史旧记录缺少快照时可以通过明确、可观测的兼容路径继续运行UR#55 上线后产生的新记录缺少快照则被识别为数据异常,不能静默读取可变套餐配置。
**Blocked by:**
- `.scratch/ur55-package-expiry-base/issues/03-snapshot-synchronous-package-purchases.md` — 03 — 同步购买链路创建不可变套餐计时快照
- `.scratch/ur55-package-expiry-base/issues/04-snapshot-auto-purchase-package-usages.md` — 04 — 自动购包链路复用不可变套餐计时快照
**Status:** ready-for-agent
- [ ] 架构主通道为复杂写Infrastructure 负责历史数据读取和可观测设施;完整收口套餐激活与接续用例,不实现 UR#46 最终到期展示、UR#33 临期能力或未触碰的套餐管理 CRUD。
- [ ] 首次激活、实名后激活、前一主套餐到期后的排队接续和退款后的接续,共享同一套餐生命周期规则,并只从使用记录快照读取生效条件、周期类型、月数和天数。
- [ ] `from_purchase``from_activation` 的起算语义由使用记录快照决定;购买后修改套餐默认值、周期、时长或分配覆盖,不改变已有记录的激活时间、到期计算或队列接续结果。
- [ ] 正式主套餐和加油包的激活均消费各自快照;加油包继续遵守既有主套餐关联规则,不借本票改变其生命周期范围。
- [ ] 仅 UR#55 上线前形成且快照缺失的历史记录允许显式回退套餐当前值;每次回退都记录可定位使用记录的结构化告警和指标。
- [ ] UR#55 上线后形成的新记录缺少或包含非法快照时,必须拒绝或标记异常并告警,不得静默进入历史回退路径。
- [ ] 移除触碰用例中直接读取套餐当前 `expiry_base`、周期或时长的生产分支;不得保留“行业卡永远直接激活”等绕过统一实名规则的特殊判断。
- [ ] 重复激活和重复接续通过状态条件保证幂等;事务失败不会留下部分状态、错误到期时间或重复激活下一套餐。
- [ ] 领域与集成测试覆盖首次购买即生效、等待实名后生效、连续排队、退款接续、历史回退、新记录缺失快照、重复执行及购买后修改全部相关配置。
- [ ] 发布验证遵循“兼容读取应用先就绪,再执行迁移并在同一维护窗口切换所有写入路径”;窗口结束后确认没有新增空快照,并能观测历史回退数量。

View File

@@ -0,0 +1,22 @@
# 01 — 修复店铺列表参数校验与默认分页
**What to build:** 让 API 调用方通过现有店铺列表接口获得可信的参数校验和分页行为:所有已声明查询参数在进入查询前完成统一解析与完整校验,非法参数只返回脱敏的统一参数错误;未提供分页参数时,数据库查询与响应元数据都明确使用第 1 页、每页 20 条。该行为通过真实认证链路和数据服务独立验收,并同步固化到接口契约与需求文档。
**Blocked by:** None — can start immediately.
**Status:** ready-for-agent
**架构通道:** 主通道为现有读取链路 `Handler → Service → Store → GORM/DTO`;不迁移到新的 Query 或 DDD 模块。
**完整业务边界:** 仅收口店铺列表请求参数校验、错误脱敏和分页一致性。明确不修改其他 Handler 的校验方式,不迁移店铺模块,不改变列表响应结构、已有筛选语义、排序或数据权限。
- [ ] 店铺列表在查询参数解析后对整个请求 DTO 执行校验,覆盖分页、名称、编号、上级店铺、层级和状态的既有约束。
- [ ] 查询参数解析失败或任一字段校验失败时,不执行店铺查询;客户端收到 HTTP 400、`code=1001``msg=参数验证失败``data=null` 和时间戳,响应不包含解析器或 Validator 的具体错误文本。
- [ ] 服务端日志保留请求上下文和具体失败原因但不记录数据库密码、JWT 密钥或其他敏感配置。
- [ ] 未传 `page``page_size` 时,查询前归一化为第 1 页、每页 20 条,并在响应中返回 `page=1``size=20`
- [ ] 显式提供的合法页码和每页数量原样生效,不因任何筛选条件被后端重置。
- [ ] HTTP 集成测试通过进程内 Fiber App、真实后台认证中间件、真实 PostgreSQL、Redis 和 JWT 配置验证成功及失败响应;测试数据和 Token 状态使用唯一标识并严格清理。
- [ ] 集成测试覆盖非法分页、非法层级、非法状态、超长名称或编号、默认分页、显式分页、未认证及无效 Token 等关键行为。
- [ ] OpenAPI 准确描述店铺列表已有字段的校验限制和分页契约,生成结果中不出现与本切片无关的漂移。
- [ ] UR#60 中文总结及 README 索引记录本切片的参数错误协议、默认分页行为、验证证据和明确不迁移的旧代码范围。
- [ ] 运行本切片目标测试、格式化、静态检查和相关构建,确认该行为可独立交付且不破坏原店铺列表语义。

View File

@@ -0,0 +1,27 @@
# 02 — 交付联系电话精确查询与数据库索引
**What to build:** 让有权限的后台运营人员能够在店铺列表页面输入完整的 11 位 ASCII 联系电话并精确定位所有可见匹配店铺。该切片贯通前端筛选交互、现有列表 API、数据权限、数据库等值查询和性能索引并用真实请求完成加载、空态、失败、组合筛选和重复号码验收。
**Blocked by:** 01 — 修复店铺列表参数校验与默认分页。
**Status:** ready-for-human
**架构通道:** 主通道为现有读取链路 `Handler → Service → Store → GORM/DTO`;辅助通道为 Infrastructure 数据库迁移和跨仓前端交付。
**完整业务边界:** 完整收口店铺列表联系电话精确查询这一用户行为包括后端、数据库、前端、接口契约和验收证据。明确不修改店铺创建或更新接口的电话校验不清洗历史号码不建立唯一约束不新增缓存、聚合、Repository 抽象或 Query 目录,也不改变前端既有页码状态策略。
- [ ] 店铺列表接受可选 `contact_phone` 参数;非空值必须完整满足 11 位 ASCII 数字规则,空字符串与未传参数均视为不启用电话筛选。
- [ ] 联系电话使用数据库等值匹配,不接受空格、全角数字、国家码、连字符、字母或长度不符的输入,也不执行 trim 或号码格式修复。
- [ ] 联系电话与所有已有筛选条件按 AND 组合,计数查询与分页数据查询使用完全一致的过滤条件。
- [ ] 相同联系电话的多个可见店铺均可返回;无匹配项返回成功空分页,不返回资源不存在错误。
- [ ] 查询保持 `created_at DESC` 排序并排除软删除记录;代理账号无法通过相同联系电话看到本店铺及下级范围之外的店铺。
- [ ] 超级管理员、平台账号和代理账号的真实 HTTP 集成测试覆盖精确匹配、非模糊匹配、重复号码、组合筛选、空参数、非法格式、空结果、分页、排序、软删除和数据隔离。
- [ ] 数据库查询失败时返回脱敏的 HTTP 500、`code=2001``msg=内部服务器错误`,响应不泄露 SQL、主机、库名或驱动错误。
- [ ] 数据库迁移创建名为 `idx_shop_contact_phone` 的非唯一部分 B-tree 索引,仅覆盖未软删除店铺的 `contact_phone`;回滚只删除该索引,不修改业务数据。
- [ ] 在可控开发环境验证向上迁移、向下回滚和再次向上迁移,确认索引类型、列、非唯一属性及 `deleted_at IS NULL` 条件均符合契约。
- [ ] OpenAPI 出现 `contact_phone`,准确描述 11 位 ASCII 数字、精确匹配、空值行为、AND 组合和分页限制;复用现有 Handler不新增文档生成器实例。
- [ ] 后台店铺列表筛选区提供“联系电话”输入、查询和清空能力;非法值展示中文提示并阻止请求,空值不提交该参数,合法值与其他筛选一并提交。
- [ ] 前端查询期间展示加载状态,无匹配项展示空态,请求失败展示可重试反馈且不保留伪装成新结果的旧数据。
- [ ] 浏览器网络请求与真实接口响应共同证明精确匹配、AND 组合、重复号码、空结果、合法分页和代理隔离,而不是只依据页面展示验收。
- [ ] UR#60 中文总结及 README 索引记录本切片的前后端契约、索引上线与回滚核验、测试证据以及明确排除的历史数据清理范围。
- [ ] 运行前后端各自的目标测试、格式化、静态检查和构建;本切片通过人工验收后可独立演示完整联系电话查询行为。

View File

@@ -0,0 +1,22 @@
# 03 — 禁止企业账号访问核心店铺管理路由
**What to build:** 让企业账号在调用店铺列表、创建、更新、删除和联级查询五个核心店铺管理入口时始终得到一致的禁止访问响应,同时保持超级管理员、平台账号和代理账号的现有能力,并证明其他 `/shops` 前缀业务入口未被误拦截。该权限行为通过真实认证请求独立验收,并同步固化到接口与需求文档。
**Blocked by:** None — can start immediately.
**Status:** ready-for-agent
**架构通道:** 路由与中间件权限通道;列表读取仍沿用现有读取链路。
**完整业务边界:** 只收口五个核心店铺管理路由的企业账号访问限制。明确不重新设计店铺角色、代理商资金概况、提现、佣金、钱包流水等独立路由的权限,也不改变认证机制或其他账号类型的数据范围。
- [ ] 企业账号访问店铺列表、创建、更新、删除或联级查询时,统一收到 HTTP 403、`code=1005``msg=无权限访问店铺管理功能``data=null` 和时间戳。
- [ ] 企业账号在进入核心 Handler 和业务查询前被拒绝,不能因没有代理店铺范围而获得全局店铺数据。
- [ ] 超级管理员和平台账号继续拥有原有核心店铺管理能力;代理账号继续遵循本店铺及全部下级店铺的数据范围。
- [ ] 访问限制只作用于五个核心店铺管理路由,不误拦截店铺角色、代理商资金概况、提现、佣金、钱包流水等独立路由。
- [ ] 真实 HTTP 集成测试覆盖四类账号的核心列表行为,以及企业账号访问其余四个核心路由的 403 行为。
- [ ] 回归测试证明独立 `/shops` 前缀路由仍由各自已有权限规则处理,未因路由分组调整产生权限漂移。
- [ ] 成功和拒绝响应均保持统一 `code``msg``data``timestamp` 外层结构,认证失败仍沿用既有认证错误契约。
- [ ] OpenAPI 或对应接口说明准确记录核心店铺管理的账号权限边界,不改变店铺角色、资金、佣金、提现和钱包流水的既有契约。
- [ ] UR#60 中文总结及 README 索引记录本切片的五个受限入口、未受影响路由、四类账号验证证据和明确不重新设计的授权范围。
- [ ] 运行本切片目标测试、格式化、静态检查和相关构建,确认权限切片能够独立发布和回滚。

View File

@@ -0,0 +1,21 @@
# 01 — 资产详情展示可控前代换货信息
**What to build:** 在统一资产详情中增加稳定的 `exchange_trace` 对象,并打通当前资产作为换货新资产时的前代查询。关系以未软删除的已完成换货单为权威来源,展示旧资产的不可变标识快照和换货单号;关联资产仍在调用方现有数据权限内时返回真实 ID 并允许跳转,无权限时保留历史文本但隐藏 ID。主架构通道为 Query数据库索引为辅助 Infrastructure完整边界止于“当前资产 → 前代”的只读投影,不迁移换货状态机、换货写侧或资产详情的其他旧读取逻辑,也不新建关系表。
**Blocked by:** None — can start immediately
**Status:** ready-for-agent
- [ ] 卡和设备通过统一资产详情查询时始终返回 `exchange_trace` 对象;没有前代时 `previous_asset``null`,不省略整个对象。
- [ ] 当前资产作为新资产出现在未软删除且状态为已完成的换货单中时,`previous_asset` 返回旧资产类型、换货单快照标识、换货单号、可空资产 ID 和可见性标记。
- [ ] 前代标识直接使用换货单保存的不可变快照,不回查或改写关联资产的当前标识。
- [ ] 关联前代资产通过现有卡或设备数据权限检查;有权限时返回真实 ID 和 `can_view=true`,无权限时返回 `asset_id=null``can_view=false`,且不返回店铺、客户、套餐、钱包、状态等额外信息。
- [ ] 关系查询不应用关联资产的数据权限过滤,不会因关联资产不可见而抹掉换货关系;当前资产本身仍先经过现有权限校验,未授权请求维持原有防枚举响应。
- [ ] 待填写、待发货、已发货待确认、已取消及软删除换货单均不会形成前代关系。
- [ ] 查询存储故障返回统一脱敏内部错误,不会被降级为“无换货关系”,并记录包含查询方向和当前资产上下文的中文错误日志。
- [ ] 增加已完成且未软删除范围内支持“新资产类型 + 新资产 ID”最新记录查询的非唯一部分 B-tree 索引,并验证向上迁移、向下迁移和代表性查询计划。
- [ ] 同一资产存在多条“当前资产为新资产”的异常完成记录时,按 `completed_at` 降序、主键降序确定性选择最新一条,并记录包含当前资产、候选数量和最终换货单的中文异常日志。
- [ ] Query 测试覆盖无关系、卡前代、设备前代、可见前代、不可见前代、非完成状态、软删除记录和异常重复完成记录。
- [ ] HTTP 集成测试使用真实 PostgreSQL、Redis、JWT 和认证中间件,贯穿当前资产权限、资产解析、前代 Query 与统一响应格式,并证明当前资产无权限时不会因换货关系泄露其存在。
- [ ] 更新资产详情 OpenAPI 的前代响应契约,明确 `exchange_trace` 始终存在、`previous_asset` 可为 `null`,以及 `can_view` 与可空 `asset_id` 的跳转规则。
- [ ] 在 UR#86 中文总结中记录前代查询的架构通道、权限防泄露、异常选择规则、索引发布与回滚方式,并准备无换货、前代可见和前代不可见的人工验收步骤。

View File

@@ -0,0 +1,23 @@
# 02 — 资产详情展示后代并形成双向换货链
**What to build:** 扩展统一资产详情换货投影,打通当前资产作为换货旧资产时的后代查询,使连续换货 A→B→C 中的中间资产 B 可以同时展示前代 A 和后代 C。后代同样使用换货单不可变快照并按关联资产当前权限决定是否返回可跳转 ID。主架构通道为 Query数据库索引为辅助 Infrastructure完整边界止于单节点前代和后代的固定次数查询不递归整条换货树、不新增换货链列表接口、不改变资产权限矩阵。
**Blocked by:** `.scratch/ur86-asset-exchange-trace/issues/01-asset-previous-exchange-trace.md` — 01 — 资产详情展示可控前代换货信息
**Status:** ready-for-agent
- [ ] 当前资产作为旧资产出现在未软删除且状态为已完成的换货单中时,`next_asset` 返回新资产类型、换货单快照标识、换货单号、可空资产 ID 和可见性标记;没有后代时为 `null`
- [ ] 卡和设备共用同一套 Query 投影逻辑,不为两种资产复制换货链查询流程。
- [ ] 后代资产有权限时返回真实 ID 和 `can_view=true`;无权限时保留快照标识及换货单号、返回 `asset_id=null``can_view=false`,不泄露其他业务信息。
- [ ] A→B→C 场景查询 B 时同时返回 A 和 C`previous_asset``next_asset` 互不覆盖;查询 A 和 C 时分别只返回存在的方向。
- [ ] 待填写、待发货、已发货待确认、已取消及软删除换货单均不会形成后代关系。
- [ ] 增加已完成且未软删除范围内支持“旧资产类型 + 旧资产 ID”最新记录查询的非唯一部分 B-tree 索引,并验证向上迁移、向下迁移和代表性查询计划。
- [ ] 前代和后代的关联资产可见性采用固定次数批量加载;查询次数不随设备绑定卡数量或其他列表数据增长,不产生 N+1。
- [ ] 同一资产存在多条“当前资产为旧资产”的异常完成记录时,按 `completed_at` 降序、主键降序确定性选择最新一条,并记录包含当前资产、候选数量和最终换货单的中文异常日志。
- [ ] Query 测试覆盖仅后代、A→B→C 中间资产、卡链、设备链、关联后代可见与不可见、非完成状态、软删除记录和异常重复完成记录。
- [ ] HTTP 集成测试使用真实 PostgreSQL、Redis、JWT 和认证中间件,贯穿当前资产权限、资产解析、双向换货 Query 与统一响应格式,覆盖无换货、仅前代、仅后代、中间资产及关联资产不可见场景。
- [ ] 使用代表性 PostgreSQL 数据验证两条部分索引的结构和查询计划,资产详情增加双向换货投影后仍满足项目数据库查询与 API 性能目标,且无 N+1。
- [ ] 完成资产详情 OpenAPI 双向契约,明确 `previous_asset``next_asset` 的空值语义,以及仅在 `can_view=true``asset_id` 非空时允许跳转。
- [ ] 完成 UR#86 中文总结并更新 README 索引,记录完整权限边界、异常选择规则、发布顺序和回滚方式;上线前抽样核验历史新旧资产 ID 与快照完整性,只记录异常、不回填数据。
- [ ] 前端人工验收覆盖无换货、换货新资产、已换出旧资产和 A→B→C 中间资产;无权限关联项只显示快照标识和换货单号,不渲染链接或可点击样式。
- [ ] 与 UR#45、UR#98 同窗发布时,仅将已实际完成的对应 Ticket 纳入发布前置检查;若形成跨 PRD 实施阻塞,必须先引用其已发布的具体 issue 文件路径和标题更新本票,不能使用模糊依赖描述。

View File

@@ -90,6 +90,18 @@ handlers := &bootstrap.Handlers{
4. 当前需求触碰复杂只读逻辑时,只迁移该查询到 `internal/query/<context>`Query 可直接使用 GORM 做联表、聚合、权限过滤和 DTO 投影。 4. 当前需求触碰复杂只读逻辑时,只迁移该查询到 `internal/query/<context>`Query 可直接使用 GORM 做联表、聚合、权限过滤和 DTO 投影。
5. 不为追求 DDD 形式创建无业务价值的接口、工厂和目录;架构选择不明确时先阅读 DDD 规范并写明判断依据。 5. 不为追求 DDD 形式创建无业务价值的接口、工厂和目录;架构选择不明确时先阅读 DDD 规范并写明判断依据。
### 规划与任务拆分约束
创建或修改 PRD、OpenSpec proposal/design/tasks、实施计划或本地 Issue/Ticket 前,必须完整阅读 `docs/7月迭代/独立方案/基础规范/DDD规范.md`,并遵守以下规则:
1. 每个可实施单元必须标明适用的架构通道复杂写、简单写、Query、Infrastructure 或 Application + Port/Adapter同一纵向切片涉及多条通道时标明主通道和辅助通道。
2. 每个任务必须说明其收口的完整业务边界及明确不迁移的旧代码范围,禁止借任务拆分扩大为模块级或全仓重构。
3. Ticket 必须按可独立验证的纵向切片拆分,不得按“先建表、再 Service、再 Handler”生成水平分层任务宽范围机械迁移按 expandmigratecontract 拆分并保持各阶段可验证。
4. 复杂写的不变量、状态机、金额、并发和可靠事件必须完整进入 Domain/Application 边界;简单写不得强行创建聚合;读取不得经过聚合根或修改状态。
5. 已评审 PRD 或 OpenSpec 已确定的架构选择是实施契约,拆票和实现阶段不得自行改变;确需调整时必须先说明影响并获得用户确认。
6. 跨 PRD 或跨 Change 的依赖必须引用具体任务或 Ticket禁止只写“依赖公共基础设施”等无法判定完成状态的模糊阻塞项。
7. 任务正文只记录本切片适用的架构约束,不机械复制整份 DDD 规范;实现和评审仍以本文件及完整 DDD 规范为准。
## 核心原则 ## 核心原则
### 错误处理 ### 错误处理