初始化

This commit is contained in:
2026-07-03 16:01:08 +08:00
parent dc90e889e1
commit b51ee98afa
37 changed files with 6282 additions and 0 deletions

View 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)
}
}

View 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)
}

View 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
}

View 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})
}

View 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
}

View 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",
})
}

View 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)
}

View 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",
})
}