提交
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
// ============ 分享公开访问 (无需登录) ============
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 备份工具配置
|
||||
|
||||
@@ -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 用户列表
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
// ========== 操作日志 ==========
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 用户权限视图对象
|
||||
|
||||
176
server/main.go
176
server/main.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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=顶级
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
// 分配目标
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
|
||||
BIN
server/myapp
BIN
server/myapp
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
/
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user