Files
starter/internal/handler/file_handler.go
limitcool 6749e117d7 feat: split project into lite and enterprise modes
This commit establishes the lite branch, which provides a simplified version of the framework:

- Simplified architecture: removed Service and Repository layers
- Single user table design: using is_admin field to distinguish administrators
- Removed complex features: RBAC permissions, menu management, and role system
- Streamlined middleware: simplified auth checks based on is_admin field
- Unified naming conventions: removed "simple" prefixes from functions and files
- Updated documentation: revised README files to reflect lite mode features

The lite mode is designed for rapid development and simple applications, while
the enterprise mode (main branch) retains all advanced features for complex
business scenarios.
2025-04-29 16:31:25 +08:00

276 lines
7.3 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 handler
import (
"context"
"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"
"go.uber.org/fx"
"gorm.io/gorm"
)
// FileHandler 文件处理器
type FileHandler struct {
db *gorm.DB
config *configs.Config
logger *logger.Logger
storage *filestore.Storage
}
// NewFileHandler 创建文件处理器
func NewFileHandler(db *gorm.DB, config *configs.Config, lc fx.Lifecycle, storage *filestore.Storage) *FileHandler {
handler := &FileHandler{
db: db,
config: config,
storage: storage,
}
// 注册生命周期钩子
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
logger.InfoContext(ctx, "FileHandler initialized")
return nil
},
OnStop: func(ctx context.Context) error {
logger.InfoContext(ctx, "FileHandler stopped")
return nil
},
})
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
}
}