--- name: 用户头像上传接口 overview: 新增服务端中转的头像上传接口 POST /api/v1/user/avatar/upload,复用现有衣物上传的图片校验与 COS 上传能力,校验通过后上传 COS 并将头像 key 写入 users.avatar,最后失效用户缓存。 todos: - id: avatar-service content: 实现 UserService.AvatarUpload(校验、COS 上传、更新 avatar、失效缓存)与 buildUserInfoResponse 头像 URL 转换 status: completed - id: avatar-api content: 新增头像上传 DTO 与 UserHandler.AvatarUpload,注册 POST /user/avatar/upload 路由 status: completed dependencies: - avatar-service - id: avatar-docs content: 同步更新接口文档.md 与 README.md 的头像上传接口说明 status: completed dependencies: - avatar-api --- ## 产品概述 为当前用户新增"头像文件上传"能力。此前后端只允许通过 `PUT /user/profile` 传入头像 URL 字符串,现需提供方案 A(服务端中转):客户端以 multipart 上传头像图片,后端完成校验、存储到 COS,并更新用户头像字段,返回可直接访问的头像 URL。 ## 核心功能 - 新增鉴权接口 `POST /api/v1/user/avatar/upload`,multipart 字段名 `file` - 后端校验上传文件:非空、扩展名白名单、大小上限、图片 magic bytes - 上传成功后写入 COS,并将 COS 对象键保存到 `users.avatar` - 更新成功后失效用户信息缓存,返回可访问的头像 URL 供前端立即展示 - 用户信息查询接口对头像做兼容转换:历史 URL 直接透传,新存 COS key 自动转可访问 URL - 头像上传不消耗衣物 COS 上传额度 ## 技术栈 - 语言/框架:Go + Gin(沿用现有项目栈) - 存储:腾讯云 COS(复用 `global.CosClient`) - 缓存:Redis(复用 `redisdao.UserInfoDel` 失效用户缓存) - 持久层:GORM(复用 `mysqldao.UserDao`) - 工具函数:复用 `pkg/upload` 的文件校验、落盘、COS 上传、URL 生成能力 - 无新增第三方依赖 ## 实施方案 ### 整体策略 仿照现有衣物上传 `ClothesService.Upload` 的同步链路,在用户模块新增头像上传能力:Handler 解析 multipart → Service 完成校验、落盘、上传 COS、更新数据库、失效缓存 → 返回头像 URL。 ### 关键决策 1. **复用现有校验链路与错误码**:扩展名白名单(`global.UploadSetting.AllowedExtensions`)、大小上限(`global.UploadSetting.MaxSizeMB`)、magic bytes 校验,以及 `UploadFileEmpty` / `UploadFileExtNotAllow` / `UploadFileTooLarge` / `UploadFileSaveFailed` 错误码,保持与衣物上传一致。 2. **avatar 存储 COS 对象键而非完整 URL**:与衣物 `processed_image` 存 key 的语义一致,读取时统一经 `upload.GetCosUrl` 转换;对历史通过 `PUT /user/profile` 传入的 http(s) URL 直接透传,保证向后兼容。 3. **COS key 规则**:`{user.UserUUID}/{userID}/avatar/{随机名+原扩展名}`,复用 `FileGenName` 防冲突,且按用户维度隔离,避免跨用户覆盖。 4. **不扣 COS 上传额度**:头像属基础资料,与衣物图片处理(抠图/分类)成本不同,不调用 `consumeCosQuota`。 5. **上传后失效缓存**:调用 `redisdao.UserInfoDel(ctx, userID)`,使 `InfoGetBySelf` / `WxLogin` 下次回读最新头像。 ### 数据流 ```mermaid sequenceDiagram participant C as 客户端 participant H as UserHandler.AvatarUpload participant S as UserService.AvatarUpload participant U as pkg/upload participant COS as 腾讯云 COS participant D as UserDao participant R as Redis C->>H: POST /user/avatar/upload (multipart file) H->>H: JWT 鉴权 + MaxBytesReader 限流 H->>S: AvatarUpload(ctx, userID, fileHeader) S->>S: 扩展名/大小/magic bytes 校验 + 读取字节 S->>D: UserGetByID 获取 UserUUID S->>U: FileGenName + FileSaveTmpBytes S->>U: FileUploadCos(cosKey) U->>COS: 上传对象 S->>D: UserUpdateByID(avatar=cosKey) S->>R: UserInfoDel 失效缓存 S-->>H: 返回头像可访问 URL H-->>C: 统一响应 ``` ## 实现细节 ### 目录结构 ``` project-root/ ├── internal/ │ ├── dto/ │ │ └── user.go # [MODIFY] 新增 UserAvatarUploadRequest(空)/ UserAvatarUploadResponse │ ├── service/ │ │ └── user.go # [MODIFY] 新增 AvatarUpload 方法;buildUserInfoResponse 头像 URL 转换;补充 imports │ ├── routers/ │ │ ├── router.go # [MODIFY] 注册 POST /user/avatar/upload │ │ └── api/v1/ │ │ └── user.go # [MODIFY] 新增 UserHandler.AvatarUpload;补充 imports ├── 接口文档.md # [MODIFY] 新增头像上传接口说明 └── README.md # [MODIFY] 同步用户接口说明 ``` ### 关键代码结构 ``` // internal/dto/user.go type UserAvatarUploadRequest struct{} type UserAvatarUploadResponse struct { Avatar string `json:"avatar"` // 头像可访问 URL } ``` ``` // internal/service/user.go func (s *UserService) AvatarUpload(ctx context.Context, userID int64, fileHeader *multipart.FileHeader) (*dto.UserAvatarUploadResponse, *errcode.Error) ``` `buildUserInfoResponse` 中头像转换逻辑: ``` avatar := user.Avatar if avatar != "" && !strings.HasPrefix(avatar, "http://") && !strings.HasPrefix(avatar, "https://") { avatar = upload.GetCosUrl(avatar) } ``` ### 执行要点 - **imports 补充**:`internal/service/user.go` 需新增 `io`、`mime/multipart`、`pkg/upload`;`internal/routers/api/v1/user.go` 需新增 `net/http`、`global`、`pkg/errcode`(若缺)。 - **错误映射**:`FileSaveTmpBytes`、`FileUploadCos` 返回原生 error 时统一映射为 `errcode.UploadFileSaveFailed`;`UserGetByID` 未命中映射 `UserNotFound`,其他 DB 错误映射 `DatabaseError`;`UserUpdateByID` 失败映射 `UserUpdateFailed`。 - **同步读取字节**:multipart 临时文件在请求返回后会被清理,必须在 Service 内同步 `io.ReadAll` 后再异步/后续处理(本方案全同步,无异步环节)。 - **`buildUserInfoResponse` 影响面**:该函数同时被 `InfoGetBySelf` 与 `WxLogin` 使用,头像 URL 转换对两处一并生效,属预期行为,无需额外改动。 - **缓存失效**:复用 `ProfileUpdate` 既有模式 `_ = redisdao.UserInfoDel(ctx, userID)`,失败仅忽略(best-effort),下次读取会回落 DB 回填。 - **兼容性保障**:不修改 `PUT /user/profile` 的现有 avatar 传 URL 语义,避免破坏已有客户端;仅扩展用户信息读取时的 URL 转换。