- 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 中相应引用改为留痕表述
6.7 KiB
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 已示例)
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、测试文件。
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)。
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.methodimport——以实际 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, ...)。
- scene 不在
- 定义
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)
POST /api/v1/uploads/presign(含/v1别名)可用,认证必需;- 类型/大小/场景三重服务端校验,错误码统一 40010;
- key 布局可审计(scene/user/日期/uuid);
- 单测全绿且无任何真实网络调用(FakeSigner 注入);
- 全部检查绿(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)。
偏差说明:<如有写在这里,无则写"无">