Files
lixi f17e6f1215
CI / docs-build (push) Successful in 34s
docs: M3 第一波收口——报告 09~14 与契约草案入档挂导航
- 09 V5+community 骨架(191→206)、10 埋点队列三项(272→286)、
  11 契约草案(13 路径/19 操作)、12 防泄漏三仓落地、
  13 media MinIO 闭环 + auth 契约测试(→226,抓修 1 漂移)、14 收口总表
- backend-modules.md 更新五模块/六容器口径
- 媒体凭据形态已定型(契约冻结输入),剩余待定型点在 T3-04/05

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 17:13:49 +08:00

14 KiB
Raw Permalink Blame History

M3 第一波 media 域最小闭环施工报告(T3-03 MinIO 接入 + T3-19 auth 契约测试补齐)

作者:Senior Developer(后端) 日期:2026-09-08 工单:T3-03media 域最小闭环:对象存储接入与上传流程,M3 关键路径起点)、T3-19 后端半边(auth 域契约一致性测试补齐) 代码基线:patbond-api 8330885206 测试全绿)→ 交付 263cd88226 测试全绿) 结论先行:媒体凭据形态定型为「预签名 PUT 直传 + 预签名 GET 读取(桶保持私有)」;两步上传全链路(创建→直传→确认→ready→GET 可访问)在 MinIO Testcontainer 与 compose 六容器上实测通过;与契约草案偏差 7 项逐条记录(T3-10 冻结输入);auth 域 6 操作 19 个响应单元格全矩阵入契约测试;全套 226 测试全绿。


1. 提交清单

按工单各一逻辑提交,全部已推送 origin/dev

提交 内容
10a43f8 T3-03:存储适配层 + 两步上传流程 + MinIO 编排与全链路集成测试
263cd88 T3-19:auth 域 6 操作契约一致性测试全响应矩阵

2. 存储适配层设计(ADR-016 落地)

2.1 分层与供应商隔离

MediaController ─ MediaService ─┬─ MediaAssetRepositorymedia.assetsJdbcClient
                                └─ ObjectStorage(接口,媒体域唯一存储缝)
                                     └─ S3ObjectStorageAWS SDK v2,指向自托管 MinIO
  • ObjectStorage 接口patbond-user/src/main/java/com/patbond/patbond/user/media/ObjectStorage.java)只暴露四个供应商无关操作:ensureBucket() / presignPut(objectKey, contentType, ttl) / stat(objectKey) / presignGet(objectKey, ttl)。桶名、端点、凭证、SDK 类型全部收敛在实现内——迁云(COS 等 S3 兼容服务)只换 MediaProperties 配置与凭证,调用侧零改动(ADR-016 迁移触发条件见该 ADR)。
  • S3ObjectStorageAWS SDK v2software.amazon.awssdk:s3,版本 2.54.13 经根 pom awssdk bom 管理)。强制 path-style(MinIO 无桶级泛域名)。双端点设计:SDK 客户端走内网端点(compose 内 http://minio:9000),预签名 URL 按 public-endpoint(客户端可达地址)签发——SigV4 把 Host 签进签名,两者必须分开。
  • 未配置时的行为patbond.media.endpoint 为空时注入 UnconfiguredObjectStorage 桩,服务照常启动、仅 /api/v1/media/** 返回 500——与 JWT 公钥未配置的既有先例一致,保证 auth E2E 等不涉媒体的上下文零外部依赖。
  • 三环境零分叉:桶初始化是应用启动时的 ensureBucket()(幂等,headBucket→createBucket),本地、compose、Testcontainers 走同一条代码路径;MinIO 镜像三处钉同一 tag minio/minio:RELEASE.2025-04-22T22-12-26Zcompose 与集成测试)。

2.2 两步上传状态机(实现语义,冻结输入)

POST /api/v1/media/uploads          POST /api/v1/media/uploads/{assetId}/complete
        │                                   │
        ▼                                   ▼
  白名单校验(purpose/mime/byteSize)    findByIdAndOwner(不存在/非本人/deleted → 404/40405 防枚举合并)
        │                                   ├─ ready    → 200 幂等返回(现签 GET URL
  insert uploading 行                       ├─ failed   → 422/42205(终态,须重新创建上传)
  (objectKey 服务端生成:                   └─ uploading → HEAD 对象:
    {purpose}/{yyyy/MM}/{assetId}              ├─ 对象不存在 → 422/42205**保持 uploading 可重试**
    不含任何用户输入)                            ├─ 大小/类型与登记不符 → 置 failed422/42205
        │                                        └─ 通过 → uploading→readyguarded UPDATE
        ▼                                              并发确认幂等收敛),200 + GET URL
  201 + 预签名 PUT 凭据
  • ready 迁移用 UPDATE ... WHERE status='uploading' 守卫,并发 complete 竞争时输家重读终态、幂等返回,不会双写 ready_at
  • 库层 CHECKck_media_location/ck_media_ready/ck_media_status)与 uq_media_object 是应用校验的兜底,集成测试对三者逐一实证(见 §7)。

2.3 配置面(全部环境变量注入,ADR-021)

patbond.media.*application.yml(.sample),占位符形态):endpoint / public-endpoint / access-key / secret-key / bucket(默认 patbond-media/ upload-ttl(默认 10m/ download-ttl(默认 1h/ max-byte-size(默认 10485760/ allowed-mime-types(默认 jpeg/png/webp/ allowed-purposes(默认 post_image)。上限与白名单按工单要求全部是配置项,不是代码常量。

3. 媒体凭据形态定型表(T3-10 契约冻结输入)

POST /api/v1/media/uploads → 201data 形态:

字段 定型 说明
assetId UUID 字符串(应用侧 UUIDv7 已登记 assetstatus=uploading
uploadUrl 预签名 PUT 完整 URL 签名以 query 参数携带(X-Amz-Algorithm/-Credential/-Signature/...);指向 public-endpoint,客户端直传不经应用服务器
method "PUT"
requiredHeaders {"Content-Type": <声明的 mimeType>} 键集定型为仅此一键Content-Type 被签进签名,客户端必须原样携带,改动即 403
expiresAt ISO-8601 date-time 凭据过期时刻 = 签发时刻 + upload-ttl(默认 10 分钟);过期后重新创建上传(原 asset 仍可在补传后确认,见 §4-2)

确认/读取侧(MediaAsset.url):预签名 GET URL,TTL 默认 1 小时,仅 status='ready' 非空;桶保持私有,无签名直访 403(有测试)。消费方(T3-05 Feed、头像)由服务端在每次响应时现签,客户端不持久化 URL、过期即重取。

4. 与契约草案(openapi-community-draft.yaml)偏差清单

# 草案 实现定型 理由
1 读取侧留白(TODO-FREEZE:公共读稳定 URL vs 签名读;avatarUrl 示例为公共读形态,D3-1 拍板意见曾倾向公共读桶) 私有桶 + 预签名 GETTTL 1h 配置项) 任务拍板「桶保持私有」;公共读桶对越权枚举无防御,且迁云后改回私有是破坏性变更,反向(私有→放开)是兼容变更
2 complete「校验失败置 failed」一刀切 对象不存在 → 42205 但保持 uploading(可重试);对象存在但大小/类型与登记不符 → 置 failed(终态) 客户端直传完成前误触 complete 不应把凭据作废;「传了不符的东西」才是不可恢复失败
3 「有 sha256 则一并核」 sha256 照收照存(bytea),M3 不核验 S3 HEAD 拿不到 sha256;逐字节回读核验与单机带宽约束(ADR-016 背景)冲突。迁云或 M4 需要时经 S3 checksum 特性补,不改契约形态
4 complete 未声明 400 实现对非 UUID assetId 返回 400/40000 冻结时给 complete 补 400/ValidationError 声明
5 TODO-FREEZEpurpose 白名单是否随 P6 扩 M3 定 post_image 一项P6 扩 user_avatar/pet_avatar 时为纯配置追加 + 契约枚举扩展(向后兼容) 白名单是配置项,扩展零代码
6 TODO-FREEZEmime 白名单与 HEIC image/jpeg image/png image/webp,不收 HEIC 客户端压缩管线统一转码 jpeg(T3-13 侧约定,见 01 号工单 T3-13 描述);服务端收 HEIC 需转码能力,M3 无
7 TODO-FREEZEbyteSize 上限草案 10 MiB 定 1048576010 MiB),配置项 与客户端压缩目标(长边约束后 jpeg 远小于 10 MiB)留足余量

另注:MediaUploadCredentials 草案的 requiredHeaders 标注「键集草案态」,本次定型为仅 Content-Type 一键(§3);expiresAt TTL 草案 10 分钟维持。complete 幂等语义(重复确认 200 返回既有 ready asset)与草案一致,已实证。

5. compose 变更与六容器实测

5.1 变更点(docker-compose.yml

  • 新增 minio 服务:镜像钉 minio/minio:RELEASE.2025-04-22T22-12-26Z(与集成测试同 tag);对象数据落 minio-data volumeADR-007 应用容器无状态不破坏);healthcheck 走 /minio/health/live发布 9000 端口——预签名直传/读取 URL 都直接指向 MinIO,客户端必须可达。
  • user 服务注入 5 个 PATBOND_MINIO_* 环境变量,depends_on minio 健康;PATBOND_MINIO_PUBLIC_ENDPOINT 默认本机回环,真机联调/生产改为客户端可达地址(.env 或环境覆盖)。
  • deploy/init-secrets.sh 幂等追加 PATBOND_MINIO_ROOT_USER(随机后缀)与 PATBOND_MINIO_ROOT_PASSWORD32 hex 随机)到被 gitignore 的 .env——凭证零入库,scripts/check-secrets.sh --all 全仓通过。
  • 桶初始化在 user 服务启动路径(ensureBucket),无需 mc 初始化容器,六容器封顶。

5.2 六容器实测(postgres + minio + user + auth + pet + community

cd <你的工作区>/patbond-api
./deploy/init-secrets.sh
JAVA_HOME=<你的 JDK17 路径> ./mvnw -DskipTests package
docker compose up -d --build
docker compose ps          # 六容器 Uppostgres/minio/user (healthy)
# 媒体链路冒烟:注册 → 创建上传 → 直传 → 确认 → GET URL 取回
docker compose down

实测结果(2026-09-08,本机):六容器全部 Uppostgres/minio healthy;媒体链路冒烟全通——注册取 token → POST /api/v1/media/uploads 201uploadUrl 指向 public-endpointX-Amz-SignedHeaders=content-type;host 证实 Content-Type 已签进签名)→ 按凭据直传 PUT 200 → complete 200 status=ready → 预签名 GET 200 且取回字节与上传逐字节一致;随后 docker compose down 干净退出。

6. uploading 超时清理——方案(本迭代只记录不实现)

  • 扫描:定时任务照 SessionCleanupJob 既有模式(@Scheduled + 配置化节奏),SELECT id, bucket, object_key FROM media.assets WHERE status='uploading' AND created_at < now() - :timeout,命中 V1 预留的部分索引 ix_media_uploading_created(该索引在位有测试锚定)。
  • 处置:先删对象(ObjectStoragedelete(objectKey),容忍对象本就不存在),再把行置 failed(保留审计轨迹与防枚举一致性;不物理删行)。两步顺序保证不产生「行没了对象还在」的孤儿。
  • 参数建议:超时阈值 24h、扫描间隔 6h、单批上限 500 行,全部配置项。
  • 排期:随 T3-09(或第二波收口)实现;实现前 uploading 僵尸行只占元数据行与零字节~少量对象空间,无正确性风险(业务侧只认 ready)。

7. 测试变化

基线 交付
全套 ./mvnw clean test 206 226+20media 12 + auth 契约 8

新增:

  • patbond-user media/MediaUploadIntegrationTest12 个MinIO Testcontainer + postgres:18 真库):全链路(创建→真实 HTTP 直传→确认→ready→预签名 GET 取回字节一致→无签名直访 403);凭据形态(201 形态、TTL 窗口);六类失败路径——非法 mime、超限 byteSize、kind/purpose 白名单外、未上传就确认(保持可重试并实证补传后恢复)、大小不符置 failed 终态、他人/不存在 asset 防枚举 40405;401 矩阵;数据库约束与应用层一致性(ck_media_ready/ck_media_location/uq_media_object 逐一触发库层拒绝);清理索引在位。
  • patbond-auth AuthContractConformanceTest8 个T3-19):机制与 patbond-pet ContractConformanceTest 同构(模块内复制 OpenApiContract/ContractValidator + v1.2.0 字节级快照,同一份每模块复制纪律);运行方式沿 AuthE2eIntegrationTest 编排——同 JVM 真实拉起 user 服务,register/login/refresh/logout 打 auth、me/trackEvents 打 user,跨服务真实纵切。全响应矩阵门禁:6 操作 19 个 (操作, 状态码) 单元格零豁免,含 register 409 双业务码(40900/40901)、login 423 锁定、refresh 40102 重放、me 404 幽灵用户、events 匿名 202 与带无效 token 401。auth 路径本就在 v1.2.0 快照内,无契约升版。
  • 契约测试首轮即抓到一处真实漂移/api/v1/events 202 响应中 accepted/duplicate 条目序列化出 "reason": null,而契约声明 reason 仅 status=rejected 时出现(且未标 nullable)。已修实现侧(EventResult.reason@JsonInclude(NON_NULL)),既有埋点测试零回归——这正是 T3-19 要补的防护网生效的实证。

错误码扩充(patbond-common ErrorCode):MEDIA_NOT_FOUND(40405, 404)MEDIA_UPLOAD_STATE_INVALID(42205, 422)——与草案错误码段取值一致;42203 MEDIA_NOT_READY 属 T3-04 引用侧,未预占。

8. 遗留与交接

  • T3-13Flutter 端)联调输入:§3 凭据形态定型表 + §4 偏差清单即两步上传协议的权威描述;客户端直传须原样携带 requiredHeaders,压缩管线出 jpeg(偏差 #6)。
  • T3-10 冻结回填:§4 七项偏差均需回填草案(TODO-FREEZE 三处媒体位 + complete 400 声明);冻结时同步 api 侧快照升版(两处复制:patbond-pet 与 patbond-auth 的 src/test/resources/contract/,快照守卫测试会拦忘记同步)。
  • 清理任务(§6)随后续波次实现;ObjectStorage.delete 届时补。
  • 生产化注意:PATBOND_MINIO_PUBLIC_ENDPOINT 必须配置为客户端可达地址;带宽瓶颈显现即触发 ADR-016 迁云条件。