大报表导出:从 CSV 按钮到异步规格包

这个案例把模糊导出需求改写成有权限、job 状态、文件过期和评审证据的后台任务流程。

风险超时、数据泄露、队列压力
边界只做 CSV 导出,不重做报表
证据Job 测试、耗时、权限检查

规格前的需求长什么样

弱工单

给 orders report 加 CSV 导出。
大客户也要能用,导出完成后邮件通知。

Spec-first 改写

Feature: 异步 orders report CSV 导出
Owner: Reporting
Status: Draft for review

目标:
- 允许 account admin 将当前 orders report 导出为 CSV。
- 超过 10,000 行的数据集走后台 job。
- 文件准备好后通知发起用户。

非目标:
- 不重做 report filter。
- 不新增分析列。
- 不支持跨 account 导出。
- 不做实时 streaming download。

评审者真正需要的规格包

spec.md

约束:
- 导出使用页面上可见的相同 filters。
- 发起用户必须属于该 account。
- 文件 7 天后过期。
- CSV 列与当前 report table 一致。

风险:
- 长查询压力
- filter snapshot 过期
- 下载链接被分享给 account 外用户

tasks.md

- [ ] 捕获 report filter snapshot。
- [ ] 创建 export_jobs 表和状态机。
- [ ] 增加 CSV 生成 worker。
- [ ] 增加带过期时间的 signed download URL。
- [ ] 增加 admin UI 状态:queued、running、ready、failed。
- [ ] 增加队列和失败指标。

acceptance-criteria.md

- Given admin 筛选 orders
  When 请求导出
  Then job 使用相同 filter snapshot。

- Given 非 admin 用户
  When 请求或下载 export
  Then 访问被拒绝。

- Given 50 万行 report
  When export 运行
  Then web request 快速返回,worker 异步完成。

evidence.md

自动化:
- export permission test
- filter snapshot test
- job state transition test
- expired download URL test

运维:
- 50 万行耗时结果
- queue depth dashboard
- failed job alert link

规格需要保护的异步状态

状态允许转移评审证据
queued权限和 filter snapshot 校验后创建。测试证明 job 存储 account id、user id 和 filters。
runningWorker claim 一个 job,并写入进度 metadata。测试证明两个 worker 不能处理同一 job。
readyCSV 已存储,并生成 signed URL。证据证明 URL 会过期,并检查 account 权限。
failedWorker 记录安全错误,用户可以重试。PR 中链接 alert 和 failed-job metric。

评审走查

弱需求说的是“加 CSV 导出”,但真正的风险不是按钮,而是按钮之后发生什么:长查询阻塞 Web 请求、用户在 job 运行时修改筛选条件、文件链接在用户失去权限后仍可访问,或后台失败无人发现。

规格包把导出变成状态机,而不是一个 UI 装饰。后台任务和普通请求的失败位置不同,所以评审者需要在合并前看到权限检查、filter snapshot、文件过期、队列监控和失败路径。

权限检查

请求和下载都要确认用户属于该 account。只在请求时检查一次不够。

Snapshot 检查

导出的 CSV 应匹配用户点击导出时可见的筛选条件,而不是后续编辑后的条件。

运维检查

队列深度、失败任务、重试率和 worker 耗时,应该在上线前可见。

如何复用这个案例

导出、导入、批量操作、对账任务、账单运行、数据回填,以及任何“用户发起、稍后完成”的流程,都可以使用这个模式。可复用的是状态机和证据清单,而不是具体业务领域。

对于小导出,10,000 行阈值可以调低,也可能不需要邮件通知。但不应该消失的是 Web 请求和 worker 的边界、下载时权限检查,以及大客户不会拖慢主应用的证据。

让 AI 编码助手实现这类工作时,不要允许它顺手引入队列库、重做报表或添加额外字段。那些都是独立决策,需要另一份规格。第一版规格包只保护安全导出行为。

上线观察计划

报表导出不能因为 staging 下载成功一次就算完成。第一版上线时应该有观察计划,因为真实账号规模、真实筛选条件和真实重试行为,往往才会暴露问题。规格里应写清首日观察什么信号,以及信号异常时采取什么动作。

这个案例里,最有价值的信号包括 queue depth、worker duration、failed job count、download permission failure,以及客服是否收到“少列、错列、筛选条件不一致”的反馈。回滚动作也要具体:关闭导出按钮、暂停 worker,或保留已完成文件但阻止新请求。

还要提前写清 owner。工程负责人看 worker 和数据库压力,产品负责人确认导出列和筛选语义,客服负责人收集用户反馈。没有 owner 的观察计划很容易变成“上线后看看”,最后没人真正看。

队列信号

队列深度和 worker 耗时能证明导出没有拖慢其他后台任务。

访问控制信号

权限失败和过期链接事件能证明下载安全逻辑被真实触发。

客服信号

少列、错列或筛选条件不一致的反馈,说明导出文件没有匹配用户预期。

职责分工也要写进规格

异步导出跨越产品、后端、前端、运维和客服。如果规格只写实现任务,不写谁负责判断结果,评审会在最后一刻才发现缺口。一个可执行的导出规格应明确:谁确认字段顺序,谁验证权限,谁观察队列,谁处理失败 job,谁决定是否关闭入口。

这个职责分工不需要复杂流程,但必须具体。比如“Reporting owner 在首日每两小时查看 failed job 仪表盘”,“Support owner 记录导出文件缺列反馈”,“Engineering owner 在 queue depth 超过阈值时暂停 worker”。这些句子比“上线后监控”更有价值,因为它们能真正触发行动。

应该拒绝的反模式

在 Web 请求里直接导出

大客户会遇到超时或数据库压力。长任务应异步执行。

下载链接没有权限保护

Signed URL 仍需要 account-aware 访问规则,或短过期时间和安全再生成路径。

没有 failed-job 路径

用户和客服需要知道导出是失败、可重试,还是需要工程介入。

添加后台导出前先用这个模式

先生成规格包,再让 PR 证明 job 状态、权限和运维证据都已经准备好。

编辑说明

这个案例关注异步任务:规格的价值在于让后台状态、权限和运维证据在实现前可见。