Files
cmroubao/docs/tasks/T-203.md
T

163 lines
9.1 KiB
Markdown
Raw Normal View History

---
id: T-203
title: 实现任务创建 API 与管理 Web
phase: 2
deps:
- T-201
- T-202
status: DONE
created: 2026-07-26
context_ref: 2b265c9
work_branch: main
write_paths:
- backend-api/**
- init.ps1
- init.sh
- docs/00-ai-start-here.md
- docs/02-requirements.md
- docs/03-tech-stack.md
- docs/04-architecture.md
- docs/05-coding-rules.md
- docs/07-user-stories.md
- docs/08-interaction-checklist.md
- docs/api.md
- docs/current-state.md
- docs/design/admin-task-create.html
- docs/design/admin-task-detail.html
- docs/design/android-task-preview.html
- docs/design/android-candidate-confirm.html
- docs/tasks/T-203.md
- progress.md
---
## 问题 / 背景
T-201 只有健康检查、SQLite 和 migration 骨架,T-202 只确认了页面信息结构。当前还
不能从管理端安全上传参考图、创建 `PENDING` 任务、查看真实列表/详情或取消待领取
任务,因此 T-205 的原子领取没有可消费的数据。
审计还发现正式合约存在四个会阻断后续 App 接入的问题:
- 现有 Android `ProbeTask` 强制标题和 SKU 非空,但任务创建 API/数据表遗漏 SKU。
- 已确认 Web 使用“最高总预算”,部分 Android 原型误写成每件预算。
- API/原型允许 JPEG/PNG/WebP,但 Android 需求提取当前只接受 JPEG。
- 管理会话属于 T-204,T-203 不能伪造登录或永久开放匿名管理接口。
## 关联需求与交互
- 功能:F-001、F-002,以及 F-007 的最小可见错误。
- 用户故事:US-001、US-002。
- 交互:IX-002、IX-003;登录 IX-001 留给 T-204。
- 页面:`/tasks`、`/tasks/new`、`/tasks/{id}`。
- API:资产上传、任务创建/列表/详情、待领取任务取消。
## 已定合约
1. 标题和 SKU 必填;描述可空。标题最多 120 个 Unicode 字符且不超过 2048 个
UTF-8 字节,SKU 不超过 512 个 UTF-8 字节,描述不超过 8192 个 UTF-8 字节。
2. `max_budget` 表示当前任务全部数量的最高商品总预算,币种 CNY,不含无法确认的
运费或优惠;数据库用整数分保存,API 固定输出两位小数字符串。
3. 参考图上传接受可解码 JPEG/PNG/WebP,原始请求最多 20 MiB、最长边最多
10000 px、总像素最多 25 MP;后端统一缩放到最长边不超过 2048 px、白底合成并
编码为匿名 JPEG。响应的媒体类型、大小和 SHA-256 都指向规范化结果。
4. SKU、数量和总预算属于不可变原始事实,任何派生结果都不能覆盖。
5. 列表增加 `q`,匹配任务 ID、来源引用、标题或 SKU;分页固定按
`created_at DESC, id DESC`,cursor 同时包含两个字段。
6. T-203 只提前实现事务化 `PENDING -> CANCELED`;其他状态取消返回冲突,完整取消
请求和状态机属于 T-205。
7. T-204 前,管理 Web/API 业务路由只接受 loopback 来源;Web 写操作还必须携带
SameSite Cookie 与表单字段匹配的随机双提交 CSRF token,API 创建/上传必须使用
JSON 或 multipart 与自定义幂等头。`/login` 不提供假实现,局域网部署仍被明确
阻塞。
8. 创建、上传均按调用主体、操作和 `Idempotency-Key` 幂等;同 key 同请求返回原
资源,同 key 不同请求返回 `409`。
## 方案
1. 增加 assets、purchase_tasks、task_events 和 idempotency_records migration,
包含外键、CHECK、唯一约束和 rollback。
2. 建立 domain/usecase/repository/sqlite 分层;handler 和模板不得直接执行 SQL。
3. 建立受控文件存储与图片规范化器,随机相对路径、同目录临时文件原子改名,失败时
清理;数据库和响应永不暴露绝对路径。
4. 实现 JSON/multipart API 和稳定错误映射,保持无默认 CORS、request ID 与
recovery 安全约束。
5. 用 `html/template`、`embed`、原生 CSS/JS 实现列表、新建和详情;真实数据为空时
显示空状态,不把原型假数据写入数据库。
6. Web 表单无 JS 也可完成,使用 PRG `303`;JS 只负责图片预览、防重复、离开提醒
和取消确认。
## 验收要点
- [x] migration 可 up/down/up,约束和外键有效,启动前 migration 门禁明确。
- [x] JPEG/PNG/WebP 被真实解码并规范化为 JPEG;伪 MIME、损坏、超限和像素炸弹拒绝。
- [x] 合法任务创建为唯一 `PENDING`,原始 title/SKU/quantity/budget/image 不可变。
- [x] 上传和创建幂等;同 key 不同请求冲突,并发重试不产生重复资源。
- [x] 列表筛选、搜索、稳定 cursor 和详情返回真实数据,404 不泄露内部信息。
- [x] 只有 `PENDING` 可立即取消;终态或不支持状态返回稳定冲突。
- [x] Web 覆盖空列表、创建字段错误、图片错误、成功跳转、详情和取消二次确认。
- [x] 非 loopback 业务请求、缺失/错误 CSRF、错误 Content-Type 被拒绝。
- [x] HTML 自动转义,日志/响应不含 Cookie、文件绝对路径、私有样本或请求正文。
- [x] Go test/race/vet/gofmt、迁移 smoke、真实 HTTP/Web smoke 和 Playwright 通过。
## 边界
- 不实现管理登录、Cookie 会话、用户表/密码哈希或设备鉴权;属于 T-204。
- 不实现 claim、租约、执行状态机、候选、执行证据或 App HTTP TaskSource。
- 不开放非 loopback 管理使用;T-204 完成前不能部署到局域网供其他用户访问。
- 不实现对象存储、ORM、SPA、WebSocket、推送或自动下单。
- 不提交运行时数据库、上传文件、密钥或真实订单/店铺/商品数据。
## 执行记录
### 2026-07-26:任务开始
- 基于提交 `2b265c9` 开始,工作区干净。
- codebase-memory MCP 本轮未暴露 graph 工具,按项目规则回退到 `rg` 和定点文件读取。
- 已审计 T-201 现有 config/database/migration/httpapi/cmd 骨架、T-202 原型以及
requirements/API/architecture/IX,先固定上述跨层合约再编码。
### 2026-07-26:合约和实现
- 修正正式需求和原型中的 SKU、最高总预算、图片格式与验收编号;总预算统一指当前
任务全部数量的商品预算,模型不得覆盖 SKU、数量或预算。
- 新增 `00002_tasks_and_assets.sql`,建立 assets、purchase_tasks、task_events 和
idempotency_records;字段长度、UTF-8 字节、外键、状态、金额和唯一性均有数据库
约束。
- 建立 domain/usecase/repository/sqlite 分层,上传和创建事务化幂等,列表按
`created_at DESC, id DESC` 稳定分页,待领取取消使用状态与 version 条件更新。
- 文件层真实解码 JPEG/PNG/WebP,限制 20 MiB、10000 px 和 25 MP,白底缩放至最长
边 2048 px 后以质量 90 编码为随机相对键 JPEG;绝对路径不进入数据库外部响应。
- Gin API 实现上传/读取资产、创建/列表/详情/取消任务;业务路由在 T-204 前只接受
loopback,错误使用稳定 code、request ID 和 no-store 响应。
- 管理 Web 使用嵌入模板/CSS/JS,实现真实空列表、搜索筛选、创建、图片预览与复用、
详情参考图和取消确认;使用双提交 CSRF、PRG 303、HTML 自动转义和固定 CSP。
- API 启动前检查全部 migration;存在 pending 版本时拒绝启动并明确提示执行
`migrate up`,服务进程不自动修改数据库。
### 2026-07-26:自动验证
- `GOTOOLCHAIN=local go test -count=1 ./...`:99 个测试通过;覆盖领域校验、金额、
图片限制、路径防护、迁移约束、事务幂等、并发、分页、HTTP 生命周期、CSRF 和
模板安全。
- `go test -race -count=1 ./...`、`go vet ./...`、`gofmt -l cmd internal migrations`
和 API/migration Windows 构建全部通过。
- 真实 migration CLI 完成 `status -> up -> up -> down -> API gate -> up`:首次
`applied=2`、重复为 0,回滚 v2 后 API 以退出码 1 拒绝旧库,恢复后 v1/v2 均
`applied`。
- `httptest` 使用临时真实 SQLite/asset 目录跑通:上传及幂等重放、创建及重放、SKU
搜索、详情、JPEG 内容读取、取消和重复取消;非 loopback 和缺失幂等键被拒绝。
- Playwright 连接真实 Gin/SQLite 进程完成创建任务和详情读取;1440x900、
390x844、360x800 均无横向溢出,可见操作控件最小 44 px,规范化参考图
`naturalWidth=64`、`naturalHeight=48` 且成功加载,只有同源 HTML/CSS/JS/资产请求,
新页面 console 为 0 error、0 warning;取消对话框默认聚焦“保留任务”。
- 根 `init.ps1` 通过:Android `test assembleDebug` 共 76 个 Gradle task 成功,
现有 166 次测试保持通过;随后 Go 测试、vet、格式检查和两个入口构建再次通过。
### 2026-07-26:隐私与收尾
- 测试只使用代码生成的脱敏小图、标题和 SKU;未读取、复制或提交本地蝦皮订单、
店铺名、真实商品文字或图片。
- Playwright、数据库、资产和 exe smoke 产物只保存在被忽略的 `.local/`、`bin/`
或 `var/`,未纳入 Git;业务响应和管理页面不显示客户端文件名或服务端存储路径。
- T-203 验收全部满足,状态改为 `DONE`;T-204 可开始实现正式管理会话和角色门禁。