diff --git a/docs/tasks/T-103.md b/docs/tasks/T-103.md index 2bee797..03daa48 100644 --- a/docs/tasks/T-103.md +++ b/docs/tasks/T-103.md @@ -19,10 +19,6 @@ write_paths: - client/scripts/capture_sku_panel_spike.py - client/scripts/run_t103_sku_selection.py - client/scripts/sanitize_sku_panel_evidence.py - - docs/02-requirements.md - - docs/03-tech-stack.md - - docs/04-architecture.md - - docs/api.md - docs/current-state.md --- diff --git a/docs/tasks/T-204.md b/docs/tasks/T-204.md new file mode 100644 index 0000000..4ff27a0 --- /dev/null +++ b/docs/tasks/T-204.md @@ -0,0 +1,100 @@ +--- +id: T-204 +title: 路由化任务详情与内部截图资产 +phase: 2 +deps: [T-203] +status: TODO +created: 2026-08-04 +vikunja_task_id: 31 +context_ref: 20aaba0 +work_branch: task/t-204-details-evidence +needs_device: false +needs_human_review: false +write_paths: + - docs/tasks/T-204.md + - admin/migrations/00003_evidence_assets.sql + - admin/internal/migrations/migrations_test.go + - admin/internal/evidence/** + - admin/internal/storage/evidence/** + - admin/internal/taskdetail/** + - admin/internal/config/** + - admin/internal/server/** + - admin/internal/transport/webui/** + - admin/cmd/server/** + - admin/README.md + - docs/api.md + - docs/routes.md + - docs/04-architecture.md +--- + + +## 问题 / 背景 + +T-203 已完成传统任务表格与批量“开始采购”授权。采购管理员还缺少可复制、可返回的任务详情,以及只供内部审计的规格面板/确认页原始截图。T-204 提供详情与截图资产底座;不实现 T-205 的 attempt/event 写入,不实现 T-306 的客户端上传调用。 + +## 关联需求与交互 + +- 功能:F-007、F-011。 +- 用户故事:US-002、US-004、US-007。 +- 交互:IX-003、IX-004;双击非控件区域或 Enter 打开同一 `/tasks/{id}`,直达显示完整页,列表增强为抽屉。 +- 架构/API:`GET /tasks/{id}`、`POST /api/v1/tasks/{id}/evidence`、`GET /evidence/{asset_id}`。 + +## 方案 + +1. 新增任务详情只读查询,返回任务要求、状态、版本、授权摘要和当前已有的 attempt/submission/evidence 摘要;缺少后续事实时显示明确空态,不伪造数据。 +2. `GET /tasks/{id}` 以同一数据模型渲染完整页或同源 HTML fragment。列表行双击非交互区域、键盘 Enter 打开抽屉并把 URL 推到同一路径;Esc、关闭或浏览器返回恢复筛选、滚动与触发行焦点。外部商品链接、复选框和批量按钮不得触发行详情。 +3. 详情是审计视图,不提供试选后确认、再次提交、付款自动化或围栏后重试。状态文案始终区分“订单已创建,系统尚未付款”。 +4. 新建 `evidence_assets` 迁移和存储边界:只接收明确的单个 PNG screenshot;attempt_id 必填且必须属于 URL 中的 task。元数据仅允许 upload_key、attempt_id、kind、privacy_tier=INTERNAL_RAW、sha256、captured_at;记录 MIME、字节数、宽高、存储键和上传主体。 +5. 上传先经注入的 DeviceAuthenticator;T-301 前生产默认拒绝,测试可用 fake principal 验证契约,不创建临时 token 或共享密钥。T-301 完成后再接真实 Bearer 身份。 +6. multipart 设严格总大小、字段/单文件限制;拒绝未知字段、重复字段、XML/manifest/目录/本机路径、非 PNG、魔数/DecodeConfig/尺寸/hash 不一致以及未批准的截图 kind。服务端文件名只由资产 id/存储键生成,不使用上传名。 +7. 文件同目录临时写、流式 SHA-256、校验、fsync、原子 rename 后才写 DB;故障只允许产生不可达孤儿,不允许 DB 指向缺失或半文件。upload_key 同任务同载荷幂等,载荷变化冲突,并发只生成一条可读资产。 +8. `GET /evidence/{asset_id}` 仅有效管理员会话可读,返回 `image/png`、`Cache-Control: no-store`、`X-Content-Type-Options: nosniff`;未登录不泄露资产是否存在,不经静态目录暴露。 +9. UI 采用现有采购服务高密度表格风格:可见关闭按钮、焦点圈、44px 交互目标、截图 width/height 预留和 lazy loading、响应式布局、reduced-motion;不得只有双击一种入口。 +10. T-204 明确定值单文件最大 10 MiB、解码后最大 16,777,216 像素;证据目录由显式配置提供。保留期限仍由后续部署任务定值,不在本任务自动删除资产。 + +## 验收要点 + +- 详情完整页与抽屉使用同一路由/数据;刷新/复制 URL、浏览器返回、Esc、焦点和滚动/筛选恢复均有测试。 +- 不存在、畸形 id、未登录、fragment 伪造请求均 fail closed,不泄露内部错误。 +- 上传覆盖认证矩阵、multipart 负例、PNG 魔数/解码/大小/尺寸/hash、未知/重复字段、任务/attempt 归属、路径穿越、幂等冲突/并发及原子故障注入。 +- 截图读取覆盖管理员会话、未登录、缺失资产和 no-store/nosniff;完整 XML、支付页和凭据不会进入接口或 Git。 +- 运行 Go 全量/竞态/vet/build、Node 测试与语法检查、完整 init、上下文校验和 diff-check。 + +## 执行记录 + +待认领。 + +## 执行记录 + +(暂无) + + +## 边界 + +- 本任务只实现管理员任务详情、内部 PNG 证据资产底座和受保护读取;不实现 T-205 的 attempt/event + 写接口、闸门计算、失败分类、提交围栏或结果调和,不实现 T-306 的客户端截图与上传调用。 +- T-301 完成前不得发明临时设备 token、共享密钥或把管理员会话当设备身份。上传 handler 必须先调用窄 + `DeviceAuthenticator`;生产默认实现一律拒绝,只有测试可以注入 fake principal。 +- 上传只接受一个调用方显式选择的 PNG;不得枚举目录,不接收 XML、manifest、本机路径、原文件名、 + 任意备注、Cookie、token、支付凭据或未批准的截图 kind。MVP 本任务只开放 + `SKU_PANEL_GATE_1`,后续 kind 必须在对应任务中收紧扩展。 +- `attempt_id` 必填且必须以复合外键证明属于 URL 中的 task。`privacy_tier` 只能是 + `INTERNAL_RAW`;截图可含页面已显示的地址/手机号,但服务端不得 OCR、提取、索引、搜索或写日志。 +- 单文件最多 10 MiB,单边最多 8192 px,解码后最多 16,777,216 像素;必须同时校验 multipart 类型、 + PNG 魔数、解码结果、字节数、尺寸与调用方声明的 64 位小写 SHA-256,任一不符零发布。 +- 文件必须在显式配置的证据根目录内以服务端生成的内容地址落盘,不进入公开静态目录。先同目录临时写、 + 流式 hash、`fsync`、原子 rename,再写数据库;故障最多留下不可达孤儿,不得留下指向半文件或缺失 + 文件的可见数据库记录,也不得为清理孤儿而删除并发复用文件。 +- 同一设备主体与 `upload_key` 的相同规范请求只返回原资产;内容、任务、attempt 或元数据变化一律 + `409`。不得把 SHA-256 当业务记录唯一键,因为不同合法证据可以复用相同物理内容。 +- `GET /evidence/{asset_id}` 只允许有效管理员会话,必须返回 `Cache-Control: no-store` 和 + `X-Content-Type-Options: nosniff`;匿名请求先拒绝,不泄露资产是否存在,不提供公开 URL、目录浏览、 + 批量导出、删除或预签名链接。 +- `/tasks/{id}` 的抽屉与完整页必须共享同一数据和详情模板。双击不是唯一入口;Enter、可见关闭按钮、 + Esc、浏览器前进/后退、焦点与列表滚动/筛选恢复均可用,外部商品链接、复选框、输入框和按钮不得误触 + 行详情。截图必须预留尺寸、响应式缩放、延迟加载,alt 不转录地址或手机号。 +- 详情是只读审计视图,不制造不存在的 attempt/event/闸门/提交事实,不出现“机器选对了吗”、围栏后 + 重试、再次提交或自动付款动作。截图不作为价格闸门通过或人工审批的唯一依据。 +- 本任务不实现、不调用通用真机点击、`submit_order_once()`、支付、免密支付、先用后付或任何扣款能力; + 既有三道价格闸门、服务端提交围栏、唯一点击一次且不重试的规则不得放宽。 +