Files
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

6.7 KiB
Raw Permalink Blame History

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、测试文件。

  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)。

偏差说明:<如有写在这里,无则写"无">