diff --git a/deploy/config.yaml.example b/deploy/config.yaml.example deleted file mode 100644 index 7a9ee17..0000000 --- a/deploy/config.yaml.example +++ /dev/null @@ -1,152 +0,0 @@ - @echo off - chcp 437 - setlocal enabledelayedexpansion - - set DEPLOY_DIR=%~dp0..\deploy - set DB_NAME=seeyon_fs - - if "%~1"=="" ( - echo Usage: deploy.bat 1/2/3/4 - echo 1=MySQL 2=PostgreSQL 3=SQLServer 4=Oracle - pause >nul - exit /b 1 - ) - set DB_TYPE=%~1 - - echo. - echo ================================ - echo Build DB type: %DB_TYPE% - echo ================================ - echo. - - echo Remove old deploy... - if exist %DEPLOY_DIR% rmdir /s /q %DEPLOY_DIR% - - mkdir %DEPLOY_DIR%\data\storage\hot 2>nul - mkdir %DEPLOY_DIR%\data\storage\cold 2>nul - mkdir %DEPLOY_DIR%\data\storage\public 2>nul - mkdir %DEPLOY_DIR%\data\storage\slave 2>nul - - echo Building... - set GOPROXY=https://goproxy.cn - go build -ldflags="-s -w" -o %DEPLOY_DIR%\server.exe main.go - - if errorlevel 1 ( - echo Build failed - pause >nul - exit /b 1 - ) - - copy config.yaml %DEPLOY_DIR%\config.yaml.example >nul - xcopy /E /I /Q sql %DEPLOY_DIR%\sql >nul - - - :: -------------- start.bat -------------- - ( - echo @echo off - echo cd %%~dp0 - echo if not exist config.yaml ( - echo copy config.yaml.example config.yaml - echo echo Please edit config.yaml first. - echo pause >nul - echo exit /b 1 - echo ^) - echo server.exe - echo pause >nul - ) > %DEPLOY_DIR%\start.bat - - - :: -------------- start.sh -------------- - ( - echo #!/bin/bash - echo cd "$(dirname "$0")" - echo if [ ! -f config.yaml ]; then - echo cp config.yaml.example config.yaml - echo echo "Please edit config.yaml first." - echo exit 1 - echo fi - echo chmod +x server - echo ./server - ) > %DEPLOY_DIR%\start.sh - - - :: -------------- init-db.bat -------------- - ( - echo @echo off - echo cd %%~dp0 - echo echo Database: %DB_NAME% - echo. - ) > %DEPLOY_DIR%\init-db.bat - - if %DB_TYPE%==1 ( - echo set /p "pwd=MySQL root password: " -echo mysql -uroot -p%%pwd% -e "CREATE DATABASE IF NOT EXISTS %DB_NAME%" - echo mysql -uroot -p%%pwd% %DB_NAME% ^< sql\mysql\init.sql - ) >> %DEPLOY_DIR%\init-db.bat - - if %DB_TYPE%==2 ( - echo set /p "pwd=Postgres password: " -echo set PGPASSWORD=%%pwd% -echo psql -U postgres -c "CREATE DATABASE %DB_NAME%" 2^>nul - echo psql -U postgres -d%DB_NAME% -f sql\postgres\init.sql - ) >> %DEPLOY_DIR%\init-db.bat - - if %DB_TYPE%==3 ( - echo set /p "pwd=SQL Server sa password: " -echo sqlcmd -U sa -P %%pwd% -Q "CREATE DATABASE [%DB_NAME%]" - echo sqlcmd -U sa -P %%pwd% -d%DB_NAME% -i sql\sqlserver\init.sql - ) >> %DEPLOY_DIR%\init-db.bat - - if %DB_TYPE%==4 ( - echo echo Oracle: create user/tablespace then run sql/oracle/init.sql - ) >> %DEPLOY_DIR%\init-db.bat - - ( - echo echo. - echo Initialize done. - echo pause >nul - ) >> %DEPLOY_DIR%\init-db.bat - - - :: -------------- init-db.sh -------------- - ( - echo #!/bin/bash - echo cd "$(dirname "$0")" - echo echo "Database: %DB_NAME%" - echo. - ) > %DEPLOY_DIR%\init-db.sh - - if %DB_TYPE%==1 ( - echo read -sp "MySQL password: " pwd -echo echo -echo mysql -uroot -p"$pwd" -e "CREATE DATABASE IF NOT EXISTS %DB_NAME%" - echo mysql -uroot -p"$pwd" %DB_NAME% < sql/mysql/init.sql - ) >> %DEPLOY_DIR%\init-db.sh - - if %DB_TYPE%==2 ( - echo read -sp "Postgres password: " pwd -echo echo -echo export PGPASSWORD="$pwd" - echo psql -U postgres -c "CREATE DATABASE %DB_NAME%" 2>/dev/null - echo psql -U postgres -d%DB_NAME% -f sql/postgres/init.sql - ) >> %DEPLOY_DIR%\init-db.sh - - if %DB_TYPE%==3 ( - echo read -sp "SQL Server sa password: " pwd -echo echo -echo sqlcmd -U sa -P "$pwd" -Q "CREATE DATABASE [%DB_NAME%]" - echo sqlcmd -U sa -P "$pwd" -d%DB_NAME% -i sql/sqlserver/init.sql - ) >> %DEPLOY_DIR%\init-db.sh - - if %DB_TYPE%==4 ( - echo echo "Oracle: create user/tablespace then run sql/oracle/init.sql" - ) >> %DEPLOY_DIR%\init-db.sh - - echo. - echo Build success: %DEPLOY_DIR% - echo. - echo Just run: - echo init-db.bat # 输入一次密码即可 - echo start.bat - echo. - pause >nul \ No newline at end of file diff --git a/deploy/init-db.bat b/deploy/init-db.bat deleted file mode 100644 index 0d6e053..0000000 --- a/deploy/init-db.bat +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -cd %~dp0 -set /p "db=DB name: " -set /p "sa_pwd=SQLServer sa pwd: " -sqlcmd -S . -U sa -P %sa_pwd% -Q "CREATE DATABASE %db%" -sqlcmd -S . -U sa -P %sa_pwd% -d %db% -i sql\sqlserver\init.sql diff --git a/deploy/server.exe b/deploy/server.exe deleted file mode 100644 index 0dd2462..0000000 Binary files a/deploy/server.exe and /dev/null differ diff --git a/deploy/sql/mysql/init.sql b/deploy/sql/mysql/init.sql deleted file mode 100644 index 29305fa..0000000 --- a/deploy/sql/mysql/init.sql +++ /dev/null @@ -1,281 +0,0 @@ --- ============================================= --- 文件管理系统 - MySQL 初始化脚本 --- ============================================= - -CREATE DATABASE IF NOT EXISTS seeyon_fs DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -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, - must_change_pwd TINYINT DEFAULT 0, - 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, - name VARCHAR(64) UNIQUE NOT NULL, - display_name VARCHAR(64) NOT NULL, - description VARCHAR(256), - is_system TINYINT 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_permission ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - code VARCHAR(64) UNIQUE NOT NULL, - name VARCHAR(64) NOT NULL, - resource VARCHAR(32) NOT NULL, - action VARCHAR(32) NOT NULL, - description VARCHAR(256) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE IF NOT EXISTS fs_role_permission ( - role_id BIGINT NOT NULL, - permission_id BIGINT NOT NULL, - PRIMARY KEY (role_id, permission_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE IF NOT EXISTS fs_user_role ( - user_id BIGINT NOT NULL, - role_id BIGINT NOT NULL, - PRIMARY KEY (user_id, role_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- ========== SSO ========== - -CREATE TABLE IF NOT EXISTS fs_sso_provider ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(64) NOT NULL, - type VARCHAR(32) NOT NULL, - display_name VARCHAR(64) NOT NULL, - icon VARCHAR(512), - config JSON, - auto_create TINYINT DEFAULT 1, - default_role VARCHAR(32) DEFAULT 'user', - status TINYINT DEFAULT 1, - sort_order INT DEFAULT 0, - 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_sso_session ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - user_id BIGINT NOT NULL, - provider_id BIGINT NOT NULL, - external_id VARCHAR(256) NOT NULL, - access_token VARCHAR(1024), - refresh_token VARCHAR(1024), - expires_at DATETIME, - last_login_at DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - INDEX idx_user (user_id), - 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, - biz_id VARCHAR(128) DEFAULT '', - name VARCHAR(256) NOT NULL, - parent_id BIGINT DEFAULT 0, - owner_id BIGINT NOT NULL, - path VARCHAR(1024) NOT NULL, - status TINYINT DEFAULT 1, - version INT DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_parent (parent_id), - INDEX idx_owner (owner_id), - INDEX idx_biz (biz_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE IF NOT EXISTS fs_file ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(256) NOT NULL, - extension VARCHAR(32), - folder_id BIGINT NOT NULL, - owner_id BIGINT NOT NULL, - size BIGINT DEFAULT 0, - md5 VARCHAR(32), - sha256 VARCHAR(64), - storage_key VARCHAR(512) NOT NULL, - storage_policy_id BIGINT DEFAULT 1, - mime_type VARCHAR(128), - is_favorite TINYINT DEFAULT 0, - status TINYINT DEFAULT 1, - download_count INT DEFAULT 0, - version INT DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_folder (folder_id), - INDEX idx_owner (owner_id), - INDEX idx_md5 (md5), - 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, - file_id BIGINT NOT NULL, - user_id BIGINT DEFAULT 0, - role_id BIGINT DEFAULT 0, - perm_level VARCHAR(16) DEFAULT 'read', - grant_by BIGINT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - INDEX idx_fp_user_file (user_id, file_id), - INDEX idx_fp_role_file (role_id, file_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE IF NOT EXISTS fs_share ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - file_id BIGINT NOT NULL, - owner_id BIGINT NOT NULL, - share_code VARCHAR(32) UNIQUE NOT NULL, - password VARCHAR(64), - expire_at DATETIME, - max_download INT DEFAULT 0, - download_count INT DEFAULT 0, - allow_preview TINYINT DEFAULT 1, - allow_download TINYINT DEFAULT 1, - 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_policy ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(64) NOT NULL, - type VARCHAR(32) NOT NULL, - policy_type VARCHAR(32) DEFAULT '', - config JSON, - 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, - policy_id BIGINT NOT NULL, - user_id BIGINT DEFAULT 0, - group_id BIGINT DEFAULT 0, - is_default TINYINT DEFAULT 0, - priority INT DEFAULT 0, - storage_quota BIGINT DEFAULT 0, - access_mode VARCHAR(32), - status TINYINT DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_policy (policy_id), - INDEX idx_user (user_id), - INDEX idx_group (group_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- ========== 开放平台 ========== - -CREATE TABLE IF NOT EXISTS fs_app ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - app_id VARCHAR(64) UNIQUE NOT NULL, - app_secret VARCHAR(128) NOT NULL, - app_name VARCHAR(128) NOT NULL, - 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_app_user ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - app_id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - INDEX idx_app (app_id), - INDEX idx_user (user_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- ========== 分片上传 ========== - -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 -) 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, - username VARCHAR(64), - action VARCHAR(32) NOT NULL, - resource VARCHAR(32) NOT NULL, - target_id BIGINT DEFAULT 0, - target_name VARCHAR(256), - detail TEXT, - ip VARCHAR(64), - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - INDEX idx_user (user_id), - 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 -) 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, - INDEX idx_policy (policy_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/deploy/sql/oracle/init.sql b/deploy/sql/oracle/init.sql deleted file mode 100644 index a7e68b6..0000000 --- a/deploy/sql/oracle/init.sql +++ /dev/null @@ -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; -/ diff --git a/deploy/sql/sqlserver/init.sql b/deploy/sql/sqlserver/init.sql deleted file mode 100644 index 3d153dc..0000000 --- a/deploy/sql/sqlserver/init.sql +++ /dev/null @@ -1,346 +0,0 @@ --- ============================================= --- 文件管理系统 - SQL Server 初始化脚本 --- ============================================= - -IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'seeyon_fs') -BEGIN - CREATE DATABASE seeyon_fs; -END -GO - -USE seeyon_fs; -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, - must_change_pwd TINYINT DEFAULT 0, - 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, - name NVARCHAR(64) UNIQUE NOT NULL, - display_name NVARCHAR(64) NOT NULL, - description NVARCHAR(256), - is_system TINYINT 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_permission') -CREATE TABLE fs_permission ( - id BIGINT IDENTITY(1,1) PRIMARY KEY, - code NVARCHAR(64) UNIQUE NOT NULL, - name NVARCHAR(64) NOT NULL, - resource NVARCHAR(32) NOT NULL, - action NVARCHAR(32) NOT NULL, - description NVARCHAR(256) -); -GO - -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_role_permission') -CREATE TABLE fs_role_permission ( - role_id BIGINT NOT NULL, - permission_id BIGINT NOT NULL, - PRIMARY KEY (role_id, permission_id) -); -GO - -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_user_role') -CREATE TABLE fs_user_role ( - user_id BIGINT NOT NULL, - role_id BIGINT NOT NULL, - PRIMARY KEY (user_id, role_id) -); -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, - name NVARCHAR(64) NOT NULL, - type NVARCHAR(32) NOT NULL, - display_name NVARCHAR(64) NOT NULL, - icon NVARCHAR(512), - config NVARCHAR(MAX), - auto_create TINYINT DEFAULT 1, - default_role NVARCHAR(32) DEFAULT 'user', - status TINYINT DEFAULT 1, - sort_order INT DEFAULT 0, - created_at DATETIME2 DEFAULT GETDATE(), - updated_at DATETIME2 DEFAULT GETDATE() -); -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, - user_id BIGINT NOT NULL, - provider_id BIGINT NOT NULL, - external_id NVARCHAR(256) NOT NULL, - access_token NVARCHAR(1024), - refresh_token NVARCHAR(1024), - expires_at DATETIME2, - last_login_at DATETIME2, - created_at DATETIME2 DEFAULT GETDATE() -); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_sso_user') - CREATE INDEX idx_sso_user ON fs_sso_session(user_id); -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, - biz_id NVARCHAR(128) DEFAULT '', - name NVARCHAR(256) NOT NULL, - parent_id BIGINT DEFAULT 0, - owner_id BIGINT NOT NULL, - path NVARCHAR(1024) NOT NULL, - status TINYINT DEFAULT 1, - version INT DEFAULT 1, - created_at DATETIME2 DEFAULT GETDATE(), - updated_at DATETIME2 DEFAULT GETDATE() -); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_folder_parent') - CREATE INDEX idx_folder_parent ON fs_folder(parent_id); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_folder_owner') - CREATE INDEX idx_folder_owner ON fs_folder(owner_id); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_folder_biz') - CREATE INDEX idx_folder_biz ON fs_folder(biz_id); -GO - -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_file') -CREATE TABLE fs_file ( - id BIGINT IDENTITY(1,1) PRIMARY KEY, - name NVARCHAR(256) NOT NULL, - extension NVARCHAR(32), - folder_id BIGINT NOT NULL, - owner_id BIGINT NOT NULL, - size BIGINT DEFAULT 0, - md5 NVARCHAR(32), - sha256 NVARCHAR(64), - storage_key NVARCHAR(512) NOT NULL, - storage_policy_id BIGINT DEFAULT 1, - mime_type NVARCHAR(128), - is_favorite TINYINT DEFAULT 0, - status TINYINT DEFAULT 1, - download_count INT DEFAULT 0, - version INT DEFAULT 1, - created_at DATETIME2 DEFAULT GETDATE(), - updated_at DATETIME2 DEFAULT GETDATE() -); -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_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, - file_id BIGINT NOT NULL, - user_id BIGINT DEFAULT 0, - role_id BIGINT DEFAULT 0, - perm_level NVARCHAR(16) DEFAULT 'read', - grant_by BIGINT NOT NULL, - created_at DATETIME2 DEFAULT GETDATE() -); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_fp_user_file') - CREATE INDEX idx_fp_user_file ON fs_file_permission(user_id, file_id); -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_share') -CREATE TABLE fs_share ( - id BIGINT IDENTITY(1,1) PRIMARY KEY, - file_id BIGINT NOT NULL, - owner_id BIGINT NOT NULL, - share_code NVARCHAR(32) UNIQUE NOT NULL, - password NVARCHAR(64), - expire_at DATETIME2, - max_download INT DEFAULT 0, - download_count INT DEFAULT 0, - allow_preview TINYINT DEFAULT 1, - allow_download TINYINT DEFAULT 1, - status TINYINT DEFAULT 1, - created_at DATETIME2 DEFAULT GETDATE() -); -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, - name NVARCHAR(64) NOT NULL, - type NVARCHAR(32) NOT NULL, - policy_type NVARCHAR(32) DEFAULT '', - config NVARCHAR(MAX), - status TINYINT DEFAULT 1, - created_at DATETIME2 DEFAULT GETDATE() -); -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, - policy_id BIGINT NOT NULL, - user_id BIGINT DEFAULT 0, - group_id BIGINT DEFAULT 0, - is_default TINYINT DEFAULT 0, - priority INT DEFAULT 0, - storage_quota BIGINT DEFAULT 0, - access_mode NVARCHAR(32), - status TINYINT DEFAULT 1, - created_at DATETIME2 DEFAULT GETDATE(), - updated_at DATETIME2 DEFAULT GETDATE() -); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_assign_policy') - CREATE INDEX idx_assign_policy ON fs_storage_assignment(policy_id); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_assign_user') - CREATE INDEX idx_assign_user ON fs_storage_assignment(user_id); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_assign_group') - CREATE INDEX idx_assign_group ON fs_storage_assignment(group_id); -GO - --- ========== 开放平台 ========== - -IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'fs_app') -CREATE TABLE fs_app ( - id BIGINT IDENTITY(1,1) PRIMARY KEY, - app_id NVARCHAR(64) UNIQUE NOT NULL, - app_secret NVARCHAR(128) NOT NULL, - app_name NVARCHAR(128) NOT NULL, - 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_app_user') -CREATE TABLE fs_app_user ( - id BIGINT IDENTITY(1,1) PRIMARY KEY, - app_id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - created_at DATETIME2 DEFAULT GETDATE() -); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_app_user_app') - CREATE INDEX idx_app_user_app ON fs_app_user(app_id); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_app_user_user') - CREATE INDEX idx_app_user_user ON fs_app_user(user_id); -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() -); -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() -); -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_created') - CREATE INDEX idx_log_created ON fs_operation_log(created_at); -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() -); -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() -); -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_backup_policy') - CREATE INDEX idx_backup_policy ON fs_backup_log(policy_id); -GO diff --git a/deploy/start.bat b/deploy/start.bat deleted file mode 100644 index 6d7f93a..0000000 --- a/deploy/start.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -cd %~dp0 -if not exist config.yaml copy config.yaml.example config.yaml -server.exe -pause diff --git a/deploy/start.sh b/deploy/start.sh deleted file mode 100644 index 15281ba..0000000 --- a/deploy/start.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -cd "$(dirname "$0")" -if [ ! -f config.yaml ]; then cp config.yaml.example config.yaml; fi -./server diff --git a/server/api/router.go b/server/api/router.go index acdb49d..47c0479 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -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) } // ============ 分享公开访问 (无需登录) ============ diff --git a/server/config.yaml b/server/config.yaml index 859eff4..26ae750 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -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 diff --git a/server/config/config.go b/server/config/config.go index 1b381d1..fdf05ae 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -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 备份工具配置 diff --git a/server/controller/admin_controller.go b/server/controller/admin_controller.go index cf4136f..2bbad6d 100644 --- a/server/controller/admin_controller.go +++ b/server/controller/admin_controller.go @@ -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 用户列表 diff --git a/server/controller/auth_controller.go b/server/controller/auth_controller.go index aa7a417..b74fe4d 100644 --- a/server/controller/auth_controller.go +++ b/server/controller/auth_controller.go @@ -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) +} diff --git a/server/controller/file_controller.go b/server/controller/file_controller.go index 10db022..09b30f4 100644 --- a/server/controller/file_controller.go +++ b/server/controller/file_controller.go @@ -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) } diff --git a/server/controller/openapi_controller.go b/server/controller/openapi_controller.go index 192cdfc..89eb828 100644 --- a/server/controller/openapi_controller.go +++ b/server/controller/openapi_controller.go @@ -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 } diff --git a/server/controller/permission_controller.go b/server/controller/permission_controller.go index fe510a1..9271627 100644 --- a/server/controller/permission_controller.go +++ b/server/controller/permission_controller.go @@ -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, }) diff --git a/server/controller/sso_controller.go b/server/controller/sso_controller.go index 0f5056e..4d38458 100644 --- a/server/controller/sso_controller.go +++ b/server/controller/sso_controller.go @@ -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, diff --git a/server/deploy.bat b/server/deploy.bat index 1abf868..780dba2 100644 --- a/server/deploy.bat +++ b/server/deploy.bat @@ -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 \ No newline at end of file +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 diff --git a/server/docs/nginx-production.conf b/server/docs/nginx-production.conf index 0e687fd..c98cb26 100644 --- a/server/docs/nginx-production.conf +++ b/server/docs/nginx-production.conf @@ -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"; + } } } diff --git a/server/dto/admin_dto.go b/server/dto/admin_dto.go index 68038da..cda5f22 100644 --- a/server/dto/admin_dto.go +++ b/server/dto/admin_dto.go @@ -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"` } // ========== 操作日志 ========== diff --git a/server/dto/file_dto.go b/server/dto/file_dto.go index 7ae0065..a48d281 100644 --- a/server/dto/file_dto.go +++ b/server/dto/file_dto.go @@ -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"` diff --git a/server/dto/permission_dto.go b/server/dto/permission_dto.go index 8d29b59..7a48800 100644 --- a/server/dto/permission_dto.go +++ b/server/dto/permission_dto.go @@ -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 用户权限视图对象 diff --git a/server/main.go b/server/main.go index c344a73..43fbd00 100644 --- a/server/main.go +++ b/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 +} diff --git a/server/middleware/auth.go b/server/middleware/auth.go index 2baf5cb..60731d3 100644 --- a/server/middleware/auth.go +++ b/server/middleware/auth.go @@ -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") diff --git a/server/model/app.go b/server/model/app.go index 2a114d5..ff800c2 100644 --- a/server/model/app.go +++ b/server/model/app.go @@ -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"` diff --git a/server/model/backup.go b/server/model/backup.go index 3051e2e..88d3d8e 100644 --- a/server/model/backup.go +++ b/server/model/backup.go @@ -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"` diff --git a/server/model/department.go b/server/model/department.go index ba0c98e..7f8f1ed 100644 --- a/server/model/department.go +++ b/server/model/department.go @@ -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=顶级 diff --git a/server/model/file.go b/server/model/file.go index a3b09c7..562d07c 100644 --- a/server/model/file.go +++ b/server/model/file.go @@ -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"` diff --git a/server/model/operation_log.go b/server/model/operation_log.go index 5ba2f42..179817e 100644 --- a/server/model/operation_log.go +++ b/server/model/operation_log.go @@ -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... diff --git a/server/model/role.go b/server/model/role.go index 283f68c..8f076db 100644 --- a/server/model/role.go +++ b/server/model/role.go @@ -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 diff --git a/server/model/share.go b/server/model/share.go index f557ae6..c5d61fb 100644 --- a/server/model/share.go +++ b/server/model/share.go @@ -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"` diff --git a/server/model/sso.go b/server/model/sso.go index e16faf9..7cc5224 100644 --- a/server/model/sso.go +++ b/server/model/sso.go @@ -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 diff --git a/server/model/storage_assignment.go b/server/model/storage_assignment.go index f3fc741..2a5a2f7 100644 --- a/server/model/storage_assignment.go +++ b/server/model/storage_assignment.go @@ -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 // 分配目标 diff --git a/server/model/storage_policy.go b/server/model/storage_policy.go index da167af..c2164e3 100644 --- a/server/model/storage_policy.go +++ b/server/model/storage_policy.go @@ -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) diff --git a/server/model/user.go b/server/model/user.go index fa20607..042788e 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -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"` diff --git a/server/myapp b/server/myapp index a034844..a318966 100644 Binary files a/server/myapp and b/server/myapp differ diff --git a/server/repository/file_repo.go b/server/repository/file_repo.go index a018ab5..ab4c657 100644 --- a/server/repository/file_repo.go +++ b/server/repository/file_repo.go @@ -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 diff --git a/server/service/admin_service.go b/server/service/admin_service.go index f39281a..3f17db4 100644 --- a/server/service/admin_service.go +++ b/server/service/admin_service.go @@ -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, diff --git a/server/service/auth_service.go b/server/service/auth_service.go index 30e6b90..6f45f48 100644 --- a/server/service/auth_service.go +++ b/server/service/auth_service.go @@ -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, diff --git a/server/service/backup_service.go b/server/service/backup_service.go index e0af095..98e6a80 100644 --- a/server/service/backup_service.go +++ b/server/service/backup_service.go @@ -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, diff --git a/server/service/file_service.go b/server/service/file_service.go index d22f880..53b4508 100644 --- a/server/service/file_service.go +++ b/server/service/file_service.go @@ -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, diff --git a/server/service/folder_service.go b/server/service/folder_service.go index ccd9824..319ee19 100644 --- a/server/service/folder_service.go +++ b/server/service/folder_service.go @@ -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), diff --git a/server/service/log_service.go b/server/service/log_service.go index 8491ca2..36152f8 100644 --- a/server/service/log_service.go +++ b/server/service/log_service.go @@ -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, diff --git a/server/service/openapi_service.go b/server/service/openapi_service.go index 8a4f4e2..a282350 100644 --- a/server/service/openapi_service.go +++ b/server/service/openapi_service.go @@ -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, diff --git a/server/service/share_service.go b/server/service/share_service.go index 0c83289..a386952 100644 --- a/server/service/share_service.go +++ b/server/service/share_service.go @@ -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) diff --git a/server/service/sso_service.go b/server/service/sso_service.go index c7ffc82..df2ea5a 100644 --- a/server/service/sso_service.go +++ b/server/service/sso_service.go @@ -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, diff --git a/server/service/storage_assign_service.go b/server/service/storage_assign_service.go index b32a82e..191badd 100644 --- a/server/service/storage_assign_service.go +++ b/server/service/storage_assign_service.go @@ -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, diff --git a/server/sql/mysql/init.sql b/server/sql/mysql/init.sql index 29305fa..2732c1f 100644 --- a/server/sql/mysql/init.sql +++ b/server/sql/mysql/init.sql @@ -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; diff --git a/server/sql/oracle/init.sql b/server/sql/oracle/init.sql deleted file mode 100644 index a7e68b6..0000000 --- a/server/sql/oracle/init.sql +++ /dev/null @@ -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; -/ diff --git a/server/sql/sqlserver/init.sql b/server/sql/sqlserver/init.sql index 3d153dc..8c722e8 100644 --- a/server/sql/sqlserver/init.sql +++ b/server/sql/sqlserver/init.sql @@ -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); diff --git a/server/storage/factory.go b/server/storage/factory.go index 9aa17b7..e6dc755 100644 --- a/server/storage/factory.go +++ b/server/storage/factory.go @@ -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 } diff --git a/server/storage/local.go b/server/storage/local.go index a23a7ec..e1b2848 100644 --- a/server/storage/local.go +++ b/server/storage/local.go @@ -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() diff --git a/server/utils/jwt.go b/server/utils/jwt.go index 0391d8b..c219af1 100644 --- a/server/utils/jwt.go +++ b/server/utils/jwt.go @@ -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 } diff --git a/web/build.bat b/web/build.bat index 01037d4..def581b 100644 --- a/web/build.bat +++ b/web/build.bat @@ -26,4 +26,3 @@ echo ============================================ echo packup success! echo ============================================ echo. - diff --git a/web/src/api/file.ts b/web/src/api/file.ts index 808a70d..988eea8 100644 --- a/web/src/api/file.ts +++ b/web/src/api/file.ts @@ -63,10 +63,12 @@ export interface ShareInfo { * @param file 文件对象 * @param folderId 目标文件夹ID, 0=根目录 */ -export function uploadFile(file: File, folderId: string = '0') { +export function uploadFile(file: File, folderId: string = '0', bizId?: string) { const formData = new FormData() formData.append('file', file) - return request.post(`/file/upload?folder_id=${folderId}`, formData, { + let url = `/file/upload?folder_id=${folderId}` + if (bizId) url += `&biz_id=${encodeURIComponent(bizId)}` + return request.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }) } diff --git a/web/src/api/request.ts b/web/src/api/request.ts index 2956415..b13e6e2 100644 --- a/web/src/api/request.ts +++ b/web/src/api/request.ts @@ -7,19 +7,23 @@ import { ElMessage } from 'element-plus' import router from '../router' import { getConfig } from '../config' -// 创建 axios 实例 -const cfg = getConfig() +// 创建 axios 实例(延迟读取 config, 确保 loadConfig 已完成) const request = axios.create({ - baseURL: cfg.apiTarget ? `${cfg.apiTarget}/api` : '/api', + baseURL: '/api', timeout: 30000, }) /** * 请求拦截器 - * 自动注入 JWT Token 到请求头 + * 1. 动态设置 baseURL(支持运行时配置) + * 2. 自动注入 JWT Token 到请求头 */ request.interceptors.request.use( (config) => { + const cfg = getConfig() + if (cfg.apiTarget) { + config.baseURL = `${cfg.apiTarget}/api` + } const token = localStorage.getItem('token') if (token) { config.headers.token = token @@ -50,15 +54,22 @@ request.interceptors.response.use( (error) => { if (error.response) { const { status, data } = error.response + const url = error.config?.url || '' switch (status) { case 401: { - // 只有之前有Token(已登录过)才提示"已过期" + // 登录接口的401直接显示错误信息(如密码错误) + if (url.includes('/auth/login') || url.includes('/auth/app-login')) { + ElMessage.error(data?.message || '用户名或密码错误') + return Promise.reject(error) + } + // 其他接口的401: token过期, 跳转登录页 const hadToken = !!localStorage.getItem('token') localStorage.removeItem('token') localStorage.removeItem('username') localStorage.removeItem('nickname') localStorage.removeItem('role') + localStorage.removeItem('roles') if (hadToken) { ElMessage.error('登录已过期, 请重新登录') } diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 7300091..2cf9099 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -4,6 +4,8 @@ * 后台: 管理员系统管理 */ import { createRouter, createWebHistory } from 'vue-router' +import { ElMessage } from 'element-plus' +import { isMobile } from '../utils/device' const router = createRouter({ history: createWebHistory(), @@ -12,31 +14,41 @@ const router = createRouter({ { path: '/login', name: 'Login', - component: () => import('../views/Login.vue'), + component: () => isMobile() + ? import('../views/MobileLogin.vue') + : import('../views/Login.vue'), meta: { requiresAuth: false }, }, { path: '/', name: 'FileManager', - component: () => import('../views/FileManager.vue'), + component: () => isMobile() + ? import('../views/MobileFileManager.vue') + : import('../views/FileManager.vue'), meta: { requiresAuth: true }, }, { path: '/s/:code', name: 'ShareView', - component: () => import('../views/ShareView.vue'), + component: () => isMobile() + ? import('../views/MobileShareView.vue') + : import('../views/ShareView.vue'), meta: { requiresAuth: false }, }, { path: '/trash', name: 'TrashView', - component: () => import('../views/TrashView.vue'), + component: () => isMobile() + ? import('../views/MobileTrash.vue') + : import('../views/TrashView.vue'), meta: { requiresAuth: true }, }, { path: '/shares', name: 'SharesView', - component: () => import('../views/SharesView.vue'), + component: () => isMobile() + ? import('../views/MobileShares.vue') + : import('../views/SharesView.vue'), meta: { requiresAuth: true }, }, @@ -59,6 +71,7 @@ const router = createRouter({ { path: 'system-log', name: 'AdminSystemLog', component: () => import('../views/admin/SystemLog.vue') }, { path: 'blacklist', name: 'AdminBlacklist', component: () => import('../views/admin/IPBlacklist.vue') }, { path: 'backup', name: 'AdminBackup', component: () => import('../views/admin/BackupManage.vue') }, + { path: 'license', name: 'AdminLicense', component: () => import('../views/admin/LicenseInfo.vue') }, ], }, ], @@ -66,11 +79,27 @@ const router = createRouter({ router.beforeEach((to, _from, next) => { const token = localStorage.getItem('token') + const role = localStorage.getItem('role') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.path === '/login' && token) { next('/') + } else if (to.meta.requiresAdmin) { + // 管理员判断: localStorage role 为 admin, 或者已加载的角色列表含 admin + const isAdmin = role === 'admin' || + (() => { + try { + const roles = JSON.parse(localStorage.getItem('roles') || '[]') + return roles.some((r: any) => r.name === 'admin') + } catch { return false } + })() + if (!isAdmin) { + ElMessage.error('需要管理员权限') + next('/') + } else { + next() + } } else { next() } diff --git a/web/src/stores/user.ts b/web/src/stores/user.ts index d832718..2a9a3ee 100644 --- a/web/src/stores/user.ts +++ b/web/src/stores/user.ts @@ -84,6 +84,8 @@ export const useUserStore = defineStore('user', () => { const data: UserPermissionVO = res.data roles.value = data.roles || [] permissions.value = data.permissions || [] + // 缓存角色到 localStorage, 供路由守卫快速判断 + localStorage.setItem('roles', JSON.stringify(roles.value)) } catch (error) { console.error('加载权限失败:', error) } @@ -104,6 +106,7 @@ export const useUserStore = defineStore('user', () => { localStorage.removeItem('username') localStorage.removeItem('nickname') localStorage.removeItem('role') + localStorage.removeItem('roles') router.push('/login') } diff --git a/web/src/views/FileManager.vue b/web/src/views/FileManager.vue index 01757e6..5bc3d82 100644 --- a/web/src/views/FileManager.vue +++ b/web/src/views/FileManager.vue @@ -195,12 +195,19 @@ - - -
拖拽文件到此处,或 点击选择
- -
+ + + + + + + +
拖拽文件到此处,或 点击选择
+ +
+
+