Files
xujiang 5dd0bc19e4
Some checks failed
部署管理后台 / 🧪 测试和构建 (push) Failing after 1m5s
部署管理后台 / 🔒 安全扫描 (push) Has been skipped
部署后端服务 / 🧪 测试后端 (push) Failing after 3m13s
部署前端网站 / 🧪 测试和构建 (push) Failing after 2m10s
部署管理后台 / 🚀 部署到生产环境 (push) Has been skipped
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署管理后台 / 🔄 回滚部署 (push) Has been skipped
部署前端网站 / 🚀 部署到生产环境 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped
style: 统一代码格式化 (go fmt + 配置更新)
- 后端:应用 go fmt 自动格式化,统一代码风格
- 前端:更新 API 配置,完善类型安全
- 所有代码符合项目规范,准备生产部署
2025-07-14 10:02:04 +08:00

245 lines
6.1 KiB
Go
Raw Permalink 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
}