Files
meida/docs/开发任务包/T5-上传预签名.md
T
xsl e511b6a66f 收尾两批并行开发的文档回写与任务包
- Sprint 执行计划 v1.1:§1.1 补第一批交付实据;T9 SPIKE 结论回写 Sprint 1 表
  (T-P0-16 通过 2.8ms;T-P0-12 有条件;T-P0-10 原型 6/6 + 真实照片重验前置);
  Sprint 2 表标注 T-P2-01~06 交付;新增 §9 两批交付记录
- 开发任务拆解 v1.5:交付回写 + 新风险登记(RTMPose 域差异、BiRefNet 异步化)
- 新增 开发任务包/:三批多 agent 并行的协作规程与 14 份任务契约
- 新增 前置开通清单-云资源与资质.md
- 清理已用毕的评审报告(文档评审 2026-08-13、项目整体评审 2026-08-14),
  README 中相应引用改为留痕表述
2026-08-14 21:07:20 +08:00

105 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T5 · 图片直传预签名 URL
> 对应任务:`T-P1-27` | 需求:`FR-SYS-03`、架构 §9.3 | 分支:`task/t5-upload-presign` | worktree:`/home/xsl/meida_backend-t5`
> 先读《README.md(协作规程)》,遵守全部纪律。
## 1. 目标
实现 `POST /uploads/presign`:为客户端签发 COS 临时桶的 PUT 预签名 URL,**图片不经应用服务器**(FR-SYS-03 验收原文)。本任务只做「签发」,不做上传回调/校验图片内容(那是报告链路 T-P3 的事)。
## 2. 已就绪的契约(协调提交已提供)
### 依赖(协调提交已 `uv add`,禁改 pyproject/uv.lock)
`cos-python-sdk-v5`(腾讯云 COS 官方 SDK,模块名 `qcloud_cos`)。mypy 对它的缺失 stub 已在协调提交里配好 override。真实调用只需要 `CosS3Config` 与 `CosS3Client.get_presigned_url` / `get_presigned_download_url` 所在的 `CosS3Client`。
### 配置(`config.py` 已加字段,`.env.example` 已示例)
```python
cos_region: str = "ap-shanghai" # env: COS_REGION
cos_secret_id: str = "" # env: COS_SECRET_ID
cos_secret_key: str = "" # env: COS_SECRET_KEY
cos_bucket_temp: str = "meida-temp" # env: COS_BUCKET_TEMP(临时桶,2h 生命周期 CR-02)
cos_bucket_user: str = "meida-user" # env: COS_BUCKET_USER
cos_bucket_kb: str = "meida-kb" # env: COS_BUCKET_KB
cos_bucket_report: str = "meida-report" # env: COS_BUCKET_REPORT
cos_presign_expire_seconds: int = 600 # env: COS_PRESIGN_EXPIRE_SECONDS
upload_max_bytes: int = 20_971_520 # env: UPLOAD_MAX_BYTES(20MB)
upload_allowed_types: str = "image/jpeg,image/png,image/webp" # env: UPLOAD_ALLOWED_TYPES
upload_scenes: dict[str, dict[str, Any]] = Field(..., alias="UPLOAD_SCENES_JSON")
# 默认:{"report_photo": {"bucket": "temp"}, "avatar": {"bucket": "temp"}}
```
(字段名/别名以 config.py 协调提交的实际代码为准——**开工先读 config.py 核对**,文档若有出入以代码为准。)
### 错误码(`errors.py` 已加)
- `UPLOAD_REJECTED = 40010`:400「不支持的图片类型或超出大小限制」,hint「请上传 20MB 以内的 JPG/PNG/WebP 图片」。
### 骨架(协调提交已建)
- `packages/storage/__init__.py`、`README.md`、`service.py`(空骨架);
- `apps/api/routers/uploads.py`(空骨架 router,prefix `/uploads`,已注册 main.py);
- `apps/api/deps.py` 已有 `StorageServiceDep`(**deps.py 禁改**;若构造参数不满足,在路由函数内用 `SettingsDep` 自建 service)。
### 分层位置
`packages.storage` 在 import-linter"共享能力层"。只准 import `packages.shared`/`packages.domain` 与 `qcloud_cos`/stdlib。
## 3. 实现要点
允许改动:`packages/storage/**`、`apps/api/routers/uploads.py`、测试文件。
1. **`packages/storage/schemas.py`**:
- `PresignRequest(scene: str, content_type: str, size_bytes: int)`(size ≥1);
- `PresignResponse(upload_url: str, object_key: str, expires_in: int, max_bytes: int, content_type: str)`。
2. **`packages/storage/service.py`**:
- 定义 `Protocol CosSigner`:`def presign_put(bucket: str, key: str, expires: int, content_type: str) -> str`(**把 SDK 隔离在 Protocol 后面**,单测用 FakeSigner,绝不连网);
- `QcloudCosSigner`:包 `CosS3Client`(构造参数 region/secret_id/secret_key),`presign_put` 调 `client.get_presigned_url(Method.PUT, bucket, key, Expire=expires, Headers={"Content-Type": content_type})`(`from qcloud_cos import CosConfig, CosS3Client`;`Method` 从 `qcloud_cos.method` import——以实际 SDK 签名为准,若 Headers 参数名不同按 SDK 文档调整,**以能签出带 content-type 约束的 PUT URL 为准**);
- `StorageService(settings, signer: CosSigner | None = None)`:signer 为 None 时用 `QcloudCosSigner` 惰性构造(**只有真实请求才建 client**;测试必传 FakeSigner);
- `def build_presign(user_id: int, request: PresignRequest) -> PresignResponse`:
- scene 不在 `settings.upload_scenes` → `AppError(UPLOAD_REJECTED)`(message 写"不支持的上传场景");
- content_type ∉ `settings.upload_allowed_types` 解析集合 → `UPLOAD_REJECTED`;
- size_bytes > `upload_max_bytes` → `UPLOAD_REJECTED`;
- 扩展名映射 `{image/jpeg: jpg, image/png: png, image/webp: webp}`(其它类型已被上一条拦下);
- object_key = `f"{scene}/{user_id}/{datetime:%Y%m%d}/{uuid4().hex}.{ext}"`(**这个 key 布局写死**:按场景分目录、按用户隔离、按日分组、uuid 防猜——注释说明这是 COS 侧做前缀审计与生命周期的基础);
- bucket = scenes 配置里的 `bucket`("temp"/"user"/"kb"/"report" → 对应 `settings.cos_bucket_*` 四字段映射);
- 调 signer 得 URL,返回 `PresignResponse(expires_in=settings.cos_presign_expire_seconds, max_bytes=settings.upload_max_bytes, ...)`。
3. **`apps/api/routers/uploads.py`**:
- `POST /presign`,`user_id: CurrentUserId` + settings;`ok(PresignResponse(...))`;
- 模块 docstring 写明:真实联调需在 `.env` 配 COS 四件套;本地单测不依赖任何真实 COS。
## 4. 测试要求
单测 `tests/unit/test_upload_presign.py`(全部用 FakeSigner,断言 URL/key/参数,**零网络**):
- 合法请求 → key 布局正确(scene 前缀、user_id 段、日期段、扩展名)、expires_in/max_bytes 回显;
- 非法 content_type(如 `application/pdf`)→ 40010;
- 超大小 → 40010;
- 未知 scene → 40010;
- bucket 映射:`{"bucket":"temp"}` → 使用 `cos_bucket_temp` 配置值(FakeSigner 捕获 bucket 参数断言);
- `upload_allowed_types` 字符串解析(空串/带空格容错)。
不需要集成测试(无 DB 依赖)。
## 5. 禁改清单(通用禁改之外)
`packages/llm/**`、`packages/billing/**`、`packages/identity/**`、`packages/analytics/**`、`apps/api/routers/{agent,auth,quota,tasks,events}.py`、`apps/api/deps.py`、`tests/integration/**`。
## 6. 验收标准(FR-SYS-03)
1. `POST /api/v1/uploads/presign`(含 `/v1` 别名)可用,认证必需;
2. 类型/大小/场景三重服务端校验,错误码统一 40010;
3. key 布局可审计(scene/user/日期/uuid);
4. 单测全绿且无任何真实网络调用(FakeSigner 注入);
5. 全部检查绿(mypy 范围同 T2 列表)。
## 7. 提交信息
```
feat(storage): COS presigned PUT URL endpoint for direct upload
POST /uploads/presign:场景/类型/大小三重校验 + 可审计 object_key 布局 +
SDK 隔离在 CosSigner Protocol 后(FR-SYS-03,T-P1-27)。
偏差说明:<如有写在这里,无则写"无">
```