- 创建头像上传API接口 (POST /api/v1/users/:id/avatar) - 实现完整的头像上传逻辑,包含权限验证和文件处理 - 添加头像图片处理功能,支持自动压缩和居中裁剪 - 完善静态文件服务,支持头像访问 - 创建完整的API测试用例 - 更新任务进度文档 任务11已完成,项目完成率提升至37.5%
244 lines
6.1 KiB
Go
244 lines
6.1 KiB
Go
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
|
||
} |