Files
zt-closet-backend/.codebuddy/plans/用户头像上传接口_eb17021f.md
2026-09-11 01:41:52 +08:00

6.6 KiB
Raw Permalink Blame History

name, overview, todos
name overview todos
用户头像上传接口 新增服务端中转的头像上传接口 POST /api/v1/user/avatar/upload,复用现有衣物上传的图片校验与 COS 上传能力,校验通过后上传 COS 并将头像 key 写入 users.avatar,最后失效用户缓存。
id content status
avatar-service 实现 UserService.AvatarUpload(校验、COS 上传、更新 avatar、失效缓存)与 buildUserInfoResponse 头像 URL 转换 completed
id content status dependencies
avatar-api 新增头像上传 DTO 与 UserHandler.AvatarUpload,注册 POST /user/avatar/upload 路由 completed
avatar-service
id content status dependencies
avatar-docs 同步更新接口文档.md 与 README.md 的头像上传接口说明 completed
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 下次回读最新头像。

数据流

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 转换。