feat(avatar): implement PUT /user/avatar with COS storage and content review - #34
Merged
Conversation
- cos-go-sdk-v5 v0.7.75: Tencent Cloud COS client (Object.Put/Delete, CI.ImageAuditing) - gabriel-vasile/mimetype: magic-byte detection incl. webp (mimetype was already indirect, now direct) - golang.org/x/image: webp DecodeConfig registration for the stdlib image package
ObjectStore persists avatar objects and returns their public URL; AvatarAuditor reviews an uploaded image and is fail-closed by contract (auditor error means the image was NOT reviewed). Implementations live in internal/adapter; the session service only sees these interfaces.
Implements the objectstore ports against COS (PRD §1.1): public-read uploads, best-effort deletes, and the synchronous CI image review (sensitive-content-recognition, porn/terrorist/politics/ads). Supports an endpoint override and a CDN base URL for public object URLs.
The STORAGE_* group is optional but all-or-nothing: all empty means avatar upload is disabled (endpoint answers 50002), a partially filled set fails at startup. STORAGE_AUDIT_ENABLED defaults to true (fail-closed content review). The avatar limiter throttles PUT /user/avatar per caller.
UploadAvatar validates the upload (5MB cap enforced on the actual
stream, magic-byte format detection, DecodeConfig integrity check),
stores it under avatar/{userID}/{uuid}.{ext}, runs the content review
(fail-closed: an unreachable reviewer rejects the upload, a sensitive
verdict deletes the object and returns 42200), writes the public URL
into profile.avatar via the existing upsert transaction, retires the
superseded object best-effort and audits upload_avatar.
A failed profile write compensates by deleting the freshly uploaded
object. ProfileUpdate gains the Avatar column.
Multipart parsing lives in the handler, the file stream is passed to the service. Missing or malformed file parts answer 40000; service errors map through the shared mapper (new KindObjectUploadFailed -> 50002). Includes handler tests and a PostgreSQL/Redis e2e covering the persist + audit path and the sensitive-review rejection.
Builds the COS client from STORAGE_* when configured; a deployment without storage keeps every other endpoint and answers 50002 on PUT /user/avatar. The auditor is attached only when STORAGE_AUDIT_ENABLED is on.
- API文档.md §3.3: drop the not-implemented banner, document the upload chain, error codes and rate limit - openapi.yaml: /user/avatar description + 422/429/500 responses - PRD: §4.9 upload section, appendix C and §11 checklist - README: implementation inventory and new packages
OpenAPI 3.0.1 has no null type; the response-envelope data field used type: 'null' in 8 schemas, which the yaml schema validator rejects. nullable: true (empty schema, any type) expresses the same contract.
markdownlint --fix pass on the docs (kept as content fixes only): - API文档.md: blockquote blank line before the §4.2 Lark-binding note - PRD: rename the duplicate '#### 端点' heading under the OIDC section to '#### OIDC 端点' - psql-db-design.md: wrap bare provider-doc URLs in angle brackets, fix a duplicate table heading - caddy-reverse-proxy.md: tabs to spaces in the Caddyfile blocks
- handler: bound the multipart body with http.MaxBytesReader (6MB) before parsing. c.FormFile reads the whole stream to a temp file past Gin's in-memory cap, so without this an oversized body was fully received before the service's 5MB check ran - service: pass the caller's User-Agent into the upload_avatar audit rows (was hardcoded empty); guard avatarKeyFromURL against a new URL that does not end with the fresh key; document the concurrent-upload cleanup boundary - config: reject STORAGE_ENDPOINT values carrying a scheme (the adapter prefixes https://, so such a value produced an unrecoverable URL) - test: assert PUT /user/avatar is registered alongside the other session routes
PUT /user/avatar is registered now; the header claimed it still 404s.
The review verdict was folded into generic codes that cannot tell a policy rejection from a malformed request or a storage fault: - 42203 头像未通过内容审核 (HTTP 422): the audit rejection now has its own code; the client can branch on it instead of parsing the message. The upload_avatar audit row records 42203 as the error code. - The unreachable review service now answers 50300 (dependency unavailable, HTTP 503) like the other fail-closed dependencies (verification codes, tickets): the image was not reviewed, so the upload is rejected and the client is told to retry. 50002 falls back to its original meaning (object storage unconfigured / upload failed). Docs updated: API文档.md error table + §3.3, openapi.yaml 422/503 responses.
docker-compose.yml's api environment is a whitelist (an omitted variable cannot be overridden from .env), but STORAGE_* and RATE_LIMIT_UPLOAD_AVATAR_* were missing — a compose deployment would silently run with no object storage and answer 50002 on avatar upload. Verified: docker compose config is valid.
- README: review rejection now 42203, review outage 50300 (fail-closed) - PRD: §4.9 and §11 error codes; feature table no longer marks content review as planned (later) - CLAUDE.md: PUT /user/avatar is registered; openapi header no longer lists it as the single unregistered path
The adapter used a bare http.Client with no Timeout, so a hung COS side could pin a request goroutine forever when the caller never cancels the context. Wider than provider.httpIOTimeout (10s for small token exchanges): an avatar upload carries a 5MB PUT plus the synchronous CI review, both over the public network.
Filename is client-supplied, so it only reaches a log line on the rejection path — never storage or a URL. Also documents why readAvatar uses DecodeConfig instead of a full Decode (pixel expansion of a 5MB image makes a full decode a memory bomb; corrupt payloads are caught by the COS review or a broken render).
s3loy
approved these changes
Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
实现 PRD §4.9 头像上传(最后一个未注册的契约路径):
PUT /user/avatar上传头像至腾讯云 COS,经内容审核后写入profile.avatar。至此docs/openapi.yaml所有路径均已注册。做了什么
存储与审核(端口 + 适配器)
internal/objectstore:ObjectStore(Upload/Delete)与AvatarAuditor(fail-closed 审核契约)两个端口,session 服务只依赖接口,可无 bucket 测试internal/adapter/cos:COS 适配器,上传带public-readACL(头像需公开展示),审核走同步 CIsensitive-content-recognition(porn/terrorist/politics/ads);HTTP client 带 30s 超时上传用例(session service)
LimitReader实际流校验,撒谎的 header 无法绕过50300并删除对象;敏感内容 →42203并删除对象 + 审计;DB 写库失败 → 补偿删除,不残留孤儿对象avatarKeyFromURL以 base 前缀 +avatar/前缀 +..拒绝三重约束,不误删外来 URL)upload_avatar(成功/拒绝/标签);按用户限流RATE_LIMIT_UPLOAD_AVATAR_*HTTP 层
MaxBytesReader6MB body 上限(否则超限流会被完整读到临时文件后才拒绝);缺file字段 →40000配置
STORAGE_*全有或全无:全空 = 未配置(端点返回50002,其他端点不受影响);半配置 = 启动报错{name}-{appid}形状、endpoint 无 scheme、base URL 绝对地址文档:openapi.yaml(avatar 路径契约)、API 文档(错误码表 + 上传链路)、PRD(实现状态)、README、caddy runbook、.env.example、docker-compose 同步
测试
go test -race -shuffle=on、golangci-lint、go vet全绿行为契约(错误码)
40000(格式/大小/损坏/空/缺 file)、42203(未过审)、42900(限流)、40102(未认证)、40301(已注销)、50002(存储未配置/上传失败)、50300(审核服务不可用)、50003(数据库错误)