初始化
This commit is contained in:
183
server/controller/backup_controller.go
Normal file
183
server/controller/backup_controller.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/service"
|
||||
"seeyon-filesystem/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// BackupController 备份控制器
|
||||
type BackupController struct {
|
||||
backupService *service.BackupService
|
||||
}
|
||||
|
||||
func NewBackupController() *BackupController {
|
||||
return &BackupController{
|
||||
backupService: service.NewBackupService(),
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 策略管理 ==========
|
||||
|
||||
// ListPolicies 备份策略列表
|
||||
// GET /api/admin/backup/policies
|
||||
func (c *BackupController) ListPolicies(ctx *gin.Context) {
|
||||
policies, err := c.backupService.ListPolicies()
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, policies)
|
||||
}
|
||||
|
||||
// CreatePolicy 创建备份策略
|
||||
// POST /api/admin/backup/policies
|
||||
func (c *BackupController) CreatePolicy(ctx *gin.Context) {
|
||||
var req dto.BackupPolicyCreateRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.backupService.CreatePolicy(&req); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "创建成功", nil)
|
||||
}
|
||||
|
||||
// DeletePolicy 删除备份策略
|
||||
// DELETE /api/admin/backup/policies/:id
|
||||
func (c *BackupController) DeletePolicy(ctx *gin.Context) {
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的ID")
|
||||
return
|
||||
}
|
||||
if err := c.backupService.DeletePolicy(id); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "删除成功", nil)
|
||||
}
|
||||
|
||||
// TogglePolicy 启用/禁用备份策略
|
||||
// PUT /api/admin/backup/policies/:id/toggle
|
||||
func (c *BackupController) TogglePolicy(ctx *gin.Context) {
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的ID")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := c.backupService.TogglePolicy(id, req.Status); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "操作成功", nil)
|
||||
}
|
||||
|
||||
// ========== 执行备份 ==========
|
||||
|
||||
// ExecuteBackup 手动执行备份
|
||||
// POST /api/admin/backup/execute/:id
|
||||
func (c *BackupController) ExecuteBackup(ctx *gin.Context) {
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
log, err := c.backupService.ExecuteBackup(id)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(ctx, gin.H{
|
||||
"log_id": log.ID,
|
||||
"message": "备份任务已启动",
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 备份日志 ==========
|
||||
|
||||
// ListLogs 备份日志列表
|
||||
// GET /api/admin/backup/logs?policy_id=1&page=1&size=20
|
||||
func (c *BackupController) ListLogs(ctx *gin.Context) {
|
||||
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(ctx.DefaultQuery("size", "20"))
|
||||
policyID, _ := strconv.ParseUint(ctx.Query("policy_id"), 10, 64)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 || size > 100 {
|
||||
size = 20
|
||||
}
|
||||
|
||||
logs, total, err := c.backupService.ListLogs(policyID, page, size)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.PageSuccess(ctx, logs, total, page, size)
|
||||
}
|
||||
|
||||
// ========== 恢复 ==========
|
||||
|
||||
// ListBackups 列出可恢复的备份
|
||||
// GET /api/admin/backup/backups
|
||||
func (c *BackupController) ListBackups(ctx *gin.Context) {
|
||||
backups, err := c.backupService.ListBackups()
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, backups)
|
||||
}
|
||||
|
||||
// Restore 从备份恢复
|
||||
// POST /api/admin/backup/restore/:logId
|
||||
func (c *BackupController) Restore(ctx *gin.Context) {
|
||||
logID, err := strconv.ParseUint(ctx.Param("logId"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的备份ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
RestoreDB bool `json:"restore_db"`
|
||||
RestoreFiles bool `json:"restore_files"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if !req.RestoreDB && !req.RestoreFiles {
|
||||
utils.BadRequest(ctx, "请选择要恢复的内容(数据库或文件)")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := c.backupService.RestoreFromBackup(logID, req.RestoreDB, req.RestoreFiles)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if result.Success {
|
||||
utils.Success(ctx, result)
|
||||
} else {
|
||||
utils.ServerError(ctx, result.Message)
|
||||
}
|
||||
}
|
||||
121
server/controller/chunk_upload_controller.go
Normal file
121
server/controller/chunk_upload_controller.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/middleware"
|
||||
"seeyon-filesystem/service"
|
||||
"seeyon-filesystem/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChunkUploadController 分片上传控制器
|
||||
type ChunkUploadController struct {
|
||||
chunkService *service.ChunkUploadService
|
||||
}
|
||||
|
||||
func NewChunkUploadController() *ChunkUploadController {
|
||||
return &ChunkUploadController{
|
||||
chunkService: service.NewChunkUploadService(),
|
||||
}
|
||||
}
|
||||
|
||||
// Init 初始化分片上传
|
||||
// POST /api/file/upload/init
|
||||
func (c *ChunkUploadController) Init(ctx *gin.Context) {
|
||||
ownerID := 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, req.FolderID, ownerID, req.PolicyID)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(ctx, result)
|
||||
}
|
||||
|
||||
// UploadChunk 上传单个分片
|
||||
// PUT /api/file/upload/chunk/:uploadId/:index
|
||||
func (c *ChunkUploadController) UploadChunk(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
|
||||
}
|
||||
|
||||
checksum := ctx.GetHeader("X-Checksum")
|
||||
|
||||
file, err := ctx.FormFile("chunk")
|
||||
if err != nil {
|
||||
// 尝试从body读取
|
||||
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)
|
||||
}
|
||||
|
||||
// Merge 合并分片完成上传
|
||||
// POST /api/file/upload/merge/:uploadId
|
||||
func (c *ChunkUploadController) Merge(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)
|
||||
}
|
||||
|
||||
// Progress 查询上传进度
|
||||
// GET /api/file/upload/progress/:uploadId
|
||||
func (c *ChunkUploadController) Progress(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)
|
||||
}
|
||||
|
||||
// Cancel 取消上传
|
||||
// DELETE /api/file/upload/:uploadId
|
||||
func (c *ChunkUploadController) Cancel(ctx *gin.Context) {
|
||||
uploadID := ctx.Param("uploadId")
|
||||
|
||||
if err := c.chunkService.CancelUpload(uploadID); err != nil {
|
||||
utils.BadRequest(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessWithMessage(ctx, "已取消", nil)
|
||||
}
|
||||
277
server/controller/file_access_controller.go
Normal file
277
server/controller/file_access_controller.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"seeyon-filesystem/config"
|
||||
"seeyon-filesystem/middleware"
|
||||
"seeyon-filesystem/model"
|
||||
"seeyon-filesystem/storage"
|
||||
"seeyon-filesystem/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// FileAccessController 文件访问控制器
|
||||
// 统一处理文件下载/预览, 支持中转和预签名两种模式
|
||||
type FileAccessController struct{}
|
||||
|
||||
func NewFileAccessController() *FileAccessController {
|
||||
return &FileAccessController{}
|
||||
}
|
||||
|
||||
// Access 统一文件访问入口
|
||||
// GET /api/file/:id/access
|
||||
// 根据存储策略自动选择: 中转(proxy) / 预签名(presigned) / CDN
|
||||
func (c *FileAccessController) Access(ctx *gin.Context) {
|
||||
ownerID := middleware.GetCurrentUserID(ctx)
|
||||
fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的文件ID")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := getFileByID(fileID)
|
||||
if err != nil {
|
||||
utils.NotFound(ctx, "文件不存在")
|
||||
return
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
utils.Forbidden(ctx, "无权访问")
|
||||
return
|
||||
}
|
||||
|
||||
c.serveFile(ctx, file)
|
||||
}
|
||||
|
||||
// AccessByShare 通过分享访问文件(不检查归属)
|
||||
// GET /api/s/:code/access
|
||||
func (c *FileAccessController) AccessByShare(ctx *gin.Context) {
|
||||
code := ctx.Param("code")
|
||||
|
||||
// 验证分享
|
||||
share, err := getShareByCode(code)
|
||||
if err != nil {
|
||||
utils.NotFound(ctx, "分享不存在或已失效")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查密码
|
||||
if share.Password != "" {
|
||||
password := ctx.Query("password")
|
||||
if !verifySharePassword(share, password) {
|
||||
utils.Unauthorized(ctx, "需要密码")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查过期
|
||||
if share.ExpireAt != nil && share.ExpireAt.Before(time.Now()) {
|
||||
utils.NotFound(ctx, "分享已过期")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := getFileByID(share.FileID)
|
||||
if err != nil {
|
||||
utils.NotFound(ctx, "文件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.serveFile(ctx, file)
|
||||
}
|
||||
|
||||
// serveFile 根据存储策略配置的访问模式提供文件
|
||||
func (c *FileAccessController) serveFile(ctx *gin.Context, file *model.File) {
|
||||
// 获取存储策略
|
||||
policy := getStoragePolicy(file.StoragePolicyID)
|
||||
|
||||
// 获取存储引擎
|
||||
engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "存储引擎不可用")
|
||||
return
|
||||
}
|
||||
|
||||
accessMode := policy.Config.AccessMode
|
||||
|
||||
switch accessMode {
|
||||
case "presigned":
|
||||
// 预签名模式: 返回URL, 客户端直连MinIO
|
||||
c.servePresigned(ctx, file, engine, policy)
|
||||
case "cdn":
|
||||
// CDN模式: 返回CDN链接
|
||||
c.serveCDN(ctx, file, engine)
|
||||
default:
|
||||
// 中转模式(默认): 服务端拉取文件流转发给客户端
|
||||
c.serveProxy(ctx, file, engine, policy)
|
||||
}
|
||||
}
|
||||
|
||||
// serveProxy 中转模式: 服务端从存储拉取文件, 转发给客户端
|
||||
func (c *FileAccessController) serveProxy(ctx *gin.Context, file *model.File, engine storage.Engine, policy *model.StoragePolicy) {
|
||||
reader, err := engine.Download(file.StorageKey)
|
||||
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.Header("Cache-Control", "private, max-age=3600")
|
||||
|
||||
// X-Sendfile模式: 返回路径让Nginx直接发送文件
|
||||
if policy.Config.UseXSendfile && policy.Config.XSendfilePath != "" {
|
||||
// Nginx会拦截这个头, 直接从本地路径发送文件
|
||||
localPath := fmt.Sprintf("%s/%s", policy.Config.XSendfilePath, file.StorageKey)
|
||||
ctx.Header("X-Accel-Redirect", localPath)
|
||||
ctx.Status(200)
|
||||
return
|
||||
}
|
||||
|
||||
// 普通中转: 流式传输
|
||||
ctx.Status(200)
|
||||
io.Copy(ctx.Writer, reader)
|
||||
}
|
||||
|
||||
// servePresigned 预签名模式: 返回MinIO预签名URL
|
||||
func (c *FileAccessController) servePresigned(ctx *gin.Context, file *model.File, engine storage.Engine, policy *model.StoragePolicy) {
|
||||
minioEngine, ok := engine.(*storage.MinIOEngine)
|
||||
if !ok {
|
||||
// 非MinIO引擎, 回退到中转模式
|
||||
c.serveProxy(ctx, file, engine, policy)
|
||||
return
|
||||
}
|
||||
|
||||
presignedURL, err := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "生成预签名URL失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 替换为Nginx公网地址
|
||||
if policy.Config.NginxEndpoint != "" {
|
||||
presignedURL = replaceMinIOEndpoint(presignedURL, minioEngine.Endpoint, policy.Config.NginxEndpoint)
|
||||
}
|
||||
|
||||
// 返回302重定向到预签名URL
|
||||
ctx.Redirect(http.StatusFound, presignedURL)
|
||||
}
|
||||
|
||||
// serveCDN CDN模式: 返回CDN链接
|
||||
func (c *FileAccessController) serveCDN(ctx *gin.Context, file *model.File, engine storage.Engine) {
|
||||
if localEngine, ok := engine.(*storage.LocalEngine); ok {
|
||||
cdnURL := localEngine.GetURL(file.StorageKey)
|
||||
ctx.Redirect(http.StatusFound, cdnURL)
|
||||
return
|
||||
}
|
||||
// 回退到中转
|
||||
c.serveProxy(ctx, file, engine, &model.StoragePolicy{})
|
||||
}
|
||||
|
||||
// GetAccessURL 获取文件访问URL(不重定向, 返回JSON)
|
||||
// GET /api/file/:id/access-url
|
||||
func (c *FileAccessController) GetAccessURL(ctx *gin.Context) {
|
||||
ownerID := middleware.GetCurrentUserID(ctx)
|
||||
fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的文件ID")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := getFileByID(fileID)
|
||||
if err != nil {
|
||||
utils.NotFound(ctx, "文件不存在")
|
||||
return
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
utils.Forbidden(ctx, "无权访问")
|
||||
return
|
||||
}
|
||||
|
||||
policy := getStoragePolicy(file.StoragePolicyID)
|
||||
engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "存储引擎不可用")
|
||||
return
|
||||
}
|
||||
|
||||
result := gin.H{
|
||||
"mode": policy.Config.AccessMode,
|
||||
"file_name": file.Name,
|
||||
"file_size": file.Size,
|
||||
"content_type": file.MimeType,
|
||||
}
|
||||
|
||||
switch policy.Config.AccessMode {
|
||||
case "presigned":
|
||||
if minioEngine, ok := engine.(*storage.MinIOEngine); ok {
|
||||
presignedURL, _ := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour)
|
||||
if policy.Config.NginxEndpoint != "" {
|
||||
presignedURL = replaceMinIOEndpoint(presignedURL, minioEngine.Endpoint, policy.Config.NginxEndpoint)
|
||||
}
|
||||
result["url"] = presignedURL
|
||||
}
|
||||
case "cdn":
|
||||
if localEngine, ok := engine.(*storage.LocalEngine); ok {
|
||||
result["url"] = localEngine.GetURL(file.StorageKey)
|
||||
}
|
||||
default:
|
||||
result["url"] = fmt.Sprintf("/api/file/%d/access", fileID)
|
||||
result["mode"] = "proxy"
|
||||
}
|
||||
|
||||
utils.Success(ctx, result)
|
||||
}
|
||||
|
||||
// ========== 辅助函数 ==========
|
||||
|
||||
func getFileByID(id uint64) (*model.File, error) {
|
||||
var file model.File
|
||||
if err := config.DB.First(&file, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func getShareByCode(code string) (*model.Share, error) {
|
||||
var share model.Share
|
||||
if err := config.DB.Where("share_code = ? AND status = 1", code).First(&share).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &share, nil
|
||||
}
|
||||
|
||||
func verifySharePassword(share *model.Share, password string) bool {
|
||||
return utils.CheckPassword(password, share.Password)
|
||||
}
|
||||
|
||||
func getStoragePolicy(policyID uint64) *model.StoragePolicy {
|
||||
var policy model.StoragePolicy
|
||||
if policyID > 0 {
|
||||
config.DB.First(&policy, policyID)
|
||||
}
|
||||
if policy.Config.AccessMode == "" {
|
||||
policy.Config.AccessMode = "proxy"
|
||||
}
|
||||
return &policy
|
||||
}
|
||||
|
||||
// replaceMinIOEndpoint 替换预签名URL中的MinIO地址为Nginx公网地址
|
||||
func replaceMinIOEndpoint(url, minioEndpoint, nginxEndpoint string) string {
|
||||
old := "http://" + minioEndpoint
|
||||
if url[:len(old)] == old {
|
||||
return nginxEndpoint + url[len(old):]
|
||||
}
|
||||
old = "https://" + minioEndpoint
|
||||
if len(url) > len(old) && url[:len(old)] == old {
|
||||
return nginxEndpoint + url[len(old):]
|
||||
}
|
||||
return url
|
||||
}
|
||||
76
server/controller/log_controller.go
Normal file
76
server/controller/log_controller.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/service"
|
||||
"seeyon-filesystem/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LogController 日志控制器
|
||||
type LogController struct {
|
||||
logService *service.LogService
|
||||
}
|
||||
|
||||
func NewLogController() *LogController {
|
||||
return &LogController{
|
||||
logService: service.NewLogService(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 查询操作日志
|
||||
// GET /api/admin/logs?page=1&size=20&action=upload&resource=file&keyword=xxx
|
||||
func (c *LogController) List(ctx *gin.Context) {
|
||||
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(ctx.DefaultQuery("size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 || size > 100 {
|
||||
size = 20
|
||||
}
|
||||
|
||||
action := ctx.Query("action")
|
||||
resource := ctx.Query("resource")
|
||||
keyword := ctx.Query("keyword")
|
||||
|
||||
logs, total, err := c.logService.List(page, size, action, resource, keyword, 0)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
utils.PageSuccess(ctx, logs, total, page, size)
|
||||
}
|
||||
|
||||
// Stats 日志统计
|
||||
// GET /api/admin/logs/stats
|
||||
func (c *LogController) Stats(ctx *gin.Context) {
|
||||
stats, err := c.logService.GetStats()
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, stats)
|
||||
}
|
||||
|
||||
// Clean 清理旧日志
|
||||
// POST /api/admin/logs/clean
|
||||
func (c *LogController) Clean(ctx *gin.Context) {
|
||||
var req dto.LogCleanRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
count, err := c.logService.CleanOld(req.Days)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "清理失败")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessWithMessage(ctx, "清理完成", gin.H{"deleted": count})
|
||||
}
|
||||
126
server/controller/monitor_controller.go
Normal file
126
server/controller/monitor_controller.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"seeyon-filesystem/middleware"
|
||||
"seeyon-filesystem/repository"
|
||||
"seeyon-filesystem/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MonitorController 监控控制器
|
||||
type MonitorController struct {
|
||||
fileRepo *repository.FileRepository
|
||||
}
|
||||
|
||||
func NewMonitorController() *MonitorController {
|
||||
return &MonitorController{
|
||||
fileRepo: repository.NewFileRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetTrafficStats 获取流量统计
|
||||
// GET /api/admin/monitor/traffic
|
||||
func (c *MonitorController) GetTrafficStats(ctx *gin.Context) {
|
||||
info := middleware.GetTrafficInfo()
|
||||
utils.Success(ctx, info)
|
||||
}
|
||||
|
||||
// GetRecentRequests 获取最近请求记录
|
||||
// GET /api/admin/monitor/requests?limit=50
|
||||
func (c *MonitorController) GetRecentRequests(ctx *gin.Context) {
|
||||
limit := 50
|
||||
if l, err := parseIntQuery(ctx, "limit"); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
if limit > 500 {
|
||||
limit = 500
|
||||
}
|
||||
|
||||
requests := middleware.GetRecentRequests(limit)
|
||||
utils.Success(ctx, requests)
|
||||
}
|
||||
|
||||
// GetStorageStats 获取存储统计(按策略分组)
|
||||
// GET /api/admin/monitor/storage
|
||||
func (c *MonitorController) GetStorageStats(ctx *gin.Context) {
|
||||
stats, err := c.fileRepo.GetPolicyStats()
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
global, _ := c.fileRepo.GetGlobalStats()
|
||||
utils.Success(ctx, gin.H{
|
||||
"policies": stats,
|
||||
"global": global,
|
||||
})
|
||||
}
|
||||
|
||||
// ========== IP 黑名单管理 ==========
|
||||
|
||||
// GetBlacklist 获取IP黑名单
|
||||
// GET /api/admin/blacklist
|
||||
func (c *MonitorController) GetBlacklist(ctx *gin.Context) {
|
||||
ips := middleware.IPBlacklist.GetBlockedIPs()
|
||||
utils.Success(ctx, gin.H{
|
||||
"enabled": middleware.IPBlacklist.IsEnabled(),
|
||||
"ips": ips,
|
||||
})
|
||||
}
|
||||
|
||||
// SetBlacklist 设置IP黑名单(全量替换)
|
||||
// PUT /api/admin/blacklist
|
||||
func (c *MonitorController) SetBlacklist(ctx *gin.Context) {
|
||||
var req struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
IPs []string `json:"ips"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
middleware.IPBlacklist.SetEnabled(req.Enabled)
|
||||
if req.IPs != nil {
|
||||
middleware.IPBlacklist.SetBlockedIPs(req.IPs)
|
||||
}
|
||||
|
||||
utils.SuccessWithMessage(ctx, "黑名单已更新", nil)
|
||||
}
|
||||
|
||||
// AddBlockedIP 添加封禁IP
|
||||
// POST /api/admin/blacklist
|
||||
func (c *MonitorController) AddBlockedIP(ctx *gin.Context) {
|
||||
var req struct {
|
||||
IP string `json:"ip" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
middleware.IPBlacklist.AddBlockedIP(req.IP)
|
||||
utils.SuccessWithMessage(ctx, "已添加", nil)
|
||||
}
|
||||
|
||||
// RemoveBlockedIP 移除封禁IP
|
||||
// DELETE /api/admin/blacklist/:ip
|
||||
func (c *MonitorController) RemoveBlockedIP(ctx *gin.Context) {
|
||||
ip := ctx.Param("ip")
|
||||
middleware.IPBlacklist.RemoveBlockedIP(ip)
|
||||
utils.SuccessWithMessage(ctx, "已移除", nil)
|
||||
}
|
||||
|
||||
// parseIntQuery 解析查询参数中的整数
|
||||
func parseIntQuery(ctx *gin.Context, key string) (int, error) {
|
||||
val := ctx.Query(key)
|
||||
if val == "" {
|
||||
return 0, nil
|
||||
}
|
||||
var result int
|
||||
_, err := fmt.Sscanf(val, "%d", &result)
|
||||
return result, err
|
||||
}
|
||||
192
server/controller/openapi_controller.go
Normal file
192
server/controller/openapi_controller.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/service"
|
||||
"seeyon-filesystem/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// OpenAPIController 开放平台控制器
|
||||
type OpenAPIController struct {
|
||||
openService *service.OpenAPIService
|
||||
}
|
||||
|
||||
func NewOpenAPIController() *OpenAPIController {
|
||||
return &OpenAPIController{
|
||||
openService: service.NewOpenAPIService(),
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 管理员接口 ==========
|
||||
|
||||
// ListApps 获取应用列表
|
||||
// GET /api/admin/apps
|
||||
func (c *OpenAPIController) ListApps(ctx *gin.Context) {
|
||||
apps, err := c.openService.ListApps()
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, apps)
|
||||
}
|
||||
|
||||
// CreateApp 创建应用
|
||||
// POST /api/admin/apps
|
||||
func (c *OpenAPIController) CreateApp(ctx *gin.Context) {
|
||||
var req dto.AppCreateRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
app, err := c.openService.CreateApp(req.AppName)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, app)
|
||||
}
|
||||
|
||||
// DeleteApp 删除应用
|
||||
// DELETE /api/admin/apps/:id
|
||||
func (c *OpenAPIController) DeleteApp(ctx *gin.Context) {
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的ID")
|
||||
return
|
||||
}
|
||||
if err := c.openService.DeleteApp(id); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "删除成功", nil)
|
||||
}
|
||||
|
||||
// ToggleApp 启用/禁用应用
|
||||
// PUT /api/admin/apps/:id/toggle
|
||||
func (c *OpenAPIController) ToggleApp(ctx *gin.Context) {
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.openService.ToggleApp(id, req.Status); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "操作成功", nil)
|
||||
}
|
||||
|
||||
// BindUsers 绑定用户到应用
|
||||
// PUT /api/admin/apps/:id/users
|
||||
func (c *OpenAPIController) BindUsers(ctx *gin.Context) {
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.AppBindUsersRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.openService.BindUsers(id, req.UserIDs); err != nil {
|
||||
utils.BadRequest(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "绑定成功", nil)
|
||||
}
|
||||
|
||||
// UnbindUser 解绑用户
|
||||
// DELETE /api/admin/apps/:id/users/:userId
|
||||
func (c *OpenAPIController) UnbindUser(ctx *gin.Context) {
|
||||
appID, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的应用ID")
|
||||
return
|
||||
}
|
||||
userID, err := strconv.ParseUint(ctx.Param("userId"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的用户ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.openService.UnbindUser(appID, userID); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "解绑成功", nil)
|
||||
}
|
||||
|
||||
// ========== 开放API (给OA调用) ==========
|
||||
|
||||
// ValidateToken 验证SSO Token
|
||||
// POST /api/openapi/token/validate
|
||||
// Body: { "app_id": "xxx", "token": "encrypted-string" }
|
||||
// 返回: JWT token
|
||||
func (c *OpenAPIController) ValidateToken(ctx *gin.Context) {
|
||||
var req struct {
|
||||
AppID string `json:"app_id" binding:"required"`
|
||||
Token string `json:"token" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误: app_id和token必填")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证token, 获取用户名
|
||||
username, err := c.openService.ValidateToken(req.AppID, req.Token)
|
||||
if err != nil {
|
||||
utils.Unauthorized(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 生成JWT
|
||||
result, err := c.openService.GenerateJWTForUser(username)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(ctx, result)
|
||||
}
|
||||
|
||||
// GenerateTestToken 生成测试Token (仅调试用)
|
||||
// POST /api/openapi/token/generate
|
||||
// Body: { "app_id": "xxx", "username": "admin" }
|
||||
func (c *OpenAPIController) GenerateTestToken(ctx *gin.Context) {
|
||||
var req struct {
|
||||
AppID string `json:"app_id" binding:"required"`
|
||||
Username string `json:"username" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := c.openService.GenerateToken(req.AppID, req.Username)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(ctx, gin.H{
|
||||
"token": token,
|
||||
"usage": "OA系统使用此token调用 POST /api/openapi/token/validate 获取JWT",
|
||||
})
|
||||
}
|
||||
151
server/controller/storage_assign_controller.go
Normal file
151
server/controller/storage_assign_controller.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/middleware"
|
||||
"seeyon-filesystem/service"
|
||||
"seeyon-filesystem/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// StorageAssignController 存储策略分配控制器
|
||||
type StorageAssignController struct {
|
||||
assignService *service.StorageAssignService
|
||||
}
|
||||
|
||||
func NewStorageAssignController() *StorageAssignController {
|
||||
return &StorageAssignController{
|
||||
assignService: service.NewStorageAssignService(),
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 用户接口 ==========
|
||||
|
||||
// GetMyStorageInfo 获取当前用户的存储策略信息
|
||||
// GET /api/user/storage-info
|
||||
func (c *StorageAssignController) GetMyStorageInfo(ctx *gin.Context) {
|
||||
userID := middleware.GetCurrentUserID(ctx)
|
||||
info, err := c.assignService.GetUserStorageInfo(userID)
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "获取存储信息失败")
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, info)
|
||||
}
|
||||
|
||||
// ========== 管理员接口 ==========
|
||||
|
||||
// ListAssignments 获取所有分配规则
|
||||
// GET /api/admin/storage-assignments
|
||||
func (c *StorageAssignController) ListAssignments(ctx *gin.Context) {
|
||||
assignments, err := c.assignService.ListAssignments()
|
||||
if err != nil {
|
||||
utils.ServerError(ctx, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.Success(ctx, assignments)
|
||||
}
|
||||
|
||||
// SetDefault 设置系统默认存储策略
|
||||
// POST /api/admin/storage-assignments/default
|
||||
func (c *StorageAssignController) SetDefault(ctx *gin.Context) {
|
||||
var req struct {
|
||||
PolicyID uint64 `json:"policy_id" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.assignService.SetDefault(req.PolicyID); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "默认策略已设置", nil)
|
||||
}
|
||||
|
||||
// AssignToUser 为用户分配存储策略
|
||||
// POST /api/admin/storage-assignments/user
|
||||
func (c *StorageAssignController) AssignToUser(ctx *gin.Context) {
|
||||
var req dto.StorageAssignRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
if req.UserID == 0 {
|
||||
utils.BadRequest(ctx, "请指定用户ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.assignService.AssignToUser(req.UserID, req.PolicyID, req.StorageQuota, req.AccessMode); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "分配成功", nil)
|
||||
}
|
||||
|
||||
// AssignToRole 为角色分配存储策略
|
||||
// POST /api/admin/storage-assignments/role
|
||||
func (c *StorageAssignController) AssignToRole(ctx *gin.Context) {
|
||||
var req dto.StorageAssignRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
if req.GroupID == 0 {
|
||||
utils.BadRequest(ctx, "请指定角色")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.assignService.AssignToRole(req.GroupID, req.PolicyID, req.StorageQuota, req.Priority, req.AccessMode); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "分配成功", nil)
|
||||
}
|
||||
|
||||
// UpdateAssignment 更新分配规则(配额/优先级/访问模式)
|
||||
// PUT /api/admin/storage-assignments/:id
|
||||
func (c *StorageAssignController) UpdateAssignment(ctx *gin.Context) {
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
PolicyID uint64 `json:"policy_id"`
|
||||
Priority *int `json:"priority"`
|
||||
StorageQuota *int64 `json:"storage_quota"`
|
||||
AccessMode string `json:"access_mode"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.assignService.UpdateAssignment(id, req.PolicyID, req.Priority, req.StorageQuota, req.AccessMode); err != nil {
|
||||
utils.BadRequest(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "更新成功", nil)
|
||||
}
|
||||
|
||||
// DeleteAssignment 删除分配规则
|
||||
// DELETE /api/admin/storage-assignments/:id
|
||||
func (c *StorageAssignController) DeleteAssignment(ctx *gin.Context) {
|
||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
utils.BadRequest(ctx, "无效的ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.assignService.DeleteAssignment(id); err != nil {
|
||||
utils.BadRequest(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMessage(ctx, "删除成功", nil)
|
||||
}
|
||||
49
server/controller/storage_controller.go
Normal file
49
server/controller/storage_controller.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"seeyon-filesystem/config"
|
||||
"seeyon-filesystem/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// StorageController 存储配置控制器
|
||||
type StorageController struct{}
|
||||
|
||||
func NewStorageController() *StorageController {
|
||||
return &StorageController{}
|
||||
}
|
||||
|
||||
// GetCDNConfig 获取CDN配置
|
||||
// GET /api/admin/settings/cdn
|
||||
func (c *StorageController) GetCDNConfig(ctx *gin.Context) {
|
||||
cfg := config.AppConfig.Storage
|
||||
utils.Success(ctx, gin.H{
|
||||
"cdn_host": cfg.CDNHost,
|
||||
"cdn_path_prefix": cfg.CDNPathPrefix,
|
||||
"local_base_path": cfg.LocalBasePath,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCDNConfig 更新CDN配置
|
||||
// PUT /api/admin/settings/cdn
|
||||
func (c *StorageController) UpdateCDNConfig(ctx *gin.Context) {
|
||||
var req struct {
|
||||
CDNHost string `json:"cdn_host"`
|
||||
CDNPathPrefix string `json:"cdn_path_prefix"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新内存配置
|
||||
config.AppConfig.Storage.CDNHost = req.CDNHost
|
||||
config.AppConfig.Storage.CDNPathPrefix = req.CDNPathPrefix
|
||||
|
||||
utils.SuccessWithMessage(ctx, "CDN配置已更新", gin.H{
|
||||
"cdn_host": req.CDNHost,
|
||||
"cdn_path_prefix": req.CDNPathPrefix,
|
||||
"example_url": req.CDNHost + req.CDNPathPrefix + "/2026/07/02/ab/abcdef.txt",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user