mirror of
https://github.com/limitcool/starter.git
synced 2025-10-05 00:12:47 +08:00
262 lines
7.0 KiB
Go
262 lines
7.0 KiB
Go
package handler
|
||
|
||
import (
|
||
"fmt"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/limitcool/starter/configs"
|
||
"github.com/limitcool/starter/internal/api/response"
|
||
"github.com/limitcool/starter/internal/filestore"
|
||
"github.com/limitcool/starter/internal/model"
|
||
"github.com/limitcool/starter/internal/pkg/errorx"
|
||
"github.com/limitcool/starter/internal/pkg/logger"
|
||
"github.com/spf13/cast"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// FileHandler 文件处理器
|
||
type FileHandler struct {
|
||
db *gorm.DB
|
||
config *configs.Config
|
||
storage *filestore.Storage
|
||
}
|
||
|
||
// NewFileHandler 创建文件处理器
|
||
func NewFileHandler(db *gorm.DB, config *configs.Config, storage *filestore.Storage) *FileHandler {
|
||
handler := &FileHandler{
|
||
db: db,
|
||
config: config,
|
||
storage: storage,
|
||
}
|
||
|
||
logger.Info("FileHandler initialized")
|
||
return handler
|
||
}
|
||
|
||
// UploadFile 上传文件
|
||
func (h *FileHandler) UploadFile(ctx *gin.Context) {
|
||
// 获取请求上下文
|
||
reqCtx := ctx.Request.Context()
|
||
|
||
// 从上下文中获取用户ID
|
||
userID, exists := ctx.Get("user_id")
|
||
if !exists {
|
||
logger.WarnContext(reqCtx, "UploadFile 未找到用户ID")
|
||
response.Error(ctx, errorx.ErrUserNoLogin)
|
||
return
|
||
}
|
||
|
||
// 转换用户ID
|
||
id := cast.ToInt64(userID)
|
||
|
||
// 获取用户类型
|
||
userTypeValue, exists := ctx.Get("user_type")
|
||
if !exists {
|
||
logger.WarnContext(reqCtx, "UploadFile 未找到用户类型")
|
||
response.Error(ctx, errorx.ErrUserNoLogin)
|
||
return
|
||
}
|
||
|
||
// 获取用户类型(简化为数字)
|
||
userType := cast.ToUint8(userTypeValue)
|
||
|
||
// 获取文件
|
||
fileHeader, err := ctx.FormFile("file")
|
||
if err != nil {
|
||
logger.WarnContext(reqCtx, "UploadFile 获取文件失败",
|
||
"error", err,
|
||
"user_id", id)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithError(err))
|
||
return
|
||
}
|
||
|
||
// 获取文件类型
|
||
fileType := ctx.DefaultPostForm("type", model.FileTypeOther)
|
||
|
||
// 获取是否公开
|
||
isPublic := ctx.DefaultPostForm("is_public", "false") == "true"
|
||
|
||
// 获取文件基本信息
|
||
originalName := fileHeader.Filename
|
||
ext := strings.ToLower(filepath.Ext(originalName))
|
||
mimeType := filestore.GetMimeType(originalName)
|
||
size := fileHeader.Size
|
||
|
||
// 验证文件类型
|
||
if !isAllowedFileType(ext, fileType) {
|
||
logger.WarnContext(reqCtx, "UploadFile 文件类型不允许",
|
||
"user_id", id,
|
||
"file_type", fileType,
|
||
"ext", ext)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("文件类型不允许"))
|
||
return
|
||
}
|
||
|
||
// 验证文件大小
|
||
if !isAllowedFileSize(size, fileType) {
|
||
logger.WarnContext(reqCtx, "UploadFile 文件大小超出限制",
|
||
"user_id", id,
|
||
"file_type", fileType,
|
||
"size", size)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("文件大小超出限制"))
|
||
return
|
||
}
|
||
|
||
// 打开上传的文件
|
||
file, err := fileHeader.Open()
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "UploadFile 打开文件失败",
|
||
"error", err,
|
||
"user_id", id)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithError(err))
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
// 生成文件名和存储路径
|
||
fileName := generateFileName(originalName)
|
||
storagePath := getStoragePath(fileType, fileName)
|
||
|
||
// 上传文件到存储
|
||
err = h.storage.Put(reqCtx, storagePath, file)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "UploadFile 上传文件到存储失败",
|
||
"error", err,
|
||
"user_id", id,
|
||
"storage_path", storagePath)
|
||
response.Error(ctx, errorx.WrapError(err, fmt.Sprintf("上传文件到存储失败: %s", storagePath)))
|
||
return
|
||
}
|
||
|
||
// 获取文件访问URL
|
||
fileURL, err := h.storage.GetURL(reqCtx, storagePath)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "UploadFile 获取文件访问URL失败",
|
||
"error", err,
|
||
"user_id", id,
|
||
"storage_path", storagePath)
|
||
response.Error(ctx, errorx.WrapError(err, fmt.Sprintf("获取文件访问URL失败: %s", storagePath)))
|
||
return
|
||
}
|
||
|
||
// 创建文件仓库
|
||
fileRepo := model.NewFileRepo(h.db)
|
||
|
||
// 记录到数据库
|
||
fileModel := &model.File{
|
||
Name: fileName,
|
||
OriginalName: originalName,
|
||
Path: storagePath,
|
||
URL: fileURL,
|
||
Type: fileType,
|
||
Usage: model.FileUsageGeneral, // 默认用途为通用
|
||
Size: size,
|
||
MimeType: mimeType,
|
||
Extension: ext,
|
||
StorageType: string(h.storage.Config.Type),
|
||
UploadedBy: id,
|
||
UploadedByType: uint8(userType),
|
||
UploadedAt: time.Now(),
|
||
Status: 1, // 状态正常
|
||
IsPublic: isPublic, // 设置是否公开
|
||
}
|
||
|
||
if err := fileRepo.Create(reqCtx, fileModel); err != nil {
|
||
logger.ErrorContext(reqCtx, "UploadFile 保存文件记录失败",
|
||
"error", err,
|
||
"user_id", id)
|
||
response.Error(ctx, err)
|
||
return
|
||
}
|
||
|
||
logger.InfoContext(reqCtx, "UploadFile 上传文件成功",
|
||
"user_id", id,
|
||
"file_id", fileModel.ID,
|
||
"file_name", fileModel.Name)
|
||
|
||
response.Success(ctx, fileModel)
|
||
}
|
||
|
||
// 生成文件名
|
||
func generateFileName(originalName string) string {
|
||
ext := filepath.Ext(originalName)
|
||
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randString(8), ext)
|
||
return name
|
||
}
|
||
|
||
// 随机字符串
|
||
func randString(n int) string {
|
||
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||
b := make([]byte, n)
|
||
for i := range b {
|
||
b[i] = letters[int(time.Now().UnixNano()%int64(len(letters)))]
|
||
time.Sleep(1 * time.Nanosecond) // 确保随机性
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
// 获取存储路径
|
||
func getStoragePath(fileType, fileName string) string {
|
||
var baseDir string
|
||
switch fileType {
|
||
case model.FileTypeImage:
|
||
baseDir = "images"
|
||
case model.FileTypeDocument:
|
||
baseDir = "documents"
|
||
case model.FileTypeVideo:
|
||
baseDir = "videos"
|
||
case model.FileTypeAudio:
|
||
baseDir = "audios"
|
||
default:
|
||
baseDir = "others"
|
||
}
|
||
|
||
// 添加日期子目录
|
||
dateDir := time.Now().Format("2006/01/02")
|
||
|
||
// 使用path包而不是filepath包,确保始终使用/作为分隔符
|
||
path := filepath.Join(baseDir, dateDir, fileName)
|
||
// 确保Windows上也使用正斜杠
|
||
return strings.ReplaceAll(path, "\\", "/")
|
||
}
|
||
|
||
// 检查文件类型是否允许
|
||
func isAllowedFileType(ext string, fileType string) bool {
|
||
ext = strings.ToLower(ext)
|
||
switch fileType {
|
||
case model.FileTypeImage:
|
||
return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".webp" || ext == ".svg"
|
||
case model.FileTypeDocument:
|
||
return ext == ".pdf" || ext == ".doc" || ext == ".docx" || ext == ".xls" || ext == ".xlsx" || ext == ".txt"
|
||
case model.FileTypeVideo:
|
||
return ext == ".mp4" || ext == ".avi" || ext == ".mov" || ext == ".wmv" || ext == ".flv"
|
||
case model.FileTypeAudio:
|
||
return ext == ".mp3" || ext == ".wav" || ext == ".ogg" || ext == ".flac"
|
||
default:
|
||
return true
|
||
}
|
||
}
|
||
|
||
// 检查文件大小是否允许
|
||
func isAllowedFileSize(size int64, fileType string) bool {
|
||
const (
|
||
MB = 1024 * 1024
|
||
)
|
||
|
||
switch fileType {
|
||
case model.FileTypeImage:
|
||
return size <= 10*MB // 图片最大10MB
|
||
case model.FileTypeDocument:
|
||
return size <= 50*MB // 文档最大50MB
|
||
case model.FileTypeVideo:
|
||
return size <= 500*MB // 视频最大500MB
|
||
case model.FileTypeAudio:
|
||
return size <= 100*MB // 音频最大100MB
|
||
default:
|
||
return size <= 50*MB // 其他类型最大50MB
|
||
}
|
||
}
|