--- url: 'https://ruoyi.plus/demo.md' --- # 🎯 项目演示 体验 ruoyi-plus-uniapp 全栈开发框架的演示功能 ### **原价: ~~¥2580~~** **限时优惠: ¥2000** ## 💬 技术支持 ::: warning 联系方式 * **作者微信/QQ:** 770492966 * **项目文档:** https://ruoyi.plus ::: ## 🌐 在线演示 | 平台 | 地址 | 账密 | 说明 | |--------------|---------------------------------------------------------------------|------------------|--------------------------| | 💻 **后台管理** | [立即体验](https://ui.ruoyi.plus/login?redirect=/index?tenantId=424410) | admin / admin123 | Vue3 + Element Plus 管理后台 | | 📱 **移动端H5** | [立即体验](http://uni.ruoyi.plus) | admin / admin123 | UniApp 跨平台应用 | ## 🖼️ 功能截图 ### 后台管理系统 [//]: # [//]: # "::: details 点击查看更多截图" [//]: # "![仪表板](/images/admin/dashboard.png)" [//]: # "*数据仪表板 - 实时监控系统状态*" [//]: # [//]: # "![用户管理](/images/admin/user-management.png)" [//]: # "*用户管理 - 完整的权限控制体系*" [//]: # [//]: # "![代码生成](/images/admin/code-generator.png)" [//]: # "*代码生成 - 一键生成CRUD代码*" [//]: # ":::" [//]: # [//]: # "### 移动端应用" [//]: # [//]: # "::: details 移动端界面展示" [//]: # "![首页](/images/mobile/home.png)" [//]: # "![登录](/images/mobile/login.png)" [//]: # "![用户中心](/images/mobile/user-center.png)" [//]: # "*移动端主要界面展示*" [//]: # ":::" *** © 2025 ruoyi-plus-uniapp | 框架商用授权,详情咨询 --- --- url: 'https://ruoyi.plus/video.md' --- # 📺 视频教程 * **[1. Ruoyi-Plus-Uniapp新特性全面解析](https://www.bilibili.com/video/BV1YrtMzvEaT/)** --- --- url: 'https://ruoyi.plus/mobile/platform/android.md' --- # Android App适配 --- --- url: 'https://ruoyi.plus/backend/common/encrypt/api-encryption.md' --- # API接口加密 (API Encryption) API接口加密功能通过Servlet过滤器实现,支持前后端数据传输的全链路加密,采用RSA+AES混合加密方案,既保证了安全性又兼顾了性能。 ## 配置说明 ### 基础配置 在 `application.yml` 中配置API加密参数: ```yaml api-decrypt: # 功能开关 enabled: true # 请求头中加密密钥的标识字段 header-flag: "encrypt-key" # RSA公钥(用于加密响应中的AES密钥) public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." # RSA私钥(用于解密请求中的AES密钥) private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC..." ``` ### 配置项说明 | 配置项 | 说明 | 默认值 | |--------|------|--------| | `enabled` | 是否启用API加密功能 | false | | `header-flag` | 请求头中存放加密密钥的字段名 | encrypt-key | | `public-key` | RSA公钥,用于加密响应中的AES密钥 | - | | `private-key` | RSA私钥,用于解密请求中的AES密钥 | - | ## @ApiEncrypt 注解使用 ### 基础用法 ```java @RestController @RequestMapping("/api/user") public class UserController { // 普通接口(不加密) @GetMapping("/info") public Result getUserInfo() { return Result.success(userService.getCurrentUser()); } // 对响应进行加密 @PostMapping("/login") @ApiEncrypt(response = true) public Result login(@RequestBody LoginRequest request) { // 请求自动解密,响应自动加密 LoginResponse response = userService.login(request); return Result.success(response); } // 敏感数据接口 @GetMapping("/profile") @ApiEncrypt(response = true) public Result getUserProfile() { // 返回的用户详细信息会被加密 UserProfile profile = userService.getUserProfile(); return Result.success(profile); } } ``` ### 注解参数 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `response` | boolean | false | 是否对响应进行加密 | **注意**: * 请求解密是自动的,只要请求头包含加密标识就会解密 * 响应加密需要显式设置 `response = true` ## 加密流程详解 ### 请求加密流程 ```mermaid sequenceDiagram participant C as 客户端 participant S as 服务端 C->>C: 1. 生成32位AES密钥 C->>C: 2. 用AES密钥加密请求体 C->>C: 3. 对AES密钥进行Base64编码 C->>C: 4. 用服务端RSA公钥加密Base64编码的AES密钥 C->>S: 5. 发送请求(加密的请求体 + 请求头中的加密AES密钥) S->>S: 6. 用RSA私钥解密获得Base64编码的AES密钥 S->>S: 7. 对AES密钥进行Base64解码 S->>S: 8. 用AES密钥解密请求体 S->>S: 9. 处理业务逻辑 ``` ### 响应加密流程 ```mermaid sequenceDiagram participant S as 服务端 participant C as 客户端 S->>S: 1. 处理业务逻辑得到响应数据 S->>S: 2. 生成新的32位AES密钥 S->>S: 3. 用AES密钥加密响应体 S->>S: 4. 对AES密钥进行Base64编码 S->>S: 5. 用RSA公钥加密Base64编码的AES密钥 S->>C: 6. 返回响应(加密的响应体 + 响应头中的加密AES密钥) C->>C: 7. 用RSA私钥解密获得Base64编码的AES密钥 C->>C: 8. 对AES密钥进行Base64解码 C->>C: 9. 用AES密钥解密响应体 ``` ## 前端对接实现 ### 环境变量配置 在前端项目的 `.env` 文件中配置加密相关参数: ```bash # 接口加密功能开关(如需关闭 后端也必须对应关闭) VITE_APP_API_ENCRYPT = 'true' # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK9s1Pbnn5W+l1hx3ukHLtevayF...' # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换 VITE_APP_RSA_PRIVATE_KEY = 'MIIBOwIBAAJBAIrZxEhzVAHKJm7BJpXIHWGU3sHJYgRiOOTw3Auj...' ``` **⚠️ 安全提醒:** * 以上为示例密钥,生产环境请使用您自己生成的密钥对 * 密钥应存储在安全的配置中心,不要直接写在代码中 * 定期轮换密钥以确保安全性 ### 系统配置 在 `SystemConfig` 中配置安全选项: ```typescript export const SystemConfig = { security: { // 是否启用API加密(从环境变量读取) apiEncrypt: process.env.VITE_APP_API_ENCRYPT === 'true', }, // 其他配置... } ``` ### 使用示例 #### 1. 加密请求示例 ```typescript import { http } from '@/composables/useHttp' // 发送加密的登录请求 const login = async (loginData: LoginRequest) => { const [err, result] = await http.post('/api/auth/login', loginData, { header: { isEncrypt: true // 开启请求加密 } }) if (!err) { console.log('登录成功', result) } else { console.error('登录失败', err.message) } } // 发送加密的用户信息更新请求 const updateProfile = async (profileData: UserProfile) => { const [err] = await http.put('/api/user/profile', profileData, { header: { isEncrypt: true // 开启请求加密 } }) if (!err) { console.log('更新成功') } } ``` #### 2. 接收加密响应 ```typescript // 响应加密由后端 @ApiEncrypt(response = true) 注解控制 // 前端会自动检测响应头中的加密标识并解密 const getUserInfo = async () => { // 后端接口标注了 @ApiEncrypt(response = true),响应会被自动解密 const [err, userInfo] = await http.get('/api/user/info') if (!err) { console.log('用户信息', userInfo) // 已自动解密的数据 } } ``` ### 核心实现原理 #### 1. 请求加密处理 ```typescript /** * 加密请求数据 */ const encryptRequestData = (data: any, header: Record) => { if (!SystemConfig.security?.apiEncrypt || !data) return data // 1. 生成32位AES密钥 const aesKey = generateAesKey() // 2. 用后端RSA公钥加密AES密钥并放入请求头 header[ENCRYPT_HEADER] = rsaEncrypt(encodeBase64(aesKey)) // 3. 用AES密钥加密请求数据 return typeof data === 'object' ? encryptWithAes(JSON.stringify(data), aesKey) : encryptWithAes(data, aesKey) } ``` #### 2. 响应解密处理 ```typescript /** * 解密响应数据 */ const decryptResponseData = (data: any, header: Record): any => { if (!SystemConfig.security?.apiEncrypt) return data // 1. 从响应头获取加密的AES密钥 const encryptKey = header[ENCRYPT_HEADER] || header[ENCRYPT_HEADER.toLowerCase()] if (!encryptKey) return data try { // 2. 用前端RSA私钥解密AES密钥 const base64Str = rsaDecrypt(encryptKey) const aesKey = decodeBase64(base64Str) // 3. 用AES密钥解密响应数据 const decryptedData = decryptWithAes(data, aesKey) return JSON.parse(decryptedData) } catch (error) { console.error('[响应解密失败]', error) throw new Error('响应数据解密失败') } } ``` ### 加密流程详解 #### 请求加密流程 ```mermaid sequenceDiagram participant F as 前端 participant S as 后端 F->>F: 1. 生成32位随机AES密钥 F->>F: 2. 用AES密钥加密请求体数据 F->>F: 3. 对AES密钥进行Base64编码 F->>F: 4. 用后端RSA公钥加密Base64编码的AES密钥 F->>S: 5. 发送请求(加密的请求体 + encrypt-key请求头) S->>S: 6. 用RSA私钥解密获得Base64编码的AES密钥 S->>S: 7. Base64解码获得原始AES密钥 S->>S: 8. 用AES密钥解密请求体 S->>S: 9. 处理业务逻辑 ``` #### 响应加密流程 ```mermaid sequenceDiagram participant S as 后端 participant F as 前端 S->>S: 1. 业务处理完成,准备响应数据 S->>S: 2. 生成新的32位随机AES密钥 S->>S: 3. 用AES密钥加密响应体数据 S->>S: 4. 对AES密钥进行Base64编码 S->>S: 5. 用RSA公钥加密Base64编码的AES密钥 S->>F: 6. 返回响应(加密的响应体 + encrypt-key响应头) F->>F: 7. 用RSA私钥解密获得Base64编码的AES密钥 F->>F: 8. Base64解码获得原始AES密钥 F->>F: 9. 用AES密钥解密响应体 F->>F: 10. 解析JSON数据供业务使用 ``` ### 密钥配置说明 #### 前后端密钥对应关系 ```bash # 后端配置(application.yml) api-decrypt: # 响应加密公钥 - 用于加密响应中的AES密钥 # 对应前端解密私钥 public-key: "后端响应加密公钥" # 请求解密私钥 - 用于解密请求中的AES密钥 # 对应前端加密公钥 private-key: "后端请求解密私钥" # 前端配置(.env) # 请求加密公钥 - 用于加密请求中的AES密钥 # 对应后端解密私钥 VITE_APP_RSA_PUBLIC_KEY = "前端请求加密公钥" # 响应解密私钥 - 用于解密响应中的AES密钥 # 对应后端加密公钥 VITE_APP_RSA_PRIVATE_KEY = "前端响应解密私钥" ``` #### 密钥对应关系图 ``` 请求加密流程: 前端请求加密公钥 ←→ 后端请求解密私钥 (密钥对A) 响应加密流程: 后端响应加密公钥 ←→ 前端响应解密私钥 (密钥对B) ``` **关键要点**: * 后端配置中的 `publicKey` 和 `privateKey` **不是一对密钥** * 需要生成**两对独立的RSA密钥**: * **密钥对A**:用于请求加密(前端公钥A + 后端私钥A) * **密钥对B**:用于响应加密(后端公钥B + 前端私钥B) * 前端用公钥A加密请求,后端用私钥A解密请求 * 后端用公钥B加密响应,前端用私钥B解密响应 --- --- url: 'https://ruoyi.plus/mobile/api/overview.md' --- # API概览 --- --- url: 'https://ruoyi.plus/frontend/types/api-types.md' --- # API类型 --- --- url: 'https://ruoyi.plus/mobile/build/app-cloud-build.md' --- # App云打包 --- --- url: 'https://ruoyi.plus/mobile/build/app-offline-build.md' --- # App离线打包 --- --- url: 'https://ruoyi.plus/frontend/utils/boolean.md' --- # Boolean工具 --- --- url: 'https://ruoyi.plus/mobile/layouts/demo.md' --- # Demo布局 (demo) --- --- url: 'https://ruoyi.plus/practices/devops-best-practices.md' --- # DevOps最佳实践 --- --- url: 'https://ruoyi.plus/frontend/utils/dom.md' --- # DOM工具 --- --- url: 'https://ruoyi.plus/frontend/dev/eslint-config.md' --- # ESLint配置 --- --- url: 'https://ruoyi.plus/backend/common/excel.md' --- # Excel处理 (excel) Excel处理模块基于EasyExcel框架,提供了完整的Excel导入导出解决方案,支持数据校验、格式转换、单元格合并、下拉选项等高级功能。 ## 模块概述 ### 核心功能 * **Excel导入**:支持同步/异步导入,数据校验,错误收集 * **Excel导出**:支持基础导出、模板导出、多Sheet导出 * **数据转换**:字典转换、枚举转换、大数字处理 * **界面增强**:下拉选项、单元格合并、批注、必填标识 * **模板支持**:单表模板、多表模板、多Sheet模板 ### 技术特性 * 基于EasyExcel 3.x版本 * 支持大文件处理,内存占用低 * 注解驱动,配置简单 * 完善的异常处理机制 * 灵活的扩展能力 ## 快速开始 ### 基础导入示例 ```java // 1. 定义实体类 public class User { @ExcelProperty("姓名") private String name; @ExcelProperty("年龄") private Integer age; @ExcelProperty("邮箱") @Email(message = "邮箱格式不正确") private String email; } // 2. 执行导入 @PostMapping("/import") public Result importUser(@RequestParam("file") MultipartFile file) { List users = ExcelUtil.importExcel(file.getInputStream(), User.class); // 处理导入的数据 userService.batchInsert(users); return Result.success("导入成功,共" + users.size() + "条数据"); } ``` ### 基础导出示例 ```java // 导出用户列表 @GetMapping("/export") public void exportUser(HttpServletResponse response) { List users = userService.list(); ExcelUtil.exportExcel(users, "用户信息", User.class, response); } ``` ## 详细功能 ### 1. Excel导入 #### 1.1 同步导入 适用于小数据量(建议1000条以内)的简单导入场景。 ```java // 导入为Map集合(不需要实体类) List> mapData = ExcelUtil.importExcel(inputStream); // 导入为实体对象 List users = ExcelUtil.importExcel(inputStream, User.class); ``` #### 1.2 异步导入(推荐) 支持数据校验,提供详细的错误信息,适用于生产环境。 ```java // 启用数据校验的导入 ExcelResult result = ExcelUtil.importExcel(inputStream, User.class, true); // 获取成功数据 List successList = result.getList(); // 获取错误信息 List errors = result.getErrorList(); // 获取统计信息 String analysis = result.getAnalysis(); // 如:读取完成!成功100条,失败5条 ``` #### 1.3 自定义监听器 对于复杂的导入逻辑,可以实现自定义监听器。 ```java public class CustomUserListener extends DefaultExcelListener { @Override public void invoke(User user, AnalysisContext context) { // 自定义数据处理逻辑 if (StringUtils.isNotBlank(user.getPhone())) { user.setPhone(formatPhone(user.getPhone())); } // 调用父类方法执行默认处理 super.invoke(user, context); } @Override public void onException(Exception exception, AnalysisContext context) { // 自定义异常处理 log.error("数据处理异常", exception); super.onException(exception, context); } } // 使用自定义监听器 CustomUserListener listener = new CustomUserListener(); ExcelResult result = ExcelUtil.importExcel(inputStream, User.class, listener); ``` ### 2. Excel导出 #### 2.1 基础导出 ```java // 导出到HTTP响应 ExcelUtil.exportExcel(userList, "用户信息", User.class, response); // 导出到输出流 ExcelUtil.exportExcel(userList, "用户信息", User.class, outputStream); ``` #### 2.2 单元格合并导出 适用于相同数据需要合并显示的场景,如部门统计报表。 ```java // 实体类配置 public class DepartmentUser { @CellMerge // 相同部门的单元格将被合并 @ExcelProperty("部门") private String department; @CellMerge(mergeBy = {"department"}) // 在同一部门内合并相同团队 @ExcelProperty("团队") private String team; @ExcelProperty("姓名") private String name; } // 导出时启用合并 ExcelUtil.exportExcel(deptUsers, "部门用户", DepartmentUser.class, true, response); ``` #### 2.3 下拉选项导出 为Excel添加下拉选择功能,提升数据录入体验。 ```java // 创建下拉选项 List options = Arrays.asList( new DropDownOptions(2, Arrays.asList("男", "女")), // 第3列性别下拉 new DropDownOptions(3, Arrays.asList("在职", "离职", "试用")) // 第4列状态下拉 ); ExcelUtil.exportExcel(userList, "用户信息", User.class, response, options); ``` #### 2.4 级联下拉选项 支持二级联动下拉选择,如省市级联。 ```java // 构建级联下拉选项 DropDownOptions cascadeOptions = DropDownOptions.buildLinkedOptions( provinceList, // 省份列表 1, // 省份列索引 cityList, // 城市列表 2, // 城市列索引 Province::getId, // 省份ID获取方法 City::getProvinceId, // 城市获取省份ID方法 province -> DropDownOptions.createOptionValue(province.getId(), province.getName()) // 选项生成方法 ); ExcelUtil.exportExcel(addressList, "地址信息", Address.class, response, Arrays.asList(cascadeOptions)); ``` ### 3. 模板导出 适用于格式固定的报表导出,支持复杂的Excel布局。 #### 3.1 单表模板导出 ```java // 模板文件:resources/templates/user-report.xlsx // 模板内容使用 {.属性名} 作为占位符 // 例如:姓名:{.name},年龄:{.age},部门:{.department} List users = userService.getReportData(); ExcelUtil.exportTemplate(users, "用户报表", "templates/user-report.xlsx", response); ``` #### 3.2 多表模板导出 在一个模板中填充多个不同类型的数据。 ```java // 模板内容使用 {key.属性名} 作为占位符 // 用户信息:{userInfo.name} {userInfo.department} // 订单列表:{orders.orderNo} {orders.amount} Map data = new HashMap<>(); data.put("userInfo", userObject); // 单个对象 data.put("orders", orderList); // 列表数据 data.put("summary", summaryObject); // 汇总信息 ExcelUtil.exportTemplateMultiList(data, "综合报表", "templates/multi-report.xlsx", response); ``` #### 3.3 多Sheet模板导出 使用相同模板生成多个工作表。 ```java List> sheetDataList = new ArrayList<>(); // 第一个Sheet的数据 Map sheet1Data = new HashMap<>(); sheet1Data.put("users", dept1Users); sheet1Data.put("summary", dept1Summary); sheetDataList.add(sheet1Data); // 第二个Sheet的数据 Map sheet2Data = new HashMap<>(); sheet2Data.put("users", dept2Users); sheet2Data.put("summary", dept2Summary); sheetDataList.add(sheet2Data); ExcelUtil.exportTemplateMultiSheet(sheetDataList, "各部门报表", "templates/dept-report.xlsx", response); ``` ## 注解详解 ### 1. @ExcelProperty EasyExcel原生注解,用于定义Excel列映射。 ```java public class User { @ExcelProperty(value = "姓名", index = 0) private String name; @ExcelProperty(value = {"基本信息", "年龄"}, index = 1) // 多级表头 private Integer age; } ``` ### 2. @ExcelDictFormat 字典格式化注解,支持字典值转换。 ```java public class User { // 使用系统字典 @ExcelDictFormat(dictType = "sys_user_gender") @ExcelProperty("性别") private String gender; // 使用自定义映射 @ExcelDictFormat(readConverterExp = "0=正常,1=停用,2=删除") @ExcelProperty("状态") private String status; // 多值字段(如角色) @ExcelDictFormat(dictType = "sys_role", separator = ",") @ExcelProperty("角色") private String roles; } ``` **转换逻辑:** * **导出时**:数据库的code值(如"0")转换为显示文本(如"正常") * **导入时**:Excel的显示文本(如"正常")转换为code值(如"0") ### 3. @ExcelEnumFormat 枚举格式化注解,支持枚举类型转换。 ```java // 枚举定义 public enum UserStatus { ACTIVE(1, "激活"), INACTIVE(0, "禁用"); private final Integer value; private final String label; // 构造函数和getter方法... } // 实体类使用 public class User { @ExcelEnumFormat(enumClass = UserStatus.class, valueField = "value", labelField = "label") @ExcelProperty("状态") private Integer status; // 存储枚举的value值 } ``` ### 4. @CellMerge 单元格合并注解,用于合并相同值的单元格。 ```java public class DepartmentUser { @CellMerge @ExcelProperty("部门") private String department; @CellMerge(mergeBy = {"department"}) // 依赖部门字段 @ExcelProperty("团队") private String team; @ExcelProperty("姓名") private String name; } ``` ### 5. @ExcelRequired 必填字段标识注解,用于标记必填列。 ```java public class User { @ExcelRequired // 使用默认红色字体 @ExcelProperty("姓名") private String name; @ExcelRequired(fontColor = IndexedColors.BLUE) @ExcelProperty("邮箱") private String email; } ``` ### 6. @ExcelNotation 批注注解,为Excel单元格添加批注说明。 ```java public class User { @ExcelNotation("请填写真实姓名") @ExcelProperty("姓名") private String name; @ExcelNotation("格式:yyyy-MM-dd") @ExcelProperty("出生日期") private Date birthDate; } ``` ## 高级特性 ### 1. 大数字处理 自动处理长整型数据,防止Excel显示科学计数法。 ```java public class User { @ExcelProperty("用户ID") private Long userId; // 如:1234567890123456789L,自动转为字符串防止精度丢失 @ExcelProperty("手机号") private Long phone; // 自动处理,保证显示完整 } ``` ### 2. 数据校验 支持Bean Validation校验注解。 ```java public class User { @NotBlank(message = "姓名不能为空") @Length(max = 50, message = "姓名长度不能超过50") @ExcelProperty("姓名") private String name; @Min(value = 0, message = "年龄不能为负数") @Max(value = 150, message = "年龄不能超过150") @ExcelProperty("年龄") private Integer age; @Email(message = "邮箱格式不正确") @ExcelProperty("邮箱") private String email; } ``` ### 3. 自定义样式 ```java // 自定义表头和内容样式 HorizontalCellStyleStrategy styleStrategy = ExcelUtil.initCellStyle(); // 或者完全自定义 WriteCellStyle headStyle = new WriteCellStyle(); headStyle.setFillForegroundColor(IndexedColors.BLUE.getIndex()); WriteFont headFont = new WriteFont(); headFont.setColor(IndexedColors.WHITE.getIndex()); headFont.setBold(true); headStyle.setWriteFont(headFont); HorizontalCellStyleStrategy customStyle = new HorizontalCellStyleStrategy(headStyle, null); ``` ### 4. 错误处理和日志 ```java @PostMapping("/import") public Result importUsers(@RequestParam("file") MultipartFile file) { try { ExcelResult result = ExcelUtil.importExcel(file.getInputStream(), User.class, true); List successList = result.getList(); List errorList = result.getErrorList(); if (!errorList.isEmpty()) { // 返回部分成功的结果 return Result.success(ImportResult.builder() .successCount(successList.size()) .errorCount(errorList.size()) .errors(errorList) .build()); } // 批量保存成功数据 userService.batchSave(successList); return Result.success("导入成功,共" + successList.size() + "条数据"); } catch (Exception e) { log.error("导入用户失败", e); return Result.error("导入失败:" + e.getMessage()); } } ``` ## 性能优化建议 ### 1. 大文件处理 ```java // 对于大文件导入,使用自定义监听器分批处理 public class BatchUserListener extends DefaultExcelListener { private final int BATCH_SIZE = 1000; private List batchList = new ArrayList<>(); @Override public void invoke(User user, AnalysisContext context) { batchList.add(user); if (batchList.size() >= BATCH_SIZE) { // 分批保存 userService.batchSave(batchList); batchList.clear(); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { // 处理剩余数据 if (!batchList.isEmpty()) { userService.batchSave(batchList); } } } ``` ### 2. 内存优化 ```java // 导出大量数据时,避免一次性加载所有数据 @GetMapping("/export-large") public void exportLargeData(HttpServletResponse response) { ExcelUtil.resetResponse("大数据导出", response); try (ServletOutputStream os = response.getOutputStream()) { ExcelWriter excelWriter = FastExcel.write(os, User.class).build(); WriteSheet writeSheet = FastExcel.writerSheet("用户数据").build(); int pageSize = 10000; int pageNum = 1; List users; do { // 分页查询数据 users = userService.getUsers(pageNum, pageSize); if (!users.isEmpty()) { excelWriter.write(users, writeSheet); } pageNum++; } while (users.size() == pageSize); excelWriter.finish(); } } ``` ## 常见问题 ### Q1: 日期格式问题 ```java public class User { @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd") @ExcelProperty("入职日期") private Date joinDate; } ``` ### Q2: 数字精度问题 长整型数据自动使用`ExcelBigNumberConvert`转换器处理,无需额外配置。 ### Q3: 中文乱码问题 确保使用UTF-8编码: ```java response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"); ``` ### Q4: 模板路径问题 模板文件放在`src/main/resources`目录下: ```java // 正确的路径写法 ExcelUtil.exportTemplate(data, "报表", "templates/report.xlsx", response); ``` ### Q5: 字典转换不生效 确保字典服务`DictService`已正确配置且Spring容器中存在对应的Bean。 ## 最佳实践 1. **实体类设计**:合理使用注解,字段命名清晰 2. **异常处理**:使用异步导入,妥善处理错误信息 3. **性能考虑**:大文件分批处理,避免内存溢出 4. **用户体验**:提供下拉选项,添加必填标识和批注 5. **模板设计**:模板格式简洁,占位符命名规范 ## 总结 Excel处理模块提供了完整的Excel导入导出解决方案,通过合理使用各种注解和配置,可以满足绝大多数业务场景的需求。模块设计注重性能和易用性,支持灵活扩展,是企业级应用的理想选择。 --- --- url: 'https://ruoyi.plus/practices/git-standards.md' --- # Git使用规范 --- --- url: 'https://ruoyi.plus/mobile/build/h5-deploy.md' --- # H5打包发布 --- --- url: 'https://ruoyi.plus/mobile/platform/h5.md' --- # H5适配 --- --- url: 'https://ruoyi.plus/mobile/uniapp/hbuilderx.md' --- # HBuilderX使用 --- --- url: 'https://ruoyi.plus/frontend/utils/http.md' --- # HTTP请求 --- --- url: 'https://ruoyi.plus/mobile/utils/http.md' --- # HTTP请求工具 --- --- url: 'https://ruoyi.plus/mobile/platform/ios.md' --- # iOS App适配 --- --- url: 'https://ruoyi.plus/backend/common/json.md' --- # JSON处理模块 ## 概述 JSON处理模块(`ruoyi-common-json`)提供了基于Jackson的JSON序列化和反序列化功能,主要解决以下问题: * **JavaScript精度丢失**:大数值超出JavaScript安全整数范围时自动转换为字符串 * **时间格式统一**:提供统一的日期时间序列化格式 * **多格式日期解析**:支持多种日期格式的自动识别和解析 * **精度保持**:BigDecimal类型序列化为字符串避免精度问题 ## 模块结构 ``` ruoyi-common-json/ ├── config/ │ └── JacksonConfig.java # Jackson配置类 ├── handler/ │ ├── BigNumberSerializer.java # 大数值序列化器 │ └── CustomDateDeserializer.java # 自定义日期反序列化器 └── utils/ └── JsonUtils.java # JSON工具类 ``` ## 核心功能 ### 1. 大数值处理 #### JavaScript精度问题 JavaScript中的Number类型采用IEEE 754双精度浮点数标准,安全整数范围为: * **最大安全整数**:`9007199254740991` (2^53-1) * **最小安全整数**:`-9007199254740991` (-(2^53-1)) 超出此范围的整数在JavaScript中会丢失精度。 #### 解决方案 `BigNumberSerializer` 自动检测数值范围: ```java @JacksonStdImpl public class BigNumberSerializer extends NumberSerializer { private static final long MAX_SAFE_INTEGER = 9007199254740991L; private static final long MIN_SAFE_INTEGER = -9007199254740991L; @Override public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) { if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { // 在安全范围内,使用数值类型 super.serialize(value, gen, provider); } else { // 超出安全范围,序列化为字符串 gen.writeString(value.toString()); } } } ``` #### 序列化效果 ```json { "smallNumber": 123456, // 安全范围内,保持数值类型 "bigNumber": "9007199254740992", // 超出范围,转为字符串 "bigInteger": "12345678901234567890", // BigInteger 转为字符串 "bigDecimal": "123.456789012345678901" // BigDecimal 转为字符串保持精度 } ``` ### 2. 时间类型处理 #### 统一时间格式 系统统一使用 `yyyy-MM-dd HH:mm:ss` 格式: ```java // LocalDateTime 序列化配置 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter)); ``` #### 多格式日期解析 `CustomDateDeserializer` 支持多种日期格式自动识别: ```java public class CustomDateDeserializer extends JsonDeserializer { @Override public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { return DateUtil.parse(p.getText()); // 基于Hutool的智能解析 } } ``` 支持的格式包括: * `yyyy-MM-dd HH:mm:ss` * `yyyy-MM-dd` * `yyyy/MM/dd HH:mm:ss` * 时间戳(毫秒) * 其他常见日期格式 ### 3. JSON工具类 #### 基本用法 ```java // 对象转JSON字符串 User user = new User("张三", 25); String json = JsonUtils.toJsonString(user); // JSON字符串转对象 String json = "{\"name\":\"张三\",\"age\":25}"; User user = JsonUtils.parseObject(json, User.class); // 复杂类型转换 String listJson = "[{\"name\":\"张三\"},{\"name\":\"李四\"}]"; List users = JsonUtils.parseArray(listJson, User.class); ``` #### 高级用法 ```java // 泛型类型转换 String json = "{\"users\":[{\"name\":\"张三\"}],\"total\":1}"; TypeReference> typeRef = new TypeReference>() {}; Map result = JsonUtils.parseObject(json, typeRef); // 转换为Dict对象(Hutool增强Map) Dict dict = JsonUtils.parseMap(json); String name = dict.getStr("name"); Integer age = dict.getInt("age"); // JSON数组转Dict列表 String arrayJson = "[{\"name\":\"张三\"},{\"name\":\"李四\"}]"; List dicts = JsonUtils.parseArrayMap(arrayJson); ``` #### 字节数组支持 ```java // 字节数组转对象 byte[] bytes = json.getBytes(); User user = JsonUtils.parseObject(bytes, User.class); ``` ## 配置说明 ### 自动配置 模块通过Spring Boot自动配置机制加载: ```java @AutoConfiguration(before = JacksonAutoConfiguration.class) public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { // 配置逻辑 } } ``` 配置文件:`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` ``` plus.ruoyi.common.json.config.JacksonConfig ``` ### 应用配置 在 `application.yml` 中的相关配置: ```yaml spring: # JSON序列化配置 jackson: # 日期格式化 date-format: yyyy-MM-dd HH:mm:ss serialization: # 格式化输出 indent_output: false # 忽略无法转换的对象 fail_on_empty_beans: false deserialization: # 允许对象忽略json中不存在的属性 fail_on_unknown_properties: false mvc: format: # 日期时间格式 date-time: yyyy-MM-dd HH:mm:ss ``` ## 使用示例 ### 实体类定义 ```java @Data public class OrderInfo { private Long id; // 可能超出JavaScript精度范围 private String orderNo; private BigDecimal amount; // 金额,需要保持精度 private LocalDateTime createTime; // 创建时间 private Date updateTime; // 更新时间 } ``` ### 序列化结果 ```java OrderInfo order = new OrderInfo(); order.setId(9007199254740992L); // 超出JavaScript安全范围 order.setOrderNo("ORD20250101001"); order.setAmount(new BigDecimal("99.99")); order.setCreateTime(LocalDateTime.now()); order.setUpdateTime(new Date()); String json = JsonUtils.toJsonString(order); ``` 输出结果: ```json { "id": "9007199254740992", // 大数值转为字符串 "orderNo": "ORD20250101001", "amount": "99.99", // BigDecimal保持精度 "createTime": "2025-01-01 10:30:00", // 统一时间格式 "updateTime": "2025-01-01 10:30:00" // 自动格式化 } ``` ### 反序列化示例 ```java // 支持多种日期格式输入 String json1 = "{\"updateTime\":\"2025-01-01 10:30:00\"}"; String json2 = "{\"updateTime\":\"2025/01/01 10:30:00\"}"; String json3 = "{\"updateTime\":\"2025-01-01\"}"; String json4 = "{\"updateTime\":\"1704096600000\"}"; // 时间戳 // 都能正确解析 OrderInfo order1 = JsonUtils.parseObject(json1, OrderInfo.class); OrderInfo order2 = JsonUtils.parseObject(json2, OrderInfo.class); OrderInfo order3 = JsonUtils.parseObject(json3, OrderInfo.class); OrderInfo order4 = JsonUtils.parseObject(json4, OrderInfo.class); ``` ## 注意事项 ### 1. 精度处理 * 超出JavaScript安全整数范围的Long/BigInteger会被序列化为字符串 * BigDecimal始终序列化为字符串以保持精度 * 前端接收时需要相应处理字符串形式的数值 ### 2. 日期处理 * 系统统一使用 `yyyy-MM-dd HH:mm:ss` 格式 * 反序列化支持多种格式自动识别 * 使用系统默认时区 ### 3. 异常处理 工具类中的所有IOException都被包装为RuntimeException: ```java try { return OBJECT_MAPPER.readValue(text, clazz); } catch (IOException e) { throw new RuntimeException(e); } ``` ### 4. 空值处理 * `toJsonString(null)` 返回 `null` * `parseObject("", Class)` 返回 `null` * `parseArray("", Class)` 返回空列表 ## 依赖信息 ### Maven坐标 ```xml plus.ruoyi ruoyi-common-json ${revision} ``` ### 核心依赖 ```xml com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.datatype jackson-datatype-jsr310 ``` ## 扩展指南 ### 自定义序列化器 如需要自定义序列化行为,可以仿照 `BigNumberSerializer` 实现: ```java public class CustomSerializer extends JsonSerializer { @Override public void serialize(YourType value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 自定义序列化逻辑 } } // 在JacksonConfig中注册 javaTimeModule.addSerializer(YourType.class, new CustomSerializer()); ``` ### 自定义反序列化器 ```java public class CustomDeserializer extends JsonDeserializer { @Override public YourType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { // 自定义反序列化逻辑 return new YourType(); } } // 注册反序列化器 javaTimeModule.addDeserializer(YourType.class, new CustomDeserializer()); ``` ## 总结 JSON处理模块通过合理的配置和自定义处理器,有效解决了以下关键问题: 1. **精度安全**:防止大数值在前后端传输过程中丢失精度 2. **格式统一**:提供统一的时间格式和灵活的解析能力 3. **易用性**:封装简洁的工具类,简化JSON操作 4. **扩展性**:支持自定义序列化器,满足特殊需求 该模块是系统中处理JSON数据的核心组件,为前后端数据交互提供了可靠的基础支撑。 --- --- url: 'https://ruoyi.plus/backend/common/mybatis.md' --- # MyBatisPlus增强 (mybatis) ## 模块简介 `ruoyi-common-mybatis` 是 Ruoyi-Plus-Uniapp 框架的数据访问增强模块,基于 MyBatis-Plus 进行深度定制和功能扩展。该模块提供了强大的查询增强、数据权限控制、分页支持等功能,极大简化了数据访问层的开发。 ## 核心特性 ### 🚀 查询增强 (Query Enhancement) * **PlusQuery**: 字符串列名查询增强器,支持聚合函数和智能条件处理 * **PlusLambdaQuery**: Lambda表达式查询增强器,类型安全的查询构建 * **智能条件处理**: 自动过滤 null 值和空集合,避免无效查询条件 ### 🛡️ 数据权限控制 (Data Permission) * **注解式权限控制**: 通过 `@DataPermission` 注解轻松实现数据权限 * **多种权限类型**: 支持部门权限、角色权限、用户权限等 * **动态权限策略**: 基于当前用户上下文动态生成权限SQL ### 📄 分页增强 (Pagination Enhancement) * **PageQuery**: 统一的分页查询参数封装 * **PageResult**: 标准化分页结果返回格式 * **自动分页**: 无需手动处理分页逻辑,框架自动注入分页参数 ### 🏗️ 三层架构支持 (Three-Layer Architecture) * **IBaseService**: 通用服务接口,支持 Entity、BO、VO 三层转换 * **BaseServiceImpl**: 基础服务实现类,减少80%样板代码 * **BaseMapperPlus**: 增强的Mapper接口,提供更多便捷方法 ### ⚡ 性能优化 (Performance Optimization) * **字段自动填充**: 创建时间、更新时间、创建人等字段自动填充 * **数据库监控**: 集成数据源监控,实时了解数据库性能 * **SQL注入防护**: 内置SQL注入检测和防护机制 ## 模块架构 ```text ruoyi-common-mybatis/ ├── annotation/ # 注解定义 │ ├── DataPermission # 数据权限注解 │ └── DataColumn # 数据列权限注解 ├── aspect/ # 切面处理 │ └── DataPermissionAspect # 数据权限切面 ├── config/ # 配置类 │ ├── MybatisPlusConfig # MyBatis-Plus配置 │ └── MyBatisDataSourceMonitor # 数据源监控 ├── core/ # 核心组件 │ ├── domain/ # 领域模型 │ │ ├── BaseEntity # 基础实体类 │ │ ├── PageQuery # 分页查询参数 │ │ └── PageResult # 分页结果封装 │ ├── mapper/ # Mapper接口 │ │ └── BaseMapperPlus # 增强Mapper基类 │ ├── query/ # 查询增强 │ │ ├── PlusQuery # 字符串查询增强 │ │ └── PlusLambdaQuery # Lambda查询增强 │ └── service/ # 服务接口 │ ├── IBaseService # 基础服务接口 │ └── impl/ │ └── BaseServiceImpl # 基础服务实现 ├── enums/ # 枚举定义 │ ├── DataBaseType # 数据库类型 │ └── DataScopeType # 数据权限类型 ├── handler/ # 处理器 │ ├── InjectionMetaObjectHandler # 字段自动填充 │ ├── PlusDataPermissionHandler # 数据权限处理 │ ├── PlusPostInitTableInfoHandler # 表信息初始化 │ └── MybatisExceptionHandler # 异常处理 ├── helper/ # 辅助工具 │ ├── DataBaseHelper # 数据库工具 │ └── DataPermissionHelper # 数据权限工具 └── interceptor/ # 拦截器 └── PlusDataPermissionInterceptor # 数据权限拦截器 ``` ## 核心组件详解 ### 1. 查询增强器 (Query Enhancement) #### PlusQuery - 字符串列名查询 ```java // 基础查询 PlusQuery query = PlusQuery.of(User.class) .eq("user_name", "张三") .like("nick_name", "admin") .gt("create_time", DateUtils.beginOfDay(new Date())); // 聚合查询支持 PlusQuery query = PlusQuery.of(User.class) .select("dept_id") .sum("age", "total_age") .count("*", "user_count") .groupBy("dept_id"); // 智能条件处理 - 自动过滤无效值 PlusQuery query = PlusQuery.of(User.class) .eq("status", null) // 自动忽略 .in("dept_id", List.of()) // 自动忽略 .between("age", 18, null); // 自动转为 >= 18 ``` #### PlusLambdaQuery - Lambda表达式查询 ```java // 类型安全的查询构建 PlusLambdaQuery query = PlusLambdaQuery.of(User.class) .eq(User::getUserName, "张三") .like(User::getNickName, "admin") .gt(User::getCreateTime, DateUtils.beginOfDay(new Date())); // 链式调用,流畅API List users = PlusLambdaQuery.of(User.class) .eq(User::getStatus, "0") .orderByDesc(User::getCreateTime) .list(); ``` ### 2. 三层架构支持 (Three-Layer Architecture) #### IBaseService - 通用服务接口 ```java /** * 用户服务实现三层架构 * T: User (Entity) - 数据库实体 * B: UserBo (Business Object) - 业务对象 * V: UserVo (View Object) - 视图对象 */ @Service public class UserServiceImpl extends BaseServiceImpl implements IUserService { // 继承大量通用方法,无需重复实现 // - get(id) - 根据ID查询并返回VO // - list(bo) - 根据BO条件查询并返回VO列表 // - page(bo, pageQuery) - 分页查询 // - add(bo) - 新增 // - update(bo) - 更新 // - delete(id) - 删除 } ``` #### 使用示例 ```java @RestController public class UserController { @Autowired private IUserService userService; // 分页查询用户 @PostMapping("/page") public R> page(@RequestBody UserBo bo, PageQuery pageQuery) { PageResult result = userService.page(bo, pageQuery); return R.ok(result); } // 新增用户 @PostMapping public R add(@Validated(AddGroup.class) @RequestBody UserBo bo) { Long userId = userService.add(bo); return R.ok(userId); } // 更新用户 @PutMapping public R update(@Validated(EditGroup.class) @RequestBody UserBo bo) { boolean result = userService.update(bo); return R.status(result); } // 删除用户 @DeleteMapping("/{id}") public R delete(@PathVariable Long id) { boolean result = userService.delete(id); return R.status(result); } } ``` ### 3. 数据权限控制 (Data Permission) #### 注解式权限控制 ```java @Service public class UserServiceImpl extends BaseServiceImpl { // 部门数据权限 - 只能查看本部门及下级部门数据 @DataPermission({ @DataColumn(key = "deptName", value = "dept_id") }) @Override public List list(UserBo bo) { return super.list(bo); } // 用户数据权限 - 只能查看自己创建的数据 @DataPermission({ @DataColumn(key = "userName", value = "create_by") }) public List listMyCreated(UserBo bo) { return super.list(bo); } } ``` #### 权限配置 ```java // 数据权限类型枚举 public enum DataScopeType { ALL(1, "全部数据权限"), CUSTOM(2, "自定数据权限"), DEPT(3, "部门数据权限"), DEPT_AND_CHILD(4, "部门及以下数据权限"), SELF(5, "仅本人数据权限"); } ``` ### 4. 分页增强 (Pagination Enhancement) #### PageQuery - 分页查询参数 ```java public class UserBo extends PageQuery { private String userName; private String status; // ... 其他查询条件 } // 控制器中使用 @PostMapping("/page") public R> page(@RequestBody UserBo bo) { // 自动处理分页参数,无需手动设置 PageResult result = userService.page(bo); return R.ok(result); } ``` #### PageResult - 分页结果封装 ```java // 标准分页结果格式 { "code": 200, "msg": "操作成功", "data": { "records": [...], // 数据列表 "total": 100, // 总记录数 "size": 10, // 每页大小 "current": 1, // 当前页 "pages": 10 // 总页数 } } ``` ### 5. BaseEntity - 基础实体类 ```java @Data @EqualsAndHashCode(callSuper = false) public class User extends BaseEntity { // 业务字段 private String userName; private String nickName; private String status; // BaseEntity 已包含以下通用字段: // - id: 主键ID // - createDept: 创建部门 // - createBy: 创建者 // - createTime: 创建时间 // - updateBy: 更新者 // - updateTime: 更新时间 // - remark: 备注 // - version: 乐观锁版本号 // - delFlag: 删除标志 // - tenantId: 租户ID } ``` ### 6. 字段自动填充 (Auto Fill) ```java // 字段自动填充配置 @Component public class InjectionMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 新增时自动填充 this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); this.strictInsertFill(metaObject, "createBy", String.class, getLoginUserId()); this.strictInsertFill(metaObject, "createDept", String.class, getLoginDeptId()); // ... } @Override public void updateFill(MetaObject metaObject) { // 更新时自动填充 this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); this.strictUpdateFill(metaObject, "updateBy", String.class, getLoginUserId()); // ... } } ``` ## 配置与使用 ### 1. 添加依赖 ```xml plus.ruoyi ruoyi-common-mybatis ${ruoyi.version} ``` ### 2. 数据库配置 ```yaml # application.yml spring: datasource: dynamic: primary: master strict: false datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ruoyi_plus username: root password: root ``` ### 3. MyBatis-Plus配置 ```java @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 数据权限插件 interceptor.addInnerInterceptor(new DataPermissionInterceptor()); // 乐观锁插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } } ``` ## 最佳实践 ### 1. 服务层开发 ```java // 继承BaseServiceImpl,获得丰富的通用方法 @Service public class UserServiceImpl extends BaseServiceImpl implements IUserService { // 只需实现特殊的业务逻辑 @Override public boolean resetPassword(Long userId, String newPassword) { return lambdaUpdate() .eq(User::getId, userId) .set(User::getPassword, SecurityUtils.encryptPassword(newPassword)) .set(User::getUpdateTime, new Date()) .update(); } // 使用查询增强器构建复杂查询 @Override public List listActiveUsers(String deptId) { return of() .eq(User::getStatus, "0") .eq(User::getDeptId, deptId) .orderByDesc(User::getCreateTime) .list(); } } ``` ### 2. 复杂查询构建 ```java // 使用PlusLambdaQuery构建复杂查询 public List searchUsers(UserSearchBo bo) { return PlusLambdaQuery.of(User.class) .like(StringUtils.isNotBlank(bo.getUserName()), User::getUserName, bo.getUserName()) .eq(StringUtils.isNotBlank(bo.getStatus()), User::getStatus, bo.getStatus()) .in(CollUtil.isNotEmpty(bo.getDeptIds()), User::getDeptId, bo.getDeptIds()) .between(Objects.nonNull(bo.getStartTime()) && Objects.nonNull(bo.getEndTime()), User::getCreateTime, bo.getStartTime(), bo.getEndTime()) .orderByDesc(User::getCreateTime) .list(); } ``` ### 3. 数据权限使用 ```java // 在Service方法上添加数据权限注解 @DataPermission({ @DataColumn(key = "deptName", value = "dept_id"), @DataColumn(key = "userName", value = "create_by") }) public PageResult page(UserBo bo, PageQuery pageQuery) { // 框架会自动在SQL中添加数据权限条件 return super.page(bo, pageQuery); } ``` ### 4. 错误处理 ```java // 统一异常处理 @ControllerAdvice public class MybatisExceptionHandler { @ExceptionHandler(DuplicateKeyException.class) public R handleDuplicateKeyException(DuplicateKeyException e) { log.error("数据库操作异常", e); return R.fail("数据重复,请检查后重试"); } @ExceptionHandler(DataAccessException.class) public R handleDataAccessException(DataAccessException e) { log.error("数据访问异常", e); return R.fail("数据操作失败"); } } ``` ## 注意事项 ### 1. 性能注意事项 * **大量数据查询**: 使用分页查询避免一次性加载过多数据 * **N+1问题**: 合理使用关联查询或批量查询解决 * **索引优化**: 确保查询条件字段建立合适的索引 ### 2. 数据权限注意事项 * **权限范围**: 明确每个数据权限注解的作用范围 * **性能影响**: 数据权限会增加SQL复杂度,注意性能影响 * **权限测试**: 充分测试各种权限场景,确保数据安全 ### 3. 事务管理 * **事务边界**: 在Service层方法上合理设置事务边界 * **异常回滚**: 确保异常情况下事务能正确回滚 * **只读事务**: 查询操作使用只读事务提高性能 ### 4. 代码规范 * **命名规范**: 遵循Java命名规范,保持代码可读性 * **注释规范**: 重要方法添加完整的JavaDoc注释 * **异常处理**: 合理处理数据访问异常,提供友好的错误信息 通过使用 ruoyi-common-mybatis 模块,可以显著提高数据访问层的开发效率,减少样板代码,同时提供强大的数据权限控制和查询增强功能。 --- --- url: 'https://ruoyi.plus/backend/common/oss.md' --- # OSS 对象存储模块 ## 概述 OSS(Object Storage Service)对象存储模块为若依系统提供了统一的文件存储服务,支持本地存储和多种云存储服务。该模块基于策略模式设计,具有良好的扩展性和可维护性。 ### 主要特性 * 🚀 **多存储支持**:支持本地存储、阿里云OSS、腾讯云COS、七牛云、华为云OBS、MinIO等 * 🔒 **安全可靠**:支持预签名URL、访问权限控制、文件完整性校验 * 🎯 **高性能**:基于AWS S3 SDK异步客户端,支持大文件传输 * 🔧 **易于配置**:支持动态配置切换,无需重启应用 * 📊 **多租户**:支持租户数据隔离 * 🎨 **灵活路径**:支持自定义文件存储路径规则 ### 支持的存储类型 | 存储类型 | 配置键 | 说明 | |---------|--------|------| | 本地存储 | `local` | 存储在应用服务器本地文件系统 | | 阿里云OSS | `aliyun` | 阿里云对象存储服务 | | 腾讯云COS | `qcloud` | 腾讯云对象存储服务 | | 七牛云 | `qiniu` | 七牛云对象存储服务 | | 华为云OBS | `obs` | 华为云对象存储服务 | | MinIO | `minio` | 开源S3兼容对象存储 | ## 模块结构 ```text ruoyi-common-oss/ ├── src/main/java/plus/ruoyi/common/oss/ │ ├── constant/ # 常量定义 │ │ └── OssConstant.java │ ├── core/ # 核心类 │ │ └── OssClient.java │ ├── entity/ # 实体类 │ │ ├── OssFileInfo.java │ │ ├── OssFileMetadata.java │ │ └── UploadResult.java │ ├── enums/ # 枚举类 │ │ └── AccessPolicyType.java │ ├── exception/ # 异常类 │ │ └── OssException.java │ ├── factory/ # 工厂类 │ │ ├── OssFactory.java │ │ └── OssStrategyFactory.java │ ├── properties/ # 配置属性 │ │ └── OssProperties.java │ └── service/ # 服务接口和实现 │ ├── OssStrategy.java │ └── impl/ │ ├── LocalOssStrategy.java │ └── S3OssStrategy.java └── pom.xml ``` ## 配置说明 ### 基础配置 OSS模块的配置存储在数据库中,通过 `OssProperties` 类定义,支持以下配置项: | 配置项 | 说明 | 示例值 | |--------|------|--------| | `tenantId` | 租户ID | `tenant123` | | `endpoint` | 访问端点 | `oss-cn-beijing.aliyuncs.com` | | `domain` | 自定义域名 | `https://cdn.yourdomain.com` | | `prefix` | 文件路径前缀 | `uploads` | | `accessKey` | 访问密钥 | `your-access-key` | | `secretKey` | 私有密钥 | `your-secret-key` | | `bucketName` | 存储桶名称 | `my-bucket` | | `region` | 存储区域 | `cn-beijing` | | `isHttps` | 是否使用HTTPS | `1`(1=是, 0=否) | | `accessPolicy` | 访问策略 | `1`(0=私有, 1=公共读写, 2=公共读) | ### 访问策略类型 ```java public enum AccessPolicyType { PRIVATE("0"), // 私有访问 PUBLIC("1"), // 公共读写 CUSTOM("2") // 自定义(公共读) } ``` ### 本地存储配置 本地存储需要在 `AppProperties` 中配置上传路径: ```yaml app: uploadPath: "/data/uploads" # 本地文件上传路径 ``` ## 核心API ### OssClient 客户端 `OssClient` 是对象存储的核心客户端,提供统一的文件操作接口。 #### 获取客户端实例 ```java // 获取默认OSS客户端 OssClient client = OssFactory.instance(); // 根据配置键获取客户端 OssClient client = OssFactory.instance("aliyun"); // 根据类型枚举获取客户端 OssClient client = OssFactory.instance(OssFactory.OssType.ALIYUN); ``` ### 文件上传 #### 上传本地文件 ```java // 上传文件并自动生成文件名 File file = new File("/path/to/file.jpg"); UploadResult result = client.uploadSuffix(file, ".jpg"); System.out.println("文件URL: " + result.getUrl()); // 指定对象键上传 String contentType = "image/jpeg"; UploadResult result = client.uploadFile(file, "images/photo.jpg", null, contentType); ``` #### 上传数据流 ```java // 上传字节数组 byte[] data = "Hello World".getBytes(); UploadResult result = client.uploadSuffix(data, ".txt", "text/plain"); // 上传输入流 InputStream inputStream = new FileInputStream(file); UploadResult result = client.uploadSuffix(inputStream, ".jpg", file.length(), "image/jpeg"); ``` #### 上传结果 ```java UploadResult result = client.uploadSuffix(file, ".jpg"); System.out.println("文件URL: " + result.getUrl()); System.out.println("文件名: " + result.getFileName()); System.out.println("文件大小: " + result.getFileSize()); System.out.println("ETag: " + result.getETag()); ``` ### 文件下载 #### 下载到临时文件 ```java String filePath = "tenant123/2024/01/15/abc123def456.jpg"; Path tempFile = client.downloadToTempFile(filePath); System.out.println("临时文件路径: " + tempFile.toString()); ``` #### 下载到输出流 ```java String objectKey = "images/photo.jpg"; FileOutputStream outputStream = new FileOutputStream("download.jpg"); client.downloadToStream(objectKey, outputStream, fileSize -> { System.out.println("文件大小: " + fileSize + " 字节"); }); ``` #### 获取文件流 ```java String filePath = "images/photo.jpg"; try (InputStream inputStream = client.getFileAsStream(filePath)) { // 处理文件流 byte[] data = inputStream.readAllBytes(); } ``` ### 文件管理 #### 删除文件 ```java String filePath = "images/photo.jpg"; client.deleteFile(filePath); ``` #### 复制文件 ```java String sourcePath = "images/original.jpg"; String targetPath = "images/backup.jpg"; client.copyFile(sourcePath, targetPath); ``` #### 获取文件元数据 ```java String filePath = "images/photo.jpg"; OssFileMetadata metadata = client.getFileMetadata(filePath); System.out.println("文件名: " + metadata.getFileName()); System.out.println("文件大小: " + metadata.getFileSize()); System.out.println("内容类型: " + metadata.getContentType()); System.out.println("创建时间: " + metadata.getCreateTime()); System.out.println("ETag: " + metadata.getETag()); ``` #### 列出文件 ```java String prefix = "images/"; int maxResults = 100; List files = client.listFiles(prefix, maxResults); for (OssFileInfo file : files) { System.out.println("文件: " + file.getFileName()); System.out.println("大小: " + file.getFileSize()); System.out.println("是否目录: " + file.getIsDirectory()); System.out.println("访问URL: " + file.getUrl()); } ``` ### URL生成 #### 生成预签名URL ```java String objectKey = "images/photo.jpg"; Duration expiredTime = Duration.ofHours(1); // 1小时过期 String presignedUrl = client.generatePresignedUrl(objectKey, expiredTime); System.out.println("预签名URL: " + presignedUrl); ``` #### 生成公共访问URL ```java String objectKey = "images/photo.jpg"; String publicUrl = client.generatePublicUrl(objectKey); System.out.println("公共URL: " + publicUrl); ``` #### 生成预签名上传URL ```java String objectKey = "images/upload.jpg"; String contentType = "image/jpeg"; int expiration = 3600; // 1小时过期 String uploadUrl = client.generatePresignedUploadUrl(objectKey, contentType, expiration); System.out.println("预签名上传URL: " + uploadUrl); ``` ## 文件路径规则 ### 自动生成路径 OSS模块会根据以下规则自动生成文件存储路径: ``` [prefix/]tenantId/yyyy/MM/dd/uuid.suffix ``` * `prefix`: 业务前缀(可选),如 `avatar`、`document` * `tenantId`: 租户ID,用于多租户数据隔离 * `yyyy/MM/dd`: 按日期分层的目录结构 * `uuid`: 32位简化UUID,确保文件名唯一性 * `suffix`: 文件扩展名 ### 路径示例 ```java // 配置了前缀的路径 avatar/tenant123/2024/01/15/abc123def456789.jpg // 无前缀的路径 tenant123/2024/01/15/abc123def456789.jpg ``` ## 最佳实践 ### 1. 客户端复用 ```java @Component public class FileService { @Autowired private OssClient ossClient; // 通过依赖注入复用客户端 public String uploadFile(MultipartFile file) throws IOException { String suffix = FileUtil.getSuffix(file.getOriginalFilename()); UploadResult result = ossClient.uploadSuffix( file.getInputStream(), suffix, file.getSize(), file.getContentType() ); return result.getUrl(); } } ``` ### 2. 异常处理 ```java public String uploadFileWithErrorHandling(File file) { try { UploadResult result = ossClient.uploadSuffix(file, ".jpg"); return result.getUrl(); } catch (OssException e) { log.error("文件上传失败: {}", e.getMessage()); throw new ServiceException("文件上传失败,请稍后重试"); } } ``` ### 3. 文件类型验证 ```java public void validateFileType(String fileName) { String suffix = FileUtil.getSuffix(fileName); if (!Arrays.asList(".jpg", ".png", ".gif").contains(suffix.toLowerCase())) { throw new ServiceException("不支持的文件类型: " + suffix); } } ``` ### 4. 大文件处理 ```java public String uploadLargeFile(File file) { // 对于大文件,建议使用分片上传或预签名上传 if (file.length() > 100 * 1024 * 1024) { // 100MB // 生成预签名上传URL,让前端直传 String objectKey = generateObjectKey(file.getName()); return ossClient.generatePresignedUploadUrl(objectKey, Files.probeContentType(file.toPath()), 3600); } else { // 小文件直接上传 return ossClient.uploadSuffix(file, FileUtil.getSuffix(file.getName())).getUrl(); } } ``` ## 配置管理 ### 动态配置切换 OSS模块支持运行时动态切换存储配置,通过数据库配置更新后会自动生效: ```java // 配置会从数据库加载并缓存到Redis @Autowired private OssConfigService ossConfigService; public void switchToAliyunOss() { // 更新数据库配置后,系统会自动刷新缓存 ossConfigService.updateOssConfig("aliyun", newOssProperties); // 下次获取客户端时会自动使用新配置 OssClient client = OssFactory.instance("aliyun"); } ``` ### 数据库配置管理 OSS配置存储在数据库的系统配置表中,可以通过管理后台进行配置: 1. **配置存储位置**:系统配置表 `sys_oss_config` 2. **缓存机制**:配置信息会缓存到Redis中,键名格式为 `sys_oss:配置键` 3. **默认配置**:通过 `sys_oss:default_config` 键指定默认使用的OSS配置 #### 配置示例 ```sql -- 阿里云OSS配置示例 INSERT INTO sys_oss_config (config_key, access_key, secret_key, bucket_name, prefix, endpoint, domain, is_https, region, access_policy, status) VALUES ('aliyun', 'your-access-key', 'your-secret-key', 'your-bucket', 'uploads', 'oss-cn-beijing.aliyuncs.com', '', '1', 'cn-beijing', '1', '1'); -- MinIO配置示例 INSERT INTO sys_oss_config (config_key, access_key, secret_key, bucket_name, prefix, endpoint, domain, is_https, region, access_policy, status) VALUES ('minio', 'minioadmin', 'minioadmin', 'test', 'uploads', 'localhost:9000', '', '0', 'us-east-1', '1', '1'); ``` ## 监控与运维 ### 健康检查 ```java @Component public class OssHealthIndicator implements HealthIndicator { @Override public Health health() { try { OssClient client = OssFactory.instance(); // 执行简单的健康检查操作 client.getBaseUrl(); return Health.up() .withDetail("type", client.getConfigKey()) .withDetail("endpoint", client.getProperties().getEndpoint()) .build(); } catch (Exception e) { return Health.down() .withDetail("error", e.getMessage()) .build(); } } } ``` ### 性能监控 ```java @Aspect @Component public class OssPerformanceAspect { @Around("execution(* plus.ruoyi.common.oss.core.OssClient.upload*(..))") public Object monitorUpload(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - startTime; log.info("文件上传耗时: {}ms", duration); return result; } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; log.error("文件上传失败,耗时: {}ms, 错误: {}", duration, e.getMessage()); throw e; } } } ``` ## 故障排查 ### 常见问题 #### 1. 配置错误 **问题**: `OssException: 系统异常, 'xxx'配置信息不存在!` **解决方案**: * 检查OSS配置是否正确设置在数据库或配置中心 * 确认 `OssConfigService.initOssConfig()` 方法是否正常执行 * 验证Redis连接是否正常 #### 2. 权限问题 **问题**: 上传失败,提示权限不足 **解决方案**: * 检查云存储的AccessKey和SecretKey是否正确 * 确认存储桶的权限配置 * 验证IP白名单设置 #### 3. 网络连接问题 **问题**: 连接超时或网络异常 **解决方案**: * 检查endpoint配置是否正确 * 确认网络连通性 * 调整超时时间配置 ### 日志配置 ```yaml logging: level: plus.ruoyi.common.oss: DEBUG software.amazon.awssdk: INFO ``` ## 扩展开发 ### 添加新的存储策略 1. 实现 `OssStrategy` 接口: ```java public class MyCustomOssStrategy implements OssStrategy { // 实现所有接口方法 @Override public UploadResult uploadFile(File file, String key, String md5Digest, String contentType) { // 自定义上传逻辑 return null; } // ... 其他方法实现 } ``` 2. 修改 `OssStrategyFactory`: ```java public static OssStrategy createStrategy(String configKey, OssProperties ossProperties) { switch (configKey) { case "custom": return new MyCustomOssStrategy(configKey, ossProperties); // ... 其他case } } ``` 3. 添加对应的枚举类型: ```java public enum OssType { CUSTOM("custom"), // ... 其他类型 } ``` ### 自定义文件路径生成 可以通过扩展 `OssClient` 或实现自定义的路径生成策略: ```java public class CustomPathGenerator { public String generatePath(String businessType, String fileName) { // 自定义路径生成逻辑 return businessType + "/" + DateUtils.datePath() + "/" + fileName; } } ``` --- --- url: 'https://ruoyi.plus/frontend/dev/prettier-config.md' --- # Prettier配置 --- --- url: 'https://ruoyi.plus/mobile/platform/qq.md' --- # QQ小程序适配 --- --- url: 'https://ruoyi.plus/backend/common/redis.md' --- # Redis缓存 (redis) ## 概述 `ruoyi-common-redis` 是一个基于 **Redisson** 实现的综合性Redis缓存模块,提供了完整的缓存解决方案,包括基础缓存操作、分布式锁、队列管理、ID生成器等功能。该模块采用二级缓存架构,结合Redis分布式缓存和Caffeine本地缓存,为应用提供高性能的缓存服务。 ## 核心特性 * **🚀 高性能二级缓存**:Redis + Caffeine 双重缓存架构 * **🔒 分布式锁**:基于 Redisson 的分布式锁机制 * **📊 分布式ID生成**:支持多种格式的唯一ID生成 * **🎯 队列管理**:普通队列、延迟队列、优先队列等 * **⚡ 限流控制**:基于令牌桶的限流机制 * **📡 发布订阅**:Redis消息发布订阅功能 * **🔧 灵活配置**:支持单机和集群模式 * **💡 智能序列化**:JSON序列化,支持Java8时间类型 ## 架构设计 ```mermaid graph TB A[应用层] --> B[CacheUtils工具层] B --> C[PlusSpringCacheManager] C --> D[CaffeineCacheDecorator] D --> E[Caffeine本地缓存] D --> F[Redis分布式缓存] A --> G[RedisUtils工具层] G --> H[RedissonClient] A --> I[SequenceUtils] I --> H A --> J[QueueUtils] J --> H ``` ## 快速开始 ### 1. 添加依赖 ```xml plus.ruoyi ruoyi-common-redis ``` ### 2. 配置文件 **开发环境配置示例** ```yaml ################## Redis缓存配置 ################## --- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉) spring.data: redis: # 地址 host: localhost # 端口,默认为6379 port: 6379 # 数据库索引 database: 0 # redis 密码必须配置 # password: ruoyi123 # 连接超时时间 timeout: 10s # 是否开启ssl ssl.enabled: false # redisson 配置 redisson: # redis key前缀(使用应用ID作为前缀) keyPrefix: ${app.id} # 线程池数量(开发环境适中配置) threads: 4 # Netty线程池数量 nettyThreads: 8 # 单节点配置 singleServerConfig: # 客户端名称(使用应用ID) clientName: ${app.id} # 最小空闲连接数(开发环境较小值) connectionMinimumIdleSize: 8 # 连接池大小(开发环境适中配置) connectionPoolSize: 32 # 连接空闲超时,单位:毫秒 idleConnectionTimeout: 10000 # 命令等待超时,单位:毫秒 timeout: 3000 # 发布和订阅连接池大小 subscriptionConnectionPoolSize: 50 ``` **生产环境配置建议** ```yaml spring.data: redis: host: your-redis-host port: 6379 database: 0 password: your-secure-password timeout: 10s ssl.enabled: true # 生产环境建议开启SSL redisson: keyPrefix: ${app.id} threads: 16 # 生产环境可适当增加 nettyThreads: 32 # 生产环境可适当增加 singleServerConfig: clientName: ${app.id} connectionMinimumIdleSize: 16 # 生产环境增加连接数 connectionPoolSize: 64 # 生产环境增加连接池大小 idleConnectionTimeout: 10000 timeout: 3000 subscriptionConnectionPoolSize: 50 ``` ## 配置说明 ### 核心配置参数 #### Spring Data Redis 配置 * `spring.data.redis.host`: Redis服务器地址(开发环境通常为localhost) * `spring.data.redis.port`: Redis端口(默认6379) * `spring.data.redis.database`: 数据库索引(0-15,默认为0) * `spring.data.redis.password`: 连接密码(开发环境可选,生产环境必须) * `spring.data.redis.timeout`: 连接超时时间 * `spring.data.redis.ssl.enabled`: 是否启用SSL(生产环境建议开启) #### Redisson 配置 * `redisson.keyPrefix`: Redis key前缀,支持使用`${app.id}`占位符 * `redisson.threads`: 用于执行各种Redis操作的线程池大小 * `redisson.nettyThreads`: Netty框架使用的线程池大小 #### 单机服务器配置 * `clientName`: 客户端连接名称,便于监控识别 * `connectionMinimumIdleSize`: 最小空闲连接数,保持一定的连接预热 * `connectionPoolSize`: 连接池最大连接数 * `idleConnectionTimeout`: 连接空闲超时时间(毫秒) * `timeout`: 单个Redis操作的超时时间(毫秒) * `subscriptionConnectionPoolSize`: 发布订阅专用连接池大小 ### 环境差异配置 #### 开发环境特点 * 较小的连接池配置,节省资源 * 可以不设置密码,简化开发 * 关闭SSL,减少配置复杂度 * 使用localhost本地Redis #### 生产环境建议 * 增加连接池大小,提高并发能力 * 必须设置强密码 * 开启SSL加密传输 * 使用独立的Redis服务器或集群 ### 1. CacheUtils - 通用缓存工具 提供基于Spring Cache抽象的统一缓存操作接口。 #### 基本用法 ```java // 存储缓存 CacheUtils.put("userCache", "user:123", userObj); // 获取缓存 User user = CacheUtils.get("userCache", "user:123"); // 删除缓存 CacheUtils.evict("userCache", "user:123"); // 清空缓存组 CacheUtils.clear("userCache"); ``` ### 2. RedisUtils - Redis操作工具 基于Redisson实现的完整Redis操作工具类。 #### 基础缓存操作 ```java // 设置缓存(永不过期) RedisUtils.setCacheObject("user:123", user); // 设置缓存并指定过期时间 RedisUtils.setCacheObject("user:123", user, Duration.ofHours(1)); // 获取缓存 User user = RedisUtils.getCacheObject("user:123"); // 删除缓存 RedisUtils.deleteObject("user:123"); // 检查是否存在 boolean exists = RedisUtils.isExistsObject("user:123"); // 设置过期时间 RedisUtils.expire("user:123", Duration.ofMinutes(30)); ``` #### 条件设置操作 ```java // 仅当key不存在时设置 boolean success = RedisUtils.setObjectIfAbsent("user:123", user, Duration.ofHours(1)); // 仅当key存在时设置 boolean success = RedisUtils.setObjectIfExists("user:123", user, Duration.ofHours(1)); ``` #### 集合操作 **List操作** ```java // 缓存List数据 List dataList = Arrays.asList("item1", "item2", "item3"); RedisUtils.setCacheList("myList", dataList); // 追加单个数据 RedisUtils.addCacheList("myList", "item4"); // 获取完整List List list = RedisUtils.getCacheList("myList"); // 获取指定范围 List range = RedisUtils.getCacheListRange("myList", 0, 10); ``` **Set操作** ```java // 缓存Set数据 Set dataSet = Sets.newHashSet("item1", "item2"); RedisUtils.setCacheSet("mySet", dataSet); // 添加元素 RedisUtils.addCacheSet("mySet", "item3"); // 获取Set数据 Set set = RedisUtils.getCacheSet("mySet"); ``` **Map操作** ```java // 缓存Map数据 Map dataMap = new HashMap<>(); dataMap.put("name", "张三"); dataMap.put("age", 25); RedisUtils.setCacheMap("userInfo", dataMap); // 设置Map中的单个值 RedisUtils.setCacheMapValue("userInfo", "city", "北京"); // 获取Map中的单个值 String name = RedisUtils.getCacheMapValue("userInfo", "name"); // 获取完整Map Map map = RedisUtils.getCacheMap("userInfo"); // 删除Map中的值 RedisUtils.delCacheMapValue("userInfo", "age"); ``` #### 发布订阅 ```java // 发布消息 RedisUtils.publish("news", "重要通知内容"); // 订阅消息 RedisUtils.subscribe("news", String.class, message -> { System.out.println("收到消息: " + message); // 处理消息逻辑 }); ``` #### 限流控制 ```java // 限流:每秒最多10个请求 long remaining = RedisUtils.rateLimiter("api:login", RateType.OVERALL, 10, 1); if (remaining == -1) { throw new RuntimeException("请求过于频繁,请稍后再试"); } ``` #### 原子操作 ```java // 设置原子值 RedisUtils.setAtomicValue("counter", 100L); // 原子递增 long newValue = RedisUtils.incrAtomicValue("counter"); // 原子递减 long newValue = RedisUtils.decrAtomicValue("counter"); // 获取当前值 long currentValue = RedisUtils.getAtomicValue("counter"); ``` ### 3. SequenceUtils - 分布式ID生成器 基于Redisson的分布式ID生成工具,支持多种ID格式。 #### 基础ID生成 ```java // 生成基础ID(使用默认初始值1和步长1) long id = SequenceUtils.nextId("order", Duration.ofDays(1)); // 生成ID字符串 String idStr = SequenceUtils.nextIdStr("order", Duration.ofDays(1)); // 自定义初始值和步长 long customId = SequenceUtils.nextId("custom", Duration.ofHours(1), 1000L, 2L); ``` #### 补零格式化 ```java // 生成6位补零的序列号:000001, 000002... String seqNo = SequenceUtils.nextPaddedIdStr("seq", Duration.ofHours(1), 6); ``` #### 带日期前缀的ID ```java // 生成带当前日期的ID:20241210001 String dateId = SequenceUtils.nextIdDate(); // 生成带业务前缀和日期的ID:ORD20241210001 String orderId = SequenceUtils.nextIdDate("ORD"); ``` #### 带日期时间前缀的ID ```java // 生成带当前日期时间的ID:20241210103001 String datetimeId = SequenceUtils.nextIdDateTime(); // 生成带业务前缀和日期时间的ID:SN20241210103001 String serialNo = SequenceUtils.nextIdDateTime("SN"); ``` ### 4. QueueUtils - 分布式队列管理 支持多种类型的分布式队列操作。 #### 普通阻塞队列 ```java // 添加数据到队列 QueueUtils.addQueueObject("taskQueue", taskData); // 从队列获取数据(非阻塞) TaskData task = QueueUtils.getQueueObject("taskQueue"); // 删除指定数据 QueueUtils.removeQueueObject("taskQueue", taskData); // 销毁队列 QueueUtils.destroyQueue("taskQueue"); ``` #### 延迟队列 ```java // 添加延迟任务(5分钟后执行) QueueUtils.addDelayedQueueObject("delayQueue", task, 5, TimeUnit.MINUTES); // 默认毫秒单位 QueueUtils.addDelayedQueueObject("delayQueue", task, 300000); // 获取已到期的任务 TaskData task = QueueUtils.getDelayedQueueObject("delayQueue"); // 删除延迟任务 QueueUtils.removeDelayedQueueObject("delayQueue", task); ``` #### 优先队列 ```java // 任务需要实现Comparable接口 public class PriorityTask implements Comparable { private int priority; private String content; @Override public int compareTo(PriorityTask other) { return Integer.compare(other.priority, this.priority); // 高优先级在前 } } // 添加优先级任务 PriorityTask highPriorityTask = new PriorityTask(10, "重要任务"); QueueUtils.addPriorityQueueObject("priorityQueue", highPriorityTask); // 获取最高优先级任务 PriorityTask task = QueueUtils.getPriorityQueueObject("priorityQueue"); ``` #### 有界队列 ```java // 设置队列容量 QueueUtils.trySetBoundedQueueCapacity("boundedQueue", 100); // 添加数据(队列满时返回false) boolean success = QueueUtils.addBoundedQueueObject("boundedQueue", data); // 获取数据 Object data = QueueUtils.getBoundedQueueObject("boundedQueue"); ``` #### 队列订阅 ```java // 订阅普通队列 QueueUtils.subscribeBlockingQueue("taskQueue", message -> { // 异步处理消息 return CompletableFuture.runAsync(() -> { System.out.println("处理任务: " + message); }); }, false); // 订阅延迟队列 QueueUtils.subscribeBlockingQueue("delayQueue", message -> { return CompletableFuture.runAsync(() -> { System.out.println("处理延迟任务: " + message); }); }, true); ``` ## 二级缓存架构 ### 缓存层级 1. **一级缓存(Caffeine)**:JVM本地缓存,访问速度最快 2. **二级缓存(Redis)**:分布式缓存,支持集群共享 ### 缓存策略 #### 读取策略 ```java @Cacheable(value = "userCache", key = "#userId") public User getUserById(Long userId) { // 1. 先查Caffeine本地缓存 // 2. 未命中则查Redis // 3. Redis命中则回写Caffeine // 4. 都未命中则执行方法并缓存结果 return userService.findById(userId); } ``` #### 更新策略 ```java @CachePut(value = "userCache", key = "#user.id") public User updateUser(User user) { // 1. 执行更新操作 // 2. 清除Caffeine中的旧数据 // 3. 更新Redis缓存 return userService.update(user); } ``` #### 删除策略 ```java @CacheEvict(value = "userCache", key = "#userId") public void deleteUser(Long userId) { // 1. 执行删除操作 // 2. 同时清除两级缓存 userService.delete(userId); } ``` ### 动态缓存配置 支持在缓存名称中动态配置参数: ```java // 格式:cacheName#ttl#maxIdleTime#maxSize#local @Cacheable("userCache#30s#10s#1000#1") public User getUser(Long id) { return userService.findById(id); } ``` 参数说明: * `ttl`: 存活时间 * `maxIdleTime`: 最大空闲时间 * `maxSize`: 最大缓存条目数 * `local`: 是否启用本地缓存(1=启用,0=禁用) ## 分布式锁 ### Lock4j注解方式 ```java @Component public class OrderService { // 基础锁 @Lock4j(keys = "#orderId") public void processOrder(String orderId) { // 业务逻辑 } // 自定义锁名称和超时时间 @Lock4j(keys = "'order:' + #orderId", expire = 30000) public void processOrderWithCustom(String orderId) { // 业务逻辑 } // 多参数锁 @Lock4j(keys = {"#userId", "#orderId"}) public void processUserOrder(String userId, String orderId) { // 业务逻辑 } } ``` ### 编程方式 ```java public void manualLock() { RLock lock = RedisUtils.getLock("manual:lock:key"); try { // 尝试获取锁,最多等待10秒,锁定30秒后自动释放 boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS); if (acquired) { // 执行需要同步的业务逻辑 doSomething(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } ``` ## 监听机制 ### 缓存对象监听 ```java // 监听对象变化 RedisUtils.addObjectListener("user:123", new ObjectListener() { @Override public void onUpdated(String name, Object object) { System.out.println("对象已更新: " + name); } @Override public void onDeleted(String name, Object object) { System.out.println("对象已删除: " + name); } @Override public void onExpired(String name, Object object) { System.out.println("对象已过期: " + name); } }); ``` ### 集合监听 ```java // 监听List变化 RedisUtils.addListListener("myList", new ObjectListener() { @Override public void onUpdated(String name, Object object) { System.out.println("列表已更新: " + name); } }); // 监听Set变化 RedisUtils.addSetListener("mySet", new ObjectListener() { @Override public void onUpdated(String name, Object object) { System.out.println("集合已更新: " + name); } }); // 监听Map变化 RedisUtils.addMapListener("myMap", new ObjectListener() { @Override public void onUpdated(String name, Object object) { System.out.println("映射已更新: " + name); } }); ``` ## 集群配置 ### Redis集群配置 **注意:单机与集群配置只能启用其中一种,需要注释掉另一种配置** ```yaml # Redis集群配置示例(注释掉单机配置后使用) spring.data: redis: cluster: nodes: - 192.168.1.100:6379 - 192.168.1.101:6379 - 192.168.1.102:6379 - 192.168.1.103:6379 - 192.168.1.104:6379 - 192.168.1.105:6379 max-redirects: 3 password: your-cluster-password timeout: 10s redisson: keyPrefix: ${app.id} threads: 16 nettyThreads: 32 # 集群配置(替换singleServerConfig) clusterServersConfig: clientName: ${app.id} masterConnectionMinimumIdleSize: 16 masterConnectionPoolSize: 32 slaveConnectionMinimumIdleSize: 16 slaveConnectionPoolSize: 32 idleConnectionTimeout: 10000 timeout: 3000 subscriptionConnectionPoolSize: 50 readMode: "SLAVE" # 读取模式:SLAVE、MASTER、MASTER_SLAVE subscriptionMode: "MASTER" # 订阅模式:SLAVE、MASTER ``` ## 核心组件 ## 最佳实践 ### 1. 缓存设计原则 **选择合适的过期时间** ```java // 用户信息:1小时过期 RedisUtils.setCacheObject("user:" + userId, user, Duration.ofHours(1)); // 配置信息:1天过期 RedisUtils.setCacheObject("config:" + key, config, Duration.ofDays(1)); // 临时token:15分钟过期 RedisUtils.setCacheObject("token:" + token, userInfo, Duration.ofMinutes(15)); ``` **使用合理的Key命名** ```java // 推荐的命名规范 String userKey = "user:profile:" + userId; String orderKey = "order:detail:" + orderId; String configKey = "system:config:" + configName; // 避免的命名方式 String badKey1 = userId; // 太简单,容易冲突 String badKey2 = "user_profile_" + userId; // 分隔符不统一 ``` ### 2. 性能优化 **批量操作** ```java // 批量删除 Collection keys = Arrays.asList("key1", "key2", "key3"); RedisUtils.deleteObject(keys); // 批量获取Map值 Set hKeys = Sets.newHashSet("field1", "field2", "field3"); Map values = RedisUtils.getMultiCacheMapValue("myMap", hKeys); ``` **合理使用二级缓存** ```java // 对于频繁访问的热点数据,启用本地缓存 @Cacheable("hotData#1h#30m#1000#1") // 启用本地缓存 public HotData getHotData(String key) { return dataService.findByKey(key); } // 对于更新频繁的数据,禁用本地缓存 @Cacheable("frequentData#30m#10m#500#0") // 禁用本地缓存 public FrequentData getFrequentData(String key) { return dataService.findByKey(key); } ``` ### 3. 错误处理 **Redis连接异常处理** ```java try { User user = RedisUtils.getCacheObject("user:" + userId); if (user == null) { // 缓存未命中,查询数据库 user = userService.findById(userId); if (user != null) { RedisUtils.setCacheObject("user:" + userId, user, Duration.ofHours(1)); } } return user; } catch (Exception e) { log.warn("Redis操作异常,降级到数据库查询", e); return userService.findById(userId); } ``` **分布式锁异常处理** ```java @Lock4j(keys = "#orderId", acquireTimeout = 5000, expire = 30000) public void processOrder(String orderId) { try { // 业务逻辑 orderService.process(orderId); } catch (LockFailureException e) { log.warn("获取订单锁失败: {}", orderId); throw new BusinessException("订单正在处理中,请稍后再试"); } catch (Exception e) { log.error("处理订单异常: {}", orderId, e); throw new BusinessException("订单处理失败"); } } ``` ### 4. 监控与运维 **关键指标监控** ```java // 自定义监控指标 @Component public class RedisMonitor { @EventListener public void onCacheHit(CacheHitEvent event) { // 记录缓存命中率 meterRegistry.counter("cache.hit", "cache", event.getCacheName()).increment(); } @EventListener public void onCacheMiss(CacheMissEvent event) { // 记录缓存未命中 meterRegistry.counter("cache.miss", "cache", event.getCacheName()).increment(); } @Scheduled(fixedRate = 60000) // 每分钟检查一次 public void checkRedisHealth() { try { RedisUtils.setCacheObject("health:check", System.currentTimeMillis()); log.debug("Redis健康检查正常"); } catch (Exception e) { log.error("Redis健康检查失败", e); // 发送告警 alertService.sendAlert("Redis连接异常"); } } } ``` ## 常见问题 ### 1. 序列化问题 **问题**:存储自定义对象时出现序列化异常 **解决方案**: ```java // 确保对象实现Serializable接口 public class User implements Serializable { private static final long serialVersionUID = 1L; // ... 字段定义 } // 或者使用JSON序列化友好的对象 @JsonIgnoreProperties(ignoreUnknown = true) public class User { // 使用Jackson注解处理序列化 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; // ... 其他字段 } ``` ### 2. 缓存穿透 **问题**:大量请求查询不存在的数据,导致缓存无法生效 **解决方案**: ```java public User getUserById(Long userId) { // 先查缓存 User user = RedisUtils.getCacheObject("user:" + userId); if (user != null) { return user; } // 查数据库 user = userService.findById(userId); // 即使查询结果为空也要缓存,设置较短过期时间 if (user == null) { RedisUtils.setCacheObject("user:" + userId, "NULL", Duration.ofMinutes(5)); return null; } // 缓存正常数据 RedisUtils.setCacheObject("user:" + userId, user, Duration.ofHours(1)); return user; } ``` ### 3. 缓存雪崩 **问题**:大量缓存在同一时间失效,导致数据库压力激增 **解决方案**: ```java public User getUserById(Long userId) { // 添加随机过期时间,避免同时失效 long baseExpireTime = Duration.ofHours(1).toMillis(); long randomExpireTime = baseExpireTime + (long)(Math.random() * 600000); // 加随机10分钟 User user = userService.findById(userId); RedisUtils.setCacheObject("user:" + userId, user, Duration.ofMillis(randomExpireTime)); return user; } ``` ### 4. 内存优化 **问题**:Caffeine本地缓存占用过多内存 **解决方案**: ```java // 调整Caffeine配置 @Bean public Cache caffeine() { return Caffeine.newBuilder() .maximumSize(500) // 减少最大缓存数量 .expireAfterWrite(15, TimeUnit.SECONDS) // 缩短过期时间 .expireAfterAccess(5, TimeUnit.SECONDS) // 增加访问过期时间 .build(); } // 或者针对特定业务禁用本地缓存 @Cacheable("largeData#1h#30m#100#0") // 最后一位设为0禁用本地缓存 public LargeData getLargeData(String key) { return dataService.findByKey(key); } ``` ## 总结 Redis模块提供了完整的缓存解决方案,通过二级缓存架构、分布式锁、队列管理等功能,为应用提供高性能、高可用的缓存服务。在使用过程中要注意合理设置过期时间、选择合适的数据结构、处理异常情况,并进行必要的监控,以确保系统的稳定性和性能。 --- --- url: 'https://ruoyi.plus/frontend/utils/rsa.md' --- # RSA加密 --- --- url: 'https://ruoyi.plus/backend/admin/module-resolution.md' --- # ruoyi-admin 模块解析 ## 1. 模块概述 ruoyi-admin 是基于 Spring Boot 的后台管理系统入口模块,作为整个应用程序的启动入口和配置中心。该模块集成了多个功能模块,提供了完整的企业级应用解决方案。 (注意:入口模块不要放业务功能) * **应用ID**: `ryplus_uni` * **应用名称**: ryplus-uni后台管理 * **运行端口**: 5500 * **版本**: 动态版本 `${revision}` ## 2. 核心文件分析 ### 2.1 主启动类 **文件**: `RuoyiPlus.java` ```java @SpringBootApplication public class RuoyiPlus { public static void main(String[] args) { // Spring应用启动配置 } } ``` **核心功能**: * Spring Boot 应用启动入口 * 配置应用启动性能监控 (BufferingApplicationStartup) * 启动完成后显示友好的成功提示信息 * 集成了应用名称、环境信息、端口等启动信息展示 ### 2.2 Web容器部署配置 **文件**: `RuoyiPlusServletInitializer.java` **作用**: * 支持传统 WAR 包部署到外部 Servlet 容器 * 继承 `SpringBootServletInitializer` * 适用于 Tomcat、Jetty 等外部容器部署场景 ## 3. 配置文件详解 ### 3.1 主配置文件 **文件**: `application.yml` #### 应用基础配置 ```yaml app: id: ryplus_uni title: ryplus-uni后台管理 version: ${revision} copyright-year: 2025 ``` #### 多租户配置 ```yaml tenant: enable: true excludes: [] # 排除表配置 ``` #### 服务器配置 ```yaml server: port: 5500 servlet: context-path: / undertow: max-http-post-size: -1 buffer-size: 512 threads: io: 8 worker: 256 ``` #### Spring框架配置 * **虚拟线程**: 启用 JDK21 虚拟线程支持 * **文件上传**: 最大单文件 10MB,总请求 20MB * **国际化**: 支持 i18n 多语言 * **JSON序列化**: Jackson 配置 ### 3.2 日志配置 **文件**: `logback-plus.xml` #### 日志策略 * **开发环境**: 仅控制台输出 * **生产环境**: 控制台 + 文件输出 * **日志分级**: INFO、ERROR 分别存储 * **异步处理**: 使用 AsyncAppender 提升性能 * **滚动策略**: 按天滚动,错误日志保留60天 #### 日志文件分类 * `sys-console.log`: 控制台日志备份 (保留1天) * `sys-info.log`: INFO级别日志 (保留60天) * `sys-error.log`: ERROR级别日志 (保留60天) ### 3.3 国际化配置 **文件**: `messages.properties` #### 消息分类 * **通用验证消息**: 必填验证、长度验证等 * **用户认证消息**: 登录、注册、密码重试等 * **权限控制消息**: 各种操作权限提示 * **文件上传消息**: 文件大小、类型限制等 * **租户管理消息**: 租户状态、同步操作等 ## 4. 核心功能配置 ### 4.1 安全认证 (Sa-Token) ```yaml sa-token: token-name: Authorization is-concurrent: true # 允许并发登录 is-share: false # 每次登录新建token jwt-secret-key: uDkkASPQVN5iR4eN ``` ### 4.2 验证码配置 ```yaml captcha: type: MATH # 数学计算验证码 category: CIRCLE # 圆圈干扰 numberLength: 1 # 数字验证码位数 charLength: 4 # 字符验证码长度 ``` ### 4.3 数据加密 * **数据库加密**: 支持字段级加密 (可选) * **API接口加密**: RSA 非对称加密 * **默认算法**: BASE64 编码 ### 4.4 数据库配置 (MyBatis-Plus) ```yaml mybatis-plus: enableLogicDelete: true # 全局逻辑删除 mapperPackage: plus.ruoyi.**.mapper typeAliasesPackage: plus.ruoyi.**.domain.entity global-config: dbConfig: idType: ASSIGN_ID # 雪花算法ID ``` ## 5. 模块依赖关系 ### 5.1 核心业务模块 ```xml plus.ruoyi ruoyi-system plus.ruoyi ruoyi-business ``` ### 5.2 功能增强模块 ```xml plus.ruoyi ruoyi-generator ``` ### 5.3 监控工具 ```xml de.codecentric spring-boot-admin-starter-client ``` ## 6. 高级特性 ### 6.1 消息推送 * **SSE**: 默认启用服务器推送事件 * **WebSocket**: 可选的双向通信 (默认关闭) ### 6.2 分布式锁 ```yaml lock4j: acquire-timeout: 3000 # 获取锁超时时间 expire: 30000 # 锁过期时间 ``` ### 6.3 线程池配置 * **虚拟线程**: JDK21 支持 (推荐) * **传统线程池**: 可配置队列容量和空闲时间 ### 6.4 API文档 (SpringDoc) * **多模块分组**: business、system、generator * **安全认证**: ApiKey 方式 * **自动生成**: 支持 OpenAPI 3.0 ### 6.5 系统监控 (Actuator) * **健康检查**: 详细健康信息展示 * **日志监控**: 外部日志文件访问 * **端点暴露**: 暴露所有监控端点 ## 7. 安全防护 ### 7.1 XSS防护 (如果富文本标签被过滤掉则需要将相关接口在此配置处排除) ```yaml xss: enabled: true excludeUrls: - /system/notice/* ``` ### 7.2 请求控制 * **重复提交防护**: 防止表单重复提交 * **访问频率限制**: 防止恶意刷新 ### 7.3 密码安全 ```yaml user: password: maxRetryCount: 5 # 最大错误次数 lockTime: 10 # 锁定时间(分钟) ``` ## 8. 部署特性 ### 8.1 多环境支持 * **开发环境**: 仅控制台日志 * **生产环境**: 完整日志体系 + 文件存储 * **配置切换**: 通过 `@profiles.active@` 动态切换 ### 8.2 容器化支持 * **内嵌容器**: Undertow (高性能) * **外部容器**: 支持 WAR 包部署 * **Docker友好**: 配置文件外置支持 ## 9. 总结 ruoyi-admin 模块是一个功能完整的企业级应用启动模块 --- --- url: 'https://ruoyi.plus/backend/common/bom.md' --- # ruoyi-common-bom 依赖管理模块 ## 1. 模块概述 ruoyi-common-bom 是common通用模块的统一依赖管理模块,负责定义和管理所有common子模块的版本信息。通过 bom 模式实现了依赖版本的统一管理,避免了版本冲突,简化了项目维护。 * **GroupId**: `plus.ruoyi` * **ArtifactId**: `ruoyi-common-bom` * **当前版本**: `5.4.0` * **打包方式**: `pom` ## 2. BOM 架构设计 ### 2.1 版本管理策略 ```xml 5.4.0 ``` * 采用统一版本号管理 * 所有模块使用 `${revision}` 占位符 * 便于版本升级和发布管理 ### 2.2 依赖管理范围 通过 父pom文件(根目录pom文件)`` 统一管理 30+ 个功能模块的版本,确保整个项目的依赖一致性。 ## 3. 功能模块分类 ### 3.1 核心基础模块 (Core Foundation) | 模块名称 | ArtifactId | 功能说明 | |-------------|-------------------------|--------------------| | **核心模块** | `ruoyi-common-core` | 基础工具类与通用功能,系统核心依赖 | | **安全模块** | `ruoyi-common-security` | 应用安全防护功能,包含加密、验证等 | | **Web服务模块** | `ruoyi-common-web` | Web应用基础功能支持,MVC配置等 | | **日志记录模块** | `ruoyi-common-log` | 统一日志处理功能,日志切面和存储 | **特点**: 这些是系统的基础设施模块,几乎所有其他模块都会依赖它们。 ### 3.2 数据处理模块 (Data Processing) | 模块名称 | ArtifactId | 功能说明 | |---------------|--------------------------|-----------------| | **数据库服务模块** | `ruoyi-common-mybatis` | MyBatis增强与数据库交互 | | **缓存服务模块** | `ruoyi-common-redis` | Redis缓存集成与工具 | | **序列化模块** | `ruoyi-common-json` | JSON序列化与反序列化工具 | | **数据库加解密模块** | `ruoyi-common-encrypt` | 敏感数据加密存储 | | **Excel处理模块** | `ruoyi-common-excel` | Excel导入导出功能 | | **脱敏模块** | `ruoyi-common-sensitive` | 数据脱敏与隐私保护 | | **OSS模块** | `ruoyi-common-oss` | 对象存储服务集成 | | **翻译模块** | `ruoyi-common-serialmap` | 多语言支持与国际化 | **特点**: 专注于数据的存储、处理、转换和安全保护。 ### 3.3 安全与认证模块 (Security & Authentication) | 模块名称 | ArtifactId | 功能说明 | |---------------|------------------------|-----------------| | **SaToken模块** | `ruoyi-common-satoken` | 权限认证框架,统一认证管理 | | **租户模块** | `ruoyi-common-tenant` | 多租户支持,数据隔离 | | **社交登录模块** | `ruoyi-common-social` | 第三方账号集成,OAuth登录 | **特点**: 提供完整的身份认证和授权解决方案。 ### 3.4 通信与消息模块 (Communication & Messaging) | 模块名称 | ArtifactId | 功能说明 | |-----------------|--------------------------|---------------| | **邮件服务模块** | `ruoyi-common-mail` | 邮件发送集成,支持模板邮件 | | **短信模块** | `ruoyi-common-sms` | 短信发送集成,验证码服务 | | **WebSocket模块** | `ruoyi-common-websocket` | 实时双向通信支持 | | **SSE模块** | `ruoyi-common-sse` | 服务器发送事件支持 | **特点**: 覆盖了现代应用的各种通信需求。 ### 3.5 API与接口模块 (API & Documentation) | 模块名称 | ArtifactId | 功能说明 | |------------|--------------------|------------------------------| | **接口文档模块** | `ruoyi-common-doc` | API文档生成与测试,集成Swagger/OpenAPI | **特点**: 提供API文档自动生成和在线测试能力。 ### 3.6 系统功能模块 (System Features) | 模块名称 | ArtifactId | 功能说明 | |----------|----------------------------|-----------| | **调度模块** | `ruoyi-common-job` | 定时任务与作业调度 | | **幂等模块** | `ruoyi-common-idempotent` | 防止重复提交与操作 | | **限流模块** | `ruoyi-common-ratelimiter` | 接口限流与流量控制 | **特点**: 提供系统级的高级功能支持。 ### 3.7 业务扩展模块 (Business Extensions) | 模块名称 | ArtifactId | 功能说明 | |-------------|------------------------|-----------| | **微信小程序模块** | `ruoyi-common-miniapp` | 微信小程序开发支持 | | **微信公众号模块** | `ruoyi-common-mp` | 微信公众号开发支持 | | **支付模块** | `ruoyi-common-pay` | 支付功能集成 | **特点**: 面向具体业务场景的功能模块。 ## 4. BOM 使用优势 ### 4.1 版本统一管理 * ✅ **一处修改,全局生效**: 只需修改 `revision` 属性即可升级所有模块 * ✅ **避免版本冲突**: 确保所有模块使用相同版本,减少兼容性问题 * ✅ **简化维护**: 集中管理依赖版本,降低维护成本 ### 4.2 依赖管理简化 ```xml plus.ruoyi ruoyi-common-core ``` ### 4.3 模块化架构 * ✅ **高内聚低耦合**: 每个模块职责单一,功能清晰 * ✅ **按需引入**: 项目可根据需要选择性引入模块 * ✅ **扩展灵活**: 新功能可独立成模块,不影响现有架构 ## 5. 模块依赖关系 ``` ruoyi-common-bom (根依赖管理) ├── 核心基础层 │ ├── ruoyi-common-core (基础工具) │ ├── ruoyi-common-security (安全防护) │ ├── ruoyi-common-web (Web支持) │ └── ruoyi-common-log (日志处理) ├── 数据处理层 │ ├── ruoyi-common-mybatis (数据库) │ ├── ruoyi-common-redis (缓存) │ ├── ruoyi-common-json (序列化) │ └── ... (其他数据处理模块) ├── 认证授权层 │ ├── ruoyi-common-satoken (认证) │ ├── ruoyi-common-tenant (多租户) │ └── ruoyi-common-social (社交登录) ├── 通信消息层 │ ├── ruoyi-common-websocket (实时通信) │ ├── ruoyi-common-sse (服务推送) │ └── ... (其他通信模块) └── 业务扩展层 ├── ruoyi-common-miniapp (小程序) ├── ruoyi-common-pay (支付) └── ... (其他业务模块) ``` ## 6. 最佳实践建议 ### 6.1 使用 BOM 的项目配置 ```xml plus.ruoyi ruoyi-common-bom ${revision} pom import ``` ### 6.2 模块选择策略 * **必选模块**: `core`, `security`, `web`, `log` * **数据相关**: 根据存储需求选择 `mybatis`, `redis`, `json` 等 * **认证授权**: 根据安全需求选择 `satoken`, `tenant` 等 * **业务功能**: 根据具体业务场景按需选择 --- --- url: 'https://ruoyi.plus/changelog.md' --- # Ruoyi-Plus-Uniapp新特性 * **[📺新特性全面解析](https://www.bilibili.com/video/BV1YrtMzvEaT/)** ### 核心理念 * **代码即文档** - 通过规范化命名和完善注释实现代码自解释 * **全栈统一** - 前后端命名规范、类型定义、接口管理保持一致 * **开发友好** - 注重开发体验和可维护性,减少冗余代码 ## 一、后端重构优化 ### 1.1 基础架构重构 #### 查询增强组件 * 新增 IBaseService 接口及实现类BaseServiceImpl,封装常见业务操作,支持泛型适配与反射优化,极致减少样板代码 * 增强 MyBatis-Plus 查询功能,Query增强为PlusQuery,LambdaQuery增强为PlusLambdaQuery 支持聚合函数及条件自动处理 #### 响应结果封装 * 重构 TableDataInfo 为 PageResult,统一返回数据为 `R>` 里面包含分页信息(是否最后一页 页码 每页条数等)和数据列表 * 完善 R 类注释,统一 API 响应结果封装,优化成功和失败消息返回方法 * 增加安全获取 data 方法和标准 API 响应结构 ### 1.2 配置与环境管理 #### 应用配置重构 * 增加前后端唯一标识符应用ID,做好不同项目间的数据隔离 * 重命名 RuoYiConfig 为 AppConfig,更新相关配置项 * 增加core核心包相关工具类功能 ### 1.3 数据库与字典系统 #### 数据库结构调整 * 重构数据库表命名规范,统一使用 sys\_ 前缀 修改 gen\_table 为 sys\_gen\_table,gen\_table\_column 为 sys\_gen\_table\_column * 逻辑删除统一修改为 isDeleted * 性别字典修改:女0 男1 未知2,sys\_user\_sex 改为 sys\_user\_gender * 修改字典数据主键 dict\_code 改为 dict\_data\_id #### 字典系统重构 * 系统中统一采用 1=是/正面状态,0=否/负面状态 的约定 * 重构字典类型和字典值,字典数据默认值统一为 1,否为 0 * 重构字典枚举命名以 Dict 开头,字典枚举统一放在core/dict包下 提高代码可发现性 * 优化字典实现类 ### 1.4 租户系统完善 #### 租户功能增强 * 统一获取租户 ID 方案,提供兜底租户 ID,确保租户 ID 不为空 可以随时开启或者关闭租户功能不产生脏数据 * OSS 存储加上租户 ID 前缀作为目录区分 * 新增租户需要同步角色,增加角色同步到租户功能 因为开发过程角色也属于业务的一部分 实现同步功能可以最小化改动来供新租户使用 ### 1.5 权限与安全系统 #### 权限标识符规范化 * 菜单权限标识符为:模块:表:标识符 格式 * 如 system:user:view/query/add/update/delete/import/export * 代码生成默认生成权限控制部分 打开注释即可启用 #### 认证系统优化 * 登录实现迁移到系统模块 auth 包下,保持 admin 模块简洁 * 重命名授权类型和设备类型为认证方式和应用类型 * 移除客户端管理,减少冗余代码,精简实现 主要分为两个客户端即可 由枚举UserType进行控制管理 * 增加miniapp小程序模块,增加mp模块,实现对小程序(包括微信小程序的完整实现,以及QQ 支付宝 京东 抖音等各类小程序的扩展实现和预留实现)以及微信公众号的完整实现 开箱即用 * 实现小程序 公众号多个配置同时使用以及租户间的数据隔离 ### 1.6 文件管理系统 #### OSS 系统增强 * 重构 OSS 模块 抽离出接口层OssStrategy,可通过不同实现类实现不同的存储方式 如S3、本地文件等 * 重构 OSS 模块同时支持 S3 和本地文件上传 * 增加转换远程图片到 OSS 的接口实现 增加OSS文件的目录管理功能,支持多租户隔离 实现素材管理 * 头像存储统一修改为字符串存储链接 * 增加图片和文件前端/移动端直传功能,一键即可开启直传,支持多种云服务 ### 1.7 系统功能模块 #### 序列化模块增强和重构 * 重构序列化模块名称为serialmap,因为只涉及序列化,不涉及反序列化,因为不叫translation,命名上不够恰当 * 调整序列化注解为SerialMap,调整注解属性更符合规范 converter转换器 source来源 param额外参数 entityClass数据源实体类 targetField 目标映射字段 * 增加序列化注解实现FieldMapImpl,实现通用字典映射转换器,同时实现缓存,减少后续序列化实现的重复样板代码 #### 监控与日志 * monitor 监控增加通知功能 * 完善操作日志、登录日志等页面优化 * 重构日志注解的使用和类的命名 Log注解属性使用operType操作类型,指定字典DictOperType为操作类型枚举 * 抽离登录日志发布者到 log 模块为 LoginLogPublisher 实现日志发布异步保存操作 #### 代码生成增强 * 完善代码生成界面使用体验,固定 tab 页签和弹窗高度 * 实现主子表代码生成功能 * 增加并实现代码生成表字段的默认值功能 * 处理 Excel 导入更新实现和参数传递 ### 1.8 国际化系统 #### 消息国际化 * 增删改查等消息实现后端返回国际化 默认情况还是由前端处理国际化 * 通过接口常量实现管理和分类,去除 code 硬编码 * 菜单国际化后端返回时增加国际化键名计算 * 增加I18nMessageInterceptor国际化消息拦截器,简化国际化消息处理,无需花括号包围 ### 1.9 高级功能模块 #### 支付系统 * 增加 IJPay 支付模块和相关商品订单逻辑 支付回调等统一处理再根据不同支付方式进行分发 * 支付模块支持租户数据隔离和智能刷新数据 自动重试请求支付状态 #### 业务扩展 * 完成公告功能的可用性,实现精准推送和查阅 * 实现已读未读统计等功能 ### 1.10 部署与运维 #### Docker 部署 * 优化 docker-compose 相关编排名称 统一相关命名 * 在主应用添加远程调试参数,支持本地 idea 远程调试 * 完美适配 Docker容器化部署 #### 轻松开发和维护 * 全部代码注释完善,使用 Javadoc 规范 * 统一使用 Lombok 注解简化代码,减少样板代码 * 统一包管理和依赖管理规范 提供统一的包使用规范 提供业务模块供开发者直接用于开发业务逻辑 * 重构代码生成器,支持主子表生成,优化代码生成体验,接口路径、方法名、变量名等语义化且唯一,快速定位 ## 二、前端重构优化 ### 2.1 架构重构 #### 目录结构调整 * 重构为 composables 目录(组合式),lang 改为 locales * store 目录统一命名为 stores,提升语义化 layout命名为layouts directives 命名为 directives * 路由模块路由进行归类和分拆管理 * 调整前端项目结构与后端结构基本统一 #### 组件命名规范化 * 全局统一组件命名,不使用 index,提升开发体验 * 自定义组件统一使用首字母大写驼峰命名,ElementPlus 组件保持连字符 * 所有页面组件改为首字母小写的驼峰,后端菜单进行适配 #### 类型重构 * 全局统一类型命名规范,与后端保持一致 减少使用认知负担 如Bo Vo等 * 请求响应类型统一使用 `R`,减少冗余代码 ,分页类型统一使用 `R>`,减少冗余代码 ### 2.2 工具类与组合函数函数重构 #### Utils部分逻辑 重构为 Composables组合函数 发挥vue3的组合式API优势 * 移除 utils/auth.ts,封装 useToken 到 composables * 移除 utils/permission.ts,改为 composables/useAuth 组合函数 * 移除 utils/theme.ts,改为 composables/useTheme 组合函数 * 移除 utils/i18n,改为 composables/useI18n,全局使用自定义 i18n * 移除animate.ts,改为 composables/useAnimation 组合函数 * 移除dict.ts,改为 composables/useDict 组合函数 * 移除request.ts,改为 composables/useHttp 组合函数 使用http.get, http.post 等方法进行请求 * 移除sse.ts,改为 composables/useSSE 组合函数 * 移除websocket.ts,改为 composables/useWS 组合函数 * 移除i18n.ts,改为 composables/useI18n 组合函数 * 移除auth.ts,改为 composables/useToken 组合函数 * 增加useTableHeight 组合函数,优化表格高度计算 * 增加useSelection 组合函数,处理表格全选和取消全选 * 增加useDownload 组合函数,处理文件下载逻辑 #### 工具类功能增强 * 增加boolean.ts,封装布尔值相关方法 * 增加date.ts,封装日期相关方法 * 增加cache.ts,封装缓存相关方法 * 增加format.ts,封装格式化相关方法 * 增加function.ts,封装函数相关方法 如防抖、节流 拷贝等 * 增加modal.ts,封装模态框相关方法 统一调用为show开头的前缀如showMsgSuccess、showMsgError等 showConfirm、showPrompt等 * 增加object.ts,封装对象相关方法 * 增加string.ts,封装字符串相关方法 * 增加tab.ts,封装标签页导航操作相关工具函数 * 抽离树形相关方法为tree.ts,封装树形结构相关方法 * 增加class.ts,封装 DOM 操作相关方法 * 增加to.ts,封装安全异步执行工具函数集 减少try catch 的使用 * 增加validators.ts,封装表单验证相关方法 * 完善 utils/crypto.ts,进行方法扩充,jsencrypt 改名为 rsa ### 2.3 样式系统重构 #### 样式系统优化 * 为全部样式文件添加完善备注 * 重构分类简化所有样式文件 * 完善 UnoCSS 配置进行增强:颜色配置、间距变量、字体配置等 #### 布局组件重构 * 重构 Layout 页面相关组件,统一取消 index 命名 * 调整 ParentView 组件到 layout,移动 TopNav 组件到 navbar 目录 * 重新调整归类Layout层组件目录结构,调整样式控制实现 * 优化导航栏、页签效果,调整鼠标滚轮滚动效果 ### 2.4 表单与表格增强 #### 表单组件系统 * 增加各类表单组件: AFormCascader 级联选择 AFormCheckbox 复选框 AFormDate 日期选择 AFormEditor 富文本编辑器 富文本组件接入基于 tiptap 的 umo editor AFormFileUpload 文件上传 AFormlmgUpload 图片上传 AFormlnput 输入框 AFormRadio 单选框 AFormSelect 下拉选择 AFormSwitch 开关选择 AFormTreeSelect 树形选择 #### 表格功能增强 * 移除vxetable,vxetable 组件重构为 el-table 组件,实现跨页选择功能 * 封装 useSelection 组合函数处理表格全选和取消全选 * 增加 useTableHeight 优化表格显示效果,让分页组件保持固定位置 ### 2.5 权限指令增强 #### 权限自定义指令 * 完善重构权限自定义指令,支持延迟加载组件 * 扩充指令:permi、role、admin、superadmin、permiAll、roleAll 等 * 移除全局 proxy 代理使用,进行对应代码适配转换 ### 2.6 媒体库功能 #### AOssMediaManager 组件 * 添加媒体库功能组件 AOssMediaManager 增强图片上传 * 增加替换功能,优化图片管理体验 * 增加目录管理功能,支持多租户隔离,可以对图片进行分类管理,可以对图片进行批量操作 批量移动等(移动只是修改文件对应的所在目录id) * 配合后端实现前端文件直传功能 一个属性即可配置开启 ### 2.7 iconify引入图标功能重构菜单图标 #### 图标系统重构 * 引入 iconify 图标库,支持多种图标格式 * 重构菜单图标使用 iconify 图标,实现图标库管理功能,方便进行维护和扩展 * 优化图标选择组件,支持多种图标格式和自定义图标 * 图标组件重构为Icon,支持图标名称类型提示,支持海量图标库 ### 2.8 国际化系统 #### 前端国际化 * 增强useI18n 组合函数,增强t函数,实现智能提示 * 前端实现菜单国际化,增加统一键名 * 引入 ElementPlus 国际化资源,优化字体大小选择组件 ### 2.9 性能优化 #### 实时通信 * SSE 连接增加重连退避策略,支持手动重连和状态监控 * 实现动态退避策略的 WebSocket 连接管理 #### 构建优化 * 优化 Vite 配置 #### 代码优化 * 全框架取消使用 reactive 函数,统一使用 ref 函数 * 移除原生滚动,改用 el-scrollbar 滚动,优化滚动体验 * 优化重构 tree 页面模板 * 重构代码生成页面结构和实现,代码职责更加清晰 ## 三、移动端重构优化 ### 3.1 UniApp 框架重构 #### 框架改造 * 基于 unibest 框架进行重量级重构改造,移除不必要模块 * 增加应用 ID 配置管理,模仿前端实现 * 实现基础的 tabbar 页面,不使用原生 tabbar,使用自定义组件实现,可以更灵活地控制样式和功能 * 增加分包管理,可以实现管理员端和代码示例等的分包加载,优化小程序加载速度 #### 目录结构重构 * 重新调整插件目录结构,分拆插件分别维护 * 复用前端 composables 目录下的部分组合函数函数,优化移动端开发体验 * 封装 pinia 的相关模块:dict、tabbar、user 等 ### 3.2 网络请求与认证 #### HTTP 请求封装 * 封装移动端 useHttp,实现移动端 API 加密解密 * 统一请求拦截和响应处理机制 #### 小程序登录认证 * 实现微信小程序登录和公众号登录,模块化管理 * 实现 unionid/手机号关联绑定用户账号唯一性 * 用户手机实现无账户则自动注册 * 实现登录页面的自动登录开关,以及多种登录方式的支持 * 实现手机号注册和登录功能,支持验证码登录和密码登录 ### 3.3 组合式函数与工具类 #### Composables 组合函数函数 * 复用前端 useDict、useAuth 等组合函数函数 * 增加移动端 usePayment 组合函数,封装支付相关逻辑 * 增加 useScroll 组合函数,封装滚动相关逻辑 * 增加 useTheme 组合函数,封装主题相关逻辑 #### 工具类函数增强 * 复用前端的 utils 工具类库部分功能:boolean.ts、date.ts、format.ts、function.ts、object.ts、string.ts 等 * 新增 tenant.ts,封装租户相关逻辑 * 新增 validators.ts,封装移动端表单验证相关方法 ### 3.4 组件库重构 #### WotUI 组件重构 * 重构 wot-ui 组件库所有组件,使用最新的 vue3 和 typescript 语法进行重构 * 提高代码可读性、自行维护性和可扩展性 * 重构单位统一为 rpx,移动端获得更好体验 * 增加 wd-paging 组件,实现下滑分页加载功能 #### 图标组件系统 * 增加 wd-iconify 组件,支持 iconify 图标库 * 增加 wd-icon 组件,支持 iconify 图标库和 json 图标 * 重构了图标库近 400 个图标,包含线条图标和实心图标两大类 * 可以方便在示例代码中搜索使用 ### 3.5 小程序功能增强 #### 多平台支持 * 支持微信小程序完整功能实现 * 预留 QQ、支付宝、京东、抖音等各类小程序的扩展实现 * 统一小程序 API 调用接口 #### 示例代码系统 * 增加完善和齐全 wd 组件示例代码,可以直接看到效果 * 效果判断统一有代码查看和复制按钮,方便开发者查看和使用 * 提供丰富的组件使用示例和最佳实践 *** 每个端的重构都遵循模块化、标准化的原则,确保代码质量和可维护性。 --- --- url: 'https://ruoyi.plus/frontend/configuration.md' --- # RyPlus-Uni 前端配置文件说明文档 ## 技术栈概述 本项目基于 Vue 3 + TypeScript + Vite 构建,采用现代化前端开发技术栈: * **构建工具**: Vite 5.x * **前端框架**: Vue 3.4+ (Composition API) * **开发语言**: TypeScript 5.x * **UI框架**: Element Plus * **CSS框架**: UnoCSS (原子化CSS) * **状态管理**: Pinia * **路由管理**: Vue Router 4.x * **代码规范**: ESLint + Prettier *** ## 配置文件结构 ### 1. 环境变量配置 #### 1.1 基础环境配置 (`.env`) ```bash # ===== 应用基础配置 ===== VITE_APP_ID = 'ryplus_uni' # 应用唯一标识符 VITE_APP_TITLE = 'ryplus-uni后台管理' # 浏览器标签页标题 VITE_APP_CONTEXT_PATH = '/' # 应用访问路径前缀 VITE_ENABLE_FRONTEND = 'false' # 是否启用前台首页 # ===== 安全配置 ===== VITE_APP_API_ENCRYPT = 'true' # 接口加密开关 VITE_APP_RSA_PUBLIC_KEY = 'MFww...' # RSA加密公钥(与后端配对) VITE_APP_RSA_PRIVATE_KEY = 'MIIBOw...' # RSA解密私钥(与后端配对) # ===== 功能开关 ===== VITE_APP_WEBSOCKET = 'false' # WebSocket功能开关 VITE_APP_SSE = 'true' # Server-Sent Events开关 # ===== 外部服务地址 ===== VITE_APP_GIT_URL = '' # 代码仓库地址 VITE_APP_DOC_URL = 'https://ruoyi.plus' # 项目文档地址 ``` #### 1.2 开发环境配置 (`.env.development`) ```bash VITE_APP_ENV = 'development' # 环境标识 VITE_APP_PORT = '80' # 开发服务器端口 VITE_APP_BASE_API = '/dev-api' # API接口前缀 VITE_APP_BASE_API_PORT = '5500' # 后端服务端口 # ===== 外部服务地址 ===== VITE_APP_MONITOR_ADMIN = 'http://127.0.0.1:9090/admin/applications' VITE_APP_SNAILJOB_ADMIN = 'http://127.0.0.1:8800/snail-job' ``` #### 1.3 生产环境配置 (`.env.production`) ```bash VITE_APP_ENV = 'production' # 生产环境标识 VITE_APP_BASE_API = '/ryplus_uni' # 生产环境API前缀 VITE_BUILD_COMPRESS = 'gzip' # 构建压缩方式 # ===== 外部服务地址 ===== VITE_APP_MONITOR_ADMIN = '/admin/applications' VITE_APP_SNAILJOB_ADMIN = '/snail-job' ``` **环境变量命名规范:** * 所有环境变量必须以 `VITE_` 开头才能在前端代码中访问 * 使用 `SCREAMING_SNAKE_CASE` 命名风格 * 按功能分组,便于管理和维护 *** ### 2. 构建配置 #### 2.1 Vite 主配置 (`vite.config.ts`) ```typescript export default async ({ command, mode }: ConfigEnv): Promise => { const env = loadEnv(mode, path.resolve(process.cwd(), 'env')) return defineConfig({ // 环境变量目录 envDir: './env', // 应用基础路径 base: env.VITE_APP_CONTEXT_PATH, // 路径别名配置 resolve: { alias: { '@': path.join(process.cwd(), './src') // @ 指向 src 目录 }, extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'] }, // 开发服务器配置 server: { host: '0.0.0.0', // 监听所有地址 port: Number(env.VITE_APP_PORT), // 服务端口 open: true, // 自动打开浏览器 proxy: { // 代理配置解决跨域 [env.VITE_APP_BASE_API]: { target: `http://localhost:${env.VITE_APP_BASE_API_PORT}`, changeOrigin: true, ws: true, rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '') } } } }) } ``` **关键配置说明:** * **envDir**: 自定义环境变量文件目录 * **base**: 应用部署的基础路径,支持子路径部署 * **proxy**: 开发环境API代理,解决跨域问题 * **alias**: 路径别名,简化模块导入 * **optimizeDeps**: 依赖预构建,提升首次加载速度 *** ### 3. TypeScript 配置 #### 3.1 TypeScript 编译配置 (`tsconfig.json`) ```json { "compilerOptions": { "baseUrl": ".", // 模块解析基准目录 "target": "ES2020", // 编译目标版本 "module": "ESNext", // 模块系统 "moduleResolution": "Bundler", // 模块解析策略 "lib": [ "ESNext", "DOM", "DOM.Iterable" ], // 包含的库文件 // 严格模式配置 "strict": true, // 启用所有严格检查 "noImplicitAny": false, // 允许隐式any类型 "strictNullChecks": false, // 关闭严格null检查 // 其他配置 "allowJs": true, // 允许编译JS文件 "jsx": "preserve", // 保留JSX语法 "sourceMap": true, // 生成source map "resolveJsonModule": true, // 支持导入JSON "esModuleInterop": true, // ES模块互操作 "experimentalDecorators": true, // 装饰器支持 // 路径映射 "paths": { "@/*": [ "./src/*" ] }, // 类型定义 "types": [ "node", "vite/client" ] }, "include": [ "src/**/*.ts", "src/**/*.vue", "vite.config.ts", "eslint.config.ts" ], "exclude": [ "node_modules", "dist", "src/**/__tests__/*" ] } ``` *** ### 4. 代码规范配置 #### 4.1 ESLint 配置 (`eslint.config.ts`) ```typescript export default defineConfigWithVueTs( // 检查的文件类型 { files: ['**/*.{js,cjs,ts,mts,tsx,vue}'] }, // 忽略的文件和目录 { ignores: [ '**/dist/**', // 构建输出目录 '**/coverage/**', // 测试覆盖率报告 '**/locales/**/*.ts' // 语言文件 ] }, // 自定义规则 { plugins: { prettier }, rules: { // TypeScript 相关规则 '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', // Vue 相关规则 'vue/multi-word-component-names': 'off', 'vue/valid-define-props': 'off', 'vue/no-v-model-argument': 'off', // 强制 Prettier 格式化 'prettier/prettier': 'error' } } ) ``` **ESLint 规则说明:** * **基础规则**: 继承Vue和TypeScript推荐配置 * **自定义规则**: 根据项目需求调整的规则 * **Prettier集成**: 确保代码格式化一致性 *** ### 5. 样式配置 #### 5.1 UnoCSS 配置 (`uno.config.ts`) ```typescript export default defineConfig({ // 快捷方式定义 shortcuts: { 'panel-title': 'pb-[5px] font-sans leading-[1.1] font-medium text-base text-[#6379bb]...', 'flex-center': 'flex items-center justify-center', 'flex-between': 'flex items-center justify-between' }, // 主题配置 theme: { colors: { // 状态颜色 'primary': 'var(--el-color-primary)', 'success': 'var(--color-success)', 'warning': 'var(--color-warning)', 'danger': 'var(--color-danger)', // 文本颜色 'text-base': 'var(--text-color)', 'text-secondary': 'var(--text-color-secondary)', // 背景颜色 'bg-base': 'var(--bg-color)', 'bg-page': 'var(--bg-color-page)' }, // 间距变量 spacing: { 'sidebar': 'var(--sidebar-width)', 'header': 'var(--header-height)' } }, // 自定义规则 rules: [ ['sidebar-width', { 'width': 'var(--sidebar-width)' }], ['scrollbar-y', { 'overflow-y': 'auto', 'overflow-x': 'hidden' }], ['text-ellipsis', { 'white-space': 'nowrap', 'overflow': 'hidden', 'text-overflow': 'ellipsis' }] ], // 预设配置 presets: [ presetUno(), // 默认工具类 presetAttributify(), // 属性化模式 presetIcons({}), // 图标支持 presetTypography(), // 排版支持 presetWebFonts({}) // Web字体支持 ] }) ``` **UnoCSS 特性:** * **原子化CSS**: 提供大量原子级样式类 * **按需生成**: 只生成实际使用的样式 * **主题系统**: 支持CSS变量和主题切换 * **快捷方式**: 将常用样式组合定义为简单类名 *** ## 开发环境配置 ### 1. 环境要求 ```bash # Node.js 版本要求 Node.js >= 18.0.0 # 包管理器(推荐使用pnpm) pnpm >= 8.0.0 npm >= 9.0.0 yarn >= 1.22.0 ``` ### 2. 安装和启动 ```bash # 安装依赖 pnpm install # 启动开发服务器 pnpm dev # 构建生产版本 pnpm build # 代码格式化 pnpm lint:fix # 类型检查 pnpm type-check ``` ### 3. 开发服务器特性 * **热模块替换(HMR)**: 代码修改后即时更新 * **API代理**: 自动转发API请求到后端服务 * **TypeScript支持**: 实时类型检查和错误提示 * **Vue DevTools**: 支持Vue开发者工具调试 *** ## 常见问题与解决方案 ### Q1: 开发环境接口请求失败? **A**: 检查以下配置: * 确认 `VITE_APP_BASE_API_PORT` 与后端服务端口一致 * 检查代理配置是否正确 * 确认后端服务已启动 ### Q2: 环境变量无法访问? **A**: 确保环境变量: * 以 `VITE_` 开头 * 在正确的 `.env` 文件中定义 * 重启开发服务器 *** > 💡 **提示**: > > * 开发前请确保所有环境变量配置正确 > * 生产部署时注意检查所有外部服务地址 > * 定期更新依赖包版本以获得最新功能和安全修复 --- --- url: 'https://ruoyi.plus/backend/common/job.md' --- # SnailJob任务调度模块文档 ## 概述 本模块基于 [SnailJob](https://snailjob.opensnail.com/) 分布式任务调度框架,为ruoyi-plus-uniapp提供强大的定时任务和分布式任务调度能力。SnailJob是一个灵活、可靠、快速的分布式任务调度平台,支持多种任务类型和执行模式。 ## 模块结构 ``` ruoyi-common-job/ ├── src/main/java/plus/ruoyi/common/job/ │ └── config/ │ └── SnailJobConfig.java # SnailJob自动配置类 └── src/main/resources/META-INF/ └── spring/ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports # 自动配置注册 ``` ## 依赖说明 ### Maven依赖 ```xml com.aizuda snail-job-client-starter com.aizuda snail-job-client-job-core com.aizuda snail-job-client-retry-core ``` ## 配置说明 ### 环境配置 #### 开发环境配置(application-dev.yml) ```yaml ################## 定时任务配置 ################## --- # snail-job 配置 snail-job: # 是否启用定时任务 enabled: false # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务 group: ${app.id} # SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表 token: "SJ_xxxxxxxxxxxxxxxxxxxxxxxxx" server: # 调度中心地址 host: 127.0.0.1 # 调度中心端口 port: 17888 # 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段 namespace: ${spring.profiles.active} # 随主应用端口漂移 port: 2${server.port} # 客户端ip指定 host: # RPC类型: netty, grpc rpc-type: grpc ``` #### 生产环境配置(application-prod.yml) ```yaml ################## 定时任务配置 ################## --- # snail-job 配置 snail-job: # 生产环境启用定时任务 enabled: true # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务 group: ${app.id} # SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config`表 token: "SJ_xxxxxxxxxxxxxxxxxxxxxxxxx" server: # 调度中心地址 host: 127.0.0.1 # 调度中心端口 port: 17888 # 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段 namespace: ${spring.profiles.active} # 随主应用端口漂移 port: 2${server.port} # 客户端ip指定 host: # RPC类型: netty, grpc rpc-type: grpc ``` ### 配置参数说明 | 参数 | 说明 | 示例值 | |------|------|--------| | `enabled` | 是否启用SnailJob模块 | `true`/`false` | | `group` | 任务组名,需要在SnailJob后台预先创建 | `${app.id}` | | `token` | 接入验证令牌,对应数据库`sj_group_config`表 | `SJ_xxxxxxxxxxxxxxxxxxxxxxxxx` | | `server.host` | SnailJob调度中心地址 | `127.0.0.1` | | `server.port` | SnailJob调度中心端口 | `17888` | | `namespace` | 命名空间UUID,对应数据库`sj_namespace`表的`unique_id`字段 | `${spring.profiles.active}` | | `port` | 客户端端口,建议随主应用端口漂移 | `2${server.port}` | | `host` | 客户端IP指定,留空则自动获取 | 留空 | | `rpc-type` | RPC通信类型 | `grpc`/`netty` | ### 重要说明 1. **开发环境默认关闭**:为避免开发时误触生产任务,开发环境默认 `enabled: false` 2. **生产环境启用**:生产环境设置 `enabled: true` 启用定时任务功能 3. **组管理**:使用 `${app.id}` 作为组名,需要在SnailJob后台组管理中预先创建对应的组 4. **命名空间隔离**:使用 `${spring.profiles.active}` 作为命名空间,实现不同环境的任务隔离 5. **端口配置**:客户端端口使用 `2${server.port}` 模式,避免端口冲突 6. **数据库初始化**:需要执行 `script/sql/ry_job.sql` 初始化SnailJob相关数据表 ### 自动配置 模块提供了自动配置类 `SnailJobConfig`: * **条件装配**:只有当 `snail-job.enabled=true` 时才会启用 * **自动启用**: * `@EnableScheduling`:启用Spring定时任务支持 * `@EnableSnailJob`:启用SnailJob客户端 * **日志收集**:自动配置日志收集器,将应用日志发送到SnailJob服务端 ## 任务开发指南 ### 任务开发目录 建议在业务模块中创建任务类: ``` plus.ruoyi.business.job/ ├── normal/ # 普通任务 ├── sharding/ # 分片任务 ├── mapreduce/ # MapReduce任务 ├── broadcast/ # 广播任务 └── workflow/ # 工作流任务 ``` ### 1. 普通任务 使用 `@JobExecutor` 注解创建普通任务: ```java @Component @JobExecutor(name = "testJobExecutor") public class TestAnnoJobExecutor { public ExecuteResult jobExecute(JobArgs jobArgs) { SnailJobLog.LOCAL.info("本地日志: {}", jobArgs.getJobParams()); SnailJobLog.REMOTE.info("远程日志: {}", jobArgs.getJobParams()); return ExecuteResult.success("任务执行成功"); } } ``` ### 2. 静态分片任务 根据服务端参数进行分片处理: ```java @Component @JobExecutor(name = "testStaticShardingJob") public class TestStaticShardingJob { public ExecuteResult jobExecute(JobArgs jobArgs) { String jobParams = Convert.toStr(jobArgs.getJobParams()); String[] split = jobParams.split(","); Long fromId = Long.parseLong(split[0]); Long toId = Long.parseLong(split[1]); // 处理指定范围的数据 processDataRange(fromId, toId); return ExecuteResult.success("分片任务执行完成"); } } ``` ### 3. Map任务(动态分片) 只分片不关注合并结果: ```java @Component @JobExecutor(name = "testMapJobAnnotation") public class TestMapJobAnnotation { @MapExecutor public ExecuteResult doJobMapExecute(MapArgs mapArgs, MapHandler mapHandler) { // 数据分片 int partitionSize = 50; List> partition = IntStream.rangeClosed(1, 200) .boxed() .collect(Collectors.groupingBy(i -> (i - 1) / partitionSize)) .values() .stream() .toList(); return mapHandler.doMap(partition, "doCalc"); } @MapExecutor(taskName = "doCalc") public ExecuteResult doCalc(MapArgs mapArgs) { List sourceList = (List) mapArgs.getMapResult(); int partitionTotal = sourceList.stream().mapToInt(i -> i).sum(); SnailJobLog.REMOTE.info("分片计算结果: {}", partitionTotal); return ExecuteResult.success(partitionTotal); } } ``` ### 4. MapReduce任务 分片后合并结果: ```java @Component @JobExecutor(name = "testMapReduceAnnotation") public class TestMapReduceAnnotation { @MapExecutor public ExecuteResult rootMapExecute(MapArgs mapArgs, MapHandler mapHandler) { // 数据分片逻辑 List> partition = createPartitions(); return mapHandler.doMap(partition, "doCalc"); } @MapExecutor(taskName = "doCalc") public ExecuteResult doCalc(MapArgs mapArgs) { // 分片计算逻辑 List sourceList = (List) mapArgs.getMapResult(); int partitionTotal = sourceList.stream().mapToInt(i -> i).sum(); return ExecuteResult.success(partitionTotal); } @ReduceExecutor public ExecuteResult reduceExecute(ReduceArgs reduceArgs) { // 合并结果 int reduceTotal = reduceArgs.getMapResult() .stream() .mapToInt(i -> Integer.parseInt((String) i)) .sum(); return ExecuteResult.success(reduceTotal); } } ``` ### 5. 广播任务 在所有客户端节点上执行: ```java @Component @JobExecutor(name = "testBroadcastJob") public class TestBroadcastJob { @Value("${snail-job.port}") private int clientPort; public ExecuteResult jobExecute(JobArgs jobArgs) { SnailJobLog.REMOTE.info("广播任务在端口 {} 执行", clientPort); // 广播任务逻辑 boolean success = executeBroadcastLogic(); if (success) { return ExecuteResult.success("广播任务执行成功"); } else { throw new RuntimeException("广播任务执行失败"); } } } ``` ### 6. 工作流任务(DAG) 支持复杂的任务依赖关系: ```java // 微信账单任务 @Component @JobExecutor(name = "wechatBillTask") public class WechatBillTask { public ExecuteResult jobExecute(JobArgs jobArgs) { BillDto billDto = new BillDto(); billDto.setBillChannel("wechat"); // 从工作流上下文获取参数 String settlementDate = (String) jobArgs.getWfContext().get("settlementDate"); if (StrUtil.equals(settlementDate, "sysdate")) { settlementDate = DateUtil.today(); } billDto.setBillDate(settlementDate); billDto.setBillAmount(new BigDecimal("1234.56")); // 将结果放入上下文传递给下游任务 jobArgs.appendContext("wechat", JsonUtils.toJsonString(billDto)); return ExecuteResult.success(billDto); } } // 支付宝账单任务 @Component @JobExecutor(name = "alipayBillTask") public class AlipayBillTask { // 类似实现... } // 汇总任务 @Component @JobExecutor(name = "summaryBillTask") public class SummaryBillTask { public ExecuteResult jobExecute(JobArgs jobArgs) { // 从上下文获取上游任务结果 String wechat = (String) jobArgs.getWfContext("wechat"); String alipay = (String) jobArgs.getWfContext("alipay"); // 汇总计算 BigDecimal totalAmount = calculateTotal(wechat, alipay); return ExecuteResult.success(totalAmount); } } ``` ### 7. 继承方式创建任务 除了注解方式,还可以通过继承 `AbstractJobExecutor`: ```java @Component public class TestClassJobExecutor extends AbstractJobExecutor { @Override protected ExecuteResult doJobExecute(JobArgs jobArgs) { // 任务逻辑 return ExecuteResult.success("继承方式任务执行成功"); } } ``` ## 核心API说明 ### 任务参数(JobArgs) ```java public class JobArgs { private Object jobParams; // 任务参数 private Map wfContext; // 工作流上下文 // 获取任务参数 public Object getJobParams(); // 获取工作流上下文 public Map getWfContext(); public Object getWfContext(String key); // 向上下文添加数据 public void appendContext(String key, Object value); } ``` ### 执行结果(ExecuteResult) ```java // 成功结果 ExecuteResult.success("执行成功"); ExecuteResult.success(resultData); // 失败结果 ExecuteResult.failure("执行失败"); ``` ### 日志记录 ```java // 本地日志(仅在客户端记录) SnailJobLog.LOCAL.info("本地日志信息"); // 远程日志(发送到SnailJob服务端) SnailJobLog.REMOTE.info("远程日志信息"); SnailJobLog.REMOTE.error("错误信息", exception); ``` ## 特性说明 ### 1. 分布式调度 * 支持集群部署,任务在多个节点间负载均衡 * 故障转移,节点宕机时任务自动迁移 ### 2. 多种任务类型 * **普通任务**:单机执行的简单任务 * **分片任务**:大数据量任务的分片并行处理 * **MapReduce任务**:支持分布式计算模式 * **广播任务**:在所有节点执行的任务 * **工作流任务**:支持DAG有向无环图的复杂任务流 ### 3. 可视化管理 * Web控制台管理任务 * 实时监控任务执行状态 * 查看任务执行日志 * 任务执行历史统计 ### 4. 高可用性 * 任务重试机制 * 失败告警通知 * 集群容错处理 ## 最佳实践 ### 1. 任务设计原则 * **幂等性**:确保任务可以重复执行而不产生副作用 * **无状态**:任务不应依赖本地状态 * **异常处理**:妥善处理异常情况 ### 2. 性能优化 * 合理设置分片大小 * 避免长时间运行的任务 * 使用异步处理提高吞吐量 ### 3. 监控告警 * 关键任务设置失败告警 * 监控任务执行时间 * 定期检查任务执行状态 ## 常见问题 ### Q: 如何调试任务? A: 可以使用 `SnailJobLog.LOCAL` 记录本地调试日志,或通过Web控制台查看 `SnailJobLog.REMOTE` 远程日志。 ### Q: 任务执行失败如何处理? A: 1. 检查任务逻辑是否正确 2. 查看异常日志定位问题 3. 确保任务具有幂等性 4. 配置合适的重试次数 ### Q: 如何处理大数据量任务? A: 使用分片任务或MapReduce任务,将大任务分解为多个小任务并行处理。 ### Q: 工作流任务如何传递数据? A: 通过 `jobArgs.appendContext(key, value)` 向上下文添加数据,下游任务通过 `jobArgs.getWfContext(key)` 获取。 ## 相关链接 * [SnailJob官方文档](https://snailjob.opensnail.com/) --- --- url: 'https://ruoyi.plus/backend/common/doc.md' --- # SpringDoc + Apifox 接口文档配置说明 ## 概述 本模块基于SpringDoc实现了自动化的API接口文档生成功能,支持OpenAPI 3.0规范。通过自定义配置和增强处理,提供了更加灵活和强大的文档生成能力,并配合Apifox进行接口管理、测试和协作。 ## 核心组件 ### 1. SpringDocProperties (配置属性类) **文件位置**: `plus.ruoyi.common.doc.config.properties.SpringDocProperties` **功能描述**: 用于绑定application.yml中以`springdoc`为前缀的配置项,提供类型安全的配置管理。 #### 模块配置结构 ```yaml springdoc: api-docs: # 是否开启接口文档 enabled: true info: # 标题 title: '${app.title}_接口文档' # 描述 description: '接口文档包括business(主业务)、system(系统)、generator(代码生成)等模块' # 版本 version: '版本号: ${app.version}' # 作者信息 contact: name: 抓蛙师 email: 770492966@qq.com url: https://space.bilibili.com/520725002 components: # 鉴权方式配置 security-schemes: apiKey: type: APIKEY in: HEADER name: ${sa-token.token-name} # 分组配置,定义多个模块 group-configs: - group: business packages-to-scan: plus.ruoyi.business - group: system packages-to-scan: plus.ruoyi.system - group: generator packages-to-scan: plus.ruoyi.generator ``` #### 主要属性说明 | 属性 | 类型 | 说明 | |-------------------------------|---------------------|--------------------| | `api-docs.enabled` | `boolean` | 是否启用API文档生成 | | `info` | `InfoProperties` | 文档基本信息配置 | | `components.security-schemes` | `SecuritySchemes` | 安全认证方案配置(ApiKey方式) | | `group-configs` | `List` | 模块分组配置,支持多模块项目 | ### 2. SpringDocConfig (自动配置类) **文件位置**: `plus.ruoyi.common.doc.config.SpringDocConfig` **功能描述**: SpringDoc接口文档的自动配置类,负责配置OpenAPI文档生成相关的Bean。 #### 核心功能 1. **OpenAPI配置创建**: 整合自定义配置属性,生成完整的API文档配置 2. **安全认证配置**: 自动配置全局ApiKey认证要求 3. **上下文路径处理**: 为所有API路径添加应用上下文路径前缀 4. **自定义处理器注册**: 使用增强的OpenApiHandler替换默认实现 ### 3. OpenApiHandler (增强处理器) **文件位置**: `plus.ruoyi.common.doc.handler.OpenApiHandler` **功能描述**: 继承并增强SpringDoc的OpenAPIService功能,主要改进标签生成逻辑。 #### 核心增强 1. **智能标签生成**: 使用Java注释的首行作为Tag名称 2. **Javadoc集成**: 自动提取类和方法的Javadoc注释作为文档描述 3. **标签去重**: 避免重复添加相同名称的标签 4. **属性占位符解析**: 支持标签名称和描述中的属性占位符 ## API文档访问方式 ### OpenAPI JSON格式访问 基于当前配置,API文档的访问地址如下: 1. **主接口文档**: `http://localhost:5500/v3/api-docs` 2. **分模块接口文档**: * 业务模块: `http://localhost:5500/v3/api-docs/business` * 系统模块: `http://localhost:5500/v3/api-docs/system` * 代码生成模块: `http://localhost:5500/v3/api-docs/generator` **注意**: * 端口号为配置的5500 * 上下文路径为根路径 "/" * 不提供Swagger UI界面,专门配合Apifox使用 ## Apifox集成使用指南 ### 1. 什么是Apifox Apifox是一款集API文档、API管理、API测试于一身的超级多功能API工具,支持导入OpenAPI/Swagger格式数据,提供Mock功能、接口测试、团队协作等全方位的API开发支持。 ### 2. 导入SpringDoc生成的API文档到Apifox #### 方法一:URL方式导入(推荐) 1. 打开Apifox,进入目标项目 2. 依次选择【项目设置 → 导入数据 → OpenAPI/Swagger】 3. 选择"URL方式"导入 4. 输入相应的API文档地址: ``` # 导入所有模块 http://localhost:5500/v3/api-docs # 或者分模块导入 http://localhost:5500/v3/api-docs/business http://localhost:5500/v3/api-docs/system http://localhost:5500/v3/api-docs/generator ``` #### 方法二:文件导入 1. 访问API文档地址,将JSON内容保存为文件 2. 在Apifox中选择"文件导入" 3. 上传保存的JSON文件 ### 3. 设置定时同步(推荐) 为了保持API文档与代码同步,建议设置定时同步功能: 1. 在Apifox中进入【项目设置 → 导入数据 → 定时导入】 2. 添加新任务,输入数据源URL 3. 设置同步频率(建议每隔2-4小时) 4. 选择"智能合并"模式,保留在Apifox中的自定义内容 ### 4. 智能合并功能 当使用"智能合并"功能时,Apifox会保留以下内容: * 中文名称 * Mock规则 * 参数说明 * 接口返回示例 * 自定义的接口描述 ## 开发最佳实践 ### 1. 控制器注释规范 为了充分利用增强的标签生成功能,建议按以下规范编写控制器类: ```java /** * 用户管理 * * 提供用户的增删改查等基础功能,包括用户注册、登录、 * 个人信息管理等相关接口。 * * @author 开发者姓名 */ @RestController @RequestMapping("/user") public class UserController { /** * 获取用户列表 */ @Operation(summary = "获取用户列表", description = "分页获取系统中的用户信息") @GetMapping("/list") public Result> getUserList(@ParameterObject PageQuery pageQuery) { // 实现逻辑 } } ``` **注释规范说明**: * **第一行**:作为API标签名称显示在Apifox中 * **详细描述**:作为标签的详细说明 * 支持多行描述和Markdown格式 ### 2. 接口安全认证 配置了ApiKey认证方式,在需要认证的接口上添加: ```java @Operation(summary = "获取用户信息") @SecurityRequirement(name = "apiKey") @GetMapping("/profile") public Result getUserProfile() { // 实现逻辑 } ``` ### 3. 分组模块开发 项目采用分组配置,不同模块的控制器应放在对应的包下: ``` plus.ruoyi.business -> business模块 plus.ruoyi.system -> system模块 plus.ruoyi.generator -> generator模块 ``` ### 4. 参数文档优化 使用SpringDoc注解提供更详细的参数说明: ```java @Operation(summary = "创建用户", description = "创建一个新的用户账户") @ApiResponses({ @ApiResponse(responseCode = "200", description = "创建成功"), @ApiResponse(responseCode = "400", description = "参数错误") }) @PostMapping("/create") public Result createUser( @Parameter(description = "用户创建请求", required = true) @RequestBody @Valid CreateUserRequest request) { // 实现逻辑 } ``` ## 团队协作工作流 ### 1. 开发阶段 1. **后端开发**: 编写Controller,添加合适的注释和注解 2. **文档生成**: 启动应用,SpringDoc自动生成OpenAPI文档 3. **导入Apifox**: 将生成的文档导入到Apifox项目中 ### 2. 文档完善阶段 1. **添加中文描述**: 在Apifox中为接口和参数添加中文名称 2. **配置Mock规则**: 设置合理的Mock数据规则 3. **添加示例**: 提供请求和响应的示例数据 ### 3. 测试阶段 1. **接口测试**: 使用Apifox进行接口调试和测试 2. **自动化测试**: 创建测试用例和测试套件 3. **环境管理**: 配置开发、测试、生产等多环境 ### 4. 文档维护 1. **定时同步**: 利用Apifox的定时导入功能保持文档同步 2. **智能合并**: 确保自定义内容不被覆盖 3. **版本管理**: 跟踪API变更历史 ## 高级配置 ### 1. 环境相关配置 ```yaml # 开发环境 spring: profiles: active: dev springdoc: api-docs: enabled: true # 开发环境启用 --- # 生产环境 spring: profiles: prod springdoc: api-docs: enabled: false # 生产环境关闭 ``` ### 2. 自定义过滤配置 ```yaml springdoc: paths-to-match: - /api/** - /admin/** paths-to-exclude: - /api/internal/** packages-to-exclude: - plus.ruoyi.common.web ``` ### 3. OpenAPI信息定制 ```yaml springdoc: info: title: '${app.name}_API文档' description: | ## 项目说明 本项目提供了完整的企业级后台管理系统API ## 模块说明 - **business**: 核心业务模块 - **system**: 系统管理模块 - **generator**: 代码生成模块 version: 'v${app.version}' terms-of-service: https://your-domain.com/terms license: name: MIT License url: https://opensource.org/licenses/MIT ``` ## 常见问题解决 ### 1. 文档无法访问 **问题**: 访问API文档地址返回404 **解决方案**: 1. 检查 `springdoc.api-docs.enabled` 是否为true 2. 确认应用已正常启动 3. 检查端口号和上下文路径配置 ### 2. Apifox导入失败 **问题**: Apifox提示解析错误 **解决方案**: 1. 访问API文档地址,检查JSON格式是否正确 2. 将JSON内容上传到 https://editor.swagger.io/ 验证OpenAPI规范 3. 检查SpringDoc配置是否有语法错误 ### 3. 分组模块显示异常 **问题**: 某些模块的接口没有出现在对应分组中 **解决方案**: 1. 检查Controller类的包路径是否匹配group-configs配置 2. 确认Controller类上有 `@RestController` 或 `@Controller` 注解 3. 检查方法上是否有正确的HTTP映射注解 ### 4. 认证配置不生效 **问题**: 接口认证信息没有正确显示 **解决方案**: 1. 确认 `sa-token.token-name` 配置正确 2. 检查Controller方法上是否添加了 `@SecurityRequirement` 注解 3. 验证security-schemes配置格式 ## 性能和安全考虑 ### 1. 生产环境配置 ```yaml # 生产环境建议配置 springdoc: api-docs: enabled: false # 关闭文档生成 swagger-ui: enabled: false # 确保Swagger UI关闭 ``` ### 2. 网络安全 如果需要在生产环境提供文档: 1. 配置访问白名单 2. 添加基础认证 3. 使用HTTPS协议 4. 定期更新访问凭据 ### 3. 性能优化 1. 合理设置分组,避免单个文档过大 2. 生产环境关闭文档生成 3. 使用缓存减少文档生成开销 ## 总结 SpringDoc + Apifox方案提供了企业级的API文档解决方案,具有以下优势: ### 技术优势 * **自动化程度高**: 基于注释和注解自动生成文档 * **模块化支持**: 支持多模块项目的分组管理 * **智能标签**: 利用Javadoc注释生成有意义的标签名称 * **标准兼容**: 完全符合OpenAPI 3.0规范 ### 协作优势 * **团队协作**: Apifox提供了完整的团队协作功能 * **智能合并**: 保护团队在Apifox中的自定义内容 * **自动同步**: 定时同步机制确保文档实时更新 * **多功能集成**: 文档、测试、Mock一体化 ### 维护优势 * **维护成本低**: 文档与代码同步更新 * **版本控制**: 支持API版本变更追踪 * **质量保证**: 自动化测试保证接口质量 通过合理配置和规范使用,这套方案可以极大提升API文档的质量和开发效率,为团队协作提供强有力的支持。 --- --- url: 'https://ruoyi.plus/backend/common/sse.md' --- # SSE推送 (sse) ## 概述 SSE模块是基于Spring Boot的服务器推送事件模块,提供实时消息推送功能。支持单机和集群环境下的消息分发,通过Redis实现跨节点的消息同步。 ## 核心特性 * **实时通信**:基于SSE协议的长连接通信 * **多连接支持**:支持单用户多设备/浏览器同时连接 * **集群支持**:通过Redis发布订阅实现集群环境下的消息分发 * **灵活配置**:可通过配置文件控制功能开启/关闭 * **自动清理**:连接异常时自动清理资源 ## 技术架构 ### 依赖关系 ```xml plus.ruoyi ruoyi-common-core plus.ruoyi ruoyi-common-redis plus.ruoyi ruoyi-common-satoken plus.ruoyi ruoyi-common-json ``` ### 核心组件 ```text ├── config/ │ ├── SseAutoConfiguration.java # 自动配置类 │ └── SseProperties.java # 配置属性类 ├── controller/ │ └── SseController.java # REST控制器 ├── core/ │ └── SseEmitterManager.java # 连接管理器 ├── dto/ │ └── SseMessageDto.java # 消息传输对象 ├── listener/ │ └── SseTopicListener.java # 主题监听器 └── utils/ └── SseMessageUtils.java # 消息工具类 ``` ## 配置说明 ### 必需配置 在 `application.yml` 中添加以下配置: ```yaml # SSE配置 sse: enabled: true # 启用SSE功能 path: /sse # SSE服务访问路径 # Redis配置(必需) spring: redis: host: localhost port: 6379 database: 0 ``` ### 配置参数说明 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `sse.enabled` | Boolean | false | 是否启用SSE功能 | | `sse.path` | String | - | SSE服务的访问路径 | ## 核心API ### 建立连接 **接口地址:** `GET ${sse.path}` **请求头:** ``` Accept: text/event-stream Cache-Control: no-cache ``` **响应格式:** ``` Content-Type: text/event-stream ``` ### 关闭连接 **接口地址:** `GET ${sse.path}/close` ### 发送消息 **向指定用户发送消息:** ``` GET ${sse.path}/send?userId=123&msg=Hello ``` **向所有用户广播消息:** ``` GET ${sse.path}/sendAll?msg=Broadcast Message ``` ## 使用指南 ### 1. 客户端连接 #### JavaScript示例 ```javascript // 建立SSE连接 const eventSource = new EventSource('/sse'); // 监听消息 eventSource.addEventListener('message', function(event) { console.log('收到消息:', event.data); }); // 监听连接状态 eventSource.addEventListener('open', function(event) { console.log('连接已建立'); }); eventSource.addEventListener('error', function(event) { console.log('连接错误:', event); }); // 关闭连接 function closeConnection() { eventSource.close(); // 可选:调用服务端关闭接口 fetch('/sse/close'); } ``` #### jQuery示例 ```javascript $(document).ready(function() { var eventSource = new EventSource('/sse'); eventSource.onmessage = function(event) { $('#messages').append('
' + event.data + '
'); }; eventSource.onerror = function(event) { console.error('SSE连接错误', event); }; }); ``` ### 2. 服务端发送消息 #### 使用工具类(推荐) ```java // 向指定用户发送消息 SseMessageUtils.sendMessage(userId, "Hello User"); // 向所有用户广播消息 SseMessageUtils.sendMessage("广播消息"); // 通过Redis发布消息(集群环境) SseMessageDto message = SseMessageDto.of(Arrays.asList(123L, 456L), "集群消息"); SseMessageUtils.publishMessage(message); // 广播消息到所有节点 SseMessageUtils.publishAll("全局广播"); ``` #### 直接使用管理器 ```java @Autowired private SseEmitterManager sseEmitterManager; public void sendNotification(Long userId, String content) { // 本地发送 sseEmitterManager.sendMessage(userId, content); // 集群发送 SseMessageDto dto = new SseMessageDto(); dto.setUserIds(Arrays.asList(userId)); dto.setMessage(content); sseEmitterManager.publishMessage(dto); } ``` ### 3. 业务集成示例 #### 订单状态推送 ```java @Service public class OrderService { public void updateOrderStatus(Long orderId, String status) { // 业务逻辑... Order order = updateOrder(orderId, status); // 推送状态更新 String message = String.format("订单 %s 状态更新为:%s", orderId, status); SseMessageUtils.sendMessage(order.getUserId(), message); } } ``` #### 系统通知推送 ```java @Service public class NotificationService { public void sendSystemNotice(String notice) { // 向所有在线用户推送系统通知 SseMessageUtils.publishAll("系统通知:" + notice); } public void sendPersonalMessage(Long userId, String message) { // 向特定用户推送个人消息 SseMessageDto dto = SseMessageDto.of(Arrays.asList(userId), message); SseMessageUtils.publishMessage(dto); } } ``` ## 高级特性 ### 1. 消息格式化 ```java // 发送JSON格式消息 @Service public class MessageService { public void sendJsonMessage(Long userId, Object data) { try { String jsonMessage = JsonUtils.toJsonString(data); SseMessageUtils.sendMessage(userId, jsonMessage); } catch (Exception e) { log.error("发送JSON消息失败", e); } } } ``` ### 2. 连接状态监控 ```java // 可以扩展SseEmitterManager来添加连接统计功能 @Component public class SseMonitor { @Autowired private SseEmitterManager sseEmitterManager; @EventListener public void handleConnect(SseConnectEvent event) { log.info("用户 {} 建立SSE连接", event.getUserId()); } @EventListener public void handleDisconnect(SseDisconnectEvent event) { log.info("用户 {} 断开SSE连接", event.getUserId()); } } ``` ### 3. 消息持久化 ```java // 对重要消息进行持久化处理 @Service public class PersistentMessageService { public void sendImportantMessage(Long userId, String message) { // 先保存到数据库 saveMessageToDb(userId, message); // 再推送给用户 SseMessageUtils.sendMessage(userId, message); } } ``` ## 错误处理 ### 常见错误及解决方案 #### 1. 连接建立失败 **原因:** 用户未登录或Token无效 **解决:** 确保客户端请求时携带有效的认证信息 ```javascript // 在请求头中添加认证信息 const eventSource = new EventSource('/sse', { headers: { 'Authorization': 'Bearer ' + token } }); ``` #### 2. 消息发送失败 **原因:** 用户连接已断开或网络异常 **解决:** 系统会自动清理无效连接,重要消息建议配合持久化机制 ## 性能优化 ### 1. 连接管理优化 * **连接数限制**:建议为每个用户设置最大连接数限制 * **心跳检测**:定期发送心跳消息检测连接有效性 * **资源清理**:及时清理无效连接释放内存 ### 2. 消息发送优化 * **批量发送**:对于大量用户的广播消息,考虑分批发送 * **消息压缩**:对于大型消息内容,考虑使用压缩算法 * **异步处理**:消息发送采用异步方式避免阻塞主线程 ### 关键日志说明 * **连接建立**:`SSE连接建立 userId={}, token={}` * **消息发送**:`SSE发送消息 userId={}, message={}` * **连接断开**:`SSE连接断开 userId={}, token={}` * **Redis消息**:`SSE主题订阅收到消息 userIds={}, message={}` ## 最佳实践 ### 1. 客户端最佳实践 * **重连机制**:实现自动重连逻辑处理网络中断 * **消息去重**:对重复消息进行客户端去重处理 * **错误处理**:优雅处理连接错误和消息解析错误 ```javascript function createSSEConnection() { const eventSource = new EventSource('/sse'); let reconnectTimer; eventSource.onmessage = function(event) { try { const data = JSON.parse(event.data); handleMessage(data); } catch (e) { console.error('消息解析错误:', e); } }; eventSource.onerror = function(event) { console.warn('SSE连接错误,3秒后重连...'); eventSource.close(); clearTimeout(reconnectTimer); reconnectTimer = setTimeout(() => { createSSEConnection(); }, 3000); }; } ``` --- --- url: 'https://ruoyi.plus/frontend/components/svg-icon.md' --- # SvgIcon组件 --- --- url: 'https://ruoyi.plus/frontend/utils/to.md' --- # To工具类 --- --- url: 'https://ruoyi.plus/frontend/architecture/typescript-config.md' --- # TypeScript配置 --- --- url: 'https://ruoyi.plus/mobile/uniapp/overview.md' --- # UniApp概览 --- --- url: 'https://ruoyi.plus/frontend/styles/unocss-config.md' --- # UnoCSS配置 --- --- url: 'https://ruoyi.plus/mobile/styles/unocss.md' --- # UnoCSS配置 --- --- url: 'https://ruoyi.plus/frontend/composables/overview.md' --- --- --- url: 'https://ruoyi.plus/frontend/dev/dev-config.md' --- --- --- url: 'https://ruoyi.plus/frontend/i18n/i18n-config.md' --- --- --- url: 'https://ruoyi.plus/frontend/stores/pinia-usage.md' --- --- --- url: 'https://ruoyi.plus/frontend/utils/utils-overview.md' --- --- --- url: 'https://ruoyi.plus/frontend/views/page-dev-guide.md' --- --- --- url: 'https://ruoyi.plus/mobile/composables/overview.md' --- --- --- url: 'https://ruoyi.plus/mobile/debug/tools.md' --- --- --- url: 'https://ruoyi.plus/mobile/pages/development-guide.md' --- --- --- url: 'https://ruoyi.plus/mobile/utils/overview.md' --- --- --- url: 'https://ruoyi.plus/practices/1panel-docker-deploy.md' --- --- --- url: 'https://ruoyi.plus/practices/coding-standards.md' --- --- --- url: 'https://ruoyi.plus/frontend/composables/use-auth.md' --- # useAuth --- --- url: 'https://ruoyi.plus/mobile/composables/use-auth.md' --- # useAuth --- --- url: 'https://ruoyi.plus/frontend/composables/use-dialog.md' --- # useDialog --- --- url: 'https://ruoyi.plus/frontend/composables/use-dict.md' --- # useDict --- --- url: 'https://ruoyi.plus/mobile/composables/use-dict.md' --- # useDict --- --- url: 'https://ruoyi.plus/frontend/composables/use-download.md' --- # useDownload --- --- url: 'https://ruoyi.plus/mobile/composables/use-http.md' --- # useHttp --- --- url: 'https://ruoyi.plus/frontend/composables/use-i18n.md' --- # useI18n --- --- url: 'https://ruoyi.plus/mobile/composables/use-modal.md' --- # useModal --- --- url: 'https://ruoyi.plus/mobile/composables/use-payment.md' --- # usePayment --- --- url: 'https://ruoyi.plus/frontend/composables/use-request.md' --- # useRequest --- --- url: 'https://ruoyi.plus/mobile/composables/use-scroll.md' --- # useScroll --- --- url: 'https://ruoyi.plus/frontend/composables/use-selection.md' --- # useSelection --- --- url: 'https://ruoyi.plus/frontend/composables/use-sse.md' --- # useSSE --- --- url: 'https://ruoyi.plus/frontend/composables/use-table-height.md' --- # useTableHeight --- --- url: 'https://ruoyi.plus/frontend/composables/use-theme.md' --- # useTheme --- --- url: 'https://ruoyi.plus/mobile/composables/use-theme.md' --- # useTheme --- --- url: 'https://ruoyi.plus/frontend/composables/use-title.md' --- # useTitle --- --- url: 'https://ruoyi.plus/mobile/composables/use-toast.md' --- # useToast --- --- url: 'https://ruoyi.plus/frontend/composables/use-token.md' --- # useToken --- --- url: 'https://ruoyi.plus/mobile/composables/use-token.md' --- # useToken --- --- url: 'https://ruoyi.plus/frontend/composables/use-websocket.md' --- # useWebSocket --- --- url: 'https://ruoyi.plus/frontend/dev/vite-config.md' --- # Vite配置优化 --- --- url: 'https://ruoyi.plus/backend/common/web.md' --- # Web组件 (web) ## 概述 Web模块(`ruoyi-common-web`)是系统的Web应用基础模块,提供Web应用的核心功能和MVC支持,包括验证码生成、过滤器配置、国际化支持、异常处理、拦截器等功能。 ## 模块依赖 ### 内部模块 * `ruoyi-common-json` - 提供数据序列化支持 * `ruoyi-common-redis` - 提供缓存与会话支持 ### 外部依赖 * **Spring Boot Web** - Web应用基础支持(排除Tomcat) * **Undertow** - 高性能Web服务器 * **Spring Boot Actuator** - 应用监控与管理 * **HuTool验证码** - 图形验证码生成 * **HuTool加密工具** - 加密解密功能 ## 核心功能 ### 1. 验证码配置 (CaptchaConfig) 提供多种类型的验证码生成器配置: #### 支持的验证码类型 * **圆圈干扰验证码** (`CircleCaptcha`) - 带有圆圈干扰线 * **线段干扰验证码** (`LineCaptcha`) - 带有线段干扰线 * **扭曲干扰验证码** (`ShearCaptcha`) - 扭曲效果,增加识别难度 #### 统一配置参数 ```java // 验证码图片尺寸 private static final int WIDTH = 160; private static final int HEIGHT = 60; // 样式配置 private static final Color BACKGROUND = Color.WHITE; private static final Font FONT = new Font("Arial", Font.BOLD, 48); ``` #### 验证码枚举定义 **验证码类别** (`CaptchaCategory`) * `LINE` - 线段干扰验证码 * `CIRCLE` - 圆圈干扰验证码 * `SHEAR` - 扭曲干扰验证码 **验证码类型** (`CaptchaType`) * `MATH` - 数学运算验证码 (如: 3+2=?) * `CHAR` - 随机字符验证码 (如: ABC123) ### 2. 过滤器配置 (FilterConfig) #### XSS过滤器 * **启用条件**: `xss.enabled=true` * **功能**: 防范XSS攻击,过滤恶意脚本 * **执行优先级**: 最高优先级+1 * **URL映射**: `/*` #### 可重复读取请求体过滤器 * **功能**: 允许多次读取HTTP请求体内容 * **应用场景**: 解决流只能读取一次的问题 * **执行优先级**: 最低优先级 * **URL映射**: `/*` ### 3. 国际化配置 (I18nConfig) #### 语言环境解析器 (`I18nLocaleResolver`) * **解析来源**: HTTP请求头的`content-language`字段 * **支持格式**: 语言\_国家 (如: zh\_CN, en\_US) * **默认行为**: 格式错误时返回系统默认语言环境 * **执行时机**: 在WebMvcAutoConfiguration之前 ### 4. Web通用配置 (ResourcesConfig) #### 功能特性 * **拦截器管理**: 注册全局访问性能监控拦截器 * **静态资源处理**: 配置文件上传路径访问 (`/resources/**`) * **跨域支持**: 允许所有来源的跨域请求 * **全局异常处理**: 统一异常处理机制 #### 跨域配置详情 ```java // 跨域配置参数 config.setAllowCredentials(true); // 允许携带凭证 config.addAllowedOriginPattern("*"); // 允许所有源 config.addAllowedHeader("*"); // 允许所有请求头 config.addAllowedMethod("*"); // 允许所有HTTP方法 config.setMaxAge(1800L); // 缓存时间30分钟 ``` ### 5. Undertow服务器配置 (UndertowConfig) #### 主要配置 * **WebSocket支持**: 配置WebSocket缓冲区池 * **虚拟线程支持**: 启用虚拟线程时使用虚拟线程池 * **安全防护**: 禁用不安全的HTTP方法 (CONNECT, TRACE, TRACK) #### 虚拟线程配置 ```java // 虚拟线程配置 VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("undertow-"); deploymentInfo.setExecutor(executor); deploymentInfo.setAsyncExecutor(executor); ``` ### 6. 过滤器实现 #### XSS过滤器 (`XssFilter`) * **过滤范围**: 排除GET和DELETE请求 * **排除配置**: 支持配置不需要过滤的URL列表 * **处理方式**: 使用`XssHttpServletRequestWrapper`包装请求 #### XSS请求包装器 (`XssHttpServletRequestWrapper`) * **参数过滤**: 清理请求参数中的HTML标签 * **JSON过滤**: 对JSON请求体进行XSS过滤 * **方法支持**: `getParameter()`, `getParameterMap()`, `getParameterValues()` #### 可重复读取包装器 (`RepeatedlyRequestWrapper`) * **适用类型**: Content-Type为`application/json`的请求 * **实现原理**: 缓存请求体内容,允许多次读取 * **字符编码**: 统一设置为UTF-8 ### 7. 全局异常处理 (GlobalExceptionHandler) #### 支持的异常类型 **HTTP相关异常** * `HttpRequestMethodNotSupportedException` - 不支持的HTTP方法 * `NoHandlerFoundException` - 404异常 * `MissingPathVariableException` - 路径变量缺失 **业务异常** * `ServiceException` - 业务逻辑异常 * `SseException` - SSE认证失败异常 * `BaseException` - 基础业务异常 **参数验证异常** * `BindException` - 数据绑定异常 * `ConstraintViolationException` - 约束违反异常 * `MethodArgumentNotValidException` - 方法参数验证异常 * `MethodArgumentTypeMismatchException` - 参数类型不匹配 **JSON处理异常** * `JsonParseException` - JSON解析异常 * `HttpMessageNotReadableException` - HTTP消息读取异常 **系统异常** * `ServletException` - Servlet异常 * `IOException` - IO异常(特别处理SSE连接中断) * `RuntimeException` - 运行时异常 * `Exception` - 兜底异常处理器 ### 8. 性能监控拦截器 (PlusWebInvokeTimeInterceptor) #### 功能特性 * **请求耗时统计**: 记录每个请求的执行时间 * **参数日志**: 记录请求参数(支持JSON和表单参数) * **线程安全**: 使用ThreadLocal存储计时器 * **内存安全**: 请求完成后自动清理ThreadLocal #### 日志格式 ``` [PLUS]开始请求 => URL[GET /api/user],参数类型[json],参数:[{"id":1}] [PLUS]结束请求 => URL[GET /api/user],耗时:[120]毫秒 ``` ### 9. 数学运算验证码生成器 (UnsignedMathGenerator) #### 功能特性 * **运算类型**: 支持加、减、乘运算 (`+-*`) * **数字范围**: 可配置参与计算的数字位数(默认2位) * **结果保证**: 确保运算结果为非负数 * **格式整齐**: 右对齐填充空格保持格式 #### 生成示例 ``` 12 + 34 = (用户需输入: 46) 25 * 3 = (用户需输入: 75) ``` ## 自动配置 模块通过Spring Boot的自动配置机制自动装载,配置文件位于: `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` ``` plus.ruoyi.common.web.config.CaptchaConfig plus.ruoyi.common.web.config.FilterConfig plus.ruoyi.common.web.config.I18nConfig plus.ruoyi.common.web.config.ResourcesConfig plus.ruoyi.common.web.config.UndertowConfig ``` ## 使用指南 ### 1. 启用XSS防护 ```yaml xss: enabled: true exclude-urls: - /system/notice/* ``` ### 2. 配置文件上传路径 ```yaml app: upload-path: /opt/uploads ``` ### 3. 国际化使用 ```http GET /api/user Content-Language: zh_CN ``` ### 4. 验证码集成 ```java @Autowired private CircleCaptcha circleCaptcha; // 生成验证码 circleCaptcha.createCode(); String code = circleCaptcha.getCode(); ``` ## 安全特性 1. **XSS防护**: 自动过滤HTML标签,防止脚本注入 2. **HTTP方法限制**: 禁用不安全的HTTP方法 3. **跨域配置**: 支持可控的跨域访问 4. **验证码保护**: 多种验证码类型防止机器攻击 5. **异常信息安全**: 统一异常处理,避免敏感信息泄露 ## 性能优化 1. **Undertow容器**: 使用高性能的Undertow替代Tomcat 2. **虚拟线程支持**: 在支持虚拟线程的环境下自动启用 3. **请求体缓存**: 合理缓存请求体,避免重复读取 4. **懒加载**: 验证码生成器使用`@Lazy`注解延迟初始化 ## 监控与日志 1. **请求耗时统计**: 自动记录所有请求的执行时间 2. **参数日志**: 详细记录请求参数便于调试 3. **异常日志**: 完整的异常信息记录 4. **Actuator集成**: 支持Spring Boot Actuator监控 ## 扩展指南 ### 自定义验证码生成器 ```java @Component public class CustomCaptchaGenerator implements CodeGenerator { @Override public String generate() { // 自定义验证码生成逻辑 return "custom"; } @Override public boolean verify(String code, String userInputCode) { // 自定义验证逻辑 return code.equals(userInputCode); } } ``` ### 自定义异常处理 ```java @ExceptionHandler(CustomException.class) public R handleCustomException(CustomException e) { log.error("自定义异常: {}", e.getMessage()); return R.fail(e.getMessage()); } ``` ### 自定义拦截器 ```java @Component public class CustomInterceptor implements HandlerInterceptor { // 实现自定义拦截逻辑 } ``` ## 注意事项 1. **XSS过滤器**: 仅对POST、PUT、PATCH等修改请求生效 2. **验证码**: 所有验证码实例都是懒加载,首次使用时才创建 3. **SSE连接**: IO异常处理中特别处理了SSE连接中断情况 4. **虚拟线程**: 需要JDK 21+且开启虚拟线程特性 5. **内存管理**: 拦截器使用ThreadLocal,注意及时清理避免内存泄漏 --- --- url: 'https://ruoyi.plus/mobile/components/wd.md' --- # Wot Design Uni组件库 --- --- url: 'https://ruoyi.plus/frontend/utils/download.md' --- # 下载工具 --- --- url: 'https://ruoyi.plus/mobile/api/business.md' --- # 业务接口 --- --- url: 'https://ruoyi.plus/backend/modules/business.md' --- # 业务模块 (business) --- --- url: 'https://ruoyi.plus/frontend/components/business-components.md' --- # 业务组件 --- --- url: 'https://ruoyi.plus/mobile/components/business.md' --- # 业务组件 --- --- url: 'https://ruoyi.plus/mobile/pages/business.md' --- # 业务页面 --- --- url: 'https://ruoyi.plus/frontend/layout/app-main.md' --- # 主内容区 (AppMain) --- --- url: 'https://ruoyi.plus/frontend/layout/main-layout.md' --- # 主布局 (Layout) --- --- url: 'https://ruoyi.plus/mobile/styles/theme.md' --- # 主题定制 --- --- url: 'https://ruoyi.plus/frontend/stores/theme-store.md' --- # 主题状态 (theme) --- --- url: 'https://ruoyi.plus/frontend/styles/theme-system.md' --- # 主题系统 --- --- url: 'https://ruoyi.plus/backend/modules/generator.md' --- # 代码生成 (generator) --- --- url: 'https://ruoyi.plus/frontend/dev/code-quality.md' --- # 代码质量工具 --- --- url: 'https://ruoyi.plus/backend/extend/snailjob-server.md' --- # 任务服务 (snailjob-server) --- --- url: 'https://ruoyi.plus/mobile/utils/location.md' --- # 位置服务工具 --- --- url: 'https://ruoyi.plus/frontend/layout/sidebar.md' --- # 侧边栏 (Sidebar) --- --- url: 'https://ruoyi.plus/frontend/styles/global-styles.md' --- # 全局样式 --- --- url: 'https://ruoyi.plus/mobile/styles/global.md' --- # 全局样式 --- --- url: 'https://ruoyi.plus/frontend/types/global-types.md' --- # 全局类型 --- --- url: 'https://ruoyi.plus/backend/common/mp.md' --- # 公众号集成 (mp) ## 模块介绍 `ruoyi-common-mp` 是基于 [WxJava](https://github.com/Wechat-Group/WxJava) 开发的微信公众号集成模块,提供了微信公众号的基础配置管理、缓存优化和自动初始化功能。 ## 主要特性 * ✅ **多公众号支持**: 支持同时管理多个微信公众号 * ✅ **Redis缓存**: 集成Redis实现access\_token等信息的缓存 * ✅ **本地缓存**: 基于Caffeine实现二级缓存,提升性能 * ✅ **自动配置**: Spring Boot自动配置,开箱即用 * ✅ **动态管理**: 支持运行时动态添加/移除公众号配置 ## 依赖说明 ```xml plus.ruoyi ruoyi-common-core plus.ruoyi ruoyi-common-redis com.github.binarywang weixin-java-mp ``` ## 核心组件 ### 1. PlusWxRedisOps 自定义Redis操作实现类,提供二级缓存功能: ```java public class PlusWxRedisOps extends BaseWxRedisOps { // Caffeine本地缓存配置 private static final Cache CAFFEINE = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) // 写入后5秒过期 .initialCapacity(100) // 初始容量 .maximumSize(1000) // 最大容量 .build(); } ``` **缓存策略**: * 一级缓存:Caffeine (本地缓存,5秒过期) * 二级缓存:Redis (分布式缓存) ### 2. WxMpServiceConfiguration Spring Boot自动配置类: ```java @RequiredArgsConstructor @AutoConfiguration public class WxMpServiceConfiguration { @Bean public WxMpService wxMpService() { WxMpService wxMpService = new WxMpServiceImpl(); wxMpService.setMaxRetryTimes(3); // 设置重试次数 return wxMpService; } @Bean public WxRedisOps wxRedisOps() { return new PlusWxRedisOps(); } } ``` ### 3. WxMpApplicationRunner 应用启动时自动初始化公众号配置: ```java @Slf4j @RequiredArgsConstructor public class WxMpApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { init(); // 启动时自动初始化 } } ``` ## 使用指南 ### 1. 模块引入 在需要使用公众号功能的模块中引入依赖: ```xml plus.ruoyi ruoyi-common-mp ``` ### 2. 数据库配置 确保平台配置表中有微信公众号的相关配置: ```sql -- 平台配置表示例 INSERT INTO platform_config ( name, type, appid, secret, token, aeskey, status ) VALUES ( '测试公众号', 'MP_OFFICIAL_ACCOUNT', 'wx1234567890', 'your_secret', 'your_token', 'your_aes_key', 1 ); ``` ### 3. 基本使用 #### 注入服务 ```java @RestController @RequiredArgsConstructor public class WxMpController { private final WxMpService wxMpService; // 使用示例 } ``` #### 发送模板消息 ```java @PostMapping("/sendTemplate") public R sendTemplateMessage(@RequestBody TemplateMessageRequest request) { try { // 切换到指定公众号 wxMpService.switchoverTo(request.getAppId()); // 构建模板消息 WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder() .toUser(request.getOpenId()) .templateId(request.getTemplateId()) .data(request.getData()) .build(); // 发送模板消息 String msgId = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage); return R.ok("发送成功,消息ID: " + msgId); } catch (WxErrorException e) { log.error("发送模板消息失败", e); return R.fail("发送失败: " + e.getError().getErrorMsg()); } } ``` #### 获取用户信息 ```java @GetMapping("/userInfo/{openId}") public R getUserInfo(@PathVariable String openId, @RequestParam String appId) { try { wxMpService.switchoverTo(appId); WxMpUser user = wxMpService.getUserService().userInfo(openId); return R.ok(user); } catch (WxErrorException e) { log.error("获取用户信息失败", e); return R.fail("获取失败: " + e.getError().getErrorMsg()); } } ``` #### 创建菜单 ```java @PostMapping("/createMenu") public R createMenu(@RequestParam String appId, @RequestBody WxMpMenu menu) { try { wxMpService.switchoverTo(appId); wxMpService.getMenuService().menuCreate(menu); return R.ok("菜单创建成功"); } catch (WxErrorException e) { log.error("创建菜单失败", e); return R.fail("创建失败: " + e.getError().getErrorMsg()); } } ``` ### 4. 动态配置管理 #### 添加新公众号配置 ```java @Autowired private WxMpApplicationRunner wxMpApplicationRunner; // 添加新配置 public void addWxMpConfig(PlatformDTO platform) { wxMpApplicationRunner.addConfig(platform); } ``` #### 移除公众号配置 ```java // 移除配置 public void removeWxMpConfig(String appId) { wxMpApplicationRunner.removeConfig(appId); } ``` ## 消息处理 ### 1. 消息处理器示例 ```java @Component public class WxMpMessageHandler { /** * 处理文本消息 */ public WxMpXmlOutMessage handleTextMessage(WxMpXmlMessage inMessage) { return WxMpXmlOutMessage.TEXT() .content("收到文本消息:" + inMessage.getContent()) .fromUser(inMessage.getToUser()) .toUser(inMessage.getFromUser()) .build(); } /** * 处理关注事件 */ public WxMpXmlOutMessage handleSubscribeEvent(WxMpXmlMessage inMessage) { return WxMpXmlOutMessage.TEXT() .content("欢迎关注!") .fromUser(inMessage.getToUser()) .toUser(inMessage.getFromUser()) .build(); } } ``` ### 2. 消息路由配置 ```java @Configuration public class WxMpMessageRouterConfig { @Bean public WxMpMessageRouter wxMpMessageRouter( WxMpService wxMpService, WxMpMessageHandler messageHandler ) { WxMpMessageRouter router = new WxMpMessageRouter(wxMpService); // 文本消息路由 router.rule() .async(false) .msgType(XmlMsgType.TEXT) .handler(messageHandler::handleTextMessage) .end(); // 关注事件路由 router.rule() .async(false) .msgType(XmlMsgType.EVENT) .event(EventType.SUBSCRIBE) .handler(messageHandler::handleSubscribeEvent) .end(); return router; } } ``` ## 配置参数 ### 缓存配置 可以通过修改 `PlusWxRedisOps` 来调整缓存策略: ```java private static final Cache CAFFEINE = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) // 过期时间 .initialCapacity(100) // 初始容量 .maximumSize(1000) // 最大容量 .recordStats() // 开启统计 .build(); ``` ### 重试配置 调整WxMpService的重试次数: ```java @Bean public WxMpService wxMpService() { WxMpService wxMpService = new WxMpServiceImpl(); wxMpService.setMaxRetryTimes(3); // 设置重试次数 wxMpService.setRetrySleepMillis(1000); // 重试间隔 return wxMpService; } ``` ## 常见问题 ### Q1: 如何处理多公众号切换? ```java // 在使用API前切换到对应的公众号 wxMpService.switchoverTo(appId); ``` ### Q2: access\_token缓存机制? 系统采用两级缓存: 1. Caffeine本地缓存(5秒) 2. Redis分布式缓存(按微信官方时间) ### Q3: 如何自定义消息处理逻辑? 实现对应的Handler接口并注册到消息路由器中: ```java public class CustomMessageHandler implements WxMpMessageHandler { @Override public WxMpXmlOutMessage handle(WxMpXmlMessage inMessage, Map context, WxMpService wxMpService, WxSessionManager sessionManager) { // 自定义处理逻辑 return null; } } ``` ### Q4: 如何监控公众号API调用? 可以通过拦截器监控API调用: ```java @Bean public WxMpService wxMpService() { WxMpService service = new WxMpServiceImpl(); service.setHttpProxyHost("your-proxy-host"); service.setHttpProxyPort(8080); return service; } ``` ## 注意事项 1. **线程安全**: WxMpService是线程安全的,可以在多线程环境下使用 2. **配置切换**: 使用多公众号时,记得调用`switchoverTo(appId)`切换配置 3. **异常处理**: 所有微信API调用都应该进行适当的异常处理 4. **缓存预热**: 系统启动时会自动加载所有配置的公众号 5. **日志监控**: 建议监控微信API的调用频率,避免超过限制 ## 扩展开发 ### 自定义缓存实现 ```java @Component public class CustomWxRedisOps extends BaseWxRedisOps { // 自定义实现 } ``` ### 自定义配置加载 ```java @Component public class CustomWxMpApplicationRunner extends WxMpApplicationRunner { // 自定义配置加载逻辑 } ``` 通过以上配置和使用方法,您可以快速集成微信公众号功能到您的应用中。 --- --- url: 'https://ruoyi.plus/mobile/debug/compatibility-testing.md' --- # 兼容性测试 --- --- url: 'https://ruoyi.plus/practices.md' --- # 最佳实践 欢迎来到 ruoyi-plus-uniapp 最佳实践指南!这里汇集了项目开发、部署、运维等各个环节的最佳实践经验。 ## 📋 llms 文档提供了llms大模型文档提示生成(目前直接打开乱码,保存下来就显示正常了) * [llms.txt](https://ruoyi.plus/llms.txt) * [llms-full.txt](https://ruoyi.plus/llms-full.txt) ## 📋 开发规范 规范的开发流程是项目成功的基础,包含编码规范、命名约定、代码审查等重要内容。 * [编码规范](/practices/coding-standards) - 统一的代码编写规范 * [命名规范](/practices/naming-conventions) - 项目中的命名约定 * [注释规范](/practices/comment-standards) - 代码注释的标准格式 * [Git使用规范](/practices/git-standards) - Git提交和分支管理规范 ## 🏗️ 架构设计 良好的架构设计是系统稳定性和可扩展性的保障。 * [系统架构设计](/practices/system-architecture) - 整体系统架构设计原则 * [数据库设计](/practices/database-design) - 数据库设计规范和优化 * [缓存策略](/practices/cache-strategy) - 缓存使用策略和最佳实践 * [分布式设计](/practices/distributed-design) - 分布式系统设计要点 ## ⚡ 性能优化 性能优化是提升用户体验的关键环节。 * [后端性能优化](/practices/backend-performance) - 后端服务性能优化指南 * [前端性能优化](/practices/frontend-performance) - 前端页面性能优化技巧 * [移动端性能优化](/practices/mobile-performance) - 移动端应用性能优化 * [数据库优化](/practices/database-optimization) - 数据库查询和存储优化 * [网络优化](/practices/network-optimization) - 网络传输和CDN优化 ## 🔒 安全指南 安全是系统运行的重要保障,需要从多个维度进行防护。 * [安全总览](/practices/security-overview) - 系统安全整体概述 * [身份认证安全](/practices/auth-security) - 用户认证和授权安全 * [数据安全](/practices/data-security) - 数据存储和传输安全 * [接口安全](/practices/api-security) - API接口安全防护 * [前端安全](/practices/frontend-security) - 前端应用安全最佳实践 * [移动端安全](/practices/mobile-security) - 移动应用安全指南 ## 🚀 部署运维 高效的部署运维流程确保系统稳定运行。 * [DevOps最佳实践](/practices/devops-best-practices) - DevOps流程和工具使用 * [监控告警](/practices/monitoring-alerting) - 系统监控和告警机制 * [日志管理](/practices/log-management) - 日志收集、分析和管理 * [备份策略](/practices/backup-strategy) - 数据备份和恢复策略 ## 💡 为什么需要最佳实践? * **提高开发效率** - 统一的规范减少沟通成本 * **保证代码质量** - 规范的流程确保代码质量 * **降低维护成本** - 良好的架构便于后期维护 * **提升系统性能** - 优化策略提升用户体验 * **增强系统安全** - 安全实践保护系统和数据 * **简化部署运维** - 标准化流程提高运维效率 ## 🎯 如何使用这些实践? 1. **循序渐进** - 根据项目阶段选择相应的实践指南 2. **结合实际** - 根据具体业务场景调整实践方案 3. **持续改进** - 在实践中不断优化和完善流程 4. **团队协作** - 确保团队成员都遵循相同的实践标准 选择左侧菜单中的具体主题,开始您的最佳实践之旅! --- --- url: 'https://ruoyi.plus/frontend/utils/function.md' --- # 函数工具 --- --- url: 'https://ruoyi.plus/mobile/utils/share.md' --- # 分享工具 --- --- url: 'https://ruoyi.plus/mobile/plugins/share.md' --- # 分享插件 --- --- url: 'https://ruoyi.plus/mobile/performance/subpackage.md' --- # 分包加载优化 --- --- url: 'https://ruoyi.plus/mobile/pages/subpackages.md' --- # 分包页面管理 --- --- url: 'https://ruoyi.plus/practices/distributed-design.md' --- # 分布式设计 --- --- url: 'https://ruoyi.plus/frontend/layout/home-layout.md' --- # 前台布局 (homeLayout) --- --- url: 'https://ruoyi.plus/frontend/views/home.md' --- # 前台页面 --- --- url: 'https://ruoyi.plus/practices/frontend-security.md' --- # 前端安全 --- --- url: 'https://ruoyi.plus/frontend/getting-started.md' --- # 前端快速启动 本章节将指导你快速启动 Ruoyi-Plus-Uniapp 前端项目,包含环境准备、项目安装和运行等完整流程。 ## 🎯 环境要求 ### 必需环境 * **Node.js**: >= 18.18.0 * **包管理器**: pnpm >= 8.9.0 (推荐) 或 npm >= 8.9.0 * **浏览器**: Chrome >= 87 / Edge >= 88 / Safari >= 14 / Firefox >= 78 ### 推荐工具 * **IDE**: IDEA / VSCode + Volar 插件 * **版本管理**: Git * **Node管理**: nvm (推荐用于多版本管理) ## 🛠️ 环境安装 ### 1. Node.js 安装 #### 方式一:使用 NVM 管理 (推荐) ```bash # Windows 用户可下载 nvm-windows # https://github.com/coreybutler/nvm-windows # 安装18 版本 nvm install 18 nvm use 18 # 验证版本 node -v # 应显示 >= v18.18.0 npm -v # 应显示 >= 8.9.0 ``` ### 2. pnpm 安装 ```bash # 全局安装 pnpm npm install -g pnpm # 验证安装 pnpm -v # 应显示 >= 8.9.0 # 如果遇到全局bin目录问题,设置正确路径 pnpm config set global-bin-dir "你的nodejs路径" # 更新 pnpm pnpm self-update ``` ::: tip 💡 为什么推荐 pnpm? * **节省磁盘空间**: 通过硬链接共享依赖 * **安装速度快**: 并行安装,显著提升效率 * **更严格的依赖管理**: 避免幻影依赖问题 * **支持 monorepo**: 更好的多包项目管理 ::: ## 🚀 项目启动 ### 1. 获取项目代码 ```bash # 方式一:使用 Git 克隆(如果有权限) git clone https://gitee.com/your-repo/ruoyi-plus-uniapp.git cd ruoyi-plus-uniapp/plus-ui # 方式二:下载压缩包解压后进入前端目录 cd plus-ui ``` ### 2. 安装依赖 ```bash # 安装项目依赖 pnpm install # 如果安装失败,可以尝试清除缓存后重新安装 pnpm store prune pnpm install ``` ::: warning 🚨 安装问题解决 如果遇到依赖安装失败: 1. **网络问题**: 配置国内镜像源 2. **权限问题**: 使用管理员权限运行终端 3. **版本冲突**: 删除 `node_modules` 和 `pnpm-lock.yaml` 后重新安装 ::: ### 3. 配置后端接口地址 编辑 `env/.env.development` 文件,配置后端基础路径和端口号 ### 4. 启动开发服务器 ```bash # 启动开发服务器 pnpm dev # 启动成功后会显示类似信息: # ➜ Local: http://localhost:80/ # ➜ Network: http://192.168.1.100:80/ ``` 成功启动后,浏览器访问: http://localhost:80 ## 🔧 可用脚本命令 | 命令 | 说明 | 用途 | |------|------|------| | `pnpm dev` | 启动开发服务器 | 开发调试,支持热更新 | | `pnpm build:prod` | 生产环境构建 | 构建用于生产部署的代码 | | `pnpm build:dev` | 开发环境构建 | 构建用于开发环境的代码 | | `pnpm preview` | 预览构建结果 | 本地预览生产构建 | | `pnpm lint:eslint` | ESLint 检查 | 代码质量检查 | | `pnpm lint:eslint:fix` | 自动修复 ESLint | 自动修复可修复的问题 | | `pnpm prettier` | 格式化代码 | 统一代码格式 | ## 📋 登录系统 ### 默认管理员账号 启动成功后,使用以下账号登录系统: * **用户名**: `admin` * **密码**: `admin123` * **验证码**: 点击验证码图片可刷新 开始你的前端开发之旅吧!🚀 --- --- url: 'https://ruoyi.plus/practices/frontend-performance.md' --- # 前端性能优化 --- --- url: 'https://ruoyi.plus/frontend.md' --- # 前端项目简介 Ruoyi-Plus-Uniapp 前端是一个基于 Vue 3 + TypeScript 的现代化企业级 Web 管理系统,遵循"代码即文档"、"全栈统一"、"开发友好" 的核心理念。项目采用组合式 API 架构,注重开发体验和可维护性。 ## 技术栈 * **核心框架**: Vue 3 + TypeScript + Vite * **UI组件库**: Element Plus * **CSS框架**: UnoCSS * **状态管理**: Pinia * **图标系统**: Iconify * **富文本编辑器**: UMO Editor (基于 Tiptap) ## 架构特色 ### 1. 组合式架构设计 * 全面采用 Vue 3 组合式 API,摒弃传统 utils 工具类 * 将工具函数重构为 **Composables 组合函数函数**,发挥 Vue 3 优势 * 提供丰富的内置组合函数:`useAuth`、`useToken`、`useDict`、`useTheme`、`useI18n` 等 ### 2. 标准化目录结构 ```text src/ ├── api/ # 接口集中管理 ├── assets/ # 静态资源 ├── components/ # 自定义组件 ├── composables/ # 组合式函数(替代传统 utils) ├── directives/ # 自定义指令 ├── layouts/ # 布局组件 ├── locales/ # 国际化配置 ├── plugins/ # 插件配置 ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── types/ # 类型定义 └── utils/ # 工具类 └── views/ # 页面文件 ``` ### 3. 组件系统重构 * **命名规范化**: 自定义组件使用大驼峰,页面组件使用小驼峰 * **表单组件库**: 提供完整的 A 系列表单组件(AFormInput、AFormSelect 等) * **媒体管理**: AOssMediaManager 组件支持文件直传和目录管理 ## 核心功能 ### 🔐 权限管理 * 完善的自定义指令:`v-permi`、`v-role`、`v-admin`、`v-superadmin` 、`v-permi-all` 、`v-role-all` 、`v-tenant`、 `v-no-permi`、`v-no-role`、`v-no-permi`等 * 支持延迟加载和细粒度权限控制 * 与后端权限标识符保持一致 ### 🌍 国际化支持 * 增强的 `useI18n` 组合函数,支持智能提示 * 菜单国际化自动键名计算 * Element Plus 国际化资源集成 ### 🎨 主题系统 * `useTheme` 组合函数统一主题管理 * UnoCSS 深度定制配置 * 支持动态主题切换 ### 📊 数据处理 * 统一类型规范:`R` 响应类型,`PageResult` 分页类型 * `useHttp` 组合函数封装网络请求 * 表格组件支持跨页选择和高度自适应 ### 🔧 开发工具增强 * **表格功能**: `useSelection`、`useTableHeight` 组合函数 * **文件处理**: `useDownload` 组合函数,支持文件直传 * **实时通信**: `useSSE`、`useWS` 组合函数,支持重连策略 * **动画效果**: `useAnimation` 组合函数 ## 项目优势 1. **开发体验优秀**: 完整的 TypeScript 类型支持和智能提示 2. **代码质量高**: 统一的命名规范和完善的注释文档 3. **功能丰富**: 覆盖企业级应用的各种场景需求 4. **扩展性强**: 模块化设计,易于定制和扩展 5. **多端支持**: Web 端 + 移动端一体化解决方案 ## 适用场景 * 企业级 Web 管理系统 * SaaS 多租户管理平台 * 内容管理系统后台 * 需要权限管理的业务系统 * 中后台管理界面 Ruoyi Plus 前端通过现代化的技术栈和精心设计的架构,为开发者提供了一个高效、可维护的企业级 Web 管理系统解决方案。 --- --- url: 'https://ruoyi.plus/frontend/utils/crypto.md' --- # 加密工具 --- --- url: 'https://ruoyi.plus/mobile/utils/crypto.md' --- # 加密工具 --- --- url: 'https://ruoyi.plus/frontend/i18n/dynamic-translation.md' --- # 动态翻译 --- --- url: 'https://ruoyi.plus/frontend/router/dynamic-routes.md' --- # 动态路由 --- --- url: 'https://ruoyi.plus/frontend/styles/animations.md' --- # 动画系统 --- --- url: 'https://ruoyi.plus/mobile/performance/bundle-size.md' --- # 包体积优化 --- --- url: 'https://ruoyi.plus/frontend/dev/testing.md' --- # 单元测试 --- --- url: 'https://ruoyi.plus/mobile/debug/unit-testing.md' --- # 单元测试 --- --- url: 'https://ruoyi.plus/backend/common/core/validation.md' --- # 参数校验 (Validation Framework) ## 模块简介 参数校验模块提供了一套完整的数据验证框架,基于 Jakarta Bean Validation (JSR-303) 标准,集成了国际化支持、自定义校验注解和分组校验功能。该模块不仅支持标准的校验注解,还提供了针对业务场景的专用校验器。 ## 核心特性 ### 🌐 国际化支持 * 支持简化的国际化键格式 * 自动消息插值处理 * 多语言错误提示 ### 🔧 自定义校验注解 * XSS 攻击防护校验 * 字典值校验 * 枚举值校验 ### 📊 分组校验 * 新增操作校验组 * 编辑操作校验组 * 查询操作校验组 ### ⚡ 性能优化 * 快速失败模式 * 枚举值缓存机制 * 高效的正则表达式匹配 ## 校验框架配置 ### 全局校验器配置 ```java @AutoConfiguration(before = ValidationAutoConfiguration.class) public class ValidatorConfig { @Bean public Validator validator(MessageSource messageSource) { try (LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean()) { // 设置自定义的国际化消息拦截器 factoryBean.setMessageInterpolator(new I18nMessageInterceptor(messageSource)); // 使用 HibernateValidator 校验器 factoryBean.setProviderClass(HibernateValidator.class); // 配置快速失败模式 Properties properties = new Properties(); properties.setProperty("hibernate.validator.fail_fast", "true"); factoryBean.setValidationProperties(properties); factoryBean.afterPropertiesSet(); return factoryBean.getValidator(); } } } ``` ### 国际化消息拦截器 ```java @Component public class I18nMessageInterceptor implements MessageInterpolator { private final MessageSource messageSource; private final MessageInterpolator defaultInterpolator; @Override public String interpolate(String messageTemplate, Context context, Locale locale) { if (StringUtils.isBlank(messageTemplate)) { return messageTemplate; } String trimmedTemplate = messageTemplate.trim(); // 判断是否为简化的国际化键格式 if (RegexValidator.isValidI18nKey(trimmedTemplate)) { try { // 获取国际化消息 String i18nMessage = messageSource.getMessage(trimmedTemplate, null, locale); // 将国际化消息交给默认插值器处理约束属性 return defaultInterpolator.interpolate(i18nMessage, context, locale); } catch (Exception e) { return trimmedTemplate; } } // 其他情况委托给默认插值器处理 return defaultInterpolator.interpolate(messageTemplate, context, locale); } } ``` **特性说明:** * 支持简化的国际化键格式(无需花括号) * 自动处理约束属性插值 * 降级处理机制,确保系统稳定性 ## 校验分组 (Validation Groups) ### 基础分组定义 #### 新增操作校验组 ```java /** * 新增操作校验分组 * 用于标识新增操作时需要进行的字段校验 */ public interface AddGroup { } ``` #### 编辑操作校验组 ```java /** * 编辑操作校验分组 * 用于标识编辑操作时需要进行的字段校验 */ public interface EditGroup { } ``` #### 查询操作校验组 ```java /** * 查询操作校验分组 * 用于标识查询操作时需要进行的字段校验 */ public interface QueryGroup { } ``` ### 分组校验使用示例 ```java public class UserBo { @Null(groups = AddGroup.class, message = "新增时ID必须为空") @NotNull(groups = EditGroup.class, message = "编辑时ID不能为空") @Min(value = 1, groups = EditGroup.class, message = "ID必须大于0") private Long id; @NotBlank(groups = {AddGroup.class, EditGroup.class}, message = "用户名不能为空") @Length(min = 2, max = 30, groups = {AddGroup.class, EditGroup.class}) private String userName; @Email(groups = {AddGroup.class, EditGroup.class}, message = "邮箱格式不正确") private String email; @Size(min = 1, max = 50, groups = QueryGroup.class, message = "查询用户名长度必须在1-50之间") private String queryUserName; } ``` ### Controller 中的分组使用 ```java @RestController @RequestMapping("/users") public class UserController { @PostMapping public R add(@Validated(AddGroup.class) @RequestBody UserBo bo) { return R.ok(userService.add(bo)); } @PutMapping public R update(@Validated(EditGroup.class) @RequestBody UserBo bo) { return R.status(userService.update(bo)); } @GetMapping public R> list(@Validated(QueryGroup.class) UserBo query) { return R.ok(userService.list(query)); } } ``` ## 自定义校验注解 ### 1. XSS 攻击防护校验 #### 注解定义 ```java @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Constraint(validatedBy = {XssValidator.class}) public @interface Xss { /** * XSS检测模式 */ enum Mode { /** 基础模式:检测常见的HTML标签和脚本 */ BASIC, /** 严格模式:检测更多潜在的XSS攻击向量 */ STRICT, /** 宽松模式:仅检测明显的脚本标签 */ LENIENT } Mode mode() default Mode.BASIC; String message() default "输入内容包含潜在的XSS攻击代码,请检查并移除脚本标签"; Class[] groups() default {}; Class[] payload() default {}; } ``` #### 校验器实现 ```java @Slf4j public class XssValidator implements ConstraintValidator { private Xss.Mode mode; // 基础模式正则表达式 private static final Pattern BASIC_XSS_PATTERN = Pattern.compile(""" (?i)( <[^>]*script[^>]*>| <[^>]*on\\w+\\s*=| <[^>]*style\\s*=.*expression\\s*\\(| javascript\\s*:| vbscript\\s*:| <[^>]*src\\s*=\\s*["']?\\s*javascript\\s*:| <\\s*/?.*(script|object|applet|embed|form|iframe|frameset|frame)\\s*[^>]*> ) """); // 严格模式正则表达式 private static final Pattern STRICT_XSS_PATTERN = Pattern.compile(""" (?i)( <[^>]*script[^>]*>| <[^>]*on\\w+\\s*=| <[^>]*style\\s*=.*expression\\s*\\(| (javascript|vbscript)\\s*:| data\\s*:.*text/html| <[^>]*src\\s*=\\s*["']?\\s*(javascript|vbscript|data)\\s*:| (&#\\d+;?)|(&#x[0-9a-f]+;?)| (%3c|%3e|%22|%27|%2f|%5c)| (\\\\x[0-9a-f]{2})|(\\\\u[0-9a-f]{4})| (eval|expression)\\s*\\(| String\\s*\\.\\s*fromCharCode| (document|window)\\s*\\.| (alert|confirm|prompt)\\s*\\(| <\\s*/?.*(script|object|applet|embed|form|iframe|frameset|frame|meta|link|style|base)\\s*[^>]*> ) """); // 宽松模式正则表达式 private static final Pattern LENIENT_XSS_PATTERN = Pattern.compile(""" (?i)( <\\s*script[^>]*>.*?| (javascript|vbscript)\\s*:| <\\s*(iframe|object|embed)[^>]*> ) """); @Override public void initialize(Xss annotation) { this.mode = annotation.mode(); log.debug("初始化XSS校验器: mode={}", mode); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (StringUtils.isBlank(value)) { return true; } try { boolean containsXss = switch (mode) { case BASIC -> detectBasicXss(value); case STRICT -> detectStrictXss(value); case LENIENT -> detectLenientXss(value); }; if (containsXss) { log.warn("检测到XSS攻击代码: mode={}, value={}", mode, value.length() > 100 ? value.substring(0, 100) + "..." : value); buildCustomErrorMessage(context, value); return false; } return true; } catch (Exception e) { log.error("XSS检测异常: mode={}, value={}", mode, value, e); return false; } } // 静态工具方法 public static boolean containsXss(String value, Xss.Mode mode) { if (StringUtils.isBlank(value)) { return false; } XssValidator validator = new XssValidator(); return switch (mode) { case BASIC -> validator.detectBasicXss(value); case STRICT -> validator.detectStrictXss(value); case LENIENT -> validator.detectLenientXss(value); }; } } ``` #### 使用示例 ```java public class UserBo { @Xss(message = "用户名不能包含脚本代码") private String userName; @Xss(mode = Xss.Mode.STRICT, message = "评论内容存在安全风险") private String comment; @Xss(mode = Xss.Mode.LENIENT, message = "富文本内容包含危险脚本") private String richText; } ``` ### 2. 字典值校验 #### 注解定义 ```java @Documented @Constraint(validatedBy = DictPatternValidator.class) @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DictPattern { /** * 字典类型编码 */ String dictType(); /** * 多值分隔符 */ String separator() default ","; /** * 是否允许空值 */ boolean allowEmpty() default true; String message() default "字典值无效,不在[{dictType}]字典范围内"; Class[] groups() default {}; Class[] payload() default {}; } ``` #### 校验器实现 ```java @Slf4j public class DictPatternValidator implements ConstraintValidator { private String dictType; private String separator; private boolean allowEmpty; private DictService dictService; @Override public void initialize(DictPattern annotation) { this.dictType = annotation.dictType(); this.separator = annotation.separator(); this.allowEmpty = annotation.allowEmpty(); log.debug("初始化字典校验器: dictType={}, separator={}, allowEmpty={}", dictType, separator, allowEmpty); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 空值检查 if (StringUtils.isBlank(value)) { return allowEmpty; } // 字典类型检查 if (StringUtils.isBlank(dictType)) { log.warn("字典类型不能为空"); return false; } try { // 延迟获取字典服务实例 if (dictService == null) { dictService = SpringUtils.getBean(DictService.class); } // 调用字典服务获取标签 String dictLabel = dictService.getDictLabel(dictType, value, separator); // 检查是否所有值都有效 if (StringUtils.isBlank(dictLabel)) { log.debug("字典值校验失败: dictType={}, value={}", dictType, value); buildCustomErrorMessage(context, value); return false; } // 对于多值情况,检查返回的标签中是否包含空字符串 if (value.contains(separator)) { String[] labels = dictLabel.split(separator); for (String label : labels) { if (StringUtils.isBlank(label.trim())) { log.debug("字典值校验失败,包含无效值: dictType={}, value={}", dictType, value); buildCustomErrorMessage(context, value); return false; } } } return true; } catch (Exception e) { log.error("字典值校验异常: dictType={}, value={}", dictType, value, e); return false; } } private void buildCustomErrorMessage(ConstraintValidatorContext context, String invalidValue) { context.disableDefaultConstraintViolation(); String customMessage = String.format("字典值[%s]无效,不在[%s]字典范围内", invalidValue, dictType); context.buildConstraintViolationWithTemplate(customMessage) .addConstraintViolation(); } } ``` #### 使用示例 ```java public class UserBo { @DictPattern(dictType = "sys_user_sex", message = "性别字典值无效") private String gender; @DictPattern(dictType = "sys_user_status", separator = ";", message = "状态值无效") private String statusList; @DictPattern(dictType = "sys_user_type", allowEmpty = false) private String userType; } ``` ### 3. 枚举值校验 #### 注解定义 ```java @Documented @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Repeatable(EnumPattern.List.class) @Constraint(validatedBy = {EnumPatternValidator.class}) public @interface EnumPattern { /** * 需要校验的枚举类型 */ Class> type(); /** * 枚举类型中用于校验的字段名称 */ String fieldName(); /** * 是否允许空值 */ boolean allowEmpty() default true; String message() default "输入值不在枚举[{type}.{fieldName}]范围内"; Class[] groups() default {}; Class[] payload() default {}; /** * 多重注解容器 */ @Documented @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @interface List { EnumPattern[] value(); } } ``` #### 校验器实现 ```java @Slf4j public class EnumPatternValidator implements ConstraintValidator { private Class> enumType; private String fieldName; private boolean allowEmpty; /** * 枚举值缓存:key为"enumType-fieldName",value为有效值集合 */ private static final ConcurrentHashMap> ENUM_VALUE_CACHE = new ConcurrentHashMap<>(); @Override public void initialize(EnumPattern annotation) { this.enumType = annotation.type(); this.fieldName = annotation.fieldName(); this.allowEmpty = annotation.allowEmpty(); log.debug("初始化枚举校验器: enumType={}, fieldName={}, allowEmpty={}", enumType.getSimpleName(), fieldName, allowEmpty); // 预加载枚举值到缓存 preloadEnumValues(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (isEmptyValue(value)) { return allowEmpty; } try { Set validValues = getValidEnumValues(); boolean isValid = validValues.contains(value); if (!isValid) { log.debug("枚举值校验失败: enumType={}, fieldName={}, value={}, validValues={}", enumType.getSimpleName(), fieldName, value, validValues); buildCustomErrorMessage(context, value, validValues); } return isValid; } catch (Exception e) { log.error("枚举值校验异常: enumType={}, fieldName={}, value={}", enumType.getSimpleName(), fieldName, value, e); return false; } } private void preloadEnumValues() { String cacheKey = getCacheKey(); if (!ENUM_VALUE_CACHE.containsKey(cacheKey)) { try { Set validValues = extractEnumValues(); ENUM_VALUE_CACHE.put(cacheKey, validValues); log.debug("枚举值缓存加载完成: key={}, values={}", cacheKey, validValues); } catch (Exception e) { log.error("枚举值缓存加载失败: enumType={}, fieldName={}", enumType.getSimpleName(), fieldName, e); } } } private Set extractEnumValues() { return Arrays.stream(enumType.getEnumConstants()) .map(enumConstant -> { try { return ReflectUtils.invokeGetter(enumConstant, fieldName); } catch (Exception e) { log.warn("获取枚举字段值失败: enum={}, fieldName={}", enumConstant, fieldName, e); return null; } }) .filter(Objects::nonNull) .collect(Collectors.toSet()); } // 缓存管理方法 public static void clearCache() { ENUM_VALUE_CACHE.clear(); log.debug("枚举值缓存已清理"); } public static int getCacheSize() { return ENUM_VALUE_CACHE.size(); } } ``` #### 使用示例 ```java public class OrderBo { // 校验订单状态枚举的code字段 @EnumPattern(type = OrderStatusEnum.class, fieldName = "code", message = "订单状态无效") private String status; // 校验性别枚举的value字段 @EnumPattern(type = GenderEnum.class, fieldName = "value") private Integer gender; // 多重校验(同时校验多个枚举) @EnumPattern(type = StatusEnum.class, fieldName = "code") @EnumPattern(type = TypeEnum.class, fieldName = "value") private String statusOrType; } ``` ## 校验工具类 ### ValidatorUtils 工具类 ```java @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ValidatorUtils { private static final Validator VALIDATOR = SpringUtils.getBean(Validator.class); /** * 校验对象参数(抛出异常) */ public static void validate(T object, Class... groups) { if (object == null) { throw new IllegalArgumentException("校验对象不能为null"); } Set> violations = VALIDATOR.validate(object, groups); if (!violations.isEmpty()) { String errorMessage = violations.stream() .map(v -> v.getPropertyPath() + ": " + v.getMessage()) .collect(Collectors.joining("; ")); throw new ConstraintViolationException("参数校验失败 - " + errorMessage, violations); } } /** * 校验对象参数(返回校验结果) */ public static Set> validateAndReturn(T object, Class... groups) { if (object == null) { throw new IllegalArgumentException("校验对象不能为null"); } return VALIDATOR.validate(object, groups); } /** * 校验对象的指定属性 */ public static void validateProperty(T object, String propertyName, Class... groups) { if (object == null) { throw new IllegalArgumentException("校验对象不能为null"); } if (StringUtils.isBlank(propertyName)) { throw new IllegalArgumentException("属性名称不能为空"); } Set> violations = VALIDATOR.validateProperty(object, propertyName, groups); if (!violations.isEmpty()) { throw new ConstraintViolationException("属性校验失败", violations); } } /** * 校验属性值 */ public static void validateValue(Class beanType, String propertyName, Object value, Class... groups) { if (beanType == null) { throw new IllegalArgumentException("对象类型不能为null"); } if (StringUtils.isBlank(propertyName)) { throw new IllegalArgumentException("属性名称不能为空"); } Set> violations = VALIDATOR.validateValue(beanType, propertyName, value, groups); if (!violations.isEmpty()) { throw new ConstraintViolationException("属性值校验失败", violations); } } /** * 检查对象是否校验通过 */ public static boolean isValid(T object, Class... groups) { if (object == null) { return false; } Set> violations = VALIDATOR.validate(object, groups); return violations.isEmpty(); } /** * 获取校验错误信息字符串 */ public static String getValidationErrors(T object, Class... groups) { if (object == null) { return "校验对象为null"; } Set> violations = VALIDATOR.validate(object, groups); if (violations.isEmpty()) { return ""; } return violations.stream() .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()) .collect(Collectors.joining("; ")); } } ``` #### 使用示例 ```java @Service public class UserService { public void createUser(UserBo userBo) { // 手动触发校验 ValidatorUtils.validate(userBo, AddGroup.class); // 检查是否校验通过 if (!ValidatorUtils.isValid(userBo, AddGroup.class)) { String errors = ValidatorUtils.getValidationErrors(userBo, AddGroup.class); throw new ServiceException("参数校验失败: " + errors); } // 校验单个属性 ValidatorUtils.validateProperty(userBo, "userName", AddGroup.class); // 校验属性值 ValidatorUtils.validateValue(UserBo.class, "userName", "testUser", AddGroup.class); } } ``` ## 国际化消息配置 ### 消息文件示例 #### messages.properties ```properties # 通用验证消息 common.required=必须填写 common.length.invalid=长度必须在{min}到{max}个字符之间 common.id.required=主键ID不能为空 # 用户相关验证消息 user.username.required=用户名不能为空 user.username.length.invalid=用户名长度必须在{min}到{max}个字符之间 user.password.required=用户密码不能为空 user.password.length.invalid=用户密码长度必须在{min}到{max}个字符之间 user.email.format.invalid=邮箱格式错误 user.phone.format.invalid=手机号格式错误 # 验证码相关消息 verify.code.captcha.required=图形验证码不能为空 verify.code.captcha.invalid=图形验证码错误 verify.code.sms.required=短信验证码不能为空 verify.code.sms.invalid=短信验证码错误 ``` #### messages\_en.properties ```properties # Common validation messages common.required=Required common.length.invalid=Length must be between {min} and {max} characters common.id.required=Primary key ID cannot be empty # User related validation messages user.username.required=Username cannot be empty user.username.length.invalid=Username length must be between {min} and {max} characters user.password.required=User password cannot be empty user.password.length.invalid=User password length must be between {min} and {max} characters user.email.format.invalid=Invalid email format user.phone.format.invalid=Invalid phone number format # Verification code related messages verify.code.captcha.required=Captcha cannot be empty verify.code.captcha.invalid=Invalid captcha verify.code.sms.required=SMS verification code cannot be empty verify.code.sms.invalid=Invalid SMS verification code ``` ### 国际化使用示例 ```java public class UserBo { @NotBlank(message = "user.username.required") @Length(min = 2, max = 30, message = "user.username.length.invalid") private String userName; @NotBlank(message = "user.password.required") @Length(min = 5, max = 30, message = "user.password.length.invalid") private String password; @Email(message = "user.email.format.invalid") private String email; } ``` ## 正则表达式校验 ### RegexValidator 工具类 ```java @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class RegexValidator extends Validator { /** * 验证国际化键格式是否正确 */ public static boolean isValidI18nKey(CharSequence i18nKey) { return isMatchPattern(RegexPatternPool.I18N_KEY, i18nKey); } /** * 验证用户账号格式是否正确 */ public static boolean isValidAccount(CharSequence account) { return isMatchPattern(RegexPatternPool.ACCOUNT, account); } /** * 验证状态值格式是否正确(0或1) */ public static boolean isValidStatus(CharSequence status) { return isMatchPattern(RegexPatternPool.BINARY_STATUS, status); } /** * 验证字典类型格式是否正确 */ public static boolean isValidDictType(CharSequence dictType) { return isMatchPattern(RegexPatternPool.DICT_TYPE, dictType); } /** * 验证密码强度是否符合要求 */ public static boolean isStrongPassword(CharSequence password) { return isMatchPattern(RegexPatternPool.STRONG_PASSWORD, password); } /** * 验证权限标识格式是否正确 */ public static boolean isValidPermission(CharSequence permission) { return isMatchPattern(RegexPatternPool.PERMISSION, permission); } } ``` ### 正则表达式模式定义 ```java public interface RegexPatterns extends RegexPool { /** * 国际化键格式:英文段.英文段.英文段...(至少2段) */ String I18N_KEY = "^[a-zA-Z][a-zA-Z0-9_]*(?:\\.[a-zA-Z][a-zA-Z0-9_]*)+$"; /** * 字典类型:小写字母开头,只能包含小写字母、数字、下划线 */ String DICT_TYPE = "^[a-z][a-z0-9_]*$"; /** * 权限标识格式:模块:操作:资源 或 空字符串 */ String PERMISSION = "^$|^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$"; /** * 用户账号:字母开头,5-16位,可包含字母、数字、下划线 */ String ACCOUNT = "^[a-zA-Z][a-zA-Z0-9_]{4,15}$"; /** * 强密码:至少8位,必须包含大小写字母、数字、特殊字符 */ String STRONG_PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"; /** * 通用状态:0(正常) 或 1(停用) */ String BINARY_STATUS = "^[01]$"; } ``` ## 复杂校验场景 ### 1. 条件校验 ```java public class ConditionalValidationBo { @NotNull private String type; // 当type为"email"时,email字段必须填写且格式正确 @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") @ConditionalValidator(condition = "type", value = "email") private String email; // 当type为"phone"时,phone字段必须填写且格式正确 @NotBlank(message = "手机号不能为空") @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") @ConditionalValidator(condition = "type", value = "phone") private String phone; } ``` ### 2. 跨字段校验 ```java @CrossFieldValidator public class PasswordChangeBo { @NotBlank(message = "原密码不能为空") private String oldPassword; @NotBlank(message = "新密码不能为空") @Length(min = 8, max = 32, message = "新密码长度必须在8-32位之间") private String newPassword; @NotBlank(message = "确认密码不能为空") private String confirmPassword; // 自定义校验方法 @AssertTrue(message = "新密码不能与原密码相同") public boolean isNewPasswordDifferent() { return !Objects.equals(oldPassword, newPassword); } @AssertTrue(message = "两次输入的密码不一致") public boolean isPasswordConfirmed() { return Objects.equals(newPassword, confirmPassword); } } ``` ### 3. 集合元素校验 ```java public class BatchOperationBo { @NotEmpty(message = "操作列表不能为空") @Size(max = 100, message = "批量操作最多支持100条记录") @Valid // 关键:启用集合元素校验 private List<@Valid UserBo> users; @NotEmpty(message = "角色ID列表不能为空") private List<@NotNull @Min(1) Long> roleIds; @Valid private Map<@NotBlank String, @Valid UserBo> userMap; } ``` ### 4. 嵌套对象校验 ```java public class OrderBo { @NotNull(message = "订单信息不能为空") @Valid // 启用嵌套对象校验 private OrderDetailBo orderDetail; @NotNull(message = "收货地址不能为空") @Valid private AddressBo shippingAddress; @NotEmpty(message = "订单项不能为空") @Valid private List<@Valid OrderItemBo> orderItems; } public class OrderDetailBo { @NotBlank(message = "订单编号不能为空") private String orderNo; @NotNull(message = "订单金额不能为空") @DecimalMin(value = "0.01", message = "订单金额必须大于0") private BigDecimal amount; } ``` ## 性能优化 ### 1. 快速失败模式 ```java // 全局配置快速失败 @Bean public Validator validator() { ValidatorFactory factory = Validation.byProvider(HibernateValidator.class) .configure() .addProperty("hibernate.validator.fail_fast", "true") .buildValidatorFactory(); return factory.getValidator(); } ``` ### 2. 校验缓存优化 ```java @Service public class CachedValidationService { private final LoadingCache>> validationCache; public CachedValidationService() { this.validationCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(Duration.ofMinutes(10)) .build(this::performValidation); } public boolean isValid(Object object, String cacheKey) { try { Set> violations = validationCache.get(cacheKey); return violations.isEmpty(); } catch (Exception e) { // 缓存失效时直接校验 return ValidatorUtils.isValid(object); } } private Set> performValidation(String cacheKey) { // 根据缓存键执行实际校验逻辑 return Collections.emptySet(); } } ``` ### 3. 异步校验 ```java @Service public class AsyncValidationService { @Async public CompletableFuture validateAsync(Object object, Class... groups) { try { Set> violations = SpringUtils.getBean(Validator.class).validate(object, groups); ValidationResult result = new ValidationResult(); result.setValid(violations.isEmpty()); result.setViolations(violations); return CompletableFuture.completedFuture(result); } catch (Exception e) { return CompletableFuture.failedFuture(e); } } } ``` ## 测试支持 ### 校验测试工具 ```java @TestConfiguration public class ValidationTestConfig { @Bean @Primary public Validator testValidator() { return Validation.buildDefaultValidatorFactory().getValidator(); } } @ExtendWith(SpringExtension.class) @Import(ValidationTestConfig.class) public class ValidationTest { @Autowired private Validator validator; @Test public void testUserBoValidation() { UserBo userBo = new UserBo(); userBo.setUserName(""); // 空用户名,应该校验失败 Set> violations = validator.validate(userBo, AddGroup.class); assertThat(violations).isNotEmpty(); assertThat(violations) .extracting(ConstraintViolation::getMessage) .contains("用户名不能为空"); } @Test public void testXssValidation() { UserBo userBo = new UserBo(); userBo.setUserName(""); Set> violations = validator.validate(userBo); assertThat(violations).isNotEmpty(); assertThat(violations) .extracting(ConstraintViolation::getMessage) .anyMatch(msg -> msg.contains("XSS")); } @Test public void testDictPatternValidation() { UserBo userBo = new UserBo(); userBo.setGender("invalid_gender"); Set> violations = validator.validate(userBo); assertThat(violations).isNotEmpty(); assertThat(violations) .extracting(ConstraintViolation::getMessage) .anyMatch(msg -> msg.contains("字典值无效")); } } ``` ### Mock 校验服务 ```java @TestComponent public class MockValidationService { public static Set> mockValidationViolations( Class beanClass, String propertyPath, String message) { ConstraintViolation violation = mock(ConstraintViolation.class); when(violation.getPropertyPath()).thenReturn(mock(Path.class)); when(violation.getPropertyPath().toString()).thenReturn(propertyPath); when(violation.getMessage()).thenReturn(message); return Set.of(violation); } public static void assertValidationError(Set> violations, String propertyPath, String expectedMessage) { assertThat(violations) .filteredOn(v -> v.getPropertyPath().toString().equals(propertyPath)) .extracting(ConstraintViolation::getMessage) .contains(expectedMessage); } } ``` ## 最佳实践 ### 1. 校验分组设计原则 ```java // ✅ 推荐:明确的分组命名 public interface UserAddGroup {} public interface UserEditGroup {} public interface UserQueryGroup {} // ❌ 避免:通用分组名称 public interface CreateGroup {} public interface UpdateGroup {} ``` ### 2. 自定义校验注解设计 ```java // ✅ 推荐:可配置的校验注解 @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PhoneValidator.class) public @interface Phone { String[] regions() default {"CN"}; // 支持多个地区 boolean strict() default false; // 严格模式 String message() default "手机号格式不正确"; Class[] groups() default {}; Class[] payload() default {}; } // ❌ 避免:硬编码的校验注解 @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = ChinaPhoneValidator.class) public @interface ChinaPhone { String message() default "中国手机号格式不正确"; Class[] groups() default {}; Class[] payload() default {}; } ``` ### 3. 错误信息国际化 ```java // ✅ 推荐:使用国际化键 @NotBlank(message = "user.username.required") private String userName; // ✅ 推荐:支持参数的国际化键 @Length(min = 2, max = 30, message = "user.username.length.invalid") private String userName; // ❌ 避免:硬编码中文消息 @NotBlank(message = "用户名不能为空") private String userName; ``` ### 4. 校验性能优化 ```java // ✅ 推荐:使用快速失败模式 ValidatorFactory factory = Validation.byProvider(HibernateValidator.class) .configure() .addProperty("hibernate.validator.fail_fast", "true") .buildValidatorFactory(); // ✅ 推荐:缓存校验结果 @Cacheable(value = "validation", key = "#object.hashCode() + '_' + #groups") public Set> validate(Object object, Class... groups) { return validator.validate(object, groups); } // ❌ 避免:重复创建Validator实例 public boolean isValid(Object object) { Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); return validator.validate(object).isEmpty(); } ``` ### 5. 异常处理最佳实践 ```java @RestControllerAdvice public class ValidationExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public R handleValidationException(MethodArgumentNotValidException e) { BindingResult bindingResult = e.getBindingResult(); String errorMessage = bindingResult.getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining("; ")); return R.fail("参数校验失败: " + errorMessage); } @ExceptionHandler(ConstraintViolationException.class) public R handleConstraintViolationException(ConstraintViolationException e) { String errorMessage = e.getConstraintViolations().stream() .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()) .collect(Collectors.joining("; ")); return R.fail("参数校验失败: " + errorMessage); } } ``` ## 注意事项 ### 1. 校验顺序 1. 基础格式校验(@NotNull, @NotBlank) 2. 格式校验(@Email, @Pattern) 3. 业务校验(@DictPattern, @EnumPattern) 4. 安全校验(@Xss) ### 2. 性能考虑 * 启用快速失败模式减少校验时间 * 合理使用校验缓存 * 避免在循环中重复校验 ### 3. 国际化支持 * 所有错误信息都应支持国际化 * 使用参数化消息模板 * 提供降级处理机制 ### 4. 安全注意事项 * XSS校验应覆盖所有用户输入 * 字典值校验防止恶意输入 * 敏感信息不应出现在错误消息中 ### 5. 维护性 * 校验规则应该可配置 * 复杂校验逻辑应该单元测试 * 定期review和更新校验规则 ## 总结 参数校验模块提供了完整的数据验证解决方案,包括: * **标准校验支持**: 基于 Jakarta Bean Validation 标准 * **国际化集成**: 自动消息插值和多语言支持 * **自定义校验器**: XSS防护、字典值校验、枚举值校验 * **分组校验**: 灵活的校验场景控制 * **性能优化**: 快速失败、缓存机制、异步校验 * **测试支持**: 完整的测试工具和Mock支持 通过合理使用这些功能,可以构建出安全、高效、可维护的参数校验体系,确保系统的数据质量和安全性。 --- --- url: 'https://ruoyi.plus/mobile/components/feedback.md' --- # 反馈组件 --- --- url: 'https://ruoyi.plus/practices/backend-performance.md' --- # 后端性能优化 --- --- url: 'https://ruoyi.plus/backend/configuration.md' --- # 后端配置文件说明文档 ## 概述 本项目采用 Spring Boot 框架,使用 YAML 格式的配置文件来管理应用的各项配置。配置文件分为三个层级: * `application.yml` - 主配置文件,包含所有通用配置 * `application-dev.yml` - 开发环境专用配置 * `application-prod.yml` - 生产环境专用配置 ## 配置文件结构 ### 1. 应用基础配置 ```yaml app: id: ryplus_uni # 应用唯一标识符 title: ryplus-uni后台管理 # 应用显示名称 version: ${revision} # 应用版本号 copyright-year: 2025 # 版权年份 upload-path: /path/to/upload # 文件上传路径 base-api: https://api.domain.com # API基础路径 生产环境必须配置正确的 需要做支付回调地址等 ``` ### 2. 服务器配置 ```yaml server: port: 5500 # 服务端口 servlet: context-path: / # 应用访问路径 undertow: # Undertow服务器配置 max-http-post-size: -1 # POST请求最大大小 buffer-size: 512 # 缓冲区大小 threads: io: 8 # IO线程数 worker: 256 # 工作线程数 ``` ### 3. Spring框架配置 #### 3.1 应用配置 ```yaml spring: application: name: ${app.id} # 应用名称 profiles: active: @profiles.active@ # 激活的环境配置 threads: virtual: enabled: true # 启用虚拟线程(JDK21+) ``` #### 3.2 文件上传配置 ```yaml spring: servlet: multipart: max-file-size: 10MB # 单文件最大大小 max-request-size: 20MB # 请求最大大小 location: ${app.upload-path} # 临时文件目录 ``` #### 3.3 JSON序列化配置 ```yaml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss serialization: indent_output: false # 是否格式化输出 fail_on_empty_beans: false # 忽略空对象 deserialization: fail_on_unknown_properties: false # 忽略未知属性 ``` ### 4. 数据库配置 #### 4.1 数据源配置 ```yaml spring: datasource: type: com.zaxxer.hikari.HikariDataSource dynamic: primary: master # 默认数据源 strict: true # 严格模式 datasource: master: # 主库配置 driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ryplus_uni username: root password: root slave: # 从库配置 lazy: true # 懒加载 # ... 其他配置同master ``` #### 4.2 连接池配置 ```yaml spring: datasource: dynamic: hikari: maxPoolSize: 20 # 最大连接数 minIdle: 10 # 最小空闲连接数 connectionTimeout: 30000 # 连接超时时间 idleTimeout: 600000 # 空闲连接超时时间 maxLifetime: 1800000 # 连接最大生命周期 ``` #### 4.3 MyBatis-Plus配置 ```yaml mybatis-plus: enableLogicDelete: true # 开启逻辑删除 mapperPackage: plus.ruoyi.**.mapper mapperLocations: classpath*:mapper/**/*Mapper.xml typeAliasesPackage: plus.ruoyi.**.domain.entity global-config: dbConfig: idType: ASSIGN_ID # 主键生成策略 ``` ### 5. Redis缓存配置 #### 5.1 Redis连接配置 ```yaml spring.data: redis: host: localhost # Redis服务器地址 port: 6379 # 端口 database: 0 # 数据库索引 password: your_password # 密码 timeout: 10s # 连接超时时间 ``` #### 5.2 Redisson配置 ```yaml redisson: keyPrefix: ${app.id} # Redis键前缀 threads: 4 # 线程池大小 nettyThreads: 8 # Netty线程池大小 singleServerConfig: clientName: ${app.id} # 客户端名称 connectionPoolSize: 32 # 连接池大小 timeout: 3000 # 命令超时时间 ``` ### 6. 安全认证配置 #### 6.1 Sa-Token配置 ```yaml sa-token: token-name: Authorization # Token名称 is-concurrent: true # 允许并发登录 is-share: false # 是否共享Token jwt-secret-key: your_secret # JWT密钥 ``` #### 6.2 用户安全配置 ```yaml user: password: maxRetryCount: 5 # 密码最大错误次数 lockTime: 10 # 锁定时间(分钟) ``` #### 6.3 验证码配置 ```yaml captcha: type: MATH # 验证码类型: MATH/CHAR category: CIRCLE # 干扰类型: LINE/CIRCLE/SHEAR numberLength: 1 # 数字验证码位数 charLength: 4 # 字符验证码长度 ``` ### 7. 第三方服务配置 #### 7.1 邮件服务配置 ```yaml mail: enabled: false # 是否启用邮件服务 host: smtp.163.com # SMTP服务器 port: 465 # SMTP端口 auth: true # 是否需要认证 from: xxx@163.com # 发送方邮箱 user: xxx@163.com # 用户名 pass: your_password # 密码 starttlsEnable: true # 启用STARTTLS sslEnable: true # 启用SSL ``` #### 7.2 短信服务配置 ```yaml sms: config-type: yaml # 配置类型 restricted: true # 是否开启限制 minute-max: 1 # 每分钟最大发送数 account-max: 30 # 每日最大发送数 blends: config1: # 阿里云短信配置 supplier: alibaba access-key-id: your_key access-key-secret: your_secret signature: your_signature ``` #### 7.3 定时任务配置 ```yaml snail-job: enabled: false # 是否启用定时任务 group: ${app.id} # 任务组名 token: your_token # 验证令牌 server: host: 127.0.0.1 # 调度中心地址 port: 17888 # 调度中心端口 namespace: ${spring.profiles.active} # 命名空间 ``` ### 8. 监控和文档配置 #### 8.1 API文档配置 ```yaml springdoc: api-docs: enabled: true # 是否开启接口文档 info: title: '${app.title}_接口文档' description: '接口文档说明' version: '版本号: ${app.version}' group-configs: # 分组配置 - group: business packages-to-scan: plus.ruoyi.business - group: system packages-to-scan: plus.ruoyi.system ``` #### 8.2 监控配置 ```yaml management: endpoints: web: exposure: include: '*' # 暴露所有监控端点 endpoint: health: show-details: ALWAYS # 显示详细健康信息 logfile: external-file: ./logs/sys-console.log # 日志文件路径 ``` ### 9. 其他重要配置 #### 9.1 多租户配置 ```yaml tenant: enable: true # 是否开启多租户 excludes: [ ] # 排除的表名 ``` #### 9.2 数据加密配置 ```yaml mybatis-encryptor: enable: false # 是否开启数据加密 algorithm: BASE64 # 加密算法 encode: BASE64 # 编码方式 password: your_password # 加密密钥 ``` #### 9.3 API接口加密配置 ```yaml api-decrypt: enabled: true # 是否开启接口加密 headerFlag: encrypt-key # 加密头标识 publicKey: your_public_key # 公钥 privateKey: your_private_key # 私钥 ``` ## 环境差异配置 ### 开发环境特点 * 开启SQL性能分析(p6spy: true) * 使用本地文件路径 * Redis无密码 * 各种调试功能开启 ### 生产环境特点 * 关闭SQL性能分析(p6spy: false) * 使用生产环境路径 * Redis配置密码 * 连接池参数优化 * 启用监控和定时任务 ## 配置最佳实践 ### 1. 安全性 * 生产环境必须配置Redis密码 * 生产环境数据库不使用root用户,创建一个表绑定的专用用户 ### 2. 性能优化 * 生产环境适当增大连接池大小 * 关闭不必要的调试功能 * 合理配置线程池参数 ### 3. 运维友好 * 使用占位符引用其他配置项 * 为不同环境准备不同的配置文件 * 配置监控和健康检查端点 > 💡 **提示**: 在生产环境部署前,请仔细检查所有配置项,特别是数据库连接、Redis密码、各种密钥等敏感信息。 --- --- url: 'https://ruoyi.plus/backend.md' --- # 后端项目简介 Ruoyi-Plus-Uniapp 后端是基于Ruoyi-Vue-Plus5.X进行深度重构,提供了查询增强组件、统一响应封装、完善的多租户 SaaS 支持、权限安全管理、小程序集成(微信、QQ、支付宝等)、公众号集成、OSS 文件管理与直传、IJPay 支付集成、主子表代码生成、国际化增强支持、数据库规范化、序列化增强等企业级功能,采用统一命名规范和完善注释减少样板代码,支持 Docker 容器化部署和远程调试,注重开发体验和代码可维护性。 ## 业务功能 | 功能模块 | 功能介绍 | |----------|----------------------------------------------| | **租户管理** | 配置系统租户,支持 SaaS 场景下的多租户功能,可以配置租户套餐 限制人数 使用时间等 | | **用户管理** | 用户是系统操作者,该功能主要完成系统用户配置 | | **部门管理** | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 | | **岗位管理** | 配置系统用户所属担任职务 | | **菜单管理** | 配置系统菜单,操作权限,按钮权限标识等 | | **角色管理** | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 | | **字典管理** | 对系统中经常使用的一些较为固定的数据进行维护 | | **参数管理** | 对系统动态配置常用参数 | | **通知公告** | 系统通知公告信息发布维护 实现精准对不同用户发布通知 | | **操作日志** | 系统正常操作日志记录和查询;系统异常信息日志记录和查询 | | **登录日志** | 系统登录日志记录查询包含登录异常 | | **文件管理** | 系统文件目录管理 上传、下载等管理以及上传配置 | | **任务调度** | 在线(添加、修改、删除)任务调度包含执行结果日志 | | **代码生成** | 前后端代码的生成(java、html、xml、sql)支持单表CRUD/主子表 | | **系统接口** | 根据业务代码自动生成相关的api接口文档 | | **服务监控** | 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等 | | **缓存监控** | 对系统的缓存信息查询,命令统计等 | | **商品管理** | 实现基础的商品管理功能 | | **订单管理** | 实现基础的订单管理 实现基础的支付功能以及统一支付 退款 回调等处理 | | **账号绑定** | 实现移动端用户的绑定功能 | | **平台配置** | 实现移动端平台的配置管理 支持租户配置隔离 | | **支付配置** | 实现支付配置管理 支持租户配置隔离 | | **广告配置** | 广告配置可实现多种增强效果,小程序跳转,小程序流量主广告,移动端不同页面轮播图等 | ## RuoYi 系列框架完整对比表 ## 基础信息对比 | 对比项 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|-----------|----------------|---------------------| | **维护组织** | 若依官方 | Dromara 开源组织 | 抓蛙师/若依工作室 | | **项目定位** | 经典快速开发框架 | 多租户权限管理系统 | 全栈统一开发平台 | | **核心理念** | 快速开发 | 多租户+现代化 | 代码即文档+全栈统一+开发友好 | | **兼容性** | 基于原版扩展 | 重写不兼容原框架 | 基于Plus深度重构 | | **开源协议** | MIT | MIT | 闭源 需要授权 | | **架构特色** | 单体架构 | 集群/微服务可选 | 全栈统一架构(集群) | | **更新频率** | 较少更新 | 稳定更新 | 持续优化更新 | ## 技术栈对比 | 技术栈 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |-----------------|-------------------------------|----------------------------|----------------------------------| | **后端** | Spring Boot + Spring Security | Spring Boot + Sa-Token | Spring Boot + Sa-Token | | **Spring Boot** | 2.7.x | 3.2.x | 3.2.x | | **Java** | JDK 8+ | JDK 17+ | JDK 21+ | | **ORM** | MyBatis | MyBatis-Plus | MyBatis-Plus增强 | | **缓存** | Redis | Redis + Redisson | Redis + Redisson | | **前端** | Vue 2 + Element UI | Vue 3 + Element Plus | Vue 3 + Element Plus重构 | | **移动端** | 无 | 无 | UniApp深度重构(基于unibest) | | **工作流** | 无 | WarmFlow | 两个分支 主分支移除工作流,工作流WarmFlow由另外分支维护 | | **文档** | Swagger | SpringDoc + apifox | SpringDoc + apifox | | **认证** | JWT | Sa-Token | Sa-Token | | **数据库** | MySQL | MySQL/PostgreSQL/Oracle/达梦 | MySQL/PostgreSQL/Oracle/达梦 | ## 核心架构重构对比 ### 后端架构重构 | 重构项目 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|---------------|----------------|-----------------------------------------------------------------------| | **基础架构** | 传统架构 | 现代化架构 | ✅ 面向未来的现代化架构 | | **查询增强** | MyBatis | MyBatis-Plus | ✅ PlusQuery+PlusLambdaQuery聚合查询 IBaseService接口+BaseServiceImpl减少样板代码 | | **响应封装** | TableDataInfo | PageResult | ✅ `R>`统一响应 | | **应用隔离** | 无 | 无 | ✅ 项目唯一标识符 应用ID隔离 | | **字典系统** | 基础字典 | 基础字典 | ✅增强字典 Dict开头枚举+1是0否统一规范 | | **租户系统** | 无 | 基础多租户 | ✅ 兜底租户ID+OSS目录租户隔离 | ### 前端架构重构 | 重构项目 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|-----------|----------------|--------------------------------| | **目录结构** | Vue2规范 | Vue3规范 | ✅ composables+layouts+stores统一 | | **组件命名** | 传统命名 | 现代命名 | ✅ 首字母大写驼峰+语义化 | | **类型系统** | 基础类型 | TypeScript | ✅ `R`+`PageResult`统一类型 | | **工具重构** | utils工具类 | 部分重构 | ✅ 完全重构为Composables组合函数 | | **样式系统** | 传统样式 | UnoCSS 现代样式 | ✅ UnoCSS配置增强+完善备注 | | **图标系统** | 基础图标/svg | 丰富图标/svg | ✅ Iconify图标库+400+图标重构 | ## 核心功能对比 ### 基础功能模块 | 功能模块 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|-----------|----------------|-----------------------| | **用户管理** | 基础功能 | 基础功能 | ✅ 基础功能+移动端用户绑定 | | **角色管理** | 基础权限 | 基础权限 | ✅ 基础权限+租户角色同步功能 | | **菜单管理** | 树形菜单 | 树形菜单 | ✅ 树形菜单+国际化菜单+国际化键自动生成 | | **部门管理** | 树形结构 | 多租户部门 | ✅ 多租户部门 | | **字典管理** | 基础字典 | 多租户字典 | ✅ 多租户字典+Dict枚举+通用转换器 | | **通知公告** | 基础通知 | 基础通知+在线推送 | ✅ 精准推送/离线留存+已读未读统计 | ### 高级功能模块 | 功能模块 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |-----------|-----------|----------------|----------------------| | **多租户** | ❌ 不支持 | ✅ 完整支持 | ✅ SaaS多租户+数据隔离增强 | | **数据权限** | ✅ 基础支持 | ✅ 增强支持 | ✅ 增强支持 | | **代码生成** | ✅ 基础生成 | ✅ 基础生成 | ✅ 主子表+默认值+权限生成 | | **文件存储** | ✅ 本地存储 | ✅ OSS多云存储 | ✅ OSS策略模式+S3+本地+前端直传 | | **支付功能** | ❌ 无 | ✅ ❌ 无 | ✅ IJPay+租户隔离+自动重试 | | **短信服务** | ❌ 无 | ✅ 多厂商支持 | ✅ 多厂商支持 | | **小程序支持** | ❌ 无 | ❌ 无 | ✅ 微信+QQ+支付宝+抖音全平台 | | **序列化增强** | ❌ 无 | 基础转换+手动逐个实现 | ✅ SerialMap注解+字段映射 | ### 移动端功能对比 | 移动端功能 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|-----------|----------------|-------------------------| | **H5支持** | ❌ 无 | ❌ 无 | ✅ 响应式增强 | | **小程序** | ❌ 无 | ❌ 无 | ✅ 微信+QQ+支付宝+京东+抖音 | | **APP** | ❌ 无 | ❌ 无 | ✅ UniApp深度重构(基于unibest) | | **组件库** | ❌ 无 | ❌ 无 | ✅ WotUI完全重构+380+图标 | | **登录认证** | ❌ 无 | ❌ 无 | ✅ 多平台登录+unionid绑定+自动注册 | | **支付集成** | ❌ 无 | ❌ 无 | ✅ usePayment组合函数+完整支付流程 | | **示例代码** | ❌ 无 | ❌ 无 | ✅ 完整组件示例+代码查看复制 | ## 开发体验对比 ### 代码质量与规范 | 开发特性 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|-----------|----------------|---------------------| | **代码注释** | 基础注释 | 基础注释 | ✅ 完整Javadoc规范+代码即文档 | | **命名规范** | 基础规范 | 改进规范 | ✅ 全栈统一命名+语义化+唯一性 | | **类型安全** | 基础类型 | TypeScript | ✅ 全栈类型统一+智能提示 | | **样板代码** | 较多重复 | 部分优化 | ✅ 极致减少样板代码+泛型适配 | | **开发友好** | 基础友好 | 较友好 | ✅ 开发体验优先+可维护性强 | ### 工具与组合函数函数 | 工具类型 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|-----------|-------------------|----------------------------------------| | **工具函数** | utils工具类 | 增强工具类 | ✅ Composables组合函数重构+utils增强工具类 | | **请求封装** | axios封装 | 增强封装+加密解密 | ✅ useHttp组合函数+加密解密 | | **权限控制** | 基础权限 | 增强权限 | ✅ useAuth组合函数+延迟加载组件 | | **主题切换** | 基础主题 | 主题支持 | ✅ useTheme组合函数 | | **国际化** | 基础i18n | 增强i18n | ✅ useI18n组合函数+智能提示+后端国际化 | | **表格增强** | 基础表格 | el-table+vxetable | ✅ el-table+useSelection+useTableHeight | | **文件下载** | 基础下载 | 基础下载 | ✅ useDownload组合函数 | | **动画效果** | 基础动画 | CSS动画 | ✅ useAnimation组合函数 | ## 组件系统对比 ### 前端组件 | 组件类型 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|------------|-------------------|---------------------------------------------| | **表单组件** | Element UI | Element Plus | ✅ A系列表单组件全套重构 | | **上传组件** | 基础上传 | 基础上传 | ✅ AFormFileUpload+AFormImgUpload +素材管理+前端直传 | | **编辑器** | Quill | Quill | ✅ AFormEditor(基于tiptap的umo)增强富文本 | | **媒体库** | ❌ 无 | 基础文件管理 | ✅ AOssMediaManager+目录管理+批量操作 | | **权限指令** | 基础指令 | 基础指令 | ✅ permi+role+admin等完整增强指令 | | **图标组件** | 基础图标 | 基础图标 + iconify图标库 | ✅ Icon组件+类型提示+iconify海量图标库 | ### 移动端组件 | 组件类型 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |--------------|-----------|----------------|-----------------------------| | **UI组件库** | ❌ 无 | ❌ 无 | ✅ WotUI完全重构+Vue3+TypeScript | | **图标组件** | ❌ 无 | ❌ 无 | ✅ wd-iconify+wd-icon+400+图标 | | **分页组件** | ❌ 无 | ❌ 无 | ✅ wd-paging下滑分页加载 | | **tabbar组件** | ❌ 无 | ❌ 无 | ✅ 自定义tabbar组件+灵活控制 | ## 国际化系统对比 | 国际化功能 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |-----------|-----------|----------------|-----------------------------------| | **前端国际化** | 基础支持 | 增强支持 | ✅ useI18n组合函数+智能提示+菜单国际化+按钮+消息国际化 | | **后端国际化** | ❌ 无 | 基础支持 | ✅ I18nMessageInterceptor+接口常量管理 | | **消息国际化** | 前端处理 | 前端处理 | ✅ 后端返回国际化+前端兜底 | | **键名管理** | 硬编码 | 部分优化 | ✅ 统一键名计算+去除硬编码 | ## 部署运维对比 | 运维特性 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |--------------|-----------|----------------|----------------------| | **Docker支持** | 基础 | ✅ 完善配置 | ✅ 优化编排+远程调试 | | **容器化部署** | 基础支持 | 完善支持 | ✅ 完美适配Docker容器化 | | **监控告警** | 基础监控 | monitorAdmin | ✅ monitorAdmin通知功能增强 | ## 学习使用对比 | 对比维度 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |-----------|-----------|----------------|---------------------| | **学习难度** | ⭐⭐⭐ 简单 | ⭐⭐⭐⭐ 中等 | ⭐⭐⭐ 简单 | | **文档完整度** | ⭐⭐⭐⭐ 比较完整 | ⭐⭐⭐⭐ 比较完整 | ⭐⭐⭐⭐ 文档完善中+代码即文档 | | **代码可读性** | ⭐⭐⭐ 一般 | ⭐⭐⭐⭐ 较好 | ⭐⭐⭐⭐⭐ 极佳 | | **上手速度** | ⭐⭐⭐⭐ 很快 | ⭐⭐⭐⭐ 一般 | ⭐⭐⭐⭐⭐ 极快 | | **二次开发** | ⭐⭐⭐⭐⭐ 容易 | ⭐⭐⭐⭐ 中等 | ⭐⭐⭐⭐⭐ 非常友好 | | **维护性** | ⭐⭐⭐ 一般 | ⭐⭐⭐⭐ 较好 | ⭐⭐⭐⭐⭐ 极佳 | ## 性能与扩展性对比 | 性能指标 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |-----------|-----------|----------------|---------------------------| | **后端性能** | 一般 | 较好 | 较强 | | **前端性能** | Vue2性能 | Vue3性能 | Vue3性能+Vite优化 | | **移动端性能** | ❌ 无 | ❌ 无 | ✅ 分包加载+优化渲染 | | **缓存策略** | Redis单机 | Redis集群+本地缓存 | Redis集群+本地缓存 | | **查询优化** | MyBatis | MyBatis-Plus | PlusQuery聚合查询优化 | | **实时通信** | ❌ 无 | WebSocket/SSE | ✅ WebSocket/SSE 重连退避+状态监控 | ## 安全特性对比 | 安全功能 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|-----------|----------------|---------------------| | **身份认证** | JWT | Sa-Token | Sa-Token增强 | | **数据加密** | ❌ 无 | 前后端加密 | 前后端加密+移动端加密 | | **数据脱敏** | ❌ 无 | ✅ 完整支持 | ✅ 完整支持 | | **租户隔离** | ❌ 无 | 租户隔离 | 完整租户隔离+OSS隔离 | | **权限控制** | RBAC | RBAC+细粒度 | RBAC+权限指令增强 | ## 许可证与生态对比 | 项目 | RuoYi-Vue | RuoYi-Vue-Plus | ⚡ RuoYi-Plus-UniApp | |----------|------------|----------------|---------------------| | **开源许可** | Apache 2.0 | MIT | 闭源需要授权 | | **商业使用** | 完全免费 | 完全免费 | 授权后可商用 | | **技术支持** | 社区免费+付费课程 | 社区+付费课程 | 专业技术支持 | | **定制开发** | 第三方服务 | 第三方+官方 | 官方定制服务 | | **源码获取** | 完全开源 | 完全开源 | 授权后提供 | | **商业授权** | 不需要 | 不需要 | 授权后可交付客户源码 | ## 适用场景建议 ### 🎯 RuoYi-Vue 适用场景 | 场景类型 | 推荐理由 | |------------|-----------------| | **新手学习项目** | 文档详细完整、学习成本低 | | **简单管理系统** | 功能完整、维护稳定、不易扩展 | | **传统项目维护** | 技术栈稳定、兼容性好、很少更新 | ### 🚀 RuoYi-Vue-Plus 适用场景 | 场景类型 | 推荐理由 | |---------------|-----------------------------| | **多租户SaaS平台** | 多租户完善、权限隔离、数据隔离 | | **现代化企业应用** | Vue3+Spring Boot3、技术先进、性能优秀 | | **中大型项目** | 功能丰富、架构清晰、扩展性强 | | **微服务架构** | 支持分布式、云原生、容器化 | ### 🌟 RuoYi-Plus-UniApp 适用场景 | 场景类型 | 推荐理由 | |----------------|-----------------------| | **全栈统一开发** | 前后端移动端统一技术栈、开发效率高 | | **多端业务平台** | Web+小程序+APP一体化解决方案 | | **高质量代码要求** | 代码即文档、极致减少样板代码、可维护性极强 | | **快速迭代项目** | 组合式API、组件化开发、开发体验优秀 | | **SaaS多租户平台** | 完整租户隔离、多平台小程序、支付集成 | | **企业级商业项目** | 专业技术支持、定制开发服务、商业授权保障 | | **对安全性要求高的项目** | 闭源保护、授权控制、专业维护 | ## 💡 总结与选择建议 ### 技术发展趋势 * **RuoYi-Vue**: 稳定成熟的经典方案 * **RuoYi-Vue-Plus**: 现代化的技术升级 * **RuoYi-Plus-UniApp**: 面向未来的全栈统一方案 ### 选择决策因子 #### 按预算和授权选择 * **预算有限**: RuoYi-Vue-Plus(完全免费开源) * **商业项目**: RuoYi-Plus-UniApp(专业授权+技术支持) #### 按开源需求选择 * **需要开源**: RuoYi-Vue 或 RuoYi-Vue-Plus * **商业保护**: RuoYi-Plus-UniApp(闭源授权) * **定制需求**: RuoYi-Plus-UniApp(官方定制服务) * **只需Web端**: RuoYi-Vue 或 RuoYi-Vue-Plus 或 RuoYi-Plus-UniApp * **需要移动端**: RuoYi-Plus-UniApp * **多租户需求**: RuoYi-Vue-Plus 或 RuoYi-Plus-UniApp * **全平台覆盖**: RuoYi-Plus-UniApp #### 按项目需求选择 * **传统开发团队**: RuoYi-Vue * **现代化团队**: RuoYi-Vue-Plus * **全栈团队**: RuoYi-Plus-UniApp #### 按开发体验选择 * **快速上手**: RuoYi-Vue * **现代开发**: RuoYi-Vue-Plus * **极致体验**: RuoYi-Plus-UniApp RuoYi-Plus-UniApp 在保持 RuoYi-Vue-Plus 所有优势的基础上,通过深度重构实现了"代码即文档、全栈统一、开发友好" 的核心理念,是面向未来的全栈统一开发平台。\*\*作为闭源商业框架,提供专业的技术支持和定制服务,适合对代码安全性和技术支持有高要求的商业项目。 \*\* --- --- url: 'https://ruoyi.plus/mobile/performance/startup.md' --- # 启动性能优化 --- --- url: 'https://ruoyi.plus/practices/naming-conventions.md' --- # 命名规范 --- --- url: 'https://ruoyi.plus/frontend/layout/responsive.md' --- # 响应式布局 --- --- url: 'https://ruoyi.plus/frontend/styles/responsive.md' --- # 响应式设计 --- --- url: 'https://ruoyi.plus/mobile/styles/responsive.md' --- # 响应式设计 --- --- url: 'https://ruoyi.plus/frontend/i18n/i18n-practices.md' --- # 国际化最佳实践 --- --- url: 'https://ruoyi.plus/mobile/styles/icon-fonts.md' --- # 图标字体 --- --- url: 'https://ruoyi.plus/frontend/components/icon-system.md' --- # 图标系统 --- --- url: 'https://ruoyi.plus/mobile/components/icons.md' --- # 图标组件 --- --- url: 'https://ruoyi.plus/mobile/performance/image-optimization.md' --- # 图片优化 --- --- url: 'https://ruoyi.plus/mobile/utils/image.md' --- # 图片处理工具 --- --- url: 'https://ruoyi.plus/mobile/plugins/map.md' --- # 地图插件 --- --- url: 'https://ruoyi.plus/mobile/components/basic.md' --- # 基础组件 --- --- url: 'https://ruoyi.plus/practices/backup-strategy.md' --- # 备份策略 --- --- url: 'https://ruoyi.plus/frontend/utils/copy.md' --- # 复制工具 --- --- url: 'https://ruoyi.plus/frontend/directives/copy.md' --- # 复制指令 (v-copy) --- --- url: 'https://ruoyi.plus/backend/common/tenant.md' --- # 多租户 (tenant) ## 概述 租户插件模块(`ruoyi-common-tenant`)是一个完整的多租户解决方案,提供了数据库、缓存、认证等多个层面的租户隔离功能。该模块确保不同租户的数据完全隔离,避免数据泄露和混淆。 ## 核心特性 * **数据库隔离**:自动为SQL查询添加租户条件 * **缓存隔离**:Redis缓存和Spring Cache支持租户前缀 * **认证隔离**:SaToken认证数据支持租户隔离 * **动态租户**:支持运行时切换租户上下文 * **忽略机制**:支持跳过租户过滤的场景 * **配置灵活**:可配置排除表和开关控制 ## 模块结构 ```text ruoyi-common-tenant/ ├── config/ # 配置类 │ └── TenantConfig.java ├── core/ # 核心组件 │ ├── TenantEntity.java │ └── TenantSaTokenDao.java ├── exception/ # 异常类 │ └── TenantException.java ├── handle/ # 处理器 │ ├── PlusTenantLineHandler.java │ └── TenantKeyPrefixHandler.java ├── helper/ # 工具类 │ └── TenantHelper.java ├── manager/ # 管理器 │ └── TenantSpringCacheManager.java └── properties/ # 配置属性 └── TenantProperties.java ``` ## 快速开始 ### 1. 添加依赖 在 `pom.xml` 中添加依赖: ```xml plus.ruoyi ruoyi-common-tenant ``` ### 2. 配置文件 在 `application.yml` 中添加配置: ```yaml # 多租户配置 tenant: # 是否启用多租户功能 enable: true # 排除表列表(这些表不会应用租户过滤) excludes: - sys_user - sys_role - sys_config ``` ### 3. 实体类配置 继承 `TenantEntity` 类: ```java @Data @EqualsAndHashCode(callSuper = true) @TableName("your_table") public class YourEntity extends TenantEntity { // 你的字段 private String name; private String description; } ``` ## 核心组件 ### TenantHelper 租户助手工具类,提供租户相关的核心操作: #### 基础功能 ```java // 检查多租户功能是否启用 boolean enabled = TenantHelper.isEnable(); // 获取当前租户ID String tenantId = TenantHelper.getTenantId(); ``` #### 忽略租户模式 在某些场景下需要跳过租户过滤: ```java // 在忽略模式下执行代码 TenantHelper.ignore(() -> { // 这里的数据库操作不会添加租户条件 userService.getAllUsers(); }); // 带返回值的忽略模式 List allUsers = TenantHelper.ignore(() -> { return userService.getAllUsers(); }); // 手动控制忽略模式 TenantHelper.enableIgnore(); try { // 业务代码 } finally { TenantHelper.disableIgnore(); } ``` #### 动态租户切换 运行时切换到指定租户: ```java // 临时切换到指定租户执行代码 TenantHelper.dynamic("tenant123", () -> { // 这里的操作都在tenant123租户下执行 dataService.queryData(); }); // 带返回值的动态租户切换 List data = TenantHelper.dynamic("tenant123", () -> { return dataService.queryData(); }); // 手动设置动态租户 TenantHelper.setDynamic("tenant123"); try { // 业务代码 } finally { TenantHelper.clearDynamic(); } // 全局设置动态租户(存储到Redis) TenantHelper.setDynamic("tenant123", true); ``` ### TenantEntity 多租户实体基类: ```java @Data @EqualsAndHashCode(callSuper = true) public class TenantEntity extends BaseEntity { /** * 租户ID - 自动管理,无需手动设置 */ private String tenantId; } ``` ### 数据库隔离 #### PlusTenantLineHandler 自动为SQL添加租户条件: ```java // 原始SQL SELECT * FROM user WHERE status = 1 // 自动添加租户条件后 SELECT * FROM user WHERE status = 1 AND tenant_id = 'current_tenant_id' ``` #### 排除表配置 某些系统表不需要租户隔离: ```java // 系统固定排除表(硬编码) - sys_tenant - sys_tenant_package - sys_gen_table - sys_gen_table_column - sys_menu - sys_role_menu - sys_role_dept - sys_user_role - sys_user_post - sys_oss_config // 配置文件排除表 tenant: excludes: - your_global_table - another_global_table ``` ### 缓存隔离 #### Redis缓存隔离 ```java // 原始Redis键 user:info:123 // 自动添加租户前缀 tenant123:user:info:123 ``` #### Spring Cache隔离 ```java @Cacheable(value = "userCache", key = "#userId") public User getUser(String userId) { // 缓存键自动添加租户前缀 // 实际缓存键:tenant123:userCache::userId return userRepository.findById(userId); } ``` ### 认证隔离 SaToken认证数据自动添加全局前缀,确保不同租户的认证信息隔离。 ## 配置说明 ### TenantProperties ```java @ConfigurationProperties(prefix = "tenant") public class TenantProperties { /** * 是否启用多租户功能 */ private Boolean enable; /** * 多租户排除表列表 */ private List excludes; } ``` ### 配置示例 ```yaml tenant: # 启用多租户 enable: true # 排除表配置 excludes: - sys_config # 系统配置表 - sys_dict_type # 字典类型表 - sys_dict_data # 字典数据表 - sys_notice # 通知公告表(如果是全局通知) ``` ## 使用场景 ### 1. SaaS应用 ```java @Service public class OrderService { @Autowired private OrderMapper orderMapper; // 查询当前租户的订单 - 自动添加租户条件 public List getCurrentTenantOrders() { return orderMapper.selectList(null); } // 管理员查看所有租户的订单 public List getAllTenantsOrders() { return TenantHelper.ignore(() -> { return orderMapper.selectList(null); }); } // 数据迁移:将数据从一个租户转移到另一个租户 public void migrateData(String fromTenant, String toTenant) { List orders = TenantHelper.dynamic(fromTenant, () -> { return orderMapper.selectList(null); }); TenantHelper.dynamic(toTenant, () -> { orders.forEach(order -> { order.setId(null); // 清除ID,插入新记录 orderMapper.insert(order); }); }); } } ``` ### 2. 多租户报表 ```java @Service public class ReportService { // 生成当前租户报表 @Cacheable(value = "report", key = "#type") public Report generateReport(String type) { // 缓存键自动添加租户前缀 return reportGenerator.generate(type); } // 生成全局报表(所有租户) public Report generateGlobalReport(String type) { return TenantHelper.ignore(() -> { return reportGenerator.generateGlobal(type); }); } } ``` ### 3. 系统管理功能 ```java @Service public class SystemService { // 系统配置管理(全局,不受租户影响) public void updateSystemConfig(String key, String value) { TenantHelper.ignore(() -> { configMapper.updateByKey(key, value); }); } // 租户特定配置 public void updateTenantConfig(String key, String value) { // 自动应用租户条件 tenantConfigMapper.updateByKey(key, value); } } ``` ## 最佳实践 ### 1. 数据库设计 ```sql -- 租户表需要包含tenant_id字段 CREATE TABLE user_info ( id BIGINT PRIMARY KEY, tenant_id VARCHAR(20) NOT NULL, username VARCHAR(50) NOT NULL, email VARCHAR(100), create_time DATETIME, INDEX idx_tenant_id (tenant_id) ); -- 全局表不包含tenant_id字段 CREATE TABLE sys_config ( id BIGINT PRIMARY KEY, config_key VARCHAR(100) NOT NULL, config_value TEXT, create_time DATETIME ); ``` ### 2. 实体类设计 ```java // 租户相关实体 @TableName("user_info") public class UserInfo extends TenantEntity { private String username; private String email; // 不需要手动设置tenantId } // 全局实体 @TableName("sys_config") public class SysConfig extends BaseEntity { private String configKey; private String configValue; // 不继承TenantEntity } ``` ### 3. 服务层设计 ```java @Service public class UserService { // 常规业务方法 - 自动应用租户过滤 public List getUserList() { return userMapper.selectList(null); } // 系统管理方法 - 需要忽略租户过滤 public List getAllUsersForAdmin() { return TenantHelper.ignore(() -> { return userMapper.selectList(null); }); } // 跨租户操作 - 使用动态租户 public void syncUserData(String sourceTenant, String targetTenant) { List users = TenantHelper.dynamic(sourceTenant, () -> { return userMapper.selectList(null); }); TenantHelper.dynamic(targetTenant, () -> { users.forEach(user -> { user.setId(null); userMapper.insert(user); }); }); } } ``` ### 4. 缓存使用 ```java @Service public class CacheService { // 租户级缓存 @Cacheable(value = "userData", key = "#userId") public UserData getUserData(String userId) { // 缓存键:tenantId:userData::userId return userDataMapper.selectById(userId); } // 全局缓存 @Cacheable(value = "global:systemData", key = "#key") public SystemData getSystemData(String key) { return TenantHelper.ignore(() -> { // 缓存键:global:systemData::key(不添加租户前缀) return systemDataMapper.selectByKey(key); }); } } ``` ## 注意事项 ### 1. 数据一致性 * 所有租户相关的表都必须包含 `tenant_id` 字段 * 建议为 `tenant_id` 字段创建索引以提高查询性能 * 数据库迁移时要注意租户数据的完整性 ### 2. 性能考虑 * 合理使用缓存来减少数据库查询 * 大量数据查询时考虑分页处理 * 监控SQL执行计划,确保租户条件使用了索引 ### 3. 安全注意 * 严格控制 `TenantHelper.ignore()` 的使用范围 * 跨租户操作需要有适当的权限控制 * 定期审计租户数据访问日志 ### 4. 开发规范 * 新增实体类时明确是否需要继承 `TenantEntity` * 系统级功能要明确是否需要忽略租户过滤 * 测试时要验证租户隔离的有效性 ## 故障排除 ### 常见问题 1. **数据查询不到** * 检查是否正确设置了租户ID * 确认表是否包含 `tenant_id` 字段 * 验证表是否被错误地加入了排除列表 2. **缓存数据错乱** * 检查缓存键是否正确添加了租户前缀 * 确认全局缓存是否正确使用了 `global:` 前缀 3. **认证异常** * 检查SaToken配置是否正确 * 确认多租户SaTokenDao是否正常工作 ### 调试方法 启用SQL日志查看实际执行的SQL: ```yaml logging: level: com.baomidou.mybatisplus: DEBUG ``` 查看租户ID获取过程: ```java String tenantId = TenantHelper.getTenantId(); log.info("当前租户ID: {}", tenantId); ``` --- --- url: 'https://ruoyi.plus/frontend/components/oss-media-manager.md' --- # 媒体库组件 (AOssMediaManager) --- --- url: 'https://ruoyi.plus/backend/common/core/enums.md' --- # 字典枚举 (Dictionary & Enum System) ## 概述 字典枚举系统是ruoyi-plus-uniapp中的一个重要设计模式,用于管理系统中的各种状态值、类型码和选项数据。该系统通过枚举类的方式提供类型安全、易于维护的字典数据管理,同时支持前端组件的数据绑定和国际化。 ## 设计理念 ### 核心原则 * **类型安全**: 使用枚举替代魔法字符串和数字 * **统一规范**: 所有字典都遵循相同的设计模式 * **易于维护**: 集中管理,便于修改和扩展 * **前端友好**: 提供标准的标签-值对结构 * **国际化支持**: 配合国际化系统使用 ### 设计模式 所有字典枚举都遵循统一的设计模式: ```java @Getter @AllArgsConstructor public enum DictXxx { ITEM1("value1", "标签1"), ITEM2("value2", "标签2"); public static final String DICT_TYPE = "dict_type_code"; private final String value; private final String label; // 标准方法 public static DictXxx getByValue(String value) { } public static DictXxx getByLabel(String label) { } public static boolean isXxx(String value) { } } ``` ## 系统内置字典 ### 1. 用户相关字典 #### 用户性别 (DictUserGender) ```java @Getter @AllArgsConstructor public enum DictUserGender { FEMALE("0", "女"), MALE("1", "男"), UNKNOWN("2", "未知"); public static final String DICT_TYPE = "sys_user_gender"; private final String value; private final String label; } ``` **使用场景:** * 用户注册/编辑性别选择 * 用户列表性别显示 * 统计报表性别分组 #### 用户类型 (UserType) ```java @Getter @AllArgsConstructor public enum UserType { PC_USER("pc", "pc_user", 43200, 604800), MOBILE_USER("mobile", "mobile_user", 172800, 2592000); private final String deviceType; private final String userType; private final int activeTimeout; private final int timeout; } ``` **特点:** * 针对不同设备类型的用户体系 * 包含会话超时配置 * 支持多端登录管理 ### 2. 状态相关字典 #### 启用状态 (DictEnableStatus) ```java @Getter @AllArgsConstructor public enum DictEnableStatus { ENABLE("1", "启用"), DISABLED("0", "禁用"); public static final String DICT_TYPE = "sys_enable_status"; // 便捷方法 public static boolean isEnabled(String value) { return ENABLE.getValue().equals(value); } public static boolean isDisabled(String value) { return DISABLED.getValue().equals(value); } } ``` #### 显示设置 (DictDisplaySetting) ```java @Getter @AllArgsConstructor public enum DictDisplaySetting { SHOW("1", "显示"), HIDE("0", "隐藏"); public static boolean isShow(String value) { return SHOW.getValue().equals(value); } public static boolean isHide(String value) { return HIDE.getValue().equals(value); } } ``` #### 逻辑标志 (DictBooleanFlag) ```java @Getter @AllArgsConstructor public enum DictBooleanFlag { YES("1", "是"), NO("0", "否"); public static boolean isYes(String value) { return YES.getValue().equals(value); } public static boolean isNo(String value) { return NO.getValue().equals(value); } } ``` ### 3. 业务流程字典 #### 审核状态 (DictAuditStatus) ```java @Getter @AllArgsConstructor public enum DictAuditStatus { PENDING("0", "待审核"), APPROVED("1", "通过"), REJECTED("2", "驳回"), DENIED("3", "拒绝"); public static final String DICT_TYPE = "sys_audit_status"; public static boolean isApproved(String value) { return APPROVED.getValue().equals(value); } public static boolean isPending(String value) { return PENDING.getValue().equals(value); } } ``` #### 订单状态 (DictOrderStatus) ```java @Getter @AllArgsConstructor public enum DictOrderStatus { PENDING("pending", "待支付"), PAID("paid", "已支付"), DELIVERED("delivered", "已发货"), COMPLETED("completed", "已完成"), CANCELLED("cancelled", "已取消"), REFUNDED("refunded", "已退款"); // 业务逻辑方法 public boolean canPay() { return this == PENDING; } public boolean canCancel() { return this == PENDING; } public boolean canDeliver() { return this == PAID; } public boolean canComplete() { return this == DELIVERED; } public boolean canRefund() { return this == PAID || this == DELIVERED; } public boolean isFinished() { return this == COMPLETED || this == CANCELLED || this == REFUNDED; } } ``` ### 4. 支付相关字典 #### 支付方式 (DictPaymentMethod) ```java @Getter @AllArgsConstructor public enum DictPaymentMethod { WECHAT("wechat", "微信支付"), ALIPAY("alipay", "支付宝"), UNIONPAY("unionpay", "银联"), BALANCE("balance", "余额支付"), POINTS("points", "积分抵扣"); public static final String DICT_TYPE = "sys_payment_method"; public static boolean isOnlinePayment(String value) { return WECHAT.getValue().equals(value) || ALIPAY.getValue().equals(value) || UNIONPAY.getValue().equals(value); } } ``` ### 5. 平台相关字典 #### 平台类型 (DictPlatformType) ```java @Getter @AllArgsConstructor public enum DictPlatformType { MP_WEIXIN("mp-weixin", "微信小程序"), MP_OFFICIAL_ACCOUNT("mp-official-account", "微信公众号"), MP_QQ("mp-qq", "QQ小程序"), MP_ALIPAY("mp-alipay", "支付宝小程序"), MP_JD("mp-jd", "京东小程序"), MP_KUAISHOU("mp-kuaishou", "快手小程序"), MP_LARK("mp-lark", "飞书小程序"), MP_BAIDU("mp-baidu", "百度小程序"), MP_TOUTIAO("mp-toutiao", "头条小程序"), MP_XHS("mp-xhs", "小红书小程序"); public static boolean isWechatPlatform(String value) { return MP_WEIXIN.getValue().equals(value) || MP_OFFICIAL_ACCOUNT.getValue().equals(value); } } ``` #### 消息类型 (DictMessageType) ```java @Getter @AllArgsConstructor public enum DictMessageType { SYSTEM("system", "系统通知"), ACTIVITY("activity", "活动通知"), AUDIT("audit", "审核通知"), ACCOUNT("account", "账户通知"), PRIVATE("private", "私信"); public static boolean isSystemLevel(String value) { return SYSTEM.getValue().equals(value) || AUDIT.getValue().equals(value) || ACCOUNT.getValue().equals(value); } } ``` ### 6. 系统操作字典 #### 操作类型 (DictOperType) ```java @Getter @AllArgsConstructor public enum DictOperType { INSERT("1", "新增"), UPDATE("2", "修改"), DELETE("3", "删除"), GRANT("4", "授权"), EXPORT("5", "导出"), IMPORT("6", "导入"), FORCE("7", "强退"), GENCODE("8", "生成代码"), CLEAN("9", "清空数据"), OTHER("99", "其他"); public static final String DICT_TYPE = "sys_oper_type"; } ``` #### 操作结果 (DictOperResult) ```java @Getter @AllArgsConstructor public enum DictOperResult { SUCCESS("1", "成功"), FAIL("0", "失败"); public static final String DICT_TYPE = "sys_oper_result"; } ``` ### 7. 文件相关字典 #### 文件类型 (DictFileType) ```java @Getter @AllArgsConstructor public enum DictFileType { IMAGE("image", "图片"), DOCUMENT("document", "文档"), VIDEO("video", "视频"), AUDIO("audio", "音频"), ARCHIVE("archive", "压缩包"), OTHER("other", "其他"); public static boolean isMediaFile(String value) { return IMAGE.getValue().equals(value) || VIDEO.getValue().equals(value) || AUDIO.getValue().equals(value); } } ``` ### 8. 通知相关字典 #### 通知类型 (DictNoticeType) ```java @Getter @AllArgsConstructor public enum DictNoticeType { NOTICE("1", "通知"), ANNOUNCEMENT("2", "公告"); public static boolean isNotice(String value) { return NOTICE.getValue().equals(value); } public static boolean isAnnouncement(String value) { return ANNOUNCEMENT.getValue().equals(value); } } ``` #### 通知状态 (DictNoticeStatus) ```java @Getter @AllArgsConstructor public enum DictNoticeStatus { SEND_IMMEDIATELY("1", "立即发送"), SAVE_AS_DRAFT("0", "保存为草稿"); public static boolean isSendImmediately(String value) { return SEND_IMMEDIATELY.getValue().equals(value); } public static boolean isDraft(String value) { return SAVE_AS_DRAFT.getValue().equals(value); } } ``` ## 认证类型枚举 ### 认证类型 (AuthType) ```java @Getter @AllArgsConstructor public enum AuthType { PASSWORD(I18nKeys.User.PASSWORD_RETRY_LOCKED, I18nKeys.User.PASSWORD_RETRY_COUNT), SMS(I18nKeys.VerifyCode.SMS_RETRY_LOCKED, I18nKeys.VerifyCode.SMS_RETRY_COUNT), EMAIL(I18nKeys.VerifyCode.EMAIL_RETRY_LOCKED, I18nKeys.VerifyCode.EMAIL_RETRY_COUNT), MINIAPP("", ""), MP("", ""), SOCIAL("", ""); private final String retryLimitExceed; private final String retryLimitCount; } ``` **特点:** * 与国际化系统集成 * 包含重试限制配置 * 支持多种认证方式 ## 配合校验使用 ### 字典值校验注解 ```java @DictPattern(dictType = "sys_user_sex", message = "性别字典值无效") private String gender; @DictPattern(dictType = "sys_user_status", separator = ";", message = "状态值无效") private String statusList; ``` ### 枚举值校验注解 ```java @EnumPattern(type = DictUserGender.class, fieldName = "value", message = "性别值无效") private String gender; @EnumPattern(type = DictOrderStatus.class, fieldName = "value") private String orderStatus; ``` ## 前端使用 ### Vue组件绑定 ```vue ``` ### 字典标签组件 ```vue ``` ## 最佳实践 ### 1. 命名规范 ```java // 字典枚举类命名 DictXxx // 通用字典 Dict + 业务域 + 类型名 // 示例 DictUserGender // 用户性别 DictOrderStatus // 订单状态 DictPaymentMethod // 支付方式 ``` ### 2. 结构规范 ```java @Getter @AllArgsConstructor public enum DictExample { // 枚举项定义 ITEM1("value1", "标签1"), ITEM2("value2", "标签2"); // 字典类型常量 public static final String DICT_TYPE = "dict_type_code"; // 基础字段 private final String value; private final String label; // 标准查找方法 public static DictExample getByValue(String value) { for (DictExample item : values()) { if (item.getValue().equals(value)) { return item; } } return DEFAULT_VALUE; // 或抛出异常 } public static DictExample getByLabel(String label) { for (DictExample item : values()) { if (item.getLabel().equals(label)) { return item; } } return DEFAULT_VALUE; } // 业务判断方法 public static boolean isSpecialType(String value) { return ITEM1.getValue().equals(value); } } ``` ### 3. 异常处理策略 #### 策略一:返回默认值 ```java public static DictUserGender getByValue(String value) { for (DictUserGender gender : values()) { if (gender.getValue().equals(value)) { return gender; } } return UNKNOWN; // 返回默认值 } ``` #### 策略二:抛出异常 ```java public static DictPaymentMethod getByValue(String value) { for (DictPaymentMethod method : values()) { if (method.getValue().equals(value)) { return method; } } throw new ServiceException(StringUtils.format("不支持的支付类型:{}", value)); } ``` ### 4. 业务逻辑集成 ```java // 在枚举中包含业务逻辑 public enum DictOrderStatus { // ...枚举定义 // 业务状态判断 public boolean canPay() { return this == PENDING; } // 状态流转验证 public boolean canTransitionTo(DictOrderStatus target) { return switch (this) { case PENDING -> target == PAID || target == CANCELLED; case PAID -> target == DELIVERED || target == REFUNDED; case DELIVERED -> target == COMPLETED; default -> false; }; } } ``` ### 5. 缓存优化 ```java // 使用静态缓存提高查找性能 public enum DictExample { // 枚举定义... private static final Map VALUE_MAP = Arrays.stream(values()) .collect(Collectors.toMap(DictExample::getValue, Function.identity())); public static DictExample getByValue(String value) { return VALUE_MAP.getOrDefault(value, DEFAULT_VALUE); } } ``` ## 扩展字典 ### 1. 添加新字典 ```java @Getter @AllArgsConstructor public enum DictCustomType { TYPE1("1", "类型1"), TYPE2("2", "类型2"); public static final String DICT_TYPE = "custom_type"; private final String value; private final String label; // 实现标准方法... } ``` ### 2. 复杂字典支持 ```java @Getter @AllArgsConstructor public enum DictComplexType { ITEM1("value1", "标签1", "描述1", "#ff0000"), ITEM2("value2", "标签2", "描述2", "#00ff00"); private final String value; private final String label; private final String description; private final String color; // 扩展查找方法 public static List getByColor(String color) { return Arrays.stream(values()) .filter(item -> item.getColor().equals(color)) .collect(Collectors.toList()); } } ``` ### 3. 分组字典 ```java @Getter @AllArgsConstructor public enum DictGroupedType { GROUP1_ITEM1("g1_i1", "组1项1", "group1"), GROUP1_ITEM2("g1_i2", "组1项2", "group1"), GROUP2_ITEM1("g2_i1", "组2项1", "group2"); private final String value; private final String label; private final String group; // 按组查找 public static List getByGroup(String group) { return Arrays.stream(values()) .filter(item -> item.getGroup().equals(group)) .collect(Collectors.toList()); } } ``` ## 国际化支持 ### 1. 标签国际化 ```java @Getter @AllArgsConstructor public enum DictI18nType { ITEM1("value1", "dict.i18n.type.item1"), ITEM2("value2", "dict.i18n.type.item2"); private final String value; private final String labelKey; // 国际化键 public String getLocalizedLabel() { return MessageUtils.message(labelKey); } } ``` ### 2. 配置文件 ```properties # messages_zh_CN.properties dict.i18n.type.item1=项目1 dict.i18n.type.item2=项目2 # messages_en_US.properties dict.i18n.type.item1=Item 1 dict.i18n.type.item2=Item 2 ``` ## 数据库同步 ### 1. 字典数据初始化 ```sql -- 初始化字典类型 INSERT INTO sys_dict_type (dict_type, dict_name, status) VALUES ('sys_user_gender', '用户性别', '0'); -- 初始化字典数据 INSERT INTO sys_dict_data (dict_type, dict_label, dict_value, dict_sort, status) VALUES ('sys_user_gender', '男', '1', 1, '0'), ('sys_user_gender', '女', '0', 2, '0'), ('sys_user_gender', '未知', '2', 3, '0'); ``` ### 2. 同步脚本 ```java @Component public class DictSyncService { public void syncEnumToDatabase() { // 遍历所有字典枚举 for (DictUserGender gender : DictUserGender.values()) { // 同步到数据库 syncDictData(DictUserGender.DICT_TYPE, gender.getValue(), gender.getLabel()); } } } ``` ## 注意事项 ### 1. 性能考虑 * 使用静态缓存Map提高查找性能 * 避免在循环中频繁调用getByValue方法 * 考虑使用枚举序号进行比较 ### 2. 兼容性 * 新增枚举项时考虑向后兼容 * 删除枚举项需要检查现有数据 * 修改枚举值需要同步更新数据库 ### 3. 维护性 * 保持枚举命名的一致性 * 及时更新相关文档 * 定期检查未使用的枚举项 ### 4. 测试覆盖 ```java @Test public void testDictUserGender() { // 测试值查找 assertEquals(DictUserGender.MALE, DictUserGender.getByValue("1")); assertEquals(DictUserGender.UNKNOWN, DictUserGender.getByValue("invalid")); // 测试标签查找 assertEquals(DictUserGender.FEMALE, DictUserGender.getByLabel("女")); // 测试业务方法 assertTrue(DictUserGender.MALE.getValue().equals("1")); } ``` ## 总结 字典枚举系统为ruoyi-plus-uniapp提供了统一、类型安全的字典数据管理方案,通过规范的设计模式和丰富的内置字典,大大提高了开发效率和代码质量。合理使用字典枚举可以有效避免魔法字符串,提升代码的可读性和维护性。 --- --- url: 'https://ruoyi.plus/frontend/stores/dict-store.md' --- # 字典状态 (dict) --- --- url: 'https://ruoyi.plus/frontend/utils/string.md' --- # 字符串工具 --- --- url: 'https://ruoyi.plus/mobile/utils/storage.md' --- # 存储工具 --- --- url: 'https://ruoyi.plus/practices/security-overview.md' --- # 安全总览 --- --- url: 'https://ruoyi.plus/backend/common/security.md' --- # 安全防护 (security) ## 模块概述 `ruoyi-common-security` 是RuoYi-PLus-Uniapp框架的核心安全模块,提供应用安全防护与权限管理功能。该模块基于Sa-Token框架实现,具有高度的灵活性和可配置性。 ### 主要功能 * **动态URL路径收集**:自动扫描所有Controller映射路径 * **灵活的权限控制**:支持白名单机制和路径排除规则 * **登录验证拦截**:全局Token验证,保护敏感资源 * **监控端点保护**:为Actuator提供HTTP Basic认证 * **SSE服务支持**:特殊处理Server-Sent Events路径 ## 模块依赖 ```xml plus.ruoyi ruoyi-common-satoken ``` ## 核心组件 ### 1. AllUrlHandler - URL路径收集器 #### 功能描述 `AllUrlHandler` 负责自动收集Spring MVC中所有Controller的URL映射路径,为安全拦截器提供完整的路径信息。 #### 工作原理 1. **自动扫描**:在Spring容器初始化完成后,扫描所有`RequestMapping` 2. **路径标准化**:将路径变量占位符(如`{id}`)替换为通配符`*` 3. **去重处理**:使用Set确保URL路径的唯一性 4. **结果存储**:提供标准化的URL列表供安全组件使用 #### 路径转换示例 | 原始路径 | 处理后路径 | |---------|-----------| | `/api/user/{id}` | `/api/user/*` | | `/system/role/{roleId}/users` | `/system/role/*/users` | | `/order/{orderId}/items/{itemId}` | `/order/*/items/*` | #### 应用场景 * 安全拦截器路径匹配 * 动态权限控制 * 避免硬编码URL路径 * 与白名单配置协同工作 ### 2. SecurityConfig - 安全配置类 #### 配置功能 `SecurityConfig` 是整个安全模块的核心配置类,基于Sa-Token框架提供以下功能: ##### 2.1 登录验证拦截器 ```java // 拦截器配置 registry.addInterceptor(new SaInterceptor(handler -> { AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class); SaRouter .match(allUrlHandler.getUrls()) // 匹配所有需要验证的路径 .check(StpUtil::checkLogin); // 执行登录检查 })) .addPathPatterns("/**") // 拦截所有路径 .excludePathPatterns(securityProperties.getExcludes()) // 排除白名单 .excludePathPatterns(ssePath); // 排除SSE路径 ``` ##### 2.2 Actuator监控保护 ```java @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() .addInclude("/actuator", "/actuator/**") .setAuth(obj -> { SaHttpBasicUtil.check(username + ":" + password); }) .setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED)); } ``` ### 3. SecurityProperties - 配置属性 #### 配置格式 ```yaml security: excludes: - /login - /register - /captcha - /api/public/** - /v3/api-docs/** - /static/** - /actuator/health - /actuator/info ``` #### 路径匹配规则 支持Ant风格的路径匹配模式: * `?` 匹配一个字符 * `*` 匹配零个或多个字符 * `**` 匹配零个或多个目录 #### 常见排除路径分类 | 分类 | 路径示例 | 说明 | |-----|---------|------| | 静态资源 | `/resources/**`, `/*.html`, `/**/*.css`, `/**/*.js` | 前端静态文件和资源 | | 网站图标 | `/favicon.ico` | 浏览器标签页图标 | | 错误页面 | `/error` | Spring Boot默认错误页面 | | API文档 | `/*/api-docs`, `/*/api-docs/**` | OpenAPI文档接口 | | HTML页面 | `/**/*.html` | 各级目录下的HTML页面 | #### 路径配置说明 根据实际配置,当前安全模块主要排除以下类型的路径: 1. **静态资源保护**: * `/resources/**` - Spring Boot默认静态资源路径 * `/**/*.css` - 所有CSS样式文件 * `/**/*.js` - 所有JavaScript脚本文件 2. **页面文件访问**: * `/*.html` - 根目录下的HTML文件 * `/**/*.html` - 各级目录下的HTML页面 3. **系统相关**: * `/favicon.ico` - 网站图标文件 * `/error` - Spring Boot错误处理页面 4. **文档接口**: * `/*/api-docs` - API文档根路径(如 `/v3/api-docs`) * `/*/api-docs/**` - API文档详细路径 ## 自动配置 模块通过Spring Boot的自动配置机制加载: ``` # META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports plus.ruoyi.common.security.handler.AllUrlHandler plus.ruoyi.common.security.config.SecurityConfig ``` ## 配置示例 ### 完整配置示例 ```yaml # 安全配置 security: # 排除路径 excludes: # 本地文件路径/resources/** - /resources/** - /*.html - /**/*.html - /**/*.css - /**/*.js - /favicon.ico - /error - /*/api-docs - /*/api-docs/** ``` ## 使用指南 ### 1. 添加模块依赖 在需要使用安全功能的模块中添加依赖: ```xml plus.ruoyi ruoyi-common-security ``` ### 2. 配置排除路径 根据应用需求配置不需要认证的路径: ```yaml security: excludes: - /resources/** # 静态资源 - /*.html # 根目录HTML文件 - /error # 错误页面 ``` ### 3. 自定义认证逻辑 如需扩展认证逻辑,可以继承或替换相关组件: ```java @Component public class CustomSecurityConfig extends SecurityConfig { @Override public void addInterceptors(InterceptorRegistry registry) { // 自定义拦截器逻辑 registry.addInterceptor(new SaInterceptor(handler -> { SaRouter.match("/**").check(() -> { // 自定义验证逻辑 if (!StpUtil.isLogin()) { // 处理未登录情况 throw new NotLoginException("用户未登录"); } // 其他验证逻辑... }); })); } } ``` ## 工作流程 ### 请求处理流程 ```mermaid graph TD A[HTTP请求] --> B{路径是否在排除列表?} B -->|是| C[直接放行] B -->|否| D{路径是否需要验证?} D -->|是| E[执行Token验证] D -->|否| C E --> F{验证是否通过?} F -->|是| G[继续处理请求] F -->|否| H[返回401未授权] C --> G G --> I[到达Controller] ``` ### 初始化流程 ```mermaid graph TD A[Spring容器启动] --> B[AllUrlHandler初始化] B --> C[扫描所有RequestMapping] C --> D[提取URL路径模式] D --> E[标准化路径变量] E --> F[去重并存储] F --> G[SecurityConfig配置拦截器] G --> H[注册Sa-Token拦截器] H --> I[配置Actuator过滤器] I --> J[安全模块就绪] ``` ## 最佳实践 ### 1. 路径配置建议 * **精确匹配优先**:能用精确路径就不用通配符 * **最小权限原则**:只排除必要的路径 * **分类管理**:按功能分类配置排除路径 * **定期审查**:定期检查配置的合理性 ### 2. 性能优化 * **缓存URL列表**:AllUrlHandler在启动时一次性收集 * **高效匹配**:使用Set存储确保查找效率 * **合理配置**:避免过度复杂的路径匹配规则 ### 3. 安全考虑 * **白名单管控**:严格控制排除路径的范围 * **监控保护**:为Actuator端点配置强密码 * **日志记录**:记录安全相关的操作日志 * **定期更新**:及时更新Sa-Token版本 ## 故障排查 ### 常见问题 #### 1. 路径无法访问(401错误) **问题原因**:路径未在排除列表中,但应用期望无需认证 **解决方案**: * 检查`security.excludes`配置 * 确认路径匹配规则是否正确 * 验证用户是否已正确登录 #### 2. AllUrlHandler收集不到路径 **问题原因**:Controller未正确注册或扫描路径错误 **解决方案**: * 确认Controller类有`@Controller`或`@RestController`注解 * 检查Spring的组件扫描配置 * 验证RequestMapping注解配置 #### 3. Actuator认证失败 **问题原因**:HTTP Basic认证配置错误 **解决方案**: * 检查`spring.boot.admin.client`配置 * 确认用户名密码正确性 * 验证请求头格式 ### 调试方法 #### 启用调试日志 ```yaml logging: level: plus.ruoyi.common.security: DEBUG cn.dev33.satoken: DEBUG ``` #### 查看收集的URL ```java @Autowired private AllUrlHandler allUrlHandler; @PostConstruct public void printUrls() { log.info("收集到的URL路径: {}", allUrlHandler.getUrls()); } ``` ## 相关链接 * [Sa-Token官方文档](https://sa-token.cc/) --- --- url: 'https://ruoyi.plus/frontend/utils/object.md' --- # 对象工具 --- --- url: 'https://ruoyi.plus/mobile/layouts/navbar.md' --- # 导航栏配置 --- --- url: 'https://ruoyi.plus/mobile/components/navigation.md' --- # 导航组件 --- --- url: 'https://ruoyi.plus/mobile/debug/miniapp-devtools.md' --- # 小程序开发者工具 --- --- url: 'https://ruoyi.plus/backend/common/miniapp.md' --- # 小程序集成 (miniapp) ## 简介 `ruoyi-common-miniapp` 是基于若依框架的微信小程序服务模块,提供了微信小程序的配置管理、消息处理、API调用等核心功能。该模块基于 `weixin-java-miniapp` 开发,支持多小程序实例配置和动态配置管理。 ## 核心特性 * **多小程序支持**:支持同时管理多个微信小程序 * **动态配置管理**:支持运行时动态添加和移除小程序配置 * **消息路由处理**:内置消息路由器,支持不同类型消息的处理 * **自动配置初始化**:应用启动时自动加载小程序配置 * **完整API支持**:支持微信小程序所有官方API ## 模块结构 ``` ruoyi-common-miniapp/ ├── src/main/java/plus/ruoyi/common/miniapp/ │ ├── config/ │ │ └── WxMaServiceConfiguration.java # 小程序服务配置类 │ └── runner/ │ └── WxMaApplicationRunner.java # 启动初始化类 └── src/main/resources/META-INF/spring/ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports ``` ## 依赖配置 ### Maven 依赖 ```xml plus.ruoyi ruoyi-common-miniapp ``` ### 核心依赖 * `ruoyi-common-core`:若依核心模块 * `weixin-java-miniapp`:微信小程序Java SDK ## 快速开始 ### 1. 添加模块依赖 在需要使用小程序功能的模块中添加依赖: ```xml plus.ruoyi ruoyi-common-miniapp ``` ### 2. 配置小程序信息 在平台管理中添加微信小程序配置: ```java // 平台配置示例 PlatformDTO platform = new PlatformDTO(); platform.setType(DictPlatformType.MP_WEIXIN.getValue()); platform.setAppid("your-appid"); platform.setSecret("your-secret"); platform.setToken("your-token"); platform.setAeskey("your-aeskey"); platform.setName("小程序名称"); ``` ### 3. 使用服务 ```java @Autowired private WxMaService wxMaService; // 切换到指定小程序 wxMaService.switchoverTo("your-appid"); // 获取用户信息 WxMaUserInfo userInfo = wxMaService.getUserService().getUserInfo(sessionKey, encryptedData, ivStr); ``` ## 核心组件详解 ### WxMaServiceConfiguration 微信小程序服务的核心配置类,负责: * **WxMaService Bean注册**:创建微信小程序服务实例 * **消息路由配置**:配置不同类型消息的处理器 * **重试机制设置**:配置API调用的重试次数 #### 关键特性 ```java @Bean public WxMaService wxMaService() { WxMaService wxMaService = new WxMaServiceImpl(); wxMaService.setMaxRetryTimes(3); // 设置重试次数 return wxMaService; } ``` #### 内置消息处理器 | 处理器 | 触发条件 | 功能描述 | |--------|----------|----------| | `logHandler` | 所有消息 | 记录消息日志并回复确认信息 | | `subscribeMsgHandler` | 内容为"订阅消息" | 发送订阅消息 | | `textHandler` | 内容为"文本" | 处理文本消息 | | `picHandler` | 内容为"图片" | 处理图片消息 | | `qrcodeHandler` | 内容为"二维码" | 生成并发送二维码 | ### WxMaApplicationRunner 应用启动时的初始化类,实现了 `ApplicationRunner` 接口: #### 主要功能 * **自动初始化**:应用启动后自动加载小程序配置 * **多配置管理**:支持同时管理多个小程序实例 * **动态配置**:提供运行时配置管理方法 #### 核心方法 ```java // 初始化所有小程序配置 public void init() // 添加单个小程序配置 public void addConfig(PlatformDTO platform) // 移除小程序配置 public void removeConfig(String appid) ``` ## API 使用示例 ### 用户信息解密 ```java @Service public class MiniappUserService { @Autowired private WxMaService wxMaService; public WxMaUserInfo getUserInfo(String appid, String sessionKey, String encryptedData, String ivStr) { try { // 切换到指定小程序 wxMaService.switchoverTo(appid); // 解密用户信息 return wxMaService.getUserService() .getUserInfo(sessionKey, encryptedData, ivStr); } catch (WxErrorException e) { log.error("获取用户信息失败", e); throw new RuntimeException("获取用户信息失败"); } } } ``` ### 发送订阅消息 ```java public void sendSubscribeMessage(String appid, String toUser, String templateId) { try { wxMaService.switchoverTo(appid); WxMaSubscribeMessage message = WxMaSubscribeMessage.builder() .templateId(templateId) .toUser(toUser) .data(Arrays.asList( new WxMaSubscribeMessage.MsgData("thing1", "订阅内容"), new WxMaSubscribeMessage.MsgData("time2", "2024-01-01 12:00:00") )) .build(); wxMaService.getMsgService().sendSubscribeMsg(message); } catch (WxErrorException e) { log.error("发送订阅消息失败", e); } } ``` ### 生成小程序码 ```java public byte[] generateQrcode(String appid, String scene, String page) { try { wxMaService.switchoverTo(appid); return wxMaService.getQrcodeService() .createWxaCodeUnlimitBytes(scene, page, 430, true, null, false); } catch (WxErrorException e) { log.error("生成小程序码失败", e); throw new RuntimeException("生成小程序码失败"); } } ``` ## 配置管理 ### 动态添加配置 ```java @Service public class MiniappConfigService { @Autowired private WxMaApplicationRunner wxMaApplicationRunner; public void addMiniappConfig(PlatformDTO platform) { // 验证配置 if (StringUtils.isBlank(platform.getAppid()) || StringUtils.isBlank(platform.getSecret())) { throw new IllegalArgumentException("AppId和Secret不能为空"); } // 添加配置 wxMaApplicationRunner.addConfig(platform); log.info("成功添加小程序配置: {}", platform.getName()); } public void removeMiniappConfig(String appid) { wxMaApplicationRunner.removeConfig(appid); log.info("成功移除小程序配置: {}", appid); } } ``` ### 配置验证 ```java public boolean validateConfig(String appid) { try { wxMaService.switchoverTo(appid); // 调用获取访问令牌接口验证配置 String accessToken = wxMaService.getAccessToken(); return StringUtils.isNotBlank(accessToken); } catch (Exception e) { log.error("配置验证失败: appid={}", appid, e); return false; } } ``` ## 消息处理扩展 ### 自定义消息处理器 ```java @Component public class CustomMessageHandler { // 自定义文本消息处理器 public WxMaMessageHandler customTextHandler = (wxMessage, context, service, sessionManager) -> { String content = wxMessage.getContent(); String fromUser = wxMessage.getFromUser(); // 自定义处理逻辑 String replyContent = processTextMessage(content); // 发送回复 try { service.getMsgService().sendKefuMsg( WxMaKefuMessage.newTextBuilder() .content(replyContent) .toUser(fromUser) .build() ); } catch (WxErrorException e) { log.error("发送客服消息失败", e); } return null; }; private String processTextMessage(String content) { // 实现自定义文本处理逻辑 return "您发送的消息是:" + content; } } ``` ### 消息路由配置扩展 ```java @Configuration public class CustomMessageRouterConfig { @Autowired private CustomMessageHandler customMessageHandler; @Bean @Primary public WxMaMessageRouter customWxMaMessageRouter(WxMaService wxMaService) { final WxMaMessageRouter router = new WxMaMessageRouter(wxMaService); router // 原有规则... .rule().async(false).content("自定义").handler(customMessageHandler.customTextHandler).end() // 可以继续添加更多规则 .rule().async(false).msgType(WxConsts.XmlMsgType.IMAGE).handler(imageHandler).end(); return router; } } ``` ## 最佳实践 ### 1. 错误处理 ```java public class MiniappServiceImpl { public T executeWithRetry(String appid, Supplier supplier) { int maxRetries = 3; int retryCount = 0; while (retryCount < maxRetries) { try { wxMaService.switchoverTo(appid); return supplier.get(); } catch (WxErrorException e) { retryCount++; if (retryCount >= maxRetries) { log.error("执行失败,已重试{}次: appid={}", maxRetries, appid, e); throw new RuntimeException("小程序API调用失败", e); } log.warn("执行失败,准备重试第{}次: appid={}", retryCount, appid, e); try { Thread.sleep(1000 * retryCount); // 递增延迟 } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } } throw new RuntimeException("执行失败"); } } ``` ### 2. 配置缓存 ```java @Component public class MiniappConfigCache { private final Map configCache = new ConcurrentHashMap<>(); public void cacheConfig(PlatformDTO platform) { configCache.put(platform.getAppid(), platform); } public PlatformDTO getConfig(String appid) { return configCache.get(appid); } public void removeConfig(String appid) { configCache.remove(appid); } public boolean hasConfig(String appid) { return configCache.containsKey(appid); } } ``` ### 3. 日志记录 ```java @Aspect @Component public class MiniappLogAspect { @Around("execution(* plus.ruoyi.common.miniapp..*(..))") public Object logMiniappOperation(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); log.info("开始执行小程序操作: method={}, args={}", methodName, args); long startTime = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); long endTime = System.currentTimeMillis(); log.info("小程序操作执行成功: method={}, duration={}ms", methodName, endTime - startTime); return result; } catch (Exception e) { long endTime = System.currentTimeMillis(); log.error("小程序操作执行失败: method={}, duration={}ms", methodName, endTime - startTime, e); throw e; } } } ``` ## 常见问题 ### Q: 如何处理多个小程序切换? A: 使用 `wxMaService.switchoverTo(appid)` 方法在调用具体API前切换到对应的小程序实例。 ```java // 正确的做法 wxMaService.switchoverTo("appid1"); String result1 = wxMaService.getAccessToken(); wxMaService.switchoverTo("appid2"); String result2 = wxMaService.getAccessToken(); ``` ### Q: 配置不生效怎么办? A: 检查以下几点: 1. 确认平台类型为 `DictPlatformType.MP_WEIXIN.getValue()` 2. 确认AppId、Secret等信息正确 3. 检查应用启动日志,确认配置初始化成功 4. 使用 `validateConfig()` 方法验证配置有效性 ### Q: 消息处理器不响应? A: 检查消息路由配置: 1. 确认消息类型和内容匹配规则 2. 检查处理器是否正确注册 3. 查看是否有异常日志 4. 确认小程序端推送配置正确 ## 注意事项 1. **并发安全**:多线程环境下使用 `switchoverTo` 时注意线程安全 2. **配置管理**:动态添加配置后建议进行验证测试 3. **异常处理**:微信API调用可能出现各种异常,需要适当的重试和降级机制 4. **日志监控**:建议添加详细的日志记录,便于问题排查 5. **资源管理**:注意及时清理不再使用的配置,避免资源浪费 通过本模块,您可以轻松地在若依框架中集成微信小程序功能,实现完整的小程序后端服务支持。 --- --- url: 'https://ruoyi.plus/mobile/components/display.md' --- # 展示组件 --- --- url: 'https://ruoyi.plus/frontend/styles/utility-classes.md' --- # 工具类 --- --- url: 'https://ruoyi.plus/mobile/styles/utilities.md' --- # 工具类 --- --- url: 'https://ruoyi.plus/frontend/types/utility-types.md' --- # 工具类型 --- --- url: 'https://ruoyi.plus/backend/common/core/utils.md' --- # 工具类库 (Utils) ## 概述 ruoyi-common-core 的工具类库提供了丰富的工具方法,覆盖字符串处理、日期时间、反射操作、网络地址、文件处理、数据流处理等多个方面。这些工具类采用静态方法设计,线程安全,高度可复用。 ## 目录结构 ```text utils/ ├── StringUtils.java # 字符串增强工具 ├── DateUtils.java # 日期时间工具 ├── ObjectUtils.java # 对象操作工具 ├── StreamUtils.java # Stream流处理工具 ├── TreeBuildUtils.java # 树形结构构建工具 ├── ValidatorUtils.java # 参数校验工具 ├── SpringUtils.java # Spring上下文工具 ├── ServletUtils.java # Servlet请求响应工具 ├── ThreadUtils.java # 线程处理工具 ├── MapstructUtils.java # 对象映射工具 ├── MessageUtils.java # 国际化消息工具 ├── file/ │ ├── FileUtils.java # 文件处理工具 │ └── FileTypeUtils.java # 文件类型工具 ├── ip/ │ ├── AddressUtils.java # IP地址解析工具 │ └── RegionUtils.java # IP区域定位工具 ├── reflect/ │ └── ReflectUtils.java # 反射操作工具 ├── regex/ │ ├── RegexPatterns.java # 正则表达式模式 │ ├── RegexPatternPool.java # 正则模式池 │ ├── RegexValidator.java # 正则校验器 │ └── RegexUtils.java # 正则工具 └── sql/ └── SqlUtil.java # SQL安全工具 ``` ## 核心工具类 ### 1. 字符串工具 (StringUtils) 扩展了 Apache Commons Lang3 的 StringUtils,提供更多实用方法。 #### 基础操作 ```java // 空值处理 String result = StringUtils.blankToDefault("", "默认值"); boolean isEmpty = StringUtils.isEmpty(""); boolean isNotEmpty = StringUtils.isNotEmpty("hello"); // 字符串截取 String sub = StringUtils.substring("hello world", 6); // "world" String sub2 = StringUtils.substring("hello world", 0, 5); // "hello" // 格式化 - 支持 {} 占位符 String msg = StringUtils.format("Hello {}, age {}", "Tom", 25); // 结果: "Hello Tom, age 25" ``` #### 分割与转换 ```java // 字符串转集合 Set set = StringUtils.stringToSet("a,b,c", ","); List list = StringUtils.stringToList("a, b, c", ",", true, true); // 分割并转换类型 List ids = StringUtils.splitToList("1,2,3", Integer::valueOf); List userIds = StringUtils.splitToList("100;200;300", ";", Long::valueOf); ``` #### 命名转换 ```java // 驼峰与下划线转换 String underscore = StringUtils.camelToUnderscore("userName"); // "user_name" String camel = StringUtils.underscoreToCamelCase("user_name"); // "userName" String pascal = StringUtils.underscoreToPascalCase("user_name"); // "UserName" ``` #### 模式匹配 ```java // URL匹配 boolean match = StringUtils.isMatch("/api/*", "/api/user"); // true boolean matchAny = StringUtils.matchesAny("/api/user", Arrays.asList("/api/*", "/admin/*")); // true // 检查是否包含任意字符串 boolean contains = StringUtils.containsAnyIgnoreCase("Hello", "hello", "world"); // true ``` #### 字符串映射转换 ```java // 单值映射 Map statusMap = Map.of("1", "启用", "0", "禁用"); String result = StringUtils.convertWithMapping("1", statusMap); // "启用" // 多值映射 String result2 = StringUtils.convertWithMapping("1,0", ",", statusMap); // "启用,禁用" // 反向映射 String key = StringUtils.convertWithReverseMapping("启用", ",", statusMap); // "1" ``` #### 实用功能 ```java // 左补零 String padded = StringUtils.leftPadWithZero(123, 5); // "00123" // URL编码/解码 String encoded = StringUtils.urlEncode("中文参数"); String decoded = StringUtils.urlDecode("%E4%B8%AD%E6%96%87"); // 向逗号分隔字符串添加元素(自动去重) String result = StringUtils.addToCommaString("a,b", "c"); // "a,b,c" ``` ### 2. 日期时间工具 (DateUtils) 提供全面的日期时间处理功能,支持多种格式和转换。 #### 获取当前时间 ```java // 获取当前时间 Date now = DateUtils.getNowDate(); String date = DateUtils.getDate(); // yyyy-MM-dd String time = DateUtils.getTime(); // yyyy-MM-dd HH:mm:ss String compact = DateUtils.dateTimeNow(); // yyyyMMddHHmmss // 路径格式日期 String path = DateUtils.datePath(); // yyyy/MM/dd ``` #### 格式化与解析 ```java // 格式化 String formatted = DateUtils.formatDate(new Date()); // yyyy-MM-dd String dateTime = DateUtils.formatDateTime(new Date()); // yyyy-MM-dd HH:mm:ss // 使用枚举格式 String custom = DateUtils.parseDateToStr(DateTimeFormat.DATE_COMPACT, new Date()); // 解析多种格式 Date parsed = DateUtils.parseDate("2023-07-22"); Date parsed2 = DateUtils.parseDate("2023/07/22 15:30:45"); ``` #### 时间差计算 ```java Date start = DateUtils.parseDate("2023-07-01"); Date end = DateUtils.parseDate("2023-07-22"); // 计算时间差 long days = DateUtils.difference(start, end, TimeUnit.DAYS); // 21 long hours = DateUtils.difference(start, end, TimeUnit.HOURS); // 格式化时间差 String diff = DateUtils.getDatePoor(end, start); // "21天 0小时 0分钟" String detail = DateUtils.getTimeDifference(end, start); // "21天" ``` #### 类型转换 ```java // LocalDateTime 转 Date Date date = DateUtils.toDate(LocalDateTime.now()); // LocalDate 转 Date Date date2 = DateUtils.toDate(LocalDate.now()); ``` #### 日期范围校验 ```java // 校验日期范围不超过30天 try { DateUtils.validateDateRange(startDate, endDate, 30, TimeUnit.DAYS); } catch (ServiceException e) { // 处理范围超限 } ``` ### 3. Stream流处理工具 (StreamUtils) 简化集合的流式处理操作。 #### 过滤与查找 ```java List users = getUserList(); // 过滤 List activeUsers = StreamUtils.filter(users, user -> "1".equals(user.getStatus())); // 查找第一个 User firstAdmin = StreamUtils.findFirst(users, user -> "admin".equals(user.getRole())); // 查找任意一个 Optional anyActive = StreamUtils.findAny(users, user -> "1".equals(user.getStatus())); ``` #### 转换操作 ```java // 转换为List List names = StreamUtils.toList(users, User::getName); List ids = StreamUtils.toList(users, User::getId); // 转换为Set Set roleSet = StreamUtils.toSet(users, User::getRole); // 拼接 String nameStr = StreamUtils.join(users, User::getName); // 逗号分隔 String customJoin = StreamUtils.join(users, User::getName, " | "); // 自定义分隔符 ``` #### 映射操作 ```java // 转为Map (key-value相同类型) Map userMap = StreamUtils.toIdentityMap(users, User::getId); // 转为Map (key-value不同类型) Map idNameMap = StreamUtils.toMap(users, User::getId, User::getName); ``` #### 分组操作 ```java // 按单一条件分组 Map> roleGroups = StreamUtils.groupByKey(users, User::getRole); // 按两个条件分组 Map>> deptRoleGroups = StreamUtils.groupBy2Key(users, User::getDeptCode, User::getRole); // 分组为Map Map> groups = StreamUtils.group2Map(users, User::getDeptCode, User::getId); ``` #### 排序 ```java // 排序 List sorted = StreamUtils.sorted(users, Comparator.comparing(User::getCreateTime).reversed()); ``` #### Map合并 ```java Map map1 = Map.of("a", 1, "b", 2); Map map2 = Map.of("b", 3, "c", 4); // 合并Map Map merged = StreamUtils.merge(map1, map2, (v1, v2) -> (v1 == null ? 0 : v1) + (v2 == null ? 0 : v2)); // 结果: {a=1, b=5, c=4} ``` ### 4. 树形结构构建工具 (TreeBuildUtils) 构建和操作树形数据结构。 #### 构建树形结构 ```java List menuList = getMenuList(); // 自动识别根节点构建 List> tree = TreeBuildUtils.build(menuList, (menu, treeNode) -> { treeNode.setId(menu.getId()); treeNode.setParentId(menu.getParentId()); treeNode.setName(menu.getName()); treeNode.putExtra("url", menu.getUrl()); }); // 指定根节点构建 List> tree2 = TreeBuildUtils.build(menuList, 0L, (menu, treeNode) -> { // 节点解析逻辑 }); ``` #### 树形操作 ```java // 获取所有叶子节点 List> leafNodes = TreeBuildUtils.getLeafNodes(tree); // 获取所有子节点(包括自身) List> allNodes = TreeBuildUtils.getAllNodes(rootNode); // 查找指定节点 Tree node = TreeBuildUtils.findNodeById(tree, 123L); // 获取树的最大深度 int depth = TreeBuildUtils.getMaxDepth(tree); ``` ### 5. 反射工具 (ReflectUtils) 扩展 HuTool 的反射工具,支持多级属性访问。 #### 多级属性访问 ```java User user = getUser(); // 获取多级属性 String profileName = ReflectUtils.invokeGetter(user, "profile.name"); String companyName = ReflectUtils.invokeGetter(user, "profile.company.name"); // 设置多级属性 ReflectUtils.invokeSetter(user, "profile.name", "张三"); ReflectUtils.invokeSetter(user, "profile.company.name", "ABC公司"); ``` #### 安全访问 ```java // 安全获取属性(遇到null不抛异常) String name = ReflectUtils.getPropertySafely(user, "profile.name"); // 检查是否具有属性路径 boolean hasProperty = ReflectUtils.hasProperty(user, "profile.name"); ``` ### 6. 对象工具 (ObjectUtils) 扩展 HuTool 的对象工具,提供安全的对象访问。 #### 安全属性访问 ```java User user = getUser(); // 可能为null // 安全获取属性 String name = ObjectUtils.getIfNotNull(user, User::getName); // 带默认值的安全获取 String name2 = ObjectUtils.getIfNotNull(user, User::getName, "未知用户"); ``` ### 7. Spring上下文工具 (SpringUtils) 访问Spring容器和Bean管理。 #### Bean操作 ```java // 获取Bean UserService userService = SpringUtils.getBean(UserService.class); UserService userService2 = SpringUtils.getBean("userService"); // 检查Bean是否存在 boolean exists = SpringUtils.containsBean("userService"); // 检查是否单例 boolean singleton = SpringUtils.isSingleton("userService"); // 获取Bean类型 Class type = SpringUtils.getType("userService"); // 获取别名 String[] aliases = SpringUtils.getAliases("userService"); ``` #### AOP代理 ```java // 获取AOP代理对象 UserService proxy = SpringUtils.getAopProxy(userServiceImpl); ``` #### 环境信息 ```java // 获取应用上下文 ApplicationContext context = SpringUtils.context(); // 判断是否虚拟线程环境 boolean isVirtual = SpringUtils.isVirtual(); ``` ### 8. Servlet工具 (ServletUtils) 处理HTTP请求响应的实用工具。 #### 参数获取 ```java // 获取请求参数 String userName = ServletUtils.getParameter("userName"); String userName2 = ServletUtils.getParameter("userName", "默认值"); // 类型转换 Integer pageNum = ServletUtils.getParameterToInt("pageNum"); Integer pageSize = ServletUtils.getParameterToInt("pageSize", 10); Boolean active = ServletUtils.getParameterToBool("active"); // 获取所有参数 Map allParams = ServletUtils.getParams(request); Map paramMap = ServletUtils.getParamMap(request); ``` #### 请求响应对象 ```java // 获取当前请求响应对象 HttpServletRequest request = ServletUtils.getRequest(); HttpServletResponse response = ServletUtils.getResponse(); HttpSession session = ServletUtils.getSession(); ``` #### 请求头处理 ```java // 获取请求头 String userAgent = ServletUtils.getHeader(request, "User-Agent"); Map headers = ServletUtils.getHeaders(request); ``` #### 响应处理 ```java // 渲染JSON响应 ServletUtils.renderString(response, "{\"success\": true}"); // 判断Ajax请求 boolean isAjax = ServletUtils.isAjaxRequest(request); ``` #### 客户端信息 ```java // 获取客户端IP String clientIP = ServletUtils.getClientIP(); // URL编码解码 String encoded = ServletUtils.urlEncode("中文"); String decoded = ServletUtils.urlDecode("%E4%B8%AD%E6%96%87"); ``` ### 9. 线程工具 (ThreadUtils) 线程池管理和异常处理。 #### 线程池优雅关闭 ```java ExecutorService executor = Executors.newFixedThreadPool(10); // 优雅关闭(默认120秒超时) ThreadUtils.shutdownGracefully(executor); // 自定义超时时间 ThreadUtils.shutdownGracefully(executor, 60); ``` #### 异常处理 ```java // 在ThreadPoolExecutor的afterExecute中使用 @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); ThreadUtils.logException(r, t); } ``` #### 线程休眠 ```java // 安全休眠(自动处理中断异常) ThreadUtils.sleep(1000); // 1秒 ThreadUtils.sleepSeconds(5); // 5秒 // 获取线程信息 String threadInfo = ThreadUtils.getCurrentThreadInfo(); ``` ### 10. 文件工具 (FileUtils & FileTypeUtils) 文件处理和类型判断。 #### 文件下载响应头 ```java @GetMapping("/download") public void download(HttpServletResponse response) { String fileName = "用户数据.xlsx"; // 设置下载响应头(自动处理文件名编码) FileUtils.setAttachmentResponseHeader(response, fileName); // 文件内容写入响应流 // ... } ``` #### 文件类型判断 ```java // 按扩展名判断 boolean isImage = FileTypeUtils.isImage("jpg"); boolean isDocument = FileTypeUtils.isDocument("pdf"); boolean isVideo = FileTypeUtils.isVideo("mp4"); // 按File对象判断 File file = new File("demo.png"); boolean isImage2 = FileTypeUtils.isImage(file); String fileType = FileTypeUtils.getFileType(file); // "图片" // 检查是否允许的类型 boolean allowed = FileTypeUtils.isAllowed("exe"); // false ``` #### URL编码工具 ```java // 百分号编码 String encoded = FileUtils.percentEncode("文件名.txt"); ``` ### 11. IP地址工具 (AddressUtils & RegionUtils) IP地址解析和地理位置定位。 #### IP地址解析 ```java // 获取IP地理位置 String location = AddressUtils.getRealAddressByIp("8.8.8.8"); // 可能返回: "美国|北美|美国|美国|谷歌" String location2 = AddressUtils.getRealAddressByIp("192.168.1.1"); // 返回: "内网IP" String location3 = AddressUtils.getRealAddressByIp("invalid-ip"); // 返回: "XX XX" ``` #### 离线IP定位 ```java // 使用ip2region库进行离线定位 String cityInfo = RegionUtils.getCityInfo("202.108.22.5"); // 返回格式: "中国|华北|北京市|北京市|联通" ``` #### 网络工具 ```java // IP类型判断 boolean isIPv4 = NetUtils.isIpv4("192.168.1.1"); // true boolean isIPv6 = NetUtils.isIpv6("2001:db8::1"); // true // 内网判断 boolean isInner = NetUtils.isInnerIP("192.168.1.1"); // true boolean isInnerV6 = NetUtils.isInnerIpv6("fe80::1"); // true ``` ### 12. 正则工具 (Regex Utils) 正则表达式相关工具。 #### 预定义模式 ```java // 使用预编译模式池 Pattern i18nPattern = RegexPatternPool.I18N_KEY; Pattern accountPattern = RegexPatternPool.ACCOUNT; Pattern statusPattern = RegexPatternPool.BINARY_STATUS; ``` #### 校验器 ```java // 国际化键格式校验 boolean valid = RegexValidator.isValidI18nKey("user.profile.name"); // true // 账号格式校验 boolean validAccount = RegexValidator.isValidAccount("admin123"); // true // 状态值校验 boolean validStatus = RegexValidator.isValidStatus("1"); // true // 字典类型校验 boolean validDict = RegexValidator.isValidDictType("sys_user_sex"); // true ``` #### 提取工具 ```java // 从字符串中提取匹配部分 String result = RegexUtils.extractFromString( "用户ID: 12345", "(\\d+)", "未找到"); // 返回: "12345" ``` ### 13. SQL安全工具 (SqlUtil) 防止SQL注入的安全工具。 #### ORDER BY安全校验 ```java // 校验排序参数 String orderBy = "name asc, create_time desc"; String safeOrderBy = SqlUtil.escapeOrderBySql(orderBy); // 检查是否安全 boolean isSafe = SqlUtil.isValidOrderBySql("name asc"); // true boolean isUnsafe = SqlUtil.isValidOrderBySql("name; drop table"); // false ``` #### SQL关键字过滤 ```java try { SqlUtil.filterKeyword("select * from user"); // 抛出异常: 参数存在SQL注入风险 } catch (IllegalArgumentException e) { // 处理SQL注入风险 } ``` ### 14. 参数校验工具 (ValidatorUtils) 手动触发Bean Validation校验。 #### 基本校验 ```java User user = new User(); user.setName(""); // 违反@NotBlank约束 try { // 校验对象(抛出异常) ValidatorUtils.validate(user); } catch (ConstraintViolationException e) { // 处理校验失败 System.out.println(e.getMessage()); } // 校验并返回结果(不抛异常) Set> violations = ValidatorUtils.validateAndReturn(user); ``` #### 分组校验 ```java // 使用校验分组 ValidatorUtils.validate(user, AddGroup.class); ValidatorUtils.validate(user, EditGroup.class); ``` #### 属性校验 ```java // 校验单个属性 ValidatorUtils.validateProperty(user, "email"); // 校验属性值 ValidatorUtils.validateValue(User.class, "email", "invalid-email"); ``` #### 便捷方法 ```java // 检查是否通过校验 boolean isValid = ValidatorUtils.isValid(user); // 获取错误信息 String errors = ValidatorUtils.getValidationErrors(user); // 返回: "name: 不能为空; email: 邮箱格式错误" ``` ### 15. 对象映射工具 (MapstructUtils) 基于 MapStruct Plus 的对象映射工具。 #### 基本映射 ```java // 对象转换 UserVo userVo = MapstructUtils.convert(user, UserVo.class); // 列表转换 List userVos = MapstructUtils.convert(userList, UserVo.class); // 属性赋值 UserVo vo = new UserVo(); MapstructUtils.convert(user, vo); // 将user属性赋值给vo ``` #### Map映射 ```java // Map转对象 Map map = Map.of("name", "张三", "age", 25); User user = MapstructUtils.convert(map, User.class); ``` ### 16. 国际化消息工具 (MessageUtils) 国际化消息处理。 #### 消息获取 ```java // 获取国际化消息 String message = MessageUtils.message("user.not.found"); // 带参数的消息 String message2 = MessageUtils.message("user.login.success", "张三"); // 消息不存在时返回键本身 String message3 = MessageUtils.message("nonexistent.key"); // 返回: "nonexistent.key" ``` ## 使用最佳实践 ### 1. 性能优化 #### 大数据量处理 ```java // 使用Stream工具处理大量数据时,注意内存使用 List vos = StreamUtils.toList(users, user -> { // 避免在转换函数中进行复杂操作 return MapstructUtils.convert(user, UserVo.class); }); // 分批处理大数据量 List> batches = Lists.partition(users, 1000); for (List batch : batches) { // 分批处理 } ``` #### 缓存利用 ```java // 正则模式使用预编译版本 Pattern pattern = RegexPatternPool.I18N_KEY; // 而不是 Pattern.compile() // 反射操作缓存Method对象 // ReflectUtils内部已实现缓存 ``` ### 2. 异常处理 #### 安全的工具方法使用 ```java // 使用安全版本的方法 String name = ObjectUtils.getIfNotNull(user, User::getName, "未知"); String profileName = ReflectUtils.getPropertySafely(user, "profile.name"); // 或者使用try-catch try { String name = ReflectUtils.invokeGetter(user, "profile.name"); } catch (Exception e) { log.warn("获取用户档案名称失败", e); // 设置默认值或其他处理 } ``` ### 3. 字符串处理优化 #### 大量字符串拼接 ```java // 使用Stream工具进行拼接 String result = StreamUtils.join(users, User::getName, ","); // 而不是循环拼接 StringBuilder sb = new StringBuilder(); for (User user : users) { if (sb.length() > 0) sb.append(","); sb.append(user.getName()); } ``` #### 字符串映射批量转换 ```java // 批量转换 List statusLabels = StreamUtils.toList(statusCodes, code -> StringUtils.convertWithMapping(code, statusMap)); ``` ### 4. 日期时间处理 #### 时区注意事项 ```java // 使用系统默认时区的转换 Date date = DateUtils.toDate(LocalDateTime.now()); // 如果需要特定时区,使用原生API ZonedDateTime zdt = LocalDateTime.now().atZone(ZoneId.of("Asia/Shanghai")); Date date2 = Date.from(zdt.toInstant()); ``` ### 5. 线程安全 #### 工具类都是线程安全的 ```java // 所有工具类方法都是静态的,线程安全 // 可以在多线程环境中安全使用 CompletableFuture.supplyAsync(() -> { return StringUtils.format("用户{}, 时间{}", userName, DateUtils.getTime()); }); ``` ## 扩展指南 ### 1. 自定义工具类 ```java // 遵循相同的设计模式 @NoArgsConstructor(access = AccessLevel.PRIVATE) public class CustomUtils { // 静态方法 public static String customMethod(String input) { // 实现逻辑 return result; } // 重载方法提供便利 public static String customMethod(String input, String defaultValue) { return StringUtils.isNotBlank(input) ? customMethod(input) : defaultValue; } } ``` ### 2. 扩展现有工具 ```java // 通过继承扩展(如果需要) public class EnhancedStringUtils extends StringUtils { public static String customFormat(String template, Object... args) { // 自定义格式化逻辑 return format(template, args); } } ``` ## 常见问题 **Q: 为什么某些工具方法返回不可变集合?** A: 为了防止意外修改,某些场景下返回不可变集合。如需修改,请创建新的可变集合。 **Q: 如何处理大数据量的Stream操作?** A: 考虑使用并行流、分批处理或者调整JVM堆内存设置。 **Q: 正则表达式性能如何优化?** A: 使用 RegexPatternPool 中的预编译模式,避免重复编译。 **Q: 反射操作的性能影响?** A: ReflectUtils 内部有缓存机制,但仍建议在性能敏感场景中谨慎使用。 **Q: 如何添加新的文件类型支持?** A: 修改 FileTypeUtils 中的扩展名集合常量,或者使用继承的方式扩展。 --- --- url: 'https://ruoyi.plus/frontend/layout/tools.md' --- # 工具组件 --- --- url: 'https://ruoyi.plus/frontend/layout/layout-overview.md' --- # 布局概述 --- --- url: 'https://ruoyi.plus/mobile/layouts/overview.md' --- # 布局概述 --- --- url: 'https://ruoyi.plus/mobile/components/layout.md' --- # 布局组件 --- --- url: 'https://ruoyi.plus/backend/common/idempotent.md' --- # 幂等处理 (idempotent) ## 概述 幂等功能模块提供基于 Redis 分布式锁机制的接口防重复提交功能,确保在短时间内用户重复点击或网络抖动等场景下,同一请求只会被处理一次,有效避免数据重复插入、重复扣款等问题。 ## 核心特性 * **分布式锁机制**:基于 Redis 实现分布式环境下的防重复提交 * **智能清理策略**:成功时保留锁定,失败时自动清理,允许重新提交 * **灵活配置**:支持自定义间隔时间、时间单位和提示消息 * **国际化支持**:提示消息支持多语言配置 * **参数过滤**:自动过滤文件上传等特殊对象,确保唯一标识准确性 ## 工作原理 ### 防重复提交流程 ```mermaid sequenceDiagram participant Client as 客户端 participant Server as 服务端 participant Redis as Redis Client->>Server: 发送请求 Server->>Redis: 尝试设置锁(key + 过期时间) alt 锁设置成功 Redis-->>Server: 返回成功 Server->>Server: 执行业务逻辑 alt 业务执行成功 Server-->>Client: 返回成功结果 Note over Redis: 保留锁,防止重复提交 else 业务执行失败 Server->>Redis: 删除锁 Server-->>Client: 返回失败结果 end else 锁已存在(重复提交) Redis-->>Server: 返回失败 Server-->>Client: 返回重复提交错误 end ``` ### 唯一标识生成规则 防重复提交通过以下信息生成唯一标识: 1. **用户标识**:从请求头中获取 Token 2. **请求参数**:序列化后的方法参数(过滤特殊对象) 3. **请求路径**:当前请求的 URI 最终的缓存 Key 格式:`repeat_submit::{requestURI}{md5(token:params)}` ## 使用指南 ### 1. 基础用法 在需要防重复提交的方法上添加 `@RepeatSubmit` 注解: ```java @RestController @RequestMapping("/user") public class UserController { @PostMapping("/create") @RepeatSubmit public R createUser(@RequestBody UserCreateReq req) { // 业务逻辑 return R.ok(); } } ``` ### 2. 自定义配置 ```java @PostMapping("/payment") @RepeatSubmit( interval = 10, // 间隔时间 timeUnit = TimeUnit.SECONDS, // 时间单位 message = "{payment.duplicate.submit}" // 自定义提示消息 ) public R processPayment(@RequestBody PaymentReq req) { // 支付逻辑 return R.ok(); } ``` ### 3. 配置参数说明 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `interval` | int | 5000 | 重复提交检测间隔时间 | | `timeUnit` | TimeUnit | MILLISECONDS | 时间单位 | | `message` | String | I18nKeys.Request.DUPLICATE\_SUBMIT | 重复提交提示消息 | ::: warning 注意事项 间隔时间不能小于 1 秒,系统会自动校验并抛出异常。 ::: ## 高级特性 ### 1. 智能参数过滤 系统会自动过滤以下类型的对象,确保唯一标识的准确性: * **文件上传对象**:`MultipartFile` 及其数组、集合 * **HTTP 对象**:`HttpServletRequest`、`HttpServletResponse` * **数据绑定结果**:`BindingResult` ```java @PostMapping("/upload") @RepeatSubmit public R uploadFile( @RequestParam("file") MultipartFile file, // 会被过滤 @RequestBody FileMetadata metadata // 参与唯一标识计算 ) { // 文件上传逻辑 return R.ok(); } ``` ### 2. 国际化消息配置 在国际化资源文件中配置提示消息: ```properties # messages.properties request.control.duplicate.submit=请勿重复提交 # messages_en.properties request.control.duplicate.submit=Please do not submit repeatedly ``` ### 3. 业务结果智能处理 系统根据业务执行结果智能处理缓存: ```java // 成功场景:保留缓存,防止重复提交 return R.ok("操作成功"); // 失败场景:清理缓存,允许重新提交 return R.fail("业务处理失败"); ``` ## 配置说明 ### 1. 自动配置 模块通过 Spring Boot 自动配置机制启用,无需手动配置: ```java @AutoConfiguration(after = RedisConfiguration.class) public class IdempotentConfig { @Bean public RepeatSubmitAspect repeatSubmitAspect() { return new RepeatSubmitAspect(); } } ``` ### 2. 依赖要求 确保项目中已引入相关依赖: ```xml plus.ruoyi ruoyi-common-idempotent ``` ## 最佳实践 ### 1. 适用场景 * **表单提交**:用户注册、信息修改等 * **支付操作**:订单支付、余额变动等 * **数据创建**:新增记录、文件上传等 * **状态变更**:订单确认、审核通过等 ### 2. 性能考虑 ```java // 推荐:合理设置间隔时间 @RepeatSubmit(interval = 3, timeUnit = TimeUnit.SECONDS) // 避免:过短的间隔时间影响用户体验 @RepeatSubmit(interval = 500, timeUnit = TimeUnit.MILLISECONDS) // 避免:过长的间隔时间占用 Redis 内存 @RepeatSubmit(interval = 10, timeUnit = TimeUnit.MINUTES) ``` ### 3. 错误处理 ```java @PostMapping("/order") @RepeatSubmit(message = "{order.duplicate.submit}") public R createOrder(@RequestBody OrderCreateReq req) { try { // 业务逻辑 OrderVO order = orderService.create(req); return R.ok(order); } catch (BusinessException e) { // 业务异常会自动清理缓存,允许重新提交 return R.fail(e.getMessage()); } } ``` ## 监控与调试 ### 1. Redis 缓存监控 可通过 Redis 客户端查看防重复提交的缓存状态: ```bash # 查看所有防重复提交缓存 redis-cli keys "repeat_submit::*" # 查看特定缓存的过期时间 redis-cli ttl "repeat_submit::/api/user/create{hash}" ``` ### 2. 日志调试 通过调整日志级别查看详细信息: ```yaml logging: level: plus.ruoyi.common.idempotent.aspectj.RepeatSubmitAspect: DEBUG ``` ## 常见问题 ### Q: 为什么有时候提示重复提交,但我确实只点击了一次? A: 可能的原因: * 网络延迟导致前端发送了多个请求 * 浏览器的重复提交行为 * 建议前端添加防抖处理,与后端防重复提交形成双重保护 ### Q: 如何处理集群环境下的防重复提交? A: 模块基于 Redis 实现分布式锁,天然支持集群环境,确保多个服务实例间的防重复提交一致性。 ### Q: 业务失败后多久可以重新提交? A: 业务失败时会立即清理 Redis 缓存,用户可以马上重新提交。只有业务成功时才会保留缓存直到过期时间。 ### Q: 如何自定义唯一标识的生成规则? A: 当前版本基于 Token + 参数 + URI 生成唯一标识。如需自定义,可以继承 `RepeatSubmitAspect` 并重写相关方法。 --- --- url: 'https://ruoyi.plus/mobile/platform/differences.md' --- # 平台差异说明 --- --- url: 'https://ruoyi.plus/backend/common/serialmap.md' --- # 序列化映射 (serialmap) ## 1. 概述 序列化映射模块是 RuoYi-Plus-Uniapp 框架的核心功能模块,提供了强大的数据字典翻译和字段映射功能。该模块通过注解驱动的方式,在 JSON 序列化过程中自动将原始数据转换为用户友好的显示数据,大幅简化了前端数据展示的复杂度。 ### 1.1 核心特性 * **注解驱动**:通过 `@SerialMap` 注解实现零侵入式的数据转换 * **高性能缓存**:内置 Redis 缓存机制,显著提升转换性能 * **灵活扩展**:支持自定义转换器,满足各种业务场景 * **类型安全**:完整的泛型支持和类型检查 * **线程安全**:基于 ThreadLocal 的上下文管理 ### 1.2 应用场景 * ID 转名称:用户 ID → 用户名、部门 ID → 部门名称 * 字典翻译:状态码 → 状态描述、性别码 → 性别标签 * 资源映射:文件 ID → 访问 URL、图片 ID → 图片链接 * 对象映射:关联 ID → 完整对象、ID 列表 → 对象列表 ## 2. 快速开始 ### 2.1 模块依赖 序列化映射模块已内置在框架中,默认包含以下依赖: ```xml plus.ruoyi ruoyi-common-serialmap ``` ### 2.2 基础使用 在 VO 类中使用 `@SerialMap` 注解: ```java public class UserVo { private Long userId; // 用户ID转用户名 @SerialMap(converter = SerialMapConstant.USER_ID_TO_NAME) private String userName; private Long deptId; // 部门ID转部门名称 @SerialMap(converter = SerialMapConstant.DEPT_ID_TO_NAME) private String deptName; // 字典翻译:性别 @SerialMap(converter = SerialMapConstant.DICT_TYPE_TO_LABEL, param = "sys_user_sex") private String sex; } ``` ## 3. 注解详解 ### 3.1 @SerialMap 注解 `@SerialMap` 是核心注解,用于标记需要进行映射转换的字段。 #### 主要参数 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | converter | String | ✓ | 转换器类型标识 | | source | String | ✗ | 源字段名称,默认使用当前字段 | | param | String | ✗ | 转换器额外参数 | | entityClass | Class\ | ✗ | 数据源实体类 | | targetField | String | ✗ | 目标映射字段 | #### 使用示例 ```java public class OrderVo { private Long userId; private Long deptId; private String status; private String fileIds; // 基础映射:用户ID → 用户名 @SerialMap(converter = SerialMapConstant.USER_ID_TO_NAME) private String userName; // 源字段映射:从deptId字段获取值转换为部门名称 @SerialMap(converter = SerialMapConstant.DEPT_ID_TO_NAME, source = "deptId") private String deptName; // 带参数映射:字典转换 @SerialMap(converter = SerialMapConstant.DICT_TYPE_TO_LABEL, param = "sys_order_status") private String statusLabel; // 文件ID转URL @SerialMap(converter = SerialMapConstant.OSS_ID_TO_URL, source = "fileIds") private String fileUrls; } ``` ### 3.2 @SerialMapType 注解 用于标识转换器实现类的类型,建立转换器与标识的映射关系。 ```java @SerialMapType(type = "custom_converter") @Component public class CustomConverter implements SerialMapInterface { @Override public String convert(Object key, String param) { // 转换逻辑 return convertLogic(key, param); } } ``` ## 4. 内置转换器 框架提供了丰富的内置转换器,覆盖常见的业务场景。 ### 4.1 转换器类型总览 | 转换器标识 | 功能描述 | 支持类型 | |------------|----------|----------| | `field_map` | 通用字段映射 | 单字段、对象、集合 | | `user_id_to_name` | 用户ID转账号 | Long | | `user_id_to_nickname` | 用户ID转昵称 | Long、String | | `user_id_to_avatar` | 用户ID转头像 | Long、String | | `dept_id_to_name` | 部门ID转名称 | Long、String | | `dict_type_to_label` | 字典值转标签 | String | | `oss_id_to_url` | OSS文件ID转URL | Long、String | | `directory_id_directory_name` | 目录ID转名称 | Long | ### 4.2 用户相关转换器 #### 用户ID转用户名 ```java @SerialMap(converter = SerialMapConstant.USER_ID_TO_NAME) private String userName; ``` #### 用户ID转昵称(支持批量) ```java // 单个用户ID @SerialMap(converter = SerialMapConstant.USER_ID_TO_NICKNAME) private String userNickName; // 多个用户ID(逗号分隔) @SerialMap(converter = SerialMapConstant.USER_ID_TO_NICKNAME) private String userNickNames; // "1,2,3" → "张三,李四,王五" ``` #### 用户ID转头像 ```java @SerialMap(converter = SerialMapConstant.USER_ID_TO_AVATAR) private String userAvatar; ``` ### 4.3 部门转换器 #### 部门ID转名称(支持批量) ```java // 单个部门ID @SerialMap(converter = SerialMapConstant.DEPT_ID_TO_NAME) private String deptName; // 多个部门ID @SerialMap(converter = SerialMapConstant.DEPT_ID_TO_NAME) private String deptNames; // "1,2,3" → "总经办,技术部,销售部" ``` ### 4.4 字典转换器 #### 字典值转标签 ```java // 性别字典 @SerialMap(converter = SerialMapConstant.DICT_TYPE_TO_LABEL, param = "sys_user_sex") private String sexLabel; // 状态字典 @SerialMap(converter = SerialMapConstant.DICT_TYPE_TO_LABEL, param = "sys_common_status") private String statusLabel; ``` ### 4.5 文件相关转换器 #### OSS文件ID转URL ```java // 单个文件ID @SerialMap(converter = SerialMapConstant.OSS_ID_TO_URL) private String fileUrl; // 多个文件ID @SerialMap(converter = SerialMapConstant.OSS_ID_TO_URL) private String fileUrls; // "1,2,3" → "url1,url2,url3" ``` #### 目录ID转名称 ```java @SerialMap(converter = SerialMapConstant.DIRECTORY_ID_DIRECTORY_NAME) private String directoryName; ``` ## 5. 通用字段映射转换器 `field_map` 是最强大的内置转换器,支持多种映射模式。 ### 5.1 单字段映射 将关联ID转换为实体的指定字段值。 ```java public class UserVo { private Long userId; // 获取用户表的nickName字段 @SerialMap(converter = SerialMapConstant.FIELD_MAP, source = "userId", entityClass = SysUser.class, targetField = "nickName") private String nickName; // 获取用户表的email字段 @SerialMap(converter = SerialMapConstant.FIELD_MAP, source = "userId", entityClass = SysUser.class, targetField = "email") private String email; } ``` ### 5.2 对象映射 将关联ID转换为完整的对象或VO。 ```java public class UserVo { private Long deptId; // 映射为部门对象 @SerialMap(converter = SerialMapConstant.FIELD_MAP, source = "deptId", entityClass = SysDept.class) private SysDeptVo deptVo; // 映射为用户对象 @SerialMap(converter = SerialMapConstant.FIELD_MAP, source = "userId", entityClass = SysUser.class) private SysUserVo userVo; } ``` ### 5.3 集合映射 将ID列表转换为对象列表。 ```java public class UserVo { private List roleIds; // ID列表映射为角色对象列表 @SerialMap(converter = SerialMapConstant.FIELD_MAP, source = "roleIds", entityClass = SysRole.class) private List roles; // 字符串ID列表(逗号分隔)映射为对象列表 private String menuIds; // "1,2,3" @SerialMap(converter = SerialMapConstant.FIELD_MAP, source = "menuIds", entityClass = SysMenu.class) private List menus; } ``` ### 5.4 智能字段匹配 `field_map` 支持智能字段名匹配,自动处理多种命名规则: ```java // 实体类字段名:user_name, userName, USER_NAME // 目标字段名:username // 系统会自动匹配以下规则: // 1. 精确匹配:username // 2. 驼峰转换:userName // 3. 下划线转换:user_name // 4. 模糊匹配:包含关键词的字段 @SerialMap(converter = SerialMapConstant.FIELD_MAP, entityClass = SysUser.class, targetField = "username") // 自动匹配 userName 字段 private String userName; ``` ## 6. 高级用法 ### 6.1 方法级注解 除了字段,还可以在getter方法上使用注解: ```java public class UserVo { private Long userId; @SerialMap(converter = SerialMapConstant.USER_ID_TO_NAME) public String getUserName() { return null; // 返回值会被转换器覆盖 } } ``` ### 6.2 复杂映射场景 ```java public class ComplexVo { private Long userId; private String roleIds; // "1,2,3" private String status; // 多级映射:用户 → 部门 → 部门名称 @SerialMap(converter = SerialMapConstant.FIELD_MAP, source = "userId", entityClass = SysUser.class, targetField = "dept.deptName") private String userDeptName; // 条件映射:根据状态显示不同内容 @SerialMap(converter = "custom_status_converter", param = "user_status") private String statusDescription; // 组合映射:角色ID列表 → 角色名称列表 → 逗号分隔字符串 @SerialMap(converter = "role_ids_to_names") private String roleNames; } ``` ### 6.3 性能优化技巧 #### 批量查询优化 ```java // 推荐:使用支持批量的转换器 @SerialMap(converter = SerialMapConstant.USER_ID_TO_NICKNAME) // 支持 "1,2,3" private String userNames; // 不推荐:多个单独字段(会产生多次数据库查询) @SerialMap(converter = SerialMapConstant.USER_ID_TO_NICKNAME) private String userName1; @SerialMap(converter = SerialMapConstant.USER_ID_TO_NICKNAME) private String userName2; ``` #### 缓存利用 ```java // 相同的转换会被缓存,第二次调用直接从缓存获取 @SerialMap(converter = SerialMapConstant.DEPT_ID_TO_NAME) private String deptName1; @SerialMap(converter = SerialMapConstant.DEPT_ID_TO_NAME, source = "parentDeptId") private String parentDeptName; // 如果ID相同,会命中缓存 ``` ## 7. 自定义转换器 ### 7.1 实现转换器接口 创建自定义转换器需要实现 `SerialMapInterface` 接口: ```java package com.example.converter; import plus.ruoyi.common.serialmap.core.SerialMapInterface; import plus.ruoyi.common.serialmap.annotation.SerialMapType; import plus.ruoyi.common.serialmap.constant.SerialMapConstant; import org.springframework.stereotype.Component; /** * 自定义转换器示例:订单状态转换 */ @Component @SerialMapType(type = "order_status_converter") public class OrderStatusConverter implements SerialMapInterface { @Override public String convert(Object key, String param) { if (key == null) { return null; } String status = key.toString(); return switch (status) { case "0" -> "待支付"; case "1" -> "已支付"; case "2" -> "已发货"; case "3" -> "已完成"; case "4" -> "已取消"; default -> "未知状态"; }; } } ``` ### 7.2 复杂业务转换器 ```java /** * 复杂业务转换器:用户积分等级转换 */ @Component @SerialMapType(type = "user_level_converter") @AllArgsConstructor public class UserLevelConverter implements SerialMapInterface> { private final UserService userService; private final PointService pointService; @Override public Map convert(Object key, String param) { if (!(key instanceof Long userId)) { return null; } // 获取用户积分 Integer points = pointService.getUserPoints(userId); // 计算等级信息 Map levelInfo = new HashMap<>(); if (points >= 10000) { levelInfo.put("level", "钻石"); levelInfo.put("levelCode", "diamond"); levelInfo.put("color", "#FF6B6B"); } else if (points >= 5000) { levelInfo.put("level", "黄金"); levelInfo.put("levelCode", "gold"); levelInfo.put("color", "#FFD93D"); } else if (points >= 1000) { levelInfo.put("level", "白银"); levelInfo.put("levelCode", "silver"); levelInfo.put("color", "#C0C0C0"); } else { levelInfo.put("level", "青铜"); levelInfo.put("levelCode", "bronze"); levelInfo.put("color", "#CD7F32"); } levelInfo.put("points", points); return levelInfo; } } ``` ### 7.3 带缓存的转换器 ```java /** * 带缓存的转换器示例 */ @Component @SerialMapType(type = "cached_converter") @AllArgsConstructor public class CachedConverter implements SerialMapInterface { private final ExternalApiService externalApiService; @Override public String convert(Object key, String param) { String cacheKey = "external_api:" + key; // 尝试从缓存获取 String cachedResult = CacheUtils.get(CacheNames.EXTERNAL_API, cacheKey); if (cachedResult != null) { return cachedResult; } // 缓存未命中,调用外部API String result = externalApiService.getData(key.toString()); // 缓存结果 if (result != null) { CacheUtils.put(CacheNames.EXTERNAL_API, cacheKey, result, Duration.ofMinutes(30)); } return result; } } ``` ### 7.4 使用自定义转换器 ```java public class OrderVo { private String status; private Long userId; // 使用自定义状态转换器 @SerialMap(converter = "order_status_converter") private String statusLabel; // 使用复杂业务转换器 @SerialMap(converter = "user_level_converter", source = "userId") private Map userLevel; } ``` ## 8. 配置与缓存 ### 8.1 缓存配置 序列化映射模块使用Redis进行缓存,相关配置在 `CacheNames` 中定义: ```java public interface CacheNames { /** * 字段映射缓存 */ String FIELD_MAP = "field_map"; } ``` ### 8.2 缓存策略 * **缓存键格式**:`实体类名:ID:后缀` * **缓存时间**:默认使用Redis配置的过期时间 * **缓存更新**:数据变更时自动清除相关缓存 ### 8.3 性能监控 ```yaml # 开启DEBUG日志可以监控缓存命中情况 logging: level: plus.ruoyi.common.serialmap: DEBUG ``` ## 9. 最佳实践 ### 9.1 命名规范 #### 转换器类型命名 * 使用小写字母和下划线 * 格式:`源_to_目标` 或 `业务_功能` * 示例:`user_id_to_name`、`dict_type_to_label` #### 字段命名 ```java // 推荐:语义明确的字段名 @SerialMap(converter = SerialMapConstant.USER_ID_TO_NAME) private String creatorName; // 而不是 creator @SerialMap(converter = SerialMapConstant.DEPT_ID_TO_NAME) private String belongDeptName; // 而不是 dept ``` ### 9.2 性能优化 #### 批量处理优先 ```java // 推荐:支持批量处理 private String userIds; // "1,2,3" @SerialMap(converter = SerialMapConstant.USER_ID_TO_NICKNAME) private String userNames; // "张三,李四,王五" // 不推荐:多个单独字段 @SerialMap(converter = SerialMapConstant.USER_ID_TO_NICKNAME, source = "userId1") private String userName1; @SerialMap(converter = SerialMapConstant.USER_ID_TO_NICKNAME, source = "userId2") private String userName2; ``` #### 合理使用缓存 ```java // 对于频繁访问的数据,利用缓存优势 @SerialMap(converter = SerialMapConstant.DICT_TYPE_TO_LABEL, param = "sys_user_sex") private String sex; // 字典数据通常变化较少,缓存效果好 ``` ### 9.3 错误处理 #### 空值处理 ```java // 框架会自动处理null值,无需额外判断 @SerialMap(converter = SerialMapConstant.USER_ID_TO_NAME) private String userName; // userId为null时,userName也为null ``` #### 异常容错 ```java // 转换异常时会保留原值,不会中断序列化过程 @SerialMap(converter = "custom_converter") private String customValue; // 转换失败时保持原值 ``` ### 9.4 代码组织 #### VO类设计 ```java public class UserVo { // 原始数据字段 private Long userId; private Long deptId; private String sex; // 映射字段(建议放在一起) @SerialMap(converter = SerialMapConstant.USER_ID_TO_NAME) private String userName; @SerialMap(converter = SerialMapConstant.DEPT_ID_TO_NAME) private String deptName; @SerialMap(converter = SerialMapConstant.DICT_TYPE_TO_LABEL, param = "sys_user_sex") private String sexLabel; } ``` #### 转换器分包管理 ``` com.example.converter/ ├── user/ # 用户相关转换器 │ ├── UserStatusConverter.java │ └── UserLevelConverter.java ├── order/ # 订单相关转换器 │ ├── OrderStatusConverter.java │ └── PaymentMethodConverter.java └── common/ # 通用转换器 ├── DateFormatConverter.java └── MoneyFormatConverter.java ``` ## 10. 常见问题 ### 10.1 转换器未生效 **问题**:标注了`@SerialMap`注解但转换不生效 **排查步骤**: 1. 确认转换器已注册为Spring Bean(使用`@Component`等注解) 2. 确认转换器标注了`@SerialMapType`注解 3. 确认converter值与转换器类型一致 4. 查看启动日志,确认转换器已加载 ```java // 错误示例:未标注@Component @SerialMapType(type = "custom_converter") public class CustomConverter implements SerialMapInterface { ... } // 正确示例 @Component @SerialMapType(type = "custom_converter") public class CustomConverter implements SerialMapInterface { ... } ``` ### 10.2 空指针异常 **问题**:转换过程中出现空指针异常 **解决方案**:在转换器中添加空值检查 ```java @Override public String convert(Object key, String param) { // 添加空值检查 if (key == null) { return null; } // 类型转换前进行检查 if (!(key instanceof Long)) { return null; } Long id = (Long) key; return someService.getNameById(id); } ``` ### 10.3 性能问题 **问题**:大量数据转换导致性能下降 **优化方案**: 1. 使用支持批量处理的转换器 2. 合理利用缓存机制 3. 避免在循环中使用单个ID转换 ```java // 优化前:N次数据库查询 List users = userList.stream().map(user -> { UserVo vo = new UserVo(); vo.setUserId(user.getId()); // 这里会触发N次数据库查询 return vo; }).collect(Collectors.toList()); // 优化后:使用field_map进行批量处理 // 或者预先批量查询相关数据 ``` ### 10.4 循环依赖问题 **问题**:转换器中注入Service时出现循环依赖 **解决方案**: 1. 使用`@Lazy`注解延迟初始化 2. 使用ApplicationContext手动获取Bean 3. 重新设计依赖关系 ```java @Component @SerialMapType(type = "user_converter") public class UserConverter implements SerialMapInterface { // 方案1:使用@Lazy注解 @Lazy private final UserService userService; // 方案2:使用ApplicationContext private final ApplicationContext applicationContext; @Override public String convert(Object key, String param) { UserService service = applicationContext.getBean(UserService.class); return service.getUserNameById((Long) key); } } ``` ## 11. 工作原理 ### 11.1 架构设计 ``` ┌─────────────────────────────────────────────────────────────┐ │ 序列化映射模块架构 │ ├─────────────────────────────────────────────────────────────┤ │ @SerialMap 注解 │ SerialMapHandler │ 转换器注册表 │ │ ↓ │ ↓ │ ↓ │ │ 字段标记 │ 序列化处理 │ 转换器映射 │ │ │ │ │ │ SerialMapContext │ 转换器接口实现 │ Redis缓存 │ │ ↓ │ ↓ │ ↓ │ │ 上下文管理 │ 业务转换逻辑 │ 性能优化 │ └─────────────────────────────────────────────────────────────┘ ``` ### 11.2 执行流程 1. **注解扫描**:Spring启动时扫描所有`@SerialMap`注解 2. **转换器注册**:将标注`@SerialMapType`的转换器注册到映射表 3. **序列化触发**:JSON序列化时触发`SerialMapHandler` 4. **上下文创建**:创建线程级上下文,存储字段信息 5. **转换执行**:根据转换器类型执行相应转换逻辑 6. **缓存处理**:查询缓存或将结果存入缓存 7. **结果输出**:输出转换后的值,清理上下文 ### 11.3 缓存机制 ```java // 缓存键生成规则 String cacheKey = entityClass.getSimpleName() + ":" + id + ":" + suffix; // 缓存查询优先级 1. 检查Redis缓存是否存在 2. 缓存命中:直接返回缓存结果 3. 缓存未命中:执行数据库查询 4. 将查询结果存入Redis缓存 5. 返回查询结果 ``` ## 12. 扩展开发 ### 12.1 自定义注解 可以基于`@SerialMap`创建更具体的业务注解: ```java /** * 用户信息转换注解 */ @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @SerialMap(converter = SerialMapConstant.USER_ID_TO_NAME) public @interface UserName { } /** * 字典转换注解 */ @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @SerialMap(converter = SerialMapConstant.DICT_TYPE_TO_LABEL) public @interface DictLabel { String value(); // 字典类型 } // 使用示例 public class UserVo { @UserName // 等价于 @SerialMap(converter = "user_id_to_name") private String creatorName; @DictLabel("sys_user_sex") // 等价于 @SerialMap(converter = "dict_type_to_label", param = "sys_user_sex") private String sex; } ``` ### 12.2 转换器继承 创建转换器基类,简化开发: ```java /** * 抽象转换器基类 */ public abstract class AbstractSerialMapConverter implements SerialMapInterface { protected final Logger log = LoggerFactory.getLogger(this.getClass()); @Override public final T convert(Object key, String param) { if (key == null) { return null; } try { return doConvert(key, param); } catch (Exception e) { log.error("转换失败: key={}, param={}, error={}", key, param, e.getMessage()); return getDefaultValue(); } } /** * 执行具体转换逻辑 */ protected abstract T doConvert(Object key, String param); /** * 获取默认值(转换失败时返回) */ protected T getDefaultValue() { return null; } } /** * 字典转换器基类 */ public abstract class AbstractDictConverter extends AbstractSerialMapConverter { @Autowired private DictService dictService; @Override protected String doConvert(Object key, String param) { if (StringUtils.isBlank(param)) { log.warn("字典类型参数为空"); return key.toString(); } return dictService.getDictLabel(param, key.toString()); } } ``` ### 12.3 条件转换器 支持条件判断的转换器: ```java /** * 条件转换器:根据不同条件返回不同结果 */ @Component @SerialMapType(type = "conditional_converter") public class ConditionalConverter implements SerialMapInterface { @Override public String convert(Object key, String param) { // param格式:condition1:result1,condition2:result2,default:defaultResult if (StringUtils.isBlank(param)) { return key.toString(); } String keyStr = key.toString(); String[] conditions = param.split(","); for (String condition : conditions) { String[] parts = condition.split(":"); if (parts.length == 2) { if ("default".equals(parts[0]) || keyStr.equals(parts[0])) { return parts[1]; } } } return keyStr; } } // 使用示例 @SerialMap(converter = "conditional_converter", param = "1:启用,0:禁用,default:未知") private String statusText; ``` ### 12.4 组合转换器 支持多个转换器组合使用: ```java /** * 组合转换器:支持多个转换器链式调用 */ @Component @SerialMapType(type = "composite_converter") public class CompositeConverter implements SerialMapInterface { @Override public String convert(Object key, String param) { // param格式:converter1|converter2|converter3 if (StringUtils.isBlank(param)) { return key.toString(); } String[] converters = param.split("\\|"); Object currentValue = key; for (String converterType : converters) { SerialMapInterface converter = SerialMapHandler.CONVERTERS.get(converterType); if (converter != null) { currentValue = converter.convert(currentValue, ""); } } return currentValue != null ? currentValue.toString() : null; } } // 使用示例:先转换为用户名,再转换为大写 @SerialMap(converter = "composite_converter", param = "user_id_to_name|uppercase_converter") private String upperUserName; ``` ## 13. 测试指南 ### 13.1 单元测试 ```java @SpringBootTest class SerialMapTest { @Autowired private ObjectMapper objectMapper; @Test void testUserNameConvert() throws Exception { UserVo userVo = new UserVo(); userVo.setUserId(1L); String json = objectMapper.writeValueAsString(userVo); // 验证转换结果 JsonNode jsonNode = objectMapper.readTree(json); assertThat(jsonNode.get("userName").asText()).isEqualTo("admin"); } @Test void testDictConvert() throws Exception { UserVo userVo = new UserVo(); userVo.setSex("1"); String json = objectMapper.writeValueAsString(userVo); JsonNode jsonNode = objectMapper.readTree(json); assertThat(jsonNode.get("sexLabel").asText()).isEqualTo("男"); } @Test void testFieldMapConvert() throws Exception { UserVo userVo = new UserVo(); userVo.setUserId(1L); String json = objectMapper.writeValueAsString(userVo); JsonNode jsonNode = objectMapper.readTree(json); // 验证对象映射 assertThat(jsonNode.has("userInfo")).isTrue(); assertThat(jsonNode.get("userInfo").get("nickName").asText()).isNotEmpty(); } } ``` ### 13.2 集成测试 ```java @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = { "spring.redis.host=localhost", "spring.redis.port=6379" }) class SerialMapIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Test void testApiResponse() { ResponseEntity response = restTemplate.getForEntity("/user/list", String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); // 验证返回的JSON中包含转换后的字段 String body = response.getBody(); assertThat(body).contains("userName"); assertThat(body).contains("deptName"); assertThat(body).contains("sexLabel"); } } ``` ### 13.3 性能测试 ```java @Component class SerialMapPerformanceTest { @Autowired private ObjectMapper objectMapper; @Test void testPerformance() throws Exception { List users = generateTestData(1000); long startTime = System.currentTimeMillis(); for (UserVo user : users) { objectMapper.writeValueAsString(user); } long endTime = System.currentTimeMillis(); long duration = endTime - startTime; System.out.println("转换1000个对象耗时: " + duration + "ms"); // 验证性能指标 assertThat(duration).isLessThan(5000); // 应该在5秒内完成 } private List generateTestData(int count) { return IntStream.range(1, count + 1) .mapToObj(i -> { UserVo user = new UserVo(); user.setUserId((long) i); user.setDeptId((long) (i % 10 + 1)); user.setSex(i % 2 == 0 ? "1" : "0"); return user; }) .collect(Collectors.toList()); } } ``` ## 14. 监控与调试 ### 14.1 日志配置 ```yaml # application.yml logging: level: plus.ruoyi.common.serialmap: DEBUG plus.ruoyi.common.serialmap.core.handler: TRACE plus.ruoyi.common.serialmap.core.impl: DEBUG ``` ### 14.2 监控指标 ```java /** * 转换器性能监控 */ @Component public class SerialMapMetrics { private final MeterRegistry meterRegistry; private final Counter conversionCounter; private final Timer conversionTimer; public SerialMapMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.conversionCounter = Counter.builder("serialmap.conversions.total") .description("Total number of conversions") .register(meterRegistry); this.conversionTimer = Timer.builder("serialmap.conversion.duration") .description("Conversion duration") .register(meterRegistry); } public void recordConversion(String converterType, Duration duration) { conversionCounter.increment(Tags.of("converter", converterType)); conversionTimer.record(duration); } } ``` ### 14.3 调试技巧 ```java // 开启详细日志 @Component @Slf4j public class DebugConverter implements SerialMapInterface { @Override public String convert(Object key, String param) { log.debug("转换开始 - key: {}, param: {}", key, param); try { String result = doActualConversion(key, param); log.debug("转换成功 - 结果: {}", result); return result; } catch (Exception e) { log.error("转换失败 - key: {}, param: {}, error: {}", key, param, e.getMessage(), e); throw e; } } private String doActualConversion(Object key, String param) { // 实际转换逻辑 return "converted_" + key; } } ``` ## 16. FAQ ### Q1: 为什么转换器不生效? **A**: 检查以下几点: 1. 转换器类是否标注了`@Component`和`@SerialMapType`注解 2. 注解中的type值是否与使用时的converter值一致 3. 检查Spring容器是否成功加载了转换器(查看启动日志) ### Q2: 如何提高转换性能? **A**: 性能优化建议: 1. 使用批量转换器(如支持逗号分隔ID的转换器) 2. 合理利用Redis缓存 3. 避免在循环中进行单个转换 4. 考虑使用`field_map`进行批量对象查询 ### Q3: 转换结果为null是正常的吗? **A**: 以下情况会返回null: 1. 源字段值为null 2. 转换器查询不到对应数据 3. 转换过程中发生异常(会有错误日志) 4. 转换器主动返回null ### Q4: 可以在转换器中注入其他Service吗? **A**: 可以,但需要注意: 1. 使用`@Lazy`注解避免循环依赖 2. 考虑使用ApplicationContext手动获取Bean 3. 避免在转换器中进行复杂的业务逻辑处理 ### Q5: 如何处理大数据量的转换? **A**: 大数据量处理策略: 1. 使用分页处理,避免一次性加载过多数据 2. 实现异步转换机制 3. 使用Redis集群提高缓存性能 4. 考虑将转换逻辑移至数据库层面 ### Q6: 转换器支持国际化吗? **A**: 支持,可以这样实现: ```java @Component @SerialMapType(type = "i18n_converter") public class I18nConverter implements SerialMapInterface { @Autowired private MessageSource messageSource; @Override public String convert(Object key, String param) { Locale locale = LocaleContextHolder.getLocale(); return messageSource.getMessage(param + "." + key, null, locale); } } ``` --- --- url: 'https://ruoyi.plus/mobile/build/store-publish.md' --- # 应用商店发布 --- --- url: 'https://ruoyi.plus/frontend/stores/app-store.md' --- # 应用状态 (app) --- --- url: 'https://ruoyi.plus/mobile/uniapp/app-config.md' --- # 应用配置 (uni.scss) --- --- url: 'https://ruoyi.plus/frontend/dev/best-practices.md' --- # 开发最佳实践 --- --- url: 'https://ruoyi.plus/backend/common/core/exception.md' --- # 异常处理 ## 概述 ruoyi-common-core 提供了一套完整的异常处理体系,采用分层设计和统一管理的方式,确保系统异常的规范化处理。该体系支持国际化消息、错误码管理、异常分类等功能,为系统提供稳定可靠的异常处理机制。 ## 异常体系架构 ### 层次结构 ``` BaseException (基础异常) ├── BaseBusinessException (业务异常基类) │ ├── ServiceException (通用业务异常) │ └── SseException (SSE专用异常) └── 模块化异常 ├── UserException (用户异常) ├── FileException (文件异常) └── CaptchaException (验证码异常) ``` ### 设计原则 * **分层设计**: 基础异常 → 业务异常 → 模块异常 * **国际化支持**: 所有异常消息支持多语言 * **错误码管理**: 统一的错误码体系 * **链式操作**: 支持流式API调用 * **向下兼容**: 保持API稳定性 ## 核心异常类 ### 1. BaseException (基础异常类) 所有自定义异常的根基类,提供国际化消息和错误码支持。 ```java public abstract class BaseException extends RuntimeException { private String module; // 所属模块 private String code; // 错误码 private Object[] args; // 错误码参数 private String defaultMessage; // 默认错误消息 } ``` **核心特性:** * 自动国际化消息处理 * 支持模块化错误管理 * 错误码参数化支持 * 异常链传递 **使用示例:** ```java // 基础构造 throw new UserException("user.not.found", userId); // 带模块信息 throw new UserException("user", "password.invalid", null, "密码错误"); // 带原因异常 throw new FileException("file.upload.failed", cause); ``` ### 2. BaseBusinessException (业务异常基类) 继承自 BaseException,专门用于业务逻辑异常处理。 ```java public abstract class BaseBusinessException extends BaseException { private Integer businessCode; // 业务错误码 private String detailMessage; // 详细错误信息 // 支持链式调用 public BaseBusinessException setMessage(String message) { /* ... */ } public BaseBusinessException setBusinessCode(Integer code) { /* ... */ } public BaseBusinessException setDetailMessage(String detail) { /* ... */ } } ``` **适用场景:** * 业务规则验证失败 * 数据状态不正确 * 业务流程异常 **使用示例:** ```java // 链式设置 throw new ServiceException("订单状态错误") .setBusinessCode(4001) .setDetailMessage("订单已支付,无法取消"); // 快速创建 throw ServiceException.of("库存不足", 4002); ``` ## 模块化异常 ### 1. ServiceException (通用业务异常) 最常用的业务异常类,适用于大部分业务场景。 ```java public final class ServiceException extends BaseBusinessException { // 基础构造方法 public ServiceException(String message); public ServiceException(String message, Integer businessCode); public ServiceException(String message, Integer businessCode, String detailMessage); // 静态工厂方法 public static ServiceException of(String message); public static ServiceException of(String message, Integer businessCode); // 条件抛出 public static void throwIf(boolean condition, String message); public static void notNull(Object object, String message); } ``` **使用场景:** ```java // 参数校验 ServiceException.notNull(user, "用户不能为空"); // 条件判断 ServiceException.throwIf(balance < amount, "余额不足"); // 业务规则 if (order.getStatus().equals("PAID")) { throw ServiceException.of("订单已支付,无法修改", 4001); } // 国际化消息 throw ServiceException.of("order.status.invalid"); ``` ### 2. UserException (用户异常) 专门处理用户相关的异常,如登录、注册、权限等。 ```java public class UserException extends BaseException { private static final String MODULE = "user"; // 构造方法 public UserException(String code, Object... args); public UserException(String code, Object[] args, String defaultMessage); // 静态工厂方法 public static UserException of(String code, Object... args); public static void throwIf(boolean condition, String code, Object... args); public static void notNull(Object object, String code, Object... args); } ``` **常用错误码:** ```java public interface UserErrors { String ACCOUNT_NOT_EXISTS = "user.account.not.exists"; String PASSWORD_MISMATCH = "user.password.mismatch"; String ACCOUNT_DISABLED = "user.account.disabled"; String SESSION_EXPIRED = "user.session.expired"; String PASSWORD_RETRY_LOCKED = "user.password.retry.locked"; } ``` **使用示例:** ```java // 账户不存在 throw UserException.of("user.account.not.exists", username); // 密码错误次数 throw UserException.of("user.password.retry.count", retryCount); // 账户锁定 throw UserException.of("user.password.retry.locked", retryCount, lockTime); // 条件检查 UserException.throwIf(user == null, "user.not.found", userId); ``` ### 3. FileException (文件异常) 专门处理文件操作相关的异常。 ```java public class FileException extends BaseException { private static final String MODULE = "file"; public static FileException of(String code, Object... args); public static FileException of(String defaultMessage); } ``` **使用示例:** ```java // 文件大小超限 throw FileException.of("file.upload.size.exceed.limit", maxSize); // 文件类型不支持 throw FileException.of("file.upload.type.not.supported"); // 文件上传失败 throw FileException.of("文件上传失败: " + e.getMessage()); ``` ### 4. CaptchaException (验证码异常) 处理验证码相关的异常。 ```java // 验证码错误 public class CaptchaException extends UserException { public static CaptchaException of() { return new CaptchaException(); } } // 验证码过期 public class CaptchaExpireException extends UserException { public static CaptchaExpireException of() { return new CaptchaExpireException(); } } ``` **使用示例:** ```java // 验证码校验 if (!captchaService.validate(code, uuid)) { throw CaptchaException.of(); } // 验证码过期 if (captchaService.isExpired(uuid)) { throw CaptchaExpireException.of(); } ``` ## 国际化支持 ### 消息键定义 异常消息支持国际化,通过 I18nKeys 接口统一管理消息键。 ```java public interface I18nKeys { interface User { String ACCOUNT_NOT_EXISTS = "user.account.not.exists"; String PASSWORD_MISMATCH = "user.password.mismatch"; String ACCOUNT_DISABLED = "user.account.disabled"; String SESSION_EXPIRED = "user.session.expired"; String PASSWORD_RETRY_LOCKED = "user.password.retry.locked"; } interface FileUpload { String SIZE_EXCEED_LIMIT = "file.upload.size.exceed.limit"; String TYPE_NOT_SUPPORTED = "file.upload.type.not.supported"; String FAILED = "file.upload.failed"; } interface VerifyCode { String CAPTCHA_INVALID = "verify.code.captcha.invalid"; String CAPTCHA_EXPIRED = "verify.code.captcha.expired"; String SMS_INVALID = "verify.code.sms.invalid"; } } ``` ### 自动消息处理 BaseException 会自动识别国际化键并进行消息转换。 ```java @Override public String getMessage() { String message = null; // 检查是否为国际化键格式 if (StringUtils.isNotBlank(code) && RegexValidator.isValidI18nKey(code)) { message = MessageUtils.message(code, args); } if (message == null) { message = defaultMessage; } return message; } ``` ### 多语言配置 **messages.properties (默认-中文)** ```properties user.account.not.exists=对不起, 您的账号:{0} 不存在 user.password.mismatch=用户不存在/密码错误 user.account.disabled=对不起,您的账号:{0} 已禁用,请联系管理员 user.password.retry.locked=密码输入错误{0}次,帐户锁定{1}分钟 file.upload.size.exceed.limit=上传的文件大小超出限制的文件大小!允许的文件最大大小是:{0}MB! ``` **messages\_en.properties (英文)** ```properties user.account.not.exists=Sorry, your account: {0} does not exist user.password.mismatch=User does not exist/password error user.account.disabled=Sorry, your account: {0} has been disabled, please contact the administrator user.password.retry.locked=Password input error {0} times, account locked for {1} minutes file.upload.size.exceed.limit=The uploaded file size exceeds the limit! Maximum allowed file size is: {0}MB! ``` ## 与响应体系集成 ### 异常自动转换 异常可以自动转换为统一的响应格式 R\。 ```java @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ServiceException.class) public R handleServiceException(ServiceException e) { return R.fail(e.getMessage()); } @ExceptionHandler(UserException.class) public R handleUserException(UserException e) { return R.fail(e.getMessage()); } @ExceptionHandler(FileException.class) public R handleFileException(FileException e) { return R.fail(e.getMessage()); } } ``` ### 业务逻辑中的使用 ```java @Service public class UserService { public UserVo login(LoginBo bo) { // 用户不存在 User user = userMapper.selectByUsername(bo.getUsername()); UserException.notNull(user, "user.account.not.exists", bo.getUsername()); // 账户被禁用 UserException.throwIf("0".equals(user.getStatus()), "user.account.disabled", user.getUsername()); // 密码错误 if (!passwordEncoder.matches(bo.getPassword(), user.getPassword())) { throw UserException.of("user.password.mismatch"); } return MapstructUtils.convert(user, UserVo.class); } public void updateUser(UserBo bo) { // 参数校验 ServiceException.notNull(bo.getId(), "用户ID不能为空"); // 业务校验 User existUser = userMapper.selectById(bo.getId()); ServiceException.throwIf(existUser == null, "用户不存在"); // 状态检查 if ("1".equals(existUser.getDelFlag())) { throw ServiceException.of("用户已删除,无法修改", 4001); } } } ``` ## 异常处理最佳实践 ### 1. 异常选择指南 ```java // ✅ 推荐: 使用具体的异常类型 throw UserException.of("user.not.found", userId); // ❌ 不推荐: 使用通用异常 throw new RuntimeException("用户不存在"); // ✅ 推荐: 业务异常使用ServiceException throw ServiceException.of("订单状态错误", 4001); // ✅ 推荐: 条件检查使用静态方法 ServiceException.throwIf(amount <= 0, "金额必须大于0"); ``` ### 2. 错误消息规范 ```java // ✅ 推荐: 使用国际化键 throw UserException.of("user.password.invalid"); // ✅ 推荐: 带参数的国际化 throw UserException.of("user.login.locked", lockTime); // ❌ 不推荐: 硬编码中文消息 throw new ServiceException("用户名不能为空"); // ✅ 推荐: 英文默认消息 throw ServiceException.of("Username cannot be empty"); ``` ### 3. 异常链传递 ```java // ✅ 推荐: 保留原异常信息 try { userMapper.insert(user); } catch (DataAccessException e) { throw ServiceException.of("用户创建失败", e); } // ✅ 推荐: 记录详细错误 catch (Exception e) { log.error("用户服务异常: userId={}, error={}", userId, e.getMessage(), e); throw ServiceException.of("系统内部错误"); } ``` ### 4. 业务异常设计 ```java @Service public class OrderService { public void cancelOrder(Long orderId) { Order order = orderMapper.selectById(orderId); // 参数校验异常 ServiceException.notNull(order, "订单不存在"); // 业务规则异常 if ("PAID".equals(order.getStatus())) { throw ServiceException.of("order.cannot.cancel.paid", 4001) .setDetailMessage("订单号: " + order.getOrderNo()); } if ("DELIVERED".equals(order.getStatus())) { throw ServiceException.of("order.cannot.cancel.delivered", 4002); } // 执行取消逻辑 order.setStatus("CANCELLED"); orderMapper.updateById(order); } } ``` ### 5. 异常日志记录 ```java @Component public class ExceptionLogger { private static final Logger log = LoggerFactory.getLogger(ExceptionLogger.class); @EventListener public void handleServiceException(ServiceException e) { // 业务异常记录为WARN级别 log.warn("业务异常: code={}, message={}", e.getBusinessCode(), e.getMessage()); } @EventListener public void handleUserException(UserException e) { // 用户异常记录为INFO级别 log.info("用户异常: module={}, code={}, message={}", e.getModule(), e.getCode(), e.getMessage()); } @EventListener public void handleSystemException(Exception e) { // 系统异常记录为ERROR级别 log.error("系统异常: ", e); } } ``` ## 自定义异常 ### 创建模块异常 ```java // 1. 继承BaseException public class PaymentException extends BaseException { private static final String MODULE = "payment"; public PaymentException(String code, Object... args) { super(MODULE, code, args, null); } public PaymentException(String code, Object[] args, String defaultMessage) { super(MODULE, code, args, defaultMessage); } // 静态工厂方法 public static PaymentException of(String code, Object... args) { return new PaymentException(code, args); } public static void throwIf(boolean condition, String code, Object... args) { if (condition) { throw new PaymentException(code, args); } } } // 2. 定义错误码 public interface PaymentErrors { String INSUFFICIENT_BALANCE = "payment.insufficient.balance"; String PAYMENT_EXPIRED = "payment.expired"; String INVALID_PAYMENT_METHOD = "payment.method.invalid"; } // 3. 使用示例 public class PaymentService { public void processPayment(PaymentBo bo) { // 余额不足 PaymentException.throwIf(balance < amount, PaymentErrors.INSUFFICIENT_BALANCE, balance, amount); // 支付方式无效 if (!isValidPaymentMethod(bo.getMethod())) { throw PaymentException.of(PaymentErrors.INVALID_PAYMENT_METHOD, bo.getMethod()); } } } ``` ### 特殊异常处理 ```java // SSE推送异常 public class SseException extends BaseBusinessException { public SseException(String message) { super(message); } public static SseException of(String message) { return new SseException(message); } public static SseException of(String message, Integer businessCode) { return new SseException(message, businessCode); } } // 使用示例 @RestController public class SseController { @GetMapping("/sse") public SseEmitter subscribe() { try { return sseService.createConnection(); } catch (Exception e) { throw SseException.of("SSE连接创建失败: " + e.getMessage()); } } } ``` ## 注意事项 ### 1. 性能考虑 * 异常创建有性能开销,避免在循环中频繁抛出 * 使用静态工厂方法减少对象创建 * 异常信息避免过长,影响日志性能 ### 2. 内存管理 * 避免在异常中保存大对象引用 * 及时释放异常相关资源 * 注意异常对象的生命周期 ### 3. 安全考虑 * 不要在异常消息中暴露敏感信息 * 对外接口的异常消息要做脱敏处理 * 记录异常日志时注意数据安全 ### 4. 国际化注意 * 异常消息的参数顺序在不同语言中可能不同 * 使用占位符时要考虑语法结构差异 * 测试多语言环境下的异常显示 ### 5. 向后兼容 * 新增异常类型要保持API稳定 * 错误码一旦发布不要随意修改 * 消息格式变更要考虑客户端兼容性 --- --- url: 'https://ruoyi.plus/mobile/build/wechat-deploy.md' --- # 微信小程序发布 --- --- url: 'https://ruoyi.plus/mobile/api/wechat.md' --- # 微信小程序接口 --- --- url: 'https://ruoyi.plus/mobile/platform/wechat.md' --- # 微信小程序适配 --- --- url: 'https://ruoyi.plus/backend/getting-started.md' --- # 快速启动 本章节将带你快速启动 Ruoyi-Plus-Uniapp 后端项目,包含完整的环境配置和启动流程。 ::: tip 💡 新手建议 开发项目前建议学习 IDEA 操作和 Git 管理等技巧,可参考教学视频: [IDEA 使用教程](https://www.bilibili.com/video/BV1RM411o7kN) ::: ## 🎯 环境要求 在开始之前,请确保你的开发环境已满足以下要求: ### 核心环境 * **Java**: OpenJDK 21+ * 📥 [JDK 下载地址](https://openjdk.org/projects/jdk/21/) * 推荐使用 OpenJDK 或 Oracle JDK * **数据库**: * **MySQL 8.0+** (推荐) * Oracle >= 12c * PostgreSQL 13/14/15 * SQL Server 2017/2019 * **缓存**: Redis 6.x / 7.x * ⚠️ **注意**: 禁止使用 Redis 7.4 版本 * 📥 [Windows Redis 下载地址](https://github.com/tporadowski/redis/releases) * **构建工具**: Maven 3.6+ ### 文件存储(可选) 支持多种存储方式: * **本地上传**: 支持本地文件系统存储 * **云存储**: 阿里云OSS / 腾讯云COS / 七牛云等一切支持 S3 协议的云存储 * **MinIO**: 私有对象存储 * 可选安装,最后推荐版本:2025-04-22T22-12-26Z * 更高版本功能被阉割,不推荐使用 ### 开发工具 * **IntelliJ IDEA**: 2025.1+ (强烈推荐) * ⚠️ **避免使用** IDEA 2023 版本 * **HBuilderX**: 仅在开发 App 时需要 ## 🔌 IDEA 插件推荐 安装以下插件可显著提升开发效率: ### 代码增强插件 * **Show Comment**: 支持代码注释的可视化显示 * **MybatisX**: 支持 MyBatis mapper 与 XML 文件的代码提示和跳转 * **CodeGlancePro**: 提供代码预览和快速拖动功能 * **Rainbow Brackets Lite**: 代码括号高亮显示 * **Unocss**: 支持 UnoCss 语法提示和补全 ## ⚙️ IDEA 配置 ### 项目编码配置 确保项目使用正确的字符编码: 1. 打开设置:`File` → `Settings` → `Editor` → `File Encodings` 2. 配置以下选项: ``` Global Encoding: UTF-8 Project Encoding: UTF-8 Default encoding for properties files: UTF-8 ``` ### 配置运行看板 为了更好地管理和监控项目运行状态: #### 启用 Services 窗口 1. 点击菜单:`View` → `Tool Windows` → `Services` #### 添加 Spring Boot 配置 1. 在 Services 窗口中右键点击 `+` 号 2. 选择 `Run Configuration Type` → `Spring Boot` 3. 可以查看和管理所有 Spring Boot 应用的运行状态 #### 添加 Docker 配置(可选) 1. 在 Services 窗口中右键点击 `+` 号 2. 选择 `Run Configuration Type` → `Docker` 3. 可以查看 Docker 构建配置和容器状态 ## 📁 项目结构 ``` ruoyi-plus-uniapp/ ├── ruoyi-admin/ # 后端主应用模块 ├── ruoyi-common/ # 公共模块 ├── ruoyi-extend/ # 扩展模块 ├── ruoyi-modules/ # 业务模块 ├── script/ # 脚本文件 │ ├── sql/ # 数据库脚本 │ └── docker/ # Docker 配置 └── pom.xml # Maven 父级配置 ``` ## 🚀 快速启动步骤 ### 步骤1:项目导入与环境配置 #### 1.1 导入项目 1. 使用 IntelliJ IDEA 打开项目根目录 2. 等待自动安装 Maven 依赖,如未自动安装请点击右上角刷新依赖图标 #### 1.2 配置 JDK 1. 点击菜单 `File` → `Project Structure` 2. 在左侧选择 `Project Settings` → `Project` 3. 将 `SDK` 设置为 **JDK 21 或更高版本** ### 步骤2:项目标识符配置(新项目必需) > ⚠️ **注意**:如果只是学习现有项目,可以跳过此步骤 如果是开发新项目,需要为项目制定唯一标识符: #### 标识符要求 * **长度**:不宜过长, 建议20个字符以内 * **格式**:英文字母和下划线组合 * **示例**:`mall`、`crm_sys`、`blog_app` #### 全局替换操作 1. **替换项目标识符** ``` 全局搜索:ryplus_uni 替换为:your_project_name ``` 2. **替换端口号** ``` 全局搜索:5500(记得勾选"整词搜索"W) 替换为:your_unique_port ``` ::: warning 📌 重要说明 项目标识符将影响以下配置,请确保唯一性: * 应用名称 * 数据库名称 * Redis前缀 * 浏览器缓存前缀 * 客户端名称 * 文件上传前缀目录 * snailjob分组名称 ::: ### 步骤3:数据库配置 #### 3.1 数据库脚本说明 根目录 `script/` 下包含以下 SQL 脚本: | 脚本文件 | 说明 | 是否必需 | |---------|------|----------| | `ry_plus_sys.sql` | 系统核心表 | ✅ 必需 | | `ry_plus_job.sql` | 定时任务表 | 🔧 按需安装 | | `ry_plus_app.sql` | 移动端业务表 | 📱 按需安装 | ::: tip 💡 数据库建议 * 定时任务数据建议与主数据库使用**不同的库名** * 使用 `utf8mb4` 字符集,排序规则为 `utf8mb4_general_ci` * 请手动创建数据库并执行对应的 SQL 脚本 ::: ### 步骤4:配置应用参数 编辑 `ruoyi-admin/src/main/resources/application-dev.yml` 中的配置: #### 4.1 配置 API 基础路径 ```yaml app: base-api: http://localhost:5500 # 如无内网映射使用此地址 # base-api: https://your-domain.com # 如有内网映射则修改为映射基础路径 ``` ::: warning 📌 重要说明 * **无内网映射**:使用 `http://localhost:5500` * **有内网映射**:修改为你的内网映射基础路径,如 `https://your-domain.com` ::: ### 步骤5:启动应用 #### 5.1 启动主应用 1. **方式一**:在右上角找到 `RuoyiPlus` 启动配置并点击启动 2. **方式二**:如果不存在启动配置,请找到以下文件并启动: ``` ruoyi-admin/src/main/java/plus/ruoyi/RuoyiPlus.java ``` 右键选择 `Run 'RuoyiPlus'` #### 5.2 启动扩展服务(可选) 在 `ruoyi-extend` 目录下可以启动: * 🕐 **定时任务服务**:分布式任务调度 * 📊 **Admin 监控服务**:应用监控管理 启动成功后访问:**http://localhost:5500** ::: tip 🎉 恭喜完成! 如果以上步骤都成功完成,说明你已经成功启动了 Ruoyi-Plus-Uniapp 后端项目!现在可以开始后端开发了。 ::: --- --- url: 'https://ruoyi.plus/mobile/performance/overview.md' --- # 性能优化概览 --- --- url: 'https://ruoyi.plus/frontend/dev/performance.md' --- # 性能分析 --- --- url: 'https://ruoyi.plus/mobile/build/overview.md' --- # 打包配置概览 --- --- url: 'https://ruoyi.plus/backend/extend/extension-development.md' --- # 扩展开发指南 --- --- url: 'https://ruoyi.plus/frontend/architecture/tech-stack.md' --- # 技术栈介绍 --- --- url: 'https://ruoyi.plus/mobile/platform/toutiao.md' --- # 抖音小程序适配 --- --- url: 'https://ruoyi.plus/frontend/directives/overview.md' --- # 指令概览 --- --- url: 'https://ruoyi.plus/practices/api-security.md' --- # 接口安全 --- --- url: 'https://ruoyi.plus/mobile/api/config.md' --- # 接口配置 --- --- url: 'https://ruoyi.plus/mobile/plugins/push.md' --- # 推送插件 --- --- url: 'https://ruoyi.plus/mobile/plugins/overview.md' --- # 插件概览 --- --- url: 'https://ruoyi.plus/frontend/components/search-form.md' --- # 搜索表单 (ASearchForm) --- --- url: 'https://ruoyi.plus/mobile/build/alipay-deploy.md' --- # 支付宝小程序发布 --- --- url: 'https://ruoyi.plus/mobile/platform/alipay.md' --- # 支付宝小程序适配 --- --- url: 'https://ruoyi.plus/mobile/api/payment.md' --- # 支付接口 --- --- url: 'https://ruoyi.plus/mobile/plugins/payment.md' --- # 支付插件 --- --- url: 'https://ruoyi.plus/backend/common/pay.md' --- # 支付模块 (pay) ## 概述 RuoYi-Plus 支付模块 (`ruoyi-common-pay`) 是一个统一的支付服务解决方案,提供了完整的支付、退款、回调处理功能。模块采用策略模式设计,支持多种支付方式的无缝集成。 ### 核心特性 * 🚀 **统一接口**: 提供统一的支付、退款、查询接口 * 💳 **多支付方式**: 支持微信支付、支付宝、余额支付等 * 🏢 **多租户支持**: 完整的多租户配置管理 * 🔒 **安全可靠**: 完整的签名验证和回调处理 * 📊 **配置管理**: 智能化的配置初始化和管理 * 🎯 **事件驱动**: 支付成功事件发布机制 ### 支持的支付方式 | 支付方式 | 支持类型 | Handler类 | |---------|---------|-----------| | 微信支付 | JSAPI、NATIVE、APP、H5 | `WxPayHandler` | | 支付宝 | WAP、PAGE、APP | `AliPayHandler` | | 余额支付 | 账户余额 | `BalancePayHandler` | ## 快速开始 ### 1. 添加依赖 ```xml plus.ruoyi ruoyi-common-pay ``` ### 2. 基础使用 ```java @Autowired private PayService payService; // 发起支付 PayRequest request = PayRequest.createWxJsapiRequest( appId, mchId, "商品描述", outTradeNo, new BigDecimal("0.01"), openId, clientIp ); PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request); // 申请退款 RefundRequest refundRequest = RefundRequest.createWxRefundRequest( appId, mchId, outTradeNo, outRefundNo, totalFee, refundFee, "退款原因" ); RefundResponse refundResponse = payService.refund(DictPaymentMethod.WECHAT, refundRequest); ``` ## 架构设计 ### 整体架构 ```mermaid graph TB A[Controller层] --> B[PayService统一服务] B --> C{支付方式路由} C --> D[WxPayHandler] C --> E[AliPayHandler] C --> F[BalancePayHandler] D --> G[微信支付API] E --> H[支付宝API] F --> I[余额业务逻辑] J[PayConfigManager] --> K[配置初始化] K --> L[WxPayInitializer] K --> M[AliPayInitializer] N[回调处理] --> B B --> O[PaySuccessEvent] ``` ### 核心组件 #### PayService - 统一服务入口 负责路由不同支付方式到对应的处理器,提供统一的调用接口。 ```java @Service public class PayService { // 根据支付方式自动路由到对应处理器 public PayResponse pay(DictPaymentMethod paymentMethod, PayRequest request); public RefundResponse refund(DictPaymentMethod paymentMethod, RefundRequest request); public NotifyResponse handleNotify(DictPaymentMethod paymentMethod, NotifyRequest request); } ``` #### PayHandler - 支付处理器接口 定义了支付处理器的标准接口,所有支付方式都需要实现此接口。 ```java public interface PayHandler { DictPaymentMethod getPaymentMethod(); PayResponse pay(PayRequest request); RefundResponse refund(RefundRequest request); PayResponse queryPayment(String outTradeNo, String appid); RefundResponse queryRefund(String outRefundNo, String appid); NotifyResponse handleNotify(NotifyRequest request); } ``` #### PayConfigManager - 配置管理器 管理所有支付配置,支持多租户场景下的配置隔离。 ```java @Service public class PayConfigManager { // 配置格式: {tenantId}:{paymentMethod}:{appid} public PayConfig getConfig(String tenantId, String paymentMethod, String appid); public void registerConfig(PayConfig config); public List getConfigsByTenant(String tenantId); } ``` ## 支付方式详解 ### 微信支付 #### 支持类型 * **JSAPI**: 微信小程序/公众号支付 * **NATIVE**: 扫码支付 * **APP**: APP支付 * **H5**: 手机网页支付 #### 配置要求 ```java // 必需配置 String appId = "wx1234567890123456"; String mchId = "1234567890"; String mchKey = "your_wx_api_key"; // 可选配置 String certPath = "cert/apiclient_cert.p12"; // 退款需要 String apiV3Key = "your_api_v3_key"; // API v3需要 ``` #### 使用示例 **JSAPI支付 (小程序/公众号)** ```java PayRequest request = PayRequest.createWxJsapiRequest( appId, mchId, "商品描述", outTradeNo, new BigDecimal("0.01"), openId, clientIp ); PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request); // 返回的payInfo可直接用于前端调起支付 Map payInfo = response.getPayInfo(); ``` **NATIVE支付 (扫码)** ```java PayRequest request = PayRequest.createWxNativeRequest( appId, mchId, "商品描述", outTradeNo, new BigDecimal("0.01"), clientIp ); PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request); // 获取二维码 String qrCodeBase64 = response.getQrCodeBase64(); String codeUrl = response.getCodeUrl(); ``` **H5支付 (手机网页)** ```java PayRequest request = PayRequest.createWxH5Request( appId, mchId, "商品描述", outTradeNo, new BigDecimal("0.01"), clientIp, sceneInfo ); PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request); // 跳转到支付页面 String payUrl = response.getPayUrl(); ``` ### 支付宝 #### 支持类型 * **WAP**: 手机网站支付 * **PAGE**: 电脑网站支付 * **APP**: APP支付 #### 配置要求 ```java // 必需配置 String appId = "2021000000000000"; String privateKey = "your_private_key"; String alipayPublicKey = "alipay_public_key"; // 证书模式 (推荐) String certPath = "cert/appCertPublicKey.crt"; String keyPath = "cert/alipayCertPublicKey_RSA2.crt"; ``` #### 使用示例 **WAP支付 (手机网站)** ```java PayRequest request = PayRequest.createAlipayWapRequest( appId, "商品描述", outTradeNo, new BigDecimal("0.01"), returnUrl ); PayResponse response = payService.pay(DictPaymentMethod.ALIPAY, request); // 返回支付表单,直接输出到页面即可 String payForm = response.getPayForm(); ``` **PAGE支付 (电脑网站)** ```java PayRequest request = PayRequest.createAlipayPageRequest( appId, "商品描述", outTradeNo, new BigDecimal("0.01"), returnUrl ); PayResponse response = payService.pay(DictPaymentMethod.ALIPAY, request); String payForm = response.getPayForm(); ``` **APP支付** ```java PayRequest request = PayRequest.createAlipayAppRequest( appId, "商品描述", outTradeNo, new BigDecimal("0.01") ); PayResponse response = payService.pay(DictPaymentMethod.ALIPAY, request); // 返回APP调起参数 String payInfo = response.getPayUrl(); ``` ### 余额支付 余额支付是系统内置的支付方式,无需第三方接口,适合积分、储值卡等场景。 ```java PayRequest request = PayRequest.createBalanceRequest( outTradeNo, new BigDecimal("10.00"), "余额支付", clientIp ); PayResponse response = payService.pay(DictPaymentMethod.BALANCE, request); ``` ## 退款处理 ### 微信退款 ```java RefundRequest request = RefundRequest.createWxRefundRequest( appId, mchId, outTradeNo, outRefundNo, totalFee, refundFee, "用户申请退款" ); RefundResponse response = payService.refund(DictPaymentMethod.WECHAT, request); ``` ### 支付宝退款 ```java RefundRequest request = RefundRequest.createAlipayRefundRequest( appId, outTradeNo, outRefundNo, refundFee, "退款原因" ); RefundResponse response = payService.refund(DictPaymentMethod.ALIPAY, request); ``` ### 余额退款 ```java RefundRequest request = RefundRequest.createBalanceRefundRequest( outTradeNo, outRefundNo, refundFee, "余额退款" ); RefundResponse response = payService.refund(DictPaymentMethod.BALANCE, request); ``` ## 回调处理 ### 回调地址规则 系统自动生成回调地址,格式为: ``` {baseApi}/payment/notify/{paymentMethod}/{merchantId} ``` 例如: * 微信支付: `https://api.example.com/payment/notify/wechat/1234567890` * 支付宝: `https://api.example.com/payment/notify/alipay/2021000000000000` ### 回调验证流程 1. **数据完整性检查**: 验证必需参数 2. **签名验证**: 使用对应平台的公钥/密钥验证签名 3. **业务状态检查**: 确认支付状态为成功 4. **事件发布**: 发布 `PaySuccessEvent` 事件 5. **响应返回**: 返回平台要求的格式 ### 监听支付成功事件 ```java @Component @Slf4j public class PaymentEventListener { @EventListener public void handlePaymentSuccess(PaySuccessEvent event) { String outTradeNo = event.getOutTradeNo(); String totalFee = event.getTotalFee(); String paymentMethod = event.getPaymentMethod(); // 处理业务逻辑 // 1. 更新订单状态 // 2. 发货处理 // 3. 积分发放 // 4. 发送通知 log.info("支付成功: 订单={}, 金额={}, 方式={}", outTradeNo, event.getTotalAmountYuan(), paymentMethod); } } ``` ## 配置管理 ### 配置初始化 系统启动时会自动初始化所有租户的支付配置: 1. **扫描租户**: 从平台配置和支付配置中获取所有租户ID 2. **构建配置**: 为每个租户构建 `PayConfig` 对象 3. **分组初始化**: 按支付方式分组,调用对应的初始化器 4. **注册配置**: 将配置注册到 `PayConfigManager` ### 配置结构 ```java @Data public class PayConfig { private String configId; // 格式: {tenantId}:{paymentMethod}:{appid} private String tenantId; // 租户ID private DictPaymentMethod paymentMethod; // 支付方式 private String appid; // 应用ID private String mchId; // 商户号 private String mchKey; // 商户密钥 private String certPath; // 证书路径 // ... 其他配置项 } ``` ### 多租户配置隔离 每个租户的配置完全隔离,通过配置ID进行区分: ```java // 获取指定租户的支付配置 PayConfig config = configManager.getConfig(tenantId, "wechat", appId); // 获取租户的所有配置 List configs = configManager.getConfigsByTenant(tenantId); ``` ## 状态查询 ### 支付状态查询 ```java // 查询微信支付状态 PayResponse response = payService.queryPayment( DictPaymentMethod.WECHAT, outTradeNo, appId ); if (response.isSuccess()) { String tradeState = response.getTradeState(); Date payTime = response.getPayTime(); // 处理查询结果 } ``` ### 退款状态查询 ```java // 查询退款状态 RefundResponse response = payService.queryRefund( DictPaymentMethod.WECHAT, outRefundNo, appId ); if (response.isSuccess()) { String refundStatus = response.getRefundStatus(); // 处理查询结果 } ``` ## 工具类 ### PayUtils - 支付工具类 ```java // 生成订单号 String outTradeNo = PayUtils.generateOutTradeNo(); String outTradeNo = PayUtils.generateOutTradeNo("ORDER"); // 生成退款单号 String outRefundNo = PayUtils.generateOutRefundNo(); // 金额转换 String fenStr = PayUtils.yuanToFen("10.50"); // 1050 String yuanStr = PayUtils.fenToYuan("1050"); // 10.50 // 敏感信息掩码 String maskedMchId = PayUtils.maskMchId("1234567890"); // 123****890 String maskedAppId = PayUtils.maskAppId("wx1234567890123456"); // wx12****3456 ``` ### PayNotifyUrlBuilder - 回调地址构建 ```java // 构建回调地址 String notifyUrl = PayNotifyUrlBuilder.buildNotifyUrl( DictPaymentMethod.WECHAT, mchId ); // 解析回调地址 String[] parsed = PayNotifyUrlBuilder.parseNotifyUrl(notifyUrl); String paymentType = parsed[0]; String merchantId = parsed[1]; ``` ## 最佳实践 ### 1. 订单号生成 ```java // 推荐使用系统提供的订单号生成方法 String outTradeNo = PayUtils.generateOutTradeNo("PAY"); // PAY20241212143025123456 String outRefundNo = PayUtils.generateOutRefundNo("REF"); // REF20241212143025123456 ``` ### 2. 异常处理 ```java try { PayResponse response = payService.pay(DictPaymentMethod.WECHAT, request); if (!response.isSuccess()) { log.error("支付失败: {}", response.getMessage()); // 处理支付失败 } } catch (ServiceException e) { log.error("支付异常: {}", e.getMessage(), e); // 处理异常情况 } ``` ### 3. 回调幂等性 支付回调可能会重复推送,业务处理时需要保证幂等性: ```java @EventListener @Transactional public void handlePaymentSuccess(PaySuccessEvent event) { String outTradeNo = event.getOutTradeNo(); // 检查订单是否已处理(幂等性保证) if (orderService.isPaid(outTradeNo)) { log.info("订单已处理,跳过: {}", outTradeNo); return; } // 更新订单状态 orderService.updateOrderStatus(outTradeNo, "PAID"); // 其他业务处理... } ``` ### 4. 金额处理 ```java // 统一使用 BigDecimal 处理金额,避免精度丢失 BigDecimal amount = new BigDecimal("10.50"); // 避免使用 double 类型 // ❌ double amount = 10.50; // ✅ BigDecimal amount = new BigDecimal("10.50"); ``` ### 5. 配置安全 生产环境中,敏感配置信息应该加密存储: ```java // 敏感信息脱敏日志输出 log.info("初始化微信支付: appId={}, mchId={}", PayUtils.maskAppId(appId), PayUtils.maskMchId(mchId) ); ``` ## 常见问题 ### Q: 支付回调收不到? A: 检查以下几点: 1. 确保回调地址可以从外网访问 2. 检查 `app.base-api` 配置是否正确 3. 确认防火墙没有拦截回调请求 4. 查看回调日志,确认签名验证是否通过 ### Q: 微信支付提示签名错误? A: 检查以下配置: 1. `mchKey` (API密钥) 是否正确 2. 参数是否按照微信要求进行签名 3. 字符编码是否为 UTF-8 ### Q: 支付宝支付失败? A: 检查以下配置: 1. `appId` 格式是否正确(16位数字) 2. 应用私钥是否正确 3. 支付宝公钥是否配置 4. 是否开通了对应的支付产品 ### Q: 多租户环境下配置混乱? A: 确保: 1. 每个租户的配置ID唯一 2. 配置初始化时按租户隔离 3. 使用时传入正确的 `appId` 参数 ### Q: 如何扩展新的支付方式? A: 按照以下步骤: 1. 实现 `PayHandler` 接口 2. 创建对应的 `PayInitializer` 3. 在 `DictPaymentMethod` 中添加新的支付方式枚举 4. 添加相关配置支持 --- --- url: 'https://ruoyi.plus/backend/common/encrypt.md' --- # 数据加密模块概览与快速入门 ## 模块介绍 数据加密模块是一个企业级的数据安全解决方案,提供了完整的数据加解密功能,支持数据库字段自动加解密和API接口传输加密两大核心场景。 ### 核心特性 * **多算法支持**:内置BASE64、AES、RSA、SM2、SM4等主流加密算法 * **自动化处理**:通过注解配置,实现数据库和API的透明加解密 * **高性能设计**:采用缓存机制,避免重复创建加密器实例 * **灵活配置**:支持全局配置和字段级别的个性化配置 * **无侵入集成**:基于MyBatis拦截器和Servlet过滤器,业务代码零改动 ### 整体架构 ``` ┌─────────────────────┐ ┌─────────────────────┐ │ API接口加密 │ │ 数据库字段加密 │ │ │ │ │ │ CryptoFilter │ │ MyBatis拦截器 │ │ @ApiEncrypt │ │ @EncryptField │ └──────────┬──────────┘ └──────────┬──────────┘ │ │ └──────────┬─────┬─────────┘ │ │ ┌──────────▼─────▼──────────┐ │ EncryptorManager │ │ (加密器管理器) │ └────────────┬─────────────┘ │ ┌────────────▼─────────────┐ │ 加密器实现 │ │ AES │ RSA │ SM2 │ ... │ └──────────────────────────┘ ``` ## 快速开始 ### 1. 基础配置 在 `application.yml` 中添加基础配置: ```yaml # 数据库字段加密配置 mybatis-encryptor: enable: true # 启用数据库字段加密 algorithm: AES # 默认加密算法 password: "1234567890123456" # AES密钥(16位) encode: BASE64 # 编码方式 # API接口加密配置 api-decrypt: enabled: true # 启用API接口加密 header-flag: "encrypt-key" # 请求头标识 public-key: "你的RSA公钥" private-key: "你的RSA私钥" ``` ### 2. 数据库字段加密示例 ```java @Data @TableName("sys_user") public class SysUser { @TableId private Long userId; private String userName; // 使用默认配置加密手机号 @EncryptField private String phone; // 使用自定义密钥加密邮箱 @EncryptField(algorithm = AlgorithmType.AES, password = "customkey123456") private String email; // 使用RSA加密身份证号 @EncryptField( algorithm = AlgorithmType.RSA, publicKey = "你的公钥", privateKey = "你的私钥" ) private String idCard; } ``` ### 3. API接口加密示例 ```java @RestController @RequestMapping("/user") public class UserController { // 对响应结果进行加密 @PostMapping("/login") @ApiEncrypt(response = true) public Result login(@RequestBody LoginRequest request) { // 请求自动解密,响应自动加密 UserInfo userInfo = userService.login(request); return Result.success(userInfo); } } ``` ## 支持的加密算法 | 算法 | 类型 | 密钥要求 | 特点 | 适用场景 | |------|------|----------|------|----------| | **BASE64** | 编码 | 无 | 速度快,但非加密 | 数据混淆、传输编码 | | **AES** | 对称加密 | 16/24/32位 | 速度快,安全性高 | 大量数据加密 | | **RSA** | 非对称加密 | 公私钥对 | 安全性极高 | 密钥传输、敏感数据 | | **SM2** | 国密非对称 | 公私钥对 | 符合国标,高安全 | 政府、金融行业 | | **SM4** | 国密对称 | 16位 | 符合国标,高性能 | 国产化要求场景 | ### 算法选择建议 * **日常业务**:推荐使用AES,性能和安全性平衡 * **高敏感数据**:推荐使用RSA或SM2,安全性最高 * **国产化项目**:必须使用SM2/SM4,符合相关规范 * **简单混淆**:可使用BASE64,但不能用于安全要求高的场景 ## 应用场景对比 ### 数据库字段加密 **适用于**: * 用户隐私信息(手机号、身份证、邮箱等) * 敏感业务数据(银行卡号、密码等) * 合规性要求的数据存储 **特点**: * 数据存储时自动加密 * 查询时自动解密 * 对业务代码透明 * 支持复杂查询和分页 ### API接口加密 **适用于**: * 前后端数据传输安全 * 移动端与服务端通信 * 第三方接口集成 * 防止数据传输被窃取 **特点**: * 请求响应全链路加密 * RSA+AES混合加密方案 * 支持跨域和CORS * 密钥动态生成 ## 性能优化特性 ### 1. 加密器缓存机制 * 相同配置的加密器实例会被复用 * 避免重复创建加密器带来的性能开销 * 支持动态移除和更新缓存 ### 2. 字段扫描缓存 * 启动时扫描并缓存包含加密字段的类信息 * 运行时无需重复反射操作 * 只处理标注了@EncryptField的字段 ### 3. 智能批量处理 * List和Map类型数据的批量加解密 * 避免逐个处理带来的性能损失 * 支持复杂嵌套对象的递归处理 ## 安全注意事项 ### 密钥管理 * **生产环境**:密钥应存储在安全的配置中心 * **定期轮换**:建议定期更换加密密钥 * **权限控制**:密钥访问应有严格的权限控制 ### 兼容性考虑 * **数据迁移**:现有数据加密需要考虑历史数据处理 * **版本升级**:算法升级时需要保证向下兼容 * **环境一致**:开发、测试、生产环境的加密配置应保持一致 ## 下一步 * 查看 [数据库字段加密](./encrypt/database-encryption.md) 了解详细的字段加密配置 * 查看 [API接口加密](./encrypt/api-encryption.md) 了解接口传输加密的实现 --- --- url: 'https://ruoyi.plus/practices/data-security.md' --- # 数据安全 --- --- url: 'https://ruoyi.plus/practices/database-optimization.md' --- # 数据库优化 --- --- url: 'https://ruoyi.plus/backend/common/encrypt/database-encryption.md' --- # 数据库字段加密 (Database Field Encryption) 数据库字段加密功能基于MyBatis拦截器实现,通过`@EncryptField`注解标记需要加密的字段,在数据入库前自动加密,查询时自动解密,对业务代码完全透明。 ## 配置说明 ### 全局配置 在 `application.yml` 中配置默认的加密参数: ```yaml mybatis-encryptor: # 功能开关 enable: true # 默认加密算法(当注解中algorithm为DEFAULT时使用) algorithm: AES # 默认编码方式 encode: BASE64 # 对称加密密钥(AES/SM4使用) password: "1234567890123456" # 非对称加密公私钥(RSA/SM2使用) public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC..." ``` ### MyBatis-Plus集成配置 确保MyBatis-Plus的实体扫描包配置正确: ```yaml mybatis-plus: # 实体类扫描包(加密器会扫描这些包下的@EncryptField注解) type-aliases-package: plus.ruoyi.**.domain.entity # Mapper扫描包 mapper-package: plus.ruoyi.**.mapper ``` ## @EncryptField 注解详解 ### 基础用法 ```java @Data @TableName("sys_user") public class SysUser { @TableId private Long userId; private String userName; // 使用默认配置加密 @EncryptField private String phone; // 指定加密算法 @EncryptField(algorithm = AlgorithmType.SM4) private String email; // 使用自定义密钥 @EncryptField( algorithm = AlgorithmType.AES, password = "customkey123456789", encode = EncodeType.HEX ) private String idCard; } ``` ### 注解参数说明 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `algorithm` | AlgorithmType | DEFAULT | 加密算法类型 | | `password` | String | "" | 对称加密密钥 | | `publicKey` | String | "" | 非对称加密公钥 | | `privateKey` | String | "" | 非对称加密私钥 | | `encode` | EncodeType | DEFAULT | 编码方式 | **参数优先级**:注解参数 > 全局配置 ## 各算法使用示例 ### 1. BASE64 编码 ```java public class User { // BASE64不需要密钥,主要用于数据混淆 @EncryptField(algorithm = AlgorithmType.BASE64) private String remark; } ``` ### 2. AES 对称加密 ```java public class User { // 使用全局配置的AES密钥 @EncryptField(algorithm = AlgorithmType.AES) private String phone; // 使用自定义AES密钥(16位) @EncryptField( algorithm = AlgorithmType.AES, password = "mykey1234567890" ) private String email; // AES + HEX编码 @EncryptField( algorithm = AlgorithmType.AES, password = "mykey1234567890", encode = EncodeType.HEX ) private String idCard; } ``` **AES密钥要求**: * 密钥长度必须是16位、24位或32位 * 密钥强度:16位 < 24位 < 32位 ### 3. RSA 非对称加密 ```java public class User { @EncryptField( algorithm = AlgorithmType.RSA, publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...", privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC..." ) private String bankCard; } ``` **RSA密钥生成**: ```java // 使用工具类生成RSA密钥对 Map keyMap = EncryptUtils.generateRsaKey(); String publicKey = keyMap.get(EncryptUtils.PUBLIC_KEY); String privateKey = keyMap.get(EncryptUtils.PRIVATE_KEY); ``` ### 4. SM2 国密非对称加密 ```java public class User { @EncryptField( algorithm = AlgorithmType.SM2, publicKey = "你的SM2公钥", privateKey = "你的SM2私钥" ) private String socialSecurityNumber; } ``` **SM2密钥生成**: ```java // 使用工具类生成SM2密钥对 Map keyMap = EncryptUtils.generateSm2Key(); String publicKey = keyMap.get(EncryptUtils.PUBLIC_KEY); String privateKey = keyMap.get(EncryptUtils.PRIVATE_KEY); ``` ### 5. SM4 国密对称加密 ```java public class User { // SM4密钥必须是16位 @EncryptField( algorithm = AlgorithmType.SM4, password = "sm4key1234567890" ) private String passport; } ``` ## 工作原理 ### 1. 启动时扫描 系统启动时,`EncryptorManager` 会扫描配置的实体包,缓存包含 `@EncryptField` 注解的类和字段信息: ```java // 扫描过程 1. 根据 mybatis-plus.type-aliases-package 配置扫描类文件 2. 检查类中是否有 @EncryptField 注解的 String 类型字段 3. 将包含加密字段的类信息缓存到 fieldCache 中 4. 设置字段为可访问状态(setAccessible(true)) ``` ### 2. 加密拦截器 `MybatisEncryptInterceptor` 拦截 `ParameterHandler.setParameters` 方法: ```java // 拦截时机:SQL参数设置前 1. 获取SQL参数对象 2. 递归遍历参数中的对象(支持Map、List、普通对象) 3. 对标注了@EncryptField的字段进行加密 4. 将加密后的值设置回字段 ``` ### 3. 解密拦截器 `MybatisDecryptInterceptor` 拦截 `ResultSetHandler.handleResultSets` 方法: ```java // 拦截时机:结果集处理完成后 1. 获取查询结果对象 2. 递归遍历结果中的对象(支持Map、List、普通对象) 3. 对标注了@EncryptField的字段进行解密 4. 将解密后的值设置回字段 ``` ## 支持的数据类型 ### 1. 普通对象 ```java // 直接查询实体 SysUser user = userMapper.selectById(1L); // phone字段自动解密 ``` ### 2. List 集合 ```java // 批量查询 List users = userMapper.selectList(null); // 每个user对象的phone字段都会自动解密 ``` ### 3. 分页查询 ```java // MyBatis-Plus分页 Page page = new Page<>(1, 10); Page result = userMapper.selectPage(page, null); // 分页结果中的记录会自动解密 ``` ### 4. Map 结果 ```java // 自定义SQL返回Map List> maps = userMapper.selectMaps(null); // Map中的加密字段值会自动解密 ``` ## 性能优化 ### 1. 缓存机制 ```java // 字段缓存:避免重复反射 Map, Set> fieldCache; // 加密器缓存:避免重复创建实例 Map encryptorMap; ``` ### 2. 智能跳过 ```java // 如果类中没有加密字段,直接跳过处理 Set fields = encryptorManager.getFieldCache(sourceObject.getClass()); if (ObjectUtil.isNull(fields)) { return; // 直接返回,不做任何处理 } ``` ### 3. 批量处理优化 ```java // 对于List类型,检查第一个元素是否有加密字段 if (CollUtil.isEmpty(list)) { return; } Object firstItem = list.get(0); if (ObjectUtil.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) { return; // 如果第一个元素没有加密字段,整个List都跳过 } ``` ## 最佳实践 ### 1. 字段选择原则 **适合加密的字段**: * 用户隐私信息(手机号、邮箱、身份证) * 敏感业务数据(银行卡号、密码) * 法规要求保护的数据 **不适合加密的字段**: * 需要进行范围查询的字段(如金额、日期) * 需要排序的字段 * 外键关联字段 * 频繁查询的索引字段 ### 2. 性能考虑 ```java // ❌ 不推荐:对所有字符串字段都加密 @EncryptField private String name; // 姓名通常不需要加密 @EncryptField private String address; // 地址可能需要模糊查询 @EncryptField private String department; // 部门名称不需要加密 // ✅ 推荐:只对真正敏感的字段加密 @EncryptField private String phone; // 手机号需要保护 @EncryptField private String idCard; // 身份证号需要保护 @EncryptField private String bankCard; // 银行卡号需要保护 ``` ### 3. 算法选择策略 ```java // 根据敏感级别选择算法 public class User { // 低敏感:BASE64混淆 @EncryptField(algorithm = AlgorithmType.BASE64) private String nickname; // 中敏感:AES加密 @EncryptField(algorithm = AlgorithmType.AES) private String phone; // 高敏感:RSA加密 @EncryptField(algorithm = AlgorithmType.RSA) private String idCard; // 国产化项目:使用国密算法 @EncryptField(algorithm = AlgorithmType.SM2) private String bankCard; } ``` ### 4. 查询注意事项 ```java // ❌ 加密字段不支持条件查询 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUser::getPhone, "13800138000"); // 这样查询不到结果 // ✅ 如果需要按加密字段查询,需要手动加密查询条件 String encryptedPhone = encryptorManager.encrypt("13800138000", encryptContext); wrapper.eq(SysUser::getPhone, encryptedPhone); ``` ## 故障排查 ### 常见问题 1. **字段值为null** * 原因:加密字段被赋值为null * 解决:检查业务逻辑,确保字段有值再进行数据库操作 2. **加密配置不生效** * 检查 `mybatis-encryptor.enable` 是否为 `true` * 检查实体类是否在扫描包路径内 * 检查字段是否为 `String` 类型 3. **密钥长度错误** * AES:确保密钥为16/24/32位 * SM4:确保密钥为16位 * RSA/SM2:确保公私钥格式正确 4. **查询结果乱码** * 检查数据库字符集设置 * 检查encode配置是否与加密时一致 ### 调试建议 ```java // 开启MyBatis SQL日志 logging: level: plus.ruoyi.common.encrypt: DEBUG ``` 通过日志可以查看加密解密的执行过程和异常信息。 --- --- url: 'https://ruoyi.plus/practices/database-design.md' --- # 数据库设计 --- --- url: 'https://ruoyi.plus/backend/common/core/domain.md' --- # 数据模型与DTO ## 概述 数据模型与DTO(Data Transfer Object)是ruoyi-plus-uniapp中用于数据传输和业务交互的核心组件。本模块提供了统一的数据传输规范、响应体系和各种业务模型,确保系统各层之间的数据交互规范化和类型安全。 ## 模块结构 ```text domain/ ├── R.java # 统一响应体 ├── dto/ # 数据传输对象 │ ├── UserDTO.java # 用户数据传输对象 │ ├── DeptDTO.java # 部门数据传输对象 │ ├── RoleDTO.java # 角色数据传输对象 │ ├── PostDTO.java # 岗位数据传输对象 │ ├── DictTypeDTO.java # 字典类型DTO │ ├── DictDataDTO.java # 字典数据DTO │ ├── OssDTO.java # 对象存储DTO │ ├── PaymentDTO.java # 支付配置DTO │ ├── PlatformDTO.java # 平台配置DTO │ └── UserOnlineDTO.java # 在线用户DTO ├── model/ # 业务模型 │ ├── LoginUser.java # 登录用户模型 │ ├── LoginBody.java # 登录基础模型 │ ├── PasswordLoginBody.java # 密码登录模型 │ ├── SmsLoginBody.java # 短信登录模型 │ ├── EmailLoginBody.java # 邮箱登录模型 │ ├── SocialLoginBody.java # 社交登录模型 │ ├── PlatformLoginBody.java # 平台登录模型 │ └── RegisterBody.java # 注册模型 └── vo/ # 视图对象 └── DictItemVo.java # 字典项视图对象 ``` ## 统一响应体系 ### `R` 统一响应类 `R` 是系统的统一响应封装类,提供标准化的API响应格式,支持泛型和国际化。 #### 基本结构 ```java @Data @NoArgsConstructor public class R implements Serializable { /** * 成功状态码 (200) */ public static final int SUCCESS = HttpStatus.SUCCESS; /** * 失败状态码 (500) */ public static final int FAIL = HttpStatus.ERROR; /** * 消息状态码 */ private int code; /** * 消息内容(支持国际化) */ private String msg; /** * 数据对象 */ private T data; } ``` #### 核心特性 **自动国际化支持** ```java // 自动判断是否为国际化键 private static String processMessage(String message, Object... args) { if (StringUtils.isBlank(message)) { return null; } // 使用正则表达式判断国际化键格式 if (RegexValidator.isValidI18nKey(message)) { return MessageUtils.message(message, args); } return message; } ``` **链式操作支持** ```java // 成功响应 R result = R.ok(user); R> result = R.ok("查询成功", userList); // 失败响应 R result = R.fail("操作失败"); R result = R.fail("user.not.found", userId); // 条件响应 R result = R.status(count > 0); R result = R.status(success, "操作成功", "操作失败"); ``` #### 使用示例 ```java @RestController public class UserController { @GetMapping("/getUser/{id}") public R getUser(@PathVariable Long id) { UserVo user = userService.get(id); return R.ok(user); } @PostMapping("/addUser") public R addUser(@RequestBody UserBo bo) { Long userId = userService.add(bo); return R.ok("user.create.success", userId); } @DeleteMapping("/deleteUser/{id}") public R deleteUser(@PathVariable Long id) { boolean success = userService.delete(id); return R.status(success, "删除成功", "删除失败"); } } ``` #### 响应格式示例 ```json // 成功响应 { "code": 200, "msg": "操作成功", "data": { "userId": 1, "userName": "admin" } } // 失败响应 { "code": 500, "msg": "用户不存在", "data": null } // 列表响应 { "code": 200, "msg": "查询成功", "data": [ {"userId": 1, "userName": "admin"}, {"userId": 2, "userName": "test"} ] } ``` ## 数据传输对象 (DTO) DTO 用于不同层之间的数据传输,通常用于Service层返回数据给Controller层。 ### 用户相关 DTO #### UserDTO - 用户数据传输对象 ```java @Data @NoArgsConstructor public class UserDTO implements Serializable { private Long userId; private Long deptId; private String userName; private String nickName; private String userType; private String email; private String phone; private String gender; private String status; private Date createTime; // ... 其他字段 } ``` #### DeptDTO - 部门数据传输对象 ```java @Data @NoArgsConstructor public class DeptDTO implements Serializable { private Long deptId; private Long parentId; private String deptName; // ... 其他字段 } ``` #### RoleDTO - 角色数据传输对象 ```java @Data @NoArgsConstructor public class RoleDTO implements Serializable { private Long roleId; private String roleName; private String roleKey; /** * 数据范围 * 1:全部数据权限 * 2:自定数据权限 * 3:本部门数据权限 * 4:本部门及以下数据权限 * 5:仅本人数据权限 * 6:部门及以下或本人数据权限 */ private String dataScope; // ... 其他字段 } ``` ### 字典相关 DTO #### DictTypeDTO - 字典类型DTO ```java @Data @NoArgsConstructor public class DictTypeDTO implements Serializable { private Long dictId; private String dictName; private String dictType; private String remark; // ... 其他字段 } ``` #### DictDataDTO - 字典数据DTO ```java @Data @NoArgsConstructor public class DictDataDTO implements Serializable { private String dictLabel; private String dictValue; private String isDefault; private String remark; // ... 其他字段 } ``` ### 业务相关 DTO #### PaymentDTO - 支付配置DTO ```java @Data public class PaymentDTO implements Serializable { private Long id; private String type; private String mchName; private String mchId; private String mchKey; private String apiV3Key; private String notifyUrl; private String status; // ============= 便捷方法 ============= /** * 判断是否为启用状态 */ public boolean isEnabled() { return DictEnableStatus.ENABLE.getValue().equals(this.status); } /** * 判断是否为微信支付 */ public boolean isWechatPayment() { return "WECHAT".equals(this.type); } /** * 掩码处理商户密钥(用于日志输出) */ public String getMaskedMchKey() { if (mchKey == null || mchKey.length() <= 8) { return "****"; } return mchKey.substring(0, 4) + "****" + mchKey.substring(mchKey.length() - 4); } } ``` #### PlatformDTO - 平台配置DTO ```java @Data public class PlatformDTO implements Serializable { private Long id; private String type; private String name; private String appid; private String secret; private String paymentIds; private String status; /** * 检查是否支持指定的支付配置 */ public boolean supportsPayment(Long paymentId) { return getPaymentIdList().contains(paymentId); } } ``` ## 业务模型 (Model) 业务模型主要用于封装业务逻辑相关的数据结构,通常包含业务行为和状态信息。 ### 登录用户模型 #### LoginUser - 登录用户身份权限 ```java @Data @NoArgsConstructor public class LoginUser implements Serializable { private String tenantId; private Long userId; private Long deptId; private String token; private String userType; private Long loginTime; private Long expireTime; private String ipaddr; private Set menuPermission; private Set rolePermission; private String userName; private String nickName; private List roles; private List posts; /** * 获取登录id */ public String getLoginId() { if (userType == null) { throw new IllegalArgumentException("用户类型不能为空"); } if (userId == null) { throw new IllegalArgumentException("用户ID不能为空"); } return userType + ":" + userId; } } ``` ### 登录请求模型 #### LoginBody - 登录基础模型 ```java @Data public class LoginBody implements Serializable { /** * 认证方式 */ @NotBlank(message = I18nKeys.Auth.TYPE_REQUIRED) private String authType; /** * 验证码 */ private String code; /** * 唯一标识 */ private String uuid; } ``` #### PasswordLoginBody - 密码登录模型 ```java @Data @EqualsAndHashCode(callSuper = true) public class PasswordLoginBody extends LoginBody { @NotBlank(message = I18nKeys.User.USERNAME_REQUIRED) @Length(min = 2, max = 30, message = I18nKeys.User.USERNAME_LENGTH_INVALID) private String userName; @NotBlank(message = I18nKeys.User.PASSWORD_REQUIRED) @Length(min = 5, max = 30, message = I18nKeys.User.PASSWORD_LENGTH_INVALID) private String password; } ``` #### SmsLoginBody - 短信登录模型 ```java @Data @EqualsAndHashCode(callSuper = true) public class SmsLoginBody extends LoginBody { @NotBlank(message = I18nKeys.User.PHONE_REQUIRED) private String phone; @NotBlank(message = I18nKeys.VerifyCode.SMS_REQUIRED) private String smsCode; } ``` #### EmailLoginBody - 邮箱登录模型 ```java @Data @EqualsAndHashCode(callSuper = true) public class EmailLoginBody extends LoginBody { @NotBlank(message = I18nKeys.User.EMAIL_REQUIRED) @Email(message = I18nKeys.User.EMAIL_FORMAT_INVALID) private String email; @NotBlank(message = I18nKeys.VerifyCode.EMAIL_REQUIRED) private String emailCode; } ``` #### SocialLoginBody - 社交登录模型 ```java @Data @EqualsAndHashCode(callSuper = true) public class SocialLoginBody extends LoginBody { @NotBlank(message = I18nKeys.SocialLogin.SOURCE_REQUIRED) private String source; @NotBlank(message = I18nKeys.SocialLogin.AUTH_CODE_REQUIRED) private String socialCode; @NotBlank(message = I18nKeys.SocialLogin.STATE_REQUIRED) private String socialState; } ``` #### RegisterBody - 注册模型 ```java @Data @EqualsAndHashCode(callSuper = true) public class RegisterBody extends LoginBody { @NotBlank(message = I18nKeys.User.USERNAME_REQUIRED) @Length(min = 2, max = 30, message = I18nKeys.User.USERNAME_LENGTH_INVALID) private String userName; @NotBlank(message = I18nKeys.User.PASSWORD_REQUIRED) @Length(min = 5, max = 30, message = I18nKeys.User.PASSWORD_LENGTH_INVALID) @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d).*$", message = I18nKeys.User.PASSWORD_COMPLEXITY_INVALID) private String password; private String userType; } ``` ## 视图对象 (VO) 视图对象主要用于前端展示,通常包含格式化后的数据和额外的展示属性。 ### DictItemVo - 字典项视图对象 ```java @Data public class DictItemVo implements Serializable { private String label; private String value; private String status; private String elTagType; private String elTagClass; /** * 快速创建字典项 */ public static DictItemVo of(String label, String value) { DictItemVo item = new DictItemVo(); item.setLabel(label); item.setValue(value); return item; } /** * 创建带标签类型的字典项 */ public static DictItemVo of(String label, String value, String elTagType) { DictItemVo item = new DictItemVo(); item.setLabel(label); item.setValue(value); item.setElTagType(elTagType); return item; } } ``` ## 设计原则与最佳实践 ### 1. 命名规范 #### DTO 命名规范 ```java // ✅ 正确命名 UserDTO.java // 用户数据传输对象 DeptDTO.java // 部门数据传输对象 PaymentDTO.java // 支付配置数据传输对象 // ❌ 错误命名 User.java // 容易与实体类混淆 UserInfo.java // 含义不明确 UserTransfer.java // 命名冗余 ``` #### Model 命名规范 ```java // ✅ 正确命名 LoginUser.java // 登录用户模型 LoginBody.java // 登录请求模型 RegisterBody.java // 注册请求模型 // ❌ 错误命名 LoginRequest.java // 不符合Body约定 LoginModel.java // 含义模糊 LoginParam.java // 混淆参数概念 ``` #### VO 命名规范 ```java // ✅ 正确命名 UserVo.java // 用户视图对象 DictItemVo.java // 字典项视图对象 MenuTreeVo.java // 菜单树视图对象 // ❌ 错误命名 UserView.java // 不符合VO约定 UserDisplay.java // 含义不明确 ``` ### 2. 数据传输层次 ``` Controller Layer ↕ Bo/Vo Objects # 业务对象/视图对象 ↕ Service Layer ↕ DTO Objects # 数据传输对象 ↕ Entity Objects # 实体对象 ``` ### 3. 字段设计原则 #### 必要字段 ```java @Data public class UserDTO implements Serializable { // ✅ 序列化版本号 @Serial private static final long serialVersionUID = 1L; // ✅ 主键ID private Long userId; // ✅ 核心业务字段 private String userName; private String nickName; // ✅ 状态字段 private String status; // ✅ 时间字段 private Date createTime; } ``` #### 可选字段处理 ```java @Data public class PaymentDTO implements Serializable { // 基础字段 private String mchId; private String mchKey; // 可选字段 - 开发环境配置 private String devNotifyUrl; private String devCertPath; /** * 判断是否有开发环境配置 */ public boolean hasDevConfig() { return (devNotifyUrl != null && !devNotifyUrl.trim().isEmpty()) || (devCertPath != null && !devCertPath.trim().isEmpty()); } /** * 获取实际配置(优先使用开发环境) */ public String getActualNotifyUrl(boolean isDev) { if (isDev && hasDevConfig()) { return devNotifyUrl; } return notifyUrl; } } ``` ### 4. 业务方法设计 #### 状态判断方法 ```java @Data public class PaymentDTO implements Serializable { private String status; private String type; /** * 判断是否为启用状态 */ public boolean isEnabled() { return DictEnableStatus.ENABLE.getValue().equals(this.status); } /** * 判断是否为微信支付 */ public boolean isWechatPayment() { return "WECHAT".equals(this.type); } /** * 判断是否配置了APIv3 */ public boolean hasApiV3Config() { return apiV3Key != null && !apiV3Key.trim().isEmpty() && certSerialNo != null && !certSerialNo.trim().isEmpty(); } } ``` #### 数据转换方法 ```java @Data public class PlatformDTO implements Serializable { private String paymentIds; // "1,2,3" /** * 获取支付配置ID列表 */ public List getPaymentIdList() { if (paymentIds == null || paymentIds.trim().isEmpty()) { return Collections.emptyList(); } return Arrays.stream(paymentIds.split(",")) .map(String.class::trim) .filter(s -> !s.isEmpty()) .map(Long::parseLong) .collect(Collectors.toList()); } /** * 设置支付配置ID列表 */ public void setPaymentIdList(List paymentIdList) { if (paymentIdList == null || paymentIdList.isEmpty()) { this.paymentIds = ""; } else { this.paymentIds = paymentIdList.stream() .map(String::valueOf) .collect(Collectors.joining(",")); } } } ``` ### 5. 安全性考虑 #### 敏感信息掩码 ```java @Data public class PaymentDTO implements Serializable { private String mchKey; private String apiV3Key; /** * 掩码处理商户密钥(用于日志输出) */ public String getMaskedMchKey() { if (mchKey == null || mchKey.length() <= 8) { return "****"; } return mchKey.substring(0, 4) + "****" + mchKey.substring(mchKey.length() - 4); } @Override public String toString() { return "PaymentDTO{" + "id=" + id + ", mchName='" + mchName + '\'' + ", mchId='" + mchId + '\'' + ", mchKey='" + getMaskedMchKey() + '\'' + ", status='" + status + '\'' + '}'; } } ``` ## 使用场景与示例 ### 1. Controller 层使用 ```java @RestController @RequestMapping("/api/users") public class UserController { /** * 查询用户列表 */ @GetMapping public R> listUsers() { List userDTOs = userService.listUsers(); List userVos = MapstructUtils.convert(userDTOs, UserVo.class); return R.ok(userVos); } /** * 获取用户详情 */ @GetMapping("/{id}") public R getUser(@PathVariable Long id) { UserDTO userDTO = userService.getUser(id); UserVo userVo = MapstructUtils.convert(userDTO, UserVo.class); return R.ok(userVo); } /** * 创建用户 */ @PostMapping public R createUser(@Validated(AddGroup.class) @RequestBody UserBo userBo) { UserDTO userDTO = MapstructUtils.convert(userBo, UserDTO.class); Long userId = userService.createUser(userDTO); return R.ok("用户创建成功", userId); } } ``` ### 2. Service 层使用 ```java @Service public class UserServiceImpl implements UserService { /** * 创建用户 */ @Override public Long createUser(UserDTO userDTO) { // 参数校验 ValidatorUtils.validate(userDTO, AddGroup.class); // DTO 转 Entity User user = MapstructUtils.convert(userDTO, User.class); // 业务逻辑处理 user.setCreateTime(new Date()); user.setStatus(DictEnableStatus.ENABLE.getValue()); // 保存用户 userRepository.save(user); return user.getUserId(); } /** * 获取用户信息 */ @Override public UserDTO getUser(Long userId) { User user = userRepository.findById(userId); if (user == null) { throw ServiceException.of("用户不存在"); } // Entity 转 DTO UserDTO userDTO = MapstructUtils.convert(user, UserDTO.class); return userDTO; } } ``` ### 3. 登录认证使用 ```java @RestController @RequestMapping("/auth") public class AuthController { /** * 密码登录 */ @PostMapping("/login/password") public R passwordLogin(@Validated @RequestBody PasswordLoginBody loginBody) { LoginUser loginUser = authService.passwordLogin(loginBody); LoginResult result = buildLoginResult(loginUser); return R.ok("登录成功", result); } /** * 短信登录 */ @PostMapping("/login/sms") public R smsLogin(@Validated @RequestBody SmsLoginBody loginBody) { LoginUser loginUser = authService.smsLogin(loginBody); LoginResult result = buildLoginResult(loginUser); return R.ok("登录成功", result); } /** * 社交登录 */ @PostMapping("/login/social") public R socialLogin(@Validated @RequestBody SocialLoginBody loginBody) { LoginUser loginUser = authService.socialLogin(loginBody); LoginResult result = buildLoginResult(loginUser); return R.ok("登录成功", result); } /** * 用户注册 */ @PostMapping("/register") public R register(@Validated @RequestBody RegisterBody registerBody) { authService.register(registerBody); return R.ok("注册成功"); } /** * 构建登录结果 */ private LoginResult buildLoginResult(LoginUser loginUser) { LoginResult result = new LoginResult(); result.setToken(loginUser.getToken()); result.setExpireTime(loginUser.getExpireTime()); // 用户信息 UserInfo userInfo = new UserInfo(); userInfo.setUserId(loginUser.getUserId()); userInfo.setUserName(loginUser.getUserName()); userInfo.setNickName(loginUser.getNickName()); userInfo.setRoles(loginUser.getRoles()); userInfo.setPermissions(loginUser.getMenuPermission()); result.setUserInfo(userInfo); return result; } } ``` ## 业务验证与扩展功能 ### 1. 业务验证 ```java @Data public class PaymentDTO implements Serializable { /** * 检查必要的配置项是否完整 */ public boolean isConfigComplete() { // 基础配置检查 if (mchId == null || mchId.trim().isEmpty()) { return false; } if (mchName == null || mchName.trim().isEmpty()) { return false; } // 根据支付类型验证不同的必要参数 return switch (type) { case "wechat" -> mchKey != null && !mchKey.trim().isEmpty() && certPath != null && !certPath.trim().isEmpty(); case "alipay" -> mchKey != null && !mchKey.trim().isEmpty(); case "balance" -> true; default -> false; }; } /** * 获取配置摘要信息(用于日志) */ public String getConfigSummary() { StringBuilder summary = new StringBuilder(); summary.append("商户[").append(mchName).append("]"); summary.append("(").append(mchId).append(")"); summary.append(" 类型:").append(type != null ? type : "未知"); summary.append(" 状态:").append("1".equals(status) ? "启用" : "禁用"); if (hasApiV3Config()) { summary.append(" [支持APIv3]"); } return summary.toString(); } } ``` ## 数据转换工具 ### MapStruct 对象映射 ```java // 基本转换 UserDTO userDTO = MapstructUtils.convert(user, UserDTO.class); UserVo userVo = MapstructUtils.convert(userDTO, UserVo.class); // 列表转换 List userVos = MapstructUtils.convert(userDTOs, UserVo.class); // Map转对象 UserDTO userDTO = MapstructUtils.convert(userMap, UserDTO.class); ``` --- --- url: 'https://ruoyi.plus/frontend/utils/array.md' --- # 数组工具 --- --- url: 'https://ruoyi.plus/mobile/utils/file.md' --- # 文件处理工具 --- --- url: 'https://ruoyi.plus/frontend/utils/file.md' --- # 文件工具 --- --- url: 'https://ruoyi.plus/mobile/api/file.md' --- # 文件接口 --- --- url: 'https://ruoyi.plus/frontend/views/log.md' --- # 日志管理 --- --- url: 'https://ruoyi.plus/mobile/debug/logging.md' --- # 日志管理 --- --- url: 'https://ruoyi.plus/practices/log-management.md' --- # 日志管理 --- --- url: 'https://ruoyi.plus/backend/common/log.md' --- # 日志管理 (log) ## 概述 日志管理模块是系统的核心功能模块之一,基于Spring AOP(面向切面编程)实现,提供操作日志记录和登录日志记录功能。通过注解驱动的方式,自动记录用户操作行为,便于系统审计、问题排查和安全监控。 ## 模块结构 ``` ruoyi-common-log/ ├── src/main/java/plus/ruoyi/common/log/ │ ├── annotation/ # 注解定义 │ │ └── Log.java # 操作日志注解 │ ├── aspect/ # 切面实现 │ │ └── LogAspect.java # 日志切面处理器 │ ├── event/ # 事件定义 │ │ ├── LoginLogEvent.java # 登录日志事件 │ │ └── OperLogEvent.java # 操作日志事件 │ └── publisher/ # 事件发布器 │ └── LoginLogPublisher.java # 登录日志发布器 └── src/main/resources/META-INF/spring/ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports ``` ## 核心功能 ### 1. 操作日志记录 自动记录用户的操作行为,包括: * 请求参数和响应结果 * 操作耗时统计 * 异常信息记录 * 用户身份信息 * 请求来源IP和URL ### 2. 登录日志记录 记录用户登录、登出等认证相关操作: * 登录成功/失败记录 * 设备类型识别 * 多租户支持 * 异常情况记录 ## 核心注解 ### @Log 操作日志注解 用于标记需要记录操作日志的方法。 #### 注解属性 | 属性名 | 类型 | 默认值 | 说明 | |----------------------|--------------|---------|--------------------| | `title` | String | `""` | 操作模块名称,用于标识操作的业务模块 | | `operType` | DictOperType | `OTHER` | 操作类型,如增删改查等 | | `isSaveRequestData` | boolean | `true` | 是否保存请求参数 | | `isSaveResponseData` | boolean | `true` | 是否保存响应结果 | | `excludeParamNames` | String\[] | `{}` | 排除记录的参数名称数组 | #### 使用示例 ```java @RestController @RequestMapping("/system/user") public class UserController { /** * 基础用法 */ @Log(title = "用户管理", operType = DictOperType.INSERT) @PostMapping("/add") public R addUser(@RequestBody User user) { return userService.insertUser(user); } /** * 排除敏感参数 */ @Log(title = "用户管理", operType = DictOperType.UPDATE, excludeParamNames = {"password", "oldPassword"}) @PutMapping("/updatePassword") public R updatePassword(@RequestBody UpdatePasswordDto dto) { return userService.updatePassword(dto); } /** * 不记录响应数据 */ @Log(title = "用户管理", operType = DictOperType.EXPORT, isSaveResponseData = false) @PostMapping("/export") public void exportUsers(HttpServletResponse response) { userService.exportUsers(response); } } ``` ## 事件模型 ### OperLogEvent 操作日志事件 记录用户操作的详细信息: ```java @Data public class OperLogEvent implements Serializable { private Long operId; // 日志主键 private String tenantId; // 租户ID private String title; // 操作模块 private String operType; // 操作类型 private String method; // 请求方法 private String requestMethod; // 请求方式(GET/POST等) private String operatorType; // 操作用户类别 private String operName; // 操作人员 private String deptName; // 部门名称 private String operUrl; // 请求URL private String operIp; // 操作IP private String operParam; // 请求参数 private String jsonResult; // 返回参数 private String status; // 操作状态 private String errorMsg; // 错误消息 private Date operTime; // 操作时间 private Long costTime; // 消耗时间(毫秒) } ``` ### LoginLogEvent 登录日志事件 记录用户登录相关信息: ```java @Data public class LoginLogEvent implements Serializable { private String tenantId; // 租户ID private String deviceType; // 设备类型 private Long userId; // 用户ID private String userName; // 用户账号 private String status; // 登录状态 private String message; // 提示消息 private HttpServletRequest request; // 请求对象 private Object[] args; // 其他参数 } ``` ## 登录日志发布器 ### LoginLogPublisher 使用方式 ```java @Service public class AuthService { @Autowired private LoginLogPublisher loginLogPublisher; /** * 用户登录 */ public R login(LoginDto loginDto) { try { // 执行登录逻辑 LoginUser loginUser = performLogin(loginDto); // 发布登录成功日志 loginLogPublisher.publishLoginLog( loginDto.getUsername(), DictOperResult.SUCCESS.getValue(), "登录成功", loginUser.getTenantId(), loginUser.getUserId(), UserType.PC_USER.getDeviceType() ); return R.ok(buildLoginVo(loginUser)); } catch (Exception e) { // 发布登录失败日志 loginLogPublisher.publishLoginLog( loginDto.getUsername(), DictOperResult.FAIL.getValue(), "登录失败:" + e.getMessage(), getTenantId() ); throw e; } } } ``` ### 重载方法说明 | 方法签名 | 适用场景 | 说明 | |----------------------------------------------------------------------------|--------|--------------------| | `publishLoginLog(userName, status, message, tenantId, userId, deviceType)` | 完整参数场景 | 适用于需要指定设备类型的场景 | | `publishLoginLog(userName, status, message, tenantId, userId)` | 默认PC设备 | 简化调用,默认PC设备类型 | | `publishLoginLog(userName, status, message, tenantId)` | 最简场景 | 用户ID为null的场景,如注册失败 | ## 配置说明 ### 自动配置 模块通过Spring Boot自动配置机制加载,配置文件位于: ``` META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ``` 内容: ``` plus.ruoyi.common.log.aspect.LogAspect plus.ruoyi.common.log.publisher.LoginLogPublisher ``` ### 依赖模块 ```xml plus.ruoyi ruoyi-common-satoken plus.ruoyi ruoyi-common-json ``` ## 特性说明 ### 1. 敏感信息过滤 系统自动过滤以下敏感字段: * `password` - 密码 * `oldPassword` - 旧密码 * `newPassword` - 新密码 * `confirmPassword` - 确认密码 ### 2. 对象类型过滤 自动过滤以下类型的对象,避免序列化问题: * `MultipartFile` - 文件上传对象 * `HttpServletRequest` - 请求对象 * `HttpServletResponse` - 响应对象 * `BindingResult` - 验证结果对象 ### 3. 参数长度限制 为防止日志过大,系统对各字段进行长度限制: * `operUrl` - 最大255字符 * `operParam` - 最大65535字符 * `jsonResult` - 最大65535字符 * `errorMsg` - 最大65535字符 ### 4. 性能优化 * 使用`ThreadLocal`存储计时器,避免线程安全问题 * 异步事件发布,不阻塞业务流程 * 异常捕获机制,确保日志记录失败不影响业务 ## 扩展说明 ### 操作类型说明 系统内置的`DictOperType`枚举定义了以下操作类型: ```java @Getter @AllArgsConstructor public enum DictOperType { INSERT("1", "新增"), // 新增操作 UPDATE("2", "修改"), // 修改操作 DELETE("3", "删除"), // 删除操作 GRANT("4", "授权"), // 授权操作 EXPORT("5", "导出"), // 导出操作 IMPORT("6", "导入"), // 导入操作 FORCE("7", "强退"), // 强制退出 GENCODE("8", "生成代码"), // 代码生成 CLEAN("9", "清空数据"), // 清空数据 OTHER("99", "其他"); // 其他操作 // 字典类型常量 public static final String DICT_TYPE = "sys_oper_type"; // 工具方法 public static DictOperType getByValue(String value) { ...} public static DictOperType getByLabel(String label) { ...} } ``` #### 操作类型使用示例 ```java // 用户管理相关 @Log(title = "用户管理", operType = DictOperType.INSERT) @Log(title = "用户管理", operType = DictOperType.UPDATE) @Log(title = "用户管理", operType = DictOperType.DELETE) // 权限管理相关 @Log(title = "角色管理", operType = DictOperType.GRANT) // 数据操作相关 @Log(title = "用户数据", operType = DictOperType.EXPORT) @Log(title = "用户数据", operType = DictOperType.IMPORT) // 系统管理相关 @Log(title = "在线用户", operType = DictOperType.FORCE) @Log(title = "代码生成", operType = DictOperType.GENCODE) @Log(title = "系统数据", operType = DictOperType.CLEAN) ``` ### 事件监听器 可以创建事件监听器来处理日志事件: ```java @Component public class LogEventListener { @EventListener @Async public void handleOperLogEvent(OperLogEvent event) { // 保存到数据库 operLogService.save(event); } @EventListener @Async public void handleLoginLogEvent(LoginLogEvent event) { // 保存到数据库 loginLogService.save(event); } } ``` ## 最佳实践 ### 1. 注解使用建议 * **标题命名**:使用清晰的模块名称,如"用户管理"、"角色管理" * **操作类型**:选择合适的操作类型,便于后续统计分析 * **敏感参数**:主动排除密码等敏感参数 * **大数据量**:对于导出等大数据量操作,考虑不保存响应数据 ### 2. 性能考虑 * 合理使用`isSaveRequestData`和`isSaveResponseData` * 对于频繁调用的接口,考虑是否需要记录日志 * 大对象参数建议排除,避免日志表过大 ### 3. 安全建议 * 确保敏感信息不被记录 * 定期清理历史日志数据 * 控制日志访问权限 ## 常见问题 ### Q1: 日志没有记录怎么办? 检查以下几点: 1. 确认方法上有`@Log`注解 2. 确认Spring AOP配置正确 3. 检查事件监听器是否正常工作 4. 查看应用日志是否有异常信息 ### Q2: 如何避免记录敏感信息? 使用`excludeParamNames`属性排除敏感字段: ```java @Log(title = "用户管理", excludeParamNames = {"password", "token", "secretKey"}) ``` ### Q3: 大文件上传时日志过大? 文件上传对象会被自动过滤,不会记录到日志中。如果仍有问题,可以设置`isSaveRequestData = false`。 ### Q4: 如何自定义日志存储? 实现事件监听器来处理日志事件: ```java @EventListener public void handleOperLogEvent(OperLogEvent event) { // 自定义存储逻辑 } ``` ## 总结 日志管理模块提供了完整的操作审计功能,通过注解驱动的方式大大简化了日志记录的复杂度。合理使用该模块可以帮助系统实现: * **操作审计**:完整记录用户操作行为 * **安全监控**:及时发现异常操作 * **问题排查**:通过日志快速定位问题 * **合规要求**:满足审计和合规要求 在实际使用中,建议根据业务需求合理配置日志记录策略,平衡功能完整性和系统性能。 --- --- url: 'https://ruoyi.plus/frontend/i18n/date-i18n.md' --- # 日期国际化 --- --- url: 'https://ruoyi.plus/frontend/utils/date.md' --- # 日期工具 --- --- url: 'https://ruoyi.plus/mobile/utils/date.md' --- # 日期工具 --- --- url: 'https://ruoyi.plus/mobile/utils/permission.md' --- # 权限工具 --- --- url: 'https://ruoyi.plus/frontend/directives/permission.md' --- # 权限指令 (v-auth) --- --- url: 'https://ruoyi.plus/frontend/stores/permission-store.md' --- # 权限状态 (permission) --- --- url: 'https://ruoyi.plus/mobile/plugins/permission.md' --- # 权限管理插件 --- --- url: 'https://ruoyi.plus/backend/common/satoken.md' --- # 权限认证 (satoken) ## 概述 Sa-Token权限认证模块是基于Sa-Token框架的权限认证组件,提供了完整的用户认证与权限控制功能。该模块采用JWT简单模式,结合Caffeine + Redis二级缓存架构,为系统提供高性能、可扩展的权限管理解决方案。 ## 核心特性 * **JWT无状态认证**:基于JWT的Token认证,支持分布式部署 * **二级缓存架构**:Caffeine本地缓存 + Redis分布式缓存 * **多用户体系**:支持不同类型用户(PC、APP等)的权限管理 * **多设备支持**:同一用户可在多种设备上登录 * **租户隔离**:支持多租户环境下的用户管理 * **统一异常处理**:友好的认证授权异常响应 ## 模块结构 ```text ruoyi-common-satoken/ ├── config/ │ └── SaTokenConfig.java # Sa-Token配置类 ├── core/ │ ├── dao/ │ │ └── PlusSaTokenDao.java # 增强Token存储层 │ └── service/ │ └── SaPermissionImpl.java # 权限管理实现类 ├── handler/ │ └── SaTokenExceptionHandler.java # 异常处理器 └── utils/ └── LoginHelper.java # 登录鉴权助手工具 ``` ## 依赖配置 ### Maven依赖 ```xml plus.ruoyi ruoyi-common-satoken ``` ### 核心依赖说明 ```xml cn.dev33 sa-token-spring-boot3-starter cn.dev33 sa-token-jwt com.github.ben-manes.caffeine caffeine ``` ## 配置说明 ### 基础配置 #### 内置配置文件:`common-satoken.yml` ```yaml # Sa-Token配置 sa-token: # 允许动态设置 token 有效期 dynamic-active-timeout: true # 允许从 请求参数 读取 token is-read-body: true # 允许从 header 读取 token is-read-header: true # 关闭 cookie 鉴权 从根源杜绝 csrf 漏洞风险 is-read-cookie: false # token前缀 token-prefix: "Bearer" ``` #### 应用配置文件:`application.yml` ```yaml ################## 安全认证配置 ################## # Sa-Token配置 sa-token: # token名称 (同时也是cookie名称) token-name: Authorization # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) is-share: false # jwt秘钥 jwt-secret-key: xxxxxxxxxxxxxxxx ``` #### 配置项详细说明 | 配置项 | 类型 | 默认值 | 说明 | |-------|-----|-------|------| | `token-name` | String | Authorization | Token名称,用于HTTP Header和Cookie | | `is-concurrent` | Boolean | true | 是否允许同一账号并发登录 | | `is-share` | Boolean | false | 多人登录同账号时是否共用Token | | `jwt-secret-key` | String | - | JWT签名密钥,生产环境务必修改 | | `dynamic-active-timeout` | Boolean | true | 是否允许动态设置Token有效期 | | `is-read-body` | Boolean | true | 是否从请求体中读取Token | | `is-read-header` | Boolean | true | 是否从请求头中读取Token | | `is-read-cookie` | Boolean | false | 是否从Cookie中读取Token | | `token-prefix` | String | Bearer | Token前缀,用于Header验证 | ### 自动配置 模块通过`SaTokenConfig`类提供自动配置: ```java @AutoConfiguration @PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class) public class SaTokenConfig { @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForSimple(); } @Bean public StpInterface stpInterface() { return new SaPermissionImpl(); } @Bean public SaTokenDao saTokenDao() { return new PlusSaTokenDao(); } @Bean public SaTokenExceptionHandler saTokenExceptionHandler() { return new SaTokenExceptionHandler(); } } ``` ## 核心组件 ### 1. 增强Token存储层 (PlusSaTokenDao) 采用二级缓存架构,提供高性能Token存储: ```java public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject { // Caffeine本地缓存,5秒过期,容量1000 private static final Cache CAFFEINE = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .initialCapacity(100) .maximumSize(1000) .build(); // 优先从本地缓存获取,未命中则查询Redis @Override public String get(String key) { Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key)); return (String) o; } } ``` **性能优化特点**: * 一级缓存:Caffeine本地缓存,提供毫秒级访问 * 二级缓存:Redis分布式缓存,支持集群数据共享 * 写入时失效本地缓存,保证数据一致性 * 搜索结果缓存,减少重复查询 ### 2. 权限管理实现 (SaPermissionImpl) 实现Sa-Token的权限接口,支持两种查询模式: ```java public class SaPermissionImpl implements StpInterface { @Override public List getPermissionList(Object loginId, String authType) { LoginUser loginUser = LoginHelper.getLoginUser(); // 判断是否为跨用户权限查询 if (ObjectUtil.isNull(loginUser) || !loginUser.getLoginId().equals(loginId)) { return getPermissionFromService(loginId); } // 当前用户权限查询 return new ArrayList<>(loginUser.getMenuPermission()); } } ``` ### 3. 登录鉴权助手 (LoginHelper) 提供便捷的登录认证和用户信息获取功能: ```java public class LoginHelper { // 用户登录 public static void login(LoginUser loginUser, SaLoginParameter loginParameter) { StpUtil.login(loginUser.getLoginId(), loginParameter); StpUtil.getTokenSession().set(LOGIN_USER, loginUser); } // 获取当前用户信息 public static T getLoginUser() { SaSession session = StpUtil.getTokenSession(); return (T) session.get(LOGIN_USER); } } ``` ### 4. 异常处理器 (SaTokenExceptionHandler) 统一处理认证授权异常: ```java @RestControllerAdvice public class SaTokenExceptionHandler { @ExceptionHandler(NotPermissionException.class) public R handleNotPermissionException(NotPermissionException e) { return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权"); } @ExceptionHandler(NotLoginException.class) public R handleNotLoginException(NotLoginException e) { return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源"); } } ``` ## 使用示例 ### 用户登录 ```java @Service public class LoginService { public String login(String username, String password) { // 验证用户信息 LoginUser loginUser = authenticateUser(username, password); // 创建登录参数 SaLoginParameter loginParameter = new SaLoginParameter() .setDevice("web") // 设备类型 .setTimeout(7200); // 2小时过期 // 执行登录 LoginHelper.login(loginUser, loginParameter); // 返回Token return StpUtil.getTokenValue(); } } ``` ### Token使用方式 根据配置,系统支持多种Token传递方式: #### 1. HTTP Header方式(推荐) ```javascript // 前端发送请求时携带Token fetch('/api/userInfo', { headers: { 'Authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...' } }) ``` #### 2. 请求参数方式 ```javascript // URL参数方式 fetch('/api/userInfo?Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...') // 表单参数方式 const formData = new FormData(); formData.append('Authorization', 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...'); ``` ### 并发登录控制 系统提供灵活的并发登录控制策略: ```java @Service public class ConcurrentLoginService { /** * 配置 is-concurrent: true, is-share: false * 允许多设备登录,每次登录生成新Token */ public void multiDeviceLogin() { // 用户在Web端登录 LoginHelper.login(loginUser, new SaLoginParameter().setDevice("web")); // 用户在APP端登录(不会挤掉Web端) LoginHelper.login(loginUser, new SaLoginParameter().setDevice("app")); // 两个Token都有效,互不影响 } /** * 配置 is-concurrent: false * 单一登录,新登录挤掉旧登录 */ public void singleLogin() { // 第一次登录 String token1 = login(loginUser); // Token有效 // 第二次登录会使第一次登录失效 String token2 = login(loginUser); // token1失效,token2有效 } } ``` ### 获取用户信息 ```java @RestController public class UserController { @GetMapping("/userInfo") public R getUserInfo() { // 获取当前登录用户 LoginUser loginUser = LoginHelper.getLoginUser(); // 获取用户基本信息 Long userId = LoginHelper.getUserId(); String userName = LoginHelper.getUserName(); String tenantId = LoginHelper.getTenantId(); return R.ok(loginUser); } } ``` ### 权限验证 ```java @RestController public class SystemController { @GetMapping("/admin/users") @SaCheckPermission("system:user:list") // 权限验证注解 public R> listUsers() { // 或者编程式权限验证 StpUtil.checkPermission("system:user:list"); return R.ok(userService.listUsers()); } @GetMapping("/admin/config") @SaCheckRole("admin") // 角色验证注解 public R getConfig() { // 编程式角色验证 StpUtil.checkRole("admin"); return R.ok(configService.getConfig()); } } ``` ### 权限判断 ```java @Service public class BusinessService { public void processData() { // 判断是否登录 if (!LoginHelper.isLogin()) { throw new ServiceException("请先登录"); } // 判断是否为超级管理员 if (LoginHelper.isSuperAdmin()) { // 超级管理员逻辑 processSuperAdminData(); } else { // 普通用户逻辑 processNormalUserData(); } // 判断是否为租户管理员 if (LoginHelper.isTenantAdmin()) { // 租户管理员逻辑 processTenantAdminData(); } } } ``` ## 多用户体系支持 ### 用户类型和设备支持 ```java // 支持不同用户类型登录 public enum UserType { SYS_USER("sys_user", "系统用户"), APP_USER("app_user", "应用用户"); public static UserType getUserType(String loginId) { return Arrays.stream(values()) .filter(type -> loginId.startsWith(type.getLoginType())) .findFirst() .orElse(SYS_USER); } } // 不同设备类型登录 SaLoginParameter webLogin = new SaLoginParameter().setDevice("web"); SaLoginParameter appLogin = new SaLoginParameter().setDevice("app"); SaLoginParameter miniLogin = new SaLoginParameter().setDevice("mini"); ``` ### 微信生态集成 ```java // 获取微信相关信息 String appid = LoginHelper.getAppid(); // 微信应用ID String unionid = LoginHelper.getUnionid(); // 微信UnionID String openid = LoginHelper.getOpenid(); // 微信OpenID ``` ## 性能优化 ### 缓存策略 1. **本地缓存**:使用Caffeine缓存热点数据,5秒过期 2. **分布式缓存**:使用Redis存储Token信息,支持集群 3. **读写策略**:读取优先本地缓存,写入失效本地缓存 4. **搜索优化**:缓存搜索结果,避免重复查询 ### 性能监控 ```java // Token存储性能统计 public void monitorCachePerformance() { CacheStats stats = CAFFEINE.stats(); log.info("缓存命中率: {}", stats.hitRate()); log.info("缓存请求次数: {}", stats.requestCount()); log.info("缓存加载次数: {}", stats.loadCount()); } ``` ## 安全配置 ### Token安全 * **Token名称**:使用"Authorization"作为Header和Cookie名称 * **前缀验证**:Token必须以"Bearer "开头 * **CSRF防护**:禁用Cookie读取Token,防止CSRF攻击 * **JWT签名**:使用强密钥进行JWT签名验证 * **动态过期**:支持动态调整Token有效期 * **多端隔离**:不同设备Token相互隔离 #### JWT密钥安全要求 ```yaml # 生产环境JWT密钥配置建议 sa-token: jwt-secret-key: ${JWT_SECRET_KEY:xxxxxxxxxxxxxxx} # 使用环境变量 ``` **密钥安全建议**: 1. **长度要求**:至少32个字符 2. **复杂性**:包含大小写字母、数字和特殊字符 3. **唯一性**:每个环境使用不同的密钥 4. **保密性**:通过环境变量或配置中心管理 5. **定期更换**:建议定期更换密钥 #### 并发登录安全策略 | 场景 | is-concurrent | is-share | 安全特点 | |------|--------------|----------|----------| | 单设备登录 | false | - | 最高安全性,新登录挤掉旧登录 | | 多设备独立Token | true | false | 平衡安全性,每设备独立Token | | 多设备共享Token | true | true | 便利性高,安全性较低 | ### 权限控制 * **细粒度权限**:支持菜单级别的权限控制 * **角色权限**:基于角色的权限管理 * **租户隔离**:多租户环境下的数据隔离 * **超级管理员**:系统级别的最高权限 ## 异常处理 系统提供三种主要异常的统一处理: | 异常类型 | HTTP状态码 | 错误信息 | |---------|-----------|---------| | NotLoginException | 401 | 认证失败,无法访问系统资源 | | NotPermissionException | 403 | 没有访问权限,请联系管理员授权 | | NotRoleException | 403 | 没有访问权限,请联系管理员授权 | ## 最佳实践 ### 1. Token管理 ```java // 设置合理的Token过期时间 SaLoginParameter loginParameter = new SaLoginParameter() .setTimeout(7200) // 2小时过期 .setActiveTimeout(1800); // 30分钟无操作自动过期 // 主动续期Token StpUtil.renewTimeout(7200); // 根据业务场景选择合适的并发登录策略 // 1. 金融类应用:单设备登录 SaLoginParameter singleDevice = new SaLoginParameter() .setDevice("web"); // 配合 is-concurrent: false // 2. 办公类应用:多设备独立Token SaLoginParameter multiDevice = new SaLoginParameter() .setDevice("web"); // 配合 is-concurrent: true, is-share: false // 3. 社交类应用:多设备共享Token SaLoginParameter sharedToken = new SaLoginParameter() .setDevice("web"); // 配合 is-concurrent: true, is-share: true ``` ### 2. 权限验证 ```java // 优先使用注解方式进行权限验证 @SaCheckPermission("system:user:add") public R addUser(User user) { // 业务逻辑 } // 复杂权限逻辑使用编程式验证 if (StpUtil.hasPermission("system:user:edit") || LoginHelper.isSuperAdmin()) { // 执行编辑操作 } ``` ### 3. 异常处理 ```java // 业务层主动检查登录状态 public void businessMethod() { if (!LoginHelper.isLogin()) { throw new ServiceException("用户未登录"); } // 业务逻辑 } ``` ### 4. 缓存使用 ```java // 合理设置缓存过期时间 public void setCacheData(String key, Object value) { // 热点数据设置较长过期时间 RedisUtils.setCacheObject(key, value, Duration.ofHours(1)); } ``` ## 扩展指南 ### 自定义权限实现 ```java @Component public class CustomPermissionService implements PermissionService { @Override public Set listMenuPermissions(Long userId) { // 自定义权限查询逻辑 return customPermissionQuery(userId); } @Override public Set listRolePermissions(Long userId) { // 自定义角色查询逻辑 return customRoleQuery(userId); } } ``` ### 自定义Token存储 ```java @Component public class CustomSaTokenDao extends PlusSaTokenDao { @Override public String get(String key) { // 自定义获取逻辑 return customGet(key); } @Override public void set(String key, String value, long timeout) { // 自定义存储逻辑 customSet(key, value, timeout); } } ``` ## 故障排查 ### 常见问题 1. **Token无效** * 检查Token格式是否正确(Bearer前缀) * 检查Token是否过期 * 检查Redis连接是否正常 2. **权限验证失败** * 检查用户是否拥有对应权限 * 检查权限标识是否正确 * 检查PermissionService实现是否正确 3. **缓存问题** * 检查Caffeine配置是否合理 * 检查Redis配置是否正确 * 监控缓存命中率和性能 ### 调试建议 ```java // 开启Sa-Token调试日志 logging: level: cn.dev33.satoken: DEBUG // 监控Token信息 public void debugToken() { log.info("Token值: {}", StpUtil.getTokenValue()); log.info("登录ID: {}", StpUtil.getLoginId()); log.info("Token剩余时间: {}", StpUtil.getTokenTimeout()); } ``` --- --- url: 'https://ruoyi.plus/frontend/router/permission-routes.md' --- # 权限路由 --- --- url: 'https://ruoyi.plus/mobile/uniapp/conditional.md' --- # 条件编译 --- --- url: 'https://ruoyi.plus/mobile/platform/conditional.md' --- # 条件编译使用 --- --- url: 'https://ruoyi.plus/frontend/architecture/build-process.md' --- # 构建流程 --- --- url: 'https://ruoyi.plus/frontend/dev/build-config.md' --- # 构建配置详解 --- --- url: 'https://ruoyi.plus/frontend/types/enums.md' --- # 枚举类型 --- --- url: 'https://ruoyi.plus/mobile/layouts/tabbar.md' --- # 标签栏配置 --- --- url: 'https://ruoyi.plus/frontend/stores/tags-view-store.md' --- # 标签视图 (tagsView) --- --- url: 'https://ruoyi.plus/frontend/layout/tags-view.md' --- # 标签视图 (TagsView) --- --- url: 'https://ruoyi.plus/frontend/utils/tab.md' --- # 标签页工具 --- --- url: 'https://ruoyi.plus/frontend/utils/tree.md' --- # 树形工具 --- --- url: 'https://ruoyi.plus/frontend/styles/best-practices.md' --- # 样式最佳实践 --- --- url: 'https://ruoyi.plus/mobile/styles/best-practices.md' --- # 样式最佳实践 --- --- url: 'https://ruoyi.plus/frontend/styles/style-architecture.md' --- # 样式架构 --- --- url: 'https://ruoyi.plus/mobile/styles/overview.md' --- # 样式概览 --- --- url: 'https://ruoyi.plus/backend/common/core.md' --- # 核心模块 (common-core) 概览 ## 模块简介 `ruoyi-common-core` 是ruoyi-plus-uniapp框架的核心基础模块,为整个系统提供通用的基础功能和工具支持。该模块采用无业务逻辑的设计理念,专注于提供可复用的技术组件和工具类,是所有其他模块的基础依赖。 ## 核心特性 ### 🎯 统一响应体系 * 提供标准化的 API 响应格式 (`R`) * 支持国际化消息处理 * 统一的状态码管理 ### 🛡️ 安全防护机制 * XSS 攻击防护 (`@Xss` 注解) * SQL 注入防护工具 * 参数校验与数据验证 ### 🔧 丰富的工具类库 * 字符串处理增强 (`StringUtils`) * 日期时间工具 (`DateUtils`) * 反射操作工具 (`ReflectUtils`) * 网络地址工具 (`NetUtils`) * 文件操作工具 (`FileUtils`) ### 📊 数据模型支持 * 登录认证模型系列 * 数据传输对象 (DTO) 规范 * 树形结构构建工具 ### 🌐 国际化支持 * 多语言消息管理 * 自定义消息插值器 * 校验错误信息国际化 ### ⚡ 高性能配置 * 虚拟线程支持 * 异步任务配置 * 线程池优化管理 ## 模块架构 ```text ruoyi-common-core/ ├── config/ # 配置管理 │ ├── AsyncConfig # 异步任务配置 │ ├── ThreadPoolConfig # 线程池配置 │ ├── ValidatorConfig # 校验器配置 │ └── properties/ # 配置属性类 ├── constant/ # 系统常量 │ ├── CacheConstants # 缓存常量 │ ├── Constants # 通用常量 │ ├── HttpStatus # HTTP状态码 │ └── I18nKeys # 国际化键常量 ├── domain/ # 数据模型 │ ├── dto/ # 数据传输对象 │ ├── model/ # 业务模型 │ └── vo/ # 视图对象 ├── enums/ # 枚举定义 │ ├── AuthType # 认证类型 │ ├── UserType # 用户类型 │ └── DateTimeFormat # 日期格式 ├── exception/ # 异常体系 │ ├── base/ # 基础异常类 │ ├── user/ # 用户异常 │ └── file/ # 文件异常 ├── service/ # 通用服务接口 │ ├── UserService # 用户服务 │ ├── DictService # 字典服务 │ └── ConfigService # 配置服务 ├── utils/ # 工具类库 │ ├── StringUtils # 字符串工具 │ ├── DateUtils # 日期工具 │ ├── ReflectUtils # 反射工具 │ └── ip/ # IP地址工具 ├── validate/ # 校验框架 │ ├── AddGroup # 新增校验组 │ ├── EditGroup # 编辑校验组 │ └── dicts/ # 字典校验 └── xss/ # XSS防护 ├── Xss # XSS注解 └── XssValidator # XSS校验器 ``` ## 核心组件 ### 1. 响应体系 (Response System) #### 统一响应类 `R` ```java // 成功响应 R result = R.ok(user); // 失败响应 R result = R.fail("操作失败"); // 支持国际化 R result = R.fail("user.not.found", userId); ``` 特性: * 泛型支持,类型安全 * 自动国际化消息处理 * 链式操作支持 * 统一状态码管理 ### 2. 安全防护 (Security Protection) #### XSS 攻击防护 ```java public class UserBo { @Xss(mode = Xss.Mode.STRICT, message = "用户名包含危险字符") private String userName; @Xss(mode = Xss.Mode.LENIENT) private String content; } ``` 支持三种检测模式: * **BASIC**: 检测常见HTML标签和脚本 * **STRICT**: 检测更多攻击向量 * **LENIENT**: 仅检测明显脚本标签 ### 3. 校验框架 (Validation Framework) #### 字典值校验 ```java public class UserBo { @DictPattern(dictType = "sys_user_sex", message = "性别值无效") private String gender; @DictPattern(dictType = "sys_user_status", separator = ";") private String statusList; } ``` #### 枚举值校验 ```java public class OrderBo { @EnumPattern(type = OrderStatusEnum.class, fieldName = "code") private String status; } ``` #### 校验分组 ```java // 新增时校验 @Validated(AddGroup.class) public R add(@RequestBody UserBo bo) { return R.ok(userService.add(bo)); } // 编辑时校验 @Validated(EditGroup.class) public R update(@RequestBody UserBo bo) { return R.status(userService.update(bo)); } ``` ### 4. 字典枚举系统 (Dictionary & Enum System) #### 字典枚举设计模式 ```java @Getter @AllArgsConstructor public enum DictUserGender { FEMALE("0", "女"), MALE("1", "男"), UNKNOWN("2", "未知"); public static final String DICT_TYPE = "sys_user_gender"; private final String value; private final String label; public static DictUserGender getByValue(String value) { // 实现逻辑 } } ``` 内置字典枚举: * **用户相关**:性别、状态、类型 * **系统相关**:启用状态、显示设置、操作结果 * **业务相关**:支付方式、订单状态、审核状态 * **平台相关**:消息类型、通知类型、文件类型 ### 5. 工具类库 (Utility Library) #### 增强的字符串工具 ```java // 多级属性转换 String result = StringUtils.convertWithMapping("1,2", statusMap); // 分割转换 List ids = StringUtils.splitToList("1,2,3", Integer::valueOf); // 路径匹配 boolean match = StringUtils.isMatch("/api/*", "/api/user"); ``` #### 日期时间工具 ```java // 格式化 String date = DateUtils.formatDate(new Date()); // 时间差计算 long days = DateUtils.difference(start, end, TimeUnit.DAYS); // 类型转换 Date date = DateUtils.toDate(LocalDateTime.now()); ``` #### 反射工具增强 ```java // 多级属性访问 String name = ReflectUtils.invokeGetter(user, "profile.name"); ReflectUtils.invokeSetter(user, "profile.name", "张三"); // 安全访问 String name = ReflectUtils.getPropertySafely(user, "profile.name"); ``` ### 6. 异步与线程 (Async & Threading) #### 自动线程模式切换 ```java @Configuration public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { // 自动检测虚拟线程支持 if (SpringUtils.isVirtual()) { return new VirtualThreadTaskExecutor("async-"); } return SpringUtils.getBean("scheduledExecutorService"); } } ``` #### 优雅关闭支持 ```java // 安全关闭线程池 ThreadUtils.shutdownGracefully(executorService); // 异常日志记录 ThreadUtils.logException(runnable, throwable); ``` ### 7. 网络与IP处理 (Network & IP) #### IP地址识别与定位 ```java // IPv4/IPv6识别 boolean isV4 = NetUtils.isIpv4("192.168.1.1"); boolean isV6 = NetUtils.isIpv6("2001:db8::1"); // 内网判断 boolean isInner = NetUtils.isInnerIP("192.168.1.1"); boolean isInnerV6 = NetUtils.isInnerIpv6("fe80::1"); // 地址解析 String location = AddressUtils.getRealAddressByIp("8.8.8.8"); ``` ## 依赖管理 ### 主要依赖 * **Spring Framework**: 核心容器和Web支持 * **Spring Boot**: 自动配置和起步依赖 * **Apache Commons Lang3**: 通用工具类 * **HuTool**: 国产工具类库 * **ip2region**: IP地址定位 * **MapStruct Plus**: 对象映射 ### 可选依赖 * **Lombok**: 代码简化 (编译时) * **Jakarta Servlet**: Web容器支持 * **Spring Validation**: 参数校验 ## 设计原则 ### 1. 零业务耦合 * 不包含任何业务逻辑 * 纯技术组件设计 * 高度可复用 ### 2. 面向接口编程 * 定义通用服务接口 * 支持多种实现方式 * 便于扩展和测试 ### 3. 国际化优先 * 所有消息支持国际化 * 自动消息插值处理 * 多语言环境适配 ### 4. 安全第一 * 内置安全防护机制 * XSS/SQL注入防护 * 输入参数严格校验 ### 5. 性能优化 * 支持虚拟线程 * 缓存机制优化 * 资源优雅释放 ## 使用示例 ### 快速开始 #### 1. 添加依赖 ```xml plus.ruoyi ruoyi-common-core ${ruoyi.version} ``` #### 2. 创建响应接口 ```java @RestController public class UserController { @PostMapping("/users") public R createUser(@Validated(AddGroup.class) @RequestBody UserBo bo) { Long userId = userService.createUser(bo); return R.ok(userId); } @GetMapping("/users/{id}") public R getUser(@PathVariable Long id) { UserVo user = userService.getUser(id); return R.ok(user); } } ``` #### 3. 使用工具类 ```java @Service public class UserService { public void processUsers(List users) { // 使用Stream工具 Map userMap = StreamUtils.toIdentityMap(users, User::getId); // 使用字符串工具 String userNames = StreamUtils.join(users, User::getName, ","); // 使用日期工具 String createTime = DateUtils.formatDateTime(new Date()); } } ``` ## 最佳实践 ### 1. 异常处理 ```java // 使用统一异常 throw ServiceException.of("用户不存在"); // 支持国际化 throw UserException.of("user.not.found", userId); ``` ### 2. 参数校验 ```java // 组合使用多种校验 public class UserBo { @NotBlank(groups = {AddGroup.class, EditGroup.class}) @Xss(message = "用户名包含危险字符") private String userName; @DictPattern(dictType = "sys_user_sex") private String gender; } ``` ### 3. 响应规范 ```java // 成功响应 return R.ok(data); // 失败响应 return R.fail("操作失败"); // 条件响应 return R.status(result > 0); ``` ### 4. 国际化消息 ```java // 直接使用国际化键 return R.fail("user.password.invalid"); // 带参数的国际化 return R.fail("user.login.locked", lockTime); ``` ## 注意事项 * **线程安全**: 所有工具类都是线程安全的 * **性能考虑**: 大量数据处理时注意内存使用 * **异常处理**: 优先使用框架提供的异常类型 * **编码规范**: 遵循阿里巴巴Java开发手册 * **版本兼容**: 保持向后兼容性 --- --- url: 'https://ruoyi.plus/frontend/utils/format.md' --- # 格式化工具 --- --- url: 'https://ruoyi.plus/mobile/utils/format.md' --- # 格式化工具 --- --- url: 'https://ruoyi.plus/frontend/architecture/modular-design.md' --- # 模块化设计 --- --- url: 'https://ruoyi.plus/backend/modules/development-guide.md' --- # 模块开发指南 --- --- url: 'https://ruoyi.plus/frontend/utils/modal.md' --- # 模态框工具 --- --- url: 'https://ruoyi.plus/frontend/views/register.md' --- # 注册页面 --- --- url: 'https://ruoyi.plus/practices/comment-standards.md' --- # 注释规范 --- --- url: 'https://ruoyi.plus/frontend/views/test.md' --- # 测试页面 --- --- url: 'https://ruoyi.plus/mobile/performance/rendering.md' --- # 渲染性能优化 --- --- url: 'https://ruoyi.plus/frontend/utils/scroll.md' --- # 滚动工具 --- --- url: 'https://ruoyi.plus/mobile/build/version-management.md' --- # 版本管理策略 --- --- url: 'https://ruoyi.plus/frontend/stores/state-persistence.md' --- # 状态持久化 --- --- url: 'https://ruoyi.plus/frontend/stores/best-practices.md' --- # 状态管理最佳实践 --- --- url: 'https://ruoyi.plus/frontend/types/store-types.md' --- # 状态类型 --- --- url: 'https://ruoyi.plus/mobile/build/environment.md' --- # 环境配置 --- --- url: 'https://ruoyi.plus/mobile/uniapp/lifecycle.md' --- # 生命周期 --- --- url: 'https://ruoyi.plus/mobile/pages/user.md' --- # 用户中心 (user) --- --- url: 'https://ruoyi.plus/mobile/api/user.md' --- # 用户接口 --- --- url: 'https://ruoyi.plus/frontend/stores/user-store.md' --- # 用户状态 (user) --- --- url: 'https://ruoyi.plus/mobile/pages/login.md' --- # 登录页 (login) --- --- url: 'https://ruoyi.plus/frontend/views/login.md' --- # 登录页面 --- --- url: 'https://ruoyi.plus/mobile/platform/baidu.md' --- # 百度小程序适配 --- --- url: 'https://ruoyi.plus/practices/monitoring-alerting.md' --- # 监控告警 --- --- url: 'https://ruoyi.plus/backend/extend/monitor-admin.md' --- # 监控管理 (monitor-admin) --- --- url: 'https://ruoyi.plus/frontend/architecture/directory-structure.md' --- # 目录结构详解 --- --- url: 'https://ruoyi.plus/mobile/plugins/camera.md' --- # 相机插件 --- --- url: 'https://ruoyi.plus/mobile/debug/device.md' --- # 真机调试 --- --- url: 'https://ruoyi.plus/backend/common/sms.md' --- # 短信服务 (sms) ## 概述 短信模块是基于若依框架开发的通用短信服务模块,提供短信发送与验证码功能。该模块集成了SMS4J框架,支持多平台短信服务商,并提供统一的API接口和缓存管理。 ## 核心特性 * 🚀 **多平台支持**: 基于SMS4J框架,支持多个主流短信服务商 * 📦 **统一接口**: 提供标准化的短信发送API * 🔐 **验证码管理**: 内置验证码生成、存储和校验功能 * 💾 **缓存支持**: 集成Redis缓存,支持短信重试和拦截机制 * 🛡️ **异常处理**: 全局异常捕获和友好错误提示 * ⚡ **自动配置**: Spring Boot自动配置,开箱即用 ## 模块结构 ```text ruoyi-common-sms/ ├── pom.xml # Maven配置文件 ├── src/main/java/plus/ruoyi/common/sms/ │ ├── config/ │ │ └── SmsAutoConfiguration.java # 自动配置类 │ ├── core/ │ │ └── dao/ │ │ └── PlusSmsDao.java # 短信缓存DAO实现 │ └── handler/ │ └── SmsExceptionHandler.java # 全局异常处理器 └── src/main/resources/META-INF/ └── spring/ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports ``` ## 依赖关系 ### Maven依赖 ```xml plus.ruoyi ruoyi-common-redis org.dromara.sms4j sms4j-spring-boot-starter ``` ### 模块依赖关系 * **ruoyi-common-redis**: 提供Redis缓存支持,用于验证码存储与校验 * **sms4j-spring-boot-starter**: SMS4J框架,提供多平台短信发送支持 ## 核心组件 ### 1. 自动配置类 (SmsAutoConfiguration) 负责模块的自动装配和Bean注册: ```java @AutoConfiguration(after = {RedisAutoConfiguration.class}) public class SmsAutoConfiguration { @Primary @Bean public SmsDao smsDao() { return new PlusSmsDao(); } @Bean public SmsExceptionHandler smsExceptionHandler() { return new SmsExceptionHandler(); } } ``` **特性说明:** * 在Redis配置完成后执行,确保Redis可用 * 注册自定义的SmsDao实现,替换默认实现 * 自动注册全局异常处理器 ### 2. 短信缓存DAO (PlusSmsDao) 实现SMS4J的SmsDao接口,提供统一的缓存管理: ```java public class PlusSmsDao implements SmsDao { // 存储键值对,指定过期时间 public void set(String key, Object value, long cacheTime); // 存储键值对,永久缓存 public void set(String key, Object value); // 根据键获取缓存值 public Object get(String key); // 删除指定键的缓存 public Object remove(String key); // 清空所有短信相关缓存 public void clean(); } ``` **功能特性:** * 使用框架统一的RedisUtils工具类 * 所有缓存键自动添加全局前缀 `GlobalConstants.GLOBAL_REDIS_KEY` * 支持带过期时间的缓存存储 * 提供批量清理功能 ### 3. 全局异常处理器 (SmsExceptionHandler) 统一处理短信相关异常: ```java @RestControllerAdvice public class SmsExceptionHandler { @ExceptionHandler(SmsBlendException.class) public R handleSmsBlendException(SmsBlendException e, HttpServletRequest request) { String requestUri = request.getRequestURI(); log.error("请求地址'{}',发生短信发送异常.", requestUri, e); return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "短信发送失败,请稍后再试..."); } } ``` **处理机制:** * 全局捕获`SmsBlendException`异常 * 记录详细错误日志,包含请求URI * 返回用户友好的错误提示 * 使用统一的响应格式 `R` ## 使用指南 ### 1. 模块引入 在需要使用短信功能的模块中添加依赖: ```xml plus.ruoyi ruoyi-common-sms ``` ### 2. 配置短信服务 在`application.yml`中配置SMS4J相关参数: ```yaml sms: # 短信服务商配置 alibaba: access-key-id: your-access-key access-key-secret: your-access-secret signature: your-signature template-id: SMS_123456789 # 缓存配置 cache: # 验证码有效期(秒) code-expire: 300 # 短信发送间隔(秒) send-interval: 60 ``` ### 3. 业务层使用 ```java @Service public class SmsService { @Autowired private SmsBlend smsBlend; /** * 发送验证码短信 */ public void sendVerifyCode(String phone) { String code = RandomUtil.randomNumbers(6); // 构建短信消息 SmsMessage message = SmsMessage.builder() .phoneNumber(phone) .message("您的验证码是:" + code + ",有效期5分钟。") .build(); // 发送短信 smsBlend.sendMessage(message); // 缓存验证码 RedisUtils.setCacheObject("verify_code:" + phone, code, Duration.ofMinutes(5)); } /** * 校验验证码 */ public boolean verifyCode(String phone, String code) { String cachedCode = RedisUtils.getCacheObject("verify_code:" + phone); return Objects.equals(code, cachedCode); } } ``` ## 配置说明 ### Redis缓存配置 短信模块依赖Redis进行缓存管理,需确保以下配置: ```yaml spring: data: redis: host: localhost port: 6379 password: your-password database: 0 timeout: 10s lettuce: pool: max-active: 200 max-idle: 20 min-idle: 5 ``` ### 全局常量配置 缓存键前缀通过`GlobalConstants.GLOBAL_REDIS_KEY`统一管理: ```java public class GlobalConstants { /** * 全局Redis键前缀 */ public static final String GLOBAL_REDIS_KEY = "global:"; } ``` ## 扩展开发 ### 1. 自定义短信模板 ```java @Component public class CustomSmsTemplate { /** * 验证码模板 */ public String buildVerifyCodeMessage(String code) { return String.format("【若依框架】您的验证码是:%s,有效期5分钟,请勿泄露。", code); } /** * 通知模板 */ public String buildNotificationMessage(String content) { return String.format("【若依框架】%s", content); } } ``` ### 2. 扩展异常处理 ```java @RestControllerAdvice public class CustomSmsExceptionHandler extends SmsExceptionHandler { @ExceptionHandler(CustomSmsException.class) public R handleCustomSmsException(CustomSmsException e, HttpServletRequest request) { // 自定义异常处理逻辑 return R.fail("自定义短信异常处理"); } } ``` ### 3. 自定义缓存策略 ```java @Component public class CustomSmsDao extends PlusSmsDao { @Override public void set(String key, Object value, long cacheTime) { // 自定义缓存逻辑 super.set(key, value, cacheTime); // 额外处理... } } ``` ## 最佳实践 ### 1. 验证码安全 * 设置合理的验证码有效期(建议5-10分钟) * 限制同一手机号的发送频率(建议60秒间隔) * 验证码使用后立即清除缓存 * 记录发送日志,便于问题排查 ### 2. 异常处理 * 对用户隐藏具体的技术错误信息 * 记录详细的错误日志供开发者排查 * 提供重试机制和降级方案 * 监控短信发送成功率和失败原因 ### 3. 性能优化 * 使用Redis缓存减少重复验证 * 异步发送短信,避免阻塞主流程 * 合理设置连接池参数 * 定期清理过期的缓存数据 ## 常见问题 ### Q1: 短信发送失败怎么办? **A1:** 检查以下几点: * 短信服务商配置是否正确 * 账户余额是否充足 * 手机号格式是否正确 * 短信模板是否已审核通过 ### Q2: 验证码收不到? **A2:** 可能的原因: * 手机号被运营商拦截 * 短信服务商网络延迟 * 验证码已过期被清理 * 发送频率过高被限制 ### Q3: Redis连接异常? **A3:** 检查Redis配置: * Redis服务是否正常运行 * 连接参数是否正确 * 网络是否通畅 * 认证密码是否正确 --- --- url: 'https://ruoyi.plus/mobile/pages/demo.md' --- # 示例页面 (demo) --- --- url: 'https://ruoyi.plus/backend/common/social.md' --- # 社交登录 (social) ## 概述 社会化登录模块(ruoyi-common-social)是基于Ruoyi-Plus-Uniapp框架的第三方平台账号认证与登录功能模块。该模块提供了统一的社交平台登录认证接口,支持多种主流社交平台的OAuth2.0认证流程。 ## 模块架构 ### 核心组件 * **SocialUtils**: 社交登录认证工具类,提供统一的第三方登录认证接口 * **AuthRedisStateCache**: Redis实现的授权状态缓存,用于存储OAuth认证过程中的状态信息 * **SocialAutoConfiguration**: 社交登录自动配置类,管理相关Bean的创建和配置 ### 自定义实现 模块扩展了JustAuth框架,实现了以下自定义社交平台认证: * **MaxKey**: 企业级身份认证管理系统 * **TopIAM**: 身份管理平台 * **Gitea**: 自建Git服务器 ## 技术依赖 ### 内部模块依赖 ```xml plus.ruoyi ruoyi-common-json plus.ruoyi ruoyi-common-redis ``` ### 第三方依赖 ```xml me.zhyd.oauth JustAuth ``` ## 支持的社交平台 ### 国内平台 | 平台 | 标识符 | 描述 | |------|--------|------| | 钉钉 | dingtalk | 企业办公平台 | | 百度 | baidu | 百度账号登录 | | Gitee | gitee | 国内Git代码托管平台 | | 微博 | weibo | 新浪微博 | | Coding | coding | 代码托管平台 | | 开源中国 | oschina | OSChina社区 | | 支付宝 | alipay\_wallet | 支付宝钱包登录 | | QQ | qq | 腾讯QQ登录 | | 微信开放平台 | wechat\_open | 微信开放平台登录 | | 淘宝 | taobao | 淘宝账号登录 | | 抖音 | douyin | 抖音账号登录 | | 华为 | huawei | 华为账号登录 | | 企业微信 | wechat\_enterprise | 企业微信登录 | | 微信公众号 | wechat\_mp | 微信公众号登录 | | 阿里云 | aliyun | 阿里云账号登录 | ### 国外平台 | 平台 | 标识符 | 描述 | |------|--------|------| | GitHub | github | GitHub代码托管平台 | | LinkedIn | linkedin | 职业社交网络 | | Microsoft | microsoft | 微软账号 | | 人人网 | renren | 人人网(已停服) | | StackOverflow | stack\_overflow | 程序员问答社区 | | GitLab | gitlab | GitLab代码托管平台 | ### 自建/企业平台 | 平台 | 标识符 | 描述 | |------|--------|------| | MaxKey | maxkey | 企业级身份认证管理系统 | | TopIAM | topiam | 身份管理平台 | | Gitea | gitea | 自建Git服务器 | ## 配置方式 ### application.yml配置示例 ```yaml justauth: type: # GitHub配置 github: client-id: your_github_client_id client-secret: your_github_client_secret redirect-uri: http://localhost:8080/auth/callback/github scopes: - user:email - read:user # 企业微信配置 wechat_enterprise: client-id: your_corp_id client-secret: your_corp_secret redirect-uri: http://localhost:8080/auth/callback/wechat_enterprise agent-id: your_agent_id # MaxKey配置 maxkey: client-id: your_maxkey_client_id client-secret: your_maxkey_client_secret redirect-uri: http://localhost:8080/auth/callback/maxkey server-url: https://your-maxkey-server.com # TopIAM配置 topiam: client-id: your_topiam_client_id client-secret: your_topiam_client_secret redirect-uri: http://localhost:8080/auth/callback/topiam server-url: https://your-topiam-server.com scopes: - openid - profile - email # Gitea配置 gitea: client-id: your_gitea_client_id client-secret: your_gitea_client_secret redirect-uri: http://localhost:8080/auth/callback/gitea server-url: https://your-gitea-server.com ``` ### 配置参数说明 | 参数 | 类型 | 必填 | 描述 | |------|------|------|------| | client-id | String | 是 | 应用ID/客户端ID | | client-secret | String | 是 | 应用密钥/客户端密钥 | | redirect-uri | String | 是 | 授权回调地址 | | scopes | List\ | 否 | OAuth授权范围 | | agent-id | String | 否 | 企业微信应用ID | | server-url | String | 否 | 自建服务器地址(MaxKey/TopIAM/Gitea) | | alipay-public-key | String | 否 | 支付宝公钥 | | stack-overflow-key | String | 否 | StackOverflow API密钥 | ## 使用方法 ### 基础用法 ```java @RestController @RequestMapping("/auth") public class AuthController { @Autowired private SocialProperties socialProperties; /** * 获取授权URL */ @GetMapping("/authorize/{source}") public String authorize(@PathVariable String source) { AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties); return authRequest.authorize(AuthStateUtils.createState()); } /** * 登录回调处理 */ @GetMapping("/callback/{source}") public AuthResponse login(@PathVariable String source, @RequestParam String code, @RequestParam String state) { return SocialUtils.loginAuth(source, code, state, socialProperties); } } ``` ### 高级用法 #### 自定义状态缓存 ```java @Configuration public class CustomSocialConfig { @Bean @Primary public AuthStateCache customAuthStateCache() { return new CustomAuthStateCache(); } } public class CustomAuthStateCache implements AuthStateCache { // 实现自定义缓存逻辑 } ``` #### 扩展新的社交平台 ```java // 1. 定义认证源 public enum AuthCustomSource implements AuthSource { CUSTOM { @Override public String authorize() { return "https://custom-platform.com/oauth/authorize"; } @Override public String accessToken() { return "https://custom-platform.com/oauth/token"; } @Override public String userInfo() { return "https://custom-platform.com/api/user"; } @Override public Class getTargetClass() { return AuthCustomRequest.class; } } } // 2. 实现认证请求 public class AuthCustomRequest extends AuthDefaultRequest { public AuthCustomRequest(AuthConfig config, AuthStateCache authStateCache) { super(config, AuthCustomSource.CUSTOM, authStateCache); } @Override public AuthToken getAccessToken(AuthCallback authCallback) { // 实现获取访问令牌逻辑 } @Override public AuthUser getUserInfo(AuthToken authToken) { // 实现获取用户信息逻辑 } } // 3. 在SocialUtils中添加对应的case ``` ## 状态缓存机制 ### Redis状态缓存 模块使用Redis作为OAuth认证过程中的状态缓存存储,防止CSRF攻击: ```java @Component public class AuthRedisStateCache implements AuthStateCache { /** * 默认过期时间:3分钟 */ private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(3); @Override public void cache(String key, String value) { RedisUtils.setCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key, value, DEFAULT_TIMEOUT); } @Override public String get(String key) { return RedisUtils.getCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key); } @Override public boolean containsKey(String key) { return RedisUtils.hasKey(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key); } } ``` ### 缓存键规则 * **前缀**: `GlobalConstants.SOCIAL_AUTH_CODE_KEY` * **格式**: `social_auth_code:{state}` * **过期时间**: 3分钟(可配置) ## 自定义实现详解 ### MaxKey集成 MaxKey是企业级身份认证管理系统,支持标准OAuth2.0协议: ```java public class AuthMaxKeyRequest extends AuthDefaultRequest { // 服务器地址从配置文件读取 public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url"); @Override public AuthToken getAccessToken(AuthCallback authCallback) { // 实现标准OAuth2.0授权码模式 } @Override public AuthUser getUserInfo(AuthToken authToken) { // 获取用户详细信息 } } ``` ### TopIAM集成 TopIAM是身份管理平台,使用Basic认证方式: ```java public class AuthTopIamRequest extends AuthDefaultRequest { @Override protected String doPostAuthorizationCode(String code) { // 使用Basic认证方式发送客户端凭证 return HttpRequest.post(source.accessToken()) .header("Authorization", "Basic " + Base64.encode( String.format("%s:%s", config.getClientId(), config.getClientSecret()))) .form("grant_type", "authorization_code") .form("code", code) .form("redirect_uri", config.getRedirectUri()) .execute() .body(); } } ``` ### Gitea集成 Gitea是轻量级的自建Git服务器,支持OAuth2.0认证: ```java public class AuthGiteaRequest extends AuthDefaultRequest { @Override protected String doPostAuthorizationCode(String code) { // 发送表单数据获取访问令牌 return HttpRequest.post(source.accessToken()) .form("client_id", config.getClientId()) .form("client_secret", config.getClientSecret()) .form("grant_type", "authorization_code") .form("code", code) .form("redirect_uri", config.getRedirectUri()) .execute() .body(); } } ``` ## 错误处理 ### 异常类型 * **AuthException**: JustAuth框架的认证异常 * **配置异常**: 当平台类型不支持或配置缺失时抛出 ### 错误响应格式 ```json { "error": "invalid_grant", "error_description": "授权码无效或已过期" } ``` ### 常见错误及解决方案 | 错误 | 原因 | 解决方案 | |------|------|----------| | 不支持的第三方登录类型 | 配置文件中未配置对应平台 | 检查application.yml配置 | | 授权码无效 | code参数错误或已过期 | 重新获取授权码 | | 回调地址不匹配 | redirect\_uri参数与配置不一致 | 检查回调地址配置 | | 客户端认证失败 | client\_id或client\_secret错误 | 检查应用凭证配置 | ## 安全考虑 ### CSRF防护 * 使用state参数防止CSRF攻击 * state值存储在Redis中,设置合理的过期时间 * 回调时验证state参数的有效性 ### 数据传输安全 * 强制使用HTTPS协议进行OAuth认证 * 敏感信息(如client\_secret)不在前端暴露 * 访问令牌具有时效性,支持刷新机制 ### 权限控制 * 按需申请OAuth授权范围(scopes) * 定期轮换应用凭证 * 监控异常登录行为 ## 性能优化 ### 缓存策略 * 使用Redis缓存认证状态,减少数据库查询 * 合理设置缓存过期时间,平衡安全性和性能 * 考虑使用连接池优化Redis连接 ### 异步处理 ```java @Async("socialAuthExecutor") public CompletableFuture getUserInfoAsync(AuthToken authToken, String source) { return CompletableFuture.supplyAsync(() -> { AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties); return authRequest.getUserInfo(authToken); }); } ``` ## 监控与日志 ### 日志配置 ```yaml logging: level: plus.ruoyi.common.social: DEBUG me.zhyd.oauth: INFO ``` ### 关键指标监控 * 认证成功率 * 认证响应时间 * 各平台使用频率 * 异常发生频率 ## 部署注意事项 ### 环境配置 1. **开发环境**: 可使用localhost回调地址(支付宝除外) 2. **生产环境**: 必须使用正式域名和HTTPS协议 3. **内网部署**: 确保能访问第三方平台的OAuth接口 ### 回调地址配置 各平台的回调地址必须在对应的开发者控制台中预先配置: * GitHub: Settings > Developer settings > OAuth Apps * 微信: 微信开放平台 > 管理中心 > 网站应用 * 企业微信: 企业微信后台 > 应用管理 > 自建应用 ## 故障排查 ### 调试步骤 1. **检查配置**: 验证application.yml中的配置是否正确 2. **网络连通性**: 确认能正常访问第三方平台接口 3. **日志分析**: 查看DEBUG级别的日志,定位具体错误 4. **状态验证**: 检查Redis中的state缓存是否正常 ### 调试工具 ```java @RestController @RequestMapping("/debug") public class SocialDebugController { @GetMapping("/config/{source}") public Object getConfig(@PathVariable String source) { return socialProperties.getType().get(source); } @GetMapping("/state/{state}") public Object getState(@PathVariable String state) { return RedisUtils.getCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + state); } } ``` --- --- url: 'https://ruoyi.plus/practices/mobile-security.md' --- # 移动端安全 --- --- url: 'https://ruoyi.plus/mobile/getting-started.md' --- # 移动端快速启动 本章节将指导你快速启动 Ruoyi-Plus-Uniapp 移动端项目,支持 H5、微信小程序、支付宝小程序、App 等多端开发。 ## 🎯 环境要求 ### 必需环境 * **Node.js**: >= 18.0.0 * **包管理器**: pnpm >= 7.30.0 (推荐) * **UniApp CLI**: 最新版本 ### 平台开发工具 * **H5开发**: 现代浏览器即可 * **微信小程序**: [微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html) * **支付宝小程序**: [支付宝开发者工具](https://opendocs.alipay.com/mini/ide/download) * **App开发**: [HBuilderX](https://www.dcloud.io/hbuilderx.html) ### 推荐工具 * **IDE**: IDEA / VS Code * **版本管理**: Git * **Node管理**: nvm (用于多版本管理) ## 🛠️ 环境准备 ### 1. Node.js 环境安装 参考前端章节的环境安装说明,确保 Node.js >= 18.0.0 ### 2. 开发工具安装 #### 微信开发者工具 1. 下载并安装微信开发者工具 2. 登录微信开发者账号 3. 配置服务端口(用于调试) #### HBuilderX (App开发) 1. 下载安装 HBuilderX 2. 安装 uni-app 插件 3. 配置 Android/iOS 开发环境 ## 🚀 项目启动 ### 1. 获取项目代码 ```bash # 进入移动端项目目录 cd ruoyi-plus-uniapp/plus-uniapp ``` ### 2. 安装依赖 ```bash # 安装项目依赖 pnpm install # 如果安装失败,清除缓存后重新安装 pnpm store prune pnpm install ``` ### 3. 环境配置 编辑 `env/.env` 配置文件: ```text # 开发环境配置 VITE_APP_ENV=development # 后端API地址 VITE_BASE_URL=http://localhost:5500 # 应用标识符 VITE_APP_ID=ryplus_uni # 微信小程序 AppID (需要申请) VITE_WECHAT_MINI_APP_ID=your_wechat_appid # 支付宝小程序 AppID (需要申请) VITE_WECHAT_OFFICIAL_APP_ID=your_alipay_appid ``` ### 4. 多端启动 #### H5 端开发 ```bash # 启动 H5 开发服务器 pnpm dev:h5 # 启动成功后访问:http://localhost:100 ``` H5 端特点: * 支持热更新,开发效率高 * 可在浏览器中直接调试 * 支持浏览器开发者工具 #### 微信小程序开发 ```bash # 编译微信小程序 pnpm dev:mp-weixin ``` 编译完成后: 1. 打开微信开发者工具 2. 选择「导入项目」 3. 项目目录选择:`dist/dev/mp-weixin` 4. AppID需提前在env/.env中配置VITE\_WECHAT\_MINI\_APP\_ID ::: tip 💡 微信小程序调试 * 在微信开发者工具中可以实时预览和调试 * 支持真机预览和调试 * 可以使用开发者工具的调试面板 ::: #### 支付宝小程序开发 ```bash # 编译支付宝小程序 pnpm dev:mp-alipay ``` 编译完成后: 1. 打开支付宝开发者工具 2. 选择「打开项目」 3. 项目目录选择:`dist/dev/mp-alipay` #### APP 端开发 ```bash # 编译 APP pnpm dev:app ``` 编译完成后: 1. 打开 HBuilderX 2. 文件 → 导入 → 从本地目录导入 3. 选择目录:`dist/dev/app-plus` 4. 连接手机或使用模拟器运行 ## 📱 多端开发脚本 ### 开发环境启动 | 平台 | 命令 | 说明 | |------|------|------| | H5 | `pnpm dev:h5` | 浏览器运行,支持热更新 | | 微信小程序 | `pnpm dev:mp-weixin` | 编译到微信小程序格式 | | 支付宝小程序 | `pnpm dev:mp-alipay` | 编译到支付宝小程序格式 | | 百度小程序 | `pnpm dev:mp-baidu` | 编译到百度小程序格式 | | QQ小程序 | `pnpm dev:mp-qq` | 编译到QQ小程序格式 | | 抖音小程序 | `pnpm dev:mp-toutiao` | 编译到抖音小程序格式 | | 京东小程序 | `pnpm dev:mp-jd` | 编译到京东小程序格式 | | APP | `pnpm dev:app` | 编译到APP格式 | ### 生产环境构建 | 平台 | 命令 | 输出目录 | |------|------|----------| | H5 | `pnpm build:h5` | `dist/build/h5` | | 微信小程序 | `pnpm build:mp-weixin` | `dist/build/mp-weixin` | | 支付宝小程序 | `pnpm build:mp-alipay` | `dist/build/mp-alipay` | | APP | `pnpm build:app` | `dist/build/app-plus` | ## 🔧 项目配置文件 ### 核心配置文件 | 文件 | 说明 | 作用 | |------|------|------| | `manifest.json` | UniApp 应用配置清单 | 配置应用信息、权限、SDK等 | | `pages.config.ts` | 页面路由配置 | 配置页面路径、窗口样式、tabBar等 | | `uni.scss` | 全局样式变量 | 定义全局SCSS变量 | | `vite.config.ts` | Vite 构建配置 | 构建工具配置 | | `uno.config.ts` | UnoCSS 配置 | 原子化CSS配置 | ### 环境配置 ```text env/ ├── .env # 公共配置 ├── .env.development # 开发环境配置 └── .env.production # 生产环境配置 ``` ## 🔍 开发调试 ### H5 端调试 ```bash # 启动并在浏览器中调试 pnpm dev:h5 # 使用浏览器开发者工具进行调试 # 支持断点调试、网络分析、性能分析等 ``` ### 小程序调试 1. 在对应的开发者工具中导入项目 2. 使用开发者工具的调试面板 3. 支持真机预览和调试 4. 可以查看网络请求、存储数据等 ### APP 调试 1. 在 HBuilderX 中导入项目 2. 连接真机或使用模拟器 3. 使用 HBuilderX 的调试功能 4. 支持断点调试和日志查看 ## 🌟 开发建议 ### 代码组织 1. **页面开发**: 在 `src/pages` 或 `src/subpackages` 中创建页面 2. **组件开发**: 使用 WotUI 组件库,保持界面一致性 3. **API调用**: 使用 `useHttp` 组合函数统一处理请求 ### 跨平台注意事项 1. **条件编译**: 使用 `#ifdef` 处理平台差异 2. **API兼容**: 注意不同平台API的差异 3. **样式适配**: 使用 rpx 单位确保不同设备适配 ### 性能优化 1. **分包加载**: 合理使用分包减少主包大小 2. **图片优化**: 使用合适格式和大小的图片 3. **组件按需引入**: 避免全量引入组件库 ## 🎉 开发环境就绪 恭喜!如果以上步骤都顺利完成,你的移动端开发环境已经成功搭建。 开始你的跨平台移动端开发之旅吧!🚀 --- --- url: 'https://ruoyi.plus/practices/mobile-performance.md' --- # 移动端性能优化 --- --- url: 'https://ruoyi.plus/mobile/configuration.md' --- # 移动端配置文件说明文档 ## 技术栈概述 本项目基于 uni-app + Vue 3 + TypeScript 构建的跨平台移动端应用,支持多个平台同时开发: * **开发框架**: uni-app (基于Vue 3) * **开发语言**: TypeScript 5.x * **构建工具**: Vite 5.x + uni-app官方插件 * **UI组件库**: wot-design-uni * **CSS框架**: UnoCSS (支持uni-app) * **代码规范**: @uni-helper/eslint-config * **支持平台**: 微信小程序、支付宝小程序、H5、App、百度/字节/QQ小程序等 *** ## 支持平台 ### 小程序平台 * **微信小程序** - 主要平台 * **支付宝小程序** - 支付场景 * **百度小程序** - 搜索入口 * **字节跳动小程序** - 抖音生态 * **QQ小程序** - QQ生态 * **快手小程序** - 短视频场景 * **其他小程序等** - 支持更多小程序平台 ### 移动应用 * **H5** - 移动端网页 * **Android App** - 原生安卓应用 * **iOS App** - 原生苹果应用 *** ## 配置文件结构 ### 1. 环境变量配置 #### 1.1 基础环境配置 (`.env`) ```bash # ===== 应用基础配置 ===== VITE_APP_ID = 'ryplus_uni' # 应用唯一标识符 VITE_APP_TITLE = 'ryplus-uni' # 应用名称 VITE_APP_PORT = '100' # 开发服务器端口 VITE_UNI_APPID = 'UNI__6E229FA' # uni-app应用ID # ===== 各平台小程序APPID配置 ===== VITE_WECHAT_MINI_APP_ID = 'wxd44a6eaefd42428c' # 微信小程序APPID VITE_WECHAT_OFFICIAL_APP_ID = 'wx08728cf8ec97725f' # 微信公众号APPID VITE_ALIPAY_MINI_APP_ID = 'your_alipay_app_id' # 支付宝小程序APPID VITE_BYTEDANCE_MINI_APP_ID = 'your_bytedance_app_id' # 字节跳动小程序APPID VITE_BAIDU_MINI_APP_ID = 'your_baidu_app_id' # 百度小程序APPID VITE_QQ_MINI_APP_ID = 'your_qq_app_id' # QQ小程序APPID # ===== H5部署配置 ===== VITE_APP_PUBLIC_BASE = '/' # H5部署基础路径 # ===== 安全配置 ===== VITE_APP_API_ENCRYPT = 'true' # 接口加密开关 VITE_APP_RSA_PUBLIC_KEY = 'MFww...' # RSA加密公钥 (与后端配对,与下方的私钥不是同一对) VITE_APP_RSA_PRIVATE_KEY = 'MIIBOw...' # RSA解密私钥 (与后端配对) # ===== 功能开关 ===== VITE_APP_WEBSOCKET = 'false' # WebSocket开关 VITE_APP_SSE = 'false' # SSE开关(小程序不支持) ``` #### 1.2 开发环境配置 (`.env.development`) ```bash VITE_APP_ENV = 'development' # 环境标识 VITE_APP_BASE_API = 'https://api.ruoyikj.top/frp/5500' # 开发环境API地址 VITE_DELETE_CONSOLE = false # 保留console调试 VITE_SHOW_SOURCEMAP = true # 开启sourcemap调试 ``` #### 1.3 生产环境配置 (`.env.production`) ```bash VITE_APP_ENV = 'production' # 生产环境标识 VITE_APP_BASE_API = 'https://uni.ruoyi.plus/ryplus_uni' # 生产环境API地址 VITE_DELETE_CONSOLE = true # 删除console VITE_SHOW_SOURCEMAP = false # 关闭sourcemap ``` *** ### 2. 应用清单配置 (`manifest.config.ts`) #### 2.1 基础应用信息 ```typescript export default defineManifestConfig({ name: VITE_APP_TITLE, // 应用名称 appid: VITE_UNI_APPID, // uni-app应用ID description: 'ryplus-uni', // 应用描述 versionName: '5.4.0', // 版本名称 versionCode: '100', // 版本号 transformPx: false, // 是否转换px单位 locale: 'zh-Hans', // 默认语言 }) ``` #### 2.2 H5平台配置 ```typescript h5: { router: { base: VITE_APP_PUBLIC_BASE, // 路由基础路径 }, title: VITE_APP_TITLE, // 页面标题 template: 'index.html', // HTML模板 devServer: { https: false, // 开发服务器HTTPS }, } ``` #### 2.3 App平台配置 ```typescript 'app-plus': { usingComponents: true, // 启用自定义组件 nvueStyleCompiler: 'uni-app', // nvue样式编译器 compilerVersion: 3, // 编译器版本 // 启动界面配置 splashscreen: { alwaysShowBeforeRender: true, // 总是显示启动界面 waiting: true, // 等待首页渲染完成 autoclose: true, // 自动关闭 delay: 0, // 延迟时间 }, // 打包配置 distribute: { android: { minSdkVersion: 30, // 最低SDK版本 targetSdkVersion: 30, // 目标SDK版本 abiFilters: ['armeabi-v7a', 'arm64-v8a'], // 支持的CPU架构 permissions: [ // 权限配置 '', '', // ... 更多权限 ], }, ios: {}, // iOS配置 icons: { // 应用图标配置 android: { hdpi: 'static/app/icons/72x72.png', xhdpi: 'static/app/icons/96x96.png', // ... 各种尺寸图标 }, ios: { // iOS各种尺寸图标配置 }, }, }, } ``` #### 2.4 小程序平台配置 ```typescript // 微信小程序 'mp-weixin': { appid: VITE_WECHAT_MINI_APP_ID, // 微信小程序APPID setting: { urlCheck: false, // 不检查安全域名 es6: true, // 启用ES6转ES5 enhance: true, // 启用增强编译 postcss: true, // 启用postcss minified: true, // 压缩代码 bigPackageSizeSupport: true, // 支持分包异步化 }, usingComponents: true, // 启用自定义组件 permission: { // 权限配置 'scope.userLocation': { desc: '您的位置信息将用于小程序位置接口的效果展示' } }, requiredPrivateInfos: [ // 隐私信息配置 'getLocation', 'chooseLocation' ], }, // 支付宝小程序 'mp-alipay': { appid: VITE_ALIPAY_MINI_APP_ID, usingComponents: true, styleIsolation: 'shared', // 样式隔离模式 }, // 其他小程序平台配置类似... ``` *** ### 3. 页面路由配置 (`pages.config.ts`) ```typescript export default defineUniPages({ // 全局页面样式配置 globalStyle: { navigationBarTitleText: 'ryplus-uni', // 默认导航栏标题 navigationStyle: 'custom', // 自定义导航栏 // 支付宝小程序特殊配置 'mp-alipay': { 'transparentTitle': 'always', // 透明标题栏 'titlePenetrate': 'YES' // 标题栏点击穿透 }, // H5端配置 'h5': { 'titleNView': false // 不显示原生标题栏 }, // APP端防抖动配置 'app-plus': { 'bounce': 'none', // 禁用页面弹性效果 }, }, // 组件自动导入配置 easycom: { autoscan: true, // 自动扫描components目录 custom: { // wot-design-uni组件库自动导入 '^wd-(.*)': '@/wd/components/wd-$1/wd-$1.vue' } } }) ``` *** ### 4. TypeScript 配置 (`tsconfig.json`) ```json { "compilerOptions": { "target": "ES2015", // 编译目标(小程序兼容) "composite": true, // 复合项目模式 "lib": ["esnext", "dom"], // 包含的库 "baseUrl": ".", // 基础目录 "module": "ESNext", // 模块系统 "moduleResolution": "Node", // 模块解析策略 // 路径别名配置 "paths": { "@/*": ["./src/*"], // @ 指向 src "@img/*": ["./src/static/*"] // @img 指向静态资源 }, // uni-app相关类型定义 "types": [ "@dcloudio/types", // uni-app核心类型 "@uni-helper/uni-types", // uni-helper类型增强 "@types/wechat-miniprogram", // 微信小程序类型 "wot-design-uni/global.d.ts", // wot-design-uni组件类型 "z-paging/types", // z-paging分页组件类型 "./src/typings.d.ts" // 自定义类型定义 ], "allowJs": true, // 允许JS文件 "allowSyntheticDefaultImports": true, // 允许合成默认导入 "skipLibCheck": true // 跳过库文件检查 }, // Vue编译器选项 "vueCompilerOptions": { "plugins": [ "@uni-helper/uni-types/volar-plugin" // uni-app类型增强插件 ] } } ``` *** ### 5. 代码规范配置 (`eslint.config.mjs`) ```javascript import uniHelper from '@uni-helper/eslint-config' export default uniHelper({ // ===== 功能开关 ===== unocss: true, // 启用UnoCSS支持 vue: true, // 启用Vue 3支持 markdown: false, // 禁用Markdown检查 // ===== 忽略文件 ===== ignores: [ 'src/uni_modules/', // uni-app插件目录 'dist', // 构建输出 'auto-imports.d.ts', // 自动导入类型文件 'uni-pages.d.ts', // 页面类型文件 'src/pages.json', // 自动生成的页面配置 'src/manifest.json', // 自动生成的应用配置 ], // ===== 自定义规则 ===== rules: { 'no-console': 'off', // 允许console(调试需要) 'no-unused-vars': 'off', // 关闭未使用变量检查 'vue/no-unused-refs': 'off', // 允许未使用的ref 'vue/block-order': 'off', // 不强制block顺序 'func-style': 'off', // 关闭函数风格检查 'antfu/top-level-function': 'off', // 关闭顶层函数检查 'style/brace-style': 'off', // 关闭大括号风格检查 'style/quote-props': 'off', // 关闭引号风格检查 // ... 更多规则配置 }, // ===== 格式化配置 ===== formatters: { css: true, // 启用CSS格式化 html: true, // 启用HTML格式化 }, }) ``` *** ### 6. 样式配置 (`uno.config.ts`) ```typescript import { presetUni } from '@uni-helper/unocss-preset-uni' export default defineConfig({ // uni-app专用预设 presets: [ presetUni({ attributify: { prefixedOnly: true, // 只支持前缀属性化 }, }), presetIcons({ // 图标支持 scale: 1.2, warn: true, extraProperties: { display: 'inline-block', 'vertical-align': 'middle', }, }), presetAttributify(), // 属性化支持 ], // 转换器 transformers: [ transformerDirectives(), // @apply等指令支持 transformerVariantGroup(), // 变体组支持 ], // 快捷方式 shortcuts: [ { center: 'flex justify-center items-center', }, ], // 自定义规则(适配移动端) rules: [ ['p-safe', { // 安全区域内边距 padding: 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)', }], ['pt-safe', { 'padding-top': 'env(safe-area-inset-top)' }], ['pb-safe', { 'padding-bottom': 'env(safe-area-inset-bottom)' }], ], // 主题配置 theme: { colors: { primary: 'var(--wot-color-theme,#0957DE)', // 主题色 }, fontSize: { '2xs': ['20rpx', '28rpx'], // 超小号字体(rpx单位) '3xs': ['18rpx', '26rpx'], }, }, }) ``` *** ### 7. 构建配置 (`vite.config.ts`) ```typescript export default async ({ command, mode }: ConfigEnv): Promise => { const { UNI_PLATFORM } = process.env // 获取当前构建平台 const env = loadEnv(mode, path.resolve(process.cwd(), 'env')) return defineConfig({ envDir: './env', // 环境变量目录 plugins: await createVitePlugins({ // uni-app插件配置 command, mode, env, }), define: { __UNI_PLATFORM__: JSON.stringify(UNI_PLATFORM), // 平台标识 }, // 路径别名 resolve: { alias: { '@': path.join(process.cwd(), './src'), '@img': path.join(process.cwd(), './src/static/images'), }, }, // 开发服务器配置 server: { host: '0.0.0.0', hmr: true, // 热更新 port: Number.parseInt(VITE_APP_PORT, 10), }, // 依赖优化 optimizeDeps: { include: [ 'jsencrypt/bin/jsencrypt.min.js', // 加密库 'crypto-js', // 加密工具 ], }, // 构建配置 build: { target: 'es6', // 构建目标 minify: mode === 'development' ? false : 'terser', terserOptions: { compress: { drop_console: VITE_DELETE_CONSOLE === 'true', drop_debugger: true, }, }, }, }) } ``` *** ## 开发命令 ### 1. 开发环境 ```bash # H5开发 pnpm dev:h5 # 微信小程序开发 pnpm dev:mp-weixin # 支付宝小程序开发 pnpm dev:mp-alipay # App开发 pnpm dev:app # 其他小程序平台 pnpm dev:mp-baidu pnpm dev:mp-toutiao pnpm dev:mp-qq ``` ### 2. 生产构建 ```bash # H5构建 pnpm build:h5 # 小程序构建 pnpm build:mp-weixin pnpm build:mp-alipay # App构建 pnpm build:app # 所有平台构建 pnpm build:all-mp ``` *** ## 平台差异处理 ### 1. 条件编译 ```vue ``` ### 2. 平台API适配 ```typescript // 获取系统信息 const getSystemInfo = () => { // #ifdef MP-WEIXIN return uni.getSystemInfoSync() // #endif // #ifdef H5 return { platform: 'h5', // ... H5特定信息 } // #endif } // 支付接口适配 const pay = (options: PayOptions) => { // #ifdef MP-WEIXIN return uni.requestPayment({ provider: 'wxpay', ...options }) // #endif // #ifdef MP-ALIPAY return uni.requestPayment({ provider: 'alipay', ...options }) // #endif } ``` *** ## 性能优化 ### 1. 组件懒加载 ```vue ``` ## 调试技巧 ### 1. 小程序调试 * **微信开发者工具**: 导入`dist/dev/mp-weixin`目录 * **支付宝开发者工具**: 导入`dist/dev/mp-alipay`目录 * **真机调试**: 使用各平台开发者工具的真机预览功能 ### 2. H5调试 ```bash # 启动H5开发服务器 pnpm dev:h5 # 浏览器访问 http://localhost:100 ``` ### 3. App调试 ```bash # 启动App开发 pnpm dev:app # 使用HBuilderX真机运行 # 或打包成自定义基座进行调试 ``` *** ## 发布部署 ### 1. 小程序发布 1. **构建生产版本** ```bash pnpm build:mp-weixin ``` 2. **上传审核** * 使用微信开发者工具上传代码 * 填写版本信息和更新说明 * 提交审核 3. **发布上线** * 审核通过后在小程序后台发布 ### 2. H5部署 ```bash # 构建H5版本 pnpm build:h5 # 部署到服务器 # 将dist/build/h5目录内容上传到服务器 ``` ### 3. App打包 1. **使用HBuilderX云打包** 2. **配置证书和签名** 3. **提交应用商店审核** *** > 💡 **提示**: > > * 开发前请确保各平台开发者工具已安装 > * 不同平台特性差异较大,需要针对性测试 > * 建议使用真机测试验证功能完整性 --- --- url: 'https://ruoyi.plus/mobile.md' --- # 移动端项目简介 Ruoyi-Plus-Uniapp 移动端是一个基于 UniApp + Vue 3 + TypeScript 的跨平台移动应用开发框架,专注于小程序、H5和移动端App应用开发。项目基于 unibest、Wot-UI 框架深度改造,协调后端和前端提供完整的移动端解决方案。 ## 技术栈 * **核心框架**: UniApp + Vue 3 + TypeScript * **UI组件库**: WotUI (自维护重构版本) * **状态管理**: Pinia * **图标系统**: Iconify + 自定义图标库 * **构建工具**: Vite ## 架构特色 ### 1. 跨平台支持 * **微信小程序**: 完整功能实现,开箱即用 * **微信公众号**: 完整功能实现,开箱即用 * **多平台扩展**: 预留 QQ、支付宝、京东、抖音等小程序支持 * **统一 API**: 封装小程序 API 调用接口,实现一套代码多端运行 ### 2. 框架重构优化 ```text src/ ├── api/ # 接口集中管理 ├── components/ # 自定义组件 ├── composables/ # 组合式函数(复用前端组合函数) ├── layouts/ # 通用布局组件 ├── pages/ # 页面文件 ├── static/ # 静态目录 ├── stores/ # Pinia 状态管理模块 ├── subpackages/ # 分包管理 ├── types/ # 类型定义 └── utils/ # 工具类 └── wd/ # 通用组件库 ``` ### 3. 分包管理策略 * **主包**: 核心功能和基础页面 * **管理员分包**: 管理功能模块 * **示例代码分包**: 组件演示和开发示例 * 优化小程序加载速度和包体积 ## 核心功能 ### 🔐 认证与登录 * **微信小程序登录**: 完整的微信授权登录流程 * **公众号登录**: 支持微信公众号登录 * **多种登录方式**: 手机号登录、验证码登录、密码登录 * **账号绑定**: unionid/手机号关联,确保用户唯一性 * **自动注册**: 无账户用户自动注册机制 ### 🎨 组件系统 * **WotUI 重构**: 使用 Vue 3 + TypeScript 重构所有组件 * **统一单位**: 全部使用 rpx 单位,适配移动端 * **核心组件**: * `wd-paging`: 下滑分页加载组件 ### 📱 图标系统 * **丰富图标库**: 重构 400+ 个图标 * **双重风格**: 线条图标 + 实心图标 * **便捷使用**: 示例代码中可直接搜索和复制 * **类型支持**: 完整的 TypeScript 类型定义 ### 🔧 开发工具 * **Composables 组合函数**: 复用前端的 `useDict`、`useAuth` 等组合函数 * **移动端专用组合函数**: * `usePayment`: 支付相关逻辑封装 * `useScroll`: 滚动相关逻辑 * `useTheme`: 主题管理 * **工具类函数**: 复用前端 utils(boolean、date、format、string 等) ### 🌐 网络与数据 * **useHttp 封装**: 移动端 HTTP 请求库 * **加密解密**: API 请求加密解密支持 * **统一拦截**: 请求和响应统一处理机制 * **租户隔离**: 支持多租户数据隔离 ## 特色功能 ### 1. 自定义 Tabbar * 不使用原生 tabbar,采用自定义组件实现 * 更灵活的样式控制和功能扩展 * 支持动态配置和权限控制 ### 2. 示例代码系统 * **完整示例**: 所有 WotUI 组件的使用示例 * **代码查看**: 统一的代码查看和复制功能 * **实时效果**: 直接在应用中查看组件效果 * **最佳实践**: 提供开发最佳实践指导 ### 3. 应用配置管理 * **应用 ID**: 模仿前端实现应用标识管理 * **环境配置**: 支持多环境配置切换 * **租户管理**: 完整的租户功能支持 ## 开发优势 1. **开发体验优秀**: 完整的 TypeScript 支持和智能提示 2. **组件丰富**: 400+ 图标 + 完整的 UI 组件库 3. **跨平台统一**: 一套代码适配多个小程序平台 4. **性能优化**: 分包加载 + 组件按需引入 5. **示例完整**: 丰富的示例代码和最佳实践 ## 适用场景 * 微信小程序应用 * 多平台小程序开发 * 移动端 H5 应用 * 企业级移动应用 * 需要快速开发的小程序项目 ## 平台支持 * ✅ **微信小程序**: 完整实现 * 🔄 **QQ 小程序**: 预留扩展 * 🔄 **支付宝小程序**: 预留扩展 * 🔄 **京东小程序**: 预留扩展 * 🔄 **抖音小程序**: 预留扩展 Ruoyi Plus 移动端通过现代化的技术栈和完善的组件库,为开发者提供了一个高效、可扩展的跨平台移动应用开发解决方案。 --- --- url: 'https://ruoyi.plus/frontend/types/type-extensions.md' --- # 类型扩展 --- --- url: 'https://ruoyi.plus/frontend/architecture/type-system.md' --- # 类型系统 --- --- url: 'https://ruoyi.plus/frontend/types/overview.md' --- # 类型系统概览 --- --- url: 'https://ruoyi.plus/frontend/utils/class.md' --- # 类操作工具 --- --- url: 'https://ruoyi.plus/frontend/views/tool.md' --- # 系统工具 --- --- url: 'https://ruoyi.plus/mobile/api/system.md' --- # 系统接口 --- --- url: 'https://ruoyi.plus/practices/system-architecture.md' --- # 系统架构设计 --- --- url: 'https://ruoyi.plus/backend/modules/system.md' --- # 系统模块 (system) --- --- url: 'https://ruoyi.plus/frontend/views/monitor.md' --- # 系统监控 --- --- url: 'https://ruoyi.plus/frontend/views/system.md' --- # 系统管理 --- --- url: 'https://ruoyi.plus/frontend/i18n/component-i18n.md' --- # 组件国际化 --- --- url: 'https://ruoyi.plus/frontend/styles/component-styles.md' --- # 组件样式 --- --- url: 'https://ruoyi.plus/mobile/styles/components.md' --- # 组件样式 --- --- url: 'https://ruoyi.plus/frontend/components/overview.md' --- # 组件概览 --- --- url: 'https://ruoyi.plus/mobile/components/overview.md' --- # 组件概览 --- --- url: 'https://ruoyi.plus/frontend/types/component-types.md' --- # 组件类型 --- --- url: 'https://ruoyi.plus/mobile/plugins/analytics.md' --- # 统计插件 --- --- url: 'https://ruoyi.plus/frontend/utils/cache.md' --- # 缓存工具 --- --- url: 'https://ruoyi.plus/practices/cache-strategy.md' --- # 缓存策略 --- --- url: 'https://ruoyi.plus/practices/network-optimization.md' --- # 网络优化 --- --- url: 'https://ruoyi.plus/mobile/plugins/request.md' --- # 网络请求插件 --- --- url: 'https://ruoyi.plus/mobile/layouts/capsule.md' --- # 胶囊组件 --- --- url: 'https://ruoyi.plus/backend/common/sensitive.md' --- # 脱敏处理 (sensitive) ## 模块概述 `ruoyi-common-sensitive` 是RuoYi-Plus-Uniapp框架的数据脱敏模块,提供数据脱敏与隐私保护功能。该模块基于Jackson序列化机制,通过注解驱动的方式自动处理敏感数据,支持多种脱敏策略和权限控制。 ### 主要功能 * **注解驱动脱敏**:通过`@Sensitive`注解标记敏感字段 * **多种脱敏策略**:内置15+种常见敏感数据脱敏算法 * **权限控制**:基于角色和权限的灵活访问控制 * **自动化处理**:集成Jackson序列化,JSON输出时自动脱敏 * **容错机制**:异常情况下保证数据正常输出 * **扩展性强**:支持自定义脱敏策略和权限判断逻辑 ## 模块依赖 ```xml plus.ruoyi ruoyi-common-json ``` ## 核心组件 ### 1. @Sensitive 注解 #### 注解属性 | 属性 | 类型 | 描述 | 默认值 | |------|------|------|--------| | `strategy` | `SensitiveStrategy` | 脱敏策略 | 必填 | | `roleKey` | `String[]` | 允许查看原数据的角色标识 | `{}` | | `perms` | `String[]` | 允许查看原数据的权限标识 | `{}` | #### 权限判断逻辑 RuoYi-Plus-Uniapp框架采用了精确的权限判断逻辑: * **未登录用户**:一律进行脱敏处理 * **角色和权限并存**:同时配置时需要同时满足才不脱敏(AND关系) * **仅配置角色**:拥有任一角色即不脱敏(OR关系) * **仅配置权限**:拥有任一权限即不脱敏(OR关系) * **管理员特权**: * 租户环境:超级管理员和租户管理员都不脱敏 * 非租户环境:仅超级管理员不脱敏 #### 使用示例 ```java public class UserInfo { // 手机号脱敏,admin角色可查看原数据 @Sensitive(strategy = SensitiveStrategy.PHONE, roleKey = {"admin"}) private String phone; // 身份证脱敏,需要用户查询权限 @Sensitive(strategy = SensitiveStrategy.ID_CARD, perms = {"user:query"}) private String idCard; // 邮箱脱敏,满足任一角色或权限即可查看 @Sensitive(strategy = SensitiveStrategy.EMAIL, roleKey = {"admin", "manager"}, perms = {"user:detail", "user:export"}) private String email; // 地址脱敏,仅超级管理员可查看 @Sensitive(strategy = SensitiveStrategy.ADDRESS, roleKey = {"superadmin"}) private String address; } ``` ### 2. SensitiveStrategy 脱敏策略枚举 #### 内置脱敏策略 | 策略名称 | 适用场景 | 脱敏效果示例 | 说明 | |---------|----------|-------------|------| | `ID_CARD` | 身份证号 | `110***********1234` | 保留前3位和后4位 | | `PHONE` | 手机号码 | `138****8888` | 保留前3位和后4位 | | `ADDRESS` | 地址信息 | `北京市朝阳区****` | 保留前8个字符 | | `EMAIL` | 邮箱地址 | `t**@example.com` | 保留用户名首尾字符和完整域名 | | `BANK_CARD` | 银行卡号 | `6222***********1234` | 保留前4位和后4位 | | `CHINESE_NAME` | 中文姓名 | `张*` | 保留姓氏,名字用*代替 | | `FIXED_PHONE` | 固定电话 | `010-****8888` | 保留区号和后4位 | | `USER_ID` | 用户ID | `随机数字` | 生成随机数字替代 | | `PASSWORD` | 密码 | `******` | 全部用*代替 | | `IPV4` | IPv4地址 | `192.168.***.***` | 保留网络段,隐藏主机段 | | `IPV6` | IPv6地址 | `2001:db8::****` | 保留前缀,隐藏接口标识 | | `CAR_LICENSE` | 车牌号 | `京A****8` | 支持普通和新能源车辆 | | `FIRST_MASK` | 首字符保留 | `张***` | 只显示第一个字符 | | `CLEAR` | 清空数据 | `""` | 返回空字符串 | | `CLEAR_TO_NULL` | 置空数据 | `null` | 返回null值 | #### 脱敏效果对比 ```java String phone = "13812345678"; String idCard = "11010119900101123X"; String email = "test@example.com"; String address = "北京市朝阳区建国路88号"; // 脱敏后效果: // PHONE: 138****5678 // ID_CARD: 110***********123X // EMAIL: t**t@example.com // ADDRESS: 北京市朝阳区**** ``` ### 3. SensitiveService 权限判断接口 #### 接口定义 ```java public interface SensitiveService { /** * 判断是否需要进行数据脱敏 * * @param roleKey 允许查看原始数据的角色标识数组 * @param perms 允许查看原始数据的权限标识数组 * @return true表示需要脱敏,false表示不需要脱敏 */ boolean isSensitive(String[] roleKey, String[] perms); } ``` #### 框架默认实现 RuoYi-Plus-Uniapp提供了完整的权限判断实现: ```java @Service public class SysSensitiveServiceImpl implements SensitiveService { @Override public boolean isSensitive(String[] roleKey, String[] perms) { // 未登录用户一律脱敏 if (!LoginHelper.isLogin()) { return true; } boolean roleExist = ArrayUtil.isNotEmpty(roleKey); boolean permsExist = ArrayUtil.isNotEmpty(perms); // 权限检查逻辑 if (roleExist && permsExist) { // 同时配置角色和权限:需要同时满足才不脱敏 if (StpUtil.hasRoleOr(roleKey) && StpUtil.hasPermissionOr(perms)) { return false; } } else if (roleExist && StpUtil.hasRoleOr(roleKey)) { // 仅配置角色:拥有任一角色即不脱敏 return false; } else if (permsExist && StpUtil.hasPermissionOr(perms)) { // 仅配置权限:拥有任一权限即不脱敏 return false; } // 管理员特权检查 if (TenantHelper.isEnable()) { // 租户环境:超级管理员和租户管理员都不脱敏 return !LoginHelper.isSuperAdmin() && !LoginHelper.isTenantAdmin(); } // 非租户环境:仅超级管理员不脱敏 return !LoginHelper.isSuperAdmin(); } } ``` #### 权限判断流程 ```mermaid graph TD A[开始权限判断] --> B{用户是否登录?} B -->|否| C[返回true-需要脱敏] B -->|是| D{是否同时配置角色和权限?} D -->|是| E{同时满足角色和权限?} E -->|是| F[返回false-不脱敏] E -->|否| G[继续管理员检查] D -->|否| H{仅配置角色?} H -->|是| I{满足任一角色?} I -->|是| F I -->|否| G H -->|否| J{仅配置权限?} J -->|是| K{满足任一权限?} K -->|是| F K -->|否| G J -->|否| G G --> L{是否启用租户?} L -->|是| M{是超级管理员或租户管理员?} L -->|否| N{是超级管理员?} M -->|是| F M -->|否| C N -->|是| F N -->|否| C ``` ### 4. SensitiveHandler JSON序列化处理器 #### 工作流程 ```mermaid graph TD A[JSON序列化开始] --> B{字段是否有@Sensitive注解?} B -->|否| C[使用默认序列化] B -->|是| D{字段类型是否为String?} D -->|否| C D -->|是| E[解析注解配置] E --> F[获取SensitiveService] F --> G{Service是否存在?} G -->|否| H[输出原始数据] G -->|是| I[调用权限判断] I --> J{是否需要脱敏?} J -->|否| H J -->|是| K[应用脱敏策略] K --> L[输出脱敏数据] H --> M[序列化完成] L --> M C --> M ``` #### 容错机制 1. **Service缺失处理**:当`SensitiveService`实现不存在时,默认输出原始数据 2. **异常捕获**:权限判断异常时记录日志并返回原始数据 3. **类型检查**:仅对String类型字段进行脱敏处理 4. **注解校验**:确保注解正确配置后才进行处理 ## 使用指南 ### 1. 添加模块依赖 ```xml plus.ruoyi ruoyi-common-sensitive ``` ### 2. 使用框架默认实现 RuoYi-Plus-Uniapp已经提供了完整的`SysSensitiveServiceImpl`实现,通常无需自定义。如需扩展,可以继承或替换: ```java // 扩展默认实现 @Service @Primary public class CustomSensitiveService extends SysSensitiveServiceImpl { @Override public boolean isSensitive(String[] roleKey, String[] perms) { // 可以添加自定义逻辑 if (isSpecialCondition()) { return false; // 特殊情况不脱敏 } // 调用父类默认实现 return super.isSensitive(roleKey, perms); } private boolean isSpecialCondition() { // 自定义判断逻辑,如特定时间段、特殊用户等 return false; } } ``` ### 3. 实体类添加脱敏注解 ```java @Data public class Customer { private Long id; @Sensitive(strategy = SensitiveStrategy.CHINESE_NAME, roleKey = {"admin"}) private String realName; @Sensitive(strategy = SensitiveStrategy.PHONE, perms = {"customer:detail"}) private String mobile; @Sensitive(strategy = SensitiveStrategy.ID_CARD, roleKey = {"admin", "manager"}, perms = {"customer:sensitive"}) private String idNumber; @Sensitive(strategy = SensitiveStrategy.EMAIL) private String email; @Sensitive(strategy = SensitiveStrategy.ADDRESS, roleKey = {"admin"}) private String address; // 非敏感字段正常处理 private String remark; private Date createTime; } ``` ### 4. Controller接口返回 ```java @RestController public class CustomerController { @GetMapping("/customer/{id}") public R getCustomer(@PathVariable Long id) { Customer customer = customerService.getById(id); // 返回时自动根据当前用户权限进行脱敏处理 return R.ok(customer); } @GetMapping("/customers") public R> getCustomers() { List customers = customerService.list(); // 列表数据也会自动脱敏 return R.ok(customers); } } ``` ## 高级用法 ### 1. 自定义脱敏策略 ```java public enum CustomSensitiveStrategy { // 自定义策略:工号脱敏(保留前2位和后2位) EMPLOYEE_NO(s -> { if (s.length() <= 4) return "****"; return s.substring(0, 2) + "****" + s.substring(s.length() - 2); }), // 自定义策略:金额脱敏(只显示千位以上) AMOUNT(s -> { try { double amount = Double.parseDouble(s); if (amount >= 10000) { return String.format("%.1f万+", amount / 10000); } else if (amount >= 1000) { return String.format("%.1f千+", amount / 1000); } return "***"; } catch (NumberFormatException e) { return "***"; } }); private final Function desensitizer; CustomSensitiveStrategy(Function desensitizer) { this.desensitizer = desensitizer; } public Function desensitizer() { return desensitizer; } } ``` ### 2. 条件脱敏 ```java @Component @Primary public class ConditionalSensitiveService extends SysSensitiveServiceImpl { @Override public boolean isSensitive(String[] roleKey, String[] perms) { // 根据不同条件判断 if (isWorkingHours()) { // 工作时间内权限宽松 return super.isSensitive(roleKey, perms); } else { // 非工作时间权限严格,需要更高权限 return !LoginHelper.isSuperAdmin(); } } private boolean isWorkingHours() { LocalTime now = LocalTime.now(); return now.isAfter(LocalTime.of(9, 0)) && now.isBefore(LocalTime.of(18, 0)); } } ``` ### 3. 动态脱敏配置 ```java @Component @Primary public class DynamicSensitiveService extends SysSensitiveServiceImpl { @Autowired private SensitiveConfigService configService; @Override public boolean isSensitive(String[] roleKey, String[] perms) { // 从配置中心或数据库读取脱敏规则 SensitiveConfig config = configService.getCurrentConfig(); if (!config.isEnabled()) { return false; // 脱敏功能关闭 } // 如果配置了特殊规则,优先使用 if (config.hasCustomRules()) { return config.shouldDesensitize(roleKey, perms); } // 否则使用框架默认逻辑 return super.isSensitive(roleKey, perms); } } ``` ## 配置示例 ### application.yml配置 ```yaml # 脱敏相关配置 sensitive: # 是否启用脱敏功能 enabled: true # 默认脱敏策略 default-strategy: FIRST_MASK # 租户配置 tenant: # 是否启用多租户 enable: true # Jackson配置 spring: jackson: # 启用脱敏序列化器 serialization: write-null-map-values: false ``` ## 测试用例 ### 单元测试示例 ```java @SpringBootTest class SensitiveTest { @Autowired private ObjectMapper objectMapper; @Test void testPhoneDesensitization() throws Exception { TestUser user = new TestUser(); user.setPhone("13812345678"); // 模拟普通用户(需要脱敏) mockUser("user", new String[0]); String json = objectMapper.writeValueAsString(user); assertThat(json).contains("138****5678"); // 模拟管理员(不脱敏) mockUser("admin", new String[]{"admin"}); json = objectMapper.writeValueAsString(user); assertThat(json).contains("13812345678"); } @Test void testMultipleFields() throws Exception { Customer customer = new Customer(); customer.setRealName("张三"); customer.setMobile("13812345678"); customer.setIdNumber("110101199001011234"); customer.setEmail("test@example.com"); mockUser("user", new String[0]); String json = objectMapper.writeValueAsString(customer); assertThat(json).contains("张*"); assertThat(json).contains("138****5678"); assertThat(json).contains("110***********1234"); assertThat(json).contains("t**t@example.com"); } private void mockUser(String role, String[] roles) { // 模拟用户登录状态和角色 // 具体实现根据项目的用户管理方式调整 } } ``` ## 性能考虑 ### 1. 序列化性能 * **缓存策略**:序列化器实例会被Jackson缓存,避免重复创建 * **类型检查**:仅对String类型字段进行处理,其他类型直接跳过 * **权限判断**:建议在权限判断中使用缓存,避免频繁查询 ### 2. 内存优化 ```java @Service @Primary public class CachedSensitiveService extends SysSensitiveServiceImpl { private final Cache permissionCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(5, TimeUnit.MINUTES) .build(); @Override public boolean isSensitive(String[] roleKey, String[] perms) { // 对于登录用户使用缓存 if (LoginHelper.isLogin()) { String cacheKey = buildCacheKey(LoginHelper.getUserId(), roleKey, perms); return permissionCache.get(cacheKey, k -> super.isSensitive(roleKey, perms)); } // 未登录用户直接返回 return true; } private String buildCacheKey(Long userId, String[] roleKey, String[] perms) { return userId + ":" + Arrays.toString(roleKey) + ":" + Arrays.toString(perms); } } ``` ## 最佳实践 ### 1. 注解使用建议 * **精确控制**:根据实际业务需求配置角色和权限 * **分层设计**:不同敏感级别使用不同的权限要求 * **文档说明**:为每个脱敏字段添加清晰的注释说明 ### 2. 权限设计 * **最小权限**:默认进行脱敏,只对特定角色开放原始数据 * **审计日志**:记录敏感数据的访问日志 * **定期审查**:定期检查权限配置的合理性 ### 3. 错误处理 * **容错机制**:确保脱敏失败时不影响业务正常运行 * **日志监控**:记录脱敏相关的错误和异常 * **降级策略**:在系统异常时采用更严格的脱敏策略 ## 故障排查 ### 常见问题 #### 1. 脱敏不生效 **可能原因**: * 框架的SysSensitiveServiceImpl未正确加载 * 注解配置错误 * 字段类型不是String **解决方案**: * 确认框架的system模块已正确引入 * 验证`@Sensitive`注解配置 * 确认字段类型为String类型 #### 2. 权限判断异常 **可能原因**: * 用户未登录 * 权限标识不正确 * 角色配置与系统不一致 * 租户配置问题 **解决方案**: * 检查用户登录状态 * 验证权限标识的正确性 * 确认角色配置与系统一致 * 检查租户环境配置 #### 3. 租户环境脱敏异常 **可能原因**: * 租户配置未启用 * 租户管理员权限配置错误 **解决方案**: * 检查`tenant.enable`配置 * 验证租户管理员角色配置 #### 3. 序列化异常 **可能原因**: * Jackson配置冲突 * 自定义序列化器冲突 **解决方案**: * 检查Jackson相关配置 * 避免在同一字段使用多个序列化注解 ### 调试方法 #### 启用调试日志 ```yaml logging: level: plus.ruoyi.common.sensitive: DEBUG com.fasterxml.jackson: DEBUG ``` #### 测试脱敏效果 ```java @RestController public class TestController { @GetMapping("/test/sensitive") public R testSensitive() { TestData data = new TestData(); data.setPhone("13812345678"); data.setIdCard("110101199001011234"); return R.ok(data); } @GetMapping("/test/sensitive/tenant") public R> testTenantSensitive() { Map result = new HashMap<>(); result.put("isTenantEnable", TenantHelper.isEnable()); result.put("isSuperAdmin", LoginHelper.isSuperAdmin()); result.put("isTenantAdmin", LoginHelper.isTenantAdmin()); return R.ok(result); } } ``` --- --- url: 'https://ruoyi.plus/mobile/composables/custom-hooks.md' --- # 自定义Hook开发 --- --- url: 'https://ruoyi.plus/mobile/layouts/custom.md' --- # 自定义布局 --- --- url: 'https://ruoyi.plus/frontend/directives/custom.md' --- # 自定义指令开发 --- --- url: 'https://ruoyi.plus/mobile/plugins/custom-dev.md' --- # 自定义插件开发 --- --- url: 'https://ruoyi.plus/backend/modules/custom-module.md' --- # 自定义模块 --- --- url: 'https://ruoyi.plus/frontend/components/custom-dev.md' --- # 自定义组件开发 --- --- url: 'https://ruoyi.plus/mobile/components/custom-dev.md' --- # 自定义组件开发 --- --- url: 'https://ruoyi.plus/frontend/i18n/menu-i18n.md' --- # 菜单国际化 --- --- url: 'https://ruoyi.plus/frontend/i18n/form-i18n.md' --- # 表单国际化 --- --- url: 'https://ruoyi.plus/frontend/components/form-components.md' --- # 表单组件 --- --- url: 'https://ruoyi.plus/mobile/components/form.md' --- # 表单组件 --- --- url: 'https://ruoyi.plus/frontend/components/table-toolbar.md' --- # 表格工具栏 (TableToolbar) --- --- url: 'https://ruoyi.plus/frontend/utils/auth.md' --- # 认证工具 --- --- url: 'https://ruoyi.plus/mobile/api/auth.md' --- # 认证接口 --- --- url: 'https://ruoyi.plus/mobile/utils/device.md' --- # 设备信息工具 --- --- url: 'https://ruoyi.plus/frontend/layout/settings.md' --- # 设置面板 (Settings) --- --- url: 'https://ruoyi.plus/mobile/pages/settings.md' --- # 设置页面 (settings) --- --- url: 'https://ruoyi.plus/frontend/i18n/language-packs.md' --- # 语言包管理 --- --- url: 'https://ruoyi.plus/mobile/uniapp/debugging.md' --- # 调试工具 --- --- url: 'https://ruoyi.plus/frontend/dev/debugging.md' --- # 调试技巧 --- --- url: 'https://ruoyi.plus/frontend/router/best-practices.md' --- # 路由最佳实践 --- --- url: 'https://ruoyi.plus/frontend/router/router-guards.md' --- # 路由守卫 --- --- url: 'https://ruoyi.plus/mobile/uniapp/navigation.md' --- # 路由导航 --- --- url: 'https://ruoyi.plus/frontend/router/router-utils.md' --- # 路由工具 --- --- url: 'https://ruoyi.plus/frontend/types/router-types.md' --- # 路由类型 --- --- url: 'https://ruoyi.plus/frontend/router/router-design.md' --- # 路由设计 --- --- url: 'https://ruoyi.plus/frontend/router/router-config.md' --- # 路由配置 --- --- url: 'https://ruoyi.plus/practices/auth-security.md' --- # 身份认证安全 --- --- url: 'https://ruoyi.plus/frontend/components/selection-tags.md' --- # 选择标签 (ASelectionTags) --- --- url: 'https://ruoyi.plus/backend/common/core/service.md' --- # 通用服务接口 core 模块定义了一系列通用服务接口,为系统各模块提供统一的数据访问和业务处理规范。这些接口采用依赖注入的方式,实现了模块间的松耦合设计。 ## 设计理念 ### 接口优先原则 所有业务逻辑都通过接口定义,具体实现由各业务模块提供,确保了: * **模块解耦**:core 模块不依赖具体实现 * **灵活扩展**:可以轻松替换不同的实现方式 * **统一规范**:所有模块遵循相同的接口契约 ### 默认方法支持 接口中大量使用 Java 8+ 的默认方法特性,提供: * **向后兼容**:新增方法不会破坏现有实现 * **便捷重载**:提供多种参数形式的便捷方法 * **降级处理**:为可选功能提供默认实现 ## 核心服务接口 ### 用户服务 (UserService) 用户相关的数据查询和管理服务。 #### 基础查询方法 ```java /** * 通过用户ID查询用户账户 */ String getUserNameById(Long userId); /** * 通过用户ID查询用户昵称 */ String getNickNameById(Long userId); /** * 通过用户ID查询用户头像 */ String getAvatarById(Long userId); ``` #### 批量查询方法 ```java /** * 通过用户ID列表查询用户昵称列表 * @param userIds 用户ID,多个用逗号隔开 * @return 用户昵称,多个用逗号隔开 */ String getNickNamesByIds(String userIds); /** * 通过用户ID查询用户列表 */ List listUsersByIds(List userIds); ``` #### 关联查询方法 ```java /** * 通过角色ID查询用户 */ List listUsersByRoleIds(List roleIds); /** * 通过部门ID查询用户 */ List listUsersByDeptIds(List deptIds); /** * 通过岗位ID查询用户 */ List listUsersByPostIds(List postIds); ``` #### 映射关系方法 ```java /** * 根据用户ID列表查询用户名称映射关系 * @return Map,key为用户ID,value为对应的用户名称 */ Map mapUserNames(List userIds); /** * 根据角色ID列表查询角色名称映射关系 */ Map mapRoleNames(List roleIds); ``` #### 使用示例 ```java @Service public class BusinessService { @Autowired private UserService userService; public void processUserData() { // 单个查询 String userName = userService.getUserNameById(1L); // 批量查询 List userIds = Arrays.asList(1L, 2L, 3L); List users = userService.listUsersByIds(userIds); // 映射关系 Map userNameMap = userService.mapUserNames(userIds); // 关联查询 List deptUsers = userService.listUsersByDeptIds(Arrays.asList(1L, 2L)); } } ``` ### 字典服务 (DictService) 系统字典数据的查询和转换服务。 #### 核心转换方法 ```java /** * 根据字典类型和字典值获取字典标签 * @param dictType 字典类型,如 "sys_user_sex" * @param dictValue 字典值,如 "1" * @return 字典标签,如 "男" */ String getDictLabel(String dictType, String dictValue); /** * 根据字典类型和字典标签获取字典值(反向查询) * @param dictType 字典类型 * @param dictLabel 字典标签,如 "男" * @return 字典值,如 "1" */ String getDictValue(String dictType, String dictLabel); ``` #### 批量转换方法 ```java /** * 支持多值转换,使用指定分隔符 * @param dictType 字典类型 * @param dictValue 字典值,支持多个值用分隔符分割,如 "1,2" * @param separator 分隔符,默认为逗号 * @return 字典标签,如 "男,女" */ String getDictLabel(String dictType, String dictValue, String separator); ``` #### 数据获取方法 ```java /** * 获取字典下所有的字典值与标签映射 * @param dictType 字典类型 * @return Map,key为字典值,value为字典标签 */ Map getAllDictByDictType(String dictType); /** * 根据字典类型查询字典数据列表 */ List getDictData(String dictType); /** * 根据字典类型查询详细信息 */ DictTypeDTO getDictType(String dictType); ``` #### 使用示例 ```java @Service public class UserBusinessService { @Autowired private DictService dictService; public void processUserData() { // 单值转换 String genderLabel = dictService.getDictLabel("sys_user_sex", "1"); // 结果: "男" // 多值转换 String statusLabels = dictService.getDictLabel("sys_user_status", "1,2", ","); // 结果: "正常,停用" // 反向查询 String genderValue = dictService.getDictValue("sys_user_sex", "男"); // 结果: "1" // 获取所有字典项 Map genderDict = dictService.getAllDictByDictType("sys_user_sex"); // 结果: {"0": "女", "1": "男", "2": "未知"} } } ``` #### 常用字典类型 ```java // 系统基础字典 sys_user_sex // 用户性别 sys_enable_status // 启用状态 sys_user_status // 用户状态 sys_boolean_flag // 逻辑标志 // 业务字典 sys_payment_method // 支付方式 sys_order_status // 订单状态 sys_message_type // 消息类型 sys_notice_type // 通知类型 ``` ### 部门服务 (DeptService) 部门组织架构的查询服务。 ```java public interface DeptService { /** * 通过部门ID查询部门名称 * @param deptIds 部门ID串,逗号分隔 * @return 部门名称串,逗号分隔 */ String getDeptNameByIds(String deptIds); /** * 根据部门ID查询部门负责人 * @param deptId 部门ID * @return 负责人用户ID */ Long getDeptLeaderById(Long deptId); /** * 查询所有正常状态的部门 * @return 部门列表 */ List listNormalDepts(); } ``` ### 配置服务 (ConfigService) 系统参数配置的查询服务。 #### 基础配置查询 ```java /** * 根据参数key获取参数值 * @param configKey 参数key,如 "sys.account.registerUser" * @return 参数值 */ String getConfigValue(String configKey); ``` #### 默认值支持 ```java /** * 根据参数key获取参数值,支持默认值 * @param configKey 参数key * @param defaultValue 默认值 * @return 参数值,如果为空则返回默认值 */ default String getConfigValue(String configKey, String defaultValue) { String value = getConfigValue(configKey); return StringUtils.isNotBlank(value) ? value : defaultValue; } ``` #### 类型转换方法 ```java /** * 根据参数key获取布尔参数值 * @param configKey 参数key * @return 布尔参数值 */ default boolean getBooleanValue(String configKey) { return BooleanUtil.toBoolean(getConfigValue(configKey)); } /** * 根据参数key获取布尔参数值,支持默认值 */ default boolean getBooleanValue(String configKey, boolean defaultValue) { String value = getConfigValue(configKey); return StringUtils.isNotBlank(value) ? BooleanUtil.toBoolean(value) : defaultValue; } ``` #### 使用示例 ```java @Service public class SystemConfigService { @Autowired private ConfigService configService; public void checkSystemConfig() { // 基础查询 String siteName = configService.getConfigValue("sys.index.skinName"); // 带默认值查询 String uploadPath = configService.getConfigValue("sys.uploadPath", "/upload"); // 布尔值查询 boolean registerEnabled = configService.getBooleanValue("sys.account.registerUser"); // 布尔值带默认值查询 boolean captchaEnabled = configService.getBooleanValue("sys.account.captchaEnabled", true); } } ``` #### 常用配置项 ```java // 系统基础配置 sys.index.skinName // 系统皮肤 sys.account.registerUser // 是否允许注册 sys.account.captchaEnabled // 是否启用验证码 sys.uploadPath // 上传路径 // 安全配置 sys.user.initPassword // 用户初始密码 sys.account.maxRetryCount // 最大重试次数 sys.account.lockTime // 锁定时间(分钟) ``` ### 权限服务 (PermissionService) 用户权限信息的查询服务。 ```java public interface PermissionService { /** * 获取角色数据权限列表 * @param userId 用户id * @return 角色权限信息 */ Set listRolePermissions(Long userId); /** * 获取菜单数据权限列表 * @param userId 用户id * @return 菜单权限信息 */ Set listMenuPermissions(Long userId); } ``` ### 文件存储服务 (OssService) 对象存储服务的查询接口。 ```java public interface OssService { /** * 通过ossId查询对应的url * @param ossIds ossId串,逗号分隔 * @return url串,逗号分隔 */ String getUrlsByIds(String ossIds); /** * 通过ossId查询列表 * @param ossIds ossId串,逗号分隔 * @return OSS对象列表 */ List listOssByIds(String ossIds); /** * 根据目录id查询目录名称 * @param directoryId 目录id * @return 目录名称 */ String getDirectoryNameById(Long directoryId); } ``` ## 支付与平台服务 ### 支付服务 (PaymentService) 支付配置的查询和管理服务。 #### 核心查询方法 ```java /** * 根据商户号获取支付配置 * @param mchId 商户号 * @return 支付配置,如果不存在返回null */ PaymentDTO getByMchId(String mchId); /** * 根据支付配置ID获取支付配置 * @param paymentId 支付配置ID * @return 支付配置,如果不存在返回null */ PaymentDTO getById(Long paymentId); ``` #### 分类查询方法 ```java /** * 根据支付类型获取支付配置列表 * @param type 支付类型(如:WECHAT, ALIPAY等) * @param tenantId 租户id * @return 支付配置列表 */ List listPaymentByType(String type, String tenantId); ``` #### 验证方法 ```java /** * 检查商户号是否存在且有效 * @param mchId 商户号 * @return true-存在且有效,false-不存在或无效 */ boolean existsValidMchId(String mchId); /** * 获取支付配置总数 * @return 支付配置总数 */ long countPayments(); ``` #### 使用示例 ```java @Service public class PaymentBusinessService { @Autowired private PaymentService paymentService; public void processPayment(String mchId) { // 根据商户号查询配置 PaymentDTO payment = paymentService.getByMchId(mchId); if (payment != null && payment.isEnabled()) { // 处理支付逻辑 if (payment.isWechatPayment()) { // 微信支付处理 } else if (payment.isAlipayPayment()) { // 支付宝支付处理 } } // 查询租户下的微信支付配置 List wechatPayments = paymentService.listPaymentByType("WECHAT", "000000"); // 验证商户号 boolean isValid = paymentService.existsValidMchId(mchId); } } ``` #### 支付类型说明 ```java // 主要支付类型 WECHAT // 微信支付 ALIPAY // 支付宝支付 UNIONPAY // 银联支付 BALANCE // 余额支付 ``` ### 平台服务 (PlatformService) 多平台应用配置的管理服务。 ```java public interface PlatformService { /** * 根据平台类型获取平台配置列表 * @param type 平台类型(如:mp-weixin, mp-alipay) * @param tenantId 租户id * @return 平台配置列表 */ default List listPlatformsByType(String type, String tenantId) { return Collections.emptyList(); } /** * 根据appid和平台类型获取平台配置 * @param appid 应用ID * @param type 平台类型 * @return 平台配置 */ default PlatformDTO getPlatformByAppidAndType(String appid, String type) { return null; } /** * 根据appid获取平台配置(跨租户查询) * @param appid 应用ID * @param tenantId 租户id * @return 平台配置 */ default PlatformDTO getPlatformByAppid(String appid, String tenantId) { return null; } } ``` ### 租户服务 (TenantService) 多租户环境下的租户信息服务。 ```java public interface TenantService { /** * 获取当前租户id * @return 租户id */ String getTenantId(); /** * 根据请求获取租户ID * 专门处理从请求中提取租户信息的逻辑:域名识别 + 请求头获取 * @return 租户ID,如果获取不到返回null */ String getTenantIdByRequest(); } ``` ## 其他服务接口 ### 角色服务 (RoleService) ```java public interface RoleService { // 当前为空接口,预留扩展 } ``` ### 岗位服务 (PostService) ```java public interface PostService { // 当前为空接口,预留扩展 } ``` ### OSS配置服务 (OssConfigService) ```java public interface OssConfigService { /** * 初始化OSS配置 */ void initOssConfig(); } ``` ## 使用指南 ### 1. 依赖注入使用 ```java @Service @RequiredArgsConstructor public class BusinessService { private final UserService userService; private final DictService dictService; private final ConfigService configService; // 业务逻辑实现 } ``` ### 2. 条件注入 ```java @Service @RequiredArgsConstructor @ConditionalOnBean(PaymentService.class) public class PaymentBusinessService { private final PaymentService paymentService; // 只有当PaymentService存在时才创建此服务 } ``` ### 3. 默认实现 如果某个服务接口没有具体实现,可以提供默认的空实现: ```java @Service @ConditionalOnMissingBean(PaymentService.class) public class DefaultPaymentService implements PaymentService { @Override public PaymentDTO getByMchId(String mchId) { return null; // 默认返回空 } // 其他方法的默认实现 } ``` ## 设计优势 * **松耦合**:各模块只依赖接口,不依赖具体实现 * **可测试**:便于进行单元测试和Mock * **可扩展**:新增功能只需扩展接口,不影响现有代码 * **统一规范**:所有模块遵循相同的接口设计原则 * **向后兼容**:使用默认方法确保接口演进的兼容性 这些通用服务接口构成了系统的基础服务层,为上层业务逻辑提供了统一、规范的数据访问方式。 --- --- url: 'https://ruoyi.plus/backend/common/websocket.md' --- # 通讯 (websocket) ## 概述 WebSocket 通讯模块 (`ruoyi-common-websocket`) 提供实时双向通信与消息推送功能,支持分布式环境下的多服务实例消息分发。 ### 核心特性 * **实时双向通信**:基于 Spring Boot WebSocket 实现 * **分布式消息分发**:基于 Redis 发布订阅机制实现跨服务实例消息推送 * **用户认证集成**:集成 SaToken 认证框架,确保连接安全 * **智能消息路由**:优先本地发送,跨实例自动路由 * **会话管理**:线程安全的用户会话管理 * **心跳检测**:内置心跳机制保持连接活跃 ## 架构设计 ### 模块依赖 ```xml plus.ruoyi ruoyi-common-core plus.ruoyi ruoyi-common-redis plus.ruoyi ruoyi-common-satoken plus.ruoyi ruoyi-common-json org.springframework.boot spring-boot-starter-websocket ``` ### 核心组件 #### 1. 配置层 (Configuration) **WebSocketConfig** - 主配置类 * 自动配置条件:`websocket.enabled=true` * 配置WebSocket端点、拦截器、处理器 * 初始化主题监听器 **WebSocketProperties** - 配置属性类 ```yaml websocket: enabled: true # 是否启用WebSocket path: "/websocket" # 服务端点路径 allowedOrigins: "*" # 允许跨域的源地址 ``` #### 2. 拦截器层 (Interceptor) **PlusWebSocketInterceptor** - 握手拦截器 * 用户身份认证验证 * 登录状态检查 * 用户信息注入会话属性 #### 3. 处理器层 (Handler) **PlusWebSocketHandler** - 消息处理器 * 连接建立与关闭处理 * 文本/二进制消息处理 * 心跳检测(Ping/Pong) * 传输错误处理 #### 4. 会话管理层 (Session Management) **WebSocketSessionHolder** - 会话管理器 * 线程安全的会话存储 (ConcurrentHashMap) * 会话添加、移除、查询 * 在线用户管理 #### 5. 工具层 (Utilities) **WebSocketUtils** - 工具类 * 消息发送(单点、批量、群发) * Redis发布订阅消息处理 * 智能消息路由分发 #### 6. 监听器层 (Listener) **WebSocketTopicListener** - 主题监听器 * 应用启动时初始化Redis订阅 * 跨服务实例消息分发处理 * 定向推送和群发消息处理 ## 消息流转机制 ### 智能分发策略 1. **本地优先**:优先在当前服务实例内直接发送消息 2. **跨实例路由**:对于不在当前实例的用户,通过Redis发布订阅机制分发到其他实例 ### 消息流程图 ``` 发送消息 → 检查本地会话 → 存在? ↓ ↓ 直接发送 加入Redis队列 ↓ ↓ 完成 发布到主题 ↓ 其他实例监听 ↓ 处理并发送 ``` ## API 使用指南 ### 配置启用 在 `application.yml` 中配置: ```yaml websocket: enabled: true path: "/websocket" allowedOrigins: "*" ``` ### 消息发送API #### 1. 向指定用户发送消息 ```java // 向单个用户发送消息 WebSocketUtils.sendMessage(userId, "Hello World"); // 向多个用户发送消息 WebSocketMessageDto message = new WebSocketMessageDto(); message.setSessionKeys(Arrays.asList(user1Id, user2Id, user3Id)); message.setMessage("批量消息内容"); WebSocketUtils.publishMessage(message); ``` #### 2. 群发消息 ```java // 向所有在线用户发送消息 WebSocketUtils.publishAll("系统公告:服务将在10分钟后维护"); ``` #### 3. 消息订阅 ```java // 订阅WebSocket消息主题(通常在监听器中使用) WebSocketUtils.subscribeMessage(message -> { // 处理接收到的消息 System.out.println("收到消息: " + message.getMessage()); System.out.println("目标用户: " + message.getSessionKeys()); }); ``` ### 会话管理API ```java // 检查用户是否在线 boolean isOnline = WebSocketSessionHolder.existSession(userId); // 获取用户会话 WebSocketSession session = WebSocketSessionHolder.getSessions(userId); // 获取所有在线用户 Set onlineUsers = WebSocketSessionHolder.getSessionsAll(); // 移除用户会话 WebSocketSessionHolder.removeSession(userId); ``` #### 4. 业务场景集成示例 **实时通知推送** ```java @Service public class NotificationPushService { /** * 推送订单状态变更通知 */ public void pushOrderStatusChange(Long userId, String orderId, String status) { WebSocketMessage message = WebSocketMessage.create( "order_status", "订单状态更新", Map.of("orderId", orderId, "status", status) ); WebSocketUtils.sendMessage(userId, JsonUtils.toJsonString(message)); } /** * 推送系统维护通知 */ public void pushSystemMaintenance(String content, LocalDateTime scheduleTime) { WebSocketMessage message = WebSocketMessage.create( "system_notice", "系统维护通知", Map.of("content", content, "scheduleTime", scheduleTime) ); // 向所有在线用户推送 WebSocketUtils.publishAll(JsonUtils.toJsonString(message)); } } ``` **在线状态管理** ```java @Service public class OnlineUserService { /** * 获取在线用户列表 */ public List getOnlineUsers() { return new ArrayList<>(WebSocketSessionHolder.getSessionsAll()); } /** * 获取在线用户数量 */ public int getOnlineUserCount() { return WebSocketSessionHolder.getSessionsAll().size(); } /** * 检查指定用户是否在线 */ public boolean isUserOnline(Long userId) { return WebSocketSessionHolder.existSession(userId); } /** * 强制下线指定用户 */ public void forceOffline(Long userId, String reason) { WebSocketMessage message = WebSocketMessage.create( "force_offline", "强制下线", Map.of("reason", reason) ); // 发送下线通知 WebSocketUtils.sendMessage(userId, JsonUtils.toJsonString(message)); // 移除会话 WebSocketSessionHolder.removeSession(userId); } } ``` **消息过滤与路由** ````java @Component public class MessageFilter { /** * 根据用户权限过滤消息 */ public boolean canReceiveMessage(Long userId, WebSocketMessage message) { // 根据消息类型和用户权限判断 switch (message.getType()) { case "admin_notice": return hasAdminRole(userId); case "vip_promotion": return hasVipLevel(userId); default: return true; } } /** * 消息路由处理 */ public void routeMessage(WebSocketMessage message, List targetUsers) { // 过滤有权限接收消息的用户 List filteredUsers = targetUsers.stream() .filter(userId -> canReceiveMessage(userId, message)) .collect(Collectors.toList()); if (!filteredUsers.isEmpty()) { WebSocketMessageDto dto = new WebSocketMessageDto(); dto.setSessionKeys(filteredUsers); dto.setMessage(JsonUtils.toJsonString(message)); WebSocketUtils.publishMessage(dto); } } private boolean hasAdminRole(Long userId) { // 检查用户是否有管理员权限 return LoginHelper.hasRole("admin"); } private boolean hasVipLevel(Long userId) { // 检查用户是否为VIP return false; // 具体实现根据业务逻辑 } } ## 后端服务集成 ### 在业务模块中使用WebSocket #### 1. 注入依赖 由于WebSocket模块通过自动配置加载,您可以直接在业务代码中使用: ```java @Service public class NotificationService { /** * 发送系统通知 */ public void sendSystemNotification(Long userId, String title, String content) { // 构造通知消息 String message = String.format("{\"type\":\"notification\",\"title\":\"%s\",\"content\":\"%s\"}", title, content); // 发送给指定用户 WebSocketUtils.sendMessage(userId, message); } /** * 发送业务消息 */ public void sendBusinessMessage(List userIds, Object data) { WebSocketMessageDto messageDto = new WebSocketMessageDto(); messageDto.setSessionKeys(userIds); messageDto.setMessage(JsonUtils.toJsonString(data)); WebSocketUtils.publishMessage(messageDto); } } ```` #### 2. 消息格式标准化 建议定义统一的消息格式: ```java @Data public class WebSocketMessage { /** * 消息类型:notification、chat、system、business等 */ private String type; /** * 消息标题 */ private String title; /** * 消息内容 */ private Object content; /** * 时间戳 */ private Long timestamp; /** * 发送者ID */ private Long senderId; /** * 扩展数据 */ private Map extra; public static WebSocketMessage create(String type, String title, Object content) { WebSocketMessage message = new WebSocketMessage(); message.setType(type); message.setTitle(title); message.setContent(content); message.setTimestamp(System.currentTimeMillis()); return message; } } ``` #### 3. 事件驱动的消息推送 使用Spring事件机制实现解耦的消息推送: ```java // 定义WebSocket消息事件 @Data @AllArgsConstructor public class WebSocketMessageEvent { private List userIds; private WebSocketMessage message; } // 事件发布者 @Service public class MessagePublisher { @Autowired private ApplicationEventPublisher eventPublisher; public void publishMessage(List userIds, WebSocketMessage message) { eventPublisher.publishEvent(new WebSocketMessageEvent(userIds, message)); } } // 事件监听者 @Component @Slf4j public class WebSocketMessageListener { @EventListener @Async("websocketExecutor") public void handleWebSocketMessage(WebSocketMessageEvent event) { try { String messageContent = JsonUtils.toJsonString(event.getMessage()); WebSocketMessageDto dto = new WebSocketMessageDto(); dto.setSessionKeys(event.getUserIds()); dto.setMessage(messageContent); WebSocketUtils.publishMessage(dto); log.info("WebSocket消息推送成功,用户数:{}", event.getUserIds().size()); } catch (Exception e) { log.error("WebSocket消息推送失败", e); } } } ``` ## 核心实现原理 ### 连接认证机制 WebSocket连接建立时的认证流程基于SaToken框架: ```java @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) { try { // 从请求中获取token并验证用户登录状态 LoginUser loginUser = LoginHelper.getLoginUser(); // 将用户信息存储到WebSocket会话属性中 attributes.put(LOGIN_USER, loginUser); return true; } catch (NotLoginException e) { log.error("WebSocket 认证失败: {}", e.getMessage()); return false; // 拒绝连接 } } ``` ### 会话生命周期管理 **连接建立**: 1. 通过握手拦截器验证用户身份 2. 将用户信息注入WebSocket会话属性 3. 在会话管理器中注册用户会话映射 4. 记录连接建立日志 **消息处理**: 1. 从会话属性中获取用户信息 2. 构造消息传输对象 3. 调用消息发布机制 **连接关闭**: 1. 从会话属性中获取用户信息 2. 从会话管理器中移除用户映射 3. 关闭底层WebSocket连接 4. 记录连接关闭日志 ### 分布式消息分发机制 **智能路由算法**: ```java public static void publishMessage(WebSocketMessageDto webSocketMessage) { List unsentSessionKeys = new ArrayList<>(); // 第一阶段:本地会话处理 for (Long sessionKey : webSocketMessage.getSessionKeys()) { if (WebSocketSessionHolder.existSession(sessionKey)) { // 用户在当前实例,直接发送 sendMessage(sessionKey, webSocketMessage.getMessage()); } else { // 用户不在当前实例,标记为待分发 unsentSessionKeys.add(sessionKey); } } // 第二阶段:跨实例分发 if (CollUtil.isNotEmpty(unsentSessionKeys)) { WebSocketMessageDto broadcastMessage = new WebSocketMessageDto(); broadcastMessage.setMessage(webSocketMessage.getMessage()); broadcastMessage.setSessionKeys(unsentSessionKeys); // 通过Redis发布订阅分发到其他实例 RedisUtils.publish(WEB_SOCKET_TOPIC, broadcastMessage, consumer -> { log.info("WebSocket跨实例消息分发 - 目标用户:{}", unsentSessionKeys); }); } } ``` **订阅处理机制**: ```java // 监听Redis主题消息 WebSocketUtils.subscribeMessage((message) -> { if (CollUtil.isNotEmpty(message.getSessionKeys())) { // 定向推送:检查用户是否在当前实例 message.getSessionKeys().forEach(key -> { if (WebSocketSessionHolder.existSession(key)) { WebSocketUtils.sendMessage(key, message.getMessage()); } }); } else { // 群发消息:向当前实例所有在线用户发送 WebSocketSessionHolder.getSessionsAll().forEach(key -> { WebSocketUtils.sendMessage(key, message.getMessage()); }); } }); ``` ### 线程安全保证 **会话并发管理**: * 使用`ConcurrentHashMap`保证线程安全的会话存储 * 会话添加/移除操作的原子性处理 * synchronized关键字保证消息发送的线程安全 ```java private static synchronized void sendMessage(WebSocketSession session, WebSocketMessage message) { if (session == null || !session.isOpen()) { log.warn("WebSocket会话已关闭,无法发送消息"); return; } try { session.sendMessage(message); } catch (IOException e) { log.error("WebSocket消息发送失败,会话ID:{},异常:{}", session.getId(), e.getMessage(), e); } } ``` ## 最佳实践 ### 1. 连接管理 * 实现客户端重连机制 * 定期清理无效连接 * 合理设置连接超时时间 ### 2. 消息处理 * 对大量消息进行批量处理 * 实现消息确认机制 * 考虑消息持久化需求 ### 3. 性能优化 * 使用连接池管理WebSocket连接 * 对频繁的消息推送进行合并 * 监控内存使用情况 ### 4. 安全考虑 * 确保所有WebSocket连接都经过认证 * 实施消息内容验证 * 防止消息泛滥攻击 ## 常见问题 ### Q1: WebSocket连接认证失败 **问题**:客户端无法建立WebSocket连接,提示认证失败 **解决方案**: 1. 确保客户端已正确登录并获取有效token 2. 检查token是否正确传递给WebSocket握手请求 3. 验证SaToken配置是否正确 ### Q2: 消息发送到其他服务实例失败 **问题**:在集群环境中,消息无法发送到其他服务实例的用户 **解决方案**: 1. 检查Redis连接配置 2. 验证Redis发布订阅功能是否正常 3. 确认WebSocketTopicListener是否正确初始化 ### Q3: 内存泄漏问题 **问题**:长时间运行后出现内存泄漏 **解决方案**: 1. 确保连接关闭时正确清理会话 2. 定期检查并清理无效会话 3. 监控WebSocketSessionHolder中的会话数量 ### Q4: 跨域问题 **问题**:浏览器提示跨域错误 **解决方案**: 1. 配置正确的allowedOrigins 2. 确保WebSocket协议与页面协议匹配 3. 检查代理服务器的WebSocket配置 ## 监控与日志 ### 关键监控指标 * 在线用户数量 * 消息发送成功率 * 连接建立/断开频率 * Redis发布订阅延迟 ### 日志配置 ```yaml logging: level: plus.ruoyi.common.websocket: DEBUG ``` ### 示例监控代码 ```java @Component public class WebSocketMonitor { @Scheduled(fixedRate = 60000) // 每分钟统计一次 public void logStatistics() { int onlineUsers = WebSocketSessionHolder.getSessionsAll().size(); log.info("当前在线用户数: {}", onlineUsers); } } ``` ## 扩展开发 ### 自定义消息处理器 ```java @Component public class CustomMessageHandler { @EventListener public void handleCustomMessage(WebSocketMessageDto message) { // 自定义消息处理逻辑 if (message.getMessage().startsWith("CUSTOM:")) { // 处理自定义消息 } } } ``` ### 消息持久化 ```java @Service public class MessagePersistenceService { public void saveMessage(WebSocketMessageDto message) { // 将消息保存到数据库 // 用于离线用户消息推送 } public List getOfflineMessages(Long userId) { // 获取用户离线期间的消息 return Collections.emptyList(); } } ``` --- --- url: 'https://ruoyi.plus/backend/common/mail.md' --- # 邮件模块 ## 概述 邮件模块是基于 Hutool 的邮件工具构建的 Spring Boot 自动配置模块,提供简单易用的邮件发送功能。支持文本邮件、HTML邮件、带附件邮件以及内嵌图片邮件的发送。 ## 核心特性 * 🚀 **开箱即用**:Spring Boot 自动配置,无需复杂设置 * 📧 **多种格式**:支持纯文本、HTML、带附件邮件 * 🖼️ **内嵌图片**:支持HTML邮件中的内嵌图片 * 👥 **批量发送**:支持多收件人、抄送、密送 * 🔒 **安全连接**:支持SSL/TLS和STARTTLS安全连接 * ⚙️ **灵活配置**:支持超时设置和多种SMTP服务器 ## 快速开始 ### 1. 添加依赖 确保项目中已包含邮件模块依赖。 ### 2. 配置文件 在 `application.yml` 中添加邮件配置: ```yaml ################## 邮件服务配置 ################## --- # mail 邮件发送 mail: # 是否启用邮件服务 enabled: true # 生产环境设为 true,开发环境可设为 false # SMTP服务器地址 host: smtp.163.com # 163邮箱SMTP服务器 # SMTP服务器端口 port: 465 # SSL端口 # 是否需要用户名密码验证 auth: true # 开启认证 # 发送方,遵循RFC-822标准 from: xxx@163.com # 发件人邮箱地址 # 用户名(注意:如果使用foxmail邮箱,此处user为qq号) user: xxx@163.com # 邮箱用户名 # 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助) pass: xxxxxxxxxx # 邮箱授权码(不是登录密码) # 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展 starttlsEnable: true # 启用STARTTLS # 使用SSL安全连接 sslEnable: true # 启用SSL # SMTP超时时长,单位毫秒,缺省值不超时 timeout: 0 # 0表示不设置超时 # Socket连接超时值,单位毫秒,缺省值不超时 connectionTimeout: 0 # 0表示不设置连接超时 ``` ### 3. 发送邮件 ```java import plus.ruoyi.common.mail.utils.MailUtils; // 发送简单文本邮件 MailUtils.sendText("recipient@example.com", "测试邮件", "这是一封测试邮件"); // 发送HTML邮件 MailUtils.sendHtml("recipient@example.com", "HTML邮件", "

这是HTML邮件

"); ``` ## 配置详解 ### 配置属性说明 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `mail.enabled` | Boolean | false | 是否启用邮件功能 | | `mail.host` | String | - | SMTP服务器域名 | | `mail.port` | Integer | - | SMTP服务端口 | | `mail.auth` | Boolean | true | 是否需要用户名密码验证 | | `mail.user` | String | - | 用户名(foxmail邮箱填写QQ号) | | `mail.pass` | String | - | 密码或授权码(不是登录密码) | | `mail.from` | String | - | 发件人地址,遵循RFC-822标准 | | `mail.starttlsEnable` | Boolean | false | 是否启用STARTTLS安全连接 | | `mail.sslEnable` | Boolean | false | 是否启用SSL安全连接 | | `mail.timeout` | Long | 0 | SMTP超时时长(毫秒),0表示不超时 | | `mail.connectionTimeout` | Long | 0 | 连接超时时长(毫秒),0表示不超时 | ### 常见邮箱服务器配置 #### QQ邮箱 ```yaml ################## 邮件服务配置 ################## --- # mail 邮件发送 mail: enabled: true host: smtp.qq.com port: 587 # 使用STARTTLS端口 from: your-email@qq.com user: your-email@qq.com pass: your-auth-code # QQ邮箱授权码 auth: true starttlsEnable: true sslEnable: false # STARTTLS方式不启用SSL timeout: 0 connectionTimeout: 0 ``` ::: tip QQ邮箱授权码获取 1. 登录QQ邮箱 2. 进入 设置 → 账户 3. 开启SMTP服务 4. 获取授权码(16位字符) ::: #### 163邮箱 ```yaml ################## 邮件服务配置 ################## --- # mail 邮件发送 mail: enabled: true host: smtp.163.com port: 465 # 使用SSL端口 from: your-email@163.com user: your-email@163.com pass: your-auth-code # 163邮箱授权码 auth: true starttlsEnable: true sslEnable: true # 启用SSL timeout: 0 # 不设置超时 connectionTimeout: 0 ``` ::: tip 163邮箱授权码获取 1. 登录163邮箱 2. 进入 设置 → POP3/SMTP/IMAP 3. 开启SMTP服务 4. 获取授权码(不是登录密码) ::: #### Gmail ```yaml ################## 邮件服务配置 ################## --- # mail 邮件发送 mail: enabled: true host: smtp.gmail.com port: 587 from: your-email@gmail.com user: your-email@gmail.com pass: your-app-password # Gmail应用专用密码 auth: true starttlsEnable: true sslEnable: false timeout: 0 connectionTimeout: 0 ``` ::: tip Gmail应用密码获取 1. 开启两步验证 2. 进入 Google账户 → 安全性 → 应用专用密码 3. 生成应用专用密码(16位字符) ::: ## API 使用指南 ### 基础邮件发送 #### 发送文本邮件 ```java // 发送给单个收件人 String messageId = MailUtils.sendText("user@example.com", "邮件标题", "邮件内容"); // 发送给多个收件人(逗号或分号分隔) MailUtils.sendText("user1@example.com,user2@example.com", "邮件标题", "邮件内容"); // 发送给收件人列表 List recipients = Arrays.asList("user1@example.com", "user2@example.com"); MailUtils.sendText(recipients, "邮件标题", "邮件内容"); ``` #### 发送HTML邮件 ```java String htmlContent = """

欢迎使用邮件服务

这是一封HTML格式的邮件

访问我们的网站 """; MailUtils.sendHtml("user@example.com", "HTML邮件", htmlContent); ``` ### 带附件邮件 ```java import java.io.File; File attachment1 = new File("/path/to/document.pdf"); File attachment2 = new File("/path/to/image.jpg"); // 发送带附件的文本邮件 MailUtils.sendText("user@example.com", "带附件邮件", "请查看附件", attachment1, attachment2); // 发送带附件的HTML邮件 MailUtils.sendHtml("user@example.com", "带附件HTML邮件", htmlContent, attachment1, attachment2); ``` ### 抄送和密送 ```java // 使用字符串形式(逗号或分号分隔) MailUtils.send( "to@example.com", // 收件人 "cc@example.com", // 抄送 "bcc@example.com", // 密送 "邮件标题", "邮件内容", true // 是否HTML格式 ); // 使用集合形式 List tos = Arrays.asList("to1@example.com", "to2@example.com"); List ccs = Arrays.asList("cc1@example.com", "cc2@example.com"); List bccs = Arrays.asList("bcc1@example.com", "bcc2@example.com"); MailUtils.send(tos, ccs, bccs, "邮件标题", "邮件内容", true); ``` ### 内嵌图片邮件 ```java import java.io.FileInputStream; import java.io.InputStream; import java.util.HashMap; import java.util.Map; // 准备图片映射 Map imageMap = new HashMap<>(); imageMap.put("logo", new FileInputStream("/path/to/logo.png")); imageMap.put("banner", new FileInputStream("/path/to/banner.jpg")); // HTML内容中引用图片 String htmlContent = """ Logo

欢迎使用我们的服务

Banner

这是包含内嵌图片的邮件

"""; MailUtils.sendHtml("user@example.com", "内嵌图片邮件", htmlContent, imageMap); ``` ### 自定义邮件账户 ```java import cn.hutool.extra.mail.MailAccount; // 临时更改发送账户信息 MailAccount customAccount = MailUtils.getMailAccount("custom@example.com", "custom@example.com", "custom-password"); MailUtils.send(customAccount, "recipient@example.com", "使用自定义账户", "邮件内容", false); // 完全自定义邮件账户 MailAccount mailAccount = new MailAccount(); mailAccount.setHost("smtp.custom.com"); mailAccount.setPort(587); mailAccount.setFrom("sender@custom.com"); mailAccount.setUser("sender@custom.com"); mailAccount.setPass("password"); mailAccount.setAuth(true); mailAccount.setStarttlsEnable(true); MailUtils.send(mailAccount, "recipient@example.com", "完全自定义账户", "邮件内容", false); ``` ## 高级用法 ### 邮件模板 建议结合模板引擎使用: ```java // 使用Thymeleaf模板(示例) @Service public class EmailService { @Autowired private TemplateEngine templateEngine; public void sendWelcomeEmail(String to, String username) { Context context = new Context(); context.setVariable("username", username); context.setVariable("loginUrl", "https://example.com/login"); String htmlContent = templateEngine.process("email/welcome", context); MailUtils.sendHtml(to, "欢迎注册", htmlContent); } } ``` ### 异步发送 ```java @Service public class AsyncEmailService { @Async("emailTaskExecutor") public CompletableFuture sendEmailAsync(String to, String subject, String content) { try { String messageId = MailUtils.sendHtml(to, subject, content); return CompletableFuture.completedFuture(messageId); } catch (Exception e) { CompletableFuture future = new CompletableFuture<>(); future.completeExceptionally(e); return future; } } } ``` ### 批量发送优化 ```java @Service public class BulkEmailService { public void sendBulkEmails(List recipients, String subject, String content) { // 分批发送,避免一次发送过多邮件 int batchSize = 50; for (int i = 0; i < recipients.size(); i += batchSize) { int end = Math.min(i + batchSize, recipients.size()); List batch = recipients.subList(i, end); try { MailUtils.sendHtml(batch, subject, content); // 批次间添加延迟,避免触发邮件服务器限制 Thread.sleep(1000); } catch (Exception e) { log.error("批量发送邮件失败,批次: {}-{}", i, end, e); } } } } ``` ## 错误处理 ### 常见异常处理 ```java public class EmailService { public boolean sendEmailSafely(String to, String subject, String content) { try { MailUtils.sendHtml(to, subject, content); return true; } catch (cn.hutool.extra.mail.MailException e) { log.error("邮件发送失败: {}", e.getMessage()); return false; } catch (Exception e) { log.error("发送邮件时发生未知错误", e); return false; } } } ``` ### 重试机制 ```java @Component public class EmailRetryService { @Retryable(value = {MailException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000)) public String sendWithRetry(String to, String subject, String content) { return MailUtils.sendHtml(to, subject, content); } @Recover public String recover(MailException ex, String to, String subject, String content) { log.error("邮件发送重试失败,收件人: {}, 标题: {}", to, subject, ex); return null; } } ``` ## 性能优化 ### 连接池配置 ```yaml mail: enabled: true # ... 其他配置 timeout: 30000 # 适当设置超时时间 connectionTimeout: 10000 # 连接超时 ``` ### 异步配置 ```java @Configuration @EnableAsync public class AsyncConfig { @Bean("emailTaskExecutor") public TaskExecutor emailTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix("Email-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } ``` ## 安全建议 1. **使用授权码**:不要使用邮箱登录密码,使用专用的授权码或应用密码 2. **配置加密**:敏感的邮箱密码建议使用配置加密 3. **限制发送频率**:避免触发邮件服务商的反垃圾机制 4. **验证收件人**:发送前验证邮箱地址格式的合法性 ```java public class EmailValidator { private static final String EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); public static boolean isValid(String email) { return EMAIL_PATTERN.matcher(email).matches(); } } ``` ## 开发环境配置 ### 配置说明 开发环境中通常将 `enabled: false` 来禁用邮件发送,避免开发过程中误发邮件。以下是开发环境的推荐配置: ```yaml ################## 邮件服务配置 ################## --- # mail 邮件发送 mail: # 开发环境建议设置为 false,避免误发邮件 enabled: false # 以下配置在开发环境中可以使用测试邮箱 host: smtp.163.com port: 465 auth: true from: test@163.com user: test@163.com pass: test-auth-code starttlsEnable: true sslEnable: true timeout: 0 connectionTimeout: 0 ``` ### 环境切换策略 #### 方案一:配置文件分离 **application-dev.yml** (开发环境) ```yaml mail: enabled: false # 开发环境禁用 ``` **application-prod.yml** (生产环境) ```yaml mail: enabled: true # 生产环境启用 host: smtp.163.com port: 465 # ... 其他配置 ``` #### 方案二:环境变量 ```yaml mail: enabled: ${MAIL_ENABLED:false} # 默认禁用 host: ${MAIL_HOST:smtp.163.com} port: ${MAIL_PORT:465} user: ${MAIL_USER:} pass: ${MAIL_PASS:} from: ${MAIL_FROM:} ``` #### 方案三:配置加密 使用 Jasypt 对敏感信息加密: ```yaml mail: enabled: true host: smtp.163.com port: 465 user: ENC(encrypted_username) pass: ENC(encrypted_password) from: ENC(encrypted_email) ``` ### 开发调试技巧 #### 1. 邮件发送测试 在开发环境中可以创建测试方法: ```java @SpringBootTest class MailUtilsTest { @Test @Disabled("开发环境测试,需手动启用") void testSendMail() { // 临时启用邮件服务进行测试 String result = MailUtils.sendText( "developer@example.com", "开发环境测试邮件", "这是一封测试邮件,请忽略" ); assertNotNull(result); System.out.println("邮件发送成功,MessageId: " + result); } } ``` #### 2. 邮件内容预览 开发环境中可以将邮件内容输出到控制台: ```java @Service public class DevEmailService { @Value("${mail.enabled:false}") private boolean mailEnabled; public void sendEmail(String to, String subject, String content) { if (mailEnabled) { MailUtils.sendHtml(to, subject, content); } else { // 开发环境下输出邮件内容到控制台 System.out.println("=== 邮件预览 ==="); System.out.println("收件人: " + to); System.out.println("标题: " + subject); System.out.println("内容: " + content); System.out.println("==============="); } } } ``` #### 3. 使用邮件捕获工具 推荐使用 MailHog 或 MailCatcher 等工具在开发环境中捕获邮件: ```yaml # 使用 MailHog 的配置 mail: enabled: true host: localhost port: 1025 # MailHog SMTP端口 auth: false from: dev@localhost sslEnable: false starttlsEnable: false ``` ### 常见问题 1. **邮件发送失败** * 检查SMTP服务器配置是否正确 * 确认用户名和密码/授权码是否正确 * 检查网络连接和防火墙设置 2. **SSL/TLS连接问题** * 确认端口和加密设置匹配 * 检查Java版本是否支持所需的SSL/TLS版本 3. **附件过大** * 检查邮件服务商的附件大小限制 * 考虑使用云存储链接替代大附件 ### 日志配置 ```yaml logging: level: cn.hutool.extra.mail: DEBUG plus.ruoyi.common.mail: DEBUG ``` ## 更新日志 ### v1.0.0 * 初始版本发布 * 支持基础文本和HTML邮件发送 * 支持附件和内嵌图片 * 支持多收件人、抄送、密送 * Spring Boot自动配置支持 --- --- url: 'https://ruoyi.plus/backend/common/core/config.md' --- # 配置管理 ## 概述 配置管理模块负责系统的核心配置,包括应用属性配置、线程池配置、异步任务配置、校验器配置等。该模块采用 Spring Boot 的自动配置机制,为系统提供开箱即用的基础配置。 ## 模块结构 ```text config/ ├── AsyncConfig.java # 异步任务配置 ├── SpringFeaturesConfig.java # Spring全局特性配置 ├── ThreadPoolConfig.java # 线程池配置 ├── ValidatorConfig.java # 校验器配置 ├── properties/ # 配置属性类 │ ├── AppProperties.java # 应用属性配置 │ └── ThreadPoolProperties.java # 线程池属性配置 ├── validation/ # 校验相关配置 │ └── I18nMessageInterceptor.java # 国际化消息拦截器 └── factory/ # 工厂类 └── YmlPropertySourceFactory.java # YAML配置源工厂 ``` ## 应用属性配置 ### AppProperties 应用级别的基础属性配置,定义了系统的核心参数。 ```java @Data @ConfigurationProperties(prefix = "app") public class AppProperties { /** * 应用id */ private String id; /** * 应用名称/标题 */ private String title; /** * 应用版本 */ private String version; /** * 应用版权年份 */ private String copyrightYear; /** * 应用本地文件上传路径 */ private String uploadPath; /** * 应用基础路径(供支付等模块构建回调地址使用) */ private String baseApi; } ``` #### 配置示例 ```yaml app: id: ryplus_uni title: ryplus-uni后台管理 version: ${revision} copyright-year: 2025 upload-path: /ruoyi/server/uploadPath base-api: https://ruoyi.plus ``` #### 使用方式 ```java @Component public class AppService { @Autowired private AppProperties appProperties; public void buildCallbackUrl(String path) { String url = appProperties.getBaseApi() + path; // 构建回调地址逻辑 } } ``` ## 线程池配置 ### ThreadPoolConfig 提供系统通用的线程池配置,支持传统线程和虚拟线程自动切换。 #### 核心特性 * **自动模式切换**: 根据 JVM 支持情况自动选择虚拟线程或传统线程 * **优雅关闭**: 应用停止时安全关闭线程池 * **异常处理**: 统一的任务执行异常处理 * **动态配置**: 支持通过配置文件调整线程池参数 #### 配置类 ```java @Slf4j @AutoConfiguration @EnableConfigurationProperties(ThreadPoolProperties.class) public class ThreadPoolConfig { /** * 核心线程数 = CPU 核心数 + 1 */ private final int core = Runtime.getRuntime().availableProcessors() + 1; /** * 普通任务线程池 */ @Bean(name = "threadPoolTaskExecutor") @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true") public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties properties) { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(core); executor.setMaxPoolSize(core * 2); executor.setQueueCapacity(properties.getQueueCapacity()); executor.setKeepAliveSeconds(properties.getKeepAliveSeconds()); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; } /** * 定时任务线程池 */ @Bean(name = "scheduledExecutorService") protected ScheduledExecutorService scheduledExecutorService() { // 根据系统支持情况选择虚拟线程或传统线程 BasicThreadFactory.Builder builder = new BasicThreadFactory.Builder().daemon(true); if (SpringUtils.isVirtual()) { // 使用虚拟线程 builder.namingPattern("virtual-schedule-pool-%d") .wrappedFactory(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); } else { // 使用传统线程 builder.namingPattern("schedule-pool-%d"); } return new ScheduledThreadPoolExecutor(core, builder.build(), new ThreadPoolExecutor.CallerRunsPolicy()) { @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); ThreadUtils.logException(r, t); } }; } } ``` #### 配置属性 ```java @Data @ConfigurationProperties(prefix = "thread-pool") public class ThreadPoolProperties { /** * 是否开启线程池 */ private boolean enabled; /** * 队列最大长度 */ private int queueCapacity; /** * 线程池维护线程所允许的空闲时间(秒) */ private int keepAliveSeconds; } ``` #### 配置示例 ```yaml thread-pool: enabled: true queue-capacity: 1000 keep-alive-seconds: 300 ``` #### 使用方式 ```java @Service public class TaskService { @Autowired @Qualifier("threadPoolTaskExecutor") private ThreadPoolTaskExecutor taskExecutor; public void executeAsyncTask() { taskExecutor.execute(() -> { // 异步任务逻辑 System.out.println("执行异步任务"); }); } } ``` ## 异步配置 ### AsyncConfig 配置 Spring 的异步执行机制,支持虚拟线程和传统线程池的自动切换。 ```java @AutoConfiguration public class AsyncConfig implements AsyncConfigurer { /** * 自定义 @Async 注解的线程执行器 */ @Override public Executor getAsyncExecutor() { // 检查是否支持虚拟线程 if (SpringUtils.isVirtual()) { return new VirtualThreadTaskExecutor("async-"); } return SpringUtils.getBean("scheduledExecutorService"); } /** * 异步执行异常处理器 */ @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (throwable, method, objects) -> { throwable.printStackTrace(); StringBuilder sb = new StringBuilder(); sb.append("异步任务异常 - ").append(throwable.getMessage()) .append(", 方法名称 - ").append(method.getName()); if (ArrayUtil.isNotEmpty(objects)) { sb.append(", 方法参数 - ").append(Arrays.toString(objects)); } throw ServiceException.of(sb.toString()); }; } } ``` #### 使用方式 ```java @Service public class EmailService { @Async public void sendEmailAsync(String to, String subject, String content) { // 异步发送邮件 try { Thread.sleep(2000); // 模拟邮件发送 log.info("邮件发送成功: {}", to); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ServiceException("邮件发送被中断"); } } } ``` ## 校验器配置 ### ValidatorConfig 自定义 Bean Validation 配置,集成国际化消息拦截器。 ```java @AutoConfiguration(before = ValidationAutoConfiguration.class) public class ValidatorConfig { /** * 配置校验框架 */ @Bean public Validator validator(MessageSource messageSource) { try (LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean()) { // 设置自定义的国际化消息拦截器 factoryBean.setMessageInterpolator(new I18nMessageInterceptor(messageSource)); // 设置使用 HibernateValidator 校验器 factoryBean.setProviderClass(HibernateValidator.class); // 配置快速失败模式 Properties properties = new Properties(); properties.setProperty("hibernate.validator.fail_fast", "true"); factoryBean.setValidationProperties(properties); factoryBean.afterPropertiesSet(); return factoryBean.getValidator(); } } } ``` ### 国际化消息拦截器 ```java @Component public class I18nMessageInterceptor implements MessageInterpolator { private final MessageSource messageSource; private final MessageInterpolator defaultInterpolator; public I18nMessageInterceptor(MessageSource messageSource) { this.messageSource = messageSource; this.defaultInterpolator = new ResourceBundleMessageInterpolator(); } @Override public String interpolate(String messageTemplate, Context context, Locale locale) { if (StringUtils.isBlank(messageTemplate)) { return messageTemplate; } String trimmedTemplate = messageTemplate.trim(); // 判断是否为简化的国际化键格式 if (RegexValidator.isValidI18nKey(trimmedTemplate)) { try { // 获取国际化消息 String i18nMessage = messageSource.getMessage(trimmedTemplate, null, locale); // 交给默认插值器处理约束属性 return defaultInterpolator.interpolate(i18nMessage, context, locale); } catch (Exception e) { return trimmedTemplate; } } // 其他情况委托给默认插值器 return defaultInterpolator.interpolate(messageTemplate, context, locale); } } ``` #### 使用示例 ```java public class UserBo { // 直接使用国际化键,无需花括号 @NotBlank(message = "user.username.required") private String username; @Email(message = "user.email.format.invalid") private String email; } ``` ## Spring 全局特性配置 ### SpringFeaturesConfig 启用 Spring 的全局特性,如 AOP 和异步处理。 ```java @AutoConfiguration @EnableAspectJAutoProxy // 启用 AOP @EnableAsync(proxyTargetClass = true) // 启用异步处理 public class SpringFeaturesConfig { // 此类仅用于配置注解,无需添加方法 } ``` 功能说明: * **@EnableAspectJAutoProxy**: 启用基于注解的 AOP 支持 * **@EnableAsync**: 启用异步方法执行支持,使用 CGLIB 代理 ## YAML 配置源工厂 ### YmlPropertySourceFactory 支持 Spring 加载 YAML 格式的配置文件。 ```java public class YmlPropertySourceFactory extends DefaultPropertySourceFactory { @Override public PropertySource createPropertySource(String name, EncodedResource resource) throws IOException { String sourceName = resource.getResource().getFilename(); // 判断是否为 YAML 格式文件 if (StringUtils.isNotBlank(sourceName) && StringUtils.endsWithAny(sourceName, ".yml", ".yaml")) { YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); factory.setResources(resource.getResource()); factory.afterPropertiesSet(); return new PropertiesPropertySource(sourceName, factory.getObject()); } return super.createPropertySource(name, resource); } } ``` #### 使用方式 ```java @Component @PropertySource(value = "classpath:custom-config.yml", factory = YmlPropertySourceFactory.class) public class CustomConfig { // 配置类实现 } ``` ## 自动配置 系统通过 `spring.factories` 文件实现自动配置: ```properties # META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports plus.ruoyi.common.core.config.properties.AppProperties plus.ruoyi.common.core.config.AsyncConfig plus.ruoyi.common.core.config.SpringFeaturesConfig plus.ruoyi.common.core.config.ThreadPoolConfig plus.ruoyi.common.core.config.ValidatorConfig plus.ruoyi.common.core.utils.SpringUtils ``` ## 配置最佳实践 ### 1. 环境配置分离 ```yaml # application.yml (通用配置) app: title: ryplus-uni后台管理 copyright-year: 2025 thread-pool: enabled: true --- # application-dev.yml (开发环境) app: upload-path: D:/ruoyi/uploads base-api: http://localhost:5500 thread-pool: queue-capacity: 100 keep-alive-seconds: 60 --- # application-prod.yml (生产环境) app: upload-path: /opt/ruoyi/uploads base-api: https://api.ruoyi.com thread-pool: queue-capacity: 1000 keep-alive-seconds: 300 ``` ### 2. 配置属性校验 ```java @Data @ConfigurationProperties(prefix = "app") @Validated public class AppProperties { @NotBlank(message = "应用ID不能为空") private String id; @NotBlank(message = "应用标题不能为空") private String title; @Pattern(regexp = "\\d+\\.\\d+\\.\\d+", message = "版本号格式不正确") private String version; @Min(value = 1990, message = "版权年份不能小于1990") @Max(value = 2030, message = "版权年份不能大于2030") private Integer copyrightYear; } ``` ### 3. 条件化配置 ```java @Configuration public class ConditionalConfig { @Bean @ConditionalOnProperty(name = "app.cache.enabled", havingValue = "true") public CacheManager cacheManager() { return new ConcurrentMapCacheManager(); } @Bean @ConditionalOnMissingBean(RedisTemplate.class) public RedisTemplate redisTemplate() { // Redis配置 return new RedisTemplate<>(); } } ``` ### 4. 配置加密 ```yaml # 敏感配置加密 spring: datasource: password: ENC(encrypted_password) app: secret-key: ENC(encrypted_secret) ``` ### 5. 配置监控 ```java @Component @ConfigurationPropertiesBinding public class ConfigurationChangeListener { @EventListener public void handleConfigChange(EnvironmentChangeEvent event) { log.info("配置发生变化: {}", event.getKeys()); // 处理配置变更逻辑 } } ``` ## 注意事项 ### 1. 线程池配置 * **核心线程数**: 建议设置为 CPU核心数 + 1 * **最大线程数**: 通常设置为核心线程数的 2 倍 * **队列容量**: 根据业务负载调整,避免 OOM * **拒绝策略**: 生产环境建议使用 CallerRunsPolicy ### 2. 虚拟线程使用 * **JDK版本**: 需要 JDK 21 或更高版本 * **适用场景**: IO 密集型任务,不适合 CPU 密集型 * **注意事项**: 避免使用 ThreadLocal,可能影响性能 ### 3. 异步配置 * **异常处理**: 必须配置异常处理器,避免异常丢失 * **线程池**: 避免使用默认线程池,自定义线程池配置 * **事务处理**: 异步方法中事务会失效,需要特殊处理 ### 4. 配置安全 * **敏感信息**: 使用配置加密,不要明文存储密码 * **权限控制**: 限制配置文件的访问权限 * **环境隔离**: 不同环境使用不同的配置文件 ### 5. 性能优化 * **懒加载**: 使用 @Lazy 注解延迟初始化 * **条件注解**: 使用 @ConditionalOn\* 避免不必要的 Bean 创建 * **配置缓存**: 避免重复读取配置文件 ## 扩展配置 ### 自定义配置属性 ```java @Data @Component @ConfigurationProperties(prefix = "custom") public class CustomProperties { private Redis redis = new Redis(); private Security security = new Security(); @Data public static class Redis { private String host = "localhost"; private int port = 6379; private String password; private int database = 0; } @Data public static class Security { private boolean enabled = true; private String[] excludePaths = {}; } } ``` ### 自定义自动配置 ```java @AutoConfiguration @ConditionalOnClass(CustomService.class) @EnableConfigurationProperties(CustomProperties.class) public class CustomAutoConfiguration { @Bean @ConditionalOnMissingBean public CustomService customService(CustomProperties properties) { return new CustomService(properties); } } ``` 通过合理的配置管理,可以确保系统的灵活性、可维护性和性能优化。配置模块作为系统的基础设施,需要重点关注其稳定性和扩展性。 --- --- url: 'https://ruoyi.plus/frontend/architecture/config-management.md' --- # 配置管理 --- --- url: 'https://ruoyi.plus/mobile/api/error-handling.md' --- # 错误处理 --- --- url: 'https://ruoyi.plus/mobile/debug/error-monitoring.md' --- # 错误监控 --- --- url: 'https://ruoyi.plus/frontend/views/error-pages.md' --- # 错误页面 (401/404) --- --- url: 'https://ruoyi.plus/backend/common/ratelimiter.md' --- # 限流功能模块 (ratelimiter) ## 概述 限流功能模块(ruoyi-common-ratelimiter)提供基于Redis和令牌桶算法的分布式限流功能,支持多种限流策略,可以有效防止接口被恶意刷新或超频访问。 ## 核心特性 * **多种限流策略**:支持全局限流、IP限流、集群限流 * **分布式支持**:基于Redis实现跨实例限流 * **令牌桶算法**:使用Redisson的高性能限流算法 * **SpEL表达式**:支持动态key生成,灵活配置限流规则 * **AOP切面**:基于注解的无侵入式使用方式 * **国际化支持**:支持多语言错误提示信息 ## 架构设计 ### 模块依赖 ```xml plus.ruoyi ruoyi-common-core plus.ruoyi ruoyi-common-redis ``` ### 自动配置 模块采用Spring Boot自动配置机制,无需手动配置即可使用: ```java @AutoConfiguration(after = RedisConfiguration.class) public class RateLimiterConfig { @Bean public RateLimiterAspect rateLimiterAspect() { return new RateLimiterAspect(); } } ``` ## 限流类型 ### LimitType 枚举 ```java public enum LimitType { /** * 默认策略全局限流 */ DEFAULT, /** * 根据请求者IP进行限流 */ IP, /** * 实例限流(集群多后端实例) */ CLUSTER } ``` ### 策略说明 | 类型 | 说明 | 适用场景 | |------|------|----------| | DEFAULT | 全局限流,所有请求共享限流配额 | 保护系统资源,控制总体流量 | | IP | 基于客户端IP地址进行独立限流 | 防止恶意攻击,单IP流量控制 | | CLUSTER | 基于集群节点进行限流 | 集群环境负载均衡 | ## 使用方法 ### 基本使用 在需要限流的方法上添加 `@RateLimiter` 注解: ```java @RateLimiter(time = 60, count = 10) public void basicMethod() { // 60秒内最多访问10次 } ``` ### IP限流 ```java @RateLimiter(time = 60, count = 10, limitType = LimitType.IP) public void ipLimitMethod() { // 每个IP在60秒内最多访问10次 } ``` ### 动态Key限流 支持使用SpEL表达式生成动态key: ```java // 基于用户ID限流 @RateLimiter(key = "#userId", time = 60, count = 10) public void userLimitMethod(String userId) { // 每个用户60秒内最多访问10次 } // 复杂SpEL表达式 @RateLimiter(key = "#{#user.id + ':' + #action}", time = 60, count = 5) public void complexKeyMethod(User user, String action) { // 基于用户ID和操作类型的组合限流 } // 模板表达式格式 @RateLimiter(key = "#{#userId}-#{#type}", time = 300, count = 20) public void templateMethod(String userId, String type) { // 使用模板表达式格式 } ``` ### 自定义错误消息 ```java @RateLimiter( time = 60, count = 10, message = "访问过于频繁,请稍后再试" ) public void customMessageMethod() { // 自定义限流提示消息 } // 使用国际化消息 @RateLimiter( time = 60, count = 10, message = "user.login.limit.exceeded" ) public void i18nMessageMethod() { // 使用国际化消息key } ``` ## 注解参数详解 ### @RateLimiter 参数 | 参数 | 类型 | 默认值 | 说明 | |------|------|---------|------| | key | String | "" | 限流缓存key,支持SpEL表达式 | | time | int | 60 | 限流时间窗口,单位秒 | | count | int | 100 | 时间窗口内允许的最大请求次数 | | limitType | LimitType | DEFAULT | 限流类型策略 | | message | String | I18nKeys.Request.RATE\_LIMIT\_EXCEEDED | 限流触发时的提示消息 | | timeout | int | 86400 | 限流策略在Redis中的存活时间,单位秒 | ### 参数配置建议 #### 时间窗口 (time) * **60**:1分钟时间窗口(常用) * **300**:5分钟时间窗口 * **3600**:1小时时间窗口 * **86400**:1天时间窗口 #### 限流次数 (count) * **登录接口**:5-10次/分钟 * **查询接口**:100-1000次/分钟 * **写入接口**:10-50次/分钟 * **敏感操作**:1-5次/分钟 #### 超时时间 (timeout) * **86400**:1天(默认,推荐) * **3600**:1小时(短期限流) * **7200**:2小时 ## SpEL表达式支持 ### 基本用法 限流模块支持丰富的SpEL表达式功能: ```java // 引用方法参数 @RateLimiter(key = "#userId", time = 60, count = 10) public void method1(String userId) { } // 引用对象属性 @RateLimiter(key = "#user.id", time = 60, count = 10) public void method2(User user) { } // 复杂表达式 @RateLimiter(key = "#{#user.id + '-' + #user.type}", time = 60, count = 10) public void method3(User user) { } // 引用Spring Bean @RateLimiter(key = "#{@userService.getCurrentUserId()}", time = 60, count = 10) public void method4() { } ``` ### 表达式格式 支持两种SpEL表达式格式: 1. **简单表达式**:`#variable` 格式 2. **模板表达式**:`#{expression}` 格式 ### 配置文件 SpEL扩展配置(spel-extension.json): ```json { "plus.ruoyi.common.ratelimiter.annotation.RateLimiter@key": { "method": { "parameters": true } } } ``` ## 缓存Key生成规则 ### Key格式 最终生成的完整缓存key格式: ``` rate_limit:请求URI:限流标识:自定义key ``` ### 生成逻辑 1. **基础前缀**:`rate_limit` 2. **请求URI**:实现不同接口独立限流 3. **限流标识**: * IP限流:客户端IP地址 * 集群限流:Redis客户端实例ID * 全局限流:无额外标识 4. **自定义key**:注解中配置的key值 ### 示例 ```java // 假设请求URI为 /api/user/info,客户端IP为 192.168.1.100 @RateLimiter(key = "getUserInfo", limitType = LimitType.IP) public void getUserInfo() { } // 生成的key: rate_limit:/api/user/info:192.168.1.100:getUserInfo @RateLimiter(key = "#userId", limitType = LimitType.DEFAULT) public void updateUser(String userId) { } // 生成的key: rate_limit:/api/user/update:user123 (假设userId="user123") ``` ## 实际应用场景 ### 1. 登录接口保护 ```java @PostMapping("/login") @RateLimiter( time = 300, // 5分钟窗口 count = 5, // 最多5次尝试 limitType = LimitType.IP, message = "登录尝试过于频繁,请5分钟后再试" ) public Result login(@RequestBody LoginRequest request) { // 登录逻辑 } ``` ### 2. 短信发送限流 ```java @PostMapping("/sms/send") @RateLimiter( key = "#phone", // 基于手机号限流 time = 60, // 1分钟窗口 count = 1, // 最多1次 message = "短信发送过于频繁,请1分钟后再试" ) public Result sendSms(@RequestParam String phone) { // 发送短信逻辑 } ``` ### 3. 用户操作限流 ```java @PostMapping("/user/update") @RateLimiter( key = "#{@securityUtils.getUserId()}", // 基于当前用户限流 time = 60, count = 10, message = "操作过于频繁,请稍后再试" ) public Result updateProfile(@RequestBody UserProfile profile) { // 更新用户资料逻辑 } ``` ### 4. API接口保护 ```java @GetMapping("/api/data/export") @RateLimiter( time = 3600, // 1小时窗口 count = 10, // 最多10次导出 limitType = LimitType.IP, message = "导出操作过于频繁,请1小时后再试" ) public Result exportData(@RequestParam String type) { // 数据导出逻辑 } ``` ## 异常处理 ### 限流异常 当触发限流时,会抛出 `ServiceException` 异常: ```java try { // 调用被限流保护的方法 someService.rateLimitedMethod(); } catch (ServiceException e) { // 处理限流异常 log.warn("触发限流: {}", e.getMessage()); return Result.error(e.getMessage()); } ``` ### 全局异常处理 建议在全局异常处理器中统一处理限流异常: ```java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ServiceException.class) public Result handleServiceException(ServiceException e) { return Result.error(e.getMessage()); } } ``` ## 监控与运维 ### 日志记录 限流切面会自动记录限流信息: ``` 限制令牌 => 10, 剩余令牌 => 5, 缓存key => 'rate_limit:/api/user/info:192.168.1.100:getUserInfo' ``` ### Redis监控 可以通过Redis命令监控限流状态: ```bash # 查看所有限流key redis-cli KEYS "rate_limit:*" # 查看特定key的值 redis-cli GET "rate_limit:/api/user/info:192.168.1.100:getUserInfo" # 查看key的过期时间 redis-cli TTL "rate_limit:/api/user/info:192.168.1.100:getUserInfo" ``` ## 最佳实践 ### 1. 合理设置参数 ```java // ✅ 推荐:根据业务特性设置合理的限流参数 @RateLimiter(time = 60, count = 100, limitType = LimitType.IP) public void queryData() { } // ❌ 不推荐:限流过于严格影响正常使用 @RateLimiter(time = 60, count = 1, limitType = LimitType.DEFAULT) public void normalQuery() { } ``` ### 2. 选择合适的限流类型 ```java // ✅ 推荐:敏感操作使用IP限流 @RateLimiter(limitType = LimitType.IP, time = 300, count = 5) public void sensitiveOperation() { } // ✅ 推荐:资源密集型操作使用全局限流 @RateLimiter(limitType = LimitType.DEFAULT, time = 60, count = 10) public void expensiveOperation() { } ``` ### 3. 使用有意义的Key ```java // ✅ 推荐:使用业务相关的key @RateLimiter(key = "#userId", time = 60, count = 10) public void userOperation(String userId) { } // ✅ 推荐:组合多个维度 @RateLimiter(key = "#{#userId + ':' + #operationType}", time = 60, count = 5) public void complexOperation(String userId, String operationType) { } ``` ### 4. 提供友好的错误提示 ```java // ✅ 推荐:提供明确的错误提示 @RateLimiter( time = 60, count = 10, message = "操作过于频繁,请1分钟后重试" ) public void operation() { } // ✅ 推荐:使用国际化消息 @RateLimiter( time = 60, count = 10, message = "rate.limit.user.operation.exceeded" ) public void i18nOperation() { } ``` ## 常见问题 ### Q1: 限流不生效? **可能原因**: 1. Redis连接配置错误 2. 方法调用方式不正确(需要通过Spring代理调用) 3. 注解配置错误 **解决方案**: ```java // ❌ 错误:内部调用不会触发AOP public void method1() { this.method2(); // 不会触发限流 } @RateLimiter(time = 60, count = 10) public void method2() { } // ✅ 正确:通过Spring容器调用 @Autowired private SomeService someService; public void method1() { someService.method2(); // 会触发限流 } ``` ### Q2: SpEL表达式不生效? **可能原因**: 1. 表达式语法错误 2. 参数名获取失败 3. Bean引用错误 **解决方案**: ```java // ✅ 正确的SpEL表达式格式 @RateLimiter(key = "#userId") // 简单参数引用 @RateLimiter(key = "#user.id") // 对象属性引用 @RateLimiter(key = "#{#userId + ':' + #type}") // 复杂表达式 @RateLimiter(key = "#{@userService.getCurrentUserId()}") // Bean方法调用 ``` ### Q3: 集群环境限流不准确? **可能原因**: 1. Redis配置不一致 2. 系统时间不同步 3. 限流类型选择错误 **解决方案**: 1. 确保所有节点使用相同的Redis配置 2. 同步各节点系统时间 3. 根据需求选择正确的限流类型 ## 总结 RuoYi-Plus-Uniapp限流功能模块提供了完善的分布式限流解决方案,具有以下优势: * **简单易用**:基于注解的使用方式,无侵入式集成 * **功能强大**:支持多种限流策略和灵活配置 * **性能优秀**:基于Redis和令牌桶算法,高性能低延迟 * **扩展性好**:支持SpEL表达式和自定义配置 通过合理使用限流功能,可以有效保护系统资源,提升应用的稳定性和安全性。 --- --- url: 'https://ruoyi.plus/mobile/debug/integration-testing.md' --- # 集成测试 --- --- url: 'https://ruoyi.plus/frontend/router/breadcrumb.md' --- # 面包屑导航 --- --- url: 'https://ruoyi.plus/frontend/router/page-cache.md' --- # 页面缓存 --- --- url: 'https://ruoyi.plus/frontend/components/page-background.md' --- # 页面背景 (APageBackground) --- --- url: 'https://ruoyi.plus/mobile/uniapp/pages-config.md' --- # 页面配置 (pages.json) --- --- url: 'https://ruoyi.plus/frontend/layout/navbar.md' --- # 顶部导航 (Navbar) --- --- url: 'https://ruoyi.plus/backend/project-structure.md' --- # 项目结构 ```text 📁 ruoyi-plus-uniapp (项目根目录) ├── 📁 ruoyi-admin // 系统入口模块,打包部署的主模块 ├── 📁 ruoyi-common // 通用工具模块,提供各种基础功能支持 │ ├── 📁 ruoyi-common-bom // 通用依赖项管理 - 定义所有模块的统一版本 │ ├── 📁 ruoyi-common-core // 核心模块 - 提供系统基础功能与通用工具类 │ ├── 📁 ruoyi-common-doc // 系统接口文档模块 - 提供API文档自动生成 │ ├── 📁 ruoyi-common-encrypt // 数据加解密模块 - 提供数据加密手段与工具 │ ├── 📁 ruoyi-common-excel // Excel处理模块 - 基于EasyExcel提供导入导出功能 │ ├── 📁 ruoyi-common-idempotent // 幂等功能模块 - 提供接口幂等性保障 │ ├── 📁 ruoyi-common-job // 定时任务模块 - 基于SnailJob提供分布式任务调度 │ ├── 📁 ruoyi-common-json // 序列化模块 - 提供JSON序列化与反序列化 │ ├── 📁 ruoyi-common-log // 日志记录模块 - 提供系统操作日志记录功能 │ ├── 📁 ruoyi-common-mail // 邮件模块 - 提供邮件发送与模板处理功能 │ ├── 📁 ruoyi-common-miniapp // 微信小程序模块 │ ├── 📁 ruoyi-common-mp // 微信公众号服务模块 │ ├── 📁 ruoyi-common-mybatis // 数据库服务模块 - 提供ORM映射与数据访问功能 │ ├── 📁 ruoyi-common-oss // 对象存储服务模块 - 提供文件上传、下载等功能 │ ├── 📁 ruoyi-common-pay // 支付模块 │ ├── 📁 ruoyi-common-ratelimiter // 限流功能模块 - 提供接口访问频率限制 │ ├── 📁 ruoyi-common-redis // 缓存服务模块 - 提供Redis缓存、分布式锁等功能 │ ├── 📁 ruoyi-common-satoken // 权限认证模块 - 基于Sa-Token提供认证授权 │ ├── 📁 ruoyi-common-security // 安全模块 - 提供应用安全防护与加密功能 │ ├── 📁 ruoyi-common-sensitive // 脱敏模块 - 提供数据脱敏与隐私保护功能 │ ├── 📁 ruoyi-common-serialmap // 序列化映射模块 - 数据映射功能+字段通用映射+缓存 │ ├── 📁 ruoyi-common-sms // 短信模块 - 提供短信发送与验证码功能 │ ├── 📁 ruoyi-common-social // 社会化登录模块 - 提供第三方平台登录功能 │ ├── 📁 ruoyi-common-sse // SSE通讯模块 - 提供服务器发送事件功能 │ ├── 📁 ruoyi-common-tenant // 租户宿主模块 - 提供多租户隔离与管理功能 │ ├── 📁 ruoyi-common-web // Web服务模块 - 提供Web应用功能组件 │ └── 📁 ruoyi-common-websocket // WebSocket通讯模块 - 提供实时通讯功能 ├── 📁 ruoyi-extend // 扩展增强模块 │ ├── 📁 ruoyi-monitor-admin // 监控管理模块 - 基于Spring Boot Admin │ └── 📁 ruoyi-snailjob-server // 任务调度服务模块 - SnailJob提供任务调度中心 ├── 📁 ruoyi-modules // 业务功能模块 │ ├── 📁 ruoyi-business // 业务模块 - 新增的业务逻辑写此模块 里面划分子模块 │ │ └── 📁 src │ │ └── 📁 main │ │ ├── 📁 java │ │ │ └── 📁 plus.ruoyi.business │ │ │ ├── 📁 api // API接口层 │ │ │ │ ├── 📁 mobile // 移动端API │ │ │ │ ├── 📁 pay // 支付相关API │ │ │ │ └── 📁 pc // PC端API │ │ │ ├── 📁 base // 基础业务服务 │ │ │ │ ├── 📁 authStrategy // 小程序、公众号认证策略 │ │ │ │ ├── 📁 controller // 基础业务控制器 │ │ │ │ ├── 📁 domain // 基础业务领域模型 │ │ │ │ ├── 📁 mapper // 基础业务数据访问 │ │ │ │ └── 📁 service // 基础业务服务 │ │ │ ├── 📁 job // 任务调度模块 │ │ │ └── 📁 mall // 商城领域 │ │ │ ├── 📁 controller // 商城控制器 │ │ │ ├── 📁 domain // 商城领域模型 │ │ │ ├── 📁 listener // 商城事件监听器 │ │ │ ├── 📁 mapper // 商城数据访问层 │ │ │ └── 📁 service // 商城服务层 │ │ └── 📁 resources │ │ └── 📁 mapper // MyBatis XML映射文件 │ │ ├── 📁 base // 基础业务映射 │ │ └── 📁 mall // 商城相关映射 │ ├── 📁 ruoyi-generator // 代码生成模块 - 提供可视化代码生成功能 │ └── 📁 ruoyi-system // 系统模块 - 提供用户、角色、菜单等系统核心功能 │ └── 📁 src │ └── 📁 main │ ├── 📁 java │ │ └── 📁 plus.ruoyi.system │ │ ├── 📁 auth // 认证授权模块 - 登录验证 │ │ ├── 📁 config // 配置模块 - 系统配置管理、通知公告 │ │ ├── 📁 core // 核心功能模块 - 核心RBAC业务逻辑 │ │ ├── 📁 dict // 数据字典模块 - 字典数据管理 │ │ ├── 📁 monitor // 系统监控模块 - 在线用户、服务监控 │ │ ├── 📁 oss // 对象存储模块 - 文件上传下载管理 │ │ └── 📁 tenant // 多租户模块 - 租户管理与数据隔离 │ └── 📁 resources // 资源文件目录 │ └── 📁 mapper // MyBatis XML映射文件 └── 📄 pom.xml // Maven主配置文件 ``` ## 项目架构特点 ### 🏗️ **模块化设计** * **分层清晰**: admin(入口层) → modules(业务层) → common(通用层) * **职责明确**: 每个模块都有明确的功能边界和职责划分 * **松耦合**: 模块间通过接口依赖,便于独立开发和维护 ### ⚡ **通用模块 (ruoyi-common)** * **基础设施**: core、web、mybatisplus 等核心功能 * **中间件集成**: redis、oss、pay、sms 等服务组件 * **安全保障**: security、encrypt、sensitive 等安全模块 * **通讯机制**: sse、websocket 等实时通讯 * **业务支撑**: excel、mail、social 等业务工具 ### 🔧 **业务模块 (ruoyi-modules)** * **系统管理**: 用户、角色、权限、租户等核心RBAC功能 * **业务扩展**: business 模块支持多领域业务开发 * **代码生成**: 可视化代码生成,提升开发效率 ### 📊 **扩展模块 (ruoyi-extend)** * **系统监控**: MonitorAdmin - 基于 Spring Boot Admin 的服务监控 * **任务调度**: SnailJobServer - 分布式任务调度服务 --- --- url: 'https://ruoyi.plus/frontend/project-structure.md' --- # 项目结构 ```text 📁 plus-ui/ ├── 📁 env/ # 源代码目录 │ ├── 📄 .env # 共同环境配置 │ ├── 📄 .env.development # 开发环境配置 │ ├── 📄 .env.production # 生产环境配置 ├── 📁 public/ # 静态资源目录 ├── 📁 src/ # 源代码目录 │ ├── 📁 api/ # API 接口管理 │ │ ├── 📁 business # 业务相关接口 │ │ ├── 📁 system # 系统相关接口 │ │ └── 📁 tool # 代码生成接口 │ │ │ ├── 📁 assets/ # 静态资源 │ │ ├── 📁 icons/ # 图标资源 │ │ ├── 📁 images/ # 图片资源 │ │ └── 📁 styles/ # 样式文件 │ │ │ ├── 📁 components/ # 全局组件 │ │ ├── 📁 ADataCard/ # 数据卡片组件 │ │ ├── 📁 ADetailDialog/ # 通用详情弹窗组件 │ │ ├── 📁 AForm/ # A表单组件库 │ │ │ ├── 📄 AFormCascader.vue # 级联选择表单组件 │ │ │ ├── 📄 AFormCheckbox.vue # 复选框表单组件 │ │ │ ├── 📄 AFormDate.vue # 日期选择表单组件 │ │ │ ├── 📄 AFormEditor.vue # 富文本编辑器组件 │ │ │ ├── 📄 AFormFileUpload.vue # 文件上传表单组件 │ │ │ ├── 📄 AFormImgUpload.vue # 图片上传表单组件 │ │ │ ├── 📄 AFormInput.vue # 输入框表单组件 │ │ │ ├── 📄 AFormRadio.vue # 单选框表单组件 │ │ │ ├── 📄 AFormSelect.vue # 下拉选择表单组件 │ │ │ ├── 📄 AFormSwitch.vue # 开关表单组件 │ │ │ └── 📄 AFormTreeSelect.vue # 树形选择表单组件 │ │ ├── 📁 AImportExcel/ # Excel导入组件 │ │ ├── 📁 AOssMediaManager/ # OSS媒体管理组件 │ │ ├── 📁 APageBackground/ # 背景组件 │ │ ├── 📁 ARecharge/ # 在线充值组件 │ │ ├── 📁 ASearchForm/ # 通用搜索表单组件 │ │ ├── 📁 ASelectionTags/ # 可选中的标签组件 │ │ ├── 📁 DictTag/ # 字典标签组件 │ │ ├── 📁 EnhancedIFrame/ # 增强的 IFrame 组件 │ │ ├── 📁 Icon/ # 图标组件 │ │ ├── 📁 ImagePreview/ # 图片预览组件 │ │ ├── 📁 Pagination/ # 分页组件 │ │ ├── 📁 TableToolbar/ # 右侧工具栏组件 │ │ └── 📁 UserSelect/ # 用户树形选择组件 │ │ │ ├── 📁 composables/ # 组合式函数(Vue 3 组合函数) │ │ ├── 📄 useAnimation.ts # 动画效果组合函数 │ │ ├── 📄 useAuth.ts # 权限相关组合函数 │ │ ├── 📄 useDialog.ts # 对话框管理组合函数 │ │ ├── 📄 useDict.ts # 字典数据组合函数 │ │ └── 📄 useDownload.ts # 文件下载组合函数 │ │ ├── 📄 useHttp.ts # HTTP 请求组合函数 │ │ ├── 📄 useI18n.ts # 国际化组合函数 │ │ ├── 📄 useSelection.ts # 表格选择组合函数 │ │ ├── 📄 useSSE.ts # SSE 连接组合函数 │ │ ├── 📄 useTableHeight.ts # 表格高度组合函数 │ │ ├── 📄 useTheme.ts # 主题管理组合函数 │ │ ├── 📄 useToken.ts # Token 管理组合函数 │ │ └── 📄 useWS.ts # WebSocket 组合函数 │ │ │ ├── 📁 directives/ # 自定义指令 │ │ └── 📄 directives.ts # 指令统一注册 │ │ └── 📄 permission.ts # 灵活的权限控制指令 │ │ │ ├── 📁 layouts/ # 布局组件 │ │ ├── 📁 components/ # 布局相关组件 │ │ │ ├── 📁 AppMain/ # 主内容区组件 │ │ │ │ ├── 📁 iframe/ # iframe 相关组件 │ │ │ │ │ ├── 📄 IframeToggle.vue # iframe 切换组件 │ │ │ │ │ └── 📄 InnerLink.vue # 内部链接组件 │ │ │ │ ├── 📄 AppMain.vue # 主内容区入口组件 │ │ │ │ └── 📄 ParentView.vue # 父级视图组件 │ │ │ │ │ │ │ ├── 📁 Navbar/ # 导航栏组件 │ │ │ │ ├── 📁 tools/ # 导航栏工具组件 │ │ │ │ │ ├── 📄 DocLink.vue # 文档链接组件 │ │ │ │ │ ├── 📄 FullscreenToggle.vue # 全屏切换组件 │ │ │ │ │ ├── 📄 GitLink.vue # Git 链接组件 │ │ │ │ │ ├── 📄 LangSelect.vue # 语言选择组件 │ │ │ │ │ ├── 📄 NavbarSearch.vue # 导航栏搜索组件 │ │ │ │ │ ├── 📄 Notice.vue # 通知组件 │ │ │ │ │ ├── 📄 SizeSelect.vue # 尺寸选择组件 │ │ │ │ │ ├── 📄 TenantSelect.vue # 租户选择组件 │ │ │ │ │ └── 📄 UserDropdown.vue # 用户下拉菜单组件 │ │ │ │ ├── 📄 Breadcrumb.vue # 面包屑导航组件 │ │ │ │ ├── 📄 Hamburger.vue # 汉堡菜单组件 │ │ │ │ ├── 📄 Navbar.vue # 导航栏主组件 │ │ │ │ └── 📄 TopNav.vue # 顶部导航组件 │ │ │ │ │ │ │ ├── 📁 Settings/ # 设置组件 │ │ │ │ └── 📄 Settings.vue # 设置主组件 │ │ │ │ │ │ │ ├── 📁 Sidebar/ # 侧边栏组件 │ │ │ │ ├── 📄 AppLink.vue # 应用链接组件 │ │ │ │ ├── 📄 Logo.vue # Logo 组件 │ │ │ │ ├── 📄 Sidebar.vue # 侧边栏主组件 │ │ │ │ └── 📄 SidebarItem.vue # 侧边栏菜单项组件 │ │ │ │ │ │ │ └── 📁 TagsView/ # 标签页组件 │ │ │ ├── 📄 ScrollPane.vue # 滚动面板组件 │ │ │ └── 📄 TagsView.vue # 标签页主组件 │ │ │ │ │ ├── 📄 HomeLayout.vue # 首页布局组件 其他布局组件 │ │ └── 📄 Layout.vue # 主布局入口 │ │ │ ├── 📁 locales/ # 国际化资源 │ │ ├── 📄 en_US.ts # 英文语言包 │ │ ├── 📄 zh_CN.ts # 中文语言包 │ │ └── 📄 i18n.ts # 国际化配置 │ │ │ ├── 📁 plugins/ # 插件配置 │ │ └── 📄 elementIcons.ts # Element Plus 图标全局注册插件 │ │ │ ├── 📁 router/ # 路由配置 │ │ ├── 📁 modules/ # 路由模块分离 │ │ │ ├── 📄 constant.ts # 常量路由配置 │ │ │ ├── 📄 system.ts # 系统模块路由 │ │ │ └── 📄 tool.ts # 工具模块路由 │ │ ├── 📁 utils/ # 路由工具函数 │ │ │ └── 📄 createCustomNameComponent.tsx # 自定义组件名称创建工具 │ │ └── 📄 guard.ts # 路由守卫 │ │ └── 📄 router.ts # 路由主配置入口 │ │ │ ├── 📁 stores/ # Pinia 状态管理 │ │ ├── 📁 modules/ # 状态管理模块 │ │ │ ├── 📄 dict.ts # 字典数据状态管理 │ │ │ ├── 📄 notice.ts # 通知公告状态管理 │ │ │ ├── 📄 permission.ts # 权限状态管理 │ │ │ ├── 📄 state.ts # 应用状态管理 │ │ │ ├── 📄 tagsView.ts # 标签页状态管理 │ │ │ ├── 📄 theme.ts # 主题状态管理 │ │ │ └── 📄 user.ts # 用户状态管理 │ │ └── 📄 store.ts # 状态管理统一入口 │ │ │ ├── 📁 types/ # 类型定义 │ │ │ ├── 📁 utils/ # 工具函数库 │ │ ├── 📄 boolean.ts # 布尔值相关方法 │ │ ├── 📄 cache.ts # 缓存相关方法 │ │ ├── 📄 class.ts # DOM 操作相关方法 │ │ ├── 📄 crypto.ts # 加密解密方法 │ │ ├── 📄 date.ts # 日期相关方法 │ │ ├── 📄 format.ts # 格式化相关方法 │ │ ├── 📄 function.ts # 函数相关方法(防抖、节流等) │ │ ├── 📄 modal.ts # 模态框相关方法 │ │ ├── 📄 object.ts # 对象相关方法 │ │ ├── 📄 string.ts # 字符串相关方法 │ │ ├── 📄 tab.ts # 标签页导航操作方法 │ │ ├── 📄 to.ts # 安全异步执行工具函数 │ │ ├── 📄 tree.ts # 树形结构相关方法 │ │ ├── 📄 validators.ts # 表单验证相关方法 │ │ └── 📄 index.ts # 工具函数统一导出 │ │ │ ├── 📁 views/ # 页面视图 │ │ ├── 📁 business/ # 业务模块 │ │ │ ├── 📁 base/ # 基础业务模块 │ │ │ └── 📁 mall/ # 商城业务模块 │ │ │ │ │ ├── 📁 common/ # 通用页面 │ │ │ ├── 📄 401.vue # 401 未授权页面 │ │ │ ├── 📄 404.vue # 404 页面未找到 │ │ │ ├── 📄 home.vue # 主页 │ │ │ ├── 📄 index.vue # 首页 │ │ │ └── 📄 redirect.vue # 页面重定向 │ │ │ │ │ ├── 📁 system/ # 系统管理模块 │ │ │ ├── 📁 auth/ # 认证管理 │ │ │ ├── 📁 config/ # 参数配置 │ │ │ ├── 📁 core/ # 核心系统功能 │ │ │ ├── 📁 dict/ # 字典管理 │ │ │ ├── 📁 monitor/ # 系统监控 │ │ │ ├── 📁 oss/ # 对象存储管理 │ │ │ └── 📁 tenant/ # 租户管理 │ │ │ │ │ └── 📁 tool/ # 系统工具 │ │ │ ├── 📄 App.vue # 根组件 │ ├── 📄 main.ts # 应用入口 │ └── 📄 systemConfig.ts # 系统配置 │ └── 📁 vite # 插件目录 │ ├── 📄 .gitignore # Git 忽略文件 ├── 📄 index.html # HTML 模板 ├── 📄 package.json # 项目依赖配置 ├── 📄 tsconfig.json # TypeScript 配置 ├── 📄 uno.config.ts # UnoCSS 配置 ├── 📄 vite.config.ts # Vite 构建配置 └── 📄 README.md # 项目说明文档 ``` --- --- url: 'https://ruoyi.plus/mobile/project-structure.md' --- # 项目结构 ```text 📁 plus-uniapp (项目根目录) ├── 📁 dist // 构建输出目录 ├── 📁 env // 环境配置 │ ├── 📄 .env // 基础环境配置 │ ├── 📄 .env.development // 开发环境配置 │ └── 📄 .env.production // 生产环境配置 ├── 📁 node_modules // 依赖包目录 ├── 📁 src // 源码目录 │ ├── 📁 api // API 接口管理 │ │ ├── 📁 business // 业务相关接口 │ │ └── 📁 system // 系统相关接口 │ │ │ ├── 📁 components // 全局组件 │ │ │ ├── 📁 composables // 组合式函数 │ │ ├── 📄 useAuth.ts // 权限相关组合函数 │ │ ├── 📄 useDict.ts // 字典相关组合函数 │ │ ├── 📄 useHttp.ts // HTTP 请求组合函数 │ │ ├── 📄 usePayment.ts // 支付相关组合函数 │ │ ├── 📄 useScroll.ts // 滚动相关组合函数 │ │ ├── 📄 useTheme.ts // 主题相关组合函数 │ │ └── 📄 useToken.ts // Token 管理组合函数 │ │ │ ├── 📁 layouts // 布局组件 │ │ └── 📄 default.vue // 默认布局 │ │ │ ├── 📁 pages // 页面文件 │ │ ├── 📁 auth // 认证相关页面 │ │ │ ├── 📁 components // 认证页面组件 │ │ │ │ └── 📄 AuthModal.vue // 认证模态框组件 │ │ │ ├── 📄 auth.vue // 认证页面 │ │ │ ├── 📄 login.vue // 登录页面 │ │ │ ├── 📄 phoneLogin.vue // 手机登录页面 │ │ │ ├── 📄 register.vue // 注册页面 │ │ │ └── 📄 smsVerify.vue // 短信验证页面 │ │ │ │ │ ├── 📁 tabbar // 底部导航页面 │ │ │ ├── 📁 components // 底部导航组件 │ │ │ │ ├── 📄 index.vue // 底部导航主组件 │ │ │ │ ├── 📄 Menu.vue // 菜单组件 │ │ │ │ └── 📄 My.vue // 我的页面组件 │ │ │ └── 📄 index.vue // 底部导航入口 │ │ │ ├── 📁 static // 静态资源 │ │ ├── 📁 app // 应用相关静态资源 │ │ ├── 📁 images // 图片资源 │ │ ├── 📁 style // 样式文件 │ │ └── 📄 logo.png // Logo 图片 │ │ │ ├── 📁 stores // Pinia 状态管理 │ │ ├── 📁 modules // 状态模块 │ │ │ ├── 📄 dict.ts // 字典状态管理 │ │ │ ├── 📄 tabbar.ts // 底部 │ │ │ └── 📄 user.ts // 用户状态管理 │ │ └── 📄 store.ts // 状态管理入口 │ │ │ ├── 📁 subpackages // 分包目录 │ │ ├── 📁 admin // 管理员分包 │ │ └── 📁 demo // 示例代码分包 │ │ │ ├── 📁 uni_modules // UniApp 模块 │ │ │ ├── 📁 utils // 工具函数 │ │ ├── 📄 boolean.ts // 布尔值相关工具 │ │ ├── 📄 cache.ts // 缓存相关工具 │ │ ├── 📄 crypto.ts // 加密相关工具 │ │ ├── 📄 date.ts // 日期相关工具 │ │ ├── 📄 format.ts // 格式化相关工具 │ │ ├── 📄 function.ts // 函数相关工具 │ │ ├── 📄 platform.ts // 平台相关工具 │ │ ├── 📄 route.ts // 路由相关工具 │ │ ├── 📄 rsa.ts // RSA 加密工具 │ │ ├── 📄 string.ts // 字符串相关工具 │ │ ├── 📄 tenant.ts // 租户相关工具 │ │ ├── 📄 to.ts // 安全异步执行工具 │ │ └── 📄 validators.ts // 表单验证工具 │ │ │ └── 📁 wd // WotUI 重构组件库相关 │ ├── 📄 App.vue // 应用入口组件 │ ├── 📄 main.ts // 应用入口文件 │ ├── 📄 manifest.json // 应用配置清单 │ ├── 📄 pages.json // 页面路由配置 │ ├── 📄 systemConfig.ts // 系统配置 │ └── 📄 uni.scss // 全局样式 │ ├── 📁 vite // 插件目录 │ ├── 📄 .gitignore // Git 忽略文件配置 ├── 📄 eslint.config.mjs // ESLint 配置 ├── 📄 package.json // 项目依赖配置 ├── 📄 pnpm-lock.yaml // 依赖锁定文件 ├── 📄 prettier.config.js // Prettier 配置 ├── 📄 README.md // 项目说明文档 ├── 📄 tsconfig.json // TypeScript 配置 ├── 📄 unocss.config.ts // UnoCSS 配置 └── 📄 vite.config.ts // Vite 构建配置 ``` --- --- url: 'https://ruoyi.plus/mobile/uniapp/manifest-config.md' --- # 项目配置 (manifest.json) --- --- url: 'https://ruoyi.plus/mobile/pages.md' --- # 首页 (index) --- --- url: 'https://ruoyi.plus/frontend/views/dashboard.md' --- # 首页仪表板 --- --- url: 'https://ruoyi.plus/frontend/utils/validators.md' --- # 验证器 --- --- url: 'https://ruoyi.plus/mobile/utils/validate.md' --- # 验证工具 --- --- url: 'https://ruoyi.plus/mobile/platform/harmony.md' --- # 鸿蒙适配 --- --- url: 'https://ruoyi.plus/mobile/layouts/default.md' --- # 默认布局 (default)