Files
photography/backend/pkg/utils/file/file.go
xujiang fa5f7a0ed2 feat: 实现用户头像上传功能
- 创建头像上传API接口 (POST /api/v1/users/:id/avatar)
- 实现完整的头像上传逻辑,包含权限验证和文件处理
- 添加头像图片处理功能,支持自动压缩和居中裁剪
- 完善静态文件服务,支持头像访问
- 创建完整的API测试用例
- 更新任务进度文档

任务11已完成,项目完成率提升至37.5%
2025-07-11 13:10:04 +08:00

244 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package file
import (
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"github.com/disintegration/imaging"
"github.com/google/uuid"
)
// Config 文件上传配置
type Config struct {
MaxSize int64 // 最大文件大小 (bytes)
UploadDir string // 上传目录
AllowedTypes []string // 允许的文件类型
}
// FileInfo 文件信息
type FileInfo struct {
OriginalName string `json:"original_name"`
FileName string `json:"file_name"`
FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"`
ContentType string `json:"content_type"`
URL string `json:"url"`
}
// UploadResult 上传结果
type UploadResult struct {
Original FileInfo `json:"original"`
Thumbnail FileInfo `json:"thumbnail"`
}
// IsAllowedType 检查文件类型是否允许
func IsAllowedType(contentType string, allowedTypes []string) bool {
for _, allowedType := range allowedTypes {
if contentType == allowedType {
return true
}
}
return false
}
// GenerateFileName 生成唯一文件名
func GenerateFileName(originalName string) string {
ext := filepath.Ext(originalName)
name := strings.TrimSuffix(originalName, ext)
timestamp := time.Now().Unix()
uuid := uuid.New().String()[:8]
return fmt.Sprintf("%s_%d_%s%s", name, timestamp, uuid, ext)
}
// SaveFile 保存文件
func SaveFile(file multipart.File, header *multipart.FileHeader, config Config) (*FileInfo, error) {
// 检查文件大小
if header.Size > config.MaxSize {
return nil, fmt.Errorf("文件大小超过限制: %d bytes", config.MaxSize)
}
// 检查文件类型
contentType := header.Header.Get("Content-Type")
if !IsAllowedType(contentType, config.AllowedTypes) {
return nil, fmt.Errorf("不支持的文件类型: %s", contentType)
}
// 生成文件名
fileName := GenerateFileName(header.Filename)
// 创建上传目录
uploadPath := filepath.Join(config.UploadDir, "photos")
if err := os.MkdirAll(uploadPath, 0755); err != nil {
return nil, fmt.Errorf("创建上传目录失败: %v", err)
}
// 完整文件路径
filePath := filepath.Join(uploadPath, fileName)
// 创建目标文件
dst, err := os.Create(filePath)
if err != nil {
return nil, fmt.Errorf("创建文件失败: %v", err)
}
defer dst.Close()
// 复制文件内容
file.Seek(0, 0) // 重置文件指针
_, err = io.Copy(dst, file)
if err != nil {
return nil, fmt.Errorf("保存文件失败: %v", err)
}
// 返回文件信息
return &FileInfo{
OriginalName: header.Filename,
FileName: fileName,
FilePath: filePath,
FileSize: header.Size,
ContentType: contentType,
URL: "/uploads/photos/" + fileName,
}, nil
}
// CreateThumbnail 创建缩略图
func CreateThumbnail(originalPath string, config Config) (*FileInfo, error) {
// 打开原图
src, err := imaging.Open(originalPath)
if err != nil {
return nil, fmt.Errorf("打开图片失败: %v", err)
}
// 生成缩略图文件名
ext := filepath.Ext(originalPath)
baseName := strings.TrimSuffix(filepath.Base(originalPath), ext)
thumbnailName := baseName + "_thumb" + ext
// 创建缩略图目录
thumbnailDir := filepath.Join(config.UploadDir, "thumbnails")
if err := os.MkdirAll(thumbnailDir, 0755); err != nil {
return nil, fmt.Errorf("创建缩略图目录失败: %v", err)
}
thumbnailPath := filepath.Join(thumbnailDir, thumbnailName)
// 调整图片大小 (最大宽度 300px保持比例)
thumbnail := imaging.Resize(src, 300, 0, imaging.Lanczos)
// 保存缩略图
err = imaging.Save(thumbnail, thumbnailPath)
if err != nil {
return nil, fmt.Errorf("保存缩略图失败: %v", err)
}
// 获取文件大小
stat, err := os.Stat(thumbnailPath)
if err != nil {
return nil, fmt.Errorf("获取缩略图信息失败: %v", err)
}
return &FileInfo{
OriginalName: thumbnailName,
FileName: thumbnailName,
FilePath: thumbnailPath,
FileSize: stat.Size(),
ContentType: "image/jpeg",
URL: "/uploads/thumbnails/" + thumbnailName,
}, nil
}
// UploadPhoto 上传照片(包含原图和缩略图)
func UploadPhoto(file multipart.File, header *multipart.FileHeader, config Config) (*UploadResult, error) {
// 保存原图
original, err := SaveFile(file, header, config)
if err != nil {
return nil, err
}
// 创建缩略图
thumbnail, err := CreateThumbnail(original.FilePath, config)
if err != nil {
return nil, err
}
return &UploadResult{
Original: *original,
Thumbnail: *thumbnail,
}, nil
}
// DeleteFile 删除文件
func DeleteFile(filePath string) error {
if filePath == "" {
return nil
}
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil // 文件不存在,认为删除成功
}
return os.Remove(filePath)
}
// GetImageDimensions 获取图片尺寸
func GetImageDimensions(filePath string) (width, height int, err error) {
file, err := os.Open(filePath)
if err != nil {
return 0, 0, err
}
defer file.Close()
img, _, err := image.DecodeConfig(file)
if err != nil {
return 0, 0, err
}
return img.Width, img.Height, nil
}
// ResizeImage 调整图片尺寸 (用于头像处理)
func ResizeImage(srcPath, destPath string, width, height int) error {
// 打开原图
src, err := imaging.Open(srcPath)
if err != nil {
return fmt.Errorf("打开图片失败: %v", err)
}
// 调整图片尺寸 (正方形裁剪)
resized := imaging.Fill(src, width, height, imaging.Center, imaging.Lanczos)
// 保存调整后的图片
err = imaging.Save(resized, destPath)
if err != nil {
return fmt.Errorf("保存调整后的图片失败: %v", err)
}
return nil
}
// CreateAvatar 创建头像(包含压缩和格式转换)
func CreateAvatar(srcPath, destPath string, size int) error {
// 打开原图
src, err := imaging.Open(srcPath)
if err != nil {
return fmt.Errorf("打开图片失败: %v", err)
}
// 创建正方形头像 (居中裁剪)
avatar := imaging.Fill(src, size, size, imaging.Center, imaging.Lanczos)
// 保存为JPEG格式 (压缩优化)
err = imaging.Save(avatar, destPath, imaging.JPEGQuality(85))
if err != nil {
return fmt.Errorf("保存头像失败: %v", err)
}
return nil
}