diff --git a/.gitignore b/.gitignore index e0e4af8..3289f81 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ picture/ ### Local Configuration Files ### **/application-local.yml + +### Documentation ### +doc/ diff --git a/doc/oss/setup-env.sh b/doc/oss/setup-env.sh deleted file mode 100755 index 958f63b..0000000 --- a/doc/oss/setup-env.sh +++ /dev/null @@ -1,240 +0,0 @@ -#!/bin/bash - -# OSS环境变量快速设置脚本 -# 用于设置阿里云OSS所需的环境变量 - -echo "🔧 OSS环境变量设置工具" -echo "==================================" - -# 检测操作系统 -OS_TYPE=$(uname -s) -SHELL_TYPE=$(basename "$SHELL") - -echo "检测到操作系统: $OS_TYPE" -echo "检测到Shell: $SHELL_TYPE" -echo "" - -# 获取当前配置文件中的值(仅用于显示) -CURRENT_KEY_ID="LTAI5t982gXi7A72gAa9yugE" -CURRENT_KEY_SECRET="Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" - -echo "📋 配置文件中的当前值:" -echo "OSS_ACCESS_KEY_ID: $CURRENT_KEY_ID" -echo "OSS_ACCESS_KEY_SECRET: ${CURRENT_KEY_SECRET:0:8}..." -echo "" - -# 选择设置方式 -echo "请选择设置方式:" -echo "1. 临时设置(当前终端会话有效)" -echo "2. 永久设置(添加到Shell配置文件)" -echo "3. 创建启动脚本" -echo "4. 创建.env文件" -echo "5. 显示手动设置命令" -echo "" - -read -p "请输入选项 (1-5): " choice - -case $choice in - 1) - echo "" - echo "🔧 临时设置环境变量..." - export OSS_ACCESS_KEY_ID="$CURRENT_KEY_ID" - export OSS_ACCESS_KEY_SECRET="$CURRENT_KEY_SECRET" - - echo "✅ 环境变量已设置(当前终端会话有效)" - echo "" - echo "验证设置:" - echo "OSS_ACCESS_KEY_ID: $OSS_ACCESS_KEY_ID" - echo "OSS_ACCESS_KEY_SECRET: ${OSS_ACCESS_KEY_SECRET:0:8}..." - echo "" - echo "⚠️ 注意:这些变量只在当前终端会话中有效" - echo " 关闭终端后需要重新设置" - ;; - - 2) - echo "" - echo "🔧 永久设置环境变量..." - - # 根据Shell类型选择配置文件 - if [[ "$SHELL_TYPE" == "zsh" ]]; then - CONFIG_FILE="$HOME/.zshrc" - elif [[ "$SHELL_TYPE" == "bash" ]]; then - CONFIG_FILE="$HOME/.bashrc" - else - CONFIG_FILE="$HOME/.profile" - fi - - echo "将添加到配置文件: $CONFIG_FILE" - - # 检查是否已经存在 - if grep -q "OSS_ACCESS_KEY_ID" "$CONFIG_FILE"; then - echo "⚠️ 配置文件中已存在OSS_ACCESS_KEY_ID,是否覆盖? (y/N)" - read -p "" overwrite - if [[ "$overwrite" != "y" && "$overwrite" != "Y" ]]; then - echo "取消设置" - exit 0 - fi - # 移除现有配置 - sed -i.bak '/OSS_ACCESS_KEY_ID/d' "$CONFIG_FILE" - sed -i.bak '/OSS_ACCESS_KEY_SECRET/d' "$CONFIG_FILE" - fi - - # 添加新配置 - echo "" >> "$CONFIG_FILE" - echo "# OSS环境变量 - 由setup-env.sh添加" >> "$CONFIG_FILE" - echo "export OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\"" >> "$CONFIG_FILE" - echo "export OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\"" >> "$CONFIG_FILE" - - echo "✅ 环境变量已添加到 $CONFIG_FILE" - echo "" - echo "请执行以下命令使配置生效:" - echo "source $CONFIG_FILE" - echo "" - echo "或者重新打开终端" - ;; - - 3) - echo "" - echo "🔧 创建启动脚本..." - - SCRIPT_FILE="start-app-with-oss.sh" - - cat > "$SCRIPT_FILE" << 'EOF' -#!/bin/bash - -# 应用程序启动脚本(包含OSS环境变量) -# 自动生成于 $(date) - -echo "🚀 启动应用程序(OSS模式)..." -echo "==================================" - -# 设置OSS环境变量 -export OSS_ACCESS_KEY_ID="LTAI5t982gXi7A72gAa9yugE" -export OSS_ACCESS_KEY_SECRET="Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" - -echo "✅ OSS环境变量已设置" -echo "OSS_ACCESS_KEY_ID: $OSS_ACCESS_KEY_ID" -echo "OSS_ACCESS_KEY_SECRET: ${OSS_ACCESS_KEY_SECRET:0:8}..." -echo "" - -# 切换到项目目录 -PROJECT_DIR="/Users/leocoder/leocoder/develop/templates/coder-common-thin/coder-common-thin-backend" -if [ -d "$PROJECT_DIR" ]; then - cd "$PROJECT_DIR" - echo "📁 切换到项目目录: $PROJECT_DIR" -else - echo "❌ 项目目录不存在: $PROJECT_DIR" - echo "请修改脚本中的PROJECT_DIR变量" - exit 1 -fi - -# 检查JAR文件是否存在 -JAR_FILE="coder-common-thin-web/target/coder-common-thin-web-1.0.0.jar" -if [ -f "$JAR_FILE" ]; then - echo "📦 找到JAR文件: $JAR_FILE" -else - echo "❌ JAR文件不存在: $JAR_FILE" - echo "请先编译项目: mvn clean package -DskipTests" - exit 1 -fi - -# 启动应用程序 -echo "" -echo "🚀 启动Spring Boot应用程序..." -echo "访问地址: http://localhost:18099" -echo "API文档: http://localhost:18099/swagger-ui.html" -echo "" -echo "按 Ctrl+C 停止应用程序" -echo "" - -java -jar "$JAR_FILE" -EOF - - chmod +x "$SCRIPT_FILE" - - echo "✅ 启动脚本已创建: $SCRIPT_FILE" - echo "" - echo "使用方法:" - echo "./$SCRIPT_FILE" - ;; - - 4) - echo "" - echo "🔧 创建.env文件..." - - ENV_FILE=".env" - - cat > "$ENV_FILE" << EOF -# OSS环境变量配置 -# 创建时间: $(date) -# 注意: 此文件包含敏感信息,不要提交到版本控制 - -OSS_ACCESS_KEY_ID=$CURRENT_KEY_ID -OSS_ACCESS_KEY_SECRET=$CURRENT_KEY_SECRET -EOF - - echo "✅ .env文件已创建: $ENV_FILE" - echo "" - echo "使用方法:" - echo "1. 使用dotenv工具: dotenv java -jar app.jar" - echo "2. 手动加载: source .env && java -jar app.jar" - echo "3. 在IDE中配置Environment Variables" - echo "" - echo "⚠️ 重要提醒:" - echo "• 将.env添加到.gitignore文件中" - echo "• 不要将.env文件提交到版本控制" - - # 检查并添加到.gitignore - if [ -f ".gitignore" ]; then - if ! grep -q "\.env" .gitignore; then - echo ".env" >> .gitignore - echo "✅ .env已添加到.gitignore" - fi - else - echo ".env" > .gitignore - echo "✅ 已创建.gitignore并添加.env" - fi - ;; - - 5) - echo "" - echo "📋 手动设置命令:" - echo "==================================" - echo "" - echo "🐧 Linux/macOS (Bash):" - echo "export OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\"" - echo "export OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\"" - echo "" - echo "🐧 Linux/macOS (Zsh):" - echo "export OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\"" - echo "export OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\"" - echo "" - echo "🪟 Windows (CMD):" - echo "set OSS_ACCESS_KEY_ID=$CURRENT_KEY_ID" - echo "set OSS_ACCESS_KEY_SECRET=$CURRENT_KEY_SECRET" - echo "" - echo "🪟 Windows (PowerShell):" - echo "\$env:OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\"" - echo "\$env:OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\"" - echo "" - echo "🐳 Docker:" - echo "docker run -e OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\" -e OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\" your-app" - echo "" - echo "☕ Java启动命令:" - echo "OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\" OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\" java -jar app.jar" - ;; - - *) - echo "❌ 无效选项,请选择1-5" - exit 1 - ;; -esac - -echo "" -echo "🧪 验证环境变量设置:" -echo "--------------------------------" -echo "echo \$OSS_ACCESS_KEY_ID" -echo "echo \$OSS_ACCESS_KEY_SECRET" -echo "" -echo "📖 详细文档: doc/oss/环境变量设置指南.md" -echo "🚀 测试脚本: doc/oss/修复后验证测试.sh" \ No newline at end of file diff --git a/doc/oss/环境变量设置指南.md b/doc/oss/环境变量设置指南.md deleted file mode 100644 index 2c85dea..0000000 --- a/doc/oss/环境变量设置指南.md +++ /dev/null @@ -1,271 +0,0 @@ -# 环境变量设置指南 - -## 📋 需要设置的环境变量 - -根据你的配置文件,需要设置以下环境变量: - -```bash -OSS_ACCESS_KEY_ID=your_access_key_id -OSS_ACCESS_KEY_SECRET=your_access_key_secret -``` - -## 🖥️ 不同操作系统的设置方法 - -### 1. macOS / Linux - -#### 方法1:临时设置(当前终端会话有效) -```bash -export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE -export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 -``` - -#### 方法2:永久设置(添加到配置文件) - -**对于 Bash 用户:** -```bash -# 编辑 ~/.bashrc 或 ~/.bash_profile -echo 'export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE' >> ~/.bashrc -echo 'export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30' >> ~/.bashrc - -# 重新加载配置 -source ~/.bashrc -``` - -**对于 Zsh 用户(macOS 默认):** -```bash -# 编辑 ~/.zshrc -echo 'export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE' >> ~/.zshrc -echo 'export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30' >> ~/.zshrc - -# 重新加载配置 -source ~/.zshrc -``` - -### 2. Windows - -#### 方法1:命令行临时设置 - -**CMD:** -```cmd -set OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE -set OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 -``` - -**PowerShell:** -```powershell -$env:OSS_ACCESS_KEY_ID="LTAI5t982gXi7A72gAa9yugE" -$env:OSS_ACCESS_KEY_SECRET="Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" -``` - -#### 方法2:系统环境变量设置 - -1. 右击"此电脑" → "属性" -2. 点击"高级系统设置" -3. 点击"环境变量" -4. 在"系统变量"中点击"新建" -5. 变量名:`OSS_ACCESS_KEY_ID`,变量值:`LTAI5t982gXi7A72gAa9yugE` -6. 重复步骤4-5,设置 `OSS_ACCESS_KEY_SECRET` - -## 🚀 启动应用程序的方法 - -### 1. 直接在终端启动 - -```bash -# 设置环境变量 -export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE -export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 - -# 启动应用程序 -cd /Users/leocoder/leocoder/develop/templates/coder-common-thin/coder-common-thin-backend -java -jar coder-common-thin-web/target/coder-common-thin-web-1.0.0.jar -``` - -### 2. 一行命令启动 - -```bash -OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 java -jar coder-common-thin-web/target/coder-common-thin-web-1.0.0.jar -``` - -### 3. 使用启动脚本 - -创建一个启动脚本: - -```bash -#!/bin/bash -# 文件名: start-app.sh - -# 设置环境变量 -export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE -export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 - -# 切换到项目目录 -cd /Users/leocoder/leocoder/develop/templates/coder-common-thin/coder-common-thin-backend - -# 启动应用程序 -java -jar coder-common-thin-web/target/coder-common-thin-web-1.0.0.jar -``` - -给脚本添加执行权限并运行: -```bash -chmod +x start-app.sh -./start-app.sh -``` - -## 🐳 Docker 环境 - -### 1. docker run 命令 -```bash -docker run -d \ - -e OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE \ - -e OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 \ - -p 18099:18099 \ - your-app-image -``` - -### 2. docker-compose.yml -```yaml -version: '3.8' -services: - app: - image: your-app-image - ports: - - "18099:18099" - environment: - - OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE - - OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 -``` - -### 3. 使用 .env 文件 -```bash -# 创建 .env 文件 -echo "OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE" > .env -echo "OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" >> .env - -# docker-compose 会自动读取 -docker-compose up -d -``` - -## 🔧 IDE 环境配置 - -### 1. IntelliJ IDEA - -1. 打开 Run Configuration -2. 选择你的 Spring Boot 应用 -3. 在 "Environment Variables" 中添加: - - `OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE` - - `OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30` - -### 2. VS Code - -在 `.vscode/launch.json` 中配置: -```json -{ - "type": "java", - "name": "Spring Boot App", - "request": "launch", - "mainClass": "org.leocoder.thin.web.CoderApplication", - "env": { - "OSS_ACCESS_KEY_ID": "LTAI5t982gXi7A72gAa9yugE", - "OSS_ACCESS_KEY_SECRET": "Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" - } -} -``` - -### 3. Eclipse - -1. 右击项目 → Run As → Run Configurations -2. 选择你的 Java Application -3. 在 "Environment" 选项卡中添加变量 - -## 🔐 安全最佳实践 - -### 1. 使用 .env 文件(推荐) - -创建 `.env` 文件(不要提交到版本控制): -```bash -# .env 文件 -OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE -OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 -``` - -在 `.gitignore` 中添加: -``` -.env -``` - -### 2. 使用系统密钥管理 - -**macOS Keychain:** -```bash -# 存储到 Keychain -security add-generic-password -s "oss-access-key" -a "your-app" -w "LTAI5t982gXi7A72gAa9yugE" - -# 从 Keychain 读取 -OSS_ACCESS_KEY_ID=$(security find-generic-password -s "oss-access-key" -a "your-app" -w) -``` - -**Linux Secret Service:** -```bash -# 使用 secret-tool -secret-tool store --label="OSS Access Key" service oss-access-key LTAI5t982gXi7A72gAa9yugE -``` - -### 3. 配置文件最佳实践 - -修改 `application-dev.yml`: -```yaml -coder: - oss: - # 只从环境变量读取,不设置默认值 - access-key-id: ${OSS_ACCESS_KEY_ID} - access-key-secret: ${OSS_ACCESS_KEY_SECRET} -``` - -## ✅ 验证环境变量 - -### 1. 检查环境变量是否设置成功 -```bash -# Linux/macOS -echo $OSS_ACCESS_KEY_ID -echo $OSS_ACCESS_KEY_SECRET - -# Windows CMD -echo %OSS_ACCESS_KEY_ID% -echo %OSS_ACCESS_KEY_SECRET% - -# Windows PowerShell -echo $env:OSS_ACCESS_KEY_ID -echo $env:OSS_ACCESS_KEY_SECRET -``` - -### 2. 在应用程序中验证 - -在 Java 代码中临时添加日志: -```java -@PostConstruct -public void logOssConfig() { - log.info("OSS_ACCESS_KEY_ID: {}", System.getenv("OSS_ACCESS_KEY_ID")); - log.info("OSS_ACCESS_KEY_SECRET: {}", - System.getenv("OSS_ACCESS_KEY_SECRET") != null ? "已设置" : "未设置"); -} -``` - -## 🚨 注意事项 - -1. **不要在版本控制中提交真实的密钥** -2. **定期轮换访问密钥** -3. **使用最小权限原则配置OSS权限** -4. **在生产环境中使用更安全的密钥管理方案** -5. **重启应用程序后环境变量才会生效** - -## 📝 快速设置脚本 - -我已经为你准备了一个快速设置脚本,运行即可: - -```bash -# 当前目录下创建 setup-env.sh -chmod +x setup-env.sh -./setup-env.sh -``` - -按照这个指南设置环境变量后,你的OSS功能就可以正常使用了! \ No newline at end of file diff --git a/doc/oss/阿里云OSS文件上传系统设计方案.md b/doc/oss/阿里云OSS文件上传系统设计方案.md deleted file mode 100644 index ca4ef85..0000000 --- a/doc/oss/阿里云OSS文件上传系统设计方案.md +++ /dev/null @@ -1,1855 +0,0 @@ -# 阿里云OSS文件上传系统设计方案 - -## 1. 项目概述 - -### 1.1 背景 -基于现有的本地文件上传系统,需要扩展支持阿里云OSS对象存储服务,实现文件的云端存储和管理。现有系统已经具备良好的架构设计,支持多种存储服务类型,需要在不破坏现有功能的基础上,集成阿里云OSS功能。 - -### 1.2 目标 -- 实现阿里云OSS文件上传功能 -- 保持与现有本地存储系统的兼容性 -- 实现文件删除时的OSS文件同步删除 -- 提供统一的文件管理接口 -- 支持图片和文档文件上传到OSS -- 确保系统的高可用性和安全性 - -### 1.3 技术栈 -- **后端框架**: Spring Boot 3.5.0 -- **ORM框架**: MyBatis Plus 3.5.12 -- **OSS SDK**: 阿里云Java SDK -- **认证框架**: Sa-Token 1.43.0 -- **数据库**: MySQL 9.3.0 -- **缓存**: Redis -- **工具库**: Hutool 5.8.38 - -## 2. 现有系统分析 - -### 2.1 前端文件上传实现 - -#### 2.1.1 核心组件 -- **文件管理页面**: `/src/views/system/file/index.vue` -- **图片管理页面**: `/src/views/system/picture/index.vue` -- **上传组件**: Naive UI的`NUpload`组件 - -#### 2.1.2 API接口 -```typescript -// 文件上传API -export function uploadFile(file: File, folderName: string, fileSize = 2, fileParam = '-1') { - const formData = new FormData() - formData.append('file', file) - return request.Post(`/coder/file/uploadFile/${fileSize}/${folderName}/${fileParam}`, formData) -} - -// 图片上传API -export function uploadPicture(file: File, pictureType = '9', fileSize = 2) { - const formData = new FormData() - formData.append('file', file) - return request.Post(`/coder/file/uploadFile/${fileSize}/pictures/${pictureType}`, formData) -} -``` - -#### 2.1.3 文件验证机制 -- **文件大小限制**: 2MB -- **支持格式**: 图片格式(jpg、png、gif等)和文档格式(doc、pdf、excel等) -- **前端验证**: 文件类型、大小验证 -- **上传进度**: 实时显示上传进度 - -### 2.2 后端文件上传实现 - -#### 2.2.1 核心Controller -- **FileController**: 文件上传核心控制器 -- **SysFileController**: 文件管理控制器 -- **SysPictureController**: 图片管理控制器 - -#### 2.2.2 数据模型 -```sql --- 文件表 -CREATE TABLE `sys_file` ( - `file_id` bigint NOT NULL AUTO_INCREMENT, - `file_name` text NOT NULL COMMENT '文件原始名称', - `new_name` text NOT NULL COMMENT '文件新名称', - `file_type` char(1) NOT NULL DEFAULT '1' COMMENT '文件类型[1-图片 2-文档 3-音频 4-视频 5-压缩包 6-应用程序 7-其他]', - `file_size` varchar(32) DEFAULT NULL COMMENT '文件大小', - `file_suffix` varchar(32) DEFAULT '' COMMENT '文件后缀', - `file_upload` text COMMENT '文件上传路径', - `file_path` text COMMENT '文件回显路径', - `file_service` char(1) NOT NULL DEFAULT '1' COMMENT '文件服务类型[1-LOCAL,2-MINIO,3-OSS]', - `create_time` datetime DEFAULT NULL, - `create_by` varchar(32) DEFAULT '', - `update_time` datetime DEFAULT NULL, - `update_by` varchar(32) DEFAULT '', - PRIMARY KEY (`file_id`) -); - --- 图片表 -CREATE TABLE `sys_picture` ( - `picture_id` bigint NOT NULL AUTO_INCREMENT, - `picture_name` text NOT NULL COMMENT '图片原始名称', - `new_name` text NOT NULL COMMENT '图片新名称', - `picture_type` char(1) NOT NULL DEFAULT '1' COMMENT '图片类型', - `picture_size` varchar(32) DEFAULT NULL COMMENT '图片大小', - `picture_suffix` varchar(32) DEFAULT '' COMMENT '图片后缀', - `picture_upload` text COMMENT '图片上传路径', - `picture_path` text COMMENT '图片回显路径', - `picture_service` char(1) NOT NULL DEFAULT '1' COMMENT '图片服务类型[1-LOCAL,2-MINIO,3-OSS]', - `create_time` datetime DEFAULT NULL, - `create_by` varchar(32) DEFAULT '', - `update_time` datetime DEFAULT NULL, - `update_by` varchar(32) DEFAULT '', - PRIMARY KEY (`picture_id`) -); -``` - -#### 2.2.3 文件删除机制 -- **SysFileController**: 删除时同时删除数据库记录和物理文件 -- **SysPictureController**: 仅删除数据库记录(存在问题) -- **FileUtil**: 提供文件删除工具方法 - -### 2.3 存在的问题 -1. **图片删除不一致**: SysPictureController删除时未删除物理文件 -2. **存储服务扩展**: 目前只实现了本地存储,OSS功能未实现 -3. **配置管理**: 缺少OSS相关配置管理 - -## 3. 阿里云OSS架构设计 - -### 3.1 整体架构图 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 前端应用层 │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ 文件管理页面 │ │ 图片管理页面 │ │ 上传组件 │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ 后端应用层 │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ FileController │ │ SysFileController│ │SysPictureController││ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ 业务服务层 │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ FileService │ │ StorageService │ │ OssService │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ 存储适配层 │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ LocalStorage │ │ MinioStorage │ │ OssStorage │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ 存储基础设施 │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ 本地磁盘 │ │ MinIO服务 │ │ 阿里云OSS │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 3.2 核心组件设计 - -#### 3.2.1 存储服务接口 -```java -public interface StorageService { - /** - * 上传文件 - * @param file 文件对象 - * @param fileName 文件名 - * @param folderPath 文件夹路径 - * @return 文件信息 - */ - FileUploadResult uploadFile(MultipartFile file, String fileName, String folderPath); - - /** - * 删除文件 - * @param filePath 文件路径 - * @return 删除结果 - */ - boolean deleteFile(String filePath); - - /** - * 获取文件访问URL - * @param filePath 文件路径 - * @return 访问URL - */ - String getFileUrl(String filePath); - - /** - * 检查文件是否存在 - * @param filePath 文件路径 - * @return 是否存在 - */ - boolean fileExists(String filePath); -} -``` - -#### 3.2.2 OSS存储实现 -```java -@Service -@ConditionalOnProperty(name = "coder.storage.type", havingValue = "oss") -public class OssStorageService implements StorageService { - - private final OSS ossClient; - private final OssConfig ossConfig; - - @Override - public FileUploadResult uploadFile(MultipartFile file, String fileName, String folderPath) { - // OSS文件上传实现 - } - - @Override - public boolean deleteFile(String filePath) { - // OSS文件删除实现 - } - - @Override - public String getFileUrl(String filePath) { - // 生成OSS访问URL - } - - @Override - public boolean fileExists(String filePath) { - // 检查OSS文件是否存在 - } -} -``` - -#### 3.2.3 存储工厂模式 -```java -@Component -public class StorageServiceFactory { - - private final Map storageServiceMap; - - public StorageService getStorageService(String storageType) { - return storageServiceMap.get(storageType); - } -} -``` - -### 3.3 插件化设计 - -#### 3.3.1 OSS插件模块 -``` -coder-common-thin-plugins/ -└── coder-common-thin-oss/ - ├── pom.xml - ├── src/main/java/org/leocoder/thin/oss/ - │ ├── annotation/ - │ │ └── EnableCoderOss.java - │ ├── config/ - │ │ ├── OssConfig.java - │ │ └── OssAutoConfiguration.java - │ ├── service/ - │ │ ├── OssService.java - │ │ └── OssServiceImpl.java - │ └── util/ - │ └── OssUtil.java - └── src/main/resources/ - └── META-INF/ - └── spring.factories -``` - -#### 3.3.2 启用注解 -```java -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Import(OssAutoConfiguration.class) -public @interface EnableCoderOss { - // OSS插件启用注解 -} -``` - -## 4. 技术实现方案 - -### 4.1 依赖管理 - -#### 4.1.1 Maven依赖 -```xml - - - org.leocoder.thin - coder-common-thin-oss - ${project.version} - - - - - com.aliyun.oss - aliyun-sdk-oss - 3.17.4 - -``` - -#### 4.1.2 主启动类配置 -```java -@EnableCoderOss -@SpringBootApplication -public class CoderCommonThinApplication { - public static void main(String[] args) { - SpringApplication.run(CoderCommonThinApplication.class, args); - } -} -``` - -### 4.2 配置管理 - -#### 4.2.1 OSS配置类 -```java -@Data -@Component -@ConfigurationProperties(prefix = "coder.oss") -public class OssConfig { - - /** - * OSS服务端点 - */ - private String endpoint; - - /** - * Access Key ID - */ - private String accessKeyId; - - /** - * Access Key Secret - */ - private String accessKeySecret; - - /** - * 存储桶名称 - */ - private String bucketName; - - /** - * 域名 - */ - private String domain; - - /** - * 路径前缀 - */ - private String pathPrefix; - - /** - * 是否启用HTTPS - */ - private Boolean https = true; - - /** - * 连接超时时间 - */ - private Long connectTimeout = 10000L; - - /** - * 读取超时时间 - */ - private Long readTimeout = 10000L; -} -``` - -#### 4.2.2 应用配置 -```yaml -# application-dev.yml -coder: - # 存储服务类型:local、minio、oss - storage: - type: oss - - # OSS配置 - oss: - endpoint: oss-cn-hangzhou.aliyuncs.com - access-key-id: ${OSS_ACCESS_KEY_ID:} - access-key-secret: ${OSS_ACCESS_KEY_SECRET:} - bucket-name: coder-file-storage - domain: https://coder-file-storage.oss-cn-hangzhou.aliyuncs.com - path-prefix: coder-files - https: true - connect-timeout: 10000 - read-timeout: 10000 -``` - -### 4.3 核心服务实现 - -#### 4.3.1 OSS客户端配置 -```java -@Configuration -@EnableConfigurationProperties(OssConfig.class) -@ConditionalOnProperty(name = "coder.storage.type", havingValue = "oss") -public class OssAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public OSS ossClient(OssConfig ossConfig) { - return new OSSClientBuilder().build( - ossConfig.getEndpoint(), - ossConfig.getAccessKeyId(), - ossConfig.getAccessKeySecret() - ); - } - - @Bean - @ConditionalOnMissingBean - public OssService ossService(OSS ossClient, OssConfig ossConfig) { - return new OssServiceImpl(ossClient, ossConfig); - } -} -``` - -#### 4.3.2 OSS服务实现 -```java -@Service -@Slf4j -@RequiredArgsConstructor -public class OssServiceImpl implements OssService { - - private final OSS ossClient; - private final OssConfig ossConfig; - - @Override - public FileUploadResult uploadFile(MultipartFile file, String fileName, String folderPath) { - try { - // 构建对象键 - String objectKey = buildObjectKey(folderPath, fileName); - - // 上传文件 - PutObjectRequest putObjectRequest = new PutObjectRequest( - ossConfig.getBucketName(), - objectKey, - file.getInputStream() - ); - - // 设置元数据 - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(file.getSize()); - metadata.setContentType(file.getContentType()); - metadata.setContentDisposition("inline"); - putObjectRequest.setMetadata(metadata); - - // 执行上传 - PutObjectResult result = ossClient.putObject(putObjectRequest); - - // 构建返回结果 - return FileUploadResult.builder() - .fileName(file.getOriginalFilename()) - .newName(fileName) - .fileSize(FileTypeUtil.formatFileSize(file.getSize())) - .suffixName(FileTypeUtil.getFileExtension(file.getOriginalFilename())) - .filePath(objectKey) - .fileUploadPath(getFileUrl(objectKey)) - .build(); - - } catch (Exception e) { - log.error("OSS文件上传失败", e); - throw new BusinessException("文件上传失败"); - } - } - - @Override - public boolean deleteFile(String objectKey) { - try { - ossClient.deleteObject(ossConfig.getBucketName(), objectKey); - return true; - } catch (Exception e) { - log.error("OSS文件删除失败: {}", objectKey, e); - return false; - } - } - - @Override - public String getFileUrl(String objectKey) { - if (StringUtils.isBlank(objectKey)) { - return ""; - } - - if (StringUtils.isNotBlank(ossConfig.getDomain())) { - return ossConfig.getDomain() + "/" + objectKey; - } - - String protocol = ossConfig.getHttps() ? "https" : "http"; - return protocol + "://" + ossConfig.getBucketName() + "." + ossConfig.getEndpoint() + "/" + objectKey; - } - - @Override - public boolean fileExists(String objectKey) { - try { - return ossClient.doesObjectExist(ossConfig.getBucketName(), objectKey); - } catch (Exception e) { - log.error("检查OSS文件是否存在失败: {}", objectKey, e); - return false; - } - } - - private String buildObjectKey(String folderPath, String fileName) { - StringBuilder keyBuilder = new StringBuilder(); - - if (StringUtils.isNotBlank(ossConfig.getPathPrefix())) { - keyBuilder.append(ossConfig.getPathPrefix()).append("/"); - } - - if (StringUtils.isNotBlank(folderPath)) { - keyBuilder.append(folderPath); - if (!folderPath.endsWith("/")) { - keyBuilder.append("/"); - } - } - - keyBuilder.append(fileName); - - return keyBuilder.toString(); - } -} -``` - -### 4.4 文件上传控制器改造 - -#### 4.4.1 存储服务工厂 -```java -@Component -@RequiredArgsConstructor -public class StorageServiceFactory { - - private final ApplicationContext applicationContext; - - @Value("${coder.storage.type:local}") - private String defaultStorageType; - - public StorageService getStorageService() { - return getStorageService(defaultStorageType); - } - - public StorageService getStorageService(String storageType) { - switch (storageType.toLowerCase()) { - case "local": - return applicationContext.getBean(LocalStorageService.class); - case "minio": - return applicationContext.getBean(MinioStorageService.class); - case "oss": - return applicationContext.getBean(OssStorageService.class); - default: - throw new BusinessException("不支持的存储类型: " + storageType); - } - } -} -``` - -#### 4.4.2 文件上传控制器改造 -```java -@RestController -@RequestMapping("/coder") -@RequiredArgsConstructor -@Slf4j -public class FileController { - - private final StorageServiceFactory storageServiceFactory; - private final SysFileService sysFileService; - private final SysPictureService sysPictureService; - - @PostMapping("/file/uploadFile/{fileSize}/{folderName}/{fileParam}") - public Map uploadSingleFile( - @RequestParam("file") MultipartFile file, - @PathVariable("fileSize") Integer fileSize, - @PathVariable("folderName") String folderName, - @PathVariable("fileParam") String fileParam) { - - // 文件预检查 - validateUploadFile(file, fileSize, folderName); - - // 获取存储服务 - StorageService storageService = storageServiceFactory.getStorageService(); - - // 生成文件名 - String fileName = generateFileName(file.getOriginalFilename()); - - // 构建文件夹路径 - String folderPath = buildFolderPath(folderName, CoderLoginUtil.getLoginName()); - - // 上传文件 - FileUploadResult uploadResult = storageService.uploadFile(file, fileName, folderPath); - - // 转换为Map格式(保持兼容性) - Map fileMap = convertToMap(uploadResult); - - // 保存文件信息到数据库 - saveUploadFilesInformation(fileMap, true); - - // 如果是图片,同时保存到图库表 - if (CoderConstants.PICTURES.equals(folderName)) { - saveUploadPicturesInformation(fileMap, fileParam, true); - } - - return fileMap; - } - - private String generateFileName(String originalFilename) { - String extension = FileTypeUtil.getFileExtension(originalFilename); - String timeStamp = DateUtil.format(new Date(), "yyyyMMddHHmmss"); - String uuid = IdUtil.fastSimpleUUID().substring(0, 6); - return timeStamp + "-" + uuid + "." + extension; - } - - private String buildFolderPath(String folderName, String username) { - LocalDateTime now = LocalDateTime.now(); - return String.format("%s/%s/%d/%02d/%02d", - folderName, username, now.getYear(), now.getMonthValue(), now.getDayOfMonth()); - } -} -``` - -### 4.5 文件删除机制改造 - -#### 4.5.1 文件删除服务 -```java -@Service -@RequiredArgsConstructor -@Slf4j -public class FileDeleteService { - - private final StorageServiceFactory storageServiceFactory; - - /** - * 删除文件(支持多种存储类型) - */ - public boolean deleteFile(String filePath, String fileService) { - try { - // 根据文件服务类型选择删除策略 - switch (fileService) { - case "1": // LOCAL - return FileUtil.deleteFile(filePath); - case "2": // MINIO - StorageService minioService = storageServiceFactory.getStorageService("minio"); - return minioService.deleteFile(filePath); - case "3": // OSS - StorageService ossService = storageServiceFactory.getStorageService("oss"); - return ossService.deleteFile(filePath); - default: - log.warn("未知的文件服务类型: {}", fileService); - return false; - } - } catch (Exception e) { - log.error("删除文件失败: filePath={}, fileService={}", filePath, fileService, e); - return false; - } - } -} -``` - -#### 4.5.2 文件管理控制器改造 -```java -@RestController -@RequestMapping("/coder") -@RequiredArgsConstructor -public class SysFileController { - - private final SysFileService sysFileService; - private final FileDeleteService fileDeleteService; - - @PostMapping("/sysFile/deleteById/{id}") - @SaCheckPermission("system:file:delete") - @OperLog(value = "删除文件资源", operType = OperType.DELETE) - public void delete(@PathVariable("id") Long id) { - SysFile sysFile = sysFileService.getById(id); - if (sysFile != null) { - // 删除物理文件 - if (StringUtils.isNotBlank(sysFile.getFileUpload())) { - fileDeleteService.deleteFile(sysFile.getFileUpload(), sysFile.getFileService()); - } - - // 删除数据库记录 - YUtil.isTrue(!sysFileService.removeById(id), "删除失败,请稍后重试"); - } - } - - @PostMapping("/sysFile/batchDelete") - @SaCheckPermission("system:file:delete") - @Transactional(rollbackFor = Exception.class) - @OperLog(value = "批量删除文件资源", operType = OperType.DELETE) - public void batchDelete(@RequestBody List ids) { - List sysFileList = sysFileService.listByIds(ids); - - // 批量删除物理文件 - for (SysFile sysFile : sysFileList) { - if (StringUtils.isNotBlank(sysFile.getFileUpload())) { - fileDeleteService.deleteFile(sysFile.getFileUpload(), sysFile.getFileService()); - } - } - - // 批量删除数据库记录 - YUtil.isTrue(!sysFileService.removeBatchByIds(ids), "删除失败,请稍后重试"); - } -} -``` - -#### 4.5.3 图片管理控制器改造 -```java -@RestController -@RequestMapping("/coder") -@RequiredArgsConstructor -public class SysPictureController { - - private final SysPictureService sysPictureService; - private final FileDeleteService fileDeleteService; - - @PostMapping("/sysPicture/deleteById/{id}") - @SaCheckPermission("system:sysPicture:delete") - @OperLog(value = "删除图库", operType = OperType.DELETE) - public void delete(@PathVariable("id") Long id) { - SysPicture sysPicture = sysPictureService.getById(id); - if (sysPicture != null) { - // 删除物理文件 - if (StringUtils.isNotBlank(sysPicture.getPictureUpload())) { - fileDeleteService.deleteFile(sysPicture.getPictureUpload(), sysPicture.getPictureService()); - } - - // 删除数据库记录 - YUtil.isTrue(!sysPictureService.removeById(id), "删除失败,请稍后重试"); - } - } - - @PostMapping("/sysPicture/batchDelete") - @SaCheckPermission("system:sysPicture:delete") - @Transactional(rollbackFor = Exception.class) - @OperLog(value = "批量删除图库", operType = OperType.DELETE) - public void batchDelete(@RequestBody List ids) { - List sysPictureList = sysPictureService.listByIds(ids); - - // 批量删除物理文件 - for (SysPicture sysPicture : sysPictureList) { - if (StringUtils.isNotBlank(sysPicture.getPictureUpload())) { - fileDeleteService.deleteFile(sysPicture.getPictureUpload(), sysPicture.getPictureService()); - } - } - - // 批量删除数据库记录 - YUtil.isTrue(!sysPictureService.removeBatchByIds(ids), "删除失败,请稍后重试"); - } -} -``` - -## 5. 数据库设计 - -### 5.1 现有表结构分析 -现有的`sys_file`和`sys_picture`表已经包含了`file_service`和`picture_service`字段,用于标识存储服务类型: -- `1` - LOCAL(本地存储) -- `2` - MINIO(MinIO存储) -- `3` - OSS(阿里云OSS) - -### 5.2 字段含义更新 - -#### 5.2.1 OSS存储字段说明 -对于OSS存储,字段含义如下: - -- **file_upload/picture_upload**: 存储OSS对象键(Object Key) - - 格式:`coder-files/files/username/2025/07/08/20250708183651-44b5b3.png` - - 用于OSS文件操作(上传、删除、检查存在) - -- **file_path/picture_path**: 存储完整的访问URL - - 格式:`https://coder-file-storage.oss-cn-hangzhou.aliyuncs.com/coder-files/files/username/2025/07/08/20250708183651-44b5b3.png` - - 用于前端显示和文件下载 - -- **file_service/picture_service**: 存储服务类型标识 - - `3` - 表示使用阿里云OSS存储 - -### 5.3 数据迁移方案 - -#### 5.3.1 存储服务升级 -对于现有的本地存储文件,可以提供迁移工具: - -```java -@Service -@RequiredArgsConstructor -@Slf4j -public class StorageMigrationService { - - private final SysFileService sysFileService; - private final SysPictureService sysPictureService; - private final StorageServiceFactory storageServiceFactory; - - /** - * 从本地存储迁移到OSS - */ - public void migrateFromLocalToOss() { - // 迁移文件 - migrateFiles(); - - // 迁移图片 - migratePictures(); - } - - private void migrateFiles() { - // 查询本地存储的文件 - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(SysFile::getFileService, "1"); - List localFiles = sysFileService.list(wrapper); - - StorageService ossService = storageServiceFactory.getStorageService("oss"); - - for (SysFile sysFile : localFiles) { - try { - // 读取本地文件 - File localFile = new File(sysFile.getFileUpload()); - if (!localFile.exists()) { - log.warn("本地文件不存在: {}", sysFile.getFileUpload()); - continue; - } - - // 上传到OSS - MockMultipartFile multipartFile = new MockMultipartFile( - "file", - sysFile.getFileName(), - Files.probeContentType(localFile.toPath()), - Files.readAllBytes(localFile.toPath()) - ); - - String folderPath = extractFolderPath(sysFile.getFileUpload()); - FileUploadResult uploadResult = ossService.uploadFile(multipartFile, sysFile.getNewName(), folderPath); - - // 更新数据库记录 - sysFile.setFileUpload(uploadResult.getFilePath()); - sysFile.setFilePath(uploadResult.getFileUploadPath()); - sysFile.setFileService("3"); - sysFileService.updateById(sysFile); - - log.info("文件迁移成功: {} -> {}", localFile.getPath(), uploadResult.getFilePath()); - - } catch (Exception e) { - log.error("文件迁移失败: {}", sysFile.getFileUpload(), e); - } - } - } - - private void migratePictures() { - // 类似实现图片迁移 - } -} -``` - -## 6. 安全机制 - -### 6.1 访问控制 - -#### 6.1.1 OSS访问权限 -```java -@Configuration -public class OssSecurityConfig { - - /** - * OSS访问策略配置 - */ - @Bean - public OssAccessPolicy ossAccessPolicy() { - return OssAccessPolicy.builder() - .allowedOrigins("https://yourdomian.com") - .allowedMethods("GET", "POST", "PUT", "DELETE") - .allowedHeaders("*") - .maxAge(3600) - .build(); - } -} -``` - -#### 6.1.2 文件上传权限验证 -```java -@Component -@RequiredArgsConstructor -public class FileUploadSecurityChecker { - - /** - * 检查文件上传权限 - */ - public void checkUploadPermission(String folderName, String username) { - // 检查用户是否有上传权限 - if (!CoderLoginUtil.hasPermission("system:file:upload")) { - throw new BusinessException("没有文件上传权限"); - } - - // 检查文件夹访问权限 - if (!isAllowedFolder(folderName, username)) { - throw new BusinessException("无权限访问该文件夹"); - } - } - - private boolean isAllowedFolder(String folderName, String username) { - // 实现文件夹权限检查逻辑 - return true; - } -} -``` - -### 6.2 文件安全 - -#### 6.2.1 文件类型检查 -```java -@Component -public class FileSecurityChecker { - - // 危险文件类型黑名单 - private static final List DANGEROUS_EXTENSIONS = Arrays.asList( - "exe", "bat", "cmd", "com", "pif", "scr", "vbs", "js", "jar", "class" - ); - - /** - * 检查文件安全性 - */ - public void checkFileSecurity(MultipartFile file) { - String filename = file.getOriginalFilename(); - if (StringUtils.isBlank(filename)) { - throw new BusinessException("文件名不能为空"); - } - - String extension = FileTypeUtil.getFileExtension(filename).toLowerCase(); - - // 检查危险文件类型 - if (DANGEROUS_EXTENSIONS.contains(extension)) { - throw new BusinessException("不允许上传该类型的文件"); - } - - // 检查文件头信息 - checkFileHeader(file); - } - - private void checkFileHeader(MultipartFile file) { - // 实现文件头检查逻辑 - } -} -``` - -#### 6.2.2 文件大小和数量限制 -```java -@Component -@ConfigurationProperties(prefix = "coder.file.limit") -@Data -public class FileUploadLimitConfig { - - /** - * 单个文件大小限制(MB) - */ - private Integer maxFileSize = 10; - - /** - * 用户总存储空间限制(MB) - */ - private Integer maxUserStorage = 1000; - - /** - * 用户每日上传数量限制 - */ - private Integer maxDailyUploads = 100; - - /** - * 用户每日上传大小限制(MB) - */ - private Integer maxDailySize = 500; -} -``` - -### 6.3 配置安全 - -#### 6.3.1 敏感信息加密 -```yaml -# application-dev.yml -coder: - oss: - # 使用环境变量或配置中心管理敏感信息 - access-key-id: ${OSS_ACCESS_KEY_ID} - access-key-secret: ${OSS_ACCESS_KEY_SECRET} -``` - -#### 6.3.2 OSS Bucket安全配置 -```java -@Configuration -@RequiredArgsConstructor -public class OssBucketSecurityConfig { - - private final OSS ossClient; - private final OssConfig ossConfig; - - @PostConstruct - public void configureBucketSecurity() { - try { - // 设置Bucket访问权限 - ossClient.setBucketAcl(ossConfig.getBucketName(), CannedAccessControlList.Private); - - // 配置跨域规则 - SetBucketCORSRequest request = new SetBucketCORSRequest(ossConfig.getBucketName()); - CORSRule corsRule = new CORSRule(); - corsRule.setAllowedOrigins(Arrays.asList("https://yourdomian.com")); - corsRule.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); - corsRule.setAllowedHeaders(Arrays.asList("*")); - corsRule.setMaxAge(3600); - request.setCorsRules(Arrays.asList(corsRule)); - - ossClient.setBucketCORS(request); - - } catch (Exception e) { - log.error("OSS Bucket安全配置失败", e); - } - } -} -``` - -## 7. 性能优化 - -### 7.1 连接池配置 - -#### 7.1.1 OSS连接池 -```java -@Configuration -@EnableConfigurationProperties(OssConfig.class) -public class OssClientConfig { - - @Bean - public OSS ossClient(OssConfig ossConfig) { - // 创建ClientConfiguration - ClientConfiguration clientConfiguration = new ClientConfiguration(); - - // 设置连接超时时间 - clientConfiguration.setConnectionTimeout(ossConfig.getConnectTimeout().intValue()); - - // 设置读取超时时间 - clientConfiguration.setSocketTimeout(ossConfig.getReadTimeout().intValue()); - - // 设置连接池大小 - clientConfiguration.setMaxConnections(200); - - // 设置请求超时时间 - clientConfiguration.setRequestTimeout(30000); - - // 设置失败重试次数 - clientConfiguration.setMaxErrorRetry(3); - - return new OSSClientBuilder().build( - ossConfig.getEndpoint(), - ossConfig.getAccessKeyId(), - ossConfig.getAccessKeySecret(), - clientConfiguration - ); - } -} -``` - -### 7.2 缓存策略 - -#### 7.2.1 文件信息缓存 -```java -@Service -@RequiredArgsConstructor -public class FileInfoCacheService { - - private final RedisTemplate redisTemplate; - - private static final String FILE_INFO_KEY = "file:info:"; - private static final Duration CACHE_TIMEOUT = Duration.ofHours(1); - - /** - * 缓存文件信息 - */ - public void cacheFileInfo(String fileId, SysFile fileInfo) { - redisTemplate.opsForValue().set(FILE_INFO_KEY + fileId, fileInfo, CACHE_TIMEOUT); - } - - /** - * 获取缓存的文件信息 - */ - public SysFile getCachedFileInfo(String fileId) { - return (SysFile) redisTemplate.opsForValue().get(FILE_INFO_KEY + fileId); - } - - /** - * 删除文件信息缓存 - */ - public void removeFileInfoCache(String fileId) { - redisTemplate.delete(FILE_INFO_KEY + fileId); - } -} -``` - -### 7.3 异步处理 - -#### 7.3.1 异步文件上传 -```java -@Service -@RequiredArgsConstructor -@Slf4j -public class AsyncFileUploadService { - - private final StorageServiceFactory storageServiceFactory; - private final SysFileService sysFileService; - - @Async("fileUploadExecutor") - public CompletableFuture uploadFileAsync( - MultipartFile file, String fileName, String folderPath) { - - try { - StorageService storageService = storageServiceFactory.getStorageService(); - FileUploadResult result = storageService.uploadFile(file, fileName, folderPath); - - log.info("异步文件上传成功: {}", result.getFilePath()); - return CompletableFuture.completedFuture(result); - - } catch (Exception e) { - log.error("异步文件上传失败", e); - return CompletableFuture.failedFuture(e); - } - } -} - -@Configuration -@EnableAsync -public class AsyncConfig { - - @Bean("fileUploadExecutor") - public ThreadPoolTaskExecutor fileUploadExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(10); - executor.setMaxPoolSize(50); - executor.setQueueCapacity(200); - executor.setThreadNamePrefix("FileUpload-"); - executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); - executor.initialize(); - return executor; - } -} -``` - -## 8. 监控和日志 - -### 8.1 操作日志 - -#### 8.1.1 文件操作日志 -```java -@Component -@RequiredArgsConstructor -@Slf4j -public class FileOperationLogger { - - private final SysOperLogService operLogService; - - /** - * 记录文件上传日志 - */ - public void logFileUpload(String fileName, String fileSize, String storageType, String username) { - SysOperLog operLog = new SysOperLog(); - operLog.setOperName(username); - operLog.setOperType(OperType.UPLOAD.name()); - operLog.setBusinessType("文件上传"); - operLog.setMethod("uploadFile"); - operLog.setOperDesc("上传文件: " + fileName + ", 大小: " + fileSize + ", 存储类型: " + storageType); - operLog.setOperTime(new Date()); - - operLogService.save(operLog); - } - - /** - * 记录文件删除日志 - */ - public void logFileDelete(String fileName, String storageType, String username) { - SysOperLog operLog = new SysOperLog(); - operLog.setOperName(username); - operLog.setOperType(OperType.DELETE.name()); - operLog.setBusinessType("文件删除"); - operLog.setMethod("deleteFile"); - operLog.setOperDesc("删除文件: " + fileName + ", 存储类型: " + storageType); - operLog.setOperTime(new Date()); - - operLogService.save(operLog); - } -} -``` - -### 8.2 性能监控 - -#### 8.2.1 文件上传性能监控 -```java -@Component -@RequiredArgsConstructor -public class FileUploadMonitor { - - private final MeterRegistry meterRegistry; - - /** - * 记录文件上传指标 - */ - public void recordUploadMetrics(String storageType, long fileSize, long uploadTime) { - // 记录上传次数 - Counter.builder("file.upload.count") - .tag("storage.type", storageType) - .register(meterRegistry) - .increment(); - - // 记录上传大小 - Counter.builder("file.upload.size") - .tag("storage.type", storageType) - .register(meterRegistry) - .increment(fileSize); - - // 记录上传时间 - Timer.builder("file.upload.duration") - .tag("storage.type", storageType) - .register(meterRegistry) - .record(uploadTime, TimeUnit.MILLISECONDS); - } -} -``` - -### 8.3 告警机制 - -#### 8.3.1 文件上传异常告警 -```java -@Component -@RequiredArgsConstructor -@Slf4j -public class FileUploadAlertService { - - private final AlarmService alarmService; - - /** - * 文件上传失败告警 - */ - public void alertUploadFailure(String fileName, String errorMessage, String username) { - AlarmMessage alarm = AlarmMessage.builder() - .title("文件上传失败告警") - .content(String.format("用户%s上传文件%s失败,错误信息:%s", username, fileName, errorMessage)) - .level(AlarmLevel.ERROR) - .timestamp(new Date()) - .build(); - - alarmService.sendAlarm(alarm); - } - - /** - * 存储空间不足告警 - */ - public void alertStorageSpaceInsufficient(String storageType, long remainingSpace) { - AlarmMessage alarm = AlarmMessage.builder() - .title("存储空间不足告警") - .content(String.format("%s存储空间不足,剩余空间:%s", storageType, FileTypeUtil.formatFileSize(remainingSpace))) - .level(AlarmLevel.WARNING) - .timestamp(new Date()) - .build(); - - alarmService.sendAlarm(alarm); - } -} -``` - -## 9. 部署和运维 - -### 9.1 环境配置 - -#### 9.1.1 开发环境配置 -```yaml -# application-dev.yml -coder: - storage: - type: oss - oss: - endpoint: oss-cn-hangzhou.aliyuncs.com - access-key-id: ${OSS_ACCESS_KEY_ID} - access-key-secret: ${OSS_ACCESS_KEY_SECRET} - bucket-name: coder-file-storage-dev - domain: https://coder-file-storage-dev.oss-cn-hangzhou.aliyuncs.com - path-prefix: coder-files - https: true - connect-timeout: 10000 - read-timeout: 10000 - -# 日志配置 -logging: - level: - org.leocoder.thin.oss: DEBUG - com.aliyun.oss: INFO -``` - -#### 9.1.2 生产环境配置 -```yaml -# application-prod.yml -coder: - storage: - type: oss - oss: - endpoint: oss-cn-hangzhou.aliyuncs.com - access-key-id: ${OSS_ACCESS_KEY_ID} - access-key-secret: ${OSS_ACCESS_KEY_SECRET} - bucket-name: coder-file-storage-prod - domain: https://files.yourcompany.com - path-prefix: coder-files - https: true - connect-timeout: 10000 - read-timeout: 10000 - -# 日志配置 -logging: - level: - org.leocoder.thin.oss: INFO - com.aliyun.oss: WARN -``` - -### 9.2 Docker部署 - -#### 9.2.1 Dockerfile - -![田园犬](https://gaoziman.oss-cn-hangzhou.aliyuncs.com/uPic/2025-07-09-%E7%94%B0%E5%9B%AD%E7%8A%AC.svg) - -```dockerfile -FROM openjdk:17-jdk-slim - -MAINTAINER Leocoder - -VOLUME /tmp - -ADD coder-common-thin-web.jar app.jar - -EXPOSE 18099 - -ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"] -``` - -#### 9.2.2 docker-compose.yml -```yaml -version: '3.8' - -services: - coder-app: - build: . - ports: - - "18099:18099" - environment: - - SPRING_PROFILES_ACTIVE=prod - - OSS_ACCESS_KEY_ID=${OSS_ACCESS_KEY_ID} - - OSS_ACCESS_KEY_SECRET=${OSS_ACCESS_KEY_SECRET} - - MYSQL_HOST=mysql - - REDIS_HOST=redis - depends_on: - - mysql - - redis - volumes: - - ./logs:/app/logs - networks: - - coder-network - - mysql: - image: mysql:8.0 - environment: - - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - - MYSQL_DATABASE=coder_common_thin - ports: - - "3306:3306" - volumes: - - mysql_data:/var/lib/mysql - networks: - - coder-network - - redis: - image: redis:7.0 - ports: - - "6379:6379" - volumes: - - redis_data:/data - networks: - - coder-network - -volumes: - mysql_data: - redis_data: - -networks: - coder-network: - driver: bridge -``` - -### 9.3 健康检查 - -#### 9.3.1 OSS连接健康检查 -```java -@Component -@RequiredArgsConstructor -public class OssHealthIndicator implements HealthIndicator { - - private final OSS ossClient; - private final OssConfig ossConfig; - - @Override - public Health health() { - try { - // 检查OSS连接 - boolean exists = ossClient.doesBucketExist(ossConfig.getBucketName()); - - if (exists) { - return Health.up() - .withDetail("oss.bucket", ossConfig.getBucketName()) - .withDetail("oss.endpoint", ossConfig.getEndpoint()) - .withDetail("status", "connected") - .build(); - } else { - return Health.down() - .withDetail("oss.bucket", ossConfig.getBucketName()) - .withDetail("error", "Bucket not found") - .build(); - } - - } catch (Exception e) { - return Health.down() - .withDetail("error", e.getMessage()) - .build(); - } - } -} -``` - -### 9.4 数据备份 - -#### 9.4.1 数据库备份脚本 -```bash -#!/bin/bash - -# 数据库备份脚本 -BACKUP_DIR="/backup/mysql" -DATE=$(date +%Y%m%d_%H%M%S) -DB_NAME="coder_common_thin" - -# 创建备份目录 -mkdir -p $BACKUP_DIR - -# 备份数据库 -mysqldump -h localhost -u root -p$MYSQL_ROOT_PASSWORD $DB_NAME > $BACKUP_DIR/backup_$DATE.sql - -# 删除7天前的备份 -find $BACKUP_DIR -name "backup_*.sql" -mtime +7 -delete - -echo "数据库备份完成: $BACKUP_DIR/backup_$DATE.sql" -``` - -#### 9.4.2 OSS文件备份 -```java -@Service -@RequiredArgsConstructor -@Slf4j -public class OssBackupService { - - private final OSS ossClient; - private final OssConfig ossConfig; - - /** - * 备份OSS文件到另一个Bucket - */ - public void backupToSecondaryBucket(String backupBucketName) { - try { - // 列出所有对象 - ObjectListing objectListing = ossClient.listObjects(ossConfig.getBucketName()); - - for (OSSObjectSummary objectSummary : objectListing.getObjectSummaries()) { - String objectKey = objectSummary.getKey(); - - // 复制到备份Bucket - CopyObjectRequest copyRequest = new CopyObjectRequest( - ossConfig.getBucketName(), - objectKey, - backupBucketName, - objectKey - ); - - ossClient.copyObject(copyRequest); - - log.info("文件备份成功: {}", objectKey); - } - - } catch (Exception e) { - log.error("OSS文件备份失败", e); - } - } -} -``` - -## 10. 风险评估和处理 - -### 10.1 技术风险 - -#### 10.1.1 OSS服务不可用 -**风险等级**: 高 -**影响**: 文件上传和访问服务中断 -**处理方案**: -1. 实现多存储策略,支持本地存储降级 -2. 配置OSS多地域容灾 -3. 实现缓存机制,缓存常用文件 - -```java -@Service -@RequiredArgsConstructor -public class FallbackStorageService implements StorageService { - - private final StorageServiceFactory storageServiceFactory; - private final LocalStorageService localStorageService; - - @Override - public FileUploadResult uploadFile(MultipartFile file, String fileName, String folderPath) { - try { - // 优先使用OSS - StorageService ossService = storageServiceFactory.getStorageService("oss"); - return ossService.uploadFile(file, fileName, folderPath); - - } catch (Exception e) { - log.warn("OSS上传失败,降级使用本地存储", e); - // 降级到本地存储 - return localStorageService.uploadFile(file, fileName, folderPath); - } - } -} -``` - -#### 10.1.2 访问密钥泄露 -**风险等级**: 高 -**影响**: 安全风险,可能导致数据泄露 -**处理方案**: -1. 使用RAM子账号,最小权限原则 -2. 定期轮换访问密钥 -3. 使用STS临时凭证 -4. 配置访问控制策略 - -```java -@Service -@RequiredArgsConstructor -public class OssSecurityService { - - private final OSS ossClient; - - /** - * 获取临时访问凭证 - */ - public STSCredentials getTemporaryCredentials() { - // 实现STS临时凭证获取 - // 返回临时的AccessKeyId、AccessKeySecret、SecurityToken - return null; - } - - /** - * 轮换访问密钥 - */ - public void rotateAccessKey() { - // 实现访问密钥轮换逻辑 - } -} -``` - -### 10.2 业务风险 - -#### 10.2.1 文件丢失 -**风险等级**: 中 -**影响**: 业务数据丢失 -**处理方案**: -1. 开启OSS版本控制 -2. 实现文件备份策略 -3. 定期数据校验 - -```java -@Service -@RequiredArgsConstructor -public class FileIntegrityService { - - /** - * 文件完整性校验 - */ - public boolean verifyFileIntegrity(String objectKey, String expectedMD5) { - try { - ObjectMetadata metadata = ossClient.getObjectMetadata(bucketName, objectKey); - String actualMD5 = metadata.getETag(); - - return Objects.equals(expectedMD5, actualMD5); - - } catch (Exception e) { - log.error("文件完整性校验失败: {}", objectKey, e); - return false; - } - } -} -``` - -#### 10.2.2 存储成本过高 -**风险等级**: 中 -**影响**: 运营成本增加 -**处理方案**: -1. 配置生命周期管理 -2. 实现文件压缩 -3. 定期清理无用文件 - -```java -@Service -@RequiredArgsConstructor -public class StorageCostOptimization { - - /** - * 设置文件生命周期 - */ - public void setLifecycleRule() { - SetBucketLifecycleRequest request = new SetBucketLifecycleRequest(bucketName); - - LifecycleRule rule = new LifecycleRule(); - rule.setId("DeleteOldFiles"); - rule.setStatus(RuleStatus.Enabled); - rule.setPrefix("temp/"); - - // 30天后删除 - rule.setExpirationDays(30); - - request.setLifecycleRules(Arrays.asList(rule)); - ossClient.setBucketLifecycle(request); - } -} -``` - -### 10.3 性能风险 - -#### 10.3.1 上传性能瓶颈 -**风险等级**: 中 -**影响**: 用户体验下降 -**处理方案**: -1. 实现分片上传 -2. 使用CDN加速 -3. 优化连接池配置 - -```java -@Service -@RequiredArgsConstructor -public class LargeFileUploadService { - - /** - * 分片上传大文件 - */ - public String uploadLargeFile(MultipartFile file, String objectKey) { - try { - // 初始化分片上传 - InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(bucketName, objectKey); - InitiateMultipartUploadResult initResult = ossClient.initiateMultipartUpload(initRequest); - - String uploadId = initResult.getUploadId(); - - // 分片上传逻辑 - List partETags = new ArrayList<>(); - - // 完成分片上传 - CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest( - bucketName, objectKey, uploadId, partETags); - - ossClient.completeMultipartUpload(completeRequest); - - return objectKey; - - } catch (Exception e) { - log.error("大文件上传失败", e); - throw new BusinessException("大文件上传失败"); - } - } -} -``` - -## 11. 测试方案 - -### 11.1 单元测试 - -#### 11.1.1 OSS服务测试 -```java -@SpringBootTest -@TestPropertySource(properties = { - "coder.storage.type=oss", - "coder.oss.endpoint=oss-cn-hangzhou.aliyuncs.com", - "coder.oss.bucket-name=test-bucket" -}) -class OssServiceTest { - - @Autowired - private OssService ossService; - - @Test - void testUploadFile() { - // 创建测试文件 - MockMultipartFile mockFile = new MockMultipartFile( - "test.txt", - "test.txt", - "text/plain", - "test content".getBytes() - ); - - // 执行上传 - FileUploadResult result = ossService.uploadFile(mockFile, "test.txt", "test/"); - - // 验证结果 - assertNotNull(result); - assertNotNull(result.getFilePath()); - assertNotNull(result.getFileUploadPath()); - } - - @Test - void testDeleteFile() { - // 测试文件删除 - boolean result = ossService.deleteFile("test/test.txt"); - assertTrue(result); - } -} -``` - -### 11.2 集成测试 - -#### 11.2.1 文件上传接口测试 -```java -@SpringBootTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@TestPropertySource(locations = "classpath:application-test.yml") -class FileUploadIntegrationTest { - - @Autowired - private TestRestTemplate restTemplate; - - @Test - void testFileUpload() { - // 创建测试文件 - FileSystemResource resource = new FileSystemResource("test.txt"); - - // 构建请求 - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("file", resource); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - - // 发送请求 - ResponseEntity response = restTemplate.postForEntity( - "/coder/file/uploadFile/2/files/-1", - requestEntity, - Map.class - ); - - // 验证响应 - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - } -} -``` - -### 11.3 性能测试 - -#### 11.3.1 并发上传测试 -```java -@Test -void testConcurrentUpload() throws InterruptedException { - int threadCount = 10; - int filesPerThread = 5; - CountDownLatch latch = new CountDownLatch(threadCount); - - ExecutorService executor = Executors.newFixedThreadPool(threadCount); - - for (int i = 0; i < threadCount; i++) { - executor.submit(() -> { - try { - for (int j = 0; j < filesPerThread; j++) { - // 模拟文件上传 - uploadTestFile(); - } - } finally { - latch.countDown(); - } - }); - } - - latch.await(30, TimeUnit.SECONDS); - executor.shutdown(); -} -``` - -## 12. 实施计划 - -### 12.1 开发阶段 - -#### 第一阶段:基础架构搭建(1-2周) -1. 创建OSS插件模块 -2. 实现存储服务接口 -3. 配置OSS客户端 -4. 实现基础的上传和删除功能 - -#### 第二阶段:功能完善(2-3周) -1. 完善文件上传控制器 -2. 实现文件删除机制 -3. 添加安全验证 -4. 实现错误处理和日志记录 - -#### 第三阶段:优化和测试(1-2周) -1. 性能优化 -2. 单元测试和集成测试 -3. 安全测试 -4. 性能测试 - -### 12.2 部署计划 - -#### 12.2.1 预发布环境 -1. 部署到测试环境 -2. 功能验证 -3. 性能测试 -4. 安全测试 - -#### 12.2.2 生产环境 -1. 灰度发布 -2. 监控告警 -3. 回滚准备 -4. 全量发布 - -### 12.3 验收标准 - -#### 12.3.1 功能验收 -- [ ] 文件上传功能正常 -- [ ] 图片上传功能正常 -- [ ] 文件删除功能正常 -- [ ] 批量删除功能正常 -- [ ] 文件访问URL正常 -- [ ] 前端页面显示正常 - -#### 12.3.2 性能验收 -- [ ] 单文件上传响应时间 < 5秒 -- [ ] 并发上传支持 > 100个请求/秒 -- [ ] 文件删除响应时间 < 2秒 -- [ ] 系统可用性 > 99.9% - -#### 12.3.3 安全验收 -- [ ] 文件类型验证正常 -- [ ] 文件大小限制正常 -- [ ] 权限验证正常 -- [ ] 敏感信息加密存储 -- [ ] 访问日志记录完整 - -## 13. 总结 - -本设计方案基于现有的文件上传系统,通过插件化架构实现了阿里云OSS的集成,具有以下特点: - -### 13.1 优势 -1. **架构清晰**: 采用分层架构和插件化设计,便于扩展和维护 -2. **兼容性好**: 保持与现有系统的完全兼容 -3. **功能完善**: 支持文件上传、删除、访问等完整功能 -4. **安全性高**: 实现了完善的安全机制 -5. **性能优化**: 通过连接池、缓存等技术提升性能 -6. **易于运维**: 提供了完善的监控和告警机制 - -### 13.2 创新点 -1. **存储服务抽象**: 通过接口抽象实现多种存储服务的统一管理 -2. **插件化设计**: 通过@Enable注解实现功能的可插拔配置 -3. **降级策略**: 实现了OSS不可用时的本地存储降级 -4. **文件完整性校验**: 确保文件上传和存储的完整性 -5. **成本优化**: 通过生命周期管理控制存储成本 - -### 13.3 扩展性 -本方案具有良好的扩展性,可以很容易地: -1. 添加新的存储服务(如华为云OBS、腾讯云COS) -2. 实现更多的文件处理功能(如图片压缩、文档转换) -3. 集成更多的安全机制(如数据加密、访问控制) -4. 优化性能(如CDN集成、边缘计算) - -通过本方案的实施,可以显著提升系统的文件处理能力,为用户提供更好的体验,同时确保系统的安全性和稳定性。 \ No newline at end of file diff --git a/doc/websocket/WebSocket权限实时推送技术方案设计.md b/doc/websocket/WebSocket权限实时推送技术方案设计.md deleted file mode 100644 index f329d7e..0000000 --- a/doc/websocket/WebSocket权限实时推送技术方案设计.md +++ /dev/null @@ -1,727 +0,0 @@ -# WebSocket权限实时推送技术方案设计 - -## 📋 项目背景 - -在当前的权限管理系统中,管理员修改用户权限后,用户需要重新登录才能获得最新权限,这严重影响了用户体验。为了解决这个问题,我们设计了基于WebSocket的权限实时推送方案,让权限变更能够立即生效。 - -## 🎯 整体架构设计 - -### 系统架构图 -``` -┌─────────────────┐ WebSocket ┌─────────────────┐ 数据库操作 ┌─────────────────┐ -│ 前端应用 │ ←──────────→ │ Spring Boot │ ←──────────→ │ MySQL数据库 │ -│ │ │ 后端服务 │ │ │ -│ - 权限缓存 │ │ - WebSocket服务 │ │ - 用户权限表 │ -│ - 实时更新 │ │ - 权限管理 │ │ - 权限变更记录 │ -│ - 用户界面 │ │ - 消息推送 │ │ │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -``` - -### 核心目标 -- ✅ 权限变更实时生效,无需重新登录 -- ✅ 支持多标签页同步更新 -- ✅ 安全可靠的消息推送机制 -- ✅ 良好的用户体验和性能表现 - -## 🔧 后端技术方案设计 - -### 1. WebSocket服务架构 - -#### 1.1 技术栈选择 -``` -Spring Boot + Spring WebSocket + SaToken + Redis + MySQL -``` - -#### 1.2 核心组件设计 -``` -┌── WebSocket管理层 -│ ├── WebSocketConfig (配置) -│ ├── WebSocketHandler (连接处理) -│ └── WebSocketInterceptor (权限验证) -│ -├── 权限推送服务层 -│ ├── PermissionPushService (权限推送核心服务) -│ ├── UserSessionManager (用户会话管理) -│ └── MessageBroadcaster (消息广播器) -│ -├── 权限监听层 -│ ├── PermissionChangeListener (权限变更监听) -│ ├── RoleChangeListener (角色变更监听) -│ └── MenuChangeListener (菜单变更监听) -│ -└── 数据存储层 - ├── Redis (会话存储 + 消息队列) - └── MySQL (权限数据 + 变更记录) -``` - -### 2. 核心服务设计 - -#### 2.1 WebSocket连接管理 -```java -// 连接管理策略 -- 用户登录后自动建立WebSocket连接 -- 一个用户可以有多个连接(多标签页支持) -- 连接断开后自动重连机制 -- 连接状态持久化到Redis - -// 会话存储结构 -Key: "websocket:user:{userId}" -Value: { - "connections": [ - { - "sessionId": "session-123", - "connectTime": 1640995200000, - "lastHeartbeat": 1640995800000, - "browser": "Chrome", - "ip": "192.168.1.100" - } - ] -} -``` - -#### 2.2 权限变更监听机制 -```java -// 监听触发点 -1. 用户角色分配/取消 -2. 角色权限修改 -3. 菜单权限调整 -4. 用户状态变更 - -// 变更事件设计 -@EventListener -public class PermissionChangeListener { - // 用户角色变更 - @Async - public void handleUserRoleChange(UserRoleChangeEvent event) - - // 角色权限变更 - @Async - public void handleRolePermissionChange(RolePermissionChangeEvent event) - - // 菜单权限变更 - @Async - public void handleMenuPermissionChange(MenuPermissionChangeEvent event) -} -``` - -#### 2.3 消息推送策略 -```java -// 消息类型设计 -enum MessageType { - PERMISSION_UPDATE, // 权限更新 - ROLE_CHANGE, // 角色变更 - FORCE_LOGOUT, // 强制退出 - SYSTEM_NOTICE // 系统通知 -} - -// 推送策略 -1. 精准推送:只推送给受影响的用户 -2. 批量推送:角色权限变更时推送给该角色的所有用户 -3. 广播推送:系统级权限调整时推送给所有在线用户 -``` - -### 3. 数据库设计扩展 - -#### 3.1 权限变更记录表 -```sql -CREATE TABLE sys_permission_change_log ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - change_type VARCHAR(50) NOT NULL, -- USER_ROLE, ROLE_PERMISSION, MENU_PERMISSION - target_user_id BIGINT, -- 目标用户ID(如果是用户级变更) - target_role_id BIGINT, -- 目标角色ID(如果是角色级变更) - operator_id BIGINT NOT NULL, -- 操作者ID - change_detail JSON, -- 变更详情 - create_time DATETIME DEFAULT CURRENT_TIMESTAMP, - INDEX idx_user_time (target_user_id, create_time), - INDEX idx_role_time (target_role_id, create_time) -); -``` - -#### 3.2 WebSocket会话表 -```sql -CREATE TABLE sys_websocket_session ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - user_id BIGINT NOT NULL, - session_id VARCHAR(100) NOT NULL UNIQUE, - connect_time DATETIME NOT NULL, - disconnect_time DATETIME, - client_info JSON, -- 客户端信息 - status TINYINT DEFAULT 1, -- 1:连接中 0:已断开 - INDEX idx_user_status (user_id, status) -); -``` - -### 4. 后端核心接口设计 - -#### 4.1 WebSocket端点配置 -```java -@Configuration -@EnableWebSocket -public class WebSocketConfig implements WebSocketConfigurer { - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(new PermissionWebSocketHandler(), "/ws/permission") - .setAllowedOrigins("*") - .addInterceptors(new WebSocketAuthInterceptor()); - } -} -``` - -#### 4.2 权限推送服务接口 -```java -@Component -public class PermissionPushService { - - /** - * 推送权限更新消息给指定用户 - */ - public void pushPermissionUpdate(Long userId, List newPermissions); - - /** - * 推送角色变更消息给指定用户 - */ - public void pushRoleChange(Long userId, List newRoles); - - /** - * 批量推送权限更新(角色权限变更时) - */ - public void batchPushPermissionUpdate(Long roleId, List newPermissions); - - /** - * 强制用户下线 - */ - public void forceUserLogout(Long userId, String reason); -} -``` - -#### 4.3 消息格式定义 -```java -@Data -public class WebSocketMessage { - private String type; // 消息类型 - private Long userId; // 目标用户ID - private Long timestamp; // 时间戳 - private Object data; // 消息数据 - private String messageId; // 消息ID - private String operator; // 操作者 -} - -@Data -public class PermissionUpdateData { - private List permissions; // 新权限列表 - private List roles; // 新角色列表 - private String updateType; // 更新类型:ADD, REMOVE, REPLACE - private String reason; // 变更原因 -} -``` - -## 🌐 前端技术方案设计 - -### 1. WebSocket客户端架构 - -#### 1.1 技术栈 -``` -Vue3 + TypeScript + Pinia + WebSocket API -``` - -#### 1.2 组件设计 -``` -┌── WebSocket管理层 -│ ├── WebSocketService (核心WebSocket服务) -│ ├── ConnectionManager (连接管理器) -│ └── MessageHandler (消息处理器) -│ -├── 权限更新层 -│ ├── PermissionUpdateService (权限更新服务) -│ ├── PermissionSyncManager (权限同步管理) -│ └── PermissionNotification (权限通知) -│ -└── 用户界面层 - ├── PermissionUpdateNotify (权限更新提示组件) - ├── ConnectionStatus (连接状态组件) - └── WebSocketDebugPanel (调试面板) -``` - -### 2. WebSocket服务设计 - -#### 2.1 连接管理策略 -```typescript -class WebSocketService { - // 连接策略 - - 用户登录成功后自动连接 - - 连接断开后指数退避重连 - - 页面可见性变化时管理连接 - - 网络状态变化时重连 - - // 连接状态管理 - enum ConnectionState { - DISCONNECTED = 'disconnected', - CONNECTING = 'connecting', - CONNECTED = 'connected', - RECONNECTING = 'reconnecting', - ERROR = 'error' - } -} -``` - -#### 2.2 消息处理机制 -```typescript -// 消息类型定义 -interface WebSocketMessage { - type: 'PERMISSION_UPDATE' | 'ROLE_CHANGE' | 'FORCE_LOGOUT' | 'SYSTEM_NOTICE' - userId: number - timestamp: number - data: any - messageId: string -} - -// 消息处理器 -class MessageHandler { - handlePermissionUpdate() // 处理权限更新 - handleRoleChange() // 处理角色变更 - handleForceLogout() // 处理强制退出 - handleSystemNotice() // 处理系统通知 -} -``` - -### 3. 权限同步机制 - -#### 3.1 同步策略 -```typescript -class PermissionSyncManager { - // 同步时机 - 1. 收到权限更新消息时立即同步 - 2. 连接重建后检查权限版本 - 3. 页面激活时检查权限一致性 - - // 同步方式 - async syncPermissions(updateType: string) { - // 1. 请求最新权限数据 - // 2. 更新本地权限缓存 - // 3. 触发UI重新渲染 - // 4. 显示权限更新通知 - } -} -``` - -#### 3.2 冲突处理 -```typescript -// 权限冲突处理策略 -1. 权限被收回:立即隐藏相关UI,显示权限不足提示 -2. 权限被授予:立即显示新的功能按钮,显示权限获得提示 -3. 强制退出:清理本地数据,跳转到登录页 -4. 操作中断:保存用户操作状态,权限恢复后继续 -``` - -### 4. 前端核心服务设计 - -#### 4.1 WebSocket服务接口 -```typescript -export interface IWebSocketService { - // 连接管理 - connect(): Promise - disconnect(): void - reconnect(): Promise - - // 消息发送 - sendMessage(message: WebSocketMessage): void - - // 事件监听 - onMessage(callback: (message: WebSocketMessage) => void): void - onConnected(callback: () => void): void - onDisconnected(callback: () => void): void - onError(callback: (error: Error) => void): void - - // 状态查询 - isConnected(): boolean - getConnectionState(): ConnectionState -} -``` - -#### 4.2 权限更新服务接口 -```typescript -export interface IPermissionUpdateService { - // 权限同步 - syncPermissions(): Promise - updateLocalPermissions(permissions: string[]): void - - // 通知管理 - showPermissionUpdateNotification(updateInfo: PermissionUpdateInfo): void - showPermissionRevokedWarning(revokedPermissions: string[]): void - - // 权限检查 - checkPermissionChange(): Promise - validateCurrentPermissions(): boolean -} -``` - -## 🔐 安全性设计 - -### 1. 连接安全 -```java -// Token验证 -- WebSocket握手时验证JWT Token -- 定期验证Token有效性 -- Token过期时自动断开连接 - -// 权限验证 -- 连接建立时验证用户权限 -- 消息发送前验证操作权限 -- 防止权限越权操作 - -// 示例:WebSocket拦截器 -@Component -public class WebSocketAuthInterceptor implements HandshakeInterceptor { - - @Override - public boolean beforeHandshake(ServerHttpRequest request, - ServerHttpResponse response, - WebSocketHandler wsHandler, - Map attributes) { - // 1. 提取Token - String token = extractTokenFromRequest(request); - - // 2. 验证Token有效性 - if (!saTokenUtil.isValidToken(token)) { - return false; - } - - // 3. 获取用户信息 - Long userId = saTokenUtil.getUserIdFromToken(token); - attributes.put("userId", userId); - - return true; - } -} -``` - -### 2. 消息安全 -```java -// 消息加密 -- 敏感消息内容加密传输 -- 消息完整性校验 -- 防止消息重放攻击 - -// 频率限制 -- 连接频率限制 -- 消息发送频率限制 -- 异常连接自动断开 - -// 示例:消息安全处理 -@Component -public class MessageSecurityHandler { - - public WebSocketMessage encryptMessage(WebSocketMessage message) { - // 对敏感数据进行加密 - if (message.getType().equals("PERMISSION_UPDATE")) { - String encryptedData = aesUtil.encrypt(message.getData().toString()); - message.setData(encryptedData); - } - return message; - } - - public boolean validateMessageIntegrity(WebSocketMessage message) { - // 验证消息完整性 - String expectedHash = calculateMessageHash(message); - return expectedHash.equals(message.getHash()); - } -} -``` - -### 3. 访问控制 -```java -// 用户隔离 -- 确保用户只能接收自己的权限变更消息 -- 防止跨用户信息泄露 -- 管理员权限特殊处理 - -// 示例:消息权限验证 -@Component -public class MessagePermissionValidator { - - public boolean canReceiveMessage(Long userId, WebSocketMessage message) { - // 1. 检查消息是否发给该用户 - if (!message.getUserId().equals(userId)) { - return false; - } - - // 2. 检查用户是否有权限接收该类型消息 - return hasPermissionToReceiveMessageType(userId, message.getType()); - } -} -``` - -## 🚀 性能优化方案 - -### 1. 连接优化 -```java -// 连接池管理 -- 合理设置连接数上限 -- 空闲连接自动清理 -- 连接状态监控 - -// 内存优化 -- 及时清理断开的连接 -- 消息队列大小限制 -- 定期清理过期数据 - -// 示例:连接池配置 -@Configuration -public class WebSocketPoolConfig { - - @Bean - public WebSocketConnectionPool connectionPool() { - return WebSocketConnectionPool.builder() - .maxConnections(10000) // 最大连接数 - .maxConnectionsPerUser(5) // 每用户最大连接数 - .idleTimeout(Duration.ofMinutes(30)) // 空闲超时 - .cleanupInterval(Duration.ofMinutes(5)) // 清理间隔 - .build(); - } -} -``` - -### 2. 推送优化 -```java -// 批量推送 -- 相同类型消息合并推送 -- 延迟推送策略 -- 推送优先级管理 - -// 缓存优化 -- Redis缓存权限数据 -- 权限变更增量推送 -- 本地权限缓存 - -// 示例:批量推送实现 -@Component -public class BatchMessagePusher { - - private final Map> messageBatches = new ConcurrentHashMap<>(); - - @Scheduled(fixedDelay = 1000) // 每秒批量推送一次 - public void flushMessageBatches() { - messageBatches.forEach((batchKey, messages) -> { - WebSocketMessage batchMessage = mergeMess - ages(messages); - webSocketHandler.broadcast(batchMessage); - }); - messageBatches.clear(); - } -} -``` - -### 3. 数据库优化 -```sql --- 权限查询优化 -CREATE INDEX idx_user_permission_version ON sys_login_user(user_id, permission_version); -CREATE INDEX idx_role_permission_update ON sys_role_menu(role_id, update_time); - --- 会话查询优化 -CREATE INDEX idx_websocket_user_status ON sys_websocket_session(user_id, status, connect_time); - --- 变更日志查询优化 -CREATE INDEX idx_permission_log_target ON sys_permission_change_log(target_user_id, target_role_id, create_time); -``` - -## 📊 监控和日志 - -### 1. 连接监控 -```java -// 监控指标 -- 当前连接数 -- 连接成功率 -- 连接断开原因统计 -- 消息推送成功率 - -// 示例:监控服务 -@Component -public class WebSocketMonitorService { - - private final MeterRegistry meterRegistry; - - public void recordConnection(String result) { - Counter.builder("websocket.connections") - .tag("result", result) - .register(meterRegistry) - .increment(); - } - - public void recordMessagePush(String type, String result) { - Counter.builder("websocket.messages") - .tag("type", type) - .tag("result", result) - .register(meterRegistry) - .increment(); - } -} -``` - -### 2. 错误处理和日志 -```java -// 日志记录 -- 连接建立/断开日志 -- 消息推送日志 -- 错误异常日志 -- 性能统计日志 - -// 示例:日志配置 -@Slf4j -@Component -public class WebSocketLogger { - - public void logConnection(Long userId, String action, String result) { - log.info("WebSocket连接 - 用户:{}, 操作:{}, 结果:{}", userId, action, result); - } - - public void logMessagePush(Long userId, String messageType, String result) { - log.info("消息推送 - 用户:{}, 类型:{}, 结果:{}", userId, messageType, result); - } - - public void logError(String operation, Exception e) { - log.error("WebSocket错误 - 操作:{}, 异常:", operation, e); - } -} -``` - -## 📋 实施步骤 - -### 第一阶段:基础WebSocket服务(1-2周) -1. **后端WebSocket服务搭建** - - 创建WebSocket配置类 - - 实现WebSocket处理器 - - 添加权限验证拦截器 - - 基础连接管理功能 - -2. **前端WebSocket客户端** - - 创建WebSocket服务类 - - 实现连接管理逻辑 - - 添加重连机制 - - 基础消息收发功能 - -3. **基础测试** - - 连接建立测试 - - 消息收发测试 - - 断线重连测试 - -### 第二阶段:权限推送核心功能(2-3周) -1. **权限变更监听** - - 实现用户角色变更监听 - - 实现角色权限变更监听 - - 实现菜单权限变更监听 - - 创建权限变更事件 - -2. **消息推送逻辑** - - 实现权限更新消息推送 - - 实现角色变更消息推送 - - 实现批量推送逻辑 - - 添加消息去重机制 - -3. **前端权限同步** - - 实现权限数据更新 - - 实现UI实时刷新 - - 添加权限变更通知 - - 处理权限冲突场景 - -### 第三阶段:高级功能和优化(2-3周) -1. **安全性增强** - - 添加消息加密 - - 实现访问控制 - - 添加频率限制 - - 防止攻击机制 - -2. **性能优化** - - 实现连接池管理 - - 添加消息批量处理 - - 优化数据库查询 - - 添加缓存机制 - -3. **用户体验优化** - - 完善重连策略 - - 优化通知交互 - - 添加调试面板 - - 处理边界情况 - -### 第四阶段:生产环境适配(1-2周) -1. **集群部署支持** - - Redis消息队列 - - 负载均衡配置 - - 会话共享机制 - -2. **监控和运维** - - 添加监控指标 - - 完善日志记录 - - 配置告警机制 - - 制定运维手册 - -3. **测试和部署** - - 压力测试 - - 兼容性测试 - - 生产环境部署 - - 回滚方案准备 - -## 🔍 风险评估和应对方案 - -### 1. 技术风险 -| 风险项 | 影响度 | 概率 | 应对方案 | -|--------|--------|------|----------| -| WebSocket连接不稳定 | 高 | 中 | 完善重连机制,降级到轮询 | -| 消息推送延迟 | 中 | 低 | 优化推送逻辑,添加超时机制 | -| 内存泄漏 | 高 | 低 | 定期清理,添加监控 | -| 安全漏洞 | 高 | 低 | 安全审计,权限校验 | - -### 2. 业务风险 -| 风险项 | 影响度 | 概率 | 应对方案 | -|--------|--------|------|----------| -| 权限同步失败 | 高 | 中 | 手动刷新机制,错误提示 | -| 用户体验下降 | 中 | 低 | 渐进式升级,用户反馈 | -| 系统复杂度增加 | 中 | 高 | 完善文档,团队培训 | - -### 3. 运维风险 -| 风险项 | 影响度 | 概率 | 应对方案 | -|--------|--------|------|----------| -| 服务器压力增加 | 中 | 中 | 性能监控,扩容预案 | -| 故障排查困难 | 中 | 中 | 详细日志,监控告警 | -| 部署复杂度增加 | 低 | 高 | 自动化部署,回滚机制 | - -## 📈 预期效果 - -### 1. 用户体验提升 -- ✅ 权限变更即时生效,无需重新登录 -- ✅ 多标签页权限状态同步 -- ✅ 清晰的权限变更通知 - -### 2. 系统性能 -- ✅ 减少不必要的接口调用 -- ✅ 提高权限检查效率 -- ✅ 降低服务器负载 - -### 3. 管理效率 -- ✅ 权限管理操作即时生效 -- ✅ 减少用户投诉和支持工作 -- ✅ 提高系统管理效率 - -## 📚 相关技术文档 - -1. [Spring WebSocket官方文档](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket) -2. [Vue3 WebSocket最佳实践](https://vuejs.org/guide/extras/web-components.html) -3. [SaToken权限认证文档](https://sa-token.dev33.cn/) -4. [Redis消息队列使用指南](https://redis.io/docs/manual/pubsub/) - -## 👥 团队分工建议 - -| 角色 | 职责 | 时间投入 | -|------|------|----------| -| 后端开发 | WebSocket服务、权限监听、消息推送 | 60% | -| 前端开发 | WebSocket客户端、权限同步、UI更新 | 40% | -| 测试工程师 | 功能测试、性能测试、安全测试 | 全程参与 | -| 运维工程师 | 部署配置、监控告警、性能调优 | 后期参与 | - ---- - -**文档版本**: v1.0 -**创建时间**: 2025-01-07 -**更新时间**: 2025-01-07 -**负责人**: 系统架构团队 -**审核人**: 技术负责人 \ No newline at end of file