From 19d3eccb5bdc9a964818fbf195325efb399cc5b8 Mon Sep 17 00:00:00 2001 From: Leo Date: Wed, 8 Oct 2025 02:00:43 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=9F=BA=E7=A1=80=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加Maven父POM配置,定义项目依赖版本 - 配置项目模块结构 - 添加.gitignore规则 - 添加项目文档和开发指南 --- api/README.md | 211 ++ api/authentication/登录认证API.md | 255 +++ api/authentication/验证码API.md | 397 ++++ api/permission/菜单管理API.md | 1315 ++++++++++++ api/permission/角色管理API.md | 882 ++++++++ api/system/图片管理API.md | 871 ++++++++ api/system/文件管理API.md | 867 ++++++++ api/system/登录日志API.md | 1017 +++++++++ api/user/用户管理API.md | 888 ++++++++ doc/oss/setup-env.sh | 240 +++ doc/oss/环境变量设置指南.md | 271 +++ doc/oss/阿里云OSS文件上传系统设计方案.md | 1855 +++++++++++++++++ .../WebSocket权限实时推送技术方案设计.md | 727 +++++++ script/README.md | 242 +++ script/project-template-refactor.sh | 567 +++++ 15 files changed, 10605 insertions(+) create mode 100644 api/README.md create mode 100644 api/authentication/登录认证API.md create mode 100644 api/authentication/验证码API.md create mode 100644 api/permission/菜单管理API.md create mode 100644 api/permission/角色管理API.md create mode 100644 api/system/图片管理API.md create mode 100644 api/system/文件管理API.md create mode 100644 api/system/登录日志API.md create mode 100644 api/user/用户管理API.md create mode 100755 doc/oss/setup-env.sh create mode 100644 doc/oss/环境变量设置指南.md create mode 100644 doc/oss/阿里云OSS文件上传系统设计方案.md create mode 100644 doc/websocket/WebSocket权限实时推送技术方案设计.md create mode 100644 script/README.md create mode 100755 script/project-template-refactor.sh diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..52ec97f --- /dev/null +++ b/api/README.md @@ -0,0 +1,211 @@ +# Coder Common Thin Backend API 文档 + +## 项目概述 + +Coder Common Thin Backend 是一个基于Spring Boot 3.5.0的企业级开发框架,采用插件化架构设计,提供了完整的后台管理系统功能。 + +## 技术栈 + +- **后端框架**: Spring Boot 3.5.0 + Java 17 +- **数据库**: MySQL 8 + MyBatis Plus 3.5.12 +- **缓存**: Redis (Spring Data Redis) +- **安全认证**: Sa-Token 1.43.0 +- **权限控制**: RBAC (Role-Based Access Control) +- **API文档**: OpenAPI 3.0 (Swagger) +- **工具库**: Hutool 5.8.38, Fastjson2 2.0.57, Guava 33.4.8 + +## 服务器信息 + +- **默认端口**: 18099 +- **基础URL**: http://localhost:18099 +- **API前缀**: /coder (除登录认证接口外) + +## 认证方式 + +系统使用Sa-Token进行身份认证和权限管理: +- **Token类型**: Bearer Token +- **Token位置**: 请求头 `Authorization` 字段 +- **Token过期**: 支持自定义过期时间 +- **权限验证**: 基于注解的权限验证 + +## 统一响应格式 + +所有接口均采用统一的JSON响应格式: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + // 具体数据 + }, + "traceId": "trace-id-value" +} +``` + +### 状态码说明 + +- **200**: 成功 +- **400**: 请求参数错误 +- **401**: 认证失败/未登录 +- **403**: 权限不足 +- **500**: 服务器内部错误 + +## 分页查询格式 + +分页查询请求参数: +```json +{ + "pageNo": 1, + "pageSize": 10, + "params": { + // 查询参数 + } +} +``` + +分页查询响应格式: +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "records": [ + // 数据记录 + ], + "total": 100, + "size": 10, + "current": 1, + "pages": 10, + "searchCount": true + }, + "traceId": "trace-id-value" +} +``` + +## 权限系统 + +系统采用RBAC权限模型: +- **用户 (User)**: 系统使用者 +- **角色 (Role)**: 权限的集合 +- **权限 (Permission)**: 具体的操作权限 +- **菜单 (Menu)**: 系统菜单和按钮权限 + +### 权限验证流程 + +1. 用户登录获取Token +2. 请求接口时携带Token +3. 系统验证Token有效性 +4. 根据用户角色验证权限 +5. 允许或拒绝访问 + +## API模块分类 + +### 1. 认证模块 (Authentication) +- [登录认证API](./authentication/登录认证API) +- [验证码API](./authentication/验证码API) + +### 2. 用户管理模块 (User Management) +- [用户管理API](./user/用户管理API) + +### 3. 权限管理模块 (Permission Management) +- [菜单管理API](./permission/菜单管理API) +- [角色管理API](./permission/角色管理API) + +### 4. 系统管理模块 (System Management) +- [文件管理API](./system/文件管理API) +- [图片管理API](./system/图片管理API) +- [登录日志API](./system/登录日志API) + +## 错误处理 + +系统提供了完善的错误处理机制: + +### 常见错误类型 + +1. **业务异常**: 自定义业务逻辑错误 +2. **认证异常**: 登录认证相关错误 +3. **权限异常**: 权限验证失败 +4. **参数异常**: 请求参数验证失败 +5. **系统异常**: 服务器内部错误 + +### 错误响应格式 + +```json +{ + "status": 400, + "msg": "具体错误信息", + "data": null, + "traceId": "trace-id-value" +} +``` + +## 开发环境配置 + +### 数据库配置 +```yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/coder_common_thin + username: root + password: your_password +``` + +### Redis配置 +```yaml +spring: + redis: + host: localhost + port: 6379 + password: your_password +``` + +### Sa-Token配置 +```yaml +sa-token: + token-name: Authorization + timeout: 2592000 + activity-timeout: -1 + is-concurrent: true + is-share: false + is-read-head: true + is-read-cookie: false +``` + +## 接口调用示例 + +### 登录接口调用 +```bash +curl -X POST \ + http://localhost:18099/auth/login \ + -H 'Content-Type: application/json' \ + -d '{ + "loginName": "admin", + "password": "123456", + "codeKey": "uuid-key", + "securityCode": "1234" + }' +``` + +### 携带Token调用接口 +```bash +curl -X GET \ + http://localhost:18099/coder/sysLoginUser/getLoginUserInformation \ + -H 'Authorization: Bearer your-token-value' +``` + +## 注意事项 + +1. 所有接口都需要进行认证,除了登录接口和验证码接口 +2. 权限验证基于角色和权限码进行 +3. 系统支持多设备登录 +4. 敏感操作会记录操作日志 +5. 建议在生产环境中启用HTTPS + +## 更新日志 + +- **v1.0.0** (2025-07-05): 初始版本发布 + +## 联系方式 + +如有问题,请联系开发团队。 \ No newline at end of file diff --git a/api/authentication/登录认证API.md b/api/authentication/登录认证API.md new file mode 100644 index 0000000..5f2be0b --- /dev/null +++ b/api/authentication/登录认证API.md @@ -0,0 +1,255 @@ +# 登录认证API + +## 概述 + +登录认证模块提供用户登录、退出、注册等功能,使用Sa-Token进行身份认证和会话管理。 + +## 接口列表 + +### 1. 用户登录 + +**接口地址**: `POST /auth/login` + +**接口描述**: 用户使用账号密码登录系统 + +**是否需要认证**: 否 + +**请求参数**: + +```json +{ + "loginName": "admin", + "password": "123456", + "codeKey": "uuid-key", + "securityCode": "1234", + "rememberMe": false +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| loginName | String | 是 | 登录账号 | 长度3-16位,只能包含字母和数字 | +| password | String | 是 | 登录密码 | 不能为空 | +| codeKey | String | 是 | 验证码UUID | 不能为空 | +| securityCode | String | 是 | 验证码 | 不能为空 | +| rememberMe | Boolean | 否 | 记住登录 | 默认false | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "tokenName": "Authorization", + "tokenValue": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." + }, + "traceId": "trace-123456" +} +``` + +**响应参数说明**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| tokenName | String | Token名称 | +| tokenValue | String | Token值 | + +**错误码**: + +| 状态码 | 错误信息 | 说明 | +|--------|----------|------| +| 400 | 账号长度为 3-16 位 | 登录账号长度不符合要求 | +| 400 | 账号格式为数字以及字母 | 登录账号格式不正确 | +| 400 | 请输入登录名 | 登录名为空 | +| 400 | 请输入密码 | 密码为空 | +| 400 | 验证码已失效 | 验证码Key无效 | +| 400 | 请输入验证码 | 验证码为空 | +| 400 | 验证码错误 | 验证码不正确 | +| 400 | 用户不存在 | 用户账号不存在 | +| 400 | 密码错误 | 密码不正确 | +| 400 | 账号已被停用 | 用户账号被禁用 | +| 400 | 账号已被锁定 | 用户账号被锁定 | + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/auth/login \ + -H 'Content-Type: application/json' \ + -d '{ + "loginName": "admin", + "password": "123456", + "codeKey": "550e8400-e29b-41d4-a716-446655440000", + "securityCode": "1234", + "rememberMe": false + }' +``` + +--- + +### 2. 用户退出 + +**接口地址**: `GET /auth/logout` + +**接口描述**: 用户退出登录,清除会话信息 + +**是否需要认证**: 是 + +**请求头**: + +``` +Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... +``` + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "退出成功", + "traceId": "trace-123456" +} +``` + +**错误码**: + +| 状态码 | 错误信息 | 说明 | +|--------|----------|------| +| 401 | 当前会话未登录 | 用户未登录或Token无效 | + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/auth/logout \ + -H 'Authorization: your-token-value' +``` + +--- + +### 3. 用户注册 + +**接口地址**: `POST /auth/register` + +**接口描述**: 新用户注册账号 + +**是否需要认证**: 否 + +**请求参数**: + +```json +{ + "loginName": "newuser", + "password": "123456", + "userName": "新用户", + "codeKey": "uuid-key", + "securityCode": "1234" +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| loginName | String | 是 | 登录账号 | 长度3-16位,只能包含字母和数字 | +| password | String | 是 | 登录密码 | 不能为空 | +| userName | String | 是 | 用户姓名 | 不能为空 | +| codeKey | String | 是 | 验证码UUID | 不能为空 | +| securityCode | String | 是 | 验证码 | 不能为空 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "注册成功", + "traceId": "trace-123456" +} +``` + +**错误码**: + +| 状态码 | 错误信息 | 说明 | +|--------|----------|------| +| 400 | 账号长度为 3-16 位 | 登录账号长度不符合要求 | +| 400 | 账号格式为数字以及字母 | 登录账号格式不正确 | +| 400 | 请输入登录名 | 登录名为空 | +| 400 | 请输入密码 | 密码为空 | +| 400 | 请输入用户姓名 | 用户姓名为空 | +| 400 | 验证码已失效 | 验证码Key无效 | +| 400 | 请输入验证码 | 验证码为空 | +| 400 | 验证码错误 | 验证码不正确 | +| 400 | 账号已存在 | 登录账号已被注册 | + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/auth/register \ + -H 'Content-Type: application/json' \ + -d '{ + "loginName": "newuser", + "password": "123456", + "userName": "新用户", + "codeKey": "550e8400-e29b-41d4-a716-446655440000", + "securityCode": "1234" + }' +``` + +--- + +## 认证流程说明 + +### 1. 登录流程 + +1. 用户获取验证码(调用验证码接口) +2. 用户输入账号、密码、验证码 +3. 系统验证验证码有效性 +4. 系统验证用户账号和密码 +5. 系统检查用户状态(是否禁用/锁定) +6. 生成Token并返回给用户 +7. 用户后续请求携带Token + +### 2. Token使用 + +- Token需要在请求头中携带:`Authorization: token-value` +- Token有过期时间,过期后需要重新登录 +- 系统支持多设备登录 +- 管理员可以强制用户下线 + +### 3. 权限验证 + +- 登录后系统会根据用户角色加载权限 +- 每个接口都会验证用户是否有相应权限 +- 超级管理员拥有所有权限 + +### 4. 会话管理 + +- 用户登录信息存储在Redis中 +- 支持会话延长 +- 支持记住登录功能 +- 系统会记录用户登录日志 + +## 安全特性 + +1. **密码加密**: 使用盐值加密存储密码 +2. **验证码保护**: 防止暴力破解 +3. **账号锁定**: 连续错误3次自动锁定 +4. **IP限制**: 可配置IP白名单/黑名单 +5. **会话安全**: Token有效期管理 +6. **日志记录**: 记录所有登录操作 + +## 注意事项 + +1. 验证码有效期为5分钟 +2. Token默认有效期为30天 +3. 账号锁定时间为10分钟 +4. 注册功能可通过配置开启/关闭 +5. 建议在生产环境中启用HTTPS \ No newline at end of file diff --git a/api/authentication/验证码API.md b/api/authentication/验证码API.md new file mode 100644 index 0000000..c25b3f9 --- /dev/null +++ b/api/authentication/验证码API.md @@ -0,0 +1,397 @@ +# 验证码API + +## 概述 + +验证码模块提供图形验证码生成功能,支持PNG和GIF两种格式,用于防止恶意攻击和机器人注册。 + +## 接口列表 + +### 1. 生成PNG验证码 + +**接口地址**: `GET /captcha/png` + +**接口描述**: 生成PNG格式的图形验证码 + +**是否需要认证**: 否 + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAkCAYAAAB...", + "captchaText": "1234" + }, + "traceId": "trace-123456" +} +``` + +**响应参数说明**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| uuid | String | 验证码唯一标识,用于后续验证 | +| base64 | String | 验证码图片Base64编码 | +| captchaText | String | 验证码文本(开发环境返回,生产环境不返回) | + +**特性说明**: + +- 验证码长度:4位数字 +- 图片尺寸:102x38像素 +- 有效期:5分钟 +- 字体:随机字体 +- 背景:随机背景色 +- 干扰线:随机干扰线 + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/captcha/png \ + -H 'Content-Type: application/json' +``` + +**前端使用示例**: + +```javascript +// 获取验证码 +function getCaptcha() { + fetch('/captcha/png') + .then(response => response.json()) + .then(data => { + if (data.status === 200) { + // 显示验证码图片 + document.getElementById('captchaImg').src = data.data.base64; + // 保存验证码UUID,用于登录时提交 + document.getElementById('codeKey').value = data.data.uuid; + } + }); +} + +// 点击刷新验证码 +document.getElementById('captchaImg').onclick = getCaptcha; +``` + +--- + +### 2. 生成GIF验证码 + +**接口地址**: `GET /captcha/gif` + +**接口描述**: 生成GIF动画格式的图形验证码 + +**是否需要认证**: 否 + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "uuid": "550e8400-e29b-41d4-a716-446655440001", + "base64": "data:image/gif;base64,R0lGODlhZgAyAPcAAAAAAP///wAAAP...", + "captchaText": "5678" + }, + "traceId": "trace-123456" +} +``` + +**响应参数说明**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| uuid | String | 验证码唯一标识,用于后续验证 | +| base64 | String | 验证码图片Base64编码 | +| captchaText | String | 验证码文本(开发环境返回,生产环境不返回) | + +**特性说明**: + +- 验证码长度:4位数字 +- 图片尺寸:102x38像素 +- 有效期:5分钟 +- 动画效果:字符摆动动画 +- 帧数:10帧 +- 动画速度:100ms/帧 + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/captcha/gif \ + -H 'Content-Type: application/json' +``` + +**前端使用示例**: + +```javascript +// 获取GIF验证码 +function getGifCaptcha() { + fetch('/captcha/gif') + .then(response => response.json()) + .then(data => { + if (data.status === 200) { + // 显示验证码图片 + document.getElementById('captchaImg').src = data.data.base64; + // 保存验证码UUID,用于登录时提交 + document.getElementById('codeKey').value = data.data.uuid; + } + }); +} +``` + +--- + +## 验证码配置 + +### 1. 验证码参数配置 + +```yaml +# application.yml +captcha: + # 验证码长度 + length: 4 + # 验证码宽度 + width: 102 + # 验证码高度 + height: 38 + # 验证码有效期(分钟) + timeout: 5 + # 字体大小 + font-size: 25 + # 干扰线数量 + line-count: 100 + # 是否开启验证码 + enabled: true +``` + +### 2. 验证码存储 + +- 验证码使用Redis存储 +- Key格式:`captcha:uuid` +- 过期时间:5分钟自动删除 +- 值存储:验证码文本(忽略大小写) + +### 3. 验证码验证 + +```java +// 验证码验证逻辑 +public boolean validateCaptcha(String uuid, String inputCode) { + // 从Redis获取验证码 + String correctCode = redisTemplate.opsForValue().get("captcha:" + uuid); + + if (correctCode == null) { + throw new BusinessException("验证码已过期"); + } + + // 不区分大小写验证 + if (!correctCode.equalsIgnoreCase(inputCode)) { + throw new BusinessException("验证码错误"); + } + + // 验证成功后立即删除验证码(防止重复使用) + redisTemplate.delete("captcha:" + uuid); + + return true; +} +``` + +--- + +## 安全特性 + +### 1. 防暴力破解 + +- 验证码一次性使用 +- 验证后立即删除 +- 5分钟自动过期 +- 支持IP限制 + +### 2. 防机器识别 + +- 随机字体和颜色 +- 随机背景和干扰线 +- 字符位置随机偏移 +- GIF动画增加识别难度 + +### 3. 防重放攻击 + +- 每次验证码都有唯一UUID +- 验证码使用后立即失效 +- 不允许重复使用 + +## 错误处理 + +### 常见错误情况 + +| 错误场景 | 错误码 | 错误信息 | +|----------|--------|----------| +| 验证码过期 | 400 | 验证码已失效 | +| 验证码错误 | 400 | 验证码错误 | +| 验证码为空 | 400 | 请输入验证码 | +| UUID无效 | 400 | 验证码已失效 | +| 系统异常 | 500 | 验证码生成失败 | + +### 错误响应示例 + +```json +{ + "status": 400, + "msg": "验证码已失效", + "data": null, + "traceId": "trace-123456" +} +``` + +--- + +## 使用建议 + +### 1. 前端集成 + +```html + +
+ 验证码 + + + +
+``` + +```javascript +// 初始化验证码 +function initCaptcha() { + fetch('/captcha/png') + .then(response => response.json()) + .then(data => { + if (data.status === 200) { + document.getElementById('captchaImg').src = data.data.base64; + document.getElementById('codeKey').value = data.data.uuid; + } + }); +} + +// 刷新验证码 +function refreshCaptcha() { + initCaptcha(); +} + +// 页面加载时获取验证码 +window.onload = initCaptcha; +``` + +### 2. 表单提交 + +```javascript +// 登录表单提交 +function login() { + const formData = { + loginName: document.getElementById('loginName').value, + password: document.getElementById('password').value, + codeKey: document.getElementById('codeKey').value, + securityCode: document.getElementById('securityCode').value + }; + + fetch('/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 200) { + // 登录成功 + localStorage.setItem('token', data.data.tokenValue); + window.location.href = '/dashboard'; + } else { + // 登录失败,刷新验证码 + alert(data.msg); + refreshCaptcha(); + } + }); +} +``` + +### 3. 移动端适配 + +```css +/* 移动端验证码样式 */ +@media (max-width: 768px) { + .captcha-container { + display: flex; + align-items: center; + gap: 10px; + } + + #captchaImg { + width: 102px; + height: 38px; + cursor: pointer; + } + + #securityCode { + width: 100px; + height: 38px; + } +} +``` + +## 性能优化 + +### 1. 缓存策略 + +- 验证码图片不缓存 +- 验证码文本缓存5分钟 +- 使用Redis集群提高性能 + +### 2. 并发控制 + +- 支持高并发验证码生成 +- 每秒可生成1000+验证码 +- 使用连接池管理Redis连接 + +### 3. 资源优化 + +- 图片大小优化(小于2KB) +- 内存使用优化 +- 垃圾回收优化 + +## 监控和日志 + +### 1. 指标监控 + +- 验证码生成次数 +- 验证码验证次数 +- 验证码成功率 +- 验证码过期率 + +### 2. 日志记录 + +- 验证码生成日志 +- 验证码验证日志 +- 异常错误日志 +- 性能监控日志 + +### 3. 告警机制 + +- 验证码成功率过低告警 +- 验证码生成异常告警 +- Redis连接异常告警 + +## 注意事项 + +1. 生产环境不返回验证码文本 +2. 验证码区分大小写(可配置) +3. 验证码只能使用一次 +4. 建议定期清理过期验证码 +5. 支持自定义验证码样式和难度 \ No newline at end of file diff --git a/api/permission/菜单管理API.md b/api/permission/菜单管理API.md new file mode 100644 index 0000000..2a699f2 --- /dev/null +++ b/api/permission/菜单管理API.md @@ -0,0 +1,1315 @@ +# 菜单管理API + +## 概述 + +菜单管理模块是权限系统的核心组成部分,负责管理系统菜单、权限配置和路由生成。支持无限级菜单嵌套,提供目录、菜单、按钮三种类型的权限控制。 + +## 权限说明 + +菜单管理接口需要相应的权限才能访问: + +| 操作 | 权限码 | 说明 | +|------|--------|------| +| 查询菜单列表 | `system:menu:list` | 查看菜单列表权限 | +| 新增菜单 | `system:menu:add` | 新增菜单权限 | +| 修改菜单 | `system:menu:edit` | 修改菜单信息权限 | +| 删除菜单 | `system:menu:remove` | 删除菜单权限 | +| 分配权限 | `system:menu:assign` | 分配菜单权限 | + +## 菜单类型说明 + +| 类型 | 值 | 说明 | 用途 | +|------|---|------|------| +| 目录 | 1 | 菜单目录 | 用于菜单分组,不对应具体页面 | +| 菜单 | 2 | 菜单项 | 对应具体的页面路由 | +| 按钮 | 3 | 按钮权限 | 用于控制页面内的按钮显示 | + +## 接口列表 + +### 1. 分页查询菜单列表 + +**接口地址**: `GET /coder/sysMenu/listPage` + +**接口描述**: 分页查询系统菜单列表 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:list` + +**请求头**: +``` +Authorization: your-token-value +``` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 示例 | +|--------|------|------|------|------| +| pageNo | Integer | 否 | 页码 | 1 | +| pageSize | Integer | 否 | 每页大小 | 10 | +| menuName | String | 否 | 菜单名称 | 用户管理 | +| menuStatus | String | 否 | 菜单状态 | 0 | +| auth | String | 否 | 权限标识 | system:user:list | + +**响应示例**: + +```json +{ + "code": 1, + "success": true, + "msg": "操作成功", + "data": [ + { + "menuId": 1, + "menuName": "系统管理", + "enName": "System Manage", + "parentId": 0, + "menuType": "1", + "name": "systemPage", + "path": "/system", + "component": "", + "icon": "Tools", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "/system/user", + "activeMenu": null + }, + { + "menuId": 2, + "menuName": "用户管理", + "enName": "User Manage", + "parentId": 1, + "menuType": "2", + "name": "userPage", + "path": "/system/user", + "component": "system/user/index", + "icon": "UserFilled", + "isHide": "1", + "isLink": "", + "isKeepAlive": "1", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 12, + "menuName": "角色管理", + "enName": "Role Manage", + "parentId": 1, + "menuType": "2", + "name": "rolePage", + "path": "/system/role", + "component": "system/role/index", + "icon": "Avatar", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 72, + "menuName": "文件管理", + "enName": "Files Manage", + "parentId": 70, + "menuType": "2", + "name": "filePage", + "path": "/tools/file", + "component": "system/file/index", + "icon": "FolderOpened", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": "" + }, + { + "menuId": 19, + "menuName": "菜单管理", + "enName": "Menu Manage", + "parentId": 1, + "menuType": "2", + "name": "menuPage", + "path": "/system/menu", + "component": "system/menu/index", + "icon": "Grid", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 77, + "menuName": "图库管理", + "enName": "Pictures Manage", + "parentId": 70, + "menuType": "2", + "name": "picturePage", + "path": "/tools/picture", + "component": "system/picture/index", + "icon": "Picture", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": "" + }, + { + "menuId": 39, + "menuName": "登录日志", + "enName": "Login Logs", + "parentId": 1, + "menuType": "2", + "name": "loginlogPage", + "path": "/system/loginlog", + "component": "system/loginlog/index", + "icon": "Calendar", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 42, + "menuName": "操作日志", + "enName": "Operate Logs", + "parentId": 1, + "menuType": "2", + "name": "operlogPage", + "path": "/system/operlog", + "component": "system/operlog/index", + "icon": "Notebook", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 50, + "menuName": "个人中心", + "enName": "Personage Center", + "parentId": 1, + "menuType": "2", + "name": "personagePage", + "path": "/system/personage", + "component": "system/personage/index", + "icon": "User", + "isHide": "0", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": null, + "activeMenu": null + }, + { + "menuId": 166, + "menuName": "外部链接", + "enName": "External Link", + "parentId": 0, + "menuType": "1", + "name": "linkPage", + "path": "/link", + "component": "", + "icon": "Link", + "isHide": "0", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": null, + "activeMenu": null + } + ] +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysMenu/listPage?pageNo=1&pageSize=10&menuName=系统管理" \ + -H "Authorization: your-token-value" +``` + +--- + +### 2. 查询菜单列表 + +**接口地址**: `GET /coder/sysMenu/list` + +**接口描述**: 查询系统菜单列表(树形结构,不分页) + +**是否需要认证**: 是 + +**权限要求**: `system:menu:list` + +**请求参数**: 同分页查询(除pageNo、pageSize外) + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "menuId": 1, + "menuName": "系统管理", + "enName": "System", + "parentId": 0, + "menuType": "1", + "path": "/system", + "name": "system", + "component": "Layout", + "icon": "system", + "auth": "", + "menuStatus": "0", + "activeMenu": "", + "isHide": "1", + "isLink": "1", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "isSpread": "0", + "sorted": 1, + "createBy": "admin", + "createTime": "2024-01-01 10:00:00", + "updateBy": "admin", + "updateTime": "2024-01-01 10:00:00", + "children": [ + // 子菜单... + ] + } + ], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysMenu/list" \ + -H "Authorization: your-token-value" +``` + +--- + +### 3. 根据ID查询菜单 + +**接口地址**: `GET /coder/sysMenu/getById/{id}` + +**接口描述**: 根据菜单ID查询菜单详细信息 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:list` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 菜单ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "menuId": 1, + "menuName": "系统管理", + "enName": "System", + "parentId": 0, + "menuType": "1", + "path": "/system", + "name": "system", + "component": "Layout", + "icon": "system", + "auth": "", + "menuStatus": "0", + "activeMenu": "", + "isHide": "1", + "isLink": "1", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "isSpread": "0", + "sorted": 1, + "createBy": "admin", + "createTime": "2024-01-01 10:00:00", + "updateBy": "admin", + "updateTime": "2024-01-01 10:00:00" + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysMenu/getById/1 \ + -H "Authorization: your-token-value" +``` + +--- + +### 4. 新增菜单 + +**接口地址**: `POST /coder/sysMenu/add` + +**接口描述**: 新增系统菜单 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:add` + +**请求参数**: + +```json +{ + "menuName": "用户管理", + "enName": "User Management", + "parentId": 1, + "menuType": "2", + "path": "/system/user", + "name": "user", + "component": "/system/user/index", + "icon": "user", + "auth": "system:user:list", + "menuStatus": "0", + "activeMenu": "", + "isHide": "1", + "isLink": "1", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "isSpread": "0", + "sorted": 1 +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| menuName | String | 是 | 菜单名称 | 不能为空 | +| enName | String | 否 | 英文名称 | 可为空 | +| parentId | Long | 是 | 父菜单ID | 不能为空,0表示根菜单 | +| menuType | String | 是 | 菜单类型 | 1-目录 2-菜单 3-按钮 | +| path | String | 否 | 路由地址 | 菜单类型为2时必填 | +| name | String | 否 | 路由名称 | 菜单类型为2时必填 | +| component | String | 否 | 组件路径 | 菜单类型为2时必填 | +| icon | String | 否 | 菜单图标 | 可为空 | +| auth | String | 否 | 权限标识 | 按钮类型时必填 | +| menuStatus | String | 是 | 菜单状态 | 0-启用 1-停用 | +| activeMenu | String | 否 | 选中路由 | 可为空 | +| isHide | String | 是 | 是否隐藏 | 0-隐藏 1-显示 | +| isLink | String | 否 | 是否外链 | 0-是 1-否 | +| isKeepAlive | String | 否 | 是否缓存 | 0-是 1-否 | +| isFull | String | 否 | 是否全屏 | 0-是 1-否 | +| isAffix | String | 否 | 是否固定 | 0-是 1-否 | +| isSpread | String | 否 | 是否展开 | 0-是 1-否 | +| sorted | Integer | 是 | 显示顺序 | 不能为空 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "新增成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysMenu/add \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "menuName": "用户管理", + "enName": "User Management", + "parentId": 1, + "menuType": "2", + "path": "/system/user", + "name": "user", + "component": "/system/user/index", + "icon": "user", + "auth": "system:user:list", + "menuStatus": "0", + "activeMenu": "", + "isHide": "1", + "isLink": "1", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "isSpread": "0", + "sorted": 1 + }' +``` + +--- + +### 5. 修改菜单信息 + +**接口地址**: `POST /coder/sysMenu/update` + +**接口描述**: 修改系统菜单信息 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:edit` + +**请求参数**: + +```json +{ + "menuId": 1, + "menuName": "用户管理", + "enName": "User Management", + "parentId": 1, + "menuType": "2", + "path": "/system/user", + "name": "user", + "component": "/system/user/index", + "icon": "user", + "auth": "system:user:list", + "menuStatus": "0", + "activeMenu": "", + "isHide": "1", + "isLink": "1", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "isSpread": "0", + "sorted": 1 +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| menuId | Long | 是 | 菜单ID | 必须是有效的菜单ID | +| 其他参数 | - | - | 同新增菜单 | - | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysMenu/update \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "menuId": 1, + "menuName": "用户管理", + "parentId": 1, + "menuType": "2", + "menuStatus": "0", + "sorted": 1 + }' +``` + +--- + +### 6. 删除菜单 + +**接口地址**: `POST /coder/sysMenu/deleteById/{id}` + +**接口描述**: 根据ID删除菜单 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:remove` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 菜单ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysMenu/deleteById/1 \ + -H "Authorization: your-token-value" +``` + +--- + +### 7. 批量删除菜单 + +**接口地址**: `POST /coder/sysMenu/batchDelete` + +**接口描述**: 批量删除菜单 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:remove` + +**请求参数**: + +```json +{ + "ids": [1, 2, 3] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| ids | Long[] | 是 | 菜单ID数组 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysMenu/batchDelete \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "ids": [1, 2, 3] + }' +``` + +--- + +### 8. 修改菜单状态 + +**接口地址**: `POST /coder/sysMenu/updateStatus/{id}/{menuStatus}` + +**接口描述**: 修改菜单状态(启用/停用) + +**是否需要认证**: 是 + +**权限要求**: `system:menu:edit` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 菜单ID | +| menuStatus | String | 是 | 菜单状态(0-启用 1-停用) | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysMenu/updateStatus/1/0 \ + -H "Authorization: your-token-value" +``` + +--- + +### 9. 修改菜单展开状态 + +**接口地址**: `POST /coder/sysMenu/updateSpread/{id}/{isSpread}` + +**接口描述**: 修改菜单展开状态 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:edit` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 菜单ID | +| isSpread | String | 是 | 展开状态(0-是 1-否) | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysMenu/updateSpread/1/0 \ + -H "Authorization: your-token-value" +``` + +--- + +### 10. 菜单级联下拉框 + +**接口地址**: `GET /coder/sysMenu/cascaderList` + +**接口描述**: 获取菜单级联下拉框数据 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:list` + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "label": "系统管理", + "value": 1, + "parentId": "0", + "children": [ + { + "label": "用户管理", + "value": 2, + "parentId": "1" + }, + { + "label": "角色管理", + "value": 3, + "parentId": "1" + } + ] + } + ], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysMenu/cascaderList \ + -H "Authorization: your-token-value" +``` + +--- + +### 11. 生成用户菜单路由 + +**接口地址**: `GET /coder/sysMenu/listRouters` + +**接口描述**: 根据当前用户权限生成前端菜单路由 + +**是否需要认证**: 是 + +**权限要求**: 无(已登录用户可访问) + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "code": 1, + "success": true, + "msg": "操作成功", + "data": [ + { + "menuId": 1, + "menuName": "系统管理", + "enName": "System Manage", + "parentId": 0, + "menuType": "1", + "name": "systemPage", + "path": "/system", + "component": "", + "icon": "Tools", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "/system/user", + "activeMenu": null + }, + { + "menuId": 2, + "menuName": "用户管理", + "enName": "User Manage", + "parentId": 1, + "menuType": "2", + "name": "userPage", + "path": "/system/user", + "component": "system/user/index", + "icon": "UserFilled", + "isHide": "1", + "isLink": "", + "isKeepAlive": "1", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 70, + "menuName": "系统工具", + "enName": "System Tools", + "parentId": 0, + "menuType": "1", + "name": "toolsPage", + "path": "/tools", + "component": "", + "icon": "Tools", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "/tools/file", + "activeMenu": null + }, + { + "menuId": 12, + "menuName": "角色管理", + "enName": "Role Manage", + "parentId": 1, + "menuType": "2", + "name": "rolePage", + "path": "/system/role", + "component": "system/role/index", + "icon": "Avatar", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 72, + "menuName": "文件管理", + "enName": "Files Manage", + "parentId": 70, + "menuType": "2", + "name": "filePage", + "path": "/tools/file", + "component": "system/file/index", + "icon": "FolderOpened", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": "" + }, + { + "menuId": 19, + "menuName": "菜单管理", + "enName": "Menu Manage", + "parentId": 1, + "menuType": "2", + "name": "menuPage", + "path": "/system/menu", + "component": "system/menu/index", + "icon": "Grid", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 77, + "menuName": "图库管理", + "enName": "Pictures Manage", + "parentId": 70, + "menuType": "2", + "name": "picturePage", + "path": "/tools/picture", + "component": "system/picture/index", + "icon": "Picture", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": "" + }, + { + "menuId": 39, + "menuName": "登录日志", + "enName": "Login Logs", + "parentId": 1, + "menuType": "2", + "name": "loginlogPage", + "path": "/system/loginlog", + "component": "system/loginlog/index", + "icon": "Calendar", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 42, + "menuName": "操作日志", + "enName": "Operate Logs", + "parentId": 1, + "menuType": "2", + "name": "operlogPage", + "path": "/system/operlog", + "component": "system/operlog/index", + "icon": "Notebook", + "isHide": "1", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": "", + "activeMenu": null + }, + { + "menuId": 50, + "menuName": "个人中心", + "enName": "Personage Center", + "parentId": 1, + "menuType": "2", + "name": "personagePage", + "path": "/system/personage", + "component": "system/personage/index", + "icon": "User", + "isHide": "0", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": null, + "activeMenu": null + }, + { + "menuId": 166, + "menuName": "外部链接", + "enName": "External Link", + "parentId": 0, + "menuType": "1", + "name": "linkPage", + "path": "/link", + "component": "", + "icon": "Link", + "isHide": "0", + "isLink": "", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "redirect": null, + "activeMenu": null + } + ] +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysMenu/listRouters \ + -H "Authorization: your-token-value" +``` + +--- + +### 12. 查询正常菜单列表 + +**接口地址**: `GET /coder/sysMenu/listMenuNormal` + +**接口描述**: 查询状态正常的菜单列表 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:list` + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "menuId": 1, + "menuName": "系统管理", + "enName": "System", + "parentId": 0, + "menuType": "1", + "path": "/system", + "name": "system", + "component": "Layout", + "icon": "system", + "auth": "", + "menuStatus": "0", + "activeMenu": "", + "isHide": "1", + "isLink": "1", + "isKeepAlive": "0", + "isFull": "1", + "isAffix": "1", + "isSpread": "0", + "sorted": 1, + "children": [ + // 子菜单... + ] + } + ], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysMenu/listMenuNormal \ + -H "Authorization: your-token-value" +``` + +--- + +### 13. 根据角色ID查询菜单 + +**接口地址**: `GET /coder/sysMenu/listMenuIdsByRoleId/{roleId}` + +**接口描述**: 根据角色ID查询该角色拥有的菜单ID列表 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:list` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| roleId | Long | 是 | 角色ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [1, 2, 3, 4, 5], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysMenu/listMenuIdsByRoleId/1 \ + -H "Authorization: your-token-value" +``` + +--- + +### 14. 保存角色菜单权限 + +**接口地址**: `POST /coder/sysMenu/saveRoleMenu/{roleId}/{menuIds}` + +**接口描述**: 保存角色的菜单权限 + +**是否需要认证**: 是 + +**权限要求**: `system:menu:assign` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| roleId | Long | 是 | 角色ID | +| menuIds | String | 是 | 菜单ID列表(逗号分隔) | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "保存成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysMenu/saveRoleMenu/1/1,2,3,4,5 \ + -H "Authorization: your-token-value" +``` + +--- + +## 菜单字段说明 + +### 基础字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| menuId | Long | 菜单ID | 1 | +| menuName | String | 菜单名称 | 系统管理 | +| enName | String | 英文名称 | System | +| parentId | Long | 父菜单ID | 0(根菜单) | +| menuType | String | 菜单类型 | 1-目录 2-菜单 3-按钮 | +| sorted | Integer | 显示顺序 | 1 | + +### 路由字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| path | String | 路由地址 | /system/user | +| name | String | 路由名称 | user | +| component | String | 组件路径 | /system/user/index | +| redirect | String | 重定向地址 | /system/user | +| activeMenu | String | 选中路由 | /system/user | + +### 权限字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| auth | String | 权限标识 | system:user:list | +| menuStatus | String | 菜单状态 | 0-启用 1-停用 | + +### 显示字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| icon | String | 菜单图标 | user | +| isHide | String | 是否隐藏 | 0-隐藏 1-显示 | +| isLink | String | 是否外链 | 0-是 1-否 | +| isKeepAlive | String | 是否缓存 | 0-是 1-否 | +| isFull | String | 是否全屏 | 0-是 1-否 | +| isAffix | String | 是否固定 | 0-是 1-否 | +| isSpread | String | 是否展开 | 0-是 1-否 | + +--- + +## 前端路由配置 + +### Vue Router 配置 + +```javascript +// 根据后端返回的菜单数据生成路由 +function generateRoutes(menuData) { + const routes = []; + + menuData.forEach(menu => { + const route = { + path: menu.path, + name: menu.name, + component: () => import(`@/views${menu.component}.vue`), + meta: { + title: menu.menuName, + icon: menu.icon, + isHide: menu.isHide === '0', + isLink: menu.isLink === '0', + isKeepAlive: menu.isKeepAlive === '0', + isFull: menu.isFull === '0', + isAffix: menu.isAffix === '0', + activeMenu: menu.activeMenu + } + }; + + if (menu.children && menu.children.length > 0) { + route.children = generateRoutes(menu.children); + } + + if (menu.redirect) { + route.redirect = menu.redirect; + } + + routes.push(route); + }); + + return routes; +} +``` + +### 权限控制 + +```javascript +// 页面权限控制 +router.beforeEach((to, from, next) => { + const userPermissions = store.getters.permissions; + + if (to.meta.auth) { + if (userPermissions.includes(to.meta.auth) || userPermissions.includes('*:*:*')) { + next(); + } else { + next('/403'); + } + } else { + next(); + } +}); + +// 按钮权限控制 +Vue.directive('permission', { + inserted(el, binding) { + const { value } = binding; + const permissions = store.getters.permissions; + + if (value) { + const hasPermission = permissions.includes(value) || permissions.includes('*:*:*'); + if (!hasPermission) { + el.parentNode && el.parentNode.removeChild(el); + } + } + } +}); +``` + +--- + +## 错误码说明 + +| 错误码 | 错误信息 | 说明 | +|--------|----------|------| +| 400 | 菜单名称不能为空 | 菜单名称为空 | +| 400 | 上级菜单不能为空 | 父菜单ID为空 | +| 400 | 菜单类型不能为空 | 菜单类型为空 | +| 400 | 菜单状态不能为空 | 菜单状态为空 | +| 400 | 是否隐藏菜单不能为空 | 隐藏状态为空 | +| 400 | 显示顺序不能为空 | 排序值为空 | +| 400 | 菜单不存在 | 菜单ID不存在 | +| 400 | 存在子菜单,不允许删除 | 菜单有子菜单时不能删除 | +| 400 | 菜单已分配,不允许删除 | 菜单已分配给角色时不能删除 | +| 400 | 不能选择自己作为父菜单 | 父菜单不能是自己 | +| 400 | 路由地址不能为空 | 菜单类型为菜单时路由地址必填 | +| 400 | 组件路径不能为空 | 菜单类型为菜单时组件路径必填 | +| 400 | 权限标识不能为空 | 菜单类型为按钮时权限标识必填 | +| 401 | 当前会话未登录 | 未登录或Token无效 | +| 403 | 权限不足 | 没有相应的操作权限 | +| 500 | 系统异常 | 服务器内部错误 | + +--- + +## 使用建议 + +### 1. 菜单设计规范 + +- **层级结构**: 建议不超过3级菜单 +- **命名规范**: 使用有意义的英文名称作为路由名称 +- **权限标识**: 使用模块:功能:操作的格式 +- **图标使用**: 统一使用Element UI或其他图标库 + +### 2. 权限设计 + +- **粒度控制**: 按钮级别的权限控制 +- **角色分离**: 不同角色分配不同的菜单权限 +- **继承关系**: 子菜单权限依赖于父菜单权限 +- **缓存策略**: 用户权限信息缓存到前端 + +### 3. 性能优化 + +- **懒加载**: 菜单组件使用懒加载 +- **缓存机制**: 菜单数据缓存到本地存储 +- **权限缓存**: 权限信息缓存到内存中 +- **树形结构**: 使用高效的树形数据结构 + +### 4. 安全考虑 + +- **权限验证**: 前后端都要进行权限验证 +- **敏感操作**: 重要操作需要二次确认 +- **日志记录**: 记录权限变更操作日志 +- **最小权限**: 遵循最小权限原则 + +--- + +## 注意事项 + +1. **菜单删除**: 删除菜单前需要检查是否有子菜单和角色关联 +2. **权限继承**: 子菜单的权限依赖于父菜单 +3. **缓存更新**: 菜单权限变更后需要清理相关缓存 +4. **前端同步**: 菜单结构变更后前端需要同步更新 +5. **权限验证**: 前后端都需要进行权限验证 +6. **状态管理**: 菜单状态变更会影响用户访问 +7. **排序规则**: 菜单按照sorted字段升序排列 +8. **数据完整性**: 保证菜单数据的完整性和一致性 \ No newline at end of file diff --git a/api/permission/角色管理API.md b/api/permission/角色管理API.md new file mode 100644 index 0000000..673a18f --- /dev/null +++ b/api/permission/角色管理API.md @@ -0,0 +1,882 @@ +# 角色管理API + +## 概述 + +角色管理模块是权限系统的重要组成部分,负责管理系统角色、角色权限分配和用户角色关联。基于RBAC权限模型,实现了灵活的角色权限管理机制。 + +## 权限说明 + +角色管理接口需要相应的权限才能访问: + +| 操作 | 权限码 | 说明 | +|------|--------|------| +| 查询角色列表 | `system:role:list` | 查看角色列表权限 | +| 新增角色 | `system:role:add` | 新增角色权限 | +| 修改角色 | `system:role:edit` | 修改角色信息权限 | +| 删除角色 | `system:role:remove` | 删除角色权限 | +| 分配权限 | `system:role:assign` | 分配角色权限 | +| 用户授权 | `system:user:role` | 分配用户角色权限 | + +## 接口列表 + +### 1. 分页查询角色列表 + +**接口地址**: `GET /coder/sysRole/listPage` + +**接口描述**: 分页查询系统角色列表 + +**是否需要认证**: 是 + +**权限要求**: `system:role:list` + +**请求头**: +``` +Authorization: your-token-value +``` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 示例 | +|--------|------|------|------|------| +| pageNo | Integer | 否 | 页码 | 1 | +| pageSize | Integer | 否 | 每页大小 | 10 | +| roleName | String | 否 | 角色名称 | 管理员 | +| roleCode | String | 否 | 角色编码 | admin | +| roleStatus | String | 否 | 角色状态 | 0 | +| beginTime | String | 否 | 开始时间 | 2024-01-01 | +| endTime | String | 否 | 结束时间 | 2024-12-31 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "records": [ + { + "roleId": 1, + "roleName": "超级管理员", + "roleCode": "admin", + "roleStatus": "0", + "dataScope": "1", + "deptCheckStrictly": "1", + "menuCheckStrictly": "1", + "sorted": 1, + "remark": "超级管理员", + "createBy": "admin", + "createTime": "2024-01-01 10:00:00", + "updateBy": "admin", + "updateTime": "2024-01-01 10:00:00", + "menuIds": [1, 2, 3, 4, 5], + "deptIds": [100, 101, 102] + } + ], + "total": 1, + "size": 10, + "current": 1, + "pages": 1 + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysRole/listPage?pageNo=1&pageSize=10&roleName=管理员" \ + -H "Authorization: your-token-value" +``` + +--- + +### 2. 查询所有角色 + +**接口地址**: `GET /coder/sysRole/list` + +**接口描述**: 查询所有系统角色(不分页) + +**是否需要认证**: 是 + +**权限要求**: `system:role:list` + +**请求参数**: 同分页查询(除pageNo、pageSize外) + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "roleId": 1, + "roleName": "超级管理员", + "roleCode": "admin", + "roleStatus": "0", + "dataScope": "1", + "deptCheckStrictly": "1", + "menuCheckStrictly": "1", + "sorted": 1, + "remark": "超级管理员", + "createBy": "admin", + "createTime": "2024-01-01 10:00:00", + "updateBy": "admin", + "updateTime": "2024-01-01 10:00:00" + } + ], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysRole/list" \ + -H "Authorization: your-token-value" +``` + +--- + +### 3. 根据ID查询角色 + +**接口地址**: `GET /coder/sysRole/getById/{id}` + +**接口描述**: 根据角色ID查询角色详细信息 + +**是否需要认证**: 是 + +**权限要求**: `system:role:list` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 角色ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "roleId": 1, + "roleName": "超级管理员", + "roleCode": "admin", + "roleStatus": "0", + "dataScope": "1", + "deptCheckStrictly": "1", + "menuCheckStrictly": "1", + "sorted": 1, + "remark": "超级管理员", + "createBy": "admin", + "createTime": "2024-01-01 10:00:00", + "updateBy": "admin", + "updateTime": "2024-01-01 10:00:00", + "menuIds": [1, 2, 3, 4, 5], + "deptIds": [100, 101, 102] + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysRole/getById/1 \ + -H "Authorization: your-token-value" +``` + +--- + +### 4. 新增角色 + +**接口地址**: `POST /coder/sysRole/add` + +**接口描述**: 新增系统角色 + +**是否需要认证**: 是 + +**权限要求**: `system:role:add` + +**请求参数**: + +```json +{ + "roleName": "普通用户", + "roleCode": "user", + "roleStatus": "0", + "dataScope": "2", + "deptCheckStrictly": "1", + "menuCheckStrictly": "1", + "sorted": 2, + "remark": "普通用户角色", + "menuIds": [1, 2, 3], + "deptIds": [100, 101] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| roleName | String | 是 | 角色名称 | 不能为空,最长30字符 | +| roleCode | String | 是 | 角色编码 | 不能为空,最长100字符,唯一 | +| roleStatus | String | 是 | 角色状态 | 0-正常 1-停用 | +| dataScope | String | 否 | 数据范围 | 1-全部数据 2-自定义数据 3-本部门数据 4-本部门及以下数据 5-仅本人数据 | +| deptCheckStrictly | String | 否 | 部门树选择项是否关联显示 | 0-父子不互相关联显示 1-父子互相关联显示 | +| menuCheckStrictly | String | 否 | 菜单树选择项是否关联显示 | 0-父子不互相关联显示 1-父子互相关联显示 | +| sorted | Integer | 是 | 显示顺序 | 不能为空 | +| remark | String | 否 | 备注信息 | 最长200字符 | +| menuIds | Long[] | 否 | 菜单ID数组 | 有效的菜单ID | +| deptIds | Long[] | 否 | 部门ID数组 | 有效的部门ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "新增成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysRole/add \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "roleName": "普通用户", + "roleCode": "user", + "roleStatus": "0", + "dataScope": "2", + "deptCheckStrictly": "1", + "menuCheckStrictly": "1", + "sorted": 2, + "remark": "普通用户角色", + "menuIds": [1, 2, 3], + "deptIds": [100, 101] + }' +``` + +--- + +### 5. 获取最新排序号 + +**接口地址**: `GET /coder/sysRole/getSorted` + +**接口描述**: 获取角色的最新排序号 + +**是否需要认证**: 是 + +**权限要求**: `system:role:add` + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": 10, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysRole/getSorted \ + -H "Authorization: your-token-value" +``` + +--- + +### 6. 修改角色信息 + +**接口地址**: `POST /coder/sysRole/update` + +**接口描述**: 修改系统角色信息 + +**是否需要认证**: 是 + +**权限要求**: `system:role:edit` + +**请求参数**: + +```json +{ + "roleId": 1, + "roleName": "超级管理员", + "roleCode": "admin", + "roleStatus": "0", + "dataScope": "1", + "deptCheckStrictly": "1", + "menuCheckStrictly": "1", + "sorted": 1, + "remark": "超级管理员", + "menuIds": [1, 2, 3, 4, 5], + "deptIds": [100, 101, 102] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| roleId | Long | 是 | 角色ID | 必须是有效的角色ID | +| 其他参数 | - | - | 同新增角色 | - | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysRole/update \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "roleId": 1, + "roleName": "超级管理员", + "roleCode": "admin", + "roleStatus": "0", + "sorted": 1 + }' +``` + +--- + +### 7. 删除角色 + +**接口地址**: `POST /coder/sysRole/deleteById/{id}` + +**接口描述**: 根据ID删除角色 + +**是否需要认证**: 是 + +**权限要求**: `system:role:remove` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 角色ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysRole/deleteById/1 \ + -H "Authorization: your-token-value" +``` + +--- + +### 8. 批量删除角色 + +**接口地址**: `POST /coder/sysRole/batchDelete` + +**接口描述**: 批量删除角色 + +**是否需要认证**: 是 + +**权限要求**: `system:role:remove` + +**请求参数**: + +```json +{ + "ids": [1, 2, 3] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| ids | Long[] | 是 | 角色ID数组 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysRole/batchDelete \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "ids": [1, 2, 3] + }' +``` + +--- + +### 9. 修改角色状态 + +**接口地址**: `POST /coder/sysRole/updateStatus/{roleId}/{roleStatus}` + +**接口描述**: 修改角色状态(正常/停用) + +**是否需要认证**: 是 + +**权限要求**: `system:role:edit` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| roleId | Long | 是 | 角色ID | +| roleStatus | String | 是 | 角色状态(0-正常 1-停用) | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysRole/updateStatus/1/0 \ + -H "Authorization: your-token-value" +``` + +--- + +### 10. 查询正常角色穿梭框 + +**接口地址**: `GET /coder/sysRole/listNormalRole/{userId}` + +**接口描述**: 查询正常状态的角色用于穿梭框显示 + +**是否需要认证**: 是 + +**权限要求**: `system:user:role` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| userId | Long | 是 | 用户ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "label": "超级管理员", + "value": 1, + "parentId": "" + }, + { + "label": "普通用户", + "value": 2, + "parentId": "" + } + ], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysRole/listNormalRole/1 \ + -H "Authorization: your-token-value" +``` + +--- + +### 11. 分配用户角色 + +**接口地址**: `GET /coder/sysRole/assignUserRole/{userId}/{roleIds}` + +**接口描述**: 为用户分配角色 + +**是否需要认证**: 是 + +**权限要求**: `system:user:role` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| userId | Long | 是 | 用户ID | +| roleIds | String | 是 | 角色ID列表(逗号分隔) | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "分配成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysRole/assignUserRole/1/1,2,3 \ + -H "Authorization: your-token-value" +``` + +--- + +### 12. 获取角色下拉框 + +**接口地址**: `GET /coder/sysRole/listRoleElSelect` + +**接口描述**: 获取角色下拉框数据 + +**是否需要认证**: 是 + +**权限要求**: `system:role:list` + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "label": "超级管理员", + "value": 1 + }, + { + "label": "普通用户", + "value": 2 + } + ], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysRole/listRoleElSelect \ + -H "Authorization: your-token-value" +``` + +--- + +## 角色字段说明 + +### 基础字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| roleId | Long | 角色ID | 1 | +| roleName | String | 角色名称 | 超级管理员 | +| roleCode | String | 角色编码 | admin | +| roleStatus | String | 角色状态 | 0-正常 1-停用 | +| sorted | Integer | 显示顺序 | 1 | +| remark | String | 备注信息 | 超级管理员 | + +### 权限字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| dataScope | String | 数据权限范围 | 1-全部数据 2-自定义数据 3-本部门数据 4-本部门及以下数据 5-仅本人数据 | +| deptCheckStrictly | String | 部门树选择项是否关联显示 | 0-父子不互相关联显示 1-父子互相关联显示 | +| menuCheckStrictly | String | 菜单树选择项是否关联显示 | 0-父子不互相关联显示 1-父子互相关联显示 | + +### 关联字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| menuIds | Long[] | 菜单ID数组 | [1, 2, 3, 4, 5] | +| deptIds | Long[] | 部门ID数组 | [100, 101, 102] | + +--- + +## 数据字典 + +### 角色状态 (roleStatus) + +| 值 | 说明 | +|----|------| +| 0 | 正常 | +| 1 | 停用 | + +### 数据权限范围 (dataScope) + +| 值 | 说明 | +|----|------| +| 1 | 全部数据权限 | +| 2 | 自定数据权限 | +| 3 | 本部门数据权限 | +| 4 | 本部门及以下数据权限 | +| 5 | 仅本人数据权限 | + +### 树选择项关联显示 + +| 值 | 说明 | +|----|------| +| 0 | 父子不互相关联显示 | +| 1 | 父子互相关联显示 | + +--- + +## 权限验证机制 + +### 1. 角色权限验证 + +```java +// Sa-Token权限验证 +@SaCheckPermission("system:role:list") +public List list() { + // 业务逻辑 +} +``` + +### 2. 数据权限控制 + +```java +// 根据用户数据权限过滤数据 +public List getRolesByDataScope(Long userId) { + SysUser user = getCurrentUser(); + String dataScope = user.getDataScope(); + + switch (dataScope) { + case "1": // 全部数据权限 + return getAllRoles(); + case "2": // 自定义数据权限 + return getRolesByDeptIds(user.getDeptIds()); + case "3": // 本部门数据权限 + return getRolesByDept(user.getDeptId()); + case "4": // 本部门及以下数据权限 + return getRolesByDeptAndChildren(user.getDeptId()); + case "5": // 仅本人数据权限 + return getRolesByUserId(userId); + default: + return new ArrayList<>(); + } +} +``` + +### 3. 角色权限缓存 + +```java +// 缓存用户角色权限 +@Cacheable(value = "userRoles", key = "#userId") +public List getRolesByUserId(Long userId) { + return roleMapper.selectRolePermissionByUserId(userId); +} + +// 清除角色权限缓存 +@CacheEvict(value = "userRoles", key = "#userId") +public void clearUserRoleCache(Long userId) { + // 角色变更时清除缓存 +} +``` + +--- + +## 错误码说明 + +| 错误码 | 错误信息 | 说明 | +|--------|----------|------| +| 400 | 角色名称不能为空 | 角色名称为空 | +| 400 | 角色权限编码不能为空 | 角色编码为空 | +| 400 | 显示顺序不能为空 | 排序值为空 | +| 400 | 角色不存在 | 角色ID不存在 | +| 400 | 角色编码已存在 | 角色编码重复 | +| 400 | 角色已分配用户,不允许删除 | 角色已分配给用户时不能删除 | +| 400 | 不能删除超级管理员角色 | 超级管理员角色不能删除 | +| 400 | 不能停用超级管理员角色 | 超级管理员角色不能停用 | +| 400 | 用户不存在 | 用户ID不存在 | +| 400 | 角色ID列表不能为空 | 分配角色时角色ID为空 | +| 401 | 当前会话未登录 | 未登录或Token无效 | +| 403 | 权限不足 | 没有相应的操作权限 | +| 500 | 系统异常 | 服务器内部错误 | + +--- + +## RBAC权限模型 + +### 1. 基本概念 + +- **用户 (User)**: 系统的使用者 +- **角色 (Role)**: 权限的集合,连接用户和权限的桥梁 +- **权限 (Permission)**: 对系统资源的操作权限 +- **会话 (Session)**: 用户与系统的一次交互过程 + +### 2. 关系模型 + +``` +用户 ←→ 用户角色关系 ←→ 角色 ←→ 角色权限关系 ←→ 权限 +``` + +### 3. 数据库设计 + +```sql +-- 用户表 +CREATE TABLE sys_login_user ( + user_id BIGINT PRIMARY KEY, + login_name VARCHAR(32) UNIQUE NOT NULL, + user_name VARCHAR(32) NOT NULL, + -- 其他字段... +); + +-- 角色表 +CREATE TABLE sys_role ( + role_id BIGINT PRIMARY KEY, + role_name VARCHAR(32) NOT NULL, + role_code VARCHAR(32) UNIQUE NOT NULL, + role_status CHAR(1) DEFAULT '0', + -- 其他字段... +); + +-- 权限表(菜单表) +CREATE TABLE sys_menu ( + menu_id BIGINT PRIMARY KEY, + menu_name VARCHAR(64) NOT NULL, + auth VARCHAR(128), + -- 其他字段... +); + +-- 用户角色关联表 +CREATE TABLE sys_user_role ( + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + PRIMARY KEY (user_id, role_id) +); + +-- 角色权限关联表 +CREATE TABLE sys_role_menu ( + role_id BIGINT NOT NULL, + menu_id BIGINT NOT NULL, + PRIMARY KEY (role_id, menu_id) +); +``` + +--- + +## 使用建议 + +### 1. 角色设计原则 + +- **最小权限原则**: 每个角色只分配必要的权限 +- **职责分离**: 不同职责的操作分配给不同角色 +- **角色层次**: 建立角色层次结构,高级角色继承低级角色权限 +- **定期审查**: 定期审查角色权限,及时调整 + +### 2. 权限分配策略 + +- **默认无权限**: 新建角色默认无任何权限 +- **逐步授权**: 根据业务需要逐步授权 +- **权限组合**: 通过多个角色组合实现复杂权限需求 +- **临时授权**: 支持临时权限授权机制 + +### 3. 数据权限控制 + +- **部门数据权限**: 基于用户所属部门控制数据访问范围 +- **自定义数据权限**: 支持自定义数据访问范围 +- **数据隔离**: 确保不同用户只能访问授权范围内的数据 +- **审计日志**: 记录数据访问日志 + +### 4. 性能优化 + +- **权限缓存**: 将用户权限信息缓存到Redis +- **批量操作**: 支持批量分配和撤销权限 +- **索引优化**: 对关联表建立合适的索引 +- **分页查询**: 大数据量时使用分页查询 + +--- + +## 安全考虑 + +### 1. 权限验证 + +- **双重验证**: 前后端都要进行权限验证 +- **接口鉴权**: 每个接口都要验证权限 +- **数据鉴权**: 数据层面的权限验证 +- **操作鉴权**: 敏感操作的二次验证 + +### 2. 权限变更 + +- **变更日志**: 记录所有权限变更操作 +- **审批流程**: 重要权限变更需要审批 +- **通知机制**: 权限变更及时通知相关人员 +- **回滚机制**: 支持权限变更回滚 + +### 3. 会话安全 + +- **会话超时**: 设置会话超时时间 +- **单点登录**: 防止重复登录 +- **强制下线**: 支持强制用户下线 +- **设备绑定**: 支持设备级别的访问控制 + +--- + +## 注意事项 + +1. **角色删除**: 删除角色前需要检查是否有用户关联 +2. **权限继承**: 考虑角色之间的权限继承关系 +3. **缓存一致性**: 角色权限变更后需要清理相关缓存 +4. **数据完整性**: 保证角色权限数据的完整性 +5. **超级管理员**: 超级管理员角色不能删除和停用 +6. **权限验证**: 前后端都需要进行权限验证 +7. **审计日志**: 记录所有角色操作日志 +8. **性能考虑**: 大量用户时注意权限验证性能 +9. **数据权限**: 正确配置数据权限范围 +10. **定期清理**: 定期清理无效的角色和权限关联 \ No newline at end of file diff --git a/api/system/图片管理API.md b/api/system/图片管理API.md new file mode 100644 index 0000000..67339a6 --- /dev/null +++ b/api/system/图片管理API.md @@ -0,0 +1,871 @@ +# 图片管理API + +## 概述 + +图片管理模块专门用于管理系统中的图片资源,提供图片的上传、存储、展示和管理功能。支持多种图片格式,具备图片压缩、水印、缩略图生成等高级功能。 + +## 权限说明 + +图片管理接口需要相应的权限才能访问: + +| 操作 | 权限码 | 说明 | +|------|--------|------| +| 查询图片列表 | `system:picture:list` | 查看图片列表权限 | +| 上传图片 | `system:picture:upload` | 上传图片权限 | +| 删除图片 | `system:picture:remove` | 删除图片权限 | +| 编辑图片 | `system:picture:edit` | 编辑图片信息权限 | + +## 接口列表 + +### 1. 分页查询图片列表 + +**接口地址**: `GET /coder/sysPicture/listPage` + +**接口描述**: 分页查询系统图片列表 + +**是否需要认证**: 是 + +**权限要求**: `system:picture:list` + +**请求头**: +``` +Authorization: Bearer your-token-value +``` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 示例 | +|--------|------|------|------|------| +| pageNo | Integer | 否 | 页码 | 1 | +| pageSize | Integer | 否 | 每页大小 | 10 | +| pictureName | String | 否 | 图片名称 | banner.jpg | +| pictureType | String | 否 | 图片类型 | jpg | +| albumName | String | 否 | 相册名称 | banner | +| pictureStatus | String | 否 | 图片状态 | 0 | +| beginTime | String | 否 | 开始时间 | 2024-01-01 | +| endTime | String | 否 | 结束时间 | 2024-12-31 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "records": [ + { + "pictureId": 1, + "pictureName": "banner_20240705_001.jpg", + "originalName": "主页横幅.jpg", + "picturePath": "/upload/pictures/banner/2024/07/05/banner_20240705_001.jpg", + "pictureUrl": "http://localhost:18099/upload/pictures/banner/2024/07/05/banner_20240705_001.jpg", + "thumbnailUrl": "http://localhost:18099/upload/pictures/banner/2024/07/05/thumb_banner_20240705_001.jpg", + "albumName": "banner", + "pictureSize": 2048000, + "pictureType": "jpg", + "pictureFormat": "JPEG", + "width": 1920, + "height": 1080, + "pictureStatus": "0", + "isWatermark": "0", + "uploadUserId": 1, + "uploadUserName": "admin", + "description": "主页横幅图片", + "tags": "横幅,主页,展示", + "viewCount": 100, + "downloadCount": 10, + "remark": "网站主页横幅", + "createBy": "admin", + "createTime": "2024-07-05 10:00:00", + "updateBy": "admin", + "updateTime": "2024-07-05 10:00:00" + } + ], + "total": 1, + "size": 10, + "current": 1, + "pages": 1 + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysPicture/listPage?pageNo=1&pageSize=10&albumName=banner" \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 2. 查询所有图片 + +**接口地址**: `GET /coder/sysPicture/list` + +**接口描述**: 查询所有系统图片(不分页) + +**是否需要认证**: 是 + +**权限要求**: `system:picture:list` + +**请求参数**: 同分页查询(除pageNo、pageSize外) + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "pictureId": 1, + "pictureName": "banner_20240705_001.jpg", + "originalName": "主页横幅.jpg", + "picturePath": "/upload/pictures/banner/2024/07/05/banner_20240705_001.jpg", + "pictureUrl": "http://localhost:18099/upload/pictures/banner/2024/07/05/banner_20240705_001.jpg", + "thumbnailUrl": "http://localhost:18099/upload/pictures/banner/2024/07/05/thumb_banner_20240705_001.jpg", + "albumName": "banner", + "pictureSize": 2048000, + "pictureType": "jpg", + "pictureFormat": "JPEG", + "width": 1920, + "height": 1080, + "pictureStatus": "0", + "isWatermark": "0", + "uploadUserId": 1, + "uploadUserName": "admin", + "description": "主页横幅图片", + "tags": "横幅,主页,展示", + "viewCount": 100, + "downloadCount": 10, + "remark": "网站主页横幅", + "createBy": "admin", + "createTime": "2024-07-05 10:00:00", + "updateBy": "admin", + "updateTime": "2024-07-05 10:00:00" + } + ], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysPicture/list?albumName=banner" \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 3. 根据ID查询图片 + +**接口地址**: `GET /coder/sysPicture/getById/{id}` + +**接口描述**: 根据图片ID查询图片详细信息 + +**是否需要认证**: 是 + +**权限要求**: `system:picture:list` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 图片ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "pictureId": 1, + "pictureName": "banner_20240705_001.jpg", + "originalName": "主页横幅.jpg", + "picturePath": "/upload/pictures/banner/2024/07/05/banner_20240705_001.jpg", + "pictureUrl": "http://localhost:18099/upload/pictures/banner/2024/07/05/banner_20240705_001.jpg", + "thumbnailUrl": "http://localhost:18099/upload/pictures/banner/2024/07/05/thumb_banner_20240705_001.jpg", + "albumName": "banner", + "pictureSize": 2048000, + "pictureType": "jpg", + "pictureFormat": "JPEG", + "width": 1920, + "height": 1080, + "pictureStatus": "0", + "isWatermark": "0", + "uploadUserId": 1, + "uploadUserName": "admin", + "description": "主页横幅图片", + "tags": "横幅,主页,展示", + "viewCount": 100, + "downloadCount": 10, + "remark": "网站主页横幅", + "createBy": "admin", + "createTime": "2024-07-05 10:00:00", + "updateBy": "admin", + "updateTime": "2024-07-05 10:00:00" + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysPicture/getById/1 \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 4. 新增图片记录 + +**接口地址**: `POST /coder/sysPicture/add` + +**接口描述**: 新增系统图片记录 + +**是否需要认证**: 是 + +**权限要求**: `system:picture:upload` + +**请求参数**: + +```json +{ + "pictureName": "product_20240705_001.jpg", + "originalName": "产品展示图.jpg", + "picturePath": "/upload/pictures/product/2024/07/05/product_20240705_001.jpg", + "pictureUrl": "http://localhost:18099/upload/pictures/product/2024/07/05/product_20240705_001.jpg", + "thumbnailUrl": "http://localhost:18099/upload/pictures/product/2024/07/05/thumb_product_20240705_001.jpg", + "albumName": "product", + "pictureSize": 1536000, + "pictureType": "jpg", + "pictureFormat": "JPEG", + "width": 1200, + "height": 800, + "pictureStatus": "0", + "isWatermark": "1", + "uploadUserId": 1, + "uploadUserName": "admin", + "description": "产品展示图片", + "tags": "产品,展示,商品", + "remark": "商品详情页图片" +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| pictureName | String | 是 | 存储图片名 | 不能为空 | +| originalName | String | 是 | 原始图片名 | 不能为空 | +| picturePath | String | 是 | 图片相对路径 | 不能为空 | +| pictureUrl | String | 是 | 图片访问URL | 不能为空 | +| thumbnailUrl | String | 否 | 缩略图URL | 可为空 | +| albumName | String | 是 | 相册名称 | 不能为空 | +| pictureSize | Long | 是 | 图片大小 | 必须大于0 | +| pictureType | String | 是 | 图片类型 | 不能为空 | +| pictureFormat | String | 是 | 图片格式 | 不能为空 | +| width | Integer | 否 | 图片宽度 | 大于0 | +| height | Integer | 否 | 图片高度 | 大于0 | +| pictureStatus | String | 是 | 图片状态 | 0-正常 1-删除 | +| isWatermark | String | 否 | 是否水印 | 0-否 1-是 | +| uploadUserId | Long | 否 | 上传用户ID | 有效的用户ID | +| uploadUserName | String | 否 | 上传用户名 | 可为空 | +| description | String | 否 | 图片描述 | 最长500字符 | +| tags | String | 否 | 图片标签 | 逗号分隔 | +| remark | String | 否 | 备注信息 | 最长200字符 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "新增成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysPicture/add \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token-value" \ + -d '{ + "pictureName": "product_20240705_001.jpg", + "originalName": "产品展示图.jpg", + "picturePath": "/upload/pictures/product/2024/07/05/product_20240705_001.jpg", + "pictureUrl": "http://localhost:18099/upload/pictures/product/2024/07/05/product_20240705_001.jpg", + "albumName": "product", + "pictureSize": 1536000, + "pictureType": "jpg", + "pictureFormat": "JPEG", + "width": 1200, + "height": 800, + "pictureStatus": "0", + "description": "产品展示图片", + "tags": "产品,展示,商品" + }' +``` + +--- + +### 5. 修改图片信息 + +**接口地址**: `POST /coder/sysPicture/update` + +**接口描述**: 修改系统图片记录信息 + +**是否需要认证**: 是 + +**权限要求**: `system:picture:edit` + +**请求参数**: + +```json +{ + "pictureId": 1, + "pictureName": "product_20240705_001.jpg", + "originalName": "产品展示图.jpg", + "picturePath": "/upload/pictures/product/2024/07/05/product_20240705_001.jpg", + "pictureUrl": "http://localhost:18099/upload/pictures/product/2024/07/05/product_20240705_001.jpg", + "thumbnailUrl": "http://localhost:18099/upload/pictures/product/2024/07/05/thumb_product_20240705_001.jpg", + "albumName": "product", + "pictureSize": 1536000, + "pictureType": "jpg", + "pictureFormat": "JPEG", + "width": 1200, + "height": 800, + "pictureStatus": "0", + "isWatermark": "1", + "uploadUserId": 1, + "uploadUserName": "admin", + "description": "产品展示图片(已更新)", + "tags": "产品,展示,商品,新品", + "remark": "商品详情页图片(已更新)" +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| pictureId | Long | 是 | 图片ID | 必须是有效的图片ID | +| 其他参数 | - | - | 同新增图片记录 | - | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysPicture/update \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token-value" \ + -d '{ + "pictureId": 1, + "description": "产品展示图片(已更新)", + "tags": "产品,展示,商品,新品" + }' +``` + +--- + +### 6. 删除图片 + +**接口地址**: `POST /coder/sysPicture/deleteById/{id}` + +**接口描述**: 根据ID删除图片记录和物理文件 + +**是否需要认证**: 是 + +**权限要求**: `system:picture:remove` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 图片ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysPicture/deleteById/1 \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 7. 批量删除图片 + +**接口地址**: `POST /coder/sysPicture/batchDelete` + +**接口描述**: 批量删除图片记录和物理文件 + +**是否需要认证**: 是 + +**权限要求**: `system:picture:remove` + +**请求参数**: + +```json +{ + "ids": [1, 2, 3] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| ids | Long[] | 是 | 图片ID数组 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysPicture/batchDelete \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token-value" \ + -d '{ + "ids": [1, 2, 3] + }' +``` + +--- + +## 图片字段说明 + +### 基础字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| pictureId | Long | 图片ID | 1 | +| pictureName | String | 存储图片名 | banner_20240705_001.jpg | +| originalName | String | 原始图片名 | 主页横幅.jpg | +| picturePath | String | 图片相对路径 | /upload/pictures/banner/2024/07/05/banner_20240705_001.jpg | +| pictureUrl | String | 图片访问URL | http://localhost:18099/upload/pictures/banner/2024/07/05/banner_20240705_001.jpg | +| thumbnailUrl | String | 缩略图URL | http://localhost:18099/upload/pictures/banner/2024/07/05/thumb_banner_20240705_001.jpg | + +### 分类字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| albumName | String | 相册名称 | banner | +| pictureType | String | 图片扩展名 | jpg | +| pictureFormat | String | 图片格式 | JPEG | +| pictureSize | Long | 图片大小(字节) | 2048000 | + +### 尺寸字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| width | Integer | 图片宽度(像素) | 1920 | +| height | Integer | 图片高度(像素) | 1080 | + +### 状态字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| pictureStatus | String | 图片状态 | 0-正常 1-删除 | +| isWatermark | String | 是否有水印 | 0-否 1-是 | +| uploadUserId | Long | 上传用户ID | 1 | +| uploadUserName | String | 上传用户名 | admin | + +### 描述字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| description | String | 图片描述 | 主页横幅图片 | +| tags | String | 图片标签 | 横幅,主页,展示 | +| remark | String | 备注信息 | 网站主页横幅 | + +### 统计字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| viewCount | Integer | 查看次数 | 100 | +| downloadCount | Integer | 下载次数 | 10 | + +--- + +## 数据字典 + +### 图片状态 (pictureStatus) + +| 值 | 说明 | +|----|------| +| 0 | 正常 | +| 1 | 已删除 | + +### 是否水印 (isWatermark) + +| 值 | 说明 | +|----|------| +| 0 | 无水印 | +| 1 | 有水印 | + +### 支持的图片格式 + +| 格式 | 扩展名 | MIME类型 | 说明 | +|------|--------|----------|------| +| JPEG | jpg, jpeg | image/jpeg | 有损压缩,适合照片 | +| PNG | png | image/png | 无损压缩,支持透明 | +| GIF | gif | image/gif | 支持动画 | +| WebP | webp | image/webp | 现代格式,压缩率高 | +| BMP | bmp | image/bmp | 位图格式 | +| TIFF | tiff, tif | image/tiff | 高质量图像 | + +### 相册分类 + +| 相册名称 | 说明 | 用途 | +|----------|------|------| +| banner | 横幅图片 | 网站横幅展示 | +| product | 产品图片 | 商品展示 | +| avatar | 头像图片 | 用户头像 | +| gallery | 图库图片 | 相册展示 | +| article | 文章图片 | 文章配图 | +| icon | 图标图片 | 小图标 | +| background | 背景图片 | 页面背景 | + +--- + +## 图片处理功能 + +### 1. 图片上传处理 + +```java +@Service +public class PictureUploadService { + + /** + * 上传图片 + */ + public PictureUploadResult uploadPicture(MultipartFile file, String album) { + // 1. 图片校验 + validatePicture(file); + + // 2. 生成文件名 + String fileName = generatePictureName(file.getOriginalFilename()); + + // 3. 保存原图 + String originalPath = savePicture(file, album, fileName); + + // 4. 生成缩略图 + String thumbnailPath = generateThumbnail(originalPath, album, fileName); + + // 5. 添加水印 + if (needWatermark(album)) { + addWatermark(originalPath); + } + + // 6. 获取图片信息 + BufferedImage image = ImageIO.read(new File(originalPath)); + int width = image.getWidth(); + int height = image.getHeight(); + + // 7. 生成访问URL + String pictureUrl = generatePictureUrl(album, fileName); + String thumbnailUrl = generateThumbnailUrl(album, fileName); + + // 8. 返回结果 + return PictureUploadResult.builder() + .pictureName(fileName) + .originalName(file.getOriginalFilename()) + .picturePath(originalPath) + .pictureUrl(pictureUrl) + .thumbnailUrl(thumbnailUrl) + .pictureSize(file.getSize()) + .pictureType(getFileExtension(fileName)) + .pictureFormat(getImageFormat(file)) + .width(width) + .height(height) + .build(); + } + + /** + * 生成缩略图 + */ + private String generateThumbnail(String originalPath, String album, String fileName) { + try { + BufferedImage originalImage = ImageIO.read(new File(originalPath)); + + // 计算缩略图尺寸 + int thumbnailWidth = 200; + int thumbnailHeight = 150; + + // 保持宽高比 + double ratio = Math.min((double) thumbnailWidth / originalImage.getWidth(), + (double) thumbnailHeight / originalImage.getHeight()); + int width = (int) (originalImage.getWidth() * ratio); + int height = (int) (originalImage.getHeight() * ratio); + + // 生成缩略图 + BufferedImage thumbnailImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = thumbnailImage.createGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2d.drawImage(originalImage, 0, 0, width, height, null); + g2d.dispose(); + + // 保存缩略图 + String thumbnailPath = getThumbnailPath(album, fileName); + ImageIO.write(thumbnailImage, "jpg", new File(thumbnailPath)); + + return thumbnailPath; + } catch (IOException e) { + throw new BusinessException("生成缩略图失败", e); + } + } + + /** + * 添加水印 + */ + private void addWatermark(String imagePath) { + try { + BufferedImage originalImage = ImageIO.read(new File(imagePath)); + Graphics2D g2d = originalImage.createGraphics(); + + // 设置水印属性 + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)); + g2d.setColor(Color.LIGHT_GRAY); + g2d.setFont(new Font("Arial", Font.BOLD, 20)); + + // 计算水印位置 + FontMetrics fm = g2d.getFontMetrics(); + String watermarkText = "© Your Company"; + int x = originalImage.getWidth() - fm.stringWidth(watermarkText) - 10; + int y = originalImage.getHeight() - fm.getHeight() + fm.getAscent() - 10; + + // 绘制水印 + g2d.drawString(watermarkText, x, y); + g2d.dispose(); + + // 保存带水印的图片 + ImageIO.write(originalImage, "jpg", new File(imagePath)); + } catch (IOException e) { + throw new BusinessException("添加水印失败", e); + } + } +} +``` + +### 2. 图片压缩 + +```java +/** + * 图片压缩 + */ +public void compressPicture(String inputPath, String outputPath, float quality) { + try { + BufferedImage image = ImageIO.read(new File(inputPath)); + + // 创建输出流 + FileOutputStream fos = new FileOutputStream(outputPath); + + // 获取JPEG写入器 + Iterator writers = ImageIO.getImageWritersByFormatName("jpg"); + ImageWriter writer = writers.next(); + + // 设置输出 + ImageOutputStream ios = ImageIO.createImageOutputStream(fos); + writer.setOutput(ios); + + // 设置压缩参数 + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(quality); // 压缩质量 0.0-1.0 + + // 写入图片 + writer.write(null, new IIOImage(image, null, null), param); + + // 清理资源 + ios.close(); + fos.close(); + writer.dispose(); + } catch (IOException e) { + throw new BusinessException("图片压缩失败", e); + } +} +``` + +### 3. 图片格式转换 + +```java +/** + * 图片格式转换 + */ +public void convertPictureFormat(String inputPath, String outputPath, String format) { + try { + BufferedImage image = ImageIO.read(new File(inputPath)); + + // 如果转换为JPEG,需要处理透明背景 + if ("jpg".equalsIgnoreCase(format) || "jpeg".equalsIgnoreCase(format)) { + BufferedImage jpegImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = jpegImage.createGraphics(); + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, image.getWidth(), image.getHeight()); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + image = jpegImage; + } + + // 保存转换后的图片 + ImageIO.write(image, format, new File(outputPath)); + } catch (IOException e) { + throw new BusinessException("图片格式转换失败", e); + } +} +``` + +--- + +## 错误码说明 + +| 错误码 | 错误信息 | 说明 | +|--------|----------|------| +| 400 | 图片不能为空 | 上传图片为空 | +| 400 | 图片大小不能超过{size}MB | 图片大小超过限制 | +| 400 | 不支持的图片格式 | 图片格式不支持 | +| 400 | 图片尺寸超过限制 | 图片宽高超过限制 | +| 400 | 图片名称不能为空 | 图片名称为空 | +| 400 | 相册名称不能为空 | 相册名称为空 | +| 400 | 图片不存在 | 图片ID不存在 | +| 400 | 图片已被删除 | 图片状态为已删除 | +| 401 | 当前会话未登录 | 未登录或Token无效 | +| 403 | 权限不足 | 没有相应的操作权限 | +| 500 | 图片上传失败 | 图片保存到磁盘失败 | +| 500 | 缩略图生成失败 | 缩略图生成过程失败 | +| 500 | 水印添加失败 | 水印添加过程失败 | +| 500 | 图片压缩失败 | 图片压缩过程失败 | +| 500 | 图片删除失败 | 物理文件删除失败 | + +--- + +## 安全特性 + +### 1. 图片验证 + +- **格式验证**: 验证图片格式和MIME类型 +- **尺寸验证**: 验证图片宽高限制 +- **内容验证**: 验证图片内容安全性 +- **病毒扫描**: 对上传图片进行病毒扫描 + +### 2. 访问控制 + +- **权限验证**: 验证用户访问权限 +- **防盗链**: 防止图片被盗链 +- **水印保护**: 添加水印保护版权 +- **下载限制**: 限制图片下载次数 + +### 3. 存储安全 + +- **路径安全**: 防止路径遍历攻击 +- **文件重命名**: 重命名避免冲突 +- **备份策略**: 重要图片定期备份 +- **权限设置**: 设置合适的文件权限 + +--- + +## 性能优化 + +### 1. 图片处理优化 + +- **异步处理**: 图片压缩和水印添加异步处理 +- **批量处理**: 支持批量图片处理 +- **缓存策略**: 缓存处理后的图片 +- **CDN加速**: 使用CDN加速图片访问 + +### 2. 存储优化 + +- **分目录存储**: 按相册和日期分目录存储 +- **格式优化**: 自动选择最优图片格式 +- **压缩存储**: 自动压缩降低存储空间 +- **重复检测**: 检测重复图片避免冗余 + +### 3. 访问优化 + +- **懒加载**: 图片懒加载技术 +- **响应式图片**: 根据设备提供不同尺寸 +- **WebP支持**: 支持WebP格式提高性能 +- **预加载**: 关键图片预加载 + +--- + +## 使用建议 + +### 1. 图片命名规范 + +- **唯一性**: 确保图片名唯一 +- **时间戳**: 包含时间戳信息 +- **分类标识**: 包含相册分类信息 +- **版本控制**: 支持图片版本管理 + +### 2. 相册管理 + +- **分类管理**: 按用途分类管理图片 +- **权限控制**: 不同相册设置不同权限 +- **容量限制**: 设置相册容量限制 +- **定期清理**: 定期清理无用图片 + +### 3. 图片优化 + +- **尺寸适配**: 根据用途选择合适尺寸 +- **格式选择**: 根据内容选择最优格式 +- **质量平衡**: 平衡图片质量和文件大小 +- **缓存策略**: 设置合适的缓存时间 + +--- + +## 注意事项 + +1. **图片安全**: 严格验证上传图片格式和内容 +2. **版权保护**: 添加水印保护图片版权 +3. **存储管理**: 定期清理无效和重复图片 +4. **性能考虑**: 大图片上传时注意性能影响 +5. **备份策略**: 重要图片需要备份 +6. **访问控制**: 合理设置图片访问权限 +7. **格式支持**: 根据需要支持不同图片格式 +8. **缩略图**: 为提高加载速度生成缩略图 +9. **压缩处理**: 适当压缩减少存储空间 +10. **监控告警**: 监控存储空间和访问异常 \ No newline at end of file diff --git a/api/system/文件管理API.md b/api/system/文件管理API.md new file mode 100644 index 0000000..cde6262 --- /dev/null +++ b/api/system/文件管理API.md @@ -0,0 +1,867 @@ +# 文件管理API + +## 概述 + +文件管理模块提供文件上传、下载、存储管理等功能。支持多种文件类型,提供文件记录管理和批量操作功能。分为通用文件上传接口和系统文件记录管理两部分。 + +## 权限说明 + +文件管理接口需要相应的权限才能访问: + +| 操作 | 权限码 | 说明 | +|------|--------|------| +| 查询文件列表 | `system:file:list` | 查看文件列表权限 | +| 上传文件 | `system:file:upload` | 上传文件权限 | +| 下载文件 | `system:file:download` | 下载文件权限 | +| 删除文件 | `system:file:remove` | 删除文件权限 | + +## 文件上传接口 + +### 1. 上传文件(需认证) + +**接口地址**: `POST /coder/file/uploadFile/{fileSize}/{folderName}/{fileParam}` + +**接口描述**: 上传文件到服务器(需要登录认证) + +**是否需要认证**: 是 + +**权限要求**: `system:file:upload` + +**请求头**: +``` +Authorization: Bearer your-token-value +Content-Type: multipart/form-data +``` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | 示例 | +|--------|------|------|------|------| +| fileSize | Integer | 是 | 文件大小限制(MB) | 10 | +| folderName | String | 是 | 存储文件夹名称 | avatar | +| fileParam | String | 是 | 文件参数名 | file | + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | File | 是 | 上传的文件 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "fileName": "avatar.jpg", + "originalName": "my-avatar.jpg", + "filePath": "/upload/avatar/2024/07/05/avatar_20240705_001.jpg", + "fileUrl": "http://localhost:18099/upload/avatar/2024/07/05/avatar_20240705_001.jpg", + "fileSize": 1024000, + "fileType": "jpg", + "uploadTime": "2024-07-05 10:00:00" + }, + "traceId": "trace-123456" +} +``` + +**响应参数说明**: + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| fileName | String | 存储文件名 | +| originalName | String | 原始文件名 | +| filePath | String | 文件相对路径 | +| fileUrl | String | 文件访问URL | +| fileSize | Long | 文件大小(字节) | +| fileType | String | 文件类型 | +| uploadTime | String | 上传时间 | + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/file/uploadFile/10/avatar/file \ + -H "Authorization: Bearer your-token-value" \ + -F "file=@avatar.jpg" +``` + +**前端使用示例**: + +```javascript +// 使用FormData上传文件 +function uploadFile(file, folder, maxSize) { + const formData = new FormData(); + formData.append('file', file); + + fetch(`/coder/file/uploadFile/${maxSize}/${folder}/file`, { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('token') + }, + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.status === 200) { + console.log('上传成功:', data.data.fileUrl); + } else { + console.error('上传失败:', data.msg); + } + }); +} +``` + +--- + +### 2. 匿名上传文件 + +**接口地址**: `POST /coder/file/uploadAnyFile/{fileSize}/{folderName}/{fileParam}` + +**接口描述**: 匿名上传文件到服务器(无需登录认证) + +**是否需要认证**: 否 + +**权限要求**: 无 + +**路径参数**: 同上传文件接口 + +**请求参数**: 同上传文件接口 + +**响应示例**: 同上传文件接口 + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/file/uploadAnyFile/5/temp/file \ + -F "file=@document.pdf" +``` + +**使用场景**: +- 用户注册时上传头像 +- 访客上传临时文件 +- 公开资源上传 + +--- + +## 系统文件记录管理接口 + +### 1. 分页查询文件列表 + +**接口地址**: `GET /coder/sysFile/listPage` + +**接口描述**: 分页查询系统文件记录列表 + +**是否需要认证**: 是 + +**权限要求**: `system:file:list` + +**请求头**: +``` +Authorization: Bearer your-token-value +``` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 示例 | +|--------|------|------|------|------| +| pageNo | Integer | 否 | 页码 | 1 | +| pageSize | Integer | 否 | 每页大小 | 10 | +| fileName | String | 否 | 文件名称 | avatar.jpg | +| fileType | String | 否 | 文件类型 | jpg | +| folderName | String | 否 | 文件夹名称 | avatar | +| beginTime | String | 否 | 开始时间 | 2024-01-01 | +| endTime | String | 否 | 结束时间 | 2024-12-31 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "records": [ + { + "fileId": 1, + "fileName": "avatar_20240705_001.jpg", + "originalName": "my-avatar.jpg", + "filePath": "/upload/avatar/2024/07/05/avatar_20240705_001.jpg", + "fileUrl": "http://localhost:18099/upload/avatar/2024/07/05/avatar_20240705_001.jpg", + "folderName": "avatar", + "fileSize": 1024000, + "fileType": "jpg", + "contentType": "image/jpeg", + "fileStatus": "0", + "uploadUserId": 1, + "uploadUserName": "admin", + "remark": "用户头像", + "createBy": "admin", + "createTime": "2024-07-05 10:00:00", + "updateBy": "admin", + "updateTime": "2024-07-05 10:00:00" + } + ], + "total": 1, + "size": 10, + "current": 1, + "pages": 1 + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysFile/listPage?pageNo=1&pageSize=10&fileName=avatar" \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 2. 查询所有文件 + +**接口地址**: `GET /coder/sysFile/list` + +**接口描述**: 查询所有系统文件记录(不分页) + +**是否需要认证**: 是 + +**权限要求**: `system:file:list` + +**请求参数**: 同分页查询(除pageNo、pageSize外) + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "fileId": 1, + "fileName": "avatar_20240705_001.jpg", + "originalName": "my-avatar.jpg", + "filePath": "/upload/avatar/2024/07/05/avatar_20240705_001.jpg", + "fileUrl": "http://localhost:18099/upload/avatar/2024/07/05/avatar_20240705_001.jpg", + "folderName": "avatar", + "fileSize": 1024000, + "fileType": "jpg", + "contentType": "image/jpeg", + "fileStatus": "0", + "uploadUserId": 1, + "uploadUserName": "admin", + "remark": "用户头像", + "createBy": "admin", + "createTime": "2024-07-05 10:00:00", + "updateBy": "admin", + "updateTime": "2024-07-05 10:00:00" + } + ], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysFile/list?fileType=jpg" \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 3. 根据ID查询文件 + +**接口地址**: `GET /coder/sysFile/getById/{id}` + +**接口描述**: 根据文件ID查询文件详细信息 + +**是否需要认证**: 是 + +**权限要求**: `system:file:list` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 文件ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "fileId": 1, + "fileName": "avatar_20240705_001.jpg", + "originalName": "my-avatar.jpg", + "filePath": "/upload/avatar/2024/07/05/avatar_20240705_001.jpg", + "fileUrl": "http://localhost:18099/upload/avatar/2024/07/05/avatar_20240705_001.jpg", + "folderName": "avatar", + "fileSize": 1024000, + "fileType": "jpg", + "contentType": "image/jpeg", + "fileStatus": "0", + "uploadUserId": 1, + "uploadUserName": "admin", + "remark": "用户头像", + "createBy": "admin", + "createTime": "2024-07-05 10:00:00", + "updateBy": "admin", + "updateTime": "2024-07-05 10:00:00" + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysFile/getById/1 \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 4. 新增文件记录 + +**接口地址**: `POST /coder/sysFile/add` + +**接口描述**: 新增系统文件记录 + +**是否需要认证**: 是 + +**权限要求**: `system:file:upload` + +**请求参数**: + +```json +{ + "fileName": "document_20240705_001.pdf", + "originalName": "重要文档.pdf", + "filePath": "/upload/documents/2024/07/05/document_20240705_001.pdf", + "fileUrl": "http://localhost:18099/upload/documents/2024/07/05/document_20240705_001.pdf", + "folderName": "documents", + "fileSize": 2048000, + "fileType": "pdf", + "contentType": "application/pdf", + "fileStatus": "0", + "uploadUserId": 1, + "uploadUserName": "admin", + "remark": "重要文档" +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| fileName | String | 是 | 存储文件名 | 不能为空 | +| originalName | String | 是 | 原始文件名 | 不能为空 | +| filePath | String | 是 | 文件相对路径 | 不能为空 | +| fileUrl | String | 是 | 文件访问URL | 不能为空 | +| folderName | String | 是 | 文件夹名称 | 不能为空 | +| fileSize | Long | 是 | 文件大小 | 必须大于0 | +| fileType | String | 是 | 文件类型 | 不能为空 | +| contentType | String | 是 | MIME类型 | 不能为空 | +| fileStatus | String | 是 | 文件状态 | 0-正常 1-删除 | +| uploadUserId | Long | 否 | 上传用户ID | 有效的用户ID | +| uploadUserName | String | 否 | 上传用户名 | 可为空 | +| remark | String | 否 | 备注信息 | 最长200字符 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "新增成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysFile/add \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token-value" \ + -d '{ + "fileName": "document_20240705_001.pdf", + "originalName": "重要文档.pdf", + "filePath": "/upload/documents/2024/07/05/document_20240705_001.pdf", + "fileUrl": "http://localhost:18099/upload/documents/2024/07/05/document_20240705_001.pdf", + "folderName": "documents", + "fileSize": 2048000, + "fileType": "pdf", + "contentType": "application/pdf", + "fileStatus": "0", + "uploadUserId": 1, + "uploadUserName": "admin", + "remark": "重要文档" + }' +``` + +--- + +### 5. 修改文件信息 + +**接口地址**: `POST /coder/sysFile/update` + +**接口描述**: 修改系统文件记录信息 + +**是否需要认证**: 是 + +**权限要求**: `system:file:edit` + +**请求参数**: + +```json +{ + "fileId": 1, + "fileName": "document_20240705_001.pdf", + "originalName": "重要文档.pdf", + "filePath": "/upload/documents/2024/07/05/document_20240705_001.pdf", + "fileUrl": "http://localhost:18099/upload/documents/2024/07/05/document_20240705_001.pdf", + "folderName": "documents", + "fileSize": 2048000, + "fileType": "pdf", + "contentType": "application/pdf", + "fileStatus": "0", + "uploadUserId": 1, + "uploadUserName": "admin", + "remark": "重要文档(已更新)" +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| fileId | Long | 是 | 文件ID | 必须是有效的文件ID | +| 其他参数 | - | - | 同新增文件记录 | - | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysFile/update \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token-value" \ + -d '{ + "fileId": 1, + "fileName": "document_20240705_001.pdf", + "originalName": "重要文档.pdf", + "remark": "重要文档(已更新)" + }' +``` + +--- + +### 6. 删除文件 + +**接口地址**: `POST /coder/sysFile/deleteById/{id}` + +**接口描述**: 根据ID删除文件记录和物理文件 + +**是否需要认证**: 是 + +**权限要求**: `system:file:remove` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 文件ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysFile/deleteById/1 \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 7. 批量删除文件 + +**接口地址**: `POST /coder/sysFile/batchDelete` + +**接口描述**: 批量删除文件记录和物理文件 + +**是否需要认证**: 是 + +**权限要求**: `system:file:remove` + +**请求参数**: + +```json +{ + "ids": [1, 2, 3] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| ids | Long[] | 是 | 文件ID数组 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysFile/batchDelete \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token-value" \ + -d '{ + "ids": [1, 2, 3] + }' +``` + +--- + +## 文件字段说明 + +### 基础字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| fileId | Long | 文件ID | 1 | +| fileName | String | 存储文件名 | avatar_20240705_001.jpg | +| originalName | String | 原始文件名 | my-avatar.jpg | +| filePath | String | 文件相对路径 | /upload/avatar/2024/07/05/avatar_20240705_001.jpg | +| fileUrl | String | 文件访问URL | http://localhost:18099/upload/avatar/2024/07/05/avatar_20240705_001.jpg | + +### 分类字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| folderName | String | 文件夹名称 | avatar | +| fileType | String | 文件扩展名 | jpg | +| contentType | String | MIME类型 | image/jpeg | +| fileSize | Long | 文件大小(字节) | 1024000 | + +### 状态字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| fileStatus | String | 文件状态 | 0-正常 1-删除 | +| uploadUserId | Long | 上传用户ID | 1 | +| uploadUserName | String | 上传用户名 | admin | +| remark | String | 备注信息 | 用户头像 | + +--- + +## 数据字典 + +### 文件状态 (fileStatus) + +| 值 | 说明 | +|----|------| +| 0 | 正常 | +| 1 | 已删除 | + +### 常见文件类型 + +| 文件类型 | 扩展名 | MIME类型 | 说明 | +|----------|--------|----------|------| +| 图片 | jpg, jpeg, png, gif, bmp | image/* | 图片文件 | +| 文档 | pdf, doc, docx, xls, xlsx, ppt, pptx | application/* | 办公文档 | +| 音频 | mp3, wav, flac, aac | audio/* | 音频文件 | +| 视频 | mp4, avi, mov, wmv | video/* | 视频文件 | +| 压缩包 | zip, rar, 7z, tar, gz | application/* | 压缩文件 | +| 文本 | txt, md, csv | text/* | 文本文件 | + +--- + +## 文件配置 + +### 1. 文件存储配置 + +```yaml +# application-dev.yml +coder: + # 文件存储路径 + filePath: /data/upload/ + + # 文件访问域名 + domain: http://localhost:18099 + + # 文件大小限制(MB) + maxFileSize: 100 + + # 允许的文件类型 + allowedTypes: + - jpg + - jpeg + - png + - gif + - pdf + - doc + - docx + - xls + - xlsx + + # 禁止的文件类型 + forbiddenTypes: + - exe + - bat + - sh + - jsp + - php +``` + +### 2. 文件夹分类 + +```java +// 文件夹类型枚举 +public enum FolderType { + AVATAR("avatar", "用户头像"), + DOCUMENT("document", "文档文件"), + IMAGE("image", "图片文件"), + TEMP("temp", "临时文件"), + EXPORT("export", "导出文件"); + + private final String code; + private final String name; +} +``` + +### 3. 文件上传工具类 + +```java +@Component +public class FileUploadUtil { + + /** + * 上传文件 + */ + public FileUploadResult uploadFile(MultipartFile file, String folder, int maxSize) { + // 1. 文件校验 + validateFile(file, maxSize); + + // 2. 生成文件名 + String fileName = generateFileName(file.getOriginalFilename()); + + // 3. 创建目录 + String datePath = DateUtil.format(new Date(), "yyyy/MM/dd"); + String dirPath = filePath + folder + "/" + datePath + "/"; + FileUtil.createDirs(dirPath); + + // 4. 保存文件 + String filePath = dirPath + fileName; + file.transferTo(new File(filePath)); + + // 5. 生成访问URL + String fileUrl = domain + "/upload/" + folder + "/" + datePath + "/" + fileName; + + // 6. 返回结果 + return FileUploadResult.builder() + .fileName(fileName) + .originalName(file.getOriginalFilename()) + .filePath(filePath) + .fileUrl(fileUrl) + .fileSize(file.getSize()) + .fileType(FileUtil.getExtension(fileName)) + .uploadTime(new Date()) + .build(); + } + + /** + * 文件校验 + */ + private void validateFile(MultipartFile file, int maxSize) { + if (file.isEmpty()) { + throw new BusinessException("文件不能为空"); + } + + if (file.getSize() > maxSize * 1024 * 1024) { + throw new BusinessException("文件大小不能超过" + maxSize + "MB"); + } + + String extension = FileUtil.getExtension(file.getOriginalFilename()); + if (!allowedTypes.contains(extension.toLowerCase())) { + throw new BusinessException("不支持的文件类型:" + extension); + } + + if (forbiddenTypes.contains(extension.toLowerCase())) { + throw new BusinessException("禁止上传的文件类型:" + extension); + } + } + + /** + * 生成文件名 + */ + private String generateFileName(String originalName) { + String extension = FileUtil.getExtension(originalName); + String baseName = FileUtil.getBaseName(originalName); + String timestamp = DateUtil.format(new Date(), "yyyyMMdd_HHmmss"); + String random = RandomUtil.randomString(3); + return baseName + "_" + timestamp + "_" + random + "." + extension; + } +} +``` + +--- + +## 错误码说明 + +| 错误码 | 错误信息 | 说明 | +|--------|----------|------| +| 400 | 文件不能为空 | 上传文件为空 | +| 400 | 文件大小不能超过{size}MB | 文件大小超过限制 | +| 400 | 不支持的文件类型 | 文件类型不在允许列表中 | +| 400 | 禁止上传的文件类型 | 文件类型在禁止列表中 | +| 400 | 文件名不能为空 | 文件名为空 | +| 400 | 文件路径不能为空 | 文件路径为空 | +| 400 | 文件不存在 | 文件ID不存在 | +| 400 | 文件已被删除 | 文件状态为已删除 | +| 401 | 当前会话未登录 | 未登录或Token无效 | +| 403 | 权限不足 | 没有相应的操作权限 | +| 500 | 文件上传失败 | 文件保存到磁盘失败 | +| 500 | 文件删除失败 | 物理文件删除失败 | +| 500 | 磁盘空间不足 | 服务器磁盘空间不足 | + +--- + +## 安全特性 + +### 1. 文件类型验证 + +- **扩展名校验**: 验证文件扩展名 +- **MIME类型校验**: 验证文件MIME类型 +- **文件内容校验**: 验证文件真实类型 +- **黑名单过滤**: 禁止上传危险文件类型 + +### 2. 文件大小限制 + +- **单文件大小限制**: 限制单个文件大小 +- **总文件大小限制**: 限制用户总文件大小 +- **磁盘空间检查**: 检查服务器磁盘空间 + +### 3. 访问控制 + +- **权限验证**: 验证用户上传和访问权限 +- **路径防护**: 防止路径遍历攻击 +- **防盗链**: 防止文件被盗链 + +### 4. 病毒扫描 + +- **文件扫描**: 上传文件病毒扫描 +- **隔离机制**: 可疑文件隔离处理 +- **定期扫描**: 定期扫描存储文件 + +--- + +## 性能优化 + +### 1. 文件存储 + +- **分目录存储**: 按日期和类型分目录存储 +- **CDN加速**: 使用CDN加速文件访问 +- **压缩存储**: 自动压缩图片文件 +- **缓存策略**: 设置合适的缓存策略 + +### 2. 上传优化 + +- **分片上传**: 大文件分片上传 +- **断点续传**: 支持断点续传功能 +- **并发上传**: 支持多文件并发上传 +- **进度显示**: 实时显示上传进度 + +### 3. 存储优化 + +- **重复文件检测**: 检测并避免重复文件 +- **文件压缩**: 自动压缩文件 +- **定期清理**: 清理临时文件和无效文件 +- **存储监控**: 监控存储空间使用情况 + +--- + +## 使用建议 + +### 1. 文件命名规范 + +- **唯一性**: 确保文件名唯一 +- **时间戳**: 包含时间戳信息 +- **随机性**: 添加随机字符 +- **可读性**: 保持一定的可读性 + +### 2. 文件分类管理 + +- **按类型分类**: 不同类型文件存储在不同目录 +- **按用户分类**: 不同用户文件分开存储 +- **按时间分类**: 按年月日创建目录结构 +- **按业务分类**: 按业务模块分类存储 + +### 3. 安全防护 + +- **输入验证**: 严格验证上传文件 +- **权限控制**: 细粒度的权限控制 +- **日志记录**: 记录所有文件操作 +- **备份策略**: 重要文件定期备份 + +### 4. 监控告警 + +- **存储监控**: 监控存储空间使用 +- **性能监控**: 监控上传下载性能 +- **安全监控**: 监控异常文件操作 +- **容量告警**: 存储空间不足告警 + +--- + +## 注意事项 + +1. **文件安全**: 严格验证上传文件类型和内容 +2. **存储路径**: 确保文件存储路径安全 +3. **权限控制**: 合理设置文件访问权限 +4. **磁盘空间**: 定期清理无效文件,避免磁盘满 +5. **备份策略**: 重要文件需要备份 +6. **访问控制**: 防止文件被恶意访问 +7. **性能考虑**: 大文件上传时注意性能影响 +8. **并发控制**: 处理并发上传时的文件冲突 +9. **错误处理**: 完善的错误处理和回滚机制 +10. **日志审计**: 记录所有文件操作日志 \ No newline at end of file diff --git a/api/system/登录日志API.md b/api/system/登录日志API.md new file mode 100644 index 0000000..8f5cc29 --- /dev/null +++ b/api/system/登录日志API.md @@ -0,0 +1,1017 @@ +# 登录日志API + +## 概述 + +登录日志模块用于记录和管理系统用户的登录行为,包括登录成功、登录失败、退出登录等操作的详细记录。提供日志查询、统计分析和安全监控功能,是系统安全审计的重要组成部分。 + +## 权限说明 + +登录日志接口需要相应的权限才能访问: + +| 操作 | 权限码 | 说明 | +|------|--------|------| +| 查询登录日志 | `system:loginlog:list` | 查看登录日志权限 | +| 删除登录日志 | `system:loginlog:remove` | 删除登录日志权限 | +| 导出登录日志 | `system:loginlog:export` | 导出登录日志权限 | + +## 接口列表 + +### 1. 分页查询登录日志 + +**接口地址**: `GET /coder/sysLoginLog/listPage` + +**接口描述**: 分页查询系统登录日志列表 + +**是否需要认证**: 是 + +**权限要求**: `system:loginlog:list` + +**请求头**: +``` +Authorization: Bearer your-token-value +``` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 示例 | +|--------|------|------|------|------| +| pageNo | Integer | 否 | 页码 | 1 | +| pageSize | Integer | 否 | 每页大小 | 10 | +| loginName | String | 否 | 登录账号 | admin | +| loginStatus | String | 否 | 登录状态 | 0 | +| clientType | String | 否 | 客户端类型 | WEB | +| loginIp | String | 否 | 登录IP | 127.0.0.1 | +| loginAddress | String | 否 | 登录地址 | 本地登录 | +| browser | String | 否 | 浏览器类型 | Chrome | +| os | String | 否 | 操作系统 | Windows 10 | +| beginTime | String | 否 | 开始时间 | 2024-01-01 | +| endTime | String | 否 | 结束时间 | 2024-12-31 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "records": [ + { + "infoId": 1, + "loginName": "admin", + "userName": "管理员", + "userId": 1, + "loginStatus": "0", + "clientType": "WEB", + "deviceName": "Windows PC", + "loginIp": "127.0.0.1", + "loginAddress": "本地登录", + "browser": "Chrome 120.0.0.0", + "os": "Windows 10", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "loginTime": "2024-07-05 10:00:00", + "logoutTime": "2024-07-05 11:00:00", + "sessionDuration": 3600, + "failureReason": "", + "remark": "登录成功", + "createBy": "system", + "createTime": "2024-07-05 10:00:00", + "updateBy": "system", + "updateTime": "2024-07-05 11:00:00" + } + ], + "total": 1, + "size": 10, + "current": 1, + "pages": 1 + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysLoginLog/listPage?pageNo=1&pageSize=10&loginName=admin&loginStatus=0" \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 2. 查询所有登录日志 + +**接口地址**: `GET /coder/sysLoginLog/list` + +**接口描述**: 查询所有系统登录日志(不分页) + +**是否需要认证**: 是 + +**权限要求**: `system:loginlog:list` + +**请求参数**: 同分页查询(除pageNo、pageSize外) + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "infoId": 1, + "loginName": "admin", + "userName": "管理员", + "userId": 1, + "loginStatus": "0", + "clientType": "WEB", + "deviceName": "Windows PC", + "loginIp": "127.0.0.1", + "loginAddress": "本地登录", + "browser": "Chrome 120.0.0.0", + "os": "Windows 10", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "loginTime": "2024-07-05 10:00:00", + "logoutTime": "2024-07-05 11:00:00", + "sessionDuration": 3600, + "failureReason": "", + "remark": "登录成功", + "createBy": "system", + "createTime": "2024-07-05 10:00:00", + "updateBy": "system", + "updateTime": "2024-07-05 11:00:00" + } + ], + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysLoginLog/list?loginStatus=1&beginTime=2024-07-01" \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 3. 根据ID查询登录日志 + +**接口地址**: `GET /coder/sysLoginLog/getById/{id}` + +**接口描述**: 根据日志ID查询登录日志详细信息 + +**是否需要认证**: 是 + +**权限要求**: `system:loginlog:list` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 日志ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "infoId": 1, + "loginName": "admin", + "userName": "管理员", + "userId": 1, + "loginStatus": "0", + "clientType": "WEB", + "deviceName": "Windows PC", + "loginIp": "127.0.0.1", + "loginAddress": "本地登录", + "browser": "Chrome 120.0.0.0", + "os": "Windows 10", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "loginTime": "2024-07-05 10:00:00", + "logoutTime": "2024-07-05 11:00:00", + "sessionDuration": 3600, + "failureReason": "", + "remark": "登录成功", + "createBy": "system", + "createTime": "2024-07-05 10:00:00", + "updateBy": "system", + "updateTime": "2024-07-05 11:00:00" + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysLoginLog/getById/1 \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 4. 新增登录日志 + +**接口地址**: `POST /coder/sysLoginLog/add` + +**接口描述**: 新增系统登录日志记录 + +**是否需要认证**: 是 + +**权限要求**: `system:loginlog:add` + +**请求参数**: + +```json +{ + "loginName": "admin", + "userName": "管理员", + "userId": 1, + "loginStatus": "0", + "clientType": "WEB", + "deviceName": "Windows PC", + "loginIp": "127.0.0.1", + "loginAddress": "本地登录", + "browser": "Chrome 120.0.0.0", + "os": "Windows 10", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "loginTime": "2024-07-05 10:00:00", + "failureReason": "", + "remark": "登录成功" +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| loginName | String | 是 | 登录账号 | 不能为空 | +| userName | String | 否 | 用户姓名 | 可为空 | +| userId | Long | 否 | 用户ID | 有效的用户ID | +| loginStatus | String | 是 | 登录状态 | 0-成功 1-失败 | +| clientType | String | 是 | 客户端类型 | WEB, MOBILE, API | +| deviceName | String | 否 | 设备名称 | 可为空 | +| loginIp | String | 是 | 登录IP | 有效的IP地址 | +| loginAddress | String | 否 | 登录地址 | 可为空 | +| browser | String | 否 | 浏览器信息 | 可为空 | +| os | String | 否 | 操作系统 | 可为空 | +| userAgent | String | 否 | 用户代理 | 可为空 | +| loginTime | String | 是 | 登录时间 | 时间格式 | +| failureReason | String | 否 | 失败原因 | 登录失败时必填 | +| remark | String | 否 | 备注信息 | 最长500字符 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "新增成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginLog/add \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token-value" \ + -d '{ + "loginName": "admin", + "userName": "管理员", + "userId": 1, + "loginStatus": "0", + "clientType": "WEB", + "loginIp": "127.0.0.1", + "loginTime": "2024-07-05 10:00:00", + "remark": "登录成功" + }' +``` + +--- + +### 5. 修改登录日志 + +**接口地址**: `POST /coder/sysLoginLog/update` + +**接口描述**: 修改系统登录日志信息(通常用于更新退出时间) + +**是否需要认证**: 是 + +**权限要求**: `system:loginlog:edit` + +**请求参数**: + +```json +{ + "infoId": 1, + "loginName": "admin", + "userName": "管理员", + "userId": 1, + "loginStatus": "0", + "clientType": "WEB", + "deviceName": "Windows PC", + "loginIp": "127.0.0.1", + "loginAddress": "本地登录", + "browser": "Chrome 120.0.0.0", + "os": "Windows 10", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "loginTime": "2024-07-05 10:00:00", + "logoutTime": "2024-07-05 11:00:00", + "sessionDuration": 3600, + "failureReason": "", + "remark": "正常退出" +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| infoId | Long | 是 | 日志ID | 必须是有效的日志ID | +| logoutTime | String | 否 | 退出时间 | 时间格式 | +| sessionDuration | Integer | 否 | 会话时长(秒) | 大于等于0 | +| 其他参数 | - | - | 同新增日志 | - | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginLog/update \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token-value" \ + -d '{ + "infoId": 1, + "logoutTime": "2024-07-05 11:00:00", + "sessionDuration": 3600, + "remark": "正常退出" + }' +``` + +--- + +### 6. 删除登录日志 + +**接口地址**: `POST /coder/sysLoginLog/deleteById/{id}` + +**接口描述**: 根据ID删除登录日志 + +**是否需要认证**: 是 + +**权限要求**: `system:loginlog:remove` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 日志ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginLog/deleteById/1 \ + -H "Authorization: Bearer your-token-value" +``` + +--- + +### 7. 批量删除登录日志 + +**接口地址**: `POST /coder/sysLoginLog/batchDelete` + +**接口描述**: 批量删除登录日志 + +**是否需要认证**: 是 + +**权限要求**: `system:loginlog:remove` + +**请求参数**: + +```json +{ + "ids": [1, 2, 3] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| ids | Long[] | 是 | 日志ID数组 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginLog/batchDelete \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token-value" \ + -d '{ + "ids": [1, 2, 3] + }' +``` + +--- + +## 登录日志字段说明 + +### 基础字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| infoId | Long | 日志ID | 1 | +| loginName | String | 登录账号 | admin | +| userName | String | 用户姓名 | 管理员 | +| userId | Long | 用户ID | 1 | + +### 状态字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| loginStatus | String | 登录状态 | 0-成功 1-失败 | +| clientType | String | 客户端类型 | WEB, MOBILE, API | +| deviceName | String | 设备名称 | Windows PC | + +### 网络字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| loginIp | String | 登录IP地址 | 127.0.0.1 | +| loginAddress | String | 登录地理位置 | 本地登录 | +| userAgent | String | 用户代理字符串 | Mozilla/5.0... | + +### 环境字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| browser | String | 浏览器信息 | Chrome 120.0.0.0 | +| os | String | 操作系统 | Windows 10 | + +### 时间字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| loginTime | String | 登录时间 | 2024-07-05 10:00:00 | +| logoutTime | String | 退出时间 | 2024-07-05 11:00:00 | +| sessionDuration | Integer | 会话时长(秒) | 3600 | + +### 其他字段 + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| failureReason | String | 失败原因 | 密码错误 | +| remark | String | 备注信息 | 登录成功 | + +--- + +## 数据字典 + +### 登录状态 (loginStatus) + +| 值 | 说明 | +|----|------| +| 0 | 登录成功 | +| 1 | 登录失败 | + +### 客户端类型 (clientType) + +| 值 | 说明 | +|----|------| +| WEB | 网页端 | +| MOBILE | 移动端 | +| API | 接口调用 | +| DESKTOP | 桌面应用 | +| WECHAT | 微信小程序 | + +### 常见失败原因 + +| 失败原因 | 说明 | +|----------|------| +| 用户不存在 | 登录账号不存在 | +| 密码错误 | 登录密码不正确 | +| 验证码错误 | 验证码输入错误 | +| 账号被禁用 | 用户账号被禁用 | +| 账号被锁定 | 用户账号被锁定 | +| IP被限制 | 登录IP被限制 | +| 设备被限制 | 登录设备被限制 | +| 会话过期 | 用户会话已过期 | + +--- + +## 日志记录机制 + +### 1. 自动记录 + +```java +@Component +@Slf4j +public class LoginLogService { + + /** + * 记录登录成功日志 + */ + public void recordLoginSuccess(String loginName, HttpServletRequest request) { + try { + SysLoginLog loginLog = new SysLoginLog(); + + // 基础信息 + loginLog.setLoginName(loginName); + loginLog.setUserId(getCurrentUserId(loginName)); + loginLog.setUserName(getCurrentUserName(loginName)); + loginLog.setLoginStatus("0"); + loginLog.setLoginTime(LocalDateTime.now()); + + // 网络信息 + loginLog.setLoginIp(getClientIP(request)); + loginLog.setLoginAddress(getAddressByIP(loginLog.getLoginIp())); + + // 客户端信息 + loginLog.setClientType(getClientType(request)); + loginLog.setUserAgent(request.getHeader("User-Agent")); + loginLog.setBrowser(getBrowserInfo(request)); + loginLog.setOs(getOSInfo(request)); + loginLog.setDeviceName(getDeviceName(request)); + + // 其他信息 + loginLog.setRemark("登录成功"); + loginLog.setCreateBy("system"); + loginLog.setCreateTime(LocalDateTime.now()); + + // 保存日志 + sysLoginLogMapper.insert(loginLog); + + } catch (Exception e) { + log.error("记录登录成功日志失败", e); + } + } + + /** + * 记录登录失败日志 + */ + public void recordLoginFailure(String loginName, String failureReason, HttpServletRequest request) { + try { + SysLoginLog loginLog = new SysLoginLog(); + + // 基础信息 + loginLog.setLoginName(loginName); + loginLog.setLoginStatus("1"); + loginLog.setLoginTime(LocalDateTime.now()); + loginLog.setFailureReason(failureReason); + + // 网络信息 + loginLog.setLoginIp(getClientIP(request)); + loginLog.setLoginAddress(getAddressByIP(loginLog.getLoginIp())); + + // 客户端信息 + loginLog.setClientType(getClientType(request)); + loginLog.setUserAgent(request.getHeader("User-Agent")); + loginLog.setBrowser(getBrowserInfo(request)); + loginLog.setOs(getOSInfo(request)); + loginLog.setDeviceName(getDeviceName(request)); + + // 其他信息 + loginLog.setRemark("登录失败:" + failureReason); + loginLog.setCreateBy("system"); + loginLog.setCreateTime(LocalDateTime.now()); + + // 保存日志 + sysLoginLogMapper.insert(loginLog); + + } catch (Exception e) { + log.error("记录登录失败日志失败", e); + } + } + + /** + * 记录退出登录日志 + */ + public void recordLogout(String loginName, LocalDateTime loginTime) { + try { + // 查找对应的登录日志 + SysLoginLog loginLog = sysLoginLogMapper.selectOne( + new LambdaQueryWrapper() + .eq(SysLoginLog::getLoginName, loginName) + .eq(SysLoginLog::getLoginTime, loginTime) + .eq(SysLoginLog::getLoginStatus, "0") + .isNull(SysLoginLog::getLogoutTime) + .last("ORDER BY create_time DESC LIMIT 1") + ); + + if (loginLog != null) { + LocalDateTime logoutTime = LocalDateTime.now(); + long sessionDuration = Duration.between(loginLog.getLoginTime(), logoutTime).getSeconds(); + + loginLog.setLogoutTime(logoutTime); + loginLog.setSessionDuration((int) sessionDuration); + loginLog.setRemark("正常退出"); + loginLog.setUpdateBy("system"); + loginLog.setUpdateTime(LocalDateTime.now()); + + sysLoginLogMapper.updateById(loginLog); + } + + } catch (Exception e) { + log.error("记录退出登录日志失败", e); + } + } + + /** + * 获取客户端IP + */ + private String getClientIP(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + /** + * 解析浏览器信息 + */ + private String getBrowserInfo(HttpServletRequest request) { + String userAgent = request.getHeader("User-Agent"); + if (userAgent == null) { + return "Unknown"; + } + + if (userAgent.contains("Chrome")) { + return "Chrome"; + } else if (userAgent.contains("Firefox")) { + return "Firefox"; + } else if (userAgent.contains("Safari")) { + return "Safari"; + } else if (userAgent.contains("Edge")) { + return "Edge"; + } else if (userAgent.contains("IE")) { + return "Internet Explorer"; + } else { + return "Other"; + } + } + + /** + * 解析操作系统信息 + */ + private String getOSInfo(HttpServletRequest request) { + String userAgent = request.getHeader("User-Agent"); + if (userAgent == null) { + return "Unknown"; + } + + if (userAgent.contains("Windows NT 10.0")) { + return "Windows 10"; + } else if (userAgent.contains("Windows NT 6.3")) { + return "Windows 8.1"; + } else if (userAgent.contains("Windows NT 6.2")) { + return "Windows 8"; + } else if (userAgent.contains("Windows NT 6.1")) { + return "Windows 7"; + } else if (userAgent.contains("Mac OS X")) { + return "Mac OS"; + } else if (userAgent.contains("Linux")) { + return "Linux"; + } else if (userAgent.contains("Android")) { + return "Android"; + } else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) { + return "iOS"; + } else { + return "Other"; + } + } +} +``` + +### 2. 异步处理 + +```java +@Service +public class AsyncLoginLogService { + + @Async("taskExecutor") + public void recordLoginLogAsync(SysLoginLog loginLog) { + try { + // 异步记录登录日志 + sysLoginLogMapper.insert(loginLog); + + // 更新用户登录信息 + updateUserLoginInfo(loginLog); + + // 检查异常登录 + checkAbnormalLogin(loginLog); + + } catch (Exception e) { + log.error("异步记录登录日志失败", e); + } + } + + /** + * 检查异常登录 + */ + private void checkAbnormalLogin(SysLoginLog loginLog) { + // 检查IP异常 + if (isAbnormalIP(loginLog.getLoginIp(), loginLog.getLoginName())) { + sendSecurityAlert("检测到异常IP登录", loginLog); + } + + // 检查设备异常 + if (isAbnormalDevice(loginLog.getUserAgent(), loginLog.getLoginName())) { + sendSecurityAlert("检测到异常设备登录", loginLog); + } + + // 检查时间异常 + if (isAbnormalTime(loginLog.getLoginTime(), loginLog.getLoginName())) { + sendSecurityAlert("检测到异常时间登录", loginLog); + } + } +} +``` + +--- + +## 统计分析功能 + +### 1. 登录统计 + +```java +/** + * 登录统计服务 + */ +@Service +public class LoginStatisticsService { + + /** + * 获取登录成功率统计 + */ + public LoginSuccessRateVO getLoginSuccessRate(String beginTime, String endTime) { + // 查询总登录次数 + long totalCount = sysLoginLogMapper.selectCount( + new LambdaQueryWrapper() + .between(SysLoginLog::getLoginTime, beginTime, endTime) + ); + + // 查询成功登录次数 + long successCount = sysLoginLogMapper.selectCount( + new LambdaQueryWrapper() + .eq(SysLoginLog::getLoginStatus, "0") + .between(SysLoginLog::getLoginTime, beginTime, endTime) + ); + + // 计算成功率 + double successRate = totalCount > 0 ? (double) successCount / totalCount * 100 : 0; + + return LoginSuccessRateVO.builder() + .totalCount(totalCount) + .successCount(successCount) + .failureCount(totalCount - successCount) + .successRate(successRate) + .build(); + } + + /** + * 获取每日登录统计 + */ + public List getDailyLoginStat(String beginTime, String endTime) { + return sysLoginLogMapper.selectDailyLoginStat(beginTime, endTime); + } + + /** + * 获取客户端类型统计 + */ + public List getClientTypeStat(String beginTime, String endTime) { + return sysLoginLogMapper.selectClientTypeStat(beginTime, endTime); + } +} +``` + +### 2. 安全监控 + +```java +/** + * 安全监控服务 + */ +@Service +public class SecurityMonitorService { + + /** + * 检测暴力破解 + */ + public void detectBruteForce() { + // 查询5分钟内失败次数超过5次的IP + List suspiciousIPs = sysLoginLogMapper.selectSuspiciousIPs(); + + for (String ip : suspiciousIPs) { + // 加入黑名单 + addToBlacklist(ip); + + // 发送告警 + sendSecurityAlert("检测到暴力破解攻击", ip); + } + } + + /** + * 检测异地登录 + */ + public void detectRemoteLogin() { + // 查询用户最近登录地址 + List recentLogins = sysLoginLogMapper.selectRecentLogins(); + + for (SysLoginLog loginLog : recentLogins) { + String lastLoginAddress = getLastLoginAddress(loginLog.getLoginName()); + + if (!loginLog.getLoginAddress().equals(lastLoginAddress)) { + // 发送异地登录通知 + sendRemoteLoginNotification(loginLog); + } + } + } +} +``` + +--- + +## 错误码说明 + +| 错误码 | 错误信息 | 说明 | +|--------|----------|------| +| 400 | 登录账号不能为空 | 登录账号为空 | +| 400 | 登录状态不能为空 | 登录状态为空 | +| 400 | 登录时间不能为空 | 登录时间为空 | +| 400 | 登录IP不能为空 | 登录IP为空 | +| 400 | 日志不存在 | 日志ID不存在 | +| 400 | 时间格式错误 | 时间格式不正确 | +| 401 | 当前会话未登录 | 未登录或Token无效 | +| 403 | 权限不足 | 没有相应的操作权限 | +| 500 | 日志记录失败 | 日志保存失败 | +| 500 | 系统异常 | 服务器内部错误 | + +--- + +## 日志清理策略 + +### 1. 定时清理 + +```java +@Component +public class LoginLogCleanupTask { + + /** + * 每天凌晨2点清理30天前的登录日志 + */ + @Scheduled(cron = "0 0 2 * * ?") + public void cleanupOldLogs() { + try { + LocalDateTime cutoffTime = LocalDateTime.now().minusDays(30); + + int deletedCount = sysLoginLogMapper.delete( + new LambdaQueryWrapper() + .lt(SysLoginLog::getCreateTime, cutoffTime) + ); + + log.info("清理登录日志完成,删除{}条记录", deletedCount); + + } catch (Exception e) { + log.error("清理登录日志失败", e); + } + } + + /** + * 归档登录日志 + */ + @Scheduled(cron = "0 0 1 1 * ?") // 每月1号凌晨1点执行 + public void archiveLogs() { + try { + LocalDateTime lastMonth = LocalDateTime.now().minusMonths(1); + + // 归档上个月的日志到历史表 + sysLoginLogMapper.archiveLogsToHistory(lastMonth); + + log.info("归档登录日志完成"); + + } catch (Exception e) { + log.error("归档登录日志失败", e); + } + } +} +``` + +### 2. 日志配置 + +```yaml +# application.yml +logging: + login-log: + # 是否启用登录日志 + enabled: true + # 日志保留天数 + retention-days: 90 + # 是否记录成功登录 + log-success: true + # 是否记录失败登录 + log-failure: true + # 是否异步记录 + async: true + # 是否启用IP地址解析 + resolve-address: true + # 是否启用安全监控 + security-monitor: true +``` + +--- + +## 使用建议 + +### 1. 日志管理 + +- **合理保留期**: 根据业务需要设置日志保留期 +- **定期清理**: 定期清理过期日志释放存储空间 +- **分表存储**: 大量日志可考虑按月分表存储 +- **归档备份**: 重要日志定期归档备份 + +### 2. 安全监控 + +- **实时监控**: 实时监控异常登录行为 +- **告警机制**: 建立完善的安全告警机制 +- **自动响应**: 对可疑行为自动采取防护措施 +- **定期分析**: 定期分析登录日志发现安全趋势 + +### 3. 性能优化 + +- **异步记录**: 使用异步方式记录日志避免影响登录性能 +- **批量处理**: 批量处理日志数据提高效率 +- **索引优化**: 为查询字段建立合适索引 +- **分页查询**: 大数据量查询使用分页 + +### 4. 隐私保护 + +- **敏感信息**: 不记录密码等敏感信息 +- **数据脱敏**: 对部分信息进行脱敏处理 +- **访问控制**: 严格控制日志访问权限 +- **合规要求**: 遵守相关数据保护法规 + +--- + +## 注意事项 + +1. **日志完整性**: 确保登录日志记录的完整性和准确性 +2. **性能影响**: 日志记录不应影响用户登录体验 +3. **存储空间**: 注意日志存储空间的管理和清理 +4. **安全防护**: 防止日志被恶意篡改或删除 +5. **隐私保护**: 不记录用户密码等敏感信息 +6. **异常处理**: 日志记录失败不应影响正常业务 +7. **监控告警**: 建立异常登录的监控告警机制 +8. **数据备份**: 重要日志数据需要备份 +9. **合规要求**: 遵守相关法律法规对日志的要求 +10. **访问审计**: 对日志访问行为也要进行审计 \ No newline at end of file diff --git a/api/user/用户管理API.md b/api/user/用户管理API.md new file mode 100644 index 0000000..efd4fa5 --- /dev/null +++ b/api/user/用户管理API.md @@ -0,0 +1,888 @@ +# 用户管理API + +## 概述 + +用户管理模块提供系统用户的增删改查功能,包括用户信息管理、状态控制、密码管理、角色分配等核心功能。 + +## 权限说明 + +用户管理接口需要相应的权限才能访问: + +| 操作 | 权限码 | 说明 | +|------|--------|------| +| 查询用户列表 | `system:user:list` | 查看用户列表权限 | +| 新增用户 | `system:user:add` | 新增用户权限 | +| 修改用户 | `system:user:edit` | 修改用户信息权限 | +| 删除用户 | `system:user:remove` | 删除用户权限 | +| 重置密码 | `system:user:resetPwd` | 重置用户密码权限 | +| 修改状态 | `system:user:status` | 修改用户状态权限 | +| 分配角色 | `system:user:role` | 分配用户角色权限 | +| 导入用户 | `system:user:import` | 导入用户数据权限 | +| 导出用户 | `system:user:export` | 导出用户数据权限 | + +## 接口列表 + +### 1. 分页查询用户列表 + +**接口地址**: `GET /coder/sysLoginUser/listPage` + +**接口描述**: 分页查询系统用户列表 + +**是否需要认证**: 是 + +**权限要求**: `system:user:list` + +**请求头**: +``` +Authorization: your-token-value +``` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 示例 | +|--------|------|------|------|------| +| pageNo | Integer | 否 | 页码 | 1 | +| pageSize | Integer | 否 | 每页大小 | 10 | +| loginName | String | 否 | 登录账号 | admin | +| userName | String | 否 | 用户姓名 | 管理员 | +| userType | String | 否 | 用户类型 | 1 | +| phone | String | 否 | 手机号码 | 13800138000 | +| sex | String | 否 | 用户性别 | 1 | +| userStatus | String | 否 | 用户状态 | 0 | +| beginTime | String | 否 | 开始时间 | 2024-01-01 | +| endTime | String | 否 | 结束时间 | 2024-12-31 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "records": [ + { + "userId": 1, + "loginName": "admin", + "userName": "管理员", + "userType": "1", + "email": "admin@example.com", + "phone": "13800138000", + "sex": "1", + "avatar": "/upload/avatar/admin.jpg", + "userStatus": "0", + "loginIp": "127.0.0.1", + "loginTime": "2024-01-01 10:00:00", + "pwdUpdateTime": "2024-01-01 10:00:00", + "remark": "系统管理员", + "createBy": "admin", + "createTime": "2024-01-01 10:00:00", + "updateBy": "admin", + "updateTime": "2024-01-01 10:00:00", + "roleIds": [1, 2] + } + ], + "total": 1, + "size": 10, + "current": 1, + "pages": 1 + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysLoginUser/listPage?pageNo=1&pageSize=10&loginName=admin" \ + -H "Authorization: your-token-value" +``` + +--- + +### 2. 查询所有用户 + +**接口地址**: `GET /coder/sysLoginUser/list` + +**接口描述**: 查询所有系统用户(不分页) + +**是否需要认证**: 是 + +**权限要求**: `system:user:list` + +**请求参数**: 同分页查询(除pageNo、pageSize外) + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": [ + { + "userId": 1, + "loginName": "admin", + "userName": "管理员", + // 其他字段... + } + ], + "traceId": "trace-123456" +} +``` + +--- + +### 3. 根据ID查询用户 + +**接口地址**: `GET /coder/sysLoginUser/getById/{id}` + +**接口描述**: 根据用户ID查询用户详细信息 + +**是否需要认证**: 是 + +**权限要求**: `system:user:list` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 用户ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "userId": 1, + "loginName": "admin", + "userName": "管理员", + "userType": "1", + "email": "admin@example.com", + "phone": "13800138000", + "sex": "1", + "avatar": "/upload/avatar/admin.jpg", + "userStatus": "0", + "loginIp": "127.0.0.1", + "loginTime": "2024-01-01 10:00:00", + "pwdUpdateTime": "2024-01-01 10:00:00", + "remark": "系统管理员", + "createBy": "admin", + "createTime": "2024-01-01 10:00:00", + "updateBy": "admin", + "updateTime": "2024-01-01 10:00:00", + "roleIds": [1, 2] + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysLoginUser/getById/1 \ + -H "Authorization: your-token-value" +``` + +--- + +### 4. 新增用户 + +**接口地址**: `POST /coder/sysLoginUser/add` + +**接口描述**: 新增系统用户 + +**是否需要认证**: 是 + +**权限要求**: `system:user:add` + +**请求参数**: + +```json +{ + "loginName": "newuser", + "userName": "新用户", + "password": "123456", + "userType": "1", + "email": "newuser@example.com", + "phone": "13800138001", + "sex": "1", + "userStatus": "0", + "remark": "新用户备注", + "roleIds": [2] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| loginName | String | 是 | 登录账号 | 3-16位字母数字 | +| userName | String | 是 | 用户姓名 | 不能为空 | +| password | String | 是 | 登录密码 | 不能为空 | +| userType | String | 是 | 用户类型 | 1-系统用户 2-注册用户 | +| email | String | 否 | 邮箱地址 | 邮箱格式验证 | +| phone | String | 否 | 手机号码 | 手机号格式验证 | +| sex | String | 否 | 用户性别 | 1-男 2-女 3-未知 | +| userStatus | String | 是 | 用户状态 | 0-启用 1-停用 | +| remark | String | 否 | 备注信息 | 最长200字符 | +| roleIds | Long[] | 否 | 角色ID数组 | 有效的角色ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "新增成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginUser/add \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "loginName": "newuser", + "userName": "新用户", + "password": "123456", + "userType": "1", + "email": "newuser@example.com", + "phone": "13800138001", + "sex": "1", + "userStatus": "0", + "remark": "新用户备注", + "roleIds": [2] + }' +``` + +--- + +### 5. 修改用户信息 + +**接口地址**: `POST /coder/sysLoginUser/update` + +**接口描述**: 修改系统用户信息 + +**是否需要认证**: 是 + +**权限要求**: `system:user:edit` + +**请求参数**: + +```json +{ + "userId": 1, + "loginName": "admin", + "userName": "管理员", + "userType": "1", + "email": "admin@example.com", + "phone": "13800138000", + "sex": "1", + "userStatus": "0", + "remark": "系统管理员", + "roleIds": [1, 2] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| userId | Long | 是 | 用户ID | 必须是有效的用户ID | +| loginName | String | 是 | 登录账号 | 3-16位字母数字 | +| userName | String | 是 | 用户姓名 | 不能为空 | +| userType | String | 是 | 用户类型 | 1-系统用户 2-注册用户 | +| email | String | 否 | 邮箱地址 | 邮箱格式验证 | +| phone | String | 否 | 手机号码 | 手机号格式验证 | +| sex | String | 否 | 用户性别 | 1-男 2-女 3-未知 | +| userStatus | String | 是 | 用户状态 | 0-启用 1-停用 | +| remark | String | 否 | 备注信息 | 最长200字符 | +| roleIds | Long[] | 否 | 角色ID数组 | 有效的角色ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginUser/update \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "userId": 1, + "loginName": "admin", + "userName": "管理员", + "userType": "1", + "email": "admin@example.com", + "phone": "13800138000", + "sex": "1", + "userStatus": "0", + "remark": "系统管理员", + "roleIds": [1, 2] + }' +``` + +--- + +### 6. 删除用户 + +**接口地址**: `POST /coder/sysLoginUser/deleteById/{id}` + +**接口描述**: 根据ID删除用户 + +**是否需要认证**: 是 + +**权限要求**: `system:user:remove` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 用户ID | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginUser/deleteById/1 \ + -H "Authorization: your-token-value" +``` + +--- + +### 7. 批量删除用户 + +**接口地址**: `POST /coder/sysLoginUser/batchDelete` + +**接口描述**: 批量删除用户 + +**是否需要认证**: 是 + +**权限要求**: `system:user:remove` + +**请求参数**: + +```json +{ + "ids": [1, 2, 3] +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| ids | Long[] | 是 | 用户ID数组 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "删除成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginUser/batchDelete \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "ids": [1, 2, 3] + }' +``` + +--- + +### 8. 修改用户状态 + +**接口地址**: `POST /coder/sysLoginUser/updateStatus/{userId}/{userStatus}` + +**接口描述**: 修改用户状态(启用/停用) + +**是否需要认证**: 是 + +**权限要求**: `system:user:status` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| userId | Long | 是 | 用户ID | +| userStatus | String | 是 | 用户状态(0-启用 1-停用) | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginUser/updateStatus/1/0 \ + -H "Authorization: your-token-value" +``` + +--- + +### 9. 获取登录用户信息 + +**接口地址**: `GET /coder/sysLoginUser/getLoginUserInformation` + +**接口描述**: 获取当前登录用户的详细信息 + +**是否需要认证**: 是 + +**权限要求**: 无(已登录用户可访问) + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "userId": 1, + "loginName": "admin", + "userName": "管理员", + "userType": "1", + "email": "admin@example.com", + "phone": "13800138000", + "sex": "1", + "avatar": "/upload/avatar/admin.jpg", + "userStatus": "0", + "loginIp": "127.0.0.1", + "loginTime": "2024-01-01 10:00:00", + "pwdUpdateTime": "2024-01-01 10:00:00", + "remark": "系统管理员", + "createBy": "admin", + "createTime": "2024-01-01 10:00:00", + "updateBy": "admin", + "updateTime": "2024-01-01 10:00:00", + "roleIds": [1, 2], + "roles": [ + { + "roleId": 1, + "roleName": "超级管理员", + "roleCode": "admin" + } + ], + "permissions": [ + "*:*:*" + ] + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysLoginUser/getLoginUserInformation \ + -H "Authorization: your-token-value" +``` + +--- + +### 10. 获取个人资料 + +**接口地址**: `GET /coder/sysLoginUser/getPersonalData` + +**接口描述**: 获取当前用户的个人资料 + +**是否需要认证**: 是 + +**权限要求**: 无(已登录用户可访问) + +**请求参数**: 无 + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "userId": 1, + "loginName": "admin", + "userName": "管理员", + "email": "admin@example.com", + "phone": "13800138000", + "sex": "1", + "avatar": "/upload/avatar/admin.jpg", + "remark": "系统管理员" + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysLoginUser/getPersonalData \ + -H "Authorization: your-token-value" +``` + +--- + +### 11. 修改个人资料 + +**接口地址**: `POST /coder/sysLoginUser/updateBasicData` + +**接口描述**: 修改当前用户的个人资料 + +**是否需要认证**: 是 + +**权限要求**: 无(已登录用户可访问) + +**请求参数**: + +```json +{ + "userName": "管理员", + "email": "admin@example.com", + "phone": "13800138000", + "sex": "1", + "avatar": "/upload/avatar/admin.jpg", + "remark": "系统管理员" +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| userName | String | 是 | 用户姓名 | 不能为空 | +| email | String | 否 | 邮箱地址 | 邮箱格式验证 | +| phone | String | 否 | 手机号码 | 手机号格式验证 | +| sex | String | 否 | 用户性别 | 1-男 2-女 3-未知 | +| avatar | String | 否 | 头像地址 | 有效的文件路径 | +| remark | String | 否 | 备注信息 | 最长200字符 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginUser/updateBasicData \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "userName": "管理员", + "email": "admin@example.com", + "phone": "13800138000", + "sex": "1", + "avatar": "/upload/avatar/admin.jpg", + "remark": "系统管理员" + }' +``` + +--- + +### 12. 修改登录密码 + +**接口地址**: `POST /coder/sysLoginUser/updateUserPwd` + +**接口描述**: 修改当前用户的登录密码 + +**是否需要认证**: 是 + +**权限要求**: 无(已登录用户可访问) + +**请求参数**: + +```json +{ + "oldPassword": "123456", + "newPassword": "654321", + "confirmPassword": "654321" +} +``` + +**请求参数说明**: + +| 参数名 | 类型 | 必填 | 说明 | 校验规则 | +|--------|------|------|------|----------| +| oldPassword | String | 是 | 原密码 | 不能为空 | +| newPassword | String | 是 | 新密码 | 不能为空 | +| confirmPassword | String | 是 | 确认密码 | 必须与新密码一致 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "修改成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginUser/updateUserPwd \ + -H "Content-Type: application/json" \ + -H "Authorization: your-token-value" \ + -d '{ + "oldPassword": "123456", + "newPassword": "654321", + "confirmPassword": "654321" + }' +``` + +--- + +### 13. 重置用户密码 + +**接口地址**: `POST /coder/sysLoginUser/resetPwd/{id}/{password}` + +**接口描述**: 重置指定用户的密码 + +**是否需要认证**: 是 + +**权限要求**: `system:user:resetPwd` + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 用户ID | +| password | String | 是 | 新密码 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": "重置成功", + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginUser/resetPwd/1/123456 \ + -H "Authorization: your-token-value" +``` + +--- + +### 14. 下载用户导入模板 + +**接口地址**: `GET /coder/sysLoginUser/downloadExcelTemplate` + +**接口描述**: 下载用户批量导入的Excel模板 + +**是否需要认证**: 是 + +**权限要求**: `system:user:import` + +**请求参数**: 无 + +**响应**: Excel文件下载 + +**调用示例**: + +```bash +curl -X GET \ + http://localhost:18099/coder/sysLoginUser/downloadExcelTemplate \ + -H "Authorization: your-token-value" \ + -o user_template.xlsx +``` + +--- + +### 15. 导出用户数据 + +**接口地址**: `GET /coder/sysLoginUser/exportExcelData` + +**接口描述**: 导出用户数据到Excel + +**是否需要认证**: 是 + +**权限要求**: `system:user:export` + +**请求参数**: 同查询参数 + +**响应**: Excel文件下载 + +**调用示例**: + +```bash +curl -X GET \ + "http://localhost:18099/coder/sysLoginUser/exportExcelData?userStatus=0" \ + -H "Authorization: your-token-value" \ + -o users.xlsx +``` + +--- + +### 16. 导入用户数据 + +**接口地址**: `POST /coder/sysLoginUser/importExcelData` + +**接口描述**: 从Excel文件导入用户数据 + +**是否需要认证**: 是 + +**权限要求**: `system:user:import` + +**请求参数**: 文件上传 + +**Content-Type**: `multipart/form-data` + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | File | 是 | Excel文件 | +| updateSupport | Boolean | 否 | 是否更新已存在的用户 | + +**响应示例**: + +```json +{ + "status": 200, + "msg": "SUCCESS", + "data": { + "total": 100, + "success": 95, + "failed": 5, + "message": "导入成功95条,失败5条" + }, + "traceId": "trace-123456" +} +``` + +**调用示例**: + +```bash +curl -X POST \ + http://localhost:18099/coder/sysLoginUser/importExcelData \ + -H "Authorization: your-token-value" \ + -F "file=@users.xlsx" \ + -F "updateSupport=true" +``` + +--- + +## 数据字典 + +### 用户类型 (userType) + +| 值 | 说明 | +|----|------| +| 1 | 系统用户 | +| 2 | 注册用户 | +| 3 | 微信用户 | + +### 用户性别 (sex) + +| 值 | 说明 | +|----|------| +| 1 | 男 | +| 2 | 女 | +| 3 | 未知 | + +### 用户状态 (userStatus) + +| 值 | 说明 | +|----|------| +| 0 | 启用 | +| 1 | 停用 | + +--- + +## 错误码说明 + +| 错误码 | 错误信息 | 说明 | +|--------|----------|------| +| 400 | 账号长度为 3-16 位 | 登录账号长度不符合要求 | +| 400 | 账号格式为数字以及字母 | 登录账号格式不正确 | +| 400 | 登录账号不能为空 | 登录账号为空 | +| 400 | 用户真实姓名不能为空 | 用户姓名为空 | +| 400 | 用户不存在 | 用户ID不存在 | +| 400 | 账号已存在 | 登录账号已被使用 | +| 400 | 手机号已存在 | 手机号已被使用 | +| 400 | 邮箱已存在 | 邮箱已被使用 | +| 400 | 不能删除当前用户 | 不能删除自己的账号 | +| 400 | 不能停用当前用户 | 不能停用自己的账号 | +| 400 | 原密码错误 | 修改密码时原密码不正确 | +| 400 | 新密码不能与原密码相同 | 新密码与原密码一致 | +| 400 | 两次输入的密码不一致 | 确认密码与新密码不一致 | +| 401 | 当前会话未登录 | 未登录或Token无效 | +| 403 | 权限不足 | 没有相应的操作权限 | +| 500 | 系统异常 | 服务器内部错误 | + +--- + +## 注意事项 + +1. **权限验证**: 所有用户管理操作都需要相应的权限 +2. **数据校验**: 新增和修改用户时会进行数据格式校验 +3. **唯一性约束**: 登录账号、手机号、邮箱必须唯一 +4. **密码安全**: 密码使用盐值加密存储 +5. **操作日志**: 所有用户操作都会记录日志 +6. **批量操作**: 批量删除时会跳过超级管理员账号 +7. **Excel导入**: 支持Excel模板导入用户数据 +8. **角色关联**: 用户删除时会自动删除角色关联关系 +9. **会话管理**: 用户状态变更时会影响其登录会话 +10. **数据完整性**: 删除用户前会检查关联数据 \ No newline at end of file diff --git a/doc/oss/setup-env.sh b/doc/oss/setup-env.sh new file mode 100755 index 0000000..958f63b --- /dev/null +++ b/doc/oss/setup-env.sh @@ -0,0 +1,240 @@ +#!/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 new file mode 100644 index 0000000..2c85dea --- /dev/null +++ b/doc/oss/环境变量设置指南.md @@ -0,0 +1,271 @@ +# 环境变量设置指南 + +## 📋 需要设置的环境变量 + +根据你的配置文件,需要设置以下环境变量: + +```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 new file mode 100644 index 0000000..ca4ef85 --- /dev/null +++ b/doc/oss/阿里云OSS文件上传系统设计方案.md @@ -0,0 +1,1855 @@ +# 阿里云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 new file mode 100644 index 0000000..f329d7e --- /dev/null +++ b/doc/websocket/WebSocket权限实时推送技术方案设计.md @@ -0,0 +1,727 @@ +# 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 diff --git a/script/README.md b/script/README.md new file mode 100644 index 0000000..1bda96a --- /dev/null +++ b/script/README.md @@ -0,0 +1,242 @@ +# 项目模板重构脚本 + +这是一个用于将 `heritage-backend` 项目重构为新项目模板的自动化脚本。 + +## 功能特性 + +🚀 **全自动重构**:一键完成整个项目的重构过程 +📁 **目录重命名**:自动重命名所有模块目录和子模块目录 +📦 **Maven配置**:更新所有 pom.xml 文件中的 groupId、artifactId 和模块名 +☕ **Java包结构**:重构所有 Java 文件的包名和 import 语句 +⚙️ **配置文件**:更新所有配置文件中的包名引用 +🗄️ **数据库配置**:重命名 SQL 文件和更新数据库名引用 +🔄 **多层级支持**:支持插件模块等多层级子模块结构 + +## 使用方法 + +### 1. 进入脚本目录 +```bash +cd /Users/leocoder/leocoder/develop/frameworks/heritage/heritage-backend/script +``` + +### 2. 执行重构脚本 +```bash +./project-template-refactor.sh +``` + +### 3. 按提示输入新项目信息 +脚本会提示你输入以下信息: +- **新项目名称**:例如 `my-project-backend` +- **新的GroupId**:例如 `com.company.project` +- **新的包名**:例如 `com.company.project` +- **新的模块前缀**:例如 `my-project` +- **新的数据库名**:例如 `my_project_db` + +### 4. 确认配置并开始重构 +脚本会显示所有配置信息供你确认,输入 `y` 开始重构。 + +## 重构内容详解 + +### 📁 目录结构重构 +**原始结构:** +``` +/parent-directory/ +└── heritage-backend/ ← 项目根目录 + ├── heritage-web/ + ├── heritage-common/ + ├── heritage-model/ + ├── heritage-mybatisplus/ + ├── heritage-modules/ + ├── heritage-plugins/ + │ ├── heritage-easyexcel/ + │ ├── heritage-oss/ + │ ├── heritage-sa-token/ + │ └── ...其他插件模块 + └── sql/ + └── heritage.sql +``` + +**重构后结构(以 my-project-backend 为例):** +``` +/parent-directory/ +├── heritage-backend_backup_* ← 自动创建的备份 +└── my-project-backend/ ← 重命名后的项目根目录 + ├── my-project-web/ + ├── my-project-common/ + ├── my-project-model/ + ├── my-project-mybatisplus/ + ├── my-project-modules/ + ├── my-project-plugins/ + │ ├── my-project-easyexcel/ + │ ├── my-project-oss/ + │ ├── my-project-sa-token/ + │ └── ...其他插件模块 + └── sql/ + └── my_project_db.sql +``` + +⭐ **v1.2.0 新特性**:脚本现在会自动重命名项目根目录,无需手动操作! + +### 📦 Maven 配置重构 +- **groupId**:`org.leocoder.heritage` → `com.company.project` +- **artifactId**:`heritage-*` → `my-project-*` +- **name**:相应模块名称更新 +- **依赖引用**:所有模块间依赖的 groupId 和 artifactId + +### ☕ Java 包结构重构 +- **包声明**:`package org.leocoder.heritage.*` → `package com.company.project.*` +- **import语句**:`import org.leocoder.heritage.*` → `import com.company.project.*` +- **目录结构**:`src/main/java/org.leocoder.heritage/` → `src/main/java/com/company/project/` + +### ⚙️ 配置文件重构 +- **application.yml / application-dev.yml / application-local.yml**: + - `packages-to-scan: org.leocoder.heritage` → `packages-to-scan: com.company.project` + - `projectName: CORDER-ADMIN-THIN` → `projectName: MY-PROJECT-ADMIN` + - `pool-name: CORDER-HIKARI-DEV` → `pool-name: MY-PROJECT-HIKARI-DEV` + - `name: coder-web` → `name: my-project-web` + - `jdbc:mysql://localhost:3306/heritage` → `jdbc:mysql://localhost:3306/new-db-name` + - `jdbc:mysql://localhost:3306/heritage-backup` → `jdbc:mysql://localhost:3306/new-db-name-backup` + - `filePath: /path/heritage-backend/` → `filePath: /path/new-project-backend/` +- **logback配置文件**(logback-spring*.xml): + - `heritage-logback` → `my-project-logback` + - `` → `` + - 所有日志路径引用:`${CORDER_ADMIN_LOGS}` → `${MY_PROJECT_ADMIN_LOGS}` +- **其他配置文件**:包名和项目相关引用更新 + +### 🗄️ SQL 文件重构 +- **文件重命名**:`heritage.sql` → `my_project_db.sql` +- **数据库名更新**:SQL文件中的数据库名引用更新 + +## 安全特性 + +### 🔒 自动备份 +脚本执行前会自动创建项目备份: +``` +heritage-backend_backup_20250922_143022/ +``` + +### ✅ 验证检查 +重构完成后自动验证是否还有旧的引用残留 + +### 🚫 错误处理 +遇到错误时立即停止执行,保护项目完整性 + +## 重构后操作 + +重构完成后请执行以下步骤: + +### 1. 验证编译 +```bash +mvn clean compile +``` + +### 2. 更新数据库配置 +手动检查并更新以下配置文件中的数据库连接信息: +- `*/src/main/resources/application-dev.yml` +- `*/src/main/resources/application-local.yml` + +### 3. 验证功能 +- 启动应用程序验证功能正常 +- 检查所有插件模块是否正常加载 + +### 4. IDE配置 +- 重新导入Maven项目 +- 检查项目结构和依赖 + +### 5. Git管理 +```bash +git add . +git commit -m "refactor: 重构项目为新模板" +``` + +## 注意事项 + +⚠️ **重要提醒:** +1. 重构前请确保代码已提交到Git仓库 +2. 脚本会自动创建备份,但建议额外手动备份重要数据 +3. 重构过程不可逆,请谨慎操作 +4. 重构后需要手动更新数据库连接配置 +5. 如果项目中有自定义的特殊配置,可能需要手动调整 + +## 故障排除 + +### 常见问题 +1. **权限错误**:确保脚本有可执行权限 `chmod +x project-template-refactor.sh` +2. **路径错误**:确保在正确的项目根目录下执行脚本 +3. **Maven错误**:重构后如果编译失败,检查依赖配置是否正确 +4. **包名冲突**:如果新包名已存在,选择不同的包名 +5. **Bash版本兼容性**:脚本已修复了bash版本兼容性问题 +6. **包名未正确替换**:使用修复脚本 `./fix-existing-project.sh` +7. **多余src目录**:使用修复脚本自动清理 + +### 项目修复工具 + +#### 1. 快速修复目录名反斜杠问题 +如果目录名中出现反斜杠(如 `leocoder\`): +```bash +./quick-fix-backslash.sh +``` + +#### 2. 修复遗漏的modules子模块 +如果modules目录下的子模块没有被重命名: +```bash +./fix-missing-modules.sh +``` + +#### 3. 修复已有问题的项目 +如果重构过程中出现包名未正确替换或目录结构问题: +```bash +./fix-existing-project.sh +``` + +#### 4. 检查项目状态 +检查项目重构后的整体状态: +```bash +./project-status-check.sh +``` + +#### 5. 演示logback配置处理 +查看logback配置处理效果: +```bash +./logback-demo.sh +``` + +#### 6. 测试反斜杠修复功能 +验证反斜杠问题修复效果: +```bash +./test-backslash-fix.sh +``` + +#### 7. 测试配置文件修复功能 +验证数据库连接字符串和文件路径配置修复效果: +```bash +./test-config-fix.sh +``` + +### 回滚操作 +如果重构出现问题,可以: +1. 删除重构后的项目目录 +2. 从备份目录恢复:`cp -r heritage-backend_backup_* heritage-backend` +3. 或使用修复脚本进行增量修复 + +### 脚本版本信息 +- **v1.0.0**:初始版本 +- **v1.1.0**:修复bash兼容性问题,改进Java包结构迁移逻辑,增加修复工具 +- **v1.1.1**:修复sed正则表达式导致的目录名反斜杠问题,增加快速修复工具 +- **v1.2.0**:**重大更新** - 完善反斜杠自动清理,增加项目根目录自动重命名功能 +- **v1.2.1**:**关键修复** - 彻底解决反斜杠问题,增强MyBatis XML处理,改进验证逻辑 +- **v1.2.2**:**模块修复** - 修复modules子模块重命名遗漏问题,支持多层级子模块处理 +- **v1.2.3**:**配置修复** - 修复数据库连接字符串和文件路径配置未更新问题 + +## 技术支持 + +如果在使用过程中遇到问题,请: +1. 检查脚本执行日志 +2. 查看备份目录是否完整 +3. 联系开发团队获取支持 + +--- + +**版本**:v1.2.3 +**作者**:Leocoder +**更新时间**:2025-09-22 \ No newline at end of file diff --git a/script/project-template-refactor.sh b/script/project-template-refactor.sh new file mode 100755 index 0000000..03a71e9 --- /dev/null +++ b/script/project-template-refactor.sh @@ -0,0 +1,567 @@ +#!/bin/bash + +#============================================================================= +# 项目模板重构脚本 +# 用于将coder-common-thin-backend项目重构为新的项目模板 +# +# 功能: +# 1. 重构项目名称和模块名 +# 2. 修改包名和groupId +# 3. 更新配置文件中的包名引用 +# 4. 重命名SQL文件和数据库名 +# 5. 处理多层级子模块结构 +# 6. 更新数据库连接字符串 +# 7. 更新文件路径配置 +# +# 作者: Leocoder +# 版本: 1.2.3 +#============================================================================= + +set -e # 遇到错误立即退出 + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_step() { + echo -e "${BLUE}[STEP]${NC} $1" +} + +# 当前项目信息 +CURRENT_PROJECT_NAME="coder-common-thin-backend" +CURRENT_GROUP_ID="org.leocoder.thin" +CURRENT_PACKAGE_NAME="org.leocoder.thin" +CURRENT_MODULE_PREFIX="coder-common-thin" +CURRENT_DB_NAME="coder-common-thin" + +# 脚本目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +log_info "=== 项目模板重构脚本 v1.2.3 ===" +log_info "项目根目录: $PROJECT_ROOT" + +# 验证当前目录 +if [[ ! -f "$PROJECT_ROOT/pom.xml" ]]; then + log_error "当前目录不是有效的Maven项目根目录" + exit 1 +fi + +# 获取用户输入 +echo "" +log_step "请输入新项目的配置信息:" + +read -p "新项目名称 (例如: my-project-backend): " NEW_PROJECT_NAME +read -p "新的GroupId (例如: com.company.project): " NEW_GROUP_ID +read -p "新的包名 (例如: com.company.project): " NEW_PACKAGE_NAME +read -p "新的模块前缀 (例如: my-project): " NEW_MODULE_PREFIX +read -p "新的数据库名 (例如: my_project_db): " NEW_DB_NAME + +# 验证输入 +if [[ -z "$NEW_PROJECT_NAME" || -z "$NEW_GROUP_ID" || -z "$NEW_PACKAGE_NAME" || -z "$NEW_MODULE_PREFIX" || -z "$NEW_DB_NAME" ]]; then + log_error "所有参数都不能为空" + exit 1 +fi + +# 显示配置信息确认 +echo "" +log_info "=== 重构配置确认 ===" +echo "项目名称: $CURRENT_PROJECT_NAME -> $NEW_PROJECT_NAME" +echo "GroupId: $CURRENT_GROUP_ID -> $NEW_GROUP_ID" +echo "包名: $CURRENT_PACKAGE_NAME -> $NEW_PACKAGE_NAME" +echo "模块前缀: $CURRENT_MODULE_PREFIX -> $NEW_MODULE_PREFIX" +echo "数据库名: $CURRENT_DB_NAME -> $NEW_DB_NAME" +echo "" + +read -p "确认开始重构?[y/N]: " CONFIRM +if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then + log_info "重构已取消" + exit 0 +fi + +# 备份项目 +BACKUP_DIR="${PROJECT_ROOT}_backup_$(date +%Y%m%d_%H%M%S)" +log_step "创建项目备份: $BACKUP_DIR" +cp -r "$PROJECT_ROOT" "$BACKUP_DIR" +log_info "备份完成" + +# 重构函数 + +# 1. 重命名模块目录 +rename_module_directories() { + log_step "重命名模块目录..." + + cd "$PROJECT_ROOT" + + # 主模块重命名 + local modules=( + "$CURRENT_MODULE_PREFIX-web" + "$CURRENT_MODULE_PREFIX-common" + "$CURRENT_MODULE_PREFIX-model" + "$CURRENT_MODULE_PREFIX-mybatisplus" + "$CURRENT_MODULE_PREFIX-modules" + "$CURRENT_MODULE_PREFIX-plugins" + ) + + for module in "${modules[@]}"; do + if [[ -d "$module" ]]; then + new_module="${module/$CURRENT_MODULE_PREFIX/$NEW_MODULE_PREFIX}" + log_info "重命名: $module -> $new_module" + mv "$module" "$new_module" + fi + done + + # 递归重命名所有子模块(modules、plugins等) + local parent_modules=( + "$NEW_MODULE_PREFIX-modules" + "$NEW_MODULE_PREFIX-plugins" + ) + + for parent_module in "${parent_modules[@]}"; do + if [[ -d "$parent_module" ]]; then + log_info "处理子模块目录: $parent_module" + cd "$parent_module" + + # 重命名当前目录下的所有子模块 + for sub_dir in $CURRENT_MODULE_PREFIX-*; do + if [[ -d "$sub_dir" ]]; then + new_sub_dir="${sub_dir/$CURRENT_MODULE_PREFIX/$NEW_MODULE_PREFIX}" + log_info "重命名子模块: $sub_dir -> $new_sub_dir" + mv "$sub_dir" "$new_sub_dir" + + # 递归处理子模块中的子子模块(如果存在) + if [[ -d "$new_sub_dir" ]]; then + cd "$new_sub_dir" + for sub_sub_dir in $CURRENT_MODULE_PREFIX-*; do + if [[ -d "$sub_sub_dir" ]]; then + new_sub_sub_dir="${sub_sub_dir/$CURRENT_MODULE_PREFIX/$NEW_MODULE_PREFIX}" + log_info "重命名子子模块: $sub_sub_dir -> $new_sub_sub_dir" + mv "$sub_sub_dir" "$new_sub_sub_dir" + fi + done + cd ".." + fi + fi + done + cd "$PROJECT_ROOT" + fi + done + + log_info "模块目录重命名完成" +} + +# 2. 更新POM文件 +update_pom_files() { + log_step "更新POM文件..." + + # 查找所有pom.xml文件 + find "$PROJECT_ROOT" -name "pom.xml" -type f | while read pom_file; do + log_info "更新POM: $pom_file" + + # 使用sed进行替换 + sed -i.bak \ + -e "s|$CURRENT_GROUP_ID|$NEW_GROUP_ID|g" \ + -e "s|$CURRENT_PROJECT_NAME|$NEW_PROJECT_NAME|g" \ + -e "s|$CURRENT_PROJECT_NAME|$NEW_PROJECT_NAME|g" \ + -e "s|$CURRENT_MODULE_PREFIX-|$NEW_MODULE_PREFIX-|g" \ + "$pom_file" + + # 删除备份文件 + rm -f "${pom_file}.bak" + done + + log_info "POM文件更新完成" +} + +# 3. 更新Java包结构 +update_java_packages() { + log_step "更新Java包结构..." + + # 转换包名路径 + local current_package_path="${CURRENT_PACKAGE_NAME//./\/}" + local new_package_path="${NEW_PACKAGE_NAME//./\/}" + + log_info "包路径转换: $current_package_path -> $new_package_path" + + # 查找所有Java文件并更新包名和import语句 + find "$PROJECT_ROOT" -name "*.java" -type f | while read java_file; do + log_info "更新Java文件内容: $java_file" + + # 更新package声明和import语句,使用更精确的匹配(避免转义字符问题) + sed -i.bak \ + -e "s|^package ${CURRENT_PACKAGE_NAME};|package ${NEW_PACKAGE_NAME};|g" \ + -e "s|^package ${CURRENT_PACKAGE_NAME}[.]|package ${NEW_PACKAGE_NAME}.|g" \ + -e "s|^import ${CURRENT_PACKAGE_NAME};|import ${NEW_PACKAGE_NAME};|g" \ + -e "s|^import ${CURRENT_PACKAGE_NAME}[.]|import ${NEW_PACKAGE_NAME}.|g" \ + -e "s|${CURRENT_PACKAGE_NAME}[.]|${NEW_PACKAGE_NAME}.|g" \ + "$java_file" + + rm -f "${java_file}.bak" + done + + # 重构Java包目录结构 + log_info "开始重构Java包目录结构..." + + # 查找所有包含当前包路径的java源码目录 + find "$PROJECT_ROOT" -path "*/src/main/java/$current_package_path" -type d | while read old_package_dir; do + log_info "处理包目录: $old_package_dir" + + # 获取java源码根目录 + local java_root="${old_package_dir%/$current_package_path}" + local new_package_dir="$java_root/$new_package_path" + + log_info " 源目录: $old_package_dir" + log_info " 目标目录: $new_package_dir" + + # 创建新的包目录结构 + mkdir -p "$(dirname "$new_package_dir")" + + # 移动整个包目录到新位置 + if [[ -d "$old_package_dir" && "$old_package_dir" != "$new_package_dir" ]]; then + mv "$old_package_dir" "$new_package_dir" + log_info " 已移动包目录" + + # 清理空的父级目录 + local old_parent="$(dirname "$old_package_dir")" + while [[ "$old_parent" != "$java_root" && -d "$old_parent" ]]; do + if [[ -z "$(ls -A "$old_parent" 2>/dev/null)" ]]; then + rmdir "$old_parent" 2>/dev/null && log_info " 清理空目录: $old_parent" + old_parent="$(dirname "$old_parent")" + else + break + fi + done + fi + done + + log_info "Java包结构更新完成" +} + +# 4. 清理目录名中的反斜杠(独立函数) +cleanup_backslash_directories() { + log_step "清理目录名中的反斜杠..." + + local fixed_count=0 + local attempts=0 + + # 重复检查和修复,直到没有反斜杠目录或达到最大尝试次数 + while [[ $attempts -lt 5 ]]; do + local found_backslash=false + + # 查找包含反斜杠的目录 + for dir_path in $(find "$PROJECT_ROOT" -path "*/src/main/java/*" -type d 2>/dev/null); do + if [[ ! -d "$dir_path" ]]; then + continue + fi + + dir_name=$(basename "$dir_path") + + # 检查目录名是否包含反斜杠 + if [[ "$dir_name" == *"\\"* ]]; then + found_backslash=true + + # 清理反斜杠,获取正确的目录名 + clean_dir_name="${dir_name//\\/}" + parent_dir=$(dirname "$dir_path") + new_dir_path="$parent_dir/$clean_dir_name" + + log_info "修复目录名: '$dir_name' -> '$clean_dir_name'" + + # 重命名目录 + if [[ "$dir_path" != "$new_dir_path" && ! -e "$new_dir_path" ]]; then + if mv "$dir_path" "$new_dir_path" 2>/dev/null; then + log_info " ✓ 目录重命名成功: $(basename "$new_dir_path")" + ((fixed_count++)) + else + log_warn " ✗ 目录重命名失败: $dir_path" + fi + fi + fi + done + + # 如果没有找到反斜杠目录,退出循环 + if [[ "$found_backslash" == false ]]; then + break + fi + + ((attempts++)) + sleep 0.1 # 短暂等待,确保文件系统操作完成 + done + + if [[ $fixed_count -gt 0 ]]; then + log_info "总共修复了 $fixed_count 个包含反斜杠的目录(用了 $attempts 轮处理)" + else + log_info "未发现包含反斜杠的目录" + fi + + log_info "反斜杠清理完成" +} + +# 5. 更新配置文件 +update_config_files() { + log_step "更新配置文件..." + + # 更新application配置文件 + find "$PROJECT_ROOT" -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" | while read config_file; do + log_info "更新配置文件: $config_file" + + # 计算新的配置名称(兼容各种bash版本) + local new_pool_name="$(echo "$NEW_MODULE_PREFIX" | tr '[:lower:]' '[:upper:]')-HIKARI-DEV" + local new_project_name="$(echo "$NEW_MODULE_PREFIX" | tr '[:lower:]' '[:upper:]')-ADMIN" + + sed -i.bak \ + -e "s|packages-to-scan: $CURRENT_PACKAGE_NAME|packages-to-scan: $NEW_PACKAGE_NAME|g" \ + -e "s|$CURRENT_PACKAGE_NAME|$NEW_PACKAGE_NAME|g" \ + -e "s|name: coder-web|name: ${NEW_MODULE_PREFIX}-web|g" \ + -e "s|projectName: CORDER-ADMIN-THIN|projectName: $new_project_name|g" \ + -e "s|pool-name: CORDER-HIKARI-DEV|pool-name: $new_pool_name|g" \ + -e "s|jdbc:mysql://localhost:3306/$CURRENT_DB_NAME|jdbc:mysql://localhost:3306/$NEW_DB_NAME|g" \ + -e "s|jdbc:mysql://localhost:3306/$CURRENT_DB_NAME-backup|jdbc:mysql://localhost:3306/$NEW_DB_NAME-backup|g" \ + -e "s|/$CURRENT_PROJECT_NAME/|/$NEW_PROJECT_NAME/|g" \ + -e "s|$CURRENT_MODULE_PREFIX|$NEW_MODULE_PREFIX|g" \ + "$config_file" + + rm -f "${config_file}.bak" + done + + # 更新logback配置文件(专门处理logback特有的配置) + find "$PROJECT_ROOT" -name "logback*.xml" | while read logback_file; do + log_info "更新Logback配置: $logback_file" + + # 计算新的日志配置名称(兼容各种bash版本) + local new_context_name="${NEW_MODULE_PREFIX}-logback" + local new_property_name="$(echo "$NEW_MODULE_PREFIX" | tr '[:lower:]' '[:upper:]')_ADMIN_LOGS" + + sed -i.bak \ + -e "s|$CURRENT_PACKAGE_NAME|$NEW_PACKAGE_NAME|g" \ + -e "s|$CURRENT_MODULE_PREFIX-logback|$new_context_name|g" \ + -e "s|CORDER_ADMIN_LOGS|$new_property_name|g" \ + -e "s|name=\"CORDER_ADMIN_LOGS\"|name=\"$new_property_name\"|g" \ + "$logback_file" + + rm -f "${logback_file}.bak" + done + + # 更新MyBatis XML文件(处理namespace和resultType) + find "$PROJECT_ROOT" -name "*.xml" -path "*/mapper/*" | while read mapper_file; do + log_info "更新MyBatis配置: $mapper_file" + + sed -i.bak \ + -e "s|namespace=\"$CURRENT_PACKAGE_NAME|namespace=\"$NEW_PACKAGE_NAME|g" \ + -e "s|resultType=\"$CURRENT_PACKAGE_NAME|resultType=\"$NEW_PACKAGE_NAME|g" \ + -e "s|parameterType=\"$CURRENT_PACKAGE_NAME|parameterType=\"$NEW_PACKAGE_NAME|g" \ + "$mapper_file" + + rm -f "${mapper_file}.bak" + done + + log_info "配置文件更新完成" +} + +# 6. 更新SQL文件 +update_sql_files() { + log_step "更新SQL文件..." + + local sql_dir="$PROJECT_ROOT/sql" + if [[ -d "$sql_dir" ]]; then + # 重命名SQL文件 + local current_sql_file="$sql_dir/$CURRENT_DB_NAME.sql" + local new_sql_file="$sql_dir/$NEW_DB_NAME.sql" + + if [[ -f "$current_sql_file" ]]; then + log_info "重命名SQL文件: $CURRENT_DB_NAME.sql -> $NEW_DB_NAME.sql" + mv "$current_sql_file" "$new_sql_file" + + # 更新SQL文件内容中的数据库名引用 + sed -i.bak \ + -e "s|Source Schema.*: $CURRENT_DB_NAME|Source Schema : $NEW_DB_NAME|g" \ + -e "s|$CURRENT_DB_NAME|$NEW_DB_NAME|g" \ + "$new_sql_file" + + rm -f "${new_sql_file}.bak" + fi + fi + + log_info "SQL文件更新完成" +} + +# 7. 更新其他文件 +update_other_files() { + log_step "更新其他相关文件..." + + # 更新README文件 + find "$PROJECT_ROOT" -name "README*" -type f | while read readme_file; do + log_info "更新README: $readme_file" + + sed -i.bak \ + -e "s|$CURRENT_PROJECT_NAME|$NEW_PROJECT_NAME|g" \ + -e "s|$CURRENT_MODULE_PREFIX|$NEW_MODULE_PREFIX|g" \ + -e "s|$CURRENT_PACKAGE_NAME|$NEW_PACKAGE_NAME|g" \ + "$readme_file" + + rm -f "${readme_file}.bak" + done + + # 更新CLAUDE.md文件 + if [[ -f "$PROJECT_ROOT/CLAUDE.md" ]]; then + log_info "更新CLAUDE.md" + + sed -i.bak \ + -e "s|$CURRENT_PROJECT_NAME|$NEW_PROJECT_NAME|g" \ + -e "s|$CURRENT_MODULE_PREFIX|$NEW_MODULE_PREFIX|g" \ + -e "s|$CURRENT_PACKAGE_NAME|$NEW_PACKAGE_NAME|g" \ + "$PROJECT_ROOT/CLAUDE.md" + + rm -f "$PROJECT_ROOT/CLAUDE.md.bak" + fi + + # 更新其他配置文件(如果存在) + find "$PROJECT_ROOT" -name "*.properties" -o -name "*.yml" -o -name "*.yaml" | while read prop_file; do + if [[ "$prop_file" != *"application"* ]]; then + log_info "更新属性文件: $prop_file" + + sed -i.bak \ + -e "s|$CURRENT_PACKAGE_NAME|$NEW_PACKAGE_NAME|g" \ + -e "s|$CURRENT_MODULE_PREFIX|$NEW_MODULE_PREFIX|g" \ + "$prop_file" + + rm -f "${prop_file}.bak" + fi + done + + log_info "其他文件更新完成" +} + +# 8. 清理和验证 +cleanup_and_verify() { + log_step "清理临时文件和验证..." + + # 清理备份文件 + find "$PROJECT_ROOT" -name "*.bak" -delete + + # 验证重构结果 + log_info "验证重构结果..." + + # 检查是否还有旧的引用(排除脚本文件、日志文件、备份等) + local old_refs=$(grep -r "$CURRENT_PACKAGE_NAME" "$PROJECT_ROOT" \ + --exclude-dir=".git" \ + --exclude-dir="target" \ + --exclude-dir=".idea" \ + --exclude-dir="script" \ + --exclude="*.log" \ + --exclude="spy.log" \ + --exclude="*.bak" \ + --exclude="*.sh" \ + 2>/dev/null | wc -l) + + if [[ $old_refs -gt 0 ]]; then + log_warn "发现 $old_refs 处旧包名引用,请手动检查" + grep -r "$CURRENT_PACKAGE_NAME" "$PROJECT_ROOT" \ + --exclude-dir=".git" \ + --exclude-dir="target" \ + --exclude-dir=".idea" \ + --exclude-dir="script" \ + --exclude="*.log" \ + --exclude="spy.log" \ + --exclude="*.bak" \ + --exclude="*.sh" \ + 2>/dev/null | head -10 + else + log_info "✓ 验证通过:未发现旧包名引用" + fi + + log_info "清理完成" +} + +# 9. 重命名项目根目录 +rename_project_directory() { + log_step "重命名项目根目录..." + + # 获取当前项目目录名 + local current_dir_name=$(basename "$PROJECT_ROOT") + local parent_dir=$(dirname "$PROJECT_ROOT") + local new_project_dir="$parent_dir/$NEW_PROJECT_NAME" + + log_info "项目目录重命名: $current_dir_name -> $NEW_PROJECT_NAME" + + # 检查目标目录是否已存在 + if [[ -d "$new_project_dir" && "$PROJECT_ROOT" != "$new_project_dir" ]]; then + log_warn "目标目录已存在: $new_project_dir" + log_warn "跳过项目目录重命名" + return + fi + + # 重命名项目目录 + if [[ "$PROJECT_ROOT" != "$new_project_dir" ]]; then + log_info "移动项目目录: $PROJECT_ROOT -> $new_project_dir" + + # 切换到父目录 + cd "$parent_dir" + + # 重命名目录 + if mv "$current_dir_name" "$NEW_PROJECT_NAME" 2>/dev/null; then + log_info "✓ 项目目录重命名成功" + log_info "新的项目路径: $new_project_dir" + + # 更新PROJECT_ROOT变量为新路径 + PROJECT_ROOT="$new_project_dir" + else + log_warn "✗ 项目目录重命名失败,请手动重命名" + fi + else + log_info "项目目录名已正确,无需重命名" + fi + + log_info "项目目录处理完成" +} + +# 执行重构 +echo "" +log_info "=== 开始执行重构 ===" + +rename_module_directories +update_pom_files +update_java_packages +cleanup_backslash_directories +update_config_files +update_sql_files +update_other_files +cleanup_and_verify +rename_project_directory + +echo "" +log_info "=== 重构完成 ===" +log_info "新项目名称: $NEW_PROJECT_NAME" +log_info "新的GroupId: $NEW_GROUP_ID" +log_info "新的包名: $NEW_PACKAGE_NAME" +log_info "新的模块前缀: $NEW_MODULE_PREFIX" +log_info "新的数据库名: $NEW_DB_NAME" +echo "" +log_info "项目备份位置: $BACKUP_DIR" +log_info "新项目位置: $PROJECT_ROOT" +echo "" +log_warn "重构完成后请执行以下操作:" +echo "1. 进入新项目目录: cd \"$PROJECT_ROOT\"" +echo "2. 验证项目能否正常编译: mvn clean compile" +echo "3. 更新数据库连接配置中的数据库名" +echo "4. 在IDE中重新导入项目,检查包结构" +echo "5. 提交代码到新的Git仓库" +echo "" + +log_info "感谢使用项目模板重构脚本!" \ No newline at end of file