package user import ( "context" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "photography-backend/internal/svc" "photography-backend/internal/types" "photography-backend/pkg/errorx" "photography-backend/pkg/utils/file" "github.com/zeromicro/go-zero/core/logx" ) type UploadAvatarLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } // 上传用户头像 func NewUploadAvatarLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadAvatarLogic { return &UploadAvatarLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *UploadAvatarLogic) UploadAvatar(req *types.UploadAvatarRequest, r *http.Request) (resp *types.UploadAvatarResponse, err error) { // 1. 验证用户权限 userId := l.ctx.Value("userId").(int64) if userId != req.Id { return nil, errorx.New(errorx.Forbidden, "您只能更新自己的头像") } // 2. 检查用户是否存在 user, err := l.svcCtx.UserModel.FindOne(l.ctx, req.Id) if err != nil { return nil, errorx.New(errorx.UserNotFound, "用户不存在") } // 3. 解析多部分表单 err = r.ParseMultipartForm(10 << 20) // 10MB if err != nil { return nil, errorx.New(errorx.ParamError, "解析表单失败") } // 4. 获取上传的文件 uploadedFile, header, err := r.FormFile("avatar") if err != nil { return nil, errorx.New(errorx.ParamError, "获取上传文件失败: "+err.Error()) } defer uploadedFile.Close() // 5. 验证文件类型 (头像应该是图片) contentType := header.Header.Get("Content-Type") if !strings.HasPrefix(contentType, "image/") { return nil, errorx.New(errorx.ParamError, "头像必须是图片文件") } // 6. 验证文件大小 (限制为5MB) if header.Size > 5*1024*1024 { return nil, errorx.New(errorx.ParamError, "头像文件大小不能超过5MB") } // 7. 生成头像文件名和路径 ext := filepath.Ext(header.Filename) if ext == "" { // 根据Content-Type推断扩展名 switch contentType { case "image/jpeg": ext = ".jpg" case "image/png": ext = ".png" case "image/gif": ext = ".gif" case "image/webp": ext = ".webp" default: ext = ".jpg" } } filename := fmt.Sprintf("avatar_%d_%d%s", req.Id, time.Now().Unix(), ext) avatarDir := "uploads/avatars" // 8. 确保头像目录存在 if err := os.MkdirAll(avatarDir, 0755); err != nil { return nil, errorx.New(errorx.ServerError, "创建头像目录失败: "+err.Error()) } avatarPath := filepath.Join(avatarDir, filename) // 9. 保存原始头像文件 destFile, err := os.Create(avatarPath) if err != nil { return nil, errorx.New(errorx.ServerError, "创建头像文件失败: "+err.Error()) } defer destFile.Close() _, err = io.Copy(destFile, uploadedFile) if err != nil { return nil, errorx.New(errorx.ServerError, "保存头像文件失败: "+err.Error()) } // 10. 生成压缩版本的头像 (150x150像素) compressedPath := filepath.Join(avatarDir, fmt.Sprintf("avatar_%d_%d_150x150%s", req.Id, time.Now().Unix(), ext)) err = file.ResizeImage(avatarPath, compressedPath, 150, 150) if err != nil { l.Errorf("头像压缩失败: %v", err) // 压缩失败不影响主流程,使用原图 compressedPath = avatarPath } // 11. 删除旧的头像文件 (如果存在) if user.Avatar != "" && user.Avatar != compressedPath { oldAvatarPath := user.Avatar if strings.HasPrefix(oldAvatarPath, "/uploads/") { oldAvatarPath = strings.TrimPrefix(oldAvatarPath, "/") } if _, err := os.Stat(oldAvatarPath); err == nil { os.Remove(oldAvatarPath) l.Infof("删除旧头像文件: %s", oldAvatarPath) } } // 12. 更新用户头像URL avatarUrl := "/" + compressedPath user.Avatar = avatarUrl err = l.svcCtx.UserModel.Update(l.ctx, user) if err != nil { // 更新失败,删除已上传的文件 os.Remove(avatarPath) if avatarPath != compressedPath { os.Remove(compressedPath) } return nil, errorx.New(errorx.ServerError, "更新用户头像失败: "+err.Error()) } return &types.UploadAvatarResponse{ BaseResponse: types.BaseResponse{ Code: 200, Message: "头像上传成功", }, Data: types.UploadAvatarData{ AvatarUrl: avatarUrl, }, }, nil }