first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
@@ -0,0 +1,133 @@
---
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 转换。