初始化2

This commit is contained in:
2026-07-10 17:33:33 +08:00
parent b51ee98afa
commit 94f6ecf901
59 changed files with 3893 additions and 980 deletions

View File

@@ -41,6 +41,24 @@ func (c *AuthController) Login(ctx *gin.Context) {
utils.Success(ctx, resp)
}
// AppLogin 应用登录(用户名+appId)
// POST /api/auth/app-login
func (c *AuthController) AppLogin(ctx *gin.Context) {
var req dto.AppLoginRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
utils.BadRequest(ctx, "参数错误: "+err.Error())
return
}
resp, err := c.authService.AppLogin(&req)
if err != nil {
utils.Unauthorized(ctx, err.Error())
return
}
utils.Success(ctx, resp)
}
// Register 用户注册
// POST /api/auth/register
func (c *AuthController) Register(ctx *gin.Context) {

View File

@@ -33,7 +33,7 @@ func (c *ChunkUploadController) Init(ctx *gin.Context) {
return
}
result, err := c.chunkService.InitUpload(req.FileName, req.FileSize, req.MD5, req.FolderID, ownerID, req.PolicyID)
result, err := c.chunkService.InitUpload(req.FileName, req.FileSize, req.MD5, uint64(req.FolderID), ownerID, req.PolicyID)
if err != nil {
utils.ServerError(ctx, err.Error())
return
@@ -79,6 +79,32 @@ func (c *ChunkUploadController) UploadChunk(ctx *gin.Context) {
utils.Success(ctx, result)
}
// ConfirmChunk 确认MinIO分片上传完成(客户端直传后调用)
// POST /api/file/upload/chunk/:uploadId/:index/confirm
func (c *ChunkUploadController) ConfirmChunk(ctx *gin.Context) {
uploadID := ctx.Param("uploadId")
indexStr := ctx.Param("index")
var index int
if _, err := fmt.Sscanf(indexStr, "%d", &index); err != nil {
utils.BadRequest(ctx, "分片索引无效")
return
}
var req struct {
ETag string `json:"etag"`
}
ctx.ShouldBindJSON(&req)
result, err := c.chunkService.UploadChunkMinIO(uploadID, index, req.ETag)
if err != nil {
utils.BadRequest(ctx, err.Error())
return
}
utils.Success(ctx, result)
}
// Merge 合并分片完成上传
// POST /api/file/upload/merge/:uploadId
func (c *ChunkUploadController) Merge(ctx *gin.Context) {

View File

@@ -37,12 +37,24 @@ func (c *FileController) Upload(ctx *gin.Context) {
return
}
// 获取目标文件夹ID
folderIDStr := ctx.DefaultQuery("folder_id", "0")
// 获取目标文件夹ID(优先查询参数, 其次表单字段)
folderIDStr := ctx.DefaultQuery("folder_id", "")
if folderIDStr == "" {
folderIDStr = ctx.PostForm("folder_id")
}
if folderIDStr == "" {
folderIDStr = "0"
}
folderID, _ := strconv.ParseUint(folderIDStr, 10, 64)
// 获取存储策略ID(可选, 0=使用默认策略)
policyIDStr := ctx.DefaultQuery("storage_policy_id", "0")
policyIDStr := ctx.DefaultQuery("storage_policy_id", "")
if policyIDStr == "" {
policyIDStr = ctx.PostForm("storage_policy_id")
}
if policyIDStr == "" {
policyIDStr = "0"
}
storagePolicyID, _ := strconv.ParseUint(policyIDStr, 10, 64)
// 调用服务层
@@ -108,7 +120,8 @@ func (c *FileController) GetDownloadURL(ctx *gin.Context) {
return
}
downloadURL, err := c.fileService.GetDownloadURL(id)
token := ctx.GetHeader("token")
downloadURL, err := c.fileService.GetDownloadURL(id, token)
if err != nil {
utils.NotFound(ctx, err.Error())
return
@@ -126,7 +139,9 @@ func (c *FileController) GetPreviewURL(ctx *gin.Context) {
return
}
previewURL, err := c.fileService.GetPreviewURL(id)
token := ctx.GetHeader("token")
previewURL, err := c.fileService.GetPreviewURL(id, token)
if err != nil {
utils.NotFound(ctx, err.Error())
return
@@ -221,14 +236,14 @@ func (c *FileController) Move(ctx *gin.Context) {
}
var req struct {
TargetFolderID uint64 `json:"target_folder_id" binding:"required"`
TargetFolderID dto.StringID `json:"target_folder_id" binding:"required"`
}
if err := ctx.ShouldBindJSON(&req); err != nil {
utils.BadRequest(ctx, "参数错误: "+err.Error())
return
}
if err := c.fileService.Move(id, req.TargetFolderID, ownerID); err != nil {
if err := c.fileService.Move(id, uint64(req.TargetFolderID), ownerID); err != nil {
utils.ServerError(ctx, err.Error())
return
}
@@ -354,3 +369,73 @@ func (c *FileController) ListTrashed(ctx *gin.Context) {
utils.Success(ctx, results)
}
// GrantPermission 授权文件权限
// POST /api/file/:id/grant
func (c *FileController) GrantPermission(ctx *gin.Context) {
ownerID := middleware.GetCurrentUserID(ctx)
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
utils.BadRequest(ctx, "无效的文件ID")
return
}
var req dto.GrantPermissionRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
utils.BadRequest(ctx, "参数错误: "+err.Error())
return
}
if err := c.fileService.GrantPermission(id, ownerID, &req); err != nil {
utils.ServerError(ctx, err.Error())
return
}
utils.SuccessWithMessage(ctx, "授权成功", nil)
}
// RevokePermission 撤销文件权限
// DELETE /api/file/:id/grant/:permId
func (c *FileController) RevokePermission(ctx *gin.Context) {
ownerID := middleware.GetCurrentUserID(ctx)
fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
utils.BadRequest(ctx, "无效的文件ID")
return
}
permID, err := strconv.ParseUint(ctx.Param("permId"), 10, 64)
if err != nil {
utils.BadRequest(ctx, "无效的权限ID")
return
}
if err := c.fileService.RevokePermission(fileID, permID, ownerID); err != nil {
utils.ServerError(ctx, err.Error())
return
}
utils.SuccessWithMessage(ctx, "已撤销", nil)
}
// ListPermissions 查看文件的授权列表
// GET /api/file/:id/grant
func (c *FileController) ListPermissions(ctx *gin.Context) {
ownerID := middleware.GetCurrentUserID(ctx)
fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
utils.BadRequest(ctx, "无效的文件ID")
return
}
results, err := c.fileService.ListPermissions(fileID, ownerID)
if err != nil {
utils.ServerError(ctx, err.Error())
return
}
utils.Success(ctx, results)
}

View File

@@ -44,8 +44,27 @@ func (c *FolderController) Create(ctx *gin.Context) {
utils.Success(ctx, result)
}
// ListChildren 获取文件夹子项
// GET /api/folder/:id/children 或 GET /api/folder/root
// GetByBizID 根据业务ID获取文件夹信息
// GET /api/folder/by-bizid/:bizid
func (c *FolderController) GetByBizID(ctx *gin.Context) {
ownerID := middleware.GetCurrentUserID(ctx)
bizID := ctx.Param("bizid")
if bizID == "" {
utils.BadRequest(ctx, "业务ID不能为空")
return
}
result, err := c.folderService.GetByBizID(bizID, ownerID)
if err != nil {
utils.NotFound(ctx, err.Error())
return
}
utils.Success(ctx, result)
}
// ListChildren 获取文件夹子项(支持分页)
// GET /api/folder/:id/children?page=1&page_size=50
func (c *FolderController) ListChildren(ctx *gin.Context) {
ownerID := middleware.GetCurrentUserID(ctx)
@@ -62,7 +81,17 @@ func (c *FolderController) ListChildren(ctx *gin.Context) {
}
}
result, err := c.folderService.ListChildren(folderID, ownerID)
// 分页参数
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(ctx.DefaultQuery("page_size", "50"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 200 {
pageSize = 50
}
result, err := c.folderService.ListChildren(folderID, ownerID, page, pageSize)
if err != nil {
utils.ServerError(ctx, err.Error())
return

View File

@@ -10,6 +10,24 @@ import (
"github.com/gin-gonic/gin"
)
// GetSystemLogs 获取系统运行日志
// GET /api/admin/monitor/logs?limit=200
func (c *MonitorController) GetSystemLogs(ctx *gin.Context) {
limit := 200
if l, err := parseIntQuery(ctx, "limit"); err == nil && l > 0 {
limit = l
}
if limit > 2000 {
limit = 2000
}
entries := utils.LogBuf.Get(limit)
utils.Success(ctx, gin.H{
"total": utils.LogBuf.Count(),
"entries": entries,
})
}
// MonitorController 监控控制器
type MonitorController struct {
fileRepo *repository.FileRepository

View File

@@ -0,0 +1,374 @@
package controller
import (
"fmt"
"net/http"
"strconv"
"time"
"seeyon-filesystem/config"
"seeyon-filesystem/dto"
"seeyon-filesystem/middleware"
"seeyon-filesystem/model"
"seeyon-filesystem/service"
"seeyon-filesystem/storage"
"seeyon-filesystem/utils"
"github.com/gin-gonic/gin"
)
// OpenAPIFileController 对外文件API控制器
type OpenAPIFileController struct {
fileService *service.FileService
folderService *service.FolderService
chunkService *service.ChunkUploadService
}
func NewOpenAPIFileController() *OpenAPIFileController {
return &OpenAPIFileController{
fileService: service.NewFileService(),
folderService: service.NewFolderService(),
chunkService: service.NewChunkUploadService(),
}
}
// ========== 1. 根据父文件夹ID分页查询 ==========
// ListByFolder 分页查询文件夹下的文件和子文件夹
// GET /api/openapi/files?folder_id=0&page=1&page_size=20
// 返回树状结构: 文件夹 + 文件
func (c *OpenAPIFileController) ListByFolder(ctx *gin.Context) {
userID := middleware.GetCurrentUserID(ctx)
folderIDStr := ctx.DefaultQuery("folder_id", "0")
folderID, _ := strconv.ParseUint(folderIDStr, 10, 64)
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(ctx.DefaultQuery("page_size", "50"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 200 {
pageSize = 50
}
// 获取用户角色
roleIDs := getUserRoleIDs(userID)
// 查询文件夹
folders, err := getFoldersByParent(folderID, userID, page, pageSize)
if err != nil {
utils.ServerError(ctx, "查询文件夹失败")
return
}
// 查询文件
files, totalFiles, err := getFilesByFolder(folderID, userID, roleIDs, page, pageSize)
if err != nil {
utils.ServerError(ctx, "查询文件失败")
return
}
// 组装树状结构
result := dto.OpenAPIListResult{
FolderID: folderID,
TotalFiles: totalFiles,
Page: page,
PageSize: pageSize,
}
for _, f := range folders {
childCount := getFolderChildCount(f.ID)
result.Folders = append(result.Folders, dto.OpenAPIFolderVO{
ID: dto.StringID(f.ID),
Name: f.Name,
ParentID: dto.StringID(f.ParentID),
ChildCount: childCount,
CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
for _, f := range files {
result.Files = append(result.Files, dto.OpenAPIFileVO{
ID: dto.StringID(f.ID),
Name: f.Name,
Extension: f.Extension,
Size: f.Size,
MD5: f.MD5,
MimeType: f.MimeType,
DownloadURL: getCDNOrPresignedURL(&f),
CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
utils.Success(ctx, result)
}
// ========== 2. 按名称搜索返回树状结构 ==========
// SearchTree 按文件名/文件夹名搜索, 返回树状结构
// GET /api/openapi/files/search?keyword=报告&page=1&page_size=20
func (c *OpenAPIFileController) SearchTree(ctx *gin.Context) {
userID := middleware.GetCurrentUserID(ctx)
keyword := ctx.Query("keyword")
if keyword == "" {
utils.BadRequest(ctx, "请输入搜索关键词")
return
}
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(ctx.DefaultQuery("page_size", "50"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 200 {
pageSize = 50
}
roleIDs := getUserRoleIDs(userID)
// 搜索文件
files, total, err := searchFilesByName(userID, roleIDs, keyword, page, pageSize)
if err != nil {
utils.ServerError(ctx, "搜索失败")
return
}
// 搜索文件夹
folders, err := searchFoldersByName(userID, keyword, page, pageSize)
if err != nil {
utils.ServerError(ctx, "搜索失败")
return
}
result := dto.OpenAPISearchResult{
Keyword: keyword,
TotalFiles: total,
Page: page,
PageSize: pageSize,
}
for _, f := range folders {
result.Folders = append(result.Folders, dto.OpenAPIFolderVO{
ID: dto.StringID(f.ID),
Name: f.Name,
ParentID: dto.StringID(f.ParentID),
CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
for _, f := range files {
result.Files = append(result.Files, dto.OpenAPIFileVO{
ID: dto.StringID(f.ID),
Name: f.Name,
Extension: f.Extension,
Size: f.Size,
MD5: f.MD5,
MimeType: f.MimeType,
DownloadURL: getCDNOrPresignedURL(&f),
CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
utils.Success(ctx, result)
}
// ========== 3. 分片上传/下载 ==========
// ChunkUploadInit 初始化分片上传
// POST /api/openapi/files/upload/init
func (c *OpenAPIFileController) ChunkUploadInit(ctx *gin.Context) {
userID := middleware.GetCurrentUserID(ctx)
var req dto.ChunkUploadInitRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
utils.BadRequest(ctx, "参数错误: "+err.Error())
return
}
result, err := c.chunkService.InitUpload(req.FileName, req.FileSize, req.MD5, uint64(req.FolderID), userID, req.PolicyID)
if err != nil {
utils.ServerError(ctx, err.Error())
return
}
utils.Success(ctx, result)
}
// ChunkUpload 上传分片
// PUT /api/openapi/files/upload/chunk/:uploadId/:index
func (c *OpenAPIFileController) ChunkUpload(ctx *gin.Context) {
uploadID := ctx.Param("uploadId")
indexStr := ctx.Param("index")
index, err := strconv.Atoi(indexStr)
if err != nil {
utils.BadRequest(ctx, "分片索引无效")
return
}
checksum := ctx.GetHeader("X-Checksum")
file, err := ctx.FormFile("chunk")
if err != nil {
utils.BadRequest(ctx, "请上传分片文件")
return
}
reader, err := file.Open()
if err != nil {
utils.BadRequest(ctx, "读取分片失败")
return
}
defer reader.Close()
result, err := c.chunkService.UploadChunk(uploadID, index, reader, checksum)
if err != nil {
utils.BadRequest(ctx, err.Error())
return
}
utils.Success(ctx, result)
}
// ChunkMerge 合并分片
// POST /api/openapi/files/upload/merge/:uploadId
func (c *OpenAPIFileController) ChunkMerge(ctx *gin.Context) {
uploadID := ctx.Param("uploadId")
result, err := c.chunkService.MergeChunks(uploadID)
if err != nil {
utils.ServerError(ctx, err.Error())
return
}
utils.Success(ctx, result)
}
// ChunkProgress 查询上传进度
// GET /api/openapi/files/upload/progress/:uploadId
func (c *OpenAPIFileController) ChunkProgress(ctx *gin.Context) {
uploadID := ctx.Param("uploadId")
result, err := c.chunkService.GetUploadProgress(uploadID)
if err != nil {
utils.NotFound(ctx, err.Error())
return
}
utils.Success(ctx, result)
}
// Download 下载文件
// GET /api/openapi/files/:id/download
func (c *OpenAPIFileController) Download(ctx *gin.Context) {
userID := middleware.GetCurrentUserID(ctx)
fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
utils.BadRequest(ctx, "无效的文件ID")
return
}
reader, file, err := c.fileService.Download(fileID, userID)
if err != nil {
utils.ServerError(ctx, err.Error())
return
}
defer reader.Close()
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Name))
ctx.Header("Content-Type", file.MimeType)
ctx.Header("Content-Length", strconv.FormatInt(file.Size, 10))
ctx.DataFromReader(http.StatusOK, file.Size, file.MimeType, reader, nil)
}
// Preview 预览文件
// GET /api/openapi/files/:id/preview
func (c *OpenAPIFileController) Preview(ctx *gin.Context) {
userID := middleware.GetCurrentUserID(ctx)
fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
utils.BadRequest(ctx, "无效的文件ID")
return
}
reader, file, err := c.fileService.Download(fileID, userID)
if err != nil {
utils.ServerError(ctx, err.Error())
return
}
defer reader.Close()
ctx.Header("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", file.Name))
ctx.Header("Content-Type", file.MimeType)
ctx.Header("Content-Length", strconv.FormatInt(file.Size, 10))
ctx.DataFromReader(http.StatusOK, file.Size, file.MimeType, reader, nil)
}
// ========== 辅助函数 ==========
func getUserRoleIDs(userID uint64) []uint64 {
var userRoles []model.UserRole
config.DB.Where("user_id = ?", userID).Find(&userRoles)
ids := make([]uint64, 0, len(userRoles))
for _, ur := range userRoles {
ids = append(ids, ur.RoleID)
}
return ids
}
func getFoldersByParent(parentID, ownerID uint64, page, pageSize int) ([]model.Folder, error) {
var folders []model.Folder
err := config.DB.Where("parent_id = ? AND owner_id = ? AND status = 1", parentID, ownerID).
Order("name ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&folders).Error
return folders, err
}
func getFilesByFolder(folderID, userID uint64, roleIDs []uint64, page, pageSize int) ([]model.File, int64, error) {
var files []model.File
query := config.DB.Where("folder_id = ? AND owner_id = ? AND status = 1", folderID, userID)
var total int64
query.Model(&model.File{}).Count(&total)
err := query.Order("name ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&files).Error
return files, total, err
}
func getFolderChildCount(folderID uint64) int64 {
var count int64
config.DB.Model(&model.Folder{}).Where("parent_id = ? AND status = 1", folderID).Count(&count)
return count
}
func searchFilesByName(userID uint64, roleIDs []uint64, keyword string, page, pageSize int) ([]model.File, int64, error) {
var files []model.File
query := config.DB.Where("owner_id = ? AND status = 1 AND name LIKE ?", userID, "%"+keyword+"%")
var total int64
query.Model(&model.File{}).Count(&total)
err := query.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&files).Error
return files, total, err
}
func searchFoldersByName(ownerID uint64, keyword string, page, pageSize int) ([]model.Folder, error) {
var folders []model.Folder
err := config.DB.Where("owner_id = ? AND status = 1 AND name LIKE ?", ownerID, "%"+keyword+"%").
Order("name ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&folders).Error
return folders, err
}
func getCDNOrPresignedURL(file *model.File) string {
if file.StorageKey == "" {
return ""
}
engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID)
if err != nil {
return ""
}
if localEngine, ok := engine.(*storage.LocalEngine); ok {
return localEngine.GetURL(file.StorageKey)
}
if minioEngine, ok := engine.(*storage.MinIOEngine); ok {
url, err := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour)
if err == nil {
return url
}
}
return ""
}