mirror of
https://github.com/limitcool/starter.git
synced 2025-09-27 04:36:18 +08:00

- Consolidated user ID retrieval and permission checks into helper functions. - Updated UserHandler to utilize BaseHandler for common database and configuration access. - Enhanced logging for user-related operations, including login, registration, and password changes. - Removed redundant context handling in middleware and improved readability. - Introduced FileUtil for file URL generation and management, encapsulating file-related logic. - Refactored FileRepo and UserRepo to streamline database operations and error handling. - Deleted unused request_id middleware and integrated its functionality into request_logger. - Removed legacy test runner script to simplify testing process.
697 lines
20 KiB
Go
697 lines
20 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 {
|
||
*BaseHandler
|
||
storage *filestore.Storage
|
||
}
|
||
|
||
// NewFileHandler 创建文件处理器
|
||
func NewFileHandler(db *gorm.DB, config *configs.Config, storage *filestore.Storage) *FileHandler {
|
||
handler := &FileHandler{
|
||
BaseHandler: NewBaseHandler(db, config),
|
||
storage: storage,
|
||
}
|
||
|
||
handler.LogInit("FileHandler")
|
||
return handler
|
||
}
|
||
|
||
// GetUploadURL 获取文件上传预签名URL(前端直接上传到MinIO)
|
||
func (h *FileHandler) GetUploadURL(ctx *gin.Context) {
|
||
reqCtx := ctx.Request.Context()
|
||
|
||
// 获取当前用户信息
|
||
userID, exists := ctx.Get("user_id")
|
||
if !exists {
|
||
logger.WarnContext(reqCtx, "GetUploadURL 未找到用户ID")
|
||
response.Error(ctx, errorx.ErrUnauthorized.WithMsg("用户未登录"))
|
||
return
|
||
}
|
||
id := cast.ToInt64(userID)
|
||
|
||
// 获取用户类型
|
||
userType := uint8(1) // 默认为普通用户
|
||
if isAdmin, exists := ctx.Get("is_admin"); exists && cast.ToBool(isAdmin) {
|
||
userType = uint8(2) // 管理员
|
||
}
|
||
|
||
// 解析请求参数
|
||
var req struct {
|
||
Filename string `json:"filename" binding:"required"`
|
||
ContentType string `json:"content_type" binding:"required"`
|
||
FileType string `json:"file_type" binding:"required"` // image, document, video, audio
|
||
IsPublic bool `json:"is_public"`
|
||
Usage string `json:"usage"` // avatar, document, etc.
|
||
}
|
||
|
||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||
logger.WarnContext(reqCtx, "GetUploadURL 请求参数无效", "error", err)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithError(err))
|
||
return
|
||
}
|
||
|
||
// 验证文件类型
|
||
ext := strings.ToLower(filepath.Ext(req.Filename))
|
||
if !h.FileUtil.IsAllowedFileType(ext, req.FileType) {
|
||
logger.WarnContext(reqCtx, "GetUploadURL 不支持的文件类型",
|
||
"user_id", id,
|
||
"file_type", req.FileType,
|
||
"extension", ext)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("不支持的文件类型"))
|
||
return
|
||
}
|
||
|
||
// 生成唯一的文件名和存储路径
|
||
fileName := h.FileUtil.GenerateFileName(req.Filename)
|
||
storagePath := h.FileUtil.GetStoragePath(req.FileType, fileName, req.IsPublic)
|
||
|
||
// 生成上传预签名URL
|
||
uploadURL, err := h.storage.GetUploadPresignedURL(reqCtx, storagePath, req.ContentType, 15) // 15分钟有效期
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "GetUploadURL 生成上传预签名URL失败",
|
||
"error", err,
|
||
"user_id", id,
|
||
"storage_path", storagePath)
|
||
response.Error(ctx, errorx.WrapError(err, "生成上传链接失败"))
|
||
return
|
||
}
|
||
|
||
// 创建文件记录(状态为pending,等待上传完成确认)
|
||
fileRepo := model.NewFileRepo(h.DB)
|
||
fileModel := &model.File{
|
||
Name: fileName,
|
||
OriginalName: req.Filename,
|
||
Path: storagePath,
|
||
URL: "", // 上传完成后再设置
|
||
Type: req.FileType,
|
||
Usage: req.Usage,
|
||
Size: 0, // 上传完成后再设置
|
||
MimeType: req.ContentType,
|
||
Extension: ext,
|
||
StorageType: string(h.storage.Config.Type),
|
||
UploadedBy: id,
|
||
UploadedByType: uint8(userType),
|
||
UploadedAt: time.Now(),
|
||
Status: 0, // 0=pending, 1=completed
|
||
IsPublic: req.IsPublic,
|
||
}
|
||
|
||
if err := fileRepo.Create(reqCtx, fileModel); err != nil {
|
||
logger.ErrorContext(reqCtx, "GetUploadURL 创建文件记录失败",
|
||
"error", err,
|
||
"user_id", id)
|
||
response.Error(ctx, err)
|
||
return
|
||
}
|
||
|
||
// 构建响应数据
|
||
result := map[string]interface{}{
|
||
"file_id": fileModel.ID,
|
||
"upload_url": uploadURL,
|
||
"storage_path": storagePath,
|
||
"expires_in": 15, // 分钟
|
||
"expires_at": time.Now().Add(15 * time.Minute).Unix(),
|
||
"method": "PUT", // 上传方法
|
||
"headers": map[string]string{
|
||
"Content-Type": req.ContentType,
|
||
},
|
||
}
|
||
|
||
logger.InfoContext(reqCtx, "GetUploadURL 生成上传URL成功",
|
||
"user_id", id,
|
||
"file_id", fileModel.ID,
|
||
"storage_path", storagePath,
|
||
"is_public", req.IsPublic)
|
||
|
||
response.Success(ctx, result)
|
||
}
|
||
|
||
// ConfirmUpload 确认文件上传完成
|
||
func (h *FileHandler) ConfirmUpload(ctx *gin.Context) {
|
||
reqCtx := ctx.Request.Context()
|
||
|
||
// 获取当前用户信息
|
||
userID, exists := ctx.Get("user_id")
|
||
if !exists {
|
||
logger.WarnContext(reqCtx, "ConfirmUpload 未找到用户ID")
|
||
response.Error(ctx, errorx.ErrUnauthorized.WithMsg("用户未登录"))
|
||
return
|
||
}
|
||
id := cast.ToInt64(userID)
|
||
|
||
// 解析请求参数
|
||
var req struct {
|
||
FileID uint `json:"file_id" binding:"required"`
|
||
Size int64 `json:"size" binding:"required"`
|
||
}
|
||
|
||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||
logger.WarnContext(reqCtx, "ConfirmUpload 请求参数无效", "error", err)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithError(err))
|
||
return
|
||
}
|
||
|
||
// 创建文件仓库
|
||
fileRepo := model.NewFileRepo(h.DB)
|
||
|
||
// 获取文件记录
|
||
fileModel, err := fileRepo.GetByID(reqCtx, req.FileID)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "ConfirmUpload 获取文件记录失败",
|
||
"error", err,
|
||
"file_id", req.FileID)
|
||
response.Error(ctx, err)
|
||
return
|
||
}
|
||
|
||
// 检查文件所有权
|
||
if fileModel.UploadedBy != id {
|
||
// 检查是否是管理员
|
||
isAdmin, exists := ctx.Get("is_admin")
|
||
if !exists || !cast.ToBool(isAdmin) {
|
||
logger.WarnContext(reqCtx, "ConfirmUpload 无权限操作文件",
|
||
"file_id", req.FileID,
|
||
"user_id", id,
|
||
"uploaded_by", fileModel.UploadedBy)
|
||
response.Error(ctx, errorx.ErrForbidden.WithMsg("无权限操作此文件"))
|
||
return
|
||
}
|
||
}
|
||
|
||
// 检查文件状态
|
||
if fileModel.Status != 0 {
|
||
logger.WarnContext(reqCtx, "ConfirmUpload 文件已确认",
|
||
"file_id", req.FileID,
|
||
"status", fileModel.Status)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("文件已确认,无需重复操作"))
|
||
return
|
||
}
|
||
|
||
// 验证文件是否真的存在于存储中
|
||
exists, err = h.storage.Exists(reqCtx, fileModel.Path)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "ConfirmUpload 检查文件存在性失败",
|
||
"error", err,
|
||
"file_id", req.FileID,
|
||
"path", fileModel.Path)
|
||
response.Error(ctx, errorx.WrapError(err, "验证文件失败"))
|
||
return
|
||
}
|
||
|
||
if !exists {
|
||
logger.WarnContext(reqCtx, "ConfirmUpload 文件不存在于存储中",
|
||
"file_id", req.FileID,
|
||
"path", fileModel.Path)
|
||
response.Error(ctx, errorx.ErrNotFound.WithMsg("文件上传未完成或已被删除"))
|
||
return
|
||
}
|
||
|
||
// 生成文件访问URL
|
||
fileURL, err := h.storage.GetURL(reqCtx, fileModel.Path)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "ConfirmUpload 获取文件访问URL失败",
|
||
"error", err,
|
||
"file_id", req.FileID,
|
||
"path", fileModel.Path)
|
||
response.Error(ctx, errorx.WrapError(err, "生成文件访问URL失败"))
|
||
return
|
||
}
|
||
|
||
// 更新文件记录
|
||
fileModel.Size = req.Size
|
||
fileModel.URL = fileURL
|
||
fileModel.Status = 1 // 完成状态
|
||
fileModel.UploadedAt = time.Now()
|
||
|
||
if err := fileRepo.Update(reqCtx, fileModel); err != nil {
|
||
logger.ErrorContext(reqCtx, "ConfirmUpload 更新文件记录失败",
|
||
"error", err,
|
||
"file_id", req.FileID)
|
||
response.Error(ctx, err)
|
||
return
|
||
}
|
||
|
||
logger.InfoContext(reqCtx, "ConfirmUpload 确认上传成功",
|
||
"user_id", id,
|
||
"file_id", req.FileID,
|
||
"size", req.Size)
|
||
|
||
response.Success(ctx, fileModel)
|
||
}
|
||
|
||
// 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)
|
||
|
||
// 获取用户类型 - 根据is_admin字段推断
|
||
isAdminValue, exists := ctx.Get("is_admin")
|
||
if !exists {
|
||
logger.WarnContext(reqCtx, "UploadFile 未找到用户管理员标识")
|
||
response.Error(ctx, errorx.ErrUserNoLogin)
|
||
return
|
||
}
|
||
|
||
// 根据is_admin推断用户类型(1=管理员,2=普通用户)
|
||
var userType uint8 = 2 // 默认为普通用户
|
||
if isAdmin, ok := isAdminValue.(bool); ok && isAdmin {
|
||
userType = 1 // 管理员
|
||
}
|
||
|
||
// 获取文件
|
||
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 !h.FileUtil.IsAllowedFileType(ext, fileType) {
|
||
logger.WarnContext(reqCtx, "UploadFile 文件类型不允许",
|
||
"user_id", id,
|
||
"file_type", fileType,
|
||
"ext", ext)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("文件类型不允许"))
|
||
return
|
||
}
|
||
|
||
// 验证文件大小
|
||
if !h.FileUtil.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 := h.FileUtil.GenerateFileName(originalName)
|
||
storagePath := h.FileUtil.GetStoragePath(fileType, fileName, isPublic)
|
||
|
||
// 上传文件到存储(权限由路径和Bucket Policy控制)
|
||
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)
|
||
}
|
||
|
||
// GetFileURL 获取文件访问URL
|
||
func (h *FileHandler) GetFileURL(ctx *gin.Context) {
|
||
reqCtx := ctx.Request.Context()
|
||
|
||
// 获取文件ID
|
||
fileIDStr := ctx.Param("id")
|
||
fileID := cast.ToUint(fileIDStr)
|
||
if fileID == 0 {
|
||
logger.WarnContext(reqCtx, "GetFileURL 文件ID无效", "file_id", fileIDStr)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("文件ID无效"))
|
||
return
|
||
}
|
||
|
||
// 获取URL类型:temporary(临时,默认)或 permanent(长期)
|
||
urlType := ctx.DefaultQuery("type", "temporary")
|
||
if urlType != "temporary" && urlType != "permanent" {
|
||
logger.WarnContext(reqCtx, "GetFileURL URL类型无效", "type", urlType)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("URL类型无效,只支持 temporary 或 permanent"))
|
||
return
|
||
}
|
||
|
||
// 获取过期时间(分钟),仅对临时URL有效
|
||
expiresIn := cast.ToInt(ctx.DefaultQuery("expires_in", "60")) // 默认1小时
|
||
if expiresIn < 1 || expiresIn > 10080 { // 最大7天
|
||
expiresIn = 60
|
||
}
|
||
|
||
// 创建文件仓库
|
||
fileRepo := model.NewFileRepo(h.DB)
|
||
|
||
// 获取文件信息
|
||
fileModel, err := fileRepo.GetByID(reqCtx, fileID)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "GetFileURL 获取文件信息失败",
|
||
"error", err,
|
||
"file_id", fileID)
|
||
response.Error(ctx, err)
|
||
return
|
||
}
|
||
|
||
// 检查文件权限
|
||
if !fileModel.IsPublic {
|
||
// 获取当前用户信息
|
||
userID, exists := ctx.Get("user_id")
|
||
if !exists {
|
||
logger.WarnContext(reqCtx, "GetFileURL 未授权访问私有文件", "file_id", fileID)
|
||
response.Error(ctx, errorx.ErrUnauthorized.WithMsg("需要登录才能访问此文件"))
|
||
return
|
||
}
|
||
|
||
// 检查是否是文件上传者或管理员
|
||
if cast.ToInt64(userID) != fileModel.UploadedBy {
|
||
// 检查是否是管理员
|
||
isAdmin, exists := ctx.Get("is_admin")
|
||
if !exists || !cast.ToBool(isAdmin) {
|
||
logger.WarnContext(reqCtx, "GetFileURL 无权限访问文件",
|
||
"file_id", fileID,
|
||
"user_id", userID,
|
||
"uploaded_by", fileModel.UploadedBy)
|
||
response.Error(ctx, errorx.ErrForbidden.WithMsg("无权限访问此文件"))
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
var fileURL string
|
||
var expiresAt *int64
|
||
|
||
switch urlType {
|
||
case "permanent":
|
||
// 长期URL:仅支持公开文件,直接返回MinIO的公开URL
|
||
if !fileModel.IsPublic {
|
||
logger.WarnContext(reqCtx, "GetFileURL 私有文件不支持长期URL", "file_id", fileID)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("私有文件不支持长期URL"))
|
||
return
|
||
}
|
||
// 检查文件是否在public路径下
|
||
if !strings.HasPrefix(fileModel.Path, "public/") {
|
||
logger.WarnContext(reqCtx, "GetFileURL 文件路径不在public目录下", "file_id", fileID, "path", fileModel.Path)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("文件不在公开目录中"))
|
||
return
|
||
}
|
||
// 返回MinIO的直接访问URL(无需签名,长期有效)
|
||
directURL, err := h.storage.GetDirectURL(reqCtx, fileModel.Path)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "GetFileURL 获取直接URL失败",
|
||
"error", err,
|
||
"file_id", fileID,
|
||
"path", fileModel.Path)
|
||
response.Error(ctx, errorx.WrapError(err, "生成文件访问链接失败"))
|
||
return
|
||
}
|
||
fileURL = directURL
|
||
// 长期URL无过期时间
|
||
case "temporary":
|
||
// 临时URL:使用预签名URL
|
||
presignedURL, err := h.storage.GetPresignedURL(reqCtx, fileModel.Path, expiresIn)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "GetFileURL 生成预签名URL失败",
|
||
"error", err,
|
||
"file_id", fileID,
|
||
"path", fileModel.Path)
|
||
response.Error(ctx, errorx.WrapError(err, "生成文件访问链接失败"))
|
||
return
|
||
}
|
||
fileURL = presignedURL
|
||
expTime := time.Now().Add(time.Duration(expiresIn) * time.Minute).Unix()
|
||
expiresAt = &expTime
|
||
}
|
||
|
||
// 构建响应数据
|
||
result := map[string]interface{}{
|
||
"file_id": fileID,
|
||
"file_name": fileModel.Name,
|
||
"original_name": fileModel.OriginalName,
|
||
"url": fileURL,
|
||
"type": urlType,
|
||
"mime_type": fileModel.MimeType,
|
||
"size": fileModel.Size,
|
||
"is_public": fileModel.IsPublic,
|
||
}
|
||
|
||
// 添加过期信息(临时和长期URL都有过期时间)
|
||
if expiresAt != nil {
|
||
result["expires_at"] = *expiresAt
|
||
if urlType == "temporary" {
|
||
result["expires_in"] = expiresIn
|
||
} else {
|
||
result["expires_in"] = 7 * 24 * 60 // 7天
|
||
}
|
||
}
|
||
|
||
logger.InfoContext(reqCtx, "GetFileURL 获取文件URL成功",
|
||
"file_id", fileID,
|
||
"type", urlType,
|
||
"expires_in", expiresIn)
|
||
|
||
response.Success(ctx, result)
|
||
}
|
||
|
||
// getBaseURL 获取基础URL
|
||
func (h *FileHandler) getBaseURL(ctx *gin.Context) string {
|
||
scheme := "http"
|
||
if ctx.Request.TLS != nil {
|
||
scheme = "https"
|
||
}
|
||
return fmt.Sprintf("%s://%s", scheme, ctx.Request.Host)
|
||
}
|
||
|
||
// ServePublicFile 提供公开文件访问(长期URL的实现)
|
||
func (h *FileHandler) ServePublicFile(ctx *gin.Context) {
|
||
reqCtx := ctx.Request.Context()
|
||
|
||
// 获取文件ID
|
||
fileIDStr := ctx.Param("id")
|
||
fileID := cast.ToUint(fileIDStr)
|
||
if fileID == 0 {
|
||
logger.WarnContext(reqCtx, "ServePublicFile 文件ID无效", "file_id", fileIDStr)
|
||
ctx.JSON(404, gin.H{"error": "文件不存在"})
|
||
return
|
||
}
|
||
|
||
// 创建文件仓库
|
||
fileRepo := model.NewFileRepo(h.DB)
|
||
|
||
// 获取文件信息
|
||
fileModel, err := fileRepo.GetByID(reqCtx, fileID)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "ServePublicFile 获取文件信息失败",
|
||
"error", err,
|
||
"file_id", fileID)
|
||
ctx.JSON(404, gin.H{"error": "文件不存在"})
|
||
return
|
||
}
|
||
|
||
// 检查是否是公开文件
|
||
if !fileModel.IsPublic {
|
||
logger.WarnContext(reqCtx, "ServePublicFile 尝试访问私有文件", "file_id", fileID)
|
||
ctx.JSON(403, gin.H{"error": "文件不可公开访问"})
|
||
return
|
||
}
|
||
|
||
// 检查文件是否在public路径下
|
||
if !strings.HasPrefix(fileModel.Path, "public/") {
|
||
logger.WarnContext(reqCtx, "ServePublicFile 文件不在public目录下", "file_id", fileID, "path", fileModel.Path)
|
||
ctx.JSON(403, gin.H{"error": "文件不在公开目录中"})
|
||
return
|
||
}
|
||
|
||
// 获取MinIO直接访问URL并重定向(避免服务器代理)
|
||
directURL, err := h.storage.GetDirectURL(reqCtx, fileModel.Path)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "ServePublicFile 获取直接URL失败",
|
||
"error", err,
|
||
"file_id", fileID)
|
||
ctx.JSON(500, gin.H{"error": "文件访问失败"})
|
||
return
|
||
}
|
||
|
||
// 302重定向到MinIO直接URL
|
||
ctx.Redirect(302, directURL)
|
||
|
||
logger.InfoContext(reqCtx, "ServePublicFile 重定向到直接URL成功",
|
||
"file_id", fileID,
|
||
"direct_url", directURL)
|
||
}
|
||
|
||
// ListFiles 获取文件列表
|
||
func (h *FileHandler) ListFiles(ctx *gin.Context) {
|
||
reqCtx := ctx.Request.Context()
|
||
|
||
// 获取查询参数
|
||
page := cast.ToInt(ctx.DefaultQuery("page", "1"))
|
||
pageSize := cast.ToInt(ctx.DefaultQuery("page_size", "20"))
|
||
fileType := ctx.Query("type")
|
||
usage := ctx.Query("usage")
|
||
|
||
// 参数验证
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 100 {
|
||
pageSize = 20
|
||
}
|
||
|
||
// 创建文件仓库
|
||
fileRepo := model.NewFileRepo(h.DB)
|
||
|
||
// 获取文件列表
|
||
files, total, err := fileRepo.ListFiles(reqCtx, page, pageSize, fileType, usage, nil)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "ListFiles 获取文件列表失败",
|
||
"error", err,
|
||
"page", page,
|
||
"page_size", pageSize)
|
||
response.Error(ctx, err)
|
||
return
|
||
}
|
||
|
||
// 构建响应数据
|
||
result := map[string]interface{}{
|
||
"files": files,
|
||
"pagination": map[string]interface{}{
|
||
"page": page,
|
||
"page_size": pageSize,
|
||
"total": total,
|
||
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||
},
|
||
}
|
||
|
||
logger.InfoContext(reqCtx, "ListFiles 获取文件列表成功",
|
||
"page", page,
|
||
"page_size", pageSize,
|
||
"total", total)
|
||
|
||
response.Success(ctx, result)
|
||
}
|
||
|
||
// GetFileInfo 获取文件信息
|
||
func (h *FileHandler) GetFileInfo(ctx *gin.Context) {
|
||
reqCtx := ctx.Request.Context()
|
||
|
||
// 获取文件ID
|
||
fileIDStr := ctx.Param("id")
|
||
fileID := cast.ToUint(fileIDStr)
|
||
if fileID == 0 {
|
||
logger.WarnContext(reqCtx, "GetFileInfo 文件ID无效", "file_id", fileIDStr)
|
||
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("文件ID无效"))
|
||
return
|
||
}
|
||
|
||
// 创建文件仓库
|
||
fileRepo := model.NewFileRepo(h.DB)
|
||
|
||
// 获取文件信息
|
||
fileModel, err := fileRepo.GetByID(reqCtx, fileID)
|
||
if err != nil {
|
||
logger.ErrorContext(reqCtx, "GetFileInfo 获取文件信息失败",
|
||
"error", err,
|
||
"file_id", fileID)
|
||
response.Error(ctx, err)
|
||
return
|
||
}
|
||
|
||
logger.InfoContext(reqCtx, "GetFileInfo 获取文件信息成功",
|
||
"file_id", fileID,
|
||
"file_name", fileModel.Name)
|
||
|
||
response.Success(ctx, fileModel)
|
||
}
|