This commit is contained in:
2026-07-18 10:52:32 +08:00
parent 2dcafae465
commit b5ef58f434
61 changed files with 1274 additions and 2142 deletions

View File

@@ -100,6 +100,8 @@ func SetupRouter() *gin.Engine {
protected.GET("/auth/profile", authCtrl.GetProfile)
protected.GET("/auth/permissions", permCtrl.GetUserPermissions)
protected.PUT("/auth/password", authCtrl.ChangePassword)
protected.POST("/auth/logout", authCtrl.Logout)
protected.GET("/auth/sessions", authCtrl.GetSessions)
protected.GET("/user/storage-info", assignCtrl.GetMyStorageInfo)
// 文件管理
@@ -252,6 +254,10 @@ func SetupRouter() *gin.Engine {
admin.GET("/backup/logs", backupCtrl.ListLogs)
admin.GET("/backup/backups", backupCtrl.ListBackups)
admin.POST("/backup/restore/:logId", backupCtrl.Restore)
// 授权管理
admin.GET("/license", adminCtrl.GetLicense)
admin.POST("/license/generate", adminCtrl.GenerateLicense)
}
// ============ 分享公开访问 (无需登录) ============

View File

@@ -32,3 +32,8 @@ backup:
pgdump_path: ""
psql_path: ""
sqlcmd_path: ""
# 授权配置
license:
file_path: "" # 授权文件路径, 留空则默认 ./license.json
verify_url: "" # 远程验证接口(可选), 如 http://verify.example.com/api/license/verify

View File

@@ -19,6 +19,13 @@ type Config struct {
JWT JWTConfig `mapstructure:"jwt"`
Storage StorageConfig `mapstructure:"storage"`
Backup BackupConfig `mapstructure:"backup"`
License LicenseConfig `mapstructure:"license"`
}
// LicenseConfig 授权配置
type LicenseConfig struct {
VerifyURL string `mapstructure:"verify_url"` // 远程验证接口地址(可选)
FilePath string `mapstructure:"file_path"` // 授权文件路径, 默认 ./license.json
}
// BackupConfig 备份工具配置

View File

@@ -1,8 +1,10 @@
package controller
import (
"encoding/json"
"strconv"
"seeyon-filesystem/config"
"seeyon-filesystem/dto"
"seeyon-filesystem/model"
"seeyon-filesystem/service"
@@ -24,6 +26,99 @@ func NewAdminController() *AdminController {
}
}
// ========== 授权管理 ==========
// GetLicense 获取当前授权信息
// GET /api/admin/license
func (c *AdminController) GetLicense(ctx *gin.Context) {
licensePath := config.AppConfig.License.FilePath
if licensePath == "" {
licensePath = "license.json"
}
resp, err := utils.ReadAndVerifyLicense(licensePath, config.AppConfig.License.VerifyURL, "seeyon-filesystem", "1.0")
if err != nil {
utils.Success(ctx, gin.H{
"status": "invalid",
"message": err.Error(),
"machine_id": utils.GetMachineID(),
})
return
}
utils.Success(ctx, gin.H{
"status": map[bool]string{true: "valid", false: "invalid"}[resp.Valid],
"message": resp.Message,
"machine_id": utils.GetMachineID(),
"product": resp.Product,
"customer_id": resp.CustomerID,
"customer": resp.Customer,
"contact": resp.Contact,
"phone": resp.Phone,
"expire_at": resp.ExpireAt,
"max_users": resp.MaxUsers,
"max_files": resp.MaxFiles,
"features": resp.Features,
"expire_days": resp.ExpireDays,
})
}
// GenerateLicense 生成授权码
// POST /api/admin/license/generate
func (c *AdminController) GenerateLicense(ctx *gin.Context) {
var req struct {
TargetMachineID string `json:"target_machine_id"`
CustomerID string `json:"customer_id"`
Customer string `json:"customer"`
Contact string `json:"contact"`
Phone string `json:"phone"`
MaxUsers int `json:"max_users"`
MaxFiles int `json:"max_files"`
Features string `json:"features"`
ExpireAt string `json:"expire_at"` // RFC3339格式, 空=永久
Version string `json:"version"`
}
if err := ctx.ShouldBindJSON(&req); err != nil {
utils.BadRequest(ctx, "参数错误: "+err.Error())
return
}
if req.Features == "" {
req.Features = "*"
}
if req.Version == "" {
req.Version = "1.0"
}
license := &utils.LicenseInfo{
Product: "seeyon-filesystem",
CustomerID: req.CustomerID,
Customer: req.Customer,
Contact: req.Contact,
Phone: req.Phone,
MachineID: req.TargetMachineID,
MaxUsers: req.MaxUsers,
MaxFiles: req.MaxFiles,
Features: req.Features,
ExpireAt: req.ExpireAt,
}
utils.SignLicense(license)
// 返回标准格式
licenseFile := map[string]interface{}{
"productType": "seeyon-filesystem",
"version": req.Version,
"licenseStr": license.LicenseStr,
}
fileJSON, _ := json.Marshal(licenseFile)
utils.Success(ctx, gin.H{
"license_file": string(fileJSON), // 保存为 license.json
"license": license,
"machine_id": utils.GetMachineID(),
})
}
// ========== 用户管理 ==========
// ListUsers 用户列表

View File

@@ -116,3 +116,32 @@ func (c *AuthController) ChangePassword(ctx *gin.Context) {
utils.SuccessWithMessage(ctx, "密码修改成功", nil)
}
// Logout 退出登录
// POST /api/auth/logout
func (c *AuthController) Logout(ctx *gin.Context) {
userID := middleware.GetCurrentUserID(ctx)
token := ctx.GetHeader("token")
if token == "" {
authHeader := ctx.GetHeader("Authorization")
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
token = authHeader[7:]
}
}
if userID > 0 && token != "" {
c.authService.Logout(userID, token)
}
utils.SuccessWithMessage(ctx, "已退出登录", nil)
}
// GetSessions 获取当前用户的登录会话列表
// GET /api/auth/sessions
func (c *AuthController) GetSessions(ctx *gin.Context) {
userID := middleware.GetCurrentUserID(ctx)
if userID == 0 {
utils.Unauthorized(ctx, "未登录")
return
}
sessions := c.authService.GetSessions(userID)
utils.Success(ctx, sessions)
}

View File

@@ -1,6 +1,8 @@
package controller
import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
@@ -57,8 +59,14 @@ func (c *FileController) Upload(ctx *gin.Context) {
}
storagePolicyID, _ := strconv.ParseUint(policyIDStr, 10, 64)
// 获取业务ID(可选)
bizID := ctx.DefaultQuery("biz_id", "")
if bizID == "" {
bizID = ctx.PostForm("biz_id")
}
// 调用服务层
result, err := c.fileService.Upload(fileHeader, folderID, ownerID, storagePolicyID)
result, err := c.fileService.Upload(fileHeader, folderID, ownerID, storagePolicyID, bizID)
if err != nil {
utils.ServerError(ctx, err.Error())
return
@@ -150,7 +158,7 @@ func (c *FileController) GetPreviewURL(ctx *gin.Context) {
utils.Success(ctx, gin.H{"preview_url": previewURL})
}
// Preview 文件预览(直接返回文件流, 浏览器内联展示)
// Preview 文件预览(直接返回文件流, 浏览器内联展示, 支持Range请求)
// GET /api/file/:id/preview
func (c *FileController) Preview(ctx *gin.Context) {
ownerID := middleware.GetCurrentUserID(ctx)
@@ -168,9 +176,58 @@ func (c *FileController) Preview(ctx *gin.Context) {
}
defer reader.Close()
// 内联展示而非下载
ctx.Header("Content-Disposition", "inline; filename=\""+file.Name+"\"")
ctx.Header("Content-Type", file.MimeType)
ctx.Header("Accept-Ranges", "bytes")
// Range请求: 视频/音频需要分段加载才能内联播放
rangeHeader := ctx.GetHeader("Range")
if rangeHeader != "" {
fileSize := file.Size
var start, end int64
_, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end)
if err != nil {
// 只有start没有end
fmt.Sscanf(rangeHeader, "bytes=%d-", &start)
end = fileSize - 1
}
if start >= fileSize || start < 0 {
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
return
}
if end >= fileSize {
end = fileSize - 1
}
length := end - start + 1
// 跳到起始位置
if seeker, ok := reader.(io.Seeker); ok {
seeker.Seek(start, io.SeekStart)
}
ctx.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize))
ctx.Header("Content-Length", fmt.Sprintf("%d", length))
ctx.Status(http.StatusPartialContent)
buf := make([]byte, 32*1024)
remaining := length
for remaining > 0 {
toRead := int64(len(buf))
if toRead > remaining {
toRead = remaining
}
n, readErr := reader.Read(buf[:toRead])
if n > 0 {
ctx.Writer.Write(buf[:n])
remaining -= int64(n)
}
if readErr != nil {
break
}
}
return
}
ctx.DataFromReader(http.StatusOK, file.Size, file.MimeType, reader, nil)
}

View File

@@ -105,7 +105,11 @@ func (c *OpenAPIController) BindUsers(ctx *gin.Context) {
return
}
if err := c.openService.BindUsers(id, req.UserIDs); err != nil {
userIDs := make([]uint64, len(req.UserIDs))
for i, uid := range req.UserIDs {
userIDs[i] = uint64(uid)
}
if err := c.openService.BindUsers(id, userIDs); err != nil {
utils.BadRequest(ctx, err.Error())
return
}

View File

@@ -39,7 +39,7 @@ func (c *PermissionController) ListRoles(ctx *gin.Context) {
perms := make([]dto.PermissionVO, 0, len(r.Permissions))
for _, p := range r.Permissions {
perms = append(perms, dto.PermissionVO{
ID: p.ID,
ID: dto.StringID(p.ID),
Code: p.Code,
Name: p.Name,
Resource: p.Resource,
@@ -48,7 +48,7 @@ func (c *PermissionController) ListRoles(ctx *gin.Context) {
})
}
vos = append(vos, dto.RoleVO{
ID: r.ID,
ID: dto.StringID(r.ID),
Name: r.Name,
DisplayName: r.DisplayName,
Description: r.Description,
@@ -145,7 +145,11 @@ func (c *PermissionController) SetRolePermissions(ctx *gin.Context) {
return
}
if err := c.permService.SetRolePermissions(roleID, req.PermissionIDs); err != nil {
permIDs := make([]uint64, len(req.PermissionIDs))
for i, id := range req.PermissionIDs {
permIDs[i] = uint64(id)
}
if err := c.permService.SetRolePermissions(roleID, permIDs); err != nil {
utils.BadRequest(ctx, err.Error())
return
}
@@ -167,7 +171,7 @@ func (c *PermissionController) ListPermissions(ctx *gin.Context) {
vos := make([]dto.PermissionVO, 0, len(perms))
for _, p := range perms {
vos = append(vos, dto.PermissionVO{
ID: p.ID,
ID: dto.StringID(p.ID),
Code: p.Code,
Name: p.Name,
Resource: p.Resource,
@@ -238,7 +242,11 @@ func (c *PermissionController) SetUserRoles(ctx *gin.Context) {
return
}
if err := c.permService.SetUserRoles(userID, req.RoleIDs); err != nil {
roleIDs := make([]uint64, len(req.RoleIDs))
for i, id := range req.RoleIDs {
roleIDs[i] = uint64(id)
}
if err := c.permService.SetUserRoles(userID, roleIDs); err != nil {
utils.BadRequest(ctx, err.Error())
return
}
@@ -272,7 +280,7 @@ func (c *PermissionController) GetUserPermissions(ctx *gin.Context) {
roleVOs := make([]dto.RoleVO, 0, len(roles))
for _, r := range roles {
roleVOs = append(roleVOs, dto.RoleVO{
ID: r.ID,
ID: dto.StringID(r.ID),
Name: r.Name,
DisplayName: r.DisplayName,
})

View File

@@ -151,6 +151,7 @@ func (c *SSOController) AdminCreateProvider(ctx *gin.Context) {
}
provider := &model.SSOProvider{
ID: utils.GenID(),
Name: req.Name,
Type: req.Type,
DisplayName: req.DisplayName,

View File

@@ -1,4 +1,50 @@
@echo off
setlocal
echo ============================================
echo Build Backend
echo ============================================
set "DEPLOY_DIR=%~dp0deploy"
if not exist "%DEPLOY_DIR%" mkdir "%DEPLOY_DIR%"
:: Windows exe
echo.
echo [1/2] Building Windows amd64...
set CGO_ENABLED=0
set GOOS=windows
set GOARCH=amd64
go build -ldflags "-s -w" -o "%DEPLOY_DIR%\server.exe" main.go
if %errorlevel% neq 0 (
echo Windows build FAILED
pause
exit /b 1
)
echo OK: server.exe
:: Linux binary
echo.
echo [2/2] Building Linux amd64...
set CGO_ENABLED=0
set GOOS=linux
set GOARCH=amd64
$env:CGO_ENABLED="0"; $env:GOOS="linux"; $env:GOARCH="amd64"; go build -ldflags "-s -w" -o myapp main.go
go build -ldflags "-s -w" -o "%DEPLOY_DIR%\server" main.go
if %errorlevel% neq 0 (
echo Linux build FAILED
pause
exit /b 1
)
echo OK: server
:: Copy config
copy /y config.yaml "%DEPLOY_DIR%\config.yaml.example" >/dev/null
echo.
echo ============================================
echo Build Complete!
echo ============================================
echo.
echo Output directory:
echo %DEPLOY_DIR%
echo.
pause

View File

@@ -1,156 +1,129 @@
# =============================================
# 文件管理系统 - 生产环境 Nginx 配置
# =============================================
# 功能:
# 1. 前端静态资源服务
# 2. API反向代理到Go后端
# 3. MinIO反向代理(预签名模式)
# 4. X-Sendfile支持(中转模式优化)
# 5. HTTPS配置(可选)
# =============================================
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 1024;
worker_connections 10240;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile off; # Windows环境建议关闭
charset utf-8;
server_tokens off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
# 全局上传限制
client_max_body_size 2g;
# Gzip压缩
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time';
# Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
# 代理通用头部
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering on;
proxy_connect_timeout 60s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# =============================================
# Upstream
# =============================================
upstream fileserver {
server 127.0.0.1:8080; # Go文件管理服务
keepalive 32;
}
upstream minio {
server 127.0.0.1:9000; # MinIO服务(不暴露公网)
keepalive 32;
}
# =============================================
# HTTP → HTTPS 重定向(可选)
# =============================================
# server {
# listen 80;
# server_name files.example.com;
# return 301 https://$host$request_uri;
# }
# =============================================
# 主站配置
# Server 1: 前端 + API (主入口)
# =============================================
server {
listen 80; # 或 443 ssl
server_name files.example.com; # 替换为你的域名
listen 5173;
server_name 36.133.248.69;
client_max_body_size 500m; # 最大上传500MB
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
# SSL配置(可选)
# ssl_certificate /path/to/cert.pem;
# ssl_certificate_key /path/to/key.pem;
# ssl_protocols TLSv1.2 TLSv1.3;
# access_log logs/access.log main;
# =============================================
# 1. API请求 → Go后端
# =============================================
location ^~ /api/ {
proxy_pass http://fileserver;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 30s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# API 反向代理
location /api/ {
proxy_pass http://36.133.248.69:8075;
client_max_body_size 2g;
}
# =============================================
# 2. MinIO反向代理(预签名模式)
# 用户通过此路径直连MinIO获取文件
# 关键: 必须保留签名参数
# =============================================
location ^~ /minio/ {
proxy_pass http://minio/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# 大文件下载超时
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# 禁用缓冲(流式传输)
proxy_buffering off;
# 隐藏MinIO内部头
proxy_hide_header X-Amz-Id-2;
proxy_hide_header X-Amz-Request-Id;
}
# =============================================
# 3. X-Sendfile路径(中转模式优化)
# Go后端返回X-Accel-Redirect头时,
# Nginx直接从本地路径发送文件
# =============================================
location ^~ /minio-files/ {
internal; # 仅允许内部重定向, 外部不可直接访问
alias D:/Seeyon/A8/localfile/;
# 优化大文件传输
sendfile on;
tcp_nopush on;
# 缓存控制
expires 30d;
# 文件 CDN / 本地存储
location /public/ {
alias D:/fileSys/localStorage/;
expires 7d;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin "https://36.133.248.69";
add_header Access-Control-Allow-Methods "GET, OPTIONS";
}
# =============================================
# 4. 前端静态资源
# =============================================
# 前端 SPA
location / {
root D:/Seeyon/A8/frontend/dist; # 前端构建产物目录
index index.html;
# SPA路由支持
root D:/fileSys/dist;
index index.html;
try_files $uri $uri/ /index.html;
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
}
# =============================================
# 5. 安全头
# =============================================
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# =============================================
# Server 2: MinIO API 代理
# =============================================
server {
listen 9033;
server_name 36.133.248.69;
client_max_body_size 2g;
location / {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
# =============================================
# Server 3: MinIO 控制台
# =============================================
server {
listen 9034;
server_name 36.133.248.69;
client_max_body_size 50m;
location / {
proxy_pass http://127.0.0.1:9001;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}

View File

@@ -4,28 +4,28 @@ package dto
// UserCreateRequest 创建用户请求
type UserCreateRequest struct {
Username string `json:"username" binding:"required,min=3,max=64"`
Password string `json:"password"` // 可选, 留空则使用默认密码
Email string `json:"email"`
Nickname string `json:"nickname"`
RoleIDs []uint64 `json:"role_ids"` // 角色ID列表
DeptID uint64 `json:"dept_id"` // 部门ID
StorageQuota int64 `json:"storage_quota"` // 存储配额(字节)
Username string `json:"username" binding:"required,min=3,max=64"`
Password string `json:"password"` // 可选, 留空则使用默认密码
Email string `json:"email"`
Nickname string `json:"nickname"`
RoleIDs []StringID `json:"role_ids"` // 角色ID列表
DeptID StringID `json:"dept_id"` // 部门ID
StorageQuota int64 `json:"storage_quota"` // 存储配额(字节)
}
// UserUpdateRequest 更新用户请求
type UserUpdateRequest struct {
Email string `json:"email"`
Nickname string `json:"nickname"`
Status *int8 `json:"status"` // 指针区分零值和未传
RoleIDs []uint64 `json:"role_ids"`
DeptID uint64 `json:"dept_id"`
StorageQuota *int64 `json:"storage_quota"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Status *int8 `json:"status"` // 指针区分零值和未传
RoleIDs []StringID `json:"role_ids"`
DeptID StringID `json:"dept_id"`
StorageQuota *int64 `json:"storage_quota"`
}
// UserVO 用户视图对象(管理后台)
type UserVO struct {
ID uint64 `json:"id"`
ID StringID `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
@@ -144,7 +144,7 @@ type AppCreateRequest struct {
// AppBindUsersRequest 绑定用户请求
type AppBindUsersRequest struct {
UserIDs []uint64 `json:"user_ids" binding:"required"`
UserIDs []StringID `json:"user_ids" binding:"required"`
}
// ========== 操作日志 ==========

View File

@@ -5,6 +5,7 @@ import "time"
// FileVO 文件视图对象 - 返回给前端
type FileVO struct {
ID StringID `json:"id"`
BizID string `json:"biz_id,omitempty"`
Name string `json:"name"`
Extension string `json:"extension"`
FolderID StringID `json:"folder_id"`

View File

@@ -15,7 +15,7 @@ type RoleUpdateRequest struct {
// RoleVO 角色视图对象
type RoleVO struct {
ID uint64 `json:"id"`
ID StringID `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
@@ -35,22 +35,22 @@ type PermissionCreateRequest struct {
// PermissionVO 权限视图对象
type PermissionVO struct {
ID uint64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Resource string `json:"resource"`
Action string `json:"action"`
Description string `json:"description"`
ID StringID `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Resource string `json:"resource"`
Action string `json:"action"`
Description string `json:"description"`
}
// SetRolePermissionsRequest 设置角色权限请求
type SetRolePermissionsRequest struct {
PermissionIDs []uint64 `json:"permission_ids" binding:"required"` // 权限ID列表
PermissionIDs []StringID `json:"permission_ids" binding:"required"` // 权限ID列表
}
// SetUserRolesRequest 设置用户角色请求
type SetUserRolesRequest struct {
RoleIDs []uint64 `json:"role_ids" binding:"required"` // 角色ID列表
RoleIDs []StringID `json:"role_ids" binding:"required"` // 角色ID列表
}
// UserPermissionVO 用户权限视图对象

View File

@@ -22,6 +22,11 @@ func main() {
log.Fatalf("加载配置失败: %v", err)
}
// 验证授权(启动时必须通过, 否则拒绝启动)
if err := checkLicense(cfg.License); err != nil {
log.Fatalf("授权验证失败: %v", err)
}
if err := config.InitDB(&cfg.Database); err != nil {
log.Fatalf("初始化数据库失败: %v", err)
}
@@ -67,6 +72,7 @@ func autoMigrate() error {
&model.OperationLog{},
&model.BackupPolicy{},
&model.BackupLog{},
&model.UserSession{},
)
if err != nil {
return err
@@ -83,6 +89,19 @@ func autoMigrate() error {
// createIndexIfNotExists 安全创建索引(忽略已存在错误)
func createIndexIfNotExists(table, indexName, columns string) {
// 先检查索引是否已存在(兼容MySQL/PostgreSQL/SQL Server)
var count int64
switch config.AppConfig.Database.Type {
case "postgres":
config.DB.Raw("SELECT COUNT(*) FROM pg_indexes WHERE indexname = ?", indexName).Scan(&count)
case "sqlserver":
config.DB.Raw("SELECT COUNT(*) FROM sys.indexes WHERE name = ?", indexName).Scan(&count)
default:
config.DB.Raw("SELECT COUNT(*) FROM information_schema.statistics WHERE table_name = ? AND index_name = ?", table, indexName).Scan(&count)
}
if count > 0 {
return
}
sql := fmt.Sprintf("CREATE INDEX %s ON %s(%s)", indexName, table, columns)
config.DB.Exec(sql)
}
@@ -107,7 +126,11 @@ func initDefaultData() {
{Code: "system:manage", Name: "系统管理", Resource: "system", Action: "manage"},
}
for _, p := range permissions {
config.DB.Where("code = ?", p.Code).FirstOrCreate(&p)
var existing model.Permission
if config.DB.Where("code = ?", p.Code).First(&existing).Error != nil {
p.ID = utils.GenID()
config.DB.Create(&p)
}
}
// ========== 2. 四级角色 ==========
@@ -118,7 +141,13 @@ func initDefaultData() {
{Name: "guest", DisplayName: "访客", Description: "仅可查看公开分享资源", IsSystem: 1},
}
for _, r := range roles {
config.DB.Where("name = ?", r.Name).FirstOrCreate(&r)
var existing model.Role
if config.DB.Where("name = ?", r.Name).First(&existing).Error != nil {
r.ID = utils.GenID()
config.DB.Create(&r)
} else {
r = existing
}
}
// 角色-权限分配
@@ -179,6 +208,7 @@ func initDefaultData() {
var existing model.StoragePolicy
if config.DB.Where("policy_type = ?", pd.PolicyType).First(&existing).Error != nil {
config.DB.Create(&model.StoragePolicy{
ID: utils.GenID(),
Name: pd.Name,
Type: pd.Type,
PolicyType: pd.PolicyType,
@@ -213,6 +243,7 @@ func initDefaultData() {
var existing model.StorageAssignment
if config.DB.Where("group_id = ? AND policy_id = ?", role.ID, policy.ID).First(&existing).Error != nil {
config.DB.Create(&model.StorageAssignment{
ID: utils.GenID(),
PolicyID: policy.ID,
GroupID: role.ID,
StorageQuota: ad.Quota,
@@ -228,6 +259,7 @@ func initDefaultData() {
var defaultAssign model.StorageAssignment
if config.DB.Where("is_default = 1").First(&defaultAssign).Error != nil {
config.DB.Create(&model.StorageAssignment{
ID: utils.GenID(),
PolicyID: hotPolicy.ID,
IsDefault: 1,
Status: 1,
@@ -239,62 +271,102 @@ func initDefaultData() {
var count int64
config.DB.Model(&model.User{}).Where("username = ?", "admin").Count(&count)
if count == 0 {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("admin123"), 10)
if err != nil {
log.Fatalf("密码加密失败: %v", err)
}
if count == 0 {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("admin123"), 10)
if err != nil {
log.Fatalf("密码加密失败: %v", err)
}
admin := &model.User{
Username: "admin",
Password: string(hashedPassword),
Status: 1,
}
admin := &model.User{
ID: utils.GenID(),
Username: "admin",
Password: string(hashedPassword),
Nickname: "管理员",
Role: "admin",
Status: 1,
StorageQuota: 107374182400, // 100GB
MustChangePwd: 1,
}
if err := config.DB.Create(admin).Error; err != nil {
log.Fatalf("创建 admin 失败: %v", err)
}
log.Println("默认管理员已创建: admin / admin123")
} else {
log.Println("⚠️ admin 用户已存在,跳过")
}
if err := config.DB.Create(admin).Error; err != nil {
log.Fatalf("创建 admin 失败: %v", err)
}
log.Println("默认管理员已创建: admin / admin123")
} else {
// 确保已有admin用户的Role字段为admin
config.DB.Model(&model.User{}).Where("username = ? AND (role IS NULL OR role = '')", "admin").Update("role", "admin")
log.Println("admin 用户已存在")
}
// ========== 6. 确保admin用户有admin角色 ==========
var adminUser model.User
if config.DB.Where("username = ?", "admin").First(&adminUser).Error == nil {
var superAdminRole model.Role
err := config.DB.Where("name = ?", "admin").First(&superAdminRole).Error
if err == nil { // ✅ 用 Error 判断,找到了才继续
var ur model.UserRole
if err := config.DB.Where("user_id = ? AND role_id = ?", adminUser.ID, superAdminRole.ID).First(&ur).Error; err != nil {
// 没找到这条关联记录,才创建
config.DB.Create(&model.UserRole{UserID: adminUser.ID, RoleID: superAdminRole.ID})
log.Println("✅ admin 已分配 admin 角色")
} else {
log.Println("⚠️ admin 已有 admin 角色,跳过")
}
} else {
log.Println("❌ 找不到 admin 角色")
}
}
// ========== 6. 确保admin用户有admin角色 ==========
var adminUser model.User
if config.DB.Where("username = ?", "admin").First(&adminUser).Error == nil {
var adminRole model.Role
if config.DB.Where("name = ?", "admin").First(&adminRole).Error == nil {
var ur model.UserRole
if config.DB.Where("user_id = ? AND role_id = ?", adminUser.ID, adminRole.ID).First(&ur).Error != nil {
config.DB.Create(&model.UserRole{UserID: adminUser.ID, RoleID: adminRole.ID})
log.Println("admin 已分配 admin 角色")
}
}
}
// ========== 7. 给没有角色的用户补上默认角色(user) ==========
var userRole model.Role
if config.DB.Where("name = ?", "user").First(&userRole).Error == nil {
// ✅ 排除 admin
var usersWithoutRole []model.User
config.DB.Where("id NOT IN (SELECT user_id FROM fs_user_role) AND username != ?", "admin").Find(&usersWithoutRole)
for _, u := range usersWithoutRole {
config.DB.Create(&model.UserRole{UserID: u.ID, RoleID: userRole.ID})
}
if len(usersWithoutRole) > 0 {
log.Printf("已为 %d 个无角色用户分配默认角色", len(usersWithoutRole))
}
}
// ========== 7. 给没有角色的用户补上默认角色(user) ==========
var userRole model.Role
if config.DB.Where("name = ?", "user").First(&userRole).Error == nil {
var usersWithoutRole []model.User
config.DB.Where("id NOT IN (SELECT user_id FROM fs_user_role) AND username != ?", "admin").Find(&usersWithoutRole)
for _, u := range usersWithoutRole {
config.DB.Create(&model.UserRole{UserID: u.ID, RoleID: userRole.ID})
}
if len(usersWithoutRole) > 0 {
log.Printf("已为 %d 个无角色用户分配默认角色", len(usersWithoutRole))
}
}
log.Println("默认数据初始化完成")
}
// checkLicense 验证产品授权
func checkLicense(cfg config.LicenseConfig) error {
machineID := utils.GetMachineID()
log.Printf("机器标识: %s", machineID)
licensePath := cfg.FilePath
if licensePath == "" {
licensePath = "license.json"
}
resp, err := utils.ReadAndVerifyLicense(licensePath, cfg.VerifyURL, "seeyon-filesystem", "1.0")
if err != nil {
return err
}
if !resp.Valid {
return fmt.Errorf("%s", resp.Message)
}
// 设置当前授权信息
licPath := cfg.FilePath
if licPath == "" {
licPath = "license.json"
}
if licData, licErr := utils.ReadLicenseFile(licPath); licErr == nil {
utils.SetCurrentLicense(licData)
}
lic := utils.GetCurrentLicense()
features := ""
if lic != nil {
features = lic.Features
}
if resp.ExpireDays == -1 {
log.Printf("[授权] 企业版 - 永久授权, 用户上限: %d, 功能: %s", resp.MaxUsers, features)
} else {
log.Printf("[授权] 企业版 - 有效期剩余 %d 天, 用户上限: %d, 功能: %s", resp.ExpireDays, resp.MaxUsers, features)
}
return nil
}

View File

@@ -155,6 +155,19 @@ func GetCurrentUserID(c *gin.Context) uint64 {
return userID.(uint64)
}
// RequireFeature 功能授权检查中间件
// 检查当前授权是否包含指定功能
func RequireFeature(feature string) gin.HandlerFunc {
return func(c *gin.Context) {
if !utils.HasFeature(feature) {
utils.Forbidden(c, "当前授权不支持此功能: "+feature)
c.Abort()
return
}
c.Next()
}
}
// GetCurrentUsername 从上下文获取当前用户名
func GetCurrentUsername(c *gin.Context) string {
username, exists := c.Get("username")

View File

@@ -4,7 +4,7 @@ import "time"
// App 开放平台应用
type App struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
AppID string `json:"app_id" gorm:"uniqueIndex;size:64;not null"`
AppSecret string `json:"-" gorm:"size:128;not null"`
AppName string `json:"app_name" gorm:"size:128;not null"`
@@ -20,7 +20,7 @@ func (App) TableName() string {
// AppUser 应用授权用户
// 只有绑定的用户才能通过该应用进行SSO登录
type AppUser struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
AppID uint64 `json:"app_id" gorm:"index;not null"` // 应用ID
UserID uint64 `json:"user_id" gorm:"index;not null"` // 用户ID
CreatedAt time.Time `json:"created_at"`

View File

@@ -4,11 +4,11 @@ import "time"
// BackupPolicy 备份策略
type BackupPolicy struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:128;not null"` // 策略名称
BackupType string `json:"backup_type" gorm:"size:32;not null"` // full=全量, incremental=增量
TargetType string `json:"target_type" gorm:"size:32;not null"` // local=本地, minio=MinIO
TargetConfig string `json:"target_config" gorm:"type:json"` // 目标配置JSON
TargetConfig string `json:"target_config" gorm:"type:text"` // 目标配置JSON
IncludeDB int8 `json:"include_db" gorm:"default:1"` // 是否备份数据库
IncludeFiles int8 `json:"include_files" gorm:"default:1"` // 是否备份文件
CronExpr string `json:"cron_expr" gorm:"size:64"` // cron表达式
@@ -26,7 +26,7 @@ func (BackupPolicy) TableName() string {
// BackupLog 备份执行日志
type BackupLog struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
PolicyID uint64 `json:"policy_id" gorm:"index;not null"`
PolicyName string `json:"policy_name" gorm:"size:128"`
BackupType string `json:"backup_type" gorm:"size:32"`

View File

@@ -5,7 +5,7 @@ import "time"
// Department 部门/组织架构模型
// 使用物化路径实现树形结构
type Department struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:128;not null"` // 部门名称
Code string `json:"code" gorm:"uniqueIndex;size:64"` // 部门编码
ParentID uint64 `json:"parent_id" gorm:"index;default:0"` // 上级部门ID, 0=顶级

View File

@@ -5,6 +5,7 @@ import "time"
// File 文件模型
type File struct {
ID uint64 `json:"id" gorm:"primaryKey"`
BizID string `json:"biz_id" gorm:"index;size:128"` // 业务ID
Name string `json:"name" gorm:"size:256;not null"`
Extension string `json:"extension" gorm:"size:32"`
FolderID uint64 `json:"folder_id" gorm:"index;not null"`
@@ -31,7 +32,7 @@ func (File) TableName() string {
// FileAssociation 业务文件关联模型
// 用于兼容 OA 系统的文件关联
type FileAssociation struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
BizID string `json:"biz_id" gorm:"size:128;index;not null"`
BizType string `json:"biz_type" gorm:"size:64;index;not null"`
FileID uint64 `json:"file_id" gorm:"not null"`
@@ -45,7 +46,7 @@ func (FileAssociation) TableName() string {
// FilePermission 文件权限模型
type FilePermission struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
FileID uint64 `json:"file_id" gorm:"not null"`
UserID uint64 `json:"user_id" gorm:"default:0"`
RoleID uint64 `json:"role_id" gorm:"default:0"`

View File

@@ -5,7 +5,7 @@ import "time"
// OperationLog 操作日志
// 记录用户的关键操作行为
type OperationLog struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
UserID uint64 `json:"user_id" gorm:"index;default:0"` // 操作人
Username string `json:"username" gorm:"size:64"` // 操作人用户名
Action string `json:"action" gorm:"size:32;index;not null"` // 操作类型: login/upload/download/delete/share...

View File

@@ -5,7 +5,7 @@ import "time"
// Role 角色模型
// RBAC核心: 用户 -> 角色 -> 权限
type Role struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"uniqueIndex;size:64;not null"` // 角色标识: admin/editor/viewer
DisplayName string `json:"display_name" gorm:"size:64;not null"` // 显示名称: 管理员/编辑者/查看者
Description string `json:"description" gorm:"size:256"` // 角色描述
@@ -25,7 +25,7 @@ func (Role) TableName() string {
// Permission 权限模型
// 定义系统中所有可控制的操作权限
type Permission struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
Code string `json:"code" gorm:"uniqueIndex;size:64;not null"` // 权限标识: file:upload, file:delete, user:manage
Name string `json:"name" gorm:"size:64;not null"` // 显示名称: 上传文件, 删除文件, 用户管理
Resource string `json:"resource" gorm:"size:32;not null"` // 资源类型: file, folder, share, user, system

View File

@@ -4,7 +4,7 @@ import "time"
// Share 文件分享模型
type Share struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
FileID uint64 `json:"file_id" gorm:"not null"`
OwnerID uint64 `json:"owner_id" gorm:"not null"`
ShareCode string `json:"share_code" gorm:"uniqueIndex;size:32;not null"`

View File

@@ -10,12 +10,12 @@ import (
// SSOProvider SSO提供商配置
// 支持 OAuth2.0 / CAS / LDAP / OIDC 等协议
type SSOProvider struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:64;not null"` // 提供商名称: 企业微信/钉钉/飞书
Type string `json:"type" gorm:"size:32;not null"` // 协议类型: oauth2/cas/oidc/ldap
DisplayName string `json:"display_name" gorm:"size:64;not null"` // 登录页显示名称
Icon string `json:"icon" gorm:"size:512"` // 图标URL
Config SSOConfig `json:"config" gorm:"type:json"` // 协议配置
Config SSOConfig `json:"config" gorm:"type:text"` // 协议配置
AutoCreate int8 `json:"auto_create" gorm:"default:1"` // 自动创建用户: 1=是 0=否
DefaultRole string `json:"default_role" gorm:"size:32;default:viewer"` // 自动创建时的默认角色
Status int8 `json:"status" gorm:"default:1"` // 1=启用 0=禁用
@@ -63,7 +63,11 @@ type SSOConfig struct {
// Value 实现 driver.Valuer
func (c SSOConfig) Value() (driver.Value, error) {
return json.Marshal(c)
b, err := json.Marshal(c)
if err != nil {
return nil, err
}
return string(b), nil
}
// Scan 实现 sql.Scanner
@@ -71,8 +75,13 @@ func (c *SSOConfig) Scan(value interface{}) error {
if value == nil {
return nil
}
bytes, ok := value.([]byte)
if !ok {
var bytes []byte
switch v := value.(type) {
case []byte:
bytes = v
case string:
bytes = []byte(v)
default:
return fmt.Errorf("SSOConfig.Scan: 无法将 %T 转换为 []byte", value)
}
return json.Unmarshal(bytes, c)
@@ -81,7 +90,7 @@ func (c *SSOConfig) Scan(value interface{}) error {
// SSOSession SSO会话记录
// 记录SSO登录的来源和关联信息
type SSOSession struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
UserID uint64 `json:"user_id" gorm:"index;not null"` // 关联的本地用户ID
ProviderID uint64 `json:"provider_id" gorm:"not null"` // SSO提供商ID
ExternalID string `json:"external_id" gorm:"size:256;not null"` // 外部系统用户ID

View File

@@ -6,7 +6,7 @@ import "time"
// 支持: 用户专属 > 角色 > 系统默认
// 同一用户可通过多个角色获得多个存储策略, 按优先级调度
type StorageAssignment struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
PolicyID uint64 `json:"policy_id" gorm:"index;not null"` // 存储策略ID
// 分配目标

View File

@@ -9,11 +9,11 @@ import (
// StoragePolicy 存储策略模型
type StoragePolicy struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:64;not null"`
Type string `json:"type" gorm:"size:32;not null"` // local/minio/s3/oss
PolicyType string `json:"policy_type" gorm:"size:32;default:hot"` // hot/cold/public/slave
Config PolicyConfig `json:"config" gorm:"type:json"`
Config PolicyConfig `json:"config" gorm:"type:text"`
Status int8 `json:"status" gorm:"default:1"`
CreatedAt time.Time `json:"created_at"`
}
@@ -48,16 +48,26 @@ type PolicyConfig struct {
// Value 实现 driver.Valuer 接口, 用于写入数据库
func (c PolicyConfig) Value() (driver.Value, error) {
return json.Marshal(c)
b, err := json.Marshal(c)
if err != nil {
return nil, err
}
return string(b), nil
}
// Scan 实现 sql.Scanner 接口, 用于从数据库读取
// MySQL 返回 []byte, PostgreSQL/SQL Server 返回 string
func (c *PolicyConfig) Scan(value interface{}) error {
if value == nil {
return nil
}
bytes, ok := value.([]byte)
if !ok {
var bytes []byte
switch v := value.(type) {
case []byte:
bytes = v
case string:
bytes = []byte(v)
default:
return fmt.Errorf("PolicyConfig.Scan: 无法将 %T 转换为 []byte", value)
}
return json.Unmarshal(bytes, c)

View File

@@ -6,7 +6,7 @@ import "time"
// User 用户模型
// 对应数据库表: fs_user
type User struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
ID uint64 `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"uniqueIndex;size:64;not null"`
Password string `json:"-" gorm:"size:256;not null"` // json:"-" 防止密码泄露
Email string `json:"email" gorm:"size:128"`

Binary file not shown.

View File

@@ -240,11 +240,14 @@ func joinStrings(strs []string, sep string) string {
return result
}
// ListTrashed 查询回收站文件
func (r *FileRepository) ListTrashed(ownerID uint64) ([]model.File, error) {
// ListTrashed 查询回收站文件(管理员看全部, 普通用户看自己的)
func (r *FileRepository) ListTrashed(ownerID uint64, isAdmin bool) ([]model.File, error) {
var files []model.File
err := config.DB.Where("owner_id = ? AND status = 0", ownerID).
Order("updated_at DESC").Find(&files).Error
query := config.DB.Where("status = 0")
if !isAdmin {
query = query.Where("owner_id = ?", ownerID)
}
err := query.Order("updated_at DESC").Find(&files).Error
return files, err
}
@@ -253,6 +256,11 @@ func (r *FileRepository) Update(file *model.File) error {
return config.DB.Save(file).Error
}
// UpdateSize 更新文件大小
func (r *FileRepository) UpdateSize(id uint64, size int64) error {
return config.DB.Model(&model.File{}).Where("id = ?", id).Update("size", size).Error
}
// SoftDelete 软删除(移入回收站)
func (r *FileRepository) SoftDelete(id uint64) error {
return config.DB.Model(&model.File{}).Where("id = ?", id).Update("status", 0).Error

View File

@@ -8,6 +8,7 @@ import (
"seeyon-filesystem/dto"
"seeyon-filesystem/model"
"seeyon-filesystem/repository"
"seeyon-filesystem/storage"
"seeyon-filesystem/utils"
)
@@ -55,6 +56,7 @@ func (s *AdminService) CreateUser(req *dto.UserCreateRequest) error {
}
user := &model.User{
ID: utils.GenID(),
Username: req.Username,
Password: hashed,
Email: req.Email,
@@ -77,7 +79,11 @@ func (s *AdminService) CreateUser(req *dto.UserCreateRequest) error {
// 分配角色
if len(req.RoleIDs) > 0 {
s.userRoleRepo.SetUserRoles(user.ID, req.RoleIDs)
roleIDs := make([]uint64, len(req.RoleIDs))
for i, id := range req.RoleIDs {
roleIDs[i] = uint64(id)
}
s.userRoleRepo.SetUserRoles(user.ID, roleIDs)
} else {
// 未指定角色时, 自动分配默认角色(user)
var defaultRole model.Role
@@ -99,7 +105,7 @@ func (s *AdminService) ListUsers(page, size int) ([]dto.UserVO, int64, error) {
vos := make([]dto.UserVO, 0, len(users))
for _, u := range users {
vo := dto.UserVO{
ID: u.ID,
ID: dto.StringID(u.ID),
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
@@ -112,7 +118,7 @@ func (s *AdminService) ListUsers(page, size int) ([]dto.UserVO, int64, error) {
// 获取用户角色
roles, _ := s.userRoleRepo.GetUserRoles(u.ID)
for _, r := range roles {
vo.Roles = append(vo.Roles, dto.RoleVO{ID: r.ID, Name: r.Name, DisplayName: r.DisplayName})
vo.Roles = append(vo.Roles, dto.RoleVO{ID: dto.StringID(r.ID), Name: r.Name, DisplayName: r.DisplayName})
}
vos = append(vos, vo)
}
@@ -146,7 +152,11 @@ func (s *AdminService) UpdateUser(userID uint64, req *dto.UserUpdateRequest) err
// 更新角色
if req.RoleIDs != nil {
s.userRoleRepo.SetUserRoles(userID, req.RoleIDs)
roleIDs := make([]uint64, len(req.RoleIDs))
for i, id := range req.RoleIDs {
roleIDs[i] = uint64(id)
}
s.userRoleRepo.SetUserRoles(userID, roleIDs)
}
return nil
@@ -255,7 +265,13 @@ func (s *AdminService) UpdateStoragePolicy(id uint64, req *dto.StoragePolicyUpda
}
}
return s.policyRepo.Update(policy)
if err := s.policyRepo.Update(policy); err != nil {
return err
}
// 清除引擎缓存, 下次访问时用新配置重建
storage.InvalidateEngine(id)
return nil
}
// DeleteStoragePolicy 删除存储策略
@@ -277,6 +293,7 @@ func (s *AdminService) CreateDepartment(req *dto.DeptCreateRequest) error {
}
dept := &model.Department{
ID: utils.GenID(),
Name: req.Name,
Code: req.Code,
ParentID: req.ParentID,

View File

@@ -3,6 +3,7 @@ package service
import (
"errors"
"time"
"seeyon-filesystem/config"
"seeyon-filesystem/dto"
@@ -13,6 +14,8 @@ import (
"gorm.io/gorm"
)
const maxSessionsPerUser = 3 // 每用户最大同时登录数
// AuthService 认证服务
type AuthService struct {
userRepo *repository.UserRepository
@@ -46,7 +49,11 @@ func (s *AuthService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
return nil, errors.New("用户名或密码错误")
}
// 生成JWT Token
return s.generateLoginResponse(user, "", "")
}
// generateLoginResponse 生成登录响应(统一处理Token生成和会话创建)
func (s *AuthService) generateLoginResponse(user *model.User, device, ip string) (*dto.LoginResponse, error) {
token, err := utils.GenerateToken(
user.ID,
user.Username,
@@ -58,6 +65,9 @@ func (s *AuthService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
return nil, errors.New("生成Token失败")
}
// 创建会话(限制最大同时登录数)
s.createSession(user.ID, token, device, ip)
return &dto.LoginResponse{
Token: token,
Username: user.Username,
@@ -67,6 +77,59 @@ func (s *AuthService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
}, nil
}
// Logout 退出登录, 删除当前会话
func (s *AuthService) Logout(userID uint64, token string) {
config.DB.Where("user_id = ? AND token = ?", userID, token).Delete(&model.UserSession{})
}
// GetSessions 获取用户的活跃会话列表
func (s *AuthService) GetSessions(userID uint64) []map[string]interface{} {
var sessions []model.UserSession
config.DB.Where("user_id = ? AND expires_at > ?", userID, time.Now()).
Order("created_at DESC").Find(&sessions)
result := make([]map[string]interface{}, 0, len(sessions))
for _, sess := range sessions {
result = append(result, map[string]interface{}{
"id": sess.ID,
"device": sess.Device,
"ip": sess.IP,
"created_at": sess.CreatedAt.Format("2006-01-02 15:04:05"),
"expires_at": sess.ExpiresAt.Format("2006-01-02 15:04:05"),
})
}
return result
}
// createSession 创建登录会话, 超过上限时踢掉最旧的
func (s *AuthService) createSession(userID uint64, token, device, ip string) {
// 清理该用户过期的会话
config.DB.Where("user_id = ? AND expires_at < ?", userID, time.Now()).Delete(&model.UserSession{})
// 统计当前活跃会话数
var count int64
config.DB.Model(&model.UserSession{}).Where("user_id = ?", userID).Count(&count)
// 超过上限, 删除最旧的会话
if count >= maxSessionsPerUser {
var oldest model.UserSession
config.DB.Where("user_id = ?", userID).Order("created_at ASC").First(&oldest)
if oldest.ID > 0 {
config.DB.Delete(&oldest)
}
}
// 创建新会话
expiresAt := time.Now().Add(time.Duration(config.AppConfig.JWT.Expiration) * time.Hour)
config.DB.Create(&model.UserSession{
UserID: userID,
Token: token,
Device: device,
IP: ip,
ExpiresAt: expiresAt,
})
}
// validateAppLogin 校验 appId 登录
func (s *AuthService) validateAppLogin(appID string, userID uint64) error {
// 1. appId 是否存在且启用
@@ -104,25 +167,7 @@ func (s *AuthService) AppLogin(req *dto.AppLoginRequest) (*dto.LoginResponse, er
return nil, err
}
// 生成JWT Token
token, err := utils.GenerateToken(
user.ID,
user.Username,
user.Role,
config.AppConfig.JWT.Secret,
config.AppConfig.JWT.Expiration,
)
if err != nil {
return nil, errors.New("生成Token失败")
}
return &dto.LoginResponse{
Token: token,
Username: user.Username,
Nickname: user.Nickname,
Role: user.Role,
MustChangePwd: user.MustChangePwd == 1,
}, nil
return s.generateLoginResponse(user, "", "")
}
// Register 用户注册
@@ -138,6 +183,7 @@ func (s *AuthService) Register(req *dto.RegisterRequest) error {
}
user := &model.User{
ID: utils.GenID(),
Username: req.Username,
Password: hashedPassword,
Email: req.Email,

View File

@@ -14,6 +14,7 @@ import (
"seeyon-filesystem/config"
"seeyon-filesystem/dto"
"seeyon-filesystem/model"
"seeyon-filesystem/utils"
)
// BackupService 备份服务
@@ -33,6 +34,7 @@ func (s *BackupService) CreatePolicy(req *dto.BackupPolicyCreateRequest) error {
}
policy := &model.BackupPolicy{
ID: utils.GenID(),
Name: req.Name,
BackupType: req.BackupType,
TargetType: req.TargetType,
@@ -105,6 +107,7 @@ func (s *BackupService) ExecuteBackup(policyID uint64) (*model.BackupLog, error)
// 创建备份日志
log := &model.BackupLog{
ID: utils.GenID(),
PolicyID: policyID,
PolicyName: policy.Name,
BackupType: policy.BackupType,

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io"
"log"
"mime/multipart"
"os"
"path/filepath"
@@ -38,7 +39,7 @@ func NewFileService() *FileService {
// Upload 上传文件
// 流程: 校验大小 -> 计算MD5 -> 检查秒传 -> 存储文件 -> 保存元数据
func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64, ownerID uint64, storagePolicyID uint64) (*dto.FileVO, error) {
func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64, ownerID uint64, storagePolicyID uint64, bizID string) (*dto.FileVO, error) {
// 校验文件大小
maxSize := s.getMaxFileSize(ownerID)
if maxSize > 0 && fileHeader.Size > maxSize {
@@ -51,11 +52,19 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
if err != nil {
return nil, errors.New("目标文件夹不存在")
}
if folder.OwnerID != ownerID {
if !s.canAccessFile(folder.ID, folder.OwnerID, ownerID) {
return nil, errors.New("无权上传到此文件夹")
}
}
// 检查biz_id唯一性
if bizID != "" {
var existingBiz model.File
if config.DB.Where("biz_id = ? AND status = 1", bizID).First(&existingBiz).Error == nil {
return nil, fmt.Errorf("业务编号 %s 已存在", bizID)
}
}
// 确定存储策略: 优先使用指定策略, 否则根据用户分配自动确定
var engine storage.Engine
var policyID uint64
@@ -68,7 +77,7 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
engine, engineErr := storage.GetEngineByPolicyID(policyID)
if engineErr != nil {
engine = storage.GetDefaultEngine()
policyID = uint64(config.AppConfig.Storage.DefaultPolicyID)
// policyID 保留原值, 不覆盖, 确保文件记录正确的存储策略
}
if engine == nil {
return nil, errors.New("存储引擎未初始化")
@@ -106,6 +115,7 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
if existing != nil {
newFile := &model.File{
ID: utils.GenID(),
BizID: bizID,
Name: fileHeader.Filename,
Extension: utils.GetExtension(fileHeader.Filename),
FolderID: folderID,
@@ -140,6 +150,7 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
newFile := &model.File{
ID: utils.GenID(),
BizID: bizID,
Name: fileHeader.Filename,
Extension: ext,
FolderID: folderID,
@@ -158,6 +169,12 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
}
s.userRepo.UpdateStorageUsed(ownerID, size)
// 异步检测视频编码, H.265自动转码为H.264
if ext == "mp4" || ext == "mkv" || ext == "avi" || ext == "mov" {
go s.asyncTranscode(engine, newFile)
}
return s.toFileVO(newFile), nil
}
@@ -350,36 +367,48 @@ func GenerateFileURL(file *model.File, urlType string, token ...string) (string,
// 根据存储策略的实际类型决定URL生成方式
policyType := getStoragePolicyType(file.StoragePolicyID)
log.Printf("[URL生成] 文件ID=%d, storage_policy_id=%d, 策略类型=%s", file.ID, file.StoragePolicyID, policyType)
switch policyType {
case "minio":
engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID)
if err != nil {
// MinIO不可用, 回退API代理
log.Printf("[URL生成] MinIO引擎获取失败, 回退API代理: %v", err)
return buildAPIURL(file.ID, urlType, tk), nil
}
minioEngine := engine.(*storage.MinIOEngine)
var presignedURL string
var fileURL string
var genErr error
if urlType == "preview" {
presignedURL, genErr = minioEngine.GetPresignedPreviewURL(file.StorageKey, file.MimeType, 24*time.Hour)
fileURL, genErr = minioEngine.GetPresignedPreviewURL(file.StorageKey, file.MimeType, 24*time.Hour)
} else {
presignedURL, genErr = minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour)
fileURL, genErr = minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour)
}
if genErr != nil {
return buildAPIURL(file.ID, urlType, tk), nil
// 预签名失败(如匿名凭证/公开桶), 用直接URL
log.Printf("[URL生成] 预签名失败, 使用直接URL: %v", genErr)
fileURL = minioEngine.GetURL(file.StorageKey)
// 预览模式追加inline头参数, 避免触发下载
if urlType == "preview" {
fileURL += "?response-content-disposition=inline"
}
}
policy := getStoragePolicyConfig(file.StoragePolicyID)
if policy != nil && policy.NginxEndpoint != "" {
presignedURL = replaceMinIOEndpoint(presignedURL, minioEngine.Endpoint, policy.NginxEndpoint)
fileURL = replaceMinIOEndpoint(fileURL, minioEngine.Endpoint, policy.NginxEndpoint)
}
return presignedURL, nil
return fileURL, nil
case "local":
// 本地存储: 有CDN就用CDN, 否则用API路径
cfg := config.AppConfig.Storage
if cfg.CDNHost != "" {
prefix := strings.TrimRight(cfg.CDNPathPrefix, "/")
// 策略分类子目录(hot/cold/public/slave)
category := getStoragePolicyCategory(file.StoragePolicyID)
if category != "" {
prefix = prefix + "/" + category
}
key := strings.TrimLeft(file.StorageKey, "/")
return fmt.Sprintf("%s%s/%s", cfg.CDNHost, prefix, key), nil
}
@@ -400,6 +429,15 @@ func getStoragePolicyType(policyID uint64) string {
return policy.Type
}
// getStoragePolicyCategory 获取存储策略分类(hot/cold/public/slave)
func getStoragePolicyCategory(policyID uint64) string {
var policy model.StoragePolicy
if config.DB.Select("policy_type").First(&policy, policyID).Error != nil {
return ""
}
return policy.PolicyType
}
// buildAPIURL 构建完整的API文件访问地址
func buildAPIURL(fileID uint64, urlType string, token ...string) string {
publicURL := config.AppConfig.Server.PublicURL
@@ -476,9 +514,97 @@ func (s *FileService) SearchByType(ownerID uint64, extensions []string) ([]dto.F
return vos, nil
}
// ListTrashed 查询回收站
// asyncTranscode 异步转码视频(H.265→H.264), 支持本地存储和MinIO
func (s *FileService) asyncTranscode(engine storage.Engine, file *model.File) {
if !utils.FFmpegAvailable() {
return
}
// 根据存储引擎类型处理
switch e := engine.(type) {
case *storage.LocalEngine:
s.transcodeLocal(e, file)
case *storage.MinIOEngine:
s.transcodeMinIO(e, file)
}
}
// transcodeLocal 本地存储转码
func (s *FileService) transcodeLocal(localEngine *storage.LocalEngine, file *model.File) {
filePath := localEngine.GetFullPath(file.StorageKey)
if !utils.NeedsTranscode(filePath) {
return
}
log.Printf("[转码] 检测到H.265视频, 开始转码: %s", file.Name)
utils.TranscodeToH264(filePath)
// 更新文件大小(转码后大小可能变化)
if info, err := os.Stat(filePath); err == nil {
s.fileRepo.UpdateSize(file.ID, info.Size())
}
}
// transcodeMinIO MinIO存储转码: 下载→转码→重新上传
func (s *FileService) transcodeMinIO(minioEngine *storage.MinIOEngine, file *model.File) {
// 下载到临时文件
tmpInput := os.TempDir() + "/transcode_in_" + file.MD5 + filepath.Ext(file.Name)
reader, err := minioEngine.Download(file.StorageKey)
if err != nil {
log.Printf("[转码] MinIO下载失败: %v", err)
return
}
defer reader.Close()
outFile, err := os.Create(tmpInput)
if err != nil {
log.Printf("[转码] 创建临时文件失败: %v", err)
return
}
io.Copy(outFile, reader)
outFile.Close()
defer os.Remove(tmpInput)
// 检测是否需要转码
if !utils.NeedsTranscode(tmpInput) {
return
}
log.Printf("[转码] 检测到H.265视频(MinIO), 开始转码: %s", file.Name)
// 转码
tmpOutput := os.TempDir() + "/transcode_out_" + file.MD5 + ".mp4"
utils.TranscodeToH264(tmpInput) // 会生成 tmpInput 的 _h264.mp4 版本并替换
// TranscodeToH264 会替换原文件, 所以用 tmpInput 作为转码结果
// 重新上传到 MinIO
uploadFile, err := os.Open(tmpInput)
if err != nil {
log.Printf("[转码] 打开转码文件失败: %v", err)
return
}
defer uploadFile.Close()
defer os.Remove(tmpInput)
newSize, _ := uploadFile.Seek(0, io.SeekEnd)
uploadFile.Seek(0, io.SeekStart)
if err := minioEngine.Upload(file.StorageKey, uploadFile, newSize); err != nil {
log.Printf("[转码] MinIO上传失败: %v", err)
return
}
// 更新文件大小
s.fileRepo.UpdateSize(file.ID, newSize)
log.Printf("[转码] MinIO转码完成: %s, 新大小: %d", file.Name, newSize)
os.Remove(tmpOutput)
}
// ListTrashed 查询回收站(管理员看全部, 普通用户看自己的)
func (s *FileService) ListTrashed(ownerID uint64) ([]dto.FileVO, error) {
files, err := s.fileRepo.ListTrashed(ownerID)
isAdmin := s.isAdmin(ownerID)
files, err := s.fileRepo.ListTrashed(ownerID, isAdmin)
if err != nil {
return nil, err
}
@@ -499,6 +625,7 @@ func (s *FileService) GetDownloadPath(storageKey string) string {
func (s *FileService) toFileVO(file *model.File) *dto.FileVO {
vo := &dto.FileVO{
ID: dto.StringID(file.ID),
BizID: file.BizID,
Name: file.Name,
Extension: file.Extension,
FolderID: dto.StringID(file.FolderID),
@@ -666,6 +793,7 @@ func (s *FileService) GrantPermission(fileID uint64, grantBy uint64, req *dto.Gr
}
return config.DB.Create(&model.FilePermission{
ID: utils.GenID(),
FileID: fileID,
UserID: userID,
RoleID: roleID,

View File

@@ -48,6 +48,14 @@ func (s *FolderService) Create(req *dto.CreateFolderRequest, ownerID uint64) (*d
return nil, errors.New("同名文件夹已存在")
}
// 检查biz_id唯一性
if req.BizID != "" {
existingBiz, _ := s.folderRepo.FindByBizID(req.BizID, ownerID)
if existingBiz != nil {
return nil, fmt.Errorf("业务编号 %s 已存在", req.BizID)
}
}
folderID := utils.GenID()
folder := &model.Folder{
ID: folderID,
@@ -253,6 +261,7 @@ func (s *FolderService) getUserRoleIDs(userID uint64) []uint64 {
func (s *FolderService) toFileVO(file *model.File) *dto.FileVO {
vo := &dto.FileVO{
ID: dto.StringID(file.ID),
BizID: file.BizID,
Name: file.Name,
Extension: file.Extension,
FolderID: dto.StringID(file.FolderID),

View File

@@ -4,6 +4,7 @@ import (
"seeyon-filesystem/config"
"seeyon-filesystem/dto"
"seeyon-filesystem/model"
"seeyon-filesystem/utils"
)
// LogService 日志服务
@@ -16,6 +17,7 @@ func NewLogService() *LogService {
// Record 记录操作日志
func (s *LogService) Record(userID uint64, username, action, resource, detail, ip, ua string, status int) {
log := &model.OperationLog{
ID: utils.GenID(),
UserID: userID,
Username: username,
Action: action,

View File

@@ -33,6 +33,7 @@ func (s *OpenAPIService) CreateApp(appName string) (*dto.AppVO, error) {
appSecret := generateRandomHex(32)
app := &model.App{
ID: utils.GenID(),
AppID: appID,
AppSecret: appSecret,
AppName: appName,

View File

@@ -4,6 +4,7 @@ import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
@@ -37,7 +38,7 @@ func (s *ShareService) Create(req *dto.ShareCreateRequest, ownerID uint64) (*dto
if err != nil {
return nil, errors.New("文件不存在")
}
if file.OwnerID != ownerID {
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
return nil, errors.New("无权分享此文件")
}
@@ -48,6 +49,7 @@ func (s *ShareService) Create(req *dto.ShareCreateRequest, ownerID uint64) (*dto
}
share := &model.Share{
ID: utils.GenID(),
FileID: fileID,
OwnerID: ownerID,
ShareCode: shareCode,
@@ -176,7 +178,7 @@ func (s *ShareService) toShareVO(share *model.Share) *dto.ShareVO {
// 生成完整分享链接
publicURL := config.AppConfig.Server.PublicURL
if publicURL == "" {
publicURL = "http://localhost:8080"
publicURL = fmt.Sprintf("http://localhost:%d", config.AppConfig.Server.Port)
}
publicURL = strings.TrimRight(publicURL, "/")
@@ -194,6 +196,29 @@ func (s *ShareService) toShareVO(share *model.Share) *dto.ShareVO {
}
}
// canAccessFile 检查用户是否有权操作该文件(owner / admin / 被授权)
func (s *ShareService) canAccessFile(fileID, fileOwnerID, userID uint64) bool {
if fileOwnerID == userID {
return true
}
// 管理员
var count int64
config.DB.Model(&model.UserRole{}).
Joins("JOIN fs_role ON fs_role.id = fs_user_role.role_id").
Where("fs_user_role.user_id = ? AND fs_role.name = ?", userID, "admin").
Count(&count)
if count > 0 {
return true
}
// 文件级授权
config.DB.Model(&model.FilePermission{}).
Where("file_id = ? AND (user_id = ? OR role_id IN (?))",
fileID, userID,
config.DB.Model(&model.UserRole{}).Select("role_id").Where("user_id = ?", userID),
).Count(&count)
return count > 0
}
// generateShareCode 生成8字节随机分享码(16位十六进制字符串)
func generateShareCode() (string, error) {
bytes := make([]byte, 8)

View File

@@ -295,6 +295,7 @@ func (s *SSOService) findOrCreateUser(provider *model.SSOProvider, ssoUserData s
localUsername := fmt.Sprintf("sso_%s_%s", provider.Type, username)
newUser := &model.User{
ID: utils.GenID(),
Username: localUsername,
Password: "_SSO_NO_PASSWORD_", // SSO用户无本地密码
Email: ssoUserData["email"],
@@ -323,6 +324,7 @@ func (s *SSOService) findOrCreateUser(provider *model.SSOProvider, ssoUserData s
// saveSession 保存SSO会话
func (s *SSOService) saveSession(userID, providerID uint64, externalID, accessToken, refreshToken string, expiresAt time.Time) {
session := &model.SSOSession{
ID: utils.GenID(),
UserID: userID,
ProviderID: providerID,
ExternalID: externalID,

View File

@@ -7,6 +7,7 @@ import (
"seeyon-filesystem/config"
"seeyon-filesystem/dto"
"seeyon-filesystem/model"
"seeyon-filesystem/utils"
)
// StorageAssignService 存储策略分配服务
@@ -245,6 +246,7 @@ func (s *StorageAssignService) SetDefault(policyID uint64) error {
// 创建新记录
return config.DB.Create(&model.StorageAssignment{
ID: utils.GenID(),
PolicyID: policyID,
IsDefault: 1,
Status: 1,
@@ -265,6 +267,7 @@ func (s *StorageAssignService) AssignToRole(roleID, policyID uint64, quota int64
}
return config.DB.Create(&model.StorageAssignment{
ID: utils.GenID(),
GroupID: roleID,
PolicyID: policyID,
StorageQuota: quota,
@@ -286,6 +289,7 @@ func (s *StorageAssignService) AssignToUser(userID, policyID uint64, quota int64
}
return config.DB.Create(&model.StorageAssignment{
ID: utils.GenID(),
UserID: userID,
PolicyID: policyID,
StorageQuota: quota,

View File

@@ -1,5 +1,6 @@
-- =============================================
-- 文件管理系统 - MySQL 初始化脚本
-- 字符集: utf8mb4
-- =============================================
CREATE DATABASE IF NOT EXISTS seeyon_fs DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -8,24 +9,24 @@ USE seeyon_fs;
-- ========== 用户与权限 ==========
CREATE TABLE IF NOT EXISTS fs_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) UNIQUE NOT NULL,
password VARCHAR(256) NOT NULL,
email VARCHAR(128),
nickname VARCHAR(64),
avatar VARCHAR(512),
role VARCHAR(32) DEFAULT 'user',
dept_id BIGINT DEFAULT 0,
status TINYINT DEFAULT 1,
storage_quota BIGINT DEFAULT 10737418240,
used_storage BIGINT DEFAULT 0,
id BIGINT PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
password VARCHAR(256) NOT NULL,
email VARCHAR(128),
nickname VARCHAR(64),
avatar VARCHAR(512),
role VARCHAR(32) DEFAULT 'user',
dept_id BIGINT DEFAULT 0,
status TINYINT DEFAULT 1,
storage_quota BIGINT DEFAULT 10737418240,
used_storage BIGINT DEFAULT 0,
must_change_pwd TINYINT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
name VARCHAR(64) UNIQUE NOT NULL,
display_name VARCHAR(64) NOT NULL,
description VARCHAR(256),
@@ -36,7 +37,7 @@ CREATE TABLE IF NOT EXISTS fs_role (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
code VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(64) NOT NULL,
resource VARCHAR(32) NOT NULL,
@@ -56,15 +57,30 @@ CREATE TABLE IF NOT EXISTS fs_user_role (
PRIMARY KEY (user_id, role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ========== SSO ==========
-- ========== 组织架构 ==========
CREATE TABLE IF NOT EXISTS fs_department (
id BIGINT PRIMARY KEY,
name VARCHAR(128) NOT NULL,
code VARCHAR(64) UNIQUE,
parent_id BIGINT DEFAULT 0,
path VARCHAR(1024) NOT NULL,
leader_id BIGINT DEFAULT 0,
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ========== SSO 单点登录 ==========
CREATE TABLE IF NOT EXISTS fs_sso_provider (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
type VARCHAR(32) NOT NULL,
display_name VARCHAR(64) NOT NULL,
icon VARCHAR(512),
config JSON,
config TEXT,
auto_create TINYINT DEFAULT 1,
default_role VARCHAR(32) DEFAULT 'user',
status TINYINT DEFAULT 1,
@@ -74,7 +90,7 @@ CREATE TABLE IF NOT EXISTS fs_sso_provider (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_sso_session (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
provider_id BIGINT NOT NULL,
external_id VARCHAR(256) NOT NULL,
@@ -87,23 +103,10 @@ CREATE TABLE IF NOT EXISTS fs_sso_session (
INDEX idx_provider_external (provider_id, external_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ========== 组织架构 ==========
CREATE TABLE IF NOT EXISTS fs_department (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(128) NOT NULL,
code VARCHAR(64),
parent_id BIGINT DEFAULT 0,
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ========== 文件管理 ==========
CREATE TABLE IF NOT EXISTS fs_folder (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
biz_id VARCHAR(128) DEFAULT '',
name VARCHAR(256) NOT NULL,
parent_id BIGINT DEFAULT 0,
@@ -119,7 +122,8 @@ CREATE TABLE IF NOT EXISTS fs_folder (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_file (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
biz_id VARCHAR(128) DEFAULT '',
name VARCHAR(256) NOT NULL,
extension VARCHAR(32),
folder_id BIGINT NOT NULL,
@@ -136,14 +140,16 @@ CREATE TABLE IF NOT EXISTS fs_file (
version INT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_biz (biz_id),
INDEX idx_folder (folder_id),
INDEX idx_owner (owner_id),
INDEX idx_md5 (md5),
INDEX idx_policy (storage_policy_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_file_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
file_id BIGINT NOT NULL,
user_id BIGINT DEFAULT 0,
role_id BIGINT DEFAULT 0,
@@ -154,8 +160,17 @@ CREATE TABLE IF NOT EXISTS fs_file_permission (
INDEX idx_fp_role_file (role_id, file_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_file_association (
id BIGINT PRIMARY KEY,
biz_id VARCHAR(128) NOT NULL,
biz_type VARCHAR(64) NOT NULL,
file_id BIGINT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_biz (biz_id, biz_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_share (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
file_id BIGINT NOT NULL,
owner_id BIGINT NOT NULL,
share_code VARCHAR(32) UNIQUE NOT NULL,
@@ -172,17 +187,17 @@ CREATE TABLE IF NOT EXISTS fs_share (
-- ========== 存储策略 ==========
CREATE TABLE IF NOT EXISTS fs_storage_policy (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
type VARCHAR(32) NOT NULL,
policy_type VARCHAR(32) DEFAULT '',
config JSON,
policy_type VARCHAR(32) DEFAULT 'hot',
config TEXT,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_storage_assignment (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
policy_id BIGINT NOT NULL,
user_id BIGINT DEFAULT 0,
group_id BIGINT DEFAULT 0,
@@ -201,7 +216,7 @@ CREATE TABLE IF NOT EXISTS fs_storage_assignment (
-- ========== 开放平台 ==========
CREATE TABLE IF NOT EXISTS fs_app (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
app_id VARCHAR(64) UNIQUE NOT NULL,
app_secret VARCHAR(128) NOT NULL,
app_name VARCHAR(128) NOT NULL,
@@ -211,7 +226,7 @@ CREATE TABLE IF NOT EXISTS fs_app (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_app_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
id BIGINT PRIMARY KEY,
app_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -222,60 +237,87 @@ CREATE TABLE IF NOT EXISTS fs_app_user (
-- ========== 分片上传 ==========
CREATE TABLE IF NOT EXISTS fs_upload_session (
id VARCHAR(64) PRIMARY KEY,
file_name VARCHAR(256) NOT NULL,
file_size BIGINT NOT NULL,
chunk_size BIGINT NOT NULL,
total_chunks INT NOT NULL,
uploaded INT DEFAULT 0,
md5 VARCHAR(64),
sha256 VARCHAR(64),
folder_id BIGINT DEFAULT 0,
owner_id BIGINT NOT NULL,
policy_id BIGINT DEFAULT 0,
file_id BIGINT DEFAULT 0,
status TINYINT DEFAULT 1,
temp_dir VARCHAR(512),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
id VARCHAR(64) PRIMARY KEY,
file_id BIGINT DEFAULT 0,
file_name VARCHAR(256) NOT NULL,
file_size BIGINT NOT NULL,
chunk_size BIGINT NOT NULL,
total_chunks INT NOT NULL,
uploaded_chunks TEXT,
md5 VARCHAR(32),
sha256 VARCHAR(64),
folder_id BIGINT NOT NULL,
owner_id BIGINT NOT NULL,
policy_id BIGINT DEFAULT 1,
status TINYINT DEFAULT 1,
temp_dir VARCHAR(512),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ========== 用户会话(多端登录) ==========
CREATE TABLE IF NOT EXISTS fs_user_session (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
token VARCHAR(512) NOT NULL,
device VARCHAR(128),
ip VARCHAR(64),
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ========== 操作日志 ==========
CREATE TABLE IF NOT EXISTS fs_operation_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
id BIGINT PRIMARY KEY,
user_id BIGINT DEFAULT 0,
username VARCHAR(64),
action VARCHAR(32) NOT NULL,
resource VARCHAR(32) NOT NULL,
target_id BIGINT DEFAULT 0,
target_name VARCHAR(256),
detail TEXT,
resource VARCHAR(32),
detail VARCHAR(512),
ip VARCHAR(64),
user_agent VARCHAR(256),
status INT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_action (action),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ========== 备份 ==========
CREATE TABLE IF NOT EXISTS fs_backup_policy (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(128) NOT NULL,
type VARCHAR(32) NOT NULL,
config JSON,
status TINYINT DEFAULT 1,
last_run DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
id BIGINT PRIMARY KEY,
name VARCHAR(128) NOT NULL,
backup_type VARCHAR(32) NOT NULL,
target_type VARCHAR(32) NOT NULL,
target_config TEXT,
include_db TINYINT DEFAULT 1,
include_files TINYINT DEFAULT 1,
cron_expr VARCHAR(64),
max_backups INT DEFAULT 7,
status TINYINT DEFAULT 1,
last_run DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS fs_backup_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
policy_id BIGINT NOT NULL,
status TINYINT DEFAULT 1,
file_path VARCHAR(512),
file_size BIGINT DEFAULT 0,
detail TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
id BIGINT PRIMARY KEY,
policy_id BIGINT NOT NULL,
policy_name VARCHAR(128),
backup_type VARCHAR(32),
status VARCHAR(32) DEFAULT 'running',
file_path VARCHAR(512),
file_size BIGINT DEFAULT 0,
db_size BIGINT DEFAULT 0,
files_count INT DEFAULT 0,
error_msg VARCHAR(512),
started_at DATETIME,
finished_at DATETIME,
duration INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_policy (policy_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -1,436 +0,0 @@
-- =============================================
-- 文件管理系统 - Oracle 初始化脚本
-- 注意: 需要 Oracle 12c+ (IDENTITY 支持)
-- =============================================
-- ========== 用户与权限 ==========
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_user (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
username VARCHAR2(64) UNIQUE NOT NULL,
password VARCHAR2(256) NOT NULL,
email VARCHAR2(128),
nickname VARCHAR2(64),
avatar VARCHAR2(512),
role VARCHAR2(32) DEFAULT ''user'',
dept_id NUMBER DEFAULT 0,
status NUMBER(3) DEFAULT 1,
storage_quota NUMBER DEFAULT 10737418240,
used_storage NUMBER DEFAULT 0,
must_change_pwd NUMBER(3) DEFAULT 0,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_role (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR2(64) UNIQUE NOT NULL,
display_name VARCHAR2(64) NOT NULL,
description VARCHAR2(256),
is_system NUMBER(3) DEFAULT 0,
status NUMBER(3) DEFAULT 1,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_permission (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
code VARCHAR2(64) UNIQUE NOT NULL,
name VARCHAR2(64) NOT NULL,
resource VARCHAR2(32) NOT NULL,
action VARCHAR2(32) NOT NULL,
description VARCHAR2(256)
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_role_permission (
role_id NUMBER NOT NULL,
permission_id NUMBER NOT NULL,
PRIMARY KEY (role_id, permission_id)
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_user_role (
user_id NUMBER NOT NULL,
role_id NUMBER NOT NULL,
PRIMARY KEY (user_id, role_id)
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
-- ========== SSO ==========
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_sso_provider (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR2(64) NOT NULL,
type VARCHAR2(32) NOT NULL,
display_name VARCHAR2(64) NOT NULL,
icon VARCHAR2(512),
config CLOB,
auto_create NUMBER(3) DEFAULT 1,
default_role VARCHAR2(32) DEFAULT ''user'',
status NUMBER(3) DEFAULT 1,
sort_order NUMBER DEFAULT 0,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_sso_session (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id NUMBER NOT NULL,
provider_id NUMBER NOT NULL,
external_id VARCHAR2(256) NOT NULL,
access_token VARCHAR2(1024),
refresh_token VARCHAR2(1024),
expires_at TIMESTAMP,
last_login_at TIMESTAMP,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_sso_user ON fs_sso_session(user_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_sso_provider_ext ON fs_sso_session(provider_id, external_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
-- ========== 组织架构 ==========
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_department (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR2(128) NOT NULL,
code VARCHAR2(64),
parent_id NUMBER DEFAULT 0,
sort_order NUMBER DEFAULT 0,
status NUMBER(3) DEFAULT 1,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
-- ========== 文件管理 ==========
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_folder (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
biz_id VARCHAR2(128) DEFAULT '''',
name VARCHAR2(256) NOT NULL,
parent_id NUMBER DEFAULT 0,
owner_id NUMBER NOT NULL,
path VARCHAR2(1024) NOT NULL,
status NUMBER(3) DEFAULT 1,
version NUMBER DEFAULT 1,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_folder_parent ON fs_folder(parent_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_folder_owner ON fs_folder(owner_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_folder_biz ON fs_folder(biz_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_file (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR2(256) NOT NULL,
extension VARCHAR2(32),
folder_id NUMBER NOT NULL,
owner_id NUMBER NOT NULL,
size NUMBER DEFAULT 0,
md5 VARCHAR2(32),
sha256 VARCHAR2(64),
storage_key VARCHAR2(512) NOT NULL,
storage_policy_id NUMBER DEFAULT 1,
mime_type VARCHAR2(128),
is_favorite NUMBER(3) DEFAULT 0,
status NUMBER(3) DEFAULT 1,
download_count NUMBER DEFAULT 0,
version NUMBER DEFAULT 1,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_file_folder ON fs_file(folder_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_file_owner ON fs_file(owner_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_file_md5 ON fs_file(md5)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_file_permission (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
file_id NUMBER NOT NULL,
user_id NUMBER DEFAULT 0,
role_id NUMBER DEFAULT 0,
perm_level VARCHAR2(16) DEFAULT ''read'',
grant_by NUMBER NOT NULL,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_fp_user_file ON fs_file_permission(user_id, file_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_fp_role_file ON fs_file_permission(role_id, file_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_share (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
file_id NUMBER NOT NULL,
owner_id NUMBER NOT NULL,
share_code VARCHAR2(32) UNIQUE NOT NULL,
password VARCHAR2(64),
expire_at TIMESTAMP,
max_download NUMBER DEFAULT 0,
download_count NUMBER DEFAULT 0,
allow_preview NUMBER(3) DEFAULT 1,
allow_download NUMBER(3) DEFAULT 1,
status NUMBER(3) DEFAULT 1,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
-- ========== 存储策略 ==========
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_storage_policy (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR2(64) NOT NULL,
type VARCHAR2(32) NOT NULL,
policy_type VARCHAR2(32) DEFAULT '''',
config CLOB,
status NUMBER(3) DEFAULT 1,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_storage_assignment (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
policy_id NUMBER NOT NULL,
user_id NUMBER DEFAULT 0,
group_id NUMBER DEFAULT 0,
is_default NUMBER(3) DEFAULT 0,
priority NUMBER DEFAULT 0,
storage_quota NUMBER DEFAULT 0,
access_mode VARCHAR2(32),
status NUMBER(3) DEFAULT 1,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_assign_policy ON fs_storage_assignment(policy_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_assign_user ON fs_storage_assignment(user_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_assign_group ON fs_storage_assignment(group_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
-- ========== 开放平台 ==========
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_app (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
app_id VARCHAR2(64) UNIQUE NOT NULL,
app_secret VARCHAR2(128) NOT NULL,
app_name VARCHAR2(128) NOT NULL,
status NUMBER(3) DEFAULT 1,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_app_user (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
app_id NUMBER NOT NULL,
user_id NUMBER NOT NULL,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
-- ========== 分片上传 ==========
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_upload_session (
id VARCHAR2(64) PRIMARY KEY,
file_name VARCHAR2(256) NOT NULL,
file_size NUMBER NOT NULL,
chunk_size NUMBER NOT NULL,
total_chunks NUMBER NOT NULL,
uploaded NUMBER DEFAULT 0,
md5 VARCHAR2(64),
sha256 VARCHAR2(64),
folder_id NUMBER DEFAULT 0,
owner_id NUMBER NOT NULL,
policy_id NUMBER DEFAULT 0,
file_id NUMBER DEFAULT 0,
status NUMBER(3) DEFAULT 1,
temp_dir VARCHAR2(512),
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
-- ========== 操作日志 ==========
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_operation_log (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id NUMBER NOT NULL,
username VARCHAR2(64),
action VARCHAR2(32) NOT NULL,
resource VARCHAR2(32) NOT NULL,
target_id NUMBER DEFAULT 0,
target_name VARCHAR2(256),
detail CLOB,
ip VARCHAR2(64),
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_log_user ON fs_operation_log(user_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_log_created ON fs_operation_log(created_at)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
-- ========== 备份 ==========
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_backup_policy (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR2(128) NOT NULL,
type VARCHAR2(32) NOT NULL,
config CLOB,
status NUMBER(3) DEFAULT 1,
last_run TIMESTAMP,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE fs_backup_log (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
policy_id NUMBER NOT NULL,
status NUMBER(3) DEFAULT 1,
file_path VARCHAR2(512),
file_size NUMBER DEFAULT 0,
detail CLOB,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP
)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX idx_backup_policy ON fs_backup_log(policy_id)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF;
END;
/

View File

@@ -15,26 +15,26 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_user')
CREATE TABLE fs_user (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
username NVARCHAR(64) UNIQUE NOT NULL,
password NVARCHAR(256) NOT NULL,
email NVARCHAR(128),
nickname NVARCHAR(64),
avatar NVARCHAR(512),
role NVARCHAR(32) DEFAULT 'user',
dept_id BIGINT DEFAULT 0,
status TINYINT DEFAULT 1,
storage_quota BIGINT DEFAULT 10737418240,
used_storage BIGINT DEFAULT 0,
id BIGINT PRIMARY KEY,
username NVARCHAR(64) UNIQUE NOT NULL,
password NVARCHAR(256) NOT NULL,
email NVARCHAR(128),
nickname NVARCHAR(64),
avatar NVARCHAR(512),
role NVARCHAR(32) DEFAULT 'user',
dept_id BIGINT DEFAULT 0,
status TINYINT DEFAULT 1,
storage_quota BIGINT DEFAULT 10737418240,
used_storage BIGINT DEFAULT 0,
must_change_pwd TINYINT DEFAULT 0,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_role')
CREATE TABLE fs_role (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
name NVARCHAR(64) UNIQUE NOT NULL,
display_name NVARCHAR(64) NOT NULL,
description NVARCHAR(256),
@@ -47,7 +47,7 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_permission')
CREATE TABLE fs_permission (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
code NVARCHAR(64) UNIQUE NOT NULL,
name NVARCHAR(64) NOT NULL,
resource NVARCHAR(32) NOT NULL,
@@ -72,11 +72,28 @@ CREATE TABLE fs_user_role (
);
GO
-- ========== 组织架构 ==========
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_department')
CREATE TABLE fs_department (
id BIGINT PRIMARY KEY,
name NVARCHAR(128) NOT NULL,
code NVARCHAR(64) UNIQUE,
parent_id BIGINT DEFAULT 0,
path NVARCHAR(1024) NOT NULL,
leader_id BIGINT DEFAULT 0,
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
GO
-- ========== SSO ==========
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_sso_provider')
CREATE TABLE fs_sso_provider (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
name NVARCHAR(64) NOT NULL,
type NVARCHAR(32) NOT NULL,
display_name NVARCHAR(64) NOT NULL,
@@ -93,7 +110,7 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_sso_session')
CREATE TABLE fs_sso_session (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
provider_id BIGINT NOT NULL,
external_id NVARCHAR(256) NOT NULL,
@@ -109,26 +126,11 @@ IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_sso_provider_ext')
CREATE INDEX idx_sso_provider_ext ON fs_sso_session(provider_id, external_id);
GO
-- ========== 组织架构 ==========
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_department')
CREATE TABLE fs_department (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(128) NOT NULL,
code NVARCHAR(64),
parent_id BIGINT DEFAULT 0,
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
GO
-- ========== 文件管理 ==========
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_folder')
CREATE TABLE fs_folder (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
biz_id NVARCHAR(128) DEFAULT '',
name NVARCHAR(256) NOT NULL,
parent_id BIGINT DEFAULT 0,
@@ -149,7 +151,8 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_file')
CREATE TABLE fs_file (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
biz_id NVARCHAR(128) DEFAULT '',
name NVARCHAR(256) NOT NULL,
extension NVARCHAR(32),
folder_id BIGINT NOT NULL,
@@ -167,19 +170,23 @@ CREATE TABLE fs_file (
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_file_biz')
CREATE INDEX idx_file_biz ON fs_file(biz_id);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_file_folder')
CREATE INDEX idx_file_folder ON fs_file(folder_id);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_file_owner')
CREATE INDEX idx_file_owner ON fs_file(owner_id);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_file_md5')
CREATE INDEX idx_file_md5 ON fs_file(md5);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_file_policy')
CREATE INDEX idx_file_policy ON fs_file(storage_policy_id);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_file_status')
CREATE INDEX idx_file_status ON fs_file(status);
GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_file_permission')
CREATE TABLE fs_file_permission (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
file_id BIGINT NOT NULL,
user_id BIGINT DEFAULT 0,
role_id BIGINT DEFAULT 0,
@@ -193,9 +200,21 @@ IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_fp_role_file')
CREATE INDEX idx_fp_role_file ON fs_file_permission(role_id, file_id);
GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_file_association')
CREATE TABLE fs_file_association (
id BIGINT PRIMARY KEY,
biz_id NVARCHAR(128) NOT NULL,
biz_type NVARCHAR(64) NOT NULL,
file_id BIGINT NOT NULL,
created_at DATETIME2 DEFAULT GETDATE()
);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_assoc_biz')
CREATE INDEX idx_assoc_biz ON fs_file_association(biz_id, biz_type);
GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_share')
CREATE TABLE fs_share (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
file_id BIGINT NOT NULL,
owner_id BIGINT NOT NULL,
share_code NVARCHAR(32) UNIQUE NOT NULL,
@@ -214,10 +233,10 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_storage_policy')
CREATE TABLE fs_storage_policy (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
name NVARCHAR(64) NOT NULL,
type NVARCHAR(32) NOT NULL,
policy_type NVARCHAR(32) DEFAULT '',
policy_type NVARCHAR(32) DEFAULT 'hot',
config NVARCHAR(MAX),
status TINYINT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE()
@@ -226,7 +245,7 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_storage_assignment')
CREATE TABLE fs_storage_assignment (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
policy_id BIGINT NOT NULL,
user_id BIGINT DEFAULT 0,
group_id BIGINT DEFAULT 0,
@@ -250,7 +269,7 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_app')
CREATE TABLE fs_app (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
app_id NVARCHAR(64) UNIQUE NOT NULL,
app_secret NVARCHAR(128) NOT NULL,
app_name NVARCHAR(128) NOT NULL,
@@ -262,7 +281,7 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_app_user')
CREATE TABLE fs_app_user (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
id BIGINT PRIMARY KEY,
app_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
created_at DATETIME2 DEFAULT GETDATE()
@@ -277,42 +296,62 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_upload_session')
CREATE TABLE fs_upload_session (
id NVARCHAR(64) PRIMARY KEY,
file_name NVARCHAR(256) NOT NULL,
file_size BIGINT NOT NULL,
chunk_size BIGINT NOT NULL,
total_chunks INT NOT NULL,
uploaded INT DEFAULT 0,
md5 NVARCHAR(64),
sha256 NVARCHAR(64),
folder_id BIGINT DEFAULT 0,
owner_id BIGINT NOT NULL,
policy_id BIGINT DEFAULT 0,
file_id BIGINT DEFAULT 0,
status TINYINT DEFAULT 1,
temp_dir NVARCHAR(512),
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
id NVARCHAR(64) PRIMARY KEY,
file_id BIGINT DEFAULT 0,
file_name NVARCHAR(256) NOT NULL,
file_size BIGINT NOT NULL,
chunk_size BIGINT NOT NULL,
total_chunks INT NOT NULL,
uploaded_chunks NVARCHAR(MAX),
md5 NVARCHAR(32),
sha256 NVARCHAR(64),
folder_id BIGINT NOT NULL,
owner_id BIGINT NOT NULL,
policy_id BIGINT DEFAULT 1,
status TINYINT DEFAULT 1,
temp_dir NVARCHAR(512),
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
GO
-- ========== 用户会话(多端登录) ==========
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_user_session')
CREATE TABLE fs_user_session (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
user_id BIGINT NOT NULL,
token NVARCHAR(512) NOT NULL,
device NVARCHAR(128),
ip NVARCHAR(64),
expires_at DATETIME2 NOT NULL,
created_at DATETIME2 DEFAULT GETDATE()
);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_session_user')
CREATE INDEX idx_session_user ON fs_user_session(user_id);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_session_expires')
CREATE INDEX idx_session_expires ON fs_user_session(expires_at);
GO
-- ========== 操作日志 ==========
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_operation_log')
CREATE TABLE fs_operation_log (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
user_id BIGINT NOT NULL,
username NVARCHAR(64),
action NVARCHAR(32) NOT NULL,
resource NVARCHAR(32) NOT NULL,
target_id BIGINT DEFAULT 0,
target_name NVARCHAR(256),
detail NVARCHAR(MAX),
ip NVARCHAR(64),
created_at DATETIME2 DEFAULT GETDATE()
id BIGINT PRIMARY KEY,
user_id BIGINT DEFAULT 0,
username NVARCHAR(64),
action NVARCHAR(32) NOT NULL,
resource NVARCHAR(32),
detail NVARCHAR(512),
ip NVARCHAR(64),
user_agent NVARCHAR(256),
status INT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE()
);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_log_user')
CREATE INDEX idx_log_user ON fs_operation_log(user_id);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_log_action')
CREATE INDEX idx_log_action ON fs_operation_log(action);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_log_created')
CREATE INDEX idx_log_created ON fs_operation_log(created_at);
GO
@@ -321,25 +360,37 @@ GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_backup_policy')
CREATE TABLE fs_backup_policy (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(128) NOT NULL,
type NVARCHAR(32) NOT NULL,
config NVARCHAR(MAX),
status TINYINT DEFAULT 1,
last_run DATETIME2,
created_at DATETIME2 DEFAULT GETDATE()
id BIGINT PRIMARY KEY,
name NVARCHAR(128) NOT NULL,
backup_type NVARCHAR(32) NOT NULL,
target_type NVARCHAR(32) NOT NULL,
target_config NVARCHAR(MAX),
include_db TINYINT DEFAULT 1,
include_files TINYINT DEFAULT 1,
cron_expr NVARCHAR(64),
max_backups INT DEFAULT 7,
status TINYINT DEFAULT 1,
last_run DATETIME2,
created_at DATETIME2 DEFAULT GETDATE()
);
GO
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_backup_log')
CREATE TABLE fs_backup_log (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
policy_id BIGINT NOT NULL,
status TINYINT DEFAULT 1,
file_path NVARCHAR(512),
file_size BIGINT DEFAULT 0,
detail NVARCHAR(MAX),
created_at DATETIME2 DEFAULT GETDATE()
id BIGINT PRIMARY KEY,
policy_id BIGINT NOT NULL,
policy_name NVARCHAR(128),
backup_type NVARCHAR(32),
status NVARCHAR(32) DEFAULT 'running',
file_path NVARCHAR(512),
file_size BIGINT DEFAULT 0,
db_size BIGINT DEFAULT 0,
files_count INT DEFAULT 0,
error_msg NVARCHAR(512),
started_at DATETIME2,
finished_at DATETIME2,
duration INT DEFAULT 0,
created_at DATETIME2 DEFAULT GETDATE()
);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_backup_policy')
CREATE INDEX idx_backup_policy ON fs_backup_log(policy_id);

View File

@@ -10,6 +10,11 @@ import (
var engines = make(map[string]Engine)
var policyEngines = make(map[uint64]Engine)
// InvalidateEngine 清除指定策略的引擎缓存, 下次访问时重新创建
func InvalidateEngine(policyID uint64) {
delete(policyEngines, policyID)
}
func Register(policyType string, engine Engine) {
engines[policyType] = engine
}
@@ -18,14 +23,41 @@ func GetEngine(policy *model.StoragePolicy) (Engine, error) {
if engine, ok := policyEngines[policy.ID]; ok {
return engine, nil
}
engine, ok := engines[policy.Type]
if !ok {
return nil, fmt.Errorf("不支持的存储类型: %s", policy.Type)
}
if policy.Type == "local" && policy.Config.BasePath != "" {
// 根据策略类型创建引擎
var engine Engine
var err error
switch policy.Type {
case "local":
cfg := config.AppConfig.Storage
engine = NewLocalEngineWithCDN(policy.Config.BasePath, cfg.CDNHost, cfg.CDNPathPrefix)
basePath := cfg.LocalBasePath
if policy.Config.BasePath != "" {
basePath = policy.Config.BasePath
}
engine = NewLocalEngineWithCDN(basePath, cfg.CDNHost, cfg.CDNPathPrefix)
case "minio":
engine, err = NewMinIOEngine(
policy.Config.Endpoint,
policy.Config.AccessKey,
policy.Config.SecretKey,
policy.Config.Bucket,
policy.Config.UseSSL,
)
if err != nil {
return nil, fmt.Errorf("创建MinIO引擎失败: %w", err)
}
default:
// 尝试从 engines map 获取(兼容旧注册方式)
var ok bool
engine, ok = engines[policy.Type]
if !ok {
return nil, fmt.Errorf("不支持的存储类型: %s", policy.Type)
}
}
policyEngines[policy.ID] = engine
return engine, nil
}

View File

@@ -102,6 +102,11 @@ func (e *LocalEngine) GetURL(storageKey string) string {
return "/storage/" + storageKey
}
// GetFullPath 获取文件在磁盘上的完整路径
func (e *LocalEngine) GetFullPath(storageKey string) string {
return filepath.Join(e.BasePath, storageKey)
}
// GenerateStorageKey 生成存储路径
func GenerateStorageKey(md5, extension string) string {
now := time.Now()

View File

@@ -43,16 +43,6 @@ func ParseToken(tokenString, secret string) (*JWTClaims, error) {
}
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
jsonBytes, err := json.MarshalIndent(claims, "", " ") // MarshalIndent 用于格式化输出,更易读
if err != nil {
// 记录序列化错误,但不一定中断业务逻辑,视需求而定
log.Printf("Failed to marshal claims to JSON: %v", err)
} else {
// 2. 在控制台输出 JSON 字符串
fmt.Println("=== Decoded JWT Claims ===")
fmt.Println(string(jsonBytes))
fmt.Println("==========================")
}
return claims, nil
}