6.6 KiB
6.6 KiB
name, overview, todos
| name | overview | todos | |||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 用户头像上传接口 | 新增服务端中转的头像上传接口 POST /api/v1/user/avatar/upload,复用现有衣物上传的图片校验与 COS 上传能力,校验通过后上传 COS 并将头像 key 写入 users.avatar,最后失效用户缓存。 |
|
产品概述
为当前用户新增"头像文件上传"能力。此前后端只允许通过 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。
关键决策
- 复用现有校验链路与错误码:扩展名白名单(
global.UploadSetting.AllowedExtensions)、大小上限(global.UploadSetting.MaxSizeMB)、magic bytes 校验,以及UploadFileEmpty/UploadFileExtNotAllow/UploadFileTooLarge/UploadFileSaveFailed错误码,保持与衣物上传一致。 - avatar 存储 COS 对象键而非完整 URL:与衣物
processed_image存 key 的语义一致,读取时统一经upload.GetCosUrl转换;对历史通过PUT /user/profile传入的 http(s) URL 直接透传,保证向后兼容。 - COS key 规则:
{user.UserUUID}/{userID}/avatar/{随机名+原扩展名},复用FileGenName防冲突,且按用户维度隔离,避免跨用户覆盖。 - 不扣 COS 上传额度:头像属基础资料,与衣物图片处理(抠图/分类)成本不同,不调用
consumeCosQuota。 - 上传后失效缓存:调用
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 转换。