mirror of
https://github.com/limitcool/starter.git
synced 2025-10-05 08:16:53 +08:00

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.
276 lines
7.3 KiB
Go
276 lines
7.3 KiB
Go
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
|
||
}
|
||
}
|