Compare commits

..

183 Commits

Author SHA1 Message Date
疯狂的狮子Li
c17948510c update 重构 增强代码生成器各项功能
update 自动类型判断可根据不同数据库精确识别 对应的java类型
update 优化java字段名与数据库名不匹配则增加别名注解
update 增加 是否导出 是否状态切换 是否组合唯一校验 是否排序调整 和树结构相关字段等功能选择
update 优化自定义路径导出 导出全部代码
update 删除无用主子表相关代码
2026-04-17 18:24:04 +08:00
疯狂的狮子Li
983b393d3e update 优化 重构抽出一些常用hooks组件简化页面编码 2026-04-17 15:46:40 +08:00
疯狂的狮子Li
e0033702b0 update 优化 重构抽出一些常用hooks组件简化页面编码 2026-04-17 15:09:16 +08:00
疯狂的狮子Li
5025afb032 update 优化 菜单勾选栏改为左菜单右按钮结构 增加禁用与隐藏图标 2026-04-17 12:08:50 +08:00
lau
2aefff69c8 update 角色菜单列表树接口返回按钮列表 2026-04-16 21:17:31 +08:00
AprilWind
a017f870e6 update 使用 StreamUtils 优化任务处理器列表转换 2026-04-16 20:07:03 +08:00
疯狂的狮子Li
26fe8b17c6 [重大更新] 将所有通用Service接口与实体类 提炼为 ruoyi-api 模块 更通用更易扩展 2026-04-16 18:41:03 +08:00
疯狂的狮子Li
d96dcd2580 update 优化 删除无用系统参数 2026-04-16 16:22:15 +08:00
疯狂的狮子Li
981743da00 update 优化 客户端管理 增加白名单路径和白名单IP功能 可限制客户端能访问的具体路径与可访问的具体IP地址 2026-04-16 14:14:25 +08:00
疯狂的狮子Li
a5e8951bcd fix 修复 访问接口文档报错问题 2026-04-16 09:44:16 +08:00
lau
ef9ae0f2e8 update 角色基础信息修改接口地址改回原样 2026-04-15 20:45:02 +08:00
lau
5057e905f4 update 角色管理菜单权限-数据权限放到一起 2026-04-15 20:39:51 +08:00
疯狂的狮子Li
1c147c20ef fix 修复 代码生成模板小问题 2026-04-14 15:49:23 +08:00
疯狂的狮子Li
5793e9a368 fix 修复 AUTO_PASS 变量取值错误 2026-04-10 17:53:39 +08:00
疯狂的狮子Li
d9cbba6b7b update 优化 YmlPropertySourceFactory 代码警告问题 2026-04-10 15:01:18 +08:00
疯狂的狮子Li
347e536a61 update 适配 根据导入数据源指定代码生成模板 2026-04-10 14:41:27 +08:00
疯狂的狮子Li
096a6a1245 update 适配代码生成模板 2026-04-10 14:41:03 +08:00
lau
620c059cab update 代码生成器模块只在gen环境引入 2026-04-10 14:17:34 +08:00
疯狂的狮子Li
78a06c8c9f update 优化代码写法 2026-04-09 17:12:46 +08:00
疯狂的狮子Li
b019a95bbe update 优化 全局异常拦截器 不返回具体的异常内容到前端页面 避免信息泄漏问题 2026-04-09 14:18:56 +08:00
疯狂的狮子Li
5e9aacd4ba add 增加 用户管理 用户详情展示抽屉 2026-04-09 13:18:06 +08:00
疯狂的狮子Li
9c25addb46 update 优化 对数据库表常用字段增加索引 提高查询效率 2026-04-09 10:46:57 +08:00
疯狂的狮子Li
563a397033 update 优化 截断token 避免日志输出具体token内容 防止盗用隐患 2026-04-09 10:34:11 +08:00
疯狂的狮子Li
42ff890830 update 优化 RepeatSubmitAspect.KEY_CACHE 清理不彻底 2026-04-09 10:33:36 +08:00
疯狂的狮子Li
f689f07bad fix 修复 ExcelBigNumberConvert 导入没法转换数据问题 2026-04-08 16:27:16 +08:00
疯狂的狮子Li
39c8105202 fix 修复 字段长度写反问题 2026-04-08 15:56:28 +08:00
疯狂的狮子Li
cb7ce69bc0 update 统一框架所有主键id均使用雪花id 2026-04-08 14:55:14 +08:00
疯狂的狮子Li
a74eb4046f update 修改sql文件命名 2026-04-08 13:21:11 +08:00
疯狂的狮子Li
7e0b172de0 update 优化 操作日志 补齐一些必要的记录数据 2026-04-08 12:40:10 +08:00
疯狂的狮子Li
5b6b9c617a update 优化 菜单管理 增加激活菜单与扩展属性字段 2026-04-08 11:49:53 +08:00
疯狂的狮子Li
3b523e5936 update 优化 mpj 只处理继承了MPJBaseMapper的类其他类不处理 优化启动效率 2026-04-07 16:44:29 +08:00
疯狂的狮子Li
4d3b32c570 fix 修复 部门vo子部门属性使用错误 2026-04-07 09:16:23 +08:00
疯狂的狮子Li
b337e0ef98 update 优化 翻译模块 增加逻辑注释和相关警告日志 2026-04-03 16:44:17 +08:00
疯狂的狮子Li
b2e07254f9 Revert "update 优化操作日志"
This reverts commit bcc11dcc12.
2026-04-03 07:07:03 +00:00
AprilWind
bcc11dcc12 update 优化操作日志 2026-04-03 14:49:46 +08:00
疯狂的狮子Li
ef3fa714bb update 优化 LocalDateTime 任意时间格式传参序列化 2026-04-03 13:44:29 +08:00
疯狂的狮子Li
d9115291bc update 优化 调整代码生成器时间类型 2026-04-03 12:22:01 +08:00
疯狂的狮子Li
0403aac1a4 update 优化 调整代码生成器时间类型 2026-04-03 12:11:02 +08:00
疯狂的狮子Li
22ed3454ba fix 修复 下拉数据字符大于255报错问题 2026-04-03 11:28:39 +08:00
疯狂的狮子Li
b9489d7406 [重大更新] 应广大用户要求 将Date换成LocalDateTime 2026-04-03 10:59:04 +08:00
疯狂的狮子Li
e20e31b5f8 update 优化 工作流类别翻译器支持批量翻译 2026-04-02 11:35:43 +08:00
疯狂的狮子Li
f8ebeaa01a update 优化 工作流类别翻译器支持批量翻译 2026-04-02 11:26:36 +08:00
疯狂的狮子Li
00a5d5c59f update 优化 已办任务列表去除抄送任务 2026-04-02 11:16:17 +08:00
疯狂的狮子Li
bb166c0742 update 优化 删除一些无用方法 2026-04-02 11:01:27 +08:00
疯狂的狮子Li
568547ada5 update 优化一些性能问题 2026-04-01 10:13:42 +08:00
疯狂的狮子Li
d11990bfd8 update 优化 项目中的一些存在null的问题 与一些性能问题 小优化 2026-03-31 18:54:43 +08:00
疯狂的狮子Li
26464c0051 update 优化 翻译处理器 避免多次序列化 实现类增加缓存避免重复解析 2026-03-31 18:00:01 +08:00
疯狂的狮子Li
eb850fb8cf update 优化 代码生成器方法 2026-03-31 17:58:18 +08:00
疯狂的狮子Li
37e1ed0bf3 fix 修复 DeptExcelConverter 存在的内存泄漏问题 与优化部分代码 2026-03-31 17:54:41 +08:00
RealXin
ec9e98096f !840 update 更新gitignore文件
* update 更新gitignore文件
2026-03-30 20:08:12 +08:00
疯狂的狮子Li
f1ef471c68 update 优化 缩短代码生成模块 包名与模块名 2026-03-30 20:02:43 +08:00
AprilWind
a6c21ac7d7 update 增加部门Excel转换处理和下拉选项数据源 2026-03-30 17:46:33 +08:00
疯狂的狮子Li
19abd82fa3 add 增加 ai编程支持 支持codex与claude的skill 2026-03-30 17:31:31 +08:00
疯狂的狮子Li
175a02f49c fix 修复 distinct 在 sqlserver 中的限制 补缺排序字段 确保语法正确 2026-03-30 15:21:37 +08:00
疯狂的狮子Li
a57694bdff update 优化 将全局继承MPJ改为按需求继承 2026-03-30 15:14:22 +08:00
AprilWind
920d717cd0 update 增加消息推送模块注释 2026-03-30 14:37:30 +08:00
疯狂的狮子Li
8300f65640 [重大更新] 重写翻译和脱敏实现 使用jackson tree解析加ResponseBodyAdvice处理数据的方案 实现可批量翻译大幅度提高效率 用法与灵活性不变 2026-03-30 14:23:35 +08:00
AprilWind
a1f8df90cf update 使用常量替代硬编码的删除标志,优化查询条件 2026-03-30 09:34:22 +08:00
疯狂的狮子Li
ee2ee27cf1 update 修改接口路径与cloud版本保持一致 2026-03-27 18:26:36 +08:00
疯狂的狮子Li
776d85ae15 update 优化 后端导入返回信息使用\n分割 避免前端出现xss问题 2026-03-27 16:43:31 +08:00
疯狂的狮子Li
58f1e2ba25 update 优化 oss 模块代码实现 2026-03-27 16:02:56 +08:00
疯狂的狮子Li
5f53681b95 update 工作流消息推送增加前端路由跳转 2026-03-27 15:20:39 +08:00
疯狂的狮子Li
60b6862c9e update 完成消息盒子功能前后端联动(已读未读在前端浏览器存储) 2026-03-27 14:37:20 +08:00
秋辞未寒
66c23b1dc4 update 文件上传支持可选项 2026-03-27 00:46:16 +08:00
疯狂的狮子Li
7722f4f685 update 优化 通知公告页面增加查看详情功能 支持预览已经编辑好的富文本内容
update 优化 消息盒子相关消息可直接跳转到详情页展示富文本内容
2026-03-26 18:05:59 +08:00
疯狂的狮子Li
029f6a4c11 update 重构 common-sse 与 common-websocket 合并为 ruoyi-common-push 推送模块 2026-03-26 17:25:36 +08:00
疯狂的狮子Li
40011e9acd update 消息推送增加 消息类型 消息来源 前端跳转路径等扩展参数 2026-03-26 15:34:02 +08:00
疯狂的狮子Li
fa8e1cd3c0 update 删除无用字段 2026-03-26 14:33:18 +08:00
秋辞未寒
f45f58c340 fix oss文件下载 2026-03-26 00:19:44 +08:00
AprilWind
e00837a26f update 修复手机号和邮箱格式校验 2026-03-25 20:47:31 +08:00
lau
d177eef25e fix 个人信息性别字段名修正 2026-03-25 20:42:33 +08:00
AprilWind
bc827570e4 update 增加手机号和邮箱格式校验 2026-03-25 17:44:39 +08:00
疯狂的狮子Li
214cefd259 update 优化 缩短oss模块命名 2026-03-25 11:40:18 +08:00
疯狂的狮子Li
d2cdc694f1 update 优化 缩短oss模块命名 2026-03-25 11:34:39 +08:00
lau
e95d1e3f33 fix 修改 oss配置切换时状态修改错误 2026-03-25 10:55:59 +08:00
疯狂的狮子Li
961793689f fix 修复 登录日志 路由名书写错误 2026-03-25 10:50:39 +08:00
疯狂的狮子Li
16da1eb30e fix 修复 登录日志 路由名书写错误 2026-03-25 10:42:24 +08:00
秋辞未寒
3540a1b94f style 格式化代码 2026-03-24 23:48:15 +08:00
秋辞未寒
8cadea285a remove 移除 旧的S3客户端
update 更新 文件上传使用新的S3客户端
2026-03-24 23:42:51 +08:00
AprilWind
8809ca2343 update 优化安全相关工具类,增加sm2验签 2026-03-24 14:07:10 +08:00
疯狂的狮子Li
1b7012e01f update bcpkix-jdk18on 1.80 => 1.83 2026-03-24 11:24:02 +08:00
疯狂的狮子Li
77fc3d37d8 update 优化 代码生成模块 2026-03-24 10:49:16 +08:00
秋辞未寒
fe8d15c6a8 refactor 重构代码生成工具链,复用代码模板以提升代码生成性能 2026-03-24 01:56:34 +08:00
疯狂的狮子Li
d547f68adc update 重新梳理readme说明 2026-03-23 16:35:20 +08:00
秋辞未寒
2dc289979f update 更新 完善新的S3客户端相关配置 2026-03-21 17:08:29 +08:00
秋辞未寒
aa38a7b98a update 更新 新的S3客户端支持初始化和刷新配置 2026-03-21 08:31:09 +08:00
秋辞未寒
160b501b37 Merge remote-tracking branch 'plus/futuer/boot4' into futuer/boot4 2026-03-21 00:41:31 +08:00
gssong
68cb82d050 fix 修复个人信息手机号码参数错误 2026-03-20 11:46:26 +08:00
gssong
7c57a2317f fix 修复保存个人信息手机号码参数错误 2026-03-20 11:36:25 +08:00
gssong
a25d87c44d Merge remote-tracking branch 'origin/futuer/boot4' into futuer/boot4 2026-03-20 10:18:17 +08:00
gssong
3294f45968 fix 修复时间转化异常 2026-03-20 10:18:09 +08:00
秋辞未寒
222f3657d0 update 提供一个自定义的S3存储客户端配置的校验方法,由调用者决定校验的结果 2026-03-19 22:40:53 +08:00
秋辞未寒
c94005e1ba update 重命名 字符串构建器,避免与标准库的字符串构建器有歧义 2026-03-19 22:40:50 +08:00
秋辞未寒
c64d611500 rebuild 重构 新的OSS客户端 2026-03-19 22:40:42 +08:00
疯狂的狮子Li
62bb368ca7 update 补全缺失sql 2026-03-19 15:14:57 +08:00
疯狂的狮子Li
a9f25e7139 update 补全缺失sql 2026-03-19 13:38:25 +08:00
疯狂的狮子Li
696ea5e0ac Merge remote-tracking branch 'origin/dev' into futuer/boot4
# Conflicts:
#	ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwDefinitionController.java
#	ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwInstanceController.java
#	ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java
#	script/sql/oracle/oracle_ry_workflow.sql
#	script/sql/postgres/postgres_ry_workflow.sql
#	script/sql/ry_workflow.sql
#	script/sql/sqlserver/sqlserver_ry_workflow.sql
2026-03-19 13:30:46 +08:00
疯狂的狮子Li
b8b75e087e update 优化 代码 2026-03-19 09:44:57 +08:00
疯狂的狮子Li
de5aa32e1d update 优化 代码 2026-03-19 09:42:36 +08:00
疯狂的狮子Li
96f2e8ac41 fix 修复 sql命名错误 2026-03-19 09:40:38 +08:00
AprilWind
f1c2f0d458 update 优化部分导入语句,修改List<Long>为Collection<Long> 2026-03-19 09:29:19 +08:00
疯狂的狮子Li
7db9749718 Revert "feat 新特性! 支持列表翻译~"
This reverts commit d9aa8484ed.
2026-03-19 01:23:19 +00:00
秋辞未寒
d9aa8484ed feat 新特性! 支持列表翻译~ 2026-03-19 01:42:26 +08:00
疯狂的狮子Li
b67cd76e04 update 优化 将 CacheConstants 合并到 CacheNames 2026-03-18 14:28:01 +08:00
AprilWind
7bc2315c64 update 优化响应信息 2026-03-18 13:47:49 +08:00
疯狂的狮子Li
afdc8366f2 update 优化 修改接口权限标识符 2026-03-18 13:36:44 +08:00
AprilWind
bcdbd7ec3a update 还原在线日志解锁功能 2026-03-18 13:30:57 +08:00
AprilWind
b4917a88b2 update 添加用户解锁功能 2026-03-18 13:18:30 +08:00
AprilWind
72ac989614 update 优化使用 SystemConstants 替代硬编码的状态值 2026-03-18 13:07:41 +08:00
疯狂的狮子Li
4b3fa35993 Merge remote-tracking branch 'origin/dev' into futuer/boot4 2026-03-18 11:45:07 +08:00
疯狂的狮子Li
d30e16cdd9 Merge remote-tracking branch 'origin/dev' into futuer/boot4 2026-03-18 11:38:42 +08:00
疯狂的狮子Li
b214817d0b update 重构 修改框架内不正常命名与规范是和否的状态 2026-03-18 10:39:06 +08:00
lau
7bbbcb2a1e fix 代码生成params时添加导包 2026-03-18 00:04:15 +08:00
lau
fd4071e257 update 代码生成查询选择between时生成params 2026-03-17 22:40:44 +08:00
lau
eb68d06c79 fix 修复代码生成RepeatSubmit注解导包错误 2026-03-17 22:36:00 +08:00
疯狂的狮子Li
a44a01ea16 update 重构 所有bo对象移除继承BaseEntity 2026-03-17 21:41:11 +08:00
疯狂的狮子Li
0203b69fa6 update 重构 所有bo对象移除继承BaseEntity 2026-03-17 21:34:12 +08:00
疯狂的狮子Li
9aed0b06ca update 回滚错误的修改 record无法投递到redis序列化 2026-03-17 21:04:24 +08:00
疯狂的狮子Li
fcfa5eb767 update 优化 使用 record 简化实体类编码 2026-03-17 19:52:46 +08:00
YueYe
13aae27579 !837 [mod]分页查询接口使用R类型返回:数据放入data中
* [mod]将PageResult从common-mybatis挪到common-core中
* [mod]TableDataInfo修改为PageResult
* [mod]分页查询接口使用R类型返回:数据放入data中
2026-03-17 11:15:55 +00:00
疯狂的狮子Li
01da77ba17 update 将官网菜单放到最下面 2026-03-17 14:58:40 +08:00
疯狂的狮子Li
c0f1614bc4 update 同步代码生成模板 2026-03-17 13:31:56 +08:00
疯狂的狮子Li
d5aa4c607a update 同步代码生成模板 2026-03-17 13:24:57 +08:00
疯狂的狮子Li
ebc170df12 fix 修复 mapper执行报错问题 2026-03-17 13:09:35 +08:00
疯狂的狮子Li
05d3634d18 Merge remote-tracking branch 'origin/dev' into futuer/boot4 2026-03-16 18:09:48 +08:00
疯狂的狮子Li
48992b574d update 优化 统一补全代码注释 2026-03-13 19:36:14 +08:00
疯狂的狮子Li
916282ba68 [重大更新] 集成 mybatis-plus-join 重构项目代码(实验性功能不稳定) 2026-03-13 17:46:23 +08:00
秋辞未寒
068e2de831 update 更新 统一枚举相关包名为enums 2026-03-13 16:01:12 +08:00
秋辞未寒
e81e250fee Merge remote-tracking branch 'plus/futuer/boot4' into futuer/boot4 2026-03-13 15:44:20 +08:00
秋辞未寒
1ecf22d61e update 优化 使用动态规划优化菜单树的构建 2026-03-13 15:43:19 +08:00
秋辞未寒
81adb94e54 add 新增注解类工具 2026-03-13 15:39:47 +08:00
疯狂的狮子Li
bdbcad7099 fix 修复 git出现问题导致文件名修改错误 2026-03-13 15:35:47 +08:00
疯狂的狮子Li
2a4dbdd974 update 优化 规范DTO命名 2026-03-13 14:38:42 +08:00
疯狂的狮子Li
40ea2e55bb update 优化 将logininfor规范化为loginInfo 2026-03-13 14:28:54 +08:00
疯狂的狮子Li
771494d0a4 update 删除无用sql 2026-03-13 14:03:40 +08:00
疯狂的狮子Li
08ec5b3f49 update 优化 多次处理性能问题增加缓存 2026-03-12 15:14:56 +08:00
疯狂的狮子Li
4db032e190 Merge remote-tracking branch 'origin/dev' into futuer/boot4 2026-03-12 14:20:36 +08:00
疯狂的狮子Li
4c5f52d47e [重大更新] 数据权限增加角色与菜单关联 实现 角色->菜单->数据权限 控制数据权限功能(实验性功能不稳定) 2026-03-12 14:20:02 +08:00
秋辞未寒
0a42df2ab2 update sa-token 1.44.0 => 1.45.0
update springdoc 3.0.1 => 3.0.2
update spring-boot-admin 4.0.1 => 4.0.2
update redisson 4.2.0 => 4.3.0
update ip2region 3.3.4 => 3.3.6
update aws-sdk 2.41.34 => 2.42.9
update anyline 8.7.3-20260202 => 8.7.3-20260306
update maven-surefire-plugin 3.5.4 => 3.5.5
2026-03-10 16:54:26 +08:00
疯狂的狮子Li
5d17aa1857 update 优化 构建命令 2026-03-09 18:27:04 +08:00
疯狂的狮子Li
5642159803 Merge remote-tracking branch 'origin/dev' into futuer/boot4
# Conflicts:
#	pom.xml
#	ruoyi-admin/src/main/resources/application.yml
#	ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java
2026-03-06 11:04:56 +08:00
秋辞未寒
39781fbe7f add 添加 maven-wrapper 以支持项目构建环境的一致性 2026-02-21 21:12:36 +08:00
秋辞未寒
819234610c update spring-boot 4.0.1 => 4.0.3
update spring-boot-admin 3.5.6 => 4.0.1
update redisson 4.1.0 => 4.2.0
update bcprov 1.80 => 1.83
update aws-sdk 2.28.22 => 2.41.34
update anyline 8.7.3-20251210 => 8.7.3-20260202
update maven-jar-plugin 3.4.2 => 3.5.0
update maven-war-plugin 3.4.0 => 3.5.1
update maven-compiler-plugin 3.14.0 => 3.15.0
update maven-surefire-plugin 3.5.3 => 3.5.4
update flatten-maven-plugin 1.3.0 => 1.7.3
2026-02-21 21:09:22 +08:00
秋辞未寒
391b674945 update 优化模块pom.xml结构格式 2026-02-21 21:01:10 +08:00
AprilWind
ea32d9c4c8 update 将 application.yml 中的 biz-thread-pool-size 重命名为 mqtt-executor-size 2026-02-12 13:41:45 +08:00
秋辞未寒
4fb9858190 update FastExcel升级迁移至Apache Fesod 2026-02-11 14:09:20 +08:00
疯狂的狮子Li
3b5d7eba37 update 优化 mqtt 客户端 增加虚拟线程支持 2026-02-10 16:32:50 +08:00
疯狂的狮子Li
529f614dae Merge remote-tracking branch 'origin/dev' into futuer/boot4
# Conflicts:
#	pom.xml
2026-02-10 13:03:48 +08:00
疯狂的狮子Li
ac6fe13bcf update 重构 将 idempotent 与 ratelimiter 模块统一合并到 redis 模块 降级模块使用复杂度 2026-02-10 11:59:59 +08:00
AprilWind
4468656a97 update 优化mqtt模块依赖 2026-02-06 14:31:26 +08:00
Kayleigh Wang
310bffef54 !830 fix 修复删除租户功能后SQL执行异常
* fix 修复删除租户功能后SQL执行异常
2026-02-05 07:24:41 +00:00
秋辞未寒
240a581311 update 更新 RedisConfig 的 Jackson 反序列化安全验证策略与旧版本保持一致 2026-01-30 00:33:50 +08:00
疯狂的狮子Li
6c1eef7ff4 update 优化 RedisConfig 的 jackson配置写法 2026-01-29 17:55:00 +08:00
疯狂的狮子Li
efae1c914b Revert "update 使用Spring Redis Data自动配置项优化Redisson 4.X配置流程"
This reverts commit 7c3a5c4a1d.
2026-01-29 09:41:17 +00:00
秋辞未寒
7c3a5c4a1d update 使用Spring Redis Data自动配置项优化Redisson 4.X配置流程
update 优化Redisson自定义配置,Json序列化依赖升级至Jackson3.X
2026-01-29 17:13:19 +08:00
疯狂的狮子Li
3797d9b8ed update 优化 使用虚拟线程优化查询速度 2026-01-23 14:56:05 +08:00
疯狂的狮子Li
59e0e6ab95 🧨🧨🧨发布 5.5.3 版本 提前祝大家新年快乐 2026-01-23 14:54:04 +08:00
疯狂的狮子Li
c2da068482 update springboot 3.5.9 => 3.5.10
update springdoc 2.8.14 => 2.8.15
update mybatis-plus 3.5.14 => 3.5.16
update hutool 5.8.40 => 5.8.43
update spring-boot-admin 3.5.5 => 3.5.6
2026-01-23 14:53:39 +08:00
Coast
8d19744cbc !824 update 优化 oss 依赖注释说明
* update 优化 oss 依赖注释说明
2026-01-22 18:03:42 +08:00
疯狂的狮子Li
32dba43ec4 update 优化 oss 依赖注释说明 2026-01-22 18:03:42 +08:00
疯狂的狮子Li
8666815963 fix 修复 不同类别菜单的判断逻辑有误问题 2026-01-22 18:03:42 +08:00
疯狂的狮子Li
eec3c44866 update README.md 2026-01-22 18:03:42 +08:00
疯狂的狮子Li
fb631283ca fix 修复 按钮菜单 不应该校验路由的问题 2026-01-22 18:03:42 +08:00
疯狂的狮子Li
9c0636978f update 优化 自行实现更漂亮的验证码图案 2026-01-22 18:03:42 +08:00
疯狂的狮子Li
fc00210a39 update 修改验证码默认样式 2026-01-22 18:03:42 +08:00
疯狂的狮子Li
ff299cea86 fix 修复 顶节点判断条件缺失 2026-01-22 18:03:42 +08:00
羡民Coding
7ffd918338 !822 fix: 文案错误
* fix: 文案错误
2026-01-22 18:03:42 +08:00
疯狂的狮子Li
19ef199da7 update 优化 StringUtils 过期的方法 2026-01-22 18:02:14 +08:00
疯狂的狮子Li
b8571e9ca1 update 优化 增加线程工具简化虚拟线程语法 2026-01-22 18:01:45 +08:00
疯狂的狮子Li
660757cb71 update 优化 使用虚拟线程优化查询速度 2026-01-22 11:27:58 +08:00
疯狂的狮子Li
4072b080fe update 优化 增加mqtt模块配置开关 2026-01-22 09:33:40 +08:00
疯狂的狮子Li
161b52d8d7 update 优化 mqtt-server 文档搭建说明 2026-01-16 15:44:42 +08:00
疯狂的狮子Li
befabc61de update 优化 mqtt-server 文档搭建说明 2026-01-16 15:42:02 +08:00
疯狂的狮子Li
5b82c12e17 add 增加 ruoyi-common-mqtt 模块 2026-01-16 15:39:10 +08:00
疯狂的狮子Li
145b903185 [重大更改] 移除多租户相关功能 2026-01-13 16:14:52 +08:00
疯狂的狮子Li
55098339d4 update 优化 兼容path大写开头搜索 2026-01-12 09:28:02 +08:00
疯狂的狮子Li
469274d9b1 update 优化 大家都认可用"账"统一改为账 2026-01-12 09:28:02 +08:00
疯狂的狮子Li
596e83701a update 优化 添加菜单路由地址和名称的校验规则 2026-01-12 09:28:02 +08:00
疯狂的狮子Li
8a87c7aa4e update 优化 添加菜单路由地址和名称的校验规则 2026-01-12 09:28:01 +08:00
疯狂的狮子Li
b4467aa8e9 update 优化 统一用词 2026-01-12 09:28:01 +08:00
AprilWind
5de3114f05 update 优化oss日志侦听器打印级别 2026-01-12 09:28:01 +08:00
ColorDreams
8d4fdd9fc8 update ip2region version to 3.3.2 2026-01-12 09:28:01 +08:00
疯狂的狮子Li
2f4e89ee42 update 不兼容整体升级 springboot 4.X
update springboot 3.5 => 4.0
update springdoc 2.8 => 3.0
update mybatis-plus 3.5.14 => 3.5.15
update redisson 3.52.0 => 4.1.0
update dynamic-ds 4.3.1 => 4.5.0
2026-01-06 17:18:08 +08:00
677 changed files with 20814 additions and 16881 deletions

View File

@@ -0,0 +1,56 @@
---
name: backend-crud
description: 标准后端 CRUD 专家。用于当前项目中的新增单表 CRUD、补 entity/bo/vo/mapper/service/controller、分页查询、导出、删除前校验等任务。
---
你负责当前项目中的标准后端 CRUD 实现。
## 核心原则
1. 先参考 `ruoyi-modules/ruoyi-gen/src/main/resources/vm/` 下的模板。
2. 再参考当前模块内最近似的标准管理模块。
3. 分层保持稳定:
`domain``domain.bo``domain.vo``mapper``service``service.impl``controller`
## 结构约定
- entity 默认继承 `BaseEntity`
- mapper 默认继承 `BaseMapperPlus<Entity, Vo>`
- BO 使用 `@AutoMapper(target = Entity.class, reverseConvertGenerate = false)`
- VO 使用 `@AutoMapper(target = Entity.class)`
- service 使用 `baseMapper`
## 默认方法集合
- `queryById`
- `queryPageList`
- `queryList`
- `insertByBo`
- `updateByBo`
- `deleteWithValidByIds`
## 查询规则
- 单表查询优先用 `LambdaQueryWrapper`
- 日期范围默认从 `bo.getParams()` 中读取 begin/end
- 分页优先返回 `PageResult<Vo>`
## 接口规则
- controller 继承 `BaseController`
- 返回值使用 `R<T>``R<Void>`
- 标准 CRUD 路由通常是:
`GET /list`
`POST /export`
`GET /{id}`
`POST`
`PUT`
`DELETE /{ids}`
- 默认检查是否需要 `@SaCheckPermission``@Log``@RepeatSubmit`
## 自检
- CRUD 链路是否完整
- BO / VO / Entity 是否职责分离
- 导出、分页、删除前校验是否齐全
- 是否只是 generator 裸产物,如果是要继续补齐项目约定

View File

@@ -0,0 +1,19 @@
---
name: backend-engineering
description: 后端工程总入口。用于在当前项目中识别任务属于标准 CRUD、复杂模块增强、联表与数据权限、或前后端联动并选择合适的后端子 agent。
---
你是当前后端工程的总入口 agent。
先判断任务类型,再按下面规则处理:
1. 如果是新增标准单表 CRUD、从表结构补 entity/bo/vo/mapper/service/controller优先使用 `backend-crud.md` 的规则。
2. 如果是修改 `system``workflow` 等已经很复杂的模块,优先使用 `backend-module-enhancement.md` 的规则。
3. 如果重点在 MPJ 联表、`@DataPermission`、复杂查询、数据范围控制,优先使用 `backend-query-permission.md` 的规则。
4. 如果同时要求同步前端接口或前端页面骨架,保持后端路由与 generator 风格稳定,便于前端 agent 对接。
通用要求:
- 先读同模块最近似实现,再动代码。
- 发生冲突时优先相信当前模块真实代码,其次是公共基础设施,再其次才是 generator 模板。
- 默认直接产出可落地代码,而不是只给抽象建议。

View File

@@ -0,0 +1,33 @@
---
name: backend-module-enhancement
description: 复杂后端模块增强专家。用于修改当前项目中已经存在较重业务逻辑的模块,例如 system、workflow 等,强调增量修改、保留现有权限、事务、缓存、导入导出和业务校验。
---
你负责复杂后端模块的增量增强,不是从零生成裸 CRUD。
## 核心原则
1. 优先阅读当前模块最近似实现。
2. 增量修改,不重写整块 service/controller。
3. 保留已有的数据权限、事务、缓存、导入导出、唯一性校验、删除前校验。
4. 不能为了“简洁”把复杂模块退化成模板式单表 CRUD。
## 常见任务
- 修改 `system``workflow` 模块的查询与导出逻辑
- 新增或调整写入前校验
- 维护角色、岗位、用户等关联数据
- 增加复杂页面所需的特殊接口
## 约束
- controller 不堆重业务逻辑
- service 里的旧逻辑要先理解再改
- 如果附近已有 `ServiceException`、缓存注解、事务注解、数据权限判断,新增逻辑默认保持一致
- 如果存在联动前端页面,接口路径与返回结构尽量稳定
## 自检
- 是否破坏了原模块的权限边界
- 是否误删了旧逻辑中的事务或校验
- 是否错误简化了复杂关系维护

View File

@@ -0,0 +1,28 @@
---
name: backend-query-permission
description: 后端查询、联表与数据权限专家。用于当前项目中的 MPJ 联表、DataPermission、复杂分页查询、范围控制和查询增强任务。
---
你负责当前项目中的复杂查询和数据权限类任务。
## 核心原则
1. 优先看当前模块已有的 mapper 查询实现。
2. 涉及数据权限时优先复用 `@DataPermission` 与已有字段映射方式。
3. 复杂联表优先参考 MPJ 风格,不轻易改回手写零散 SQL。
4. 如果 `BaseMapperPlus + wrapper` 足够,不要额外补 XML。
## 重点关注
- `BaseMapperPlus`
- `@DataPermission`
- `DataColumn`
- `MPJBaseMapper`
- `JoinWrappers.lambda(...)`
- 复杂分页与列表查询
## 输出要求
- 明确说明查询是单表、联表还是带权限控制
- 保持与当前模块 mapper 风格一致
- 不要让查询参数风格和前端现有调用脱节

View File

@@ -0,0 +1,152 @@
---
name: ruoyi-plus-ai-coding
description: 在仓库内按代码生成器模板和项目既有约定生成或修改代码。用于新增 CRUD 模块、补全 controller/service/mapper/BO/VO/entity、编写 MyBatis-Plus 查询,以及新增与后端接口配套的 Vue 3 + TypeScript 页面、types 和 api 文件。
---
# RuoYi Plus AI 编码规范
先对齐代码生成器产物,再叠加仓库里真实业务代码已经形成的更强约定。
## 适用场景
在下面这些任务里优先使用此 skill
- 新增标准 CRUD 模块。
- 根据新表结构补齐 entity、bo、vo、mapper、service、controller。
- 修改已有模块的查询、校验、导入导出、数据权限、事务逻辑。
- 在系统、监控、工作流、demo 等模块内按现有约定扩展业务代码。
- 为后端新增接口同步补前端 `api/types/index.vue` 骨架。
## 不适用场景
下面这些任务不要机械套用本 skill 的 CRUD 规则:
- 基础框架升级、Spring Boot 主版本迁移。
- 与当前分层明显不同的实验性模块。
- 第三方中间件深度接入、基础设施改造。
- 完全脱离 generator 体系的独立子系统。
## 执行流程
1. 先确认目标模块,优先复用同模块中最近似功能的写法。
2. 新增标准 CRUD 代码前,先读取 `ruoyi-modules/ruoyi-gen/src/main/resources/vm/` 下的模板。
3. 命名和分层保持与仓库一致:
`domain` entity、`domain.bo``domain.vo``mapper``service``service.impl``controller`
4. 优先在生成器结构上扩展,不要自行发明新的分层。
5. 修改 `ruoyi-system` 这类复杂模块前,先阅读同类现有实现,因为这些模块通常比生成器默认产物多出数据权限、联表、缓存、安全校验等逻辑。
## 优先级规则
发生冲突时按下面顺序决策:
1. 当前模块内最近似业务代码。
2. 当前仓库公共基础模块约定,例如 `common-mybatis``common-core``common-web`
3. 代码生成器模板。
4. 通用 Spring Boot / MyBatis-Plus 习惯。
也就是说:
- 同模块已有成熟实现时,优先复用该实现。
- 同模块没有现成代码时,再参考 generator 模板。
- 不要因为“更通用”就覆盖掉项目已形成的强约定。
## 后端规则
Java、MyBatis-Plus、BO/VO/entity、controller、mapper、service 的具体规则见 [references/backend.md](references/backend.md)。
## 前端规则
Vue 3、TypeScript API 文件、生成式列表页、表单状态、字典和日期范围约定见 [references/frontend.md](references/frontend.md)。
## 使用案例
具体调用方式见 [references/examples.md](references/examples.md)。
## 仓库通用规则
- 遵循 [`.editorconfig`](../../../.editorconfig)UTF-8、LF默认 4 空格JSON/YAML 为 2 空格。
- 不要把 `BaseMapperPlus``PageQuery``PageResult``R``MapstructUtils` 或项目工具类替换成临时自造方案。
- 仓库已使用 `List.of(...)` 的地方,数组转列表优先继续沿用。
- import、注解顺序、文件结构以附近代码为准不要顺手重排整个文件。
- 只有在业务逻辑不直观时才加简短注释。
## 决策规则
- 如果任务是围绕单表的标准 CRUD尽量贴近生成器默认产物。
- 如果目标模块已经存在自定义校验、数据权限、事务、缓存、Excel 导入导出、联表查询等逻辑,应在此基础上扩展,不要为了“简洁”把它们削平。
- 如果附近 controller 接口已经带有权限、日志、防重、加密、分组校验等注解,新接口默认同步保持一致,除非有明确理由不这样做。
- 如果 BO 或 VO 需要字段校验、翻译、Excel 注解,应优先参考同模块同用途对象,不要机械套通用注解。
## 目录映射规则
标准后端模块通常按下面结构组织:
- `src/main/java/.../domain/Entity.java`
- `src/main/java/.../domain/bo/EntityBo.java`
- `src/main/java/.../domain/vo/EntityVo.java`
- `src/main/java/.../mapper/EntityMapper.java`
- `src/main/java/.../service/IEntityService.java`
- `src/main/java/.../service/impl/EntityServiceImpl.java`
- `src/main/java/.../controller/EntityController.java`
标准生成器模板通常对应:
- `vm/java/domain.java.vm` -> entity
- `vm/java/bo.java.vm` -> bo
- `vm/java/vo.java.vm` -> vo
- `vm/java/mapper.java.vm` -> mapper
- `vm/java/service.java.vm` -> service interface
- `vm/java/serviceImpl.java.vm` -> service impl
- `vm/java/controller.java.vm` -> controller
- `vm/xml/mapper.xml.vm` -> 自定义 XML mapper 起点
## 任务分型
### 1. 标准单表 CRUD
优先按 generator 模板落骨架,再补校验、权限、导出、翻译等项目约定。
### 2. 强业务模块扩展
如果目标模块像 `system``workflow` 一样已经有复杂逻辑,优先增量修改,不要回退成模板式简化代码。
### 3. 基础能力复用
如果涉及数据权限、缓存、事务、导入导出、字典、翻译、加密、分组校验,优先查项目已有做法并复用公共能力。
## 输出要求
使用本 skill 时,默认期望产出应满足:
- 后端分层完整,不直接在 controller 里堆业务逻辑。
- `BO/VO/Entity` 职责分明。
- 查询、分页、删除校验、写入校验逻辑闭环完整。
- 权限、日志、防重、事务、数据权限尽量贴近同模块现有实现。
- 如果同步改前端,前端 API 路径和后端接口保持一致。
## 快速检查清单
- 包路径和 `@RequestMapping` 与模块保持一致。
- 权限标识遵循 `${module}:${business}:${action}`
- Mapper 继承 `BaseMapperPlus<Entity, Vo>`
- Service 使用 `baseMapper`,并按场景返回 `PageResult``List<Vo>`
- 查询代码优先使用 `LambdaQueryWrapper`,复杂模块沿用既有 MPJ 联表风格。
- BO 使用 `@AutoMapper(target = Entity.class, reverseConvertGenerate = false)`
- VO 使用 `@AutoMapper(target = Entity.class)`
- 前端 API 路径与后端路由完全对应。
- 前端列表页继续使用仓库里的 `proxy?.addDateRange``proxy?.$modal``proxy?.download``useDict``pagination` 等工具。
## 推荐提问方式
推荐把任务描述到下面这个粒度:
- 目标模块和业务名
- 是新建模块还是修改已有模块
- 表名或接口前缀
- 是否需要分页、导出、导入、数据权限、字典、翻译、联表
- 希望参考哪个现有模块
例如:
- 使用 `$ruoyi-plus-ai-coding``system` 模块新增一个标准单表 CRUD参考 `SysConfig` 与 generator 模板。
- 使用 `$ruoyi-plus-ai-coding` 修改 `workflow/category` 的查询和导出逻辑,保持现有模块风格。

View File

@@ -0,0 +1,7 @@
interface:
display_name: "RuoYi Plus 编码"
short_description: "按生成器与仓库约定编写代码"
default_prompt: "使用 $ruoyi-plus-ai-coding 在这个仓库里按现有约定实现代码修改。"
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,221 @@
# 后端约定
## 优先参考的代码来源
- `ruoyi-modules/ruoyi-gen/src/main/resources/vm/java/*.vm`
- `ruoyi-modules/ruoyi-demo/...`
- `ruoyi-modules/ruoyi-system/...`
- `ruoyi-common/ruoyi-common-mybatis/...`
## 决策顺序
写代码时按下面顺序取样:
1. 当前业务模块下最近似实现。
2. 当前仓库公共能力模块中的统一约定。
3. generator 模板。
4. 通用 Spring / MyBatis-Plus 默认习惯。
如果规则冲突,优先相信当前仓库真实代码。
## 分层结构
标准 CRUD 代码应优先遵循下面这套结构:
- `domain/Entity.java`
- `domain/bo/EntityBo.java`
- `domain/vo/EntityVo.java`
- `mapper/EntityMapper.java`
- `service/IEntityService.java`
- `service/impl/EntityServiceImpl.java`
- `controller/EntityController.java`
## Entity 规则
- 除非所在模块明显另有约定,否则实体类继承 `org.dromara.common.mybatis.core.domain.BaseEntity`
- 使用 Lombok `@Data``@EqualsAndHashCode(callSuper = true)`
- 使用 `@TableName("table_name")`
- 主键使用 `@TableId`
- 存在 `delFlag` 时保留 `@TableLogic`,存在乐观锁字段时保留 `@Version`
- 如果附近实体已经使用 `@OrderBy` 等额外注解,应继续保持。
## BO 规则
- 实现 `Serializable`
- 添加 `@AutoMapper(target = Entity.class, reverseConvertGenerate = false)`
- 请求专用字段、查询专用字段放在 BO 中,包括 `params`
- 在生成器或附近代码已有分组校验时,继续使用:`AddGroup``EditGroup``QueryGroup`
- `@Xss``@Email``@Size``@NotBlank``@NotNull` 要按真实业务语义添加,不要一股脑全套上。
- 查询存在日期范围或扩展条件时,保留 `params = new HashMap<>()`
## VO 规则
- 实现 `Serializable`
- 添加 `@AutoMapper(target = Entity.class)`
- 生成器风格的导出对象通常带 `@ExcelIgnoreUnannotated`
- `@ExcelProperty``@ExcelDictFormat``ExcelDictConvert``@ExcelRequired``@ExcelNotation``@DateTimeFormat` 只在导入导出场景下使用。
- 如果附近代码会把 ID 翻译成展示字段,沿用 `@Translation(type = TransConstant.USER_ID_TO_NAME, mapper = "createBy")` 这类写法。
- 展示型派生字段放在 VO不放在 Entity。
## Mapper 规则
- 默认形式是 `interface XxxMapper extends BaseMapperPlus<Xxx, XxxVo>`
- 不要为简单的 entity 转 vo 手写重复代码,优先依赖 `BaseMapperPlus`
- 模块已经使用 `@DataPermission` 时,在重写方法和自定义查询上继续保留。
- 复杂模块里 mapper 可能同时继承 `MPJBaseMapper<Entity>` 并使用 `JoinWrappers.lambda(...)`,遇到这种风格要延续,不要换一种写法。
- 只有在 `selectVoList/selectVoPage` 不够用时,才补 XML 或自定义 mapper 方法。
### Mapper 建议结构
标准 mapper 一般按这个顺序组织:
1. 接口声明
2. 默认查询方法
3. 自定义分页或列表方法
4. 特殊数据权限重写
5. 辅助构造方法
### 什么时候需要 XML
- 复杂联表 SQL 无法仅靠 wrapper 清晰表达时。
- 需要手写查询列和结果映射时。
- 项目当前模块已经大量使用 XML 时。
如果 `BaseMapperPlus + wrapper` 已足够,优先不要补 XML。
## Service 规则
- 类声明通常是 `@RequiredArgsConstructor``@Service`,按需补 `@Slf4j`
- mapper 注入字段命名为 `private final XxxMapper baseMapper;`
- 读操作通常返回 `Vo``List<Vo>``PageResult<Vo>`
- BO 转实体用 `MapstructUtils.convert(bo, Entity.class)`
- 查询条件优先用 `LambdaQueryWrapper``Wrappers.lambdaQuery()`
- 在 wrapper 条件里直接写 `StringUtils.isNotBlank(...)` 和 null 判断。
- 分页查询优先采用:
`Page<Vo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);`
`return PageResult.build(result.getRecords(), result.getTotal());`
- 生成器风格模块保留 `validEntityBeforeSave(...)` 这种扩展点。
- 多表写操作使用 `@Transactional(rollbackFor = Exception.class)`
- 明确的业务失败,尤其是权限、数据完整性、删除校验,使用 `ServiceException`
- 不要绕过模块现有的数据权限、角色校验、删除前校验。
### Service 建议结构
标准 service impl 一般按下面顺序组织:
1. 查询单条
2. 分页查询
3. 列表查询
4. 构建查询条件
5. 新增
6. 修改
7. 保存前校验
8. 删除前校验与删除
9. 其他扩展业务方法
### 查询逻辑建议
- 单表查询优先使用 `LambdaQueryWrapper`
- 条件判断直接放在 wrapper 上,不要额外写大量 if 套壳。
- 日期范围统一从 `bo.getParams()` 取 begin/end。
- 复杂联表查询优先查同模块是否已有 MPJ 风格可复用。
### 写入逻辑建议
- BO 转实体统一走 `MapstructUtils.convert`
- 批量关系维护时优先拆成私有方法,例如角色、岗位、用户关联。
- 修改前优先保留已有防误删、防越权、防并发覆盖逻辑。
## Controller 规则
- 继承 `BaseController`
- 类上通常带 `@Validated``@RestController``@RequiredArgsConstructor``@RequestMapping`
- 返回值使用 `R<T>``R<Void>`
- 标准 CRUD 接口通常是:`GET /list``POST /export``GET /{id}``POST``PUT``DELETE /{ids}`
- `@SaCheckPermission` 权限格式遵循 `${module}:${business}:${action}`
- 写操作、导入导出接口通常加 `@Log(title = "...", businessType = BusinessType.X)`
- 附近接口已有防重时,写接口继续使用 `@RepeatSubmit`
- 适合分组校验时,使用 `@Validated(AddGroup.class)``@Validated(EditGroup.class)`
- 特殊接口直接复用模块内现成做法,例如导入导出、`@ApiEncrypt`、multipart 上传、数据权限检查、写入前唯一性校验。
### Controller 建议结构
标准 controller 一般按下面顺序组织:
1. 列表
2. 导出
3. 详情
4. 新增
5. 修改
6. 删除
7. 特殊接口
### Controller 边界
- controller 负责接参、校验、权限、日志、返回值转换。
- 重业务逻辑尽量放 service不要在 controller 里堆长逻辑。
- 但前置权限检查、唯一性提示、显式业务失败提示可以留在 controller前提是同模块已有这种习惯。
## 查询与工具规则
- 分页统一使用 `PageQuery``PageResult`,不要无故引入新的分页 DTO。
- 优先使用项目工具类:`MapstructUtils``StringUtils``StreamUtils``ValidatorUtils``SpringUtils``RedisUtils`
- 数组转列表按附近代码习惯使用 `List.of(ids)``Arrays.asList(ids)`
- 日期范围查询通常从 `bo.getParams()` 中读取 `beginTime``endTime``beginFieldName``endFieldName`
## 前后端联动规则
- 新增后端接口时,路径和权限前缀尽量保持 generator 约定,方便前端目录和 API 命名同步。
- 新增日期范围查询时,记得保留 `bo.params` 结构,避免前端 `addDateRange` 无法对接。
- 导出接口通常保持 `POST /export` 风格,便于前端直接复用现有下载逻辑。
- 批量删除接口通常使用 `DELETE /{ids}`,便于前端直接传数组或逗号串。
## 生成器优先模式
从零新增 CRUD 时,优先对齐生成器默认方法集合:
- `queryById`
- `queryPageList`
- `queryList`
- `insertByBo`
- `updateByBo`
- `deleteWithValidByIds`
然后再叠加模块内已有增强,例如:
- 唯一性校验
- 数据权限注解
- MPJ 联表查询
- 缓存注解
- Excel 导入导出监听器
- 关联表维护逻辑
## 什么时候优先看 generator
- 新增一个标准单表 CRUD 时。
- 只有表结构和基本接口需求,没有现成业务模块可参考时。
- 需要快速补齐整套骨架代码时。
## 什么时候优先看现有模块
- 目标模块已经有类似业务。
- 涉及数据权限、联表、缓存、角色岗位关系、导入导出、工作流扩展时。
- 任务是“修改已有模块”而不是“新建模块”时。
## 避免事项
- 不要在 controller 里直接暴露 entity 代替 BO/VO。
- 不要给新的管理接口漏掉权限注解。
- 没有明确必要时,不要从 `BaseMapperPlus` 风格退回手工映射。
- 前端查询页用了日期范围时,不要删掉后端 `params` 相关处理。
- 不要把 `ruoyi-system` 这类复杂逻辑强行简化成生成器式单表 CRUD。
## 交付前自检
交付前至少检查这些点:
- CRUD 主链路是否完整。
- BO / VO / Entity 职责是否清晰。
- 分页、查询、删除校验是否与前端对得上。
- 权限、日志、防重、事务是否遗漏。
- 是否只是 generator 裸产物,如果是,需要继续补齐同模块已有增强。

View File

@@ -0,0 +1,101 @@
# 使用案例
## 案例 1新增标准单表 CRUD
### 用户提问示例
```text
使用 $ruoyi-plus-ai-coding 在 system 模块新增一个 client 管理的标准 CRUD。
请参考 generator 模板和现有 system 模块写法,补齐 entity、bo、vo、mapper、service、controller。
```
### 期望执行方式
- 先读 generator 的 `domain/bo/vo/service/serviceImpl/controller` 模板。
- 再读 `system` 模块里最接近的现有管理模块。
- 先生成骨架,再补权限、日志、校验、导出等细节。
## 案例 2修改已有复杂模块
### 用户提问示例
```text
使用 $ruoyi-plus-ai-coding 修改 workflow/category 的查询和导出逻辑,保持现有模块风格,不要简化成模板式单表 CRUD。
```
### 期望执行方式
- 先读当前 workflow 模块同类代码。
- 判断这是“复杂模块增强”,不是“从零生成”。
- 增量修改原逻辑,不要重写整个 service/controller。
## 案例 3补唯一性校验与删除前校验
### 用户提问示例
```text
使用 $ruoyi-plus-ai-coding 为 demo/demo 模块补充新增和修改时的唯一性校验,并补充删除前校验。
```
### 期望执行方式
- 优先修改 `validEntityBeforeSave(...)`
- 根据模块现有风格补 `ServiceException` 或显式失败返回。
- 删除逻辑只补必要校验,不重构整套 CRUD。
## 案例 4补数据权限与联表查询
### 用户提问示例
```text
使用 $ruoyi-plus-ai-coding 为 system 模块某个列表查询增加部门数据权限和联表字段返回,参考现有 user mapper 的 MPJ 与 DataPermission 写法。
```
### 期望执行方式
- 先看 `SysUserMapper` 和相关 service。
- 判断需要 `BaseMapperPlus` 重写还是 MPJ 联表。
- 保持权限注解和联表风格一致。
## 案例 5新增后端接口并同步前端骨架
### 用户提问示例
```text
使用 $ruoyi-plus-ai-coding 为 monitor/cache 新增一个导出接口,并同步补齐前端 api/types 调用骨架。
```
### 期望执行方式
- 先补后端 `controller/service`
- 再根据后端路由补前端 `src/api` 或 generator 风格的前端骨架。
- 保证导出接口路径和前端下载调用一致。
## 案例 6推荐的高质量任务描述
下面这种描述最容易得到稳定结果:
```text
使用 $ruoyi-plus-ai-coding 在 workflow 模块新增一个标准列表管理功能:
1. 需要分页、导出、详情、增删改
2. 查询包含状态和创建时间范围
3. 保持现有 workflow 模块风格
4. 参考 generator 模板生成基础骨架
5. 删除前需要做业务校验
```
## 不推荐的任务描述
下面这种描述太模糊,容易导致产物偏离项目:
```text
帮我加个后端接口
```
更好的写法至少要补充:
- 模块名
- 表或业务名
- 是新增还是修改
- 是否需要分页、导出、权限、数据范围、联表
- 想参考哪个现有模块

View File

@@ -0,0 +1,71 @@
# 前端约定
## 优先参考的代码来源
- `ruoyi-modules/ruoyi-gen/src/main/resources/vm/ts/*.vm`
- `ruoyi-modules/ruoyi-gen/src/main/resources/vm/vue/*.vm`
- 前端工程中与目标模块最接近的现有页面
如果任务涉及前端,先看仓库里实际使用的前端目录和同类页面,不要直接套通用 Vue 习惯。
## API 文件规则
-`@/utils/request` 引入 `request`
-`axios` 引入 `AxiosPromise`
-`@/api/<module>/<business>/types` 引入本模块类型。
- 列表接口通常返回 `AxiosPromise<PageResult<Vo>>`
- 常规接口命名和路由保持:
`listXxx` -> `GET /<module>/<business>/list`
`getXxx` -> `GET /<module>/<business>/{id}`
`addXxx` -> `POST /<module>/<business>`
`updateXxx` -> `PUT /<module>/<business>`
`delXxx` -> `DELETE /<module>/<business>/{id or ids}`
## 类型文件规则
- 定义 `VO``Form``Query`
- `Form` 通常继承 `BaseEntity`
- 非树表页面的 `Query` 通常继承 `PageQuery`
- 各类 ID 字段通常用 `string | number`
- Java 数值类型通常映射为 `number`
- Boolean 映射为 `boolean`
- 其他生成字段默认多为 `string`
- 存在日期范围查询时保留 `params?: any`
## Vue 页面规则
- 使用 `<script setup lang="ts">`
- 常见 import 来自本模块 API 和本地 `types`
- 通过 `getCurrentInstance()``proxy`,使用项目注入的公共工具。
- 字典通常通过 `proxy?.useDict(...)` 获取,再用 `toRefs` 解构。
- 常见状态包括:列表数组、`loading``buttonLoading``showSearch``ids``single``multiple``total`
- 查询和表单状态通常放在 `reactive<PageData<Form, Query>>({...})` 中。
- 弹窗状态通常使用 `dialog.visible``dialog.title`
- 表单引用通常命名为 `queryFormRef``<business>FormRef`
## 页面行为规则
- `getList` 负责设置 loading、处理日期范围参数、调用列表接口、回填 `rows``total`
- `handleQuery` 通常先把 `pageNum` 重置为 `1`,再重新查询。
- `resetQuery` 负责清空日期范围和查询表单,然后重新加载。
- `handleSelectionChange` 更新 `ids``single``multiple`
- `handleAdd` 先重置表单,再打开弹窗。
- `handleUpdate` 先查详情,再把数据赋值到表单。
- `submitForm` 校验表单、切换 `buttonLoading`、根据主键判断调用新增还是更新、提示成功并刷新列表。
- `handleDelete` 使用 `proxy?.$modal.confirm(...)` 确认,再调用删除接口并刷新。
- `handleExport` 使用 `proxy?.download(...)`
## 模板结构规则
- 优先保持生成器的页面布局结构:搜索区卡片、表格区卡片、工具栏、分页、弹窗表单。
- 保留 `v-hasPermi="['module:business:add']"` 这类权限指令。
- 继续使用仓库已有组件:`right-toolbar``pagination``dict-tag``image-preview``image-upload``file-upload``editor`
- 已有页面对时间列使用 `parseTime` 时,新页面保持一致。
- BETWEEN 日期查询继续使用 `el-date-picker``proxy?.addDateRange(...)`
## 避免事项
- 生成器风格页面不要突然换成完全不同的状态管理方式,除非该前端目录本身已经这么做。
- 模块已使用字典时,不要把选项文案硬编码到页面里。
- 不要让 API 函数名和路由段偏离后端约定。
- 后端 BO/service 依赖 begin/end 参数时,不要从查询对象里删掉 `params` 和日期范围处理。

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

8
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@@ -0,0 +1,8 @@
wrapperVersion=3.3.4
distributionType=only-script
# Maven 3.9.X
distributionUrl=https://mirrors.huaweicloud.com/apache/maven/maven-3/3.9.12/binaries/apache-maven-3.9.12-bin.zip
# Maven 4.0
#distributionUrl=https://mirrors.huaweicloud.com/apache/maven/maven-4/4.0.0-rc-5/binaries/apache-maven-4.0.0-rc-5-bin.zip
# 分发类型为 only-script 或 bin 可以不需要该项因为用不到具体参考https://maven.apache.org/tools/wrapper/maven-wrapper-distribution/index.html
#wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> <configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings> <settings>
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.6.0" /> <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.5.3" />
<option name="buildOnly" value="true" /> <option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" /> <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
</settings> </settings>

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings> <settings>
<option name="imageTag" value="ruoyi/ruoyi-server:5.6.0" /> <option name="imageTag" value="ruoyi/ruoyi-server:5.5.3" />
<option name="buildOnly" value="true" /> <option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-admin/Dockerfile" /> <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
</settings> </settings>

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> <configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings> <settings>
<option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.6.0" /> <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.5.3" />
<option name="buildOnly" value="true" /> <option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" /> <option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
</settings> </settings>

View File

@@ -9,7 +9,7 @@
[![Star](https://gitcode.com/dromara/RuoYi-Vue-Plus/star/badge.svg)](https://gitcode.com/dromara/RuoYi-Vue-Plus) [![Star](https://gitcode.com/dromara/RuoYi-Vue-Plus/star/badge.svg)](https://gitcode.com/dromara/RuoYi-Vue-Plus)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE)
<br> <br>
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.6.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) [![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.5.3-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5-blue.svg)]() [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5-blue.svg)]()
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]() [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]() [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
@@ -47,8 +47,8 @@ Topiam IAM/IDaaS身份管理平台 - https://www.topiam.cn/ <br>
|-------------|-------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| |-------------|-------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| 前端项目 | 采用 Vue3 + TS + ElementPlus 重写 | 基于Vue2/Vue3 + JS | | 前端项目 | 采用 Vue3 + TS + ElementPlus 重写 | 基于Vue2/Vue3 + JS |
| 后端项目结构 | 采用插件化 + 扩展包形式 结构解耦 易于扩展 | 模块相互注入耦合严重难以扩展 | | 后端项目结构 | 采用插件化 + 扩展包形式 结构解耦 易于扩展 | 模块相互注入耦合严重难以扩展 |
| 后端代码风格 | 严格遵守Alibaba规范与项目统一配置的代码格式化 | 代码书写与常规结构不同阅读障碍大 | | 后端代码风格 | 参考 Alibaba 规范与项目统一配置的代码格式化 | 代码书写与常规结构不同阅读障碍大 |
| Web容器 | 采用 Undertow 基于 XNIO 的高性能容器 | 采用 Tomcat | | Web容器 | 采用 Jetty 基于 Netty 的高性能容器 | 采用 Tomcat |
| 权限认证 | 采用 Sa-Token、Jwt 静态使用功能齐全 低耦合 高扩展 | Spring Security 配置繁琐扩展性极差 | | 权限认证 | 采用 Sa-Token、Jwt 静态使用功能齐全 低耦合 高扩展 | Spring Security 配置繁琐扩展性极差 |
| 权限注解 | 采用 Sa-Token 支持注解 登录校验、角色校验、权限校验、二级认证校验、HttpBasic校验、忽略校验<br/>角色与权限校验支持多种条件 如 `AND` `OR``权限 OR 角色` 等复杂表达式 | 只支持是否存在匹配 | | 权限注解 | 采用 Sa-Token 支持注解 登录校验、角色校验、权限校验、二级认证校验、HttpBasic校验、忽略校验<br/>角色与权限校验支持多种条件 如 `AND` `OR``权限 OR 角色` 等复杂表达式 | 只支持是否存在匹配 |
| 三方鉴权 | 采用 JustAuth 第三方登录组件 支持微信、钉钉等数十种三方认证 | 无 | | 三方鉴权 | 采用 JustAuth 第三方登录组件 支持微信、钉钉等数十种三方认证 | 无 |
@@ -74,14 +74,14 @@ Topiam IAM/IDaaS身份管理平台 - https://www.topiam.cn/ <br>
| 分布式幂等 | 参考美团GTIS防重系统简化实现(细节可看文档) | 手动编写注解基于aop实现 | | 分布式幂等 | 参考美团GTIS防重系统简化实现(细节可看文档) | 手动编写注解基于aop实现 |
| 分布式锁 | 采用 Lock4j 底层基于 Redisson | 无 | | 分布式锁 | 采用 Lock4j 底层基于 Redisson | 无 |
| 分布式任务调度 | 采用 SnailJob 天生支持分布式 统一的管理中心 支持多种数据库 支持分片重试DAG任务流等 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造 | | 分布式任务调度 | 采用 SnailJob 天生支持分布式 统一的管理中心 支持多种数据库 支持分片重试DAG任务流等 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造 |
| 文件存储 | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储 | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应 | | 文件存储 | 采用 Minio、RustFS 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储 | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应 |
| 云存储 | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家 | 不支持 | | 云存储 | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家 | 不支持 |
| 短信 | 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用 | 不支持 | | 短信 | 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用 | 不支持 |
| 邮件 | 采用 mail-api 通用协议支持大部分邮件厂商 | 不支持 | | 邮件 | 采用 mail-api 通用协议支持大部分邮件厂商 | 不支持 |
| 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 | | 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 |
| 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 | | 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 |
| Excel框架 | 采用 FastExcel(原Alibaba EasyExcel) 基于插件化<br/>框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等 | 基于 POI 手写实现 功能有限 复杂 扩展性差 | | Excel框架 | 采用 Apache Fesod(原Alibaba EasyExcel) 基于插件化<br/>框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等 | 基于 POI 手写实现 功能有限 复杂 扩展性差 |
| 工作流支持 | 支持各种复杂审批 转办 委派 加减签 会签 或签 票签 等功能 | 无 | | 工作流支持 | 采用 WarmFlow 支持各种复杂审批 转办 委派 加减签 会签 或签 票签 等功能 | 无 |
| 工具类框架 | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码 | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等 | | 工具类框架 | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码 | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等 |
| 监控框架 | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控 | 无 | | 监控框架 | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控 | 无 |
| 链路追踪 | 采用 Apache SkyWalking 还在为请求不知道去哪了 到哪出了问题而烦恼吗<br/>用了它即可实时查看请求经过的每一处每一个节点 | 无 | | 链路追踪 | 采用 Apache SkyWalking 还在为请求不知道去哪了 到哪出了问题而烦恼吗<br/>用了它即可实时查看请求经过的每一处每一个节点 | 无 |
@@ -97,8 +97,6 @@ Topiam IAM/IDaaS身份管理平台 - https://www.topiam.cn/ <br>
| 业务 | 功能说明 | 本框架 | RuoYi | | 业务 | 功能说明 | 本框架 | RuoYi |
|--------|----------------------------------------------------------------------|-----|------------------| |--------|----------------------------------------------------------------------|-----|------------------|
| 租户管理 | 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等 | 支持 | 无 |
| 租户套餐管理 | 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等 | 支持 | 无 |
| 客户端管理 | 系统内对接的所有客户端管理 如: pc端、小程序端等<br>支持动态授权登录方式 如: 短信登录、密码登录等 支持动态控制token时效 | 支持 | 无 | | 客户端管理 | 系统内对接的所有客户端管理 如: pc端、小程序端等<br>支持动态授权登录方式 如: 短信登录、密码登录等 支持动态控制token时效 | 支持 | 无 |
| 用户管理 | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等 | 支持 | 支持 | | 用户管理 | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等 | 支持 | 支持 |
| 部门管理 | 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限 | 支持 | 支持 | | 部门管理 | 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限 | 支持 | 支持 |

295
mvnw vendored Normal file
View File

@@ -0,0 +1,295 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

191
mvnw.cmd vendored Normal file
View File

@@ -0,0 +1,191 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.4
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
$mavenM2Item = Get-Item $MAVEN_M2_PATH
$mavenM2Target = @($mavenM2Item.Target)
if ($mavenM2Target.Count -gt 0 -and $null -ne $mavenM2Target[0]) {
$MAVEN_WRAPPER_DISTS = $mavenM2Target[0] + "/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

107
pom.xml
View File

@@ -13,24 +13,25 @@
<description>Dromara RuoYi-Vue-Plus多租户管理系统</description> <description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
<properties> <properties>
<revision>5.6.0</revision> <revision>5.5.3</revision>
<spring-boot.version>3.5.12</spring-boot.version> <spring-boot.version>4.0.3</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version> <java.version>21</java.version>
<mybatis.version>3.5.19</mybatis.version> <mybatis.version>3.5.19</mybatis.version>
<springdoc.version>2.8.15</springdoc.version> <springdoc.version>3.0.2</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version> <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<fastexcel.version>1.3.0</fastexcel.version> <fesod.version>2.0.1-incubating</fesod.version>
<velocity.version>2.3</velocity.version> <velocity.version>2.4.1</velocity.version>
<satoken.version>1.44.0</satoken.version> <satoken.version>1.45.0</satoken.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version> <mybatis-plus.version>3.5.16</mybatis-plus.version>
<mybatis-plus-join.version>1.5.6</mybatis-plus-join.version>
<p6spy.version>3.9.1</p6spy.version> <p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.43</hutool.version> <hutool.version>5.8.43</hutool.version>
<spring-boot-admin.version>3.5.6</spring-boot-admin.version> <spring-boot-admin.version>4.0.2</spring-boot-admin.version>
<redisson.version>3.52.0</redisson.version> <redisson.version>4.3.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version> <lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version> <dynamic-ds.version>4.5.0</dynamic-ds.version>
<snailjob.version>1.9.0</snailjob.version> <snailjob.version>1.9.0</snailjob.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.version> <mapstruct-plus.version>1.5.0</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version> <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
@@ -38,24 +39,27 @@
<bouncycastle.version>1.83</bouncycastle.version> <bouncycastle.version>1.83</bouncycastle.version>
<justauth.version>1.16.7</justauth.version> <justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 --> <!-- 离线IP地址定位库 -->
<ip2region.version>3.3.4</ip2region.version> <ip2region.version>3.3.6</ip2region.version>
<!-- OSS 配置 --> <!-- OSS 配置 -->
<aws.sdk.version>2.28.22</aws.sdk.version> <aws.sdk.version>2.42.9</aws.sdk.version>
<!-- SMS 配置 --> <!-- SMS 配置 -->
<sms4j.version>3.3.5</sms4j.version> <sms4j.version>3.3.5</sms4j.version>
<!-- 限制框架中的fastjson版本 --> <!-- 限制框架中的fastjson版本 -->
<fastjson.version>1.2.83</fastjson.version> <fastjson.version>1.2.83</fastjson.version>
<!-- 面向运行时的D-ORM依赖 --> <!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.3-20251210</anyline.version> <anyline.version>8.7.3-20260306</anyline.version>
<!-- 工作流配置 --> <!-- 工作流配置 -->
<warm-flow.version>1.8.4</warm-flow.version> <warm-flow.version>1.8.4</warm-flow.version>
<!-- mqtt客户端 -->
<mica-mqtt.version>2.5.12</mica-mqtt.version>
<!-- 插件版本 --> <!-- 插件版本 -->
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version> <maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>
<maven-war-plugin.version>3.4.0</maven-war-plugin.version> <maven-war-plugin.version>3.5.1</maven-war-plugin.version>
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version> <maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version> <maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version> <!-- 统一版本号管理Maven3.X需要Maven4.0之后已原生支持 -->
<flatten-maven-plugin.version>1.7.3</flatten-maven-plugin.version>
<!-- 打包默认跳过测试 --> <!-- 打包默认跳过测试 -->
<skipTests>true</skipTests> <skipTests>true</skipTests>
</properties> </properties>
@@ -118,6 +122,16 @@
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<!-- sa-token-bom 依赖 -->
<!-- Sa-Token 权限认证, 在线文档http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-bom</artifactId>
<version>${satoken.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- common 的依赖配置--> <!-- common 的依赖配置-->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
@@ -145,10 +159,11 @@
<version>${lombok.version}</version> <version>${lombok.version}</version>
</dependency> </dependency>
<!-- fesod (EasyExcel/FastExcel的前身) 的依赖 -->
<dependency> <dependency>
<groupId>cn.idev.excel</groupId> <groupId>org.apache.fesod</groupId>
<artifactId>fastexcel</artifactId> <artifactId>fesod-sheet</artifactId>
<version>${fastexcel.version}</version> <version>${fesod.version}</version>
</dependency> </dependency>
<!-- velocity代码生成使用模板 --> <!-- velocity代码生成使用模板 -->
@@ -158,34 +173,10 @@
<version>${velocity.version}</version> <version>${velocity.version}</version>
</dependency> </dependency>
<!-- Sa-Token 权限认证, 在线文档http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- Sa-Token 整合 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>${satoken.version}</version>
<exclusions>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
<version>${satoken.version}</version>
</dependency>
<!-- dynamic-datasource 多数据源--> <!-- dynamic-datasource 多数据源-->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <artifactId>dynamic-datasource-spring-boot4-starter</artifactId>
<version>${dynamic-ds.version}</version> <version>${dynamic-ds.version}</version>
</dependency> </dependency>
@@ -197,7 +188,7 @@
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId> <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>${mybatis-plus.version}</version> <version>${mybatis-plus.version}</version>
</dependency> </dependency>
@@ -213,6 +204,12 @@
<version>${mybatis-plus.version}</version> <version>${mybatis-plus.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
<version>${mybatis-plus-join.version}</version>
</dependency>
<!-- sql性能分析插件 --> <!-- sql性能分析插件 -->
<dependency> <dependency>
<groupId>p6spy</groupId> <groupId>p6spy</groupId>
@@ -306,6 +303,12 @@
<version>${warm-flow.version}</version> <version>${warm-flow.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.dromara.mica-mqtt</groupId>
<artifactId>mica-mqtt-client-spring-boot-starter</artifactId>
<version>${mica-mqtt.version}</version>
</dependency>
<!-- JustAuth 的依赖配置--> <!-- JustAuth 的依赖配置-->
<dependency> <dependency>
<groupId>me.zhyd.oauth</groupId> <groupId>me.zhyd.oauth</groupId>
@@ -340,7 +343,7 @@
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-generator</artifactId> <artifactId>ruoyi-gen</artifactId>
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
@@ -357,6 +360,13 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- api模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api</artifactId>
<version>${revision}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@@ -365,6 +375,7 @@
<module>ruoyi-common</module> <module>ruoyi-common</module>
<module>ruoyi-extend</module> <module>ruoyi-extend</module>
<module>ruoyi-modules</module> <module>ruoyi-modules</module>
<module>ruoyi-api</module>
</modules> </modules>
<packaging>pom</packaging> <packaging>pom</packaging>

View File

@@ -57,12 +57,12 @@
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ratelimiter</artifactId> <artifactId>ruoyi-common-mail</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId> <artifactId>ruoyi-api</artifactId>
</dependency> </dependency>
<dependency> <dependency>
@@ -75,11 +75,6 @@
<artifactId>ruoyi-job</artifactId> <artifactId>ruoyi-job</artifactId>
</dependency> </dependency>
<!-- 代码生成-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- demo模块 --> <!-- demo模块 -->
<dependency> <dependency>
@@ -118,6 +113,21 @@
</dependencies> </dependencies>
<profiles>
<!-- 仅在激活 'gen' profile 时,才引入代码生成模块 -->
<profile>
<id>gen</id>
<dependencies>
<!-- 代码生成-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-gen</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
<build> <build>
<finalName>${project.artifactId}</finalName> <finalName>${project.artifactId}</finalName>
<plugins> <plugins>

View File

@@ -1,10 +1,7 @@
package org.dromara.web.controller; package org.dromara.web.controller;
import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -16,47 +13,42 @@ import me.zhyd.oauth.utils.AuthStateUtils;
import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.model.LoginBody; import org.dromara.common.core.domain.model.LoginBody;
import org.dromara.common.core.domain.model.RegisterBody; import org.dromara.common.core.enums.PushSourceEnum;
import org.dromara.common.core.domain.model.SocialLoginBody; import org.dromara.common.core.enums.PushTypeEnum;
import org.dromara.common.core.utils.*; import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.encrypt.annotation.ApiEncrypt; import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.ratelimiter.annotation.RateLimiter; import org.dromara.common.redis.annotation.RateLimiter;
import org.dromara.common.ratelimiter.enums.LimitType; import org.dromara.common.redis.enums.LimitType;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.social.config.properties.SocialLoginConfigProperties; import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
import org.dromara.common.social.config.properties.SocialProperties; import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.utils.SocialUtils; import org.dromara.common.social.utils.SocialUtils;
import org.dromara.common.sse.dto.SseMessageDto; import org.dromara.system.api.MessageService;
import org.dromara.common.sse.utils.SseMessageUtils; import org.dromara.system.api.domain.PushPayloadDTO;
import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.system.api.model.RegisterBody;
import org.dromara.system.domain.bo.SysTenantBo; import org.dromara.system.api.model.SocialLoginBody;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysTenantVo;
import org.dromara.system.service.ISysClientService; import org.dromara.system.service.ISysClientService;
import org.dromara.system.service.ISysConfigService; import org.dromara.system.service.ISysConfigService;
import org.dromara.system.service.ISysSocialService; import org.dromara.system.service.ISysSocialService;
import org.dromara.system.service.ISysTenantService;
import org.dromara.web.domain.vo.LoginTenantVo;
import org.dromara.web.domain.vo.LoginVo; import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.domain.vo.TenantListVo;
import org.dromara.web.service.IAuthStrategy; import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService; import org.dromara.web.service.SysLoginService;
import org.dromara.web.service.SysRegisterService; import org.dromara.web.service.SysRegisterService;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* 认证 * 认证控制器,提供登录、注册、社交绑定和退出能力。
* *
* @author Lion Li * @author Lion Li
*/ */
@@ -71,10 +63,10 @@ public class AuthController {
private final SysLoginService loginService; private final SysLoginService loginService;
private final SysRegisterService registerService; private final SysRegisterService registerService;
private final ISysConfigService configService; private final ISysConfigService configService;
private final ISysTenantService tenantService;
private final ISysSocialService socialUserService; private final ISysSocialService socialUserService;
private final ISysClientService clientService; private final ISysClientService clientService;
private final ScheduledExecutorService scheduledExecutorService; private final ScheduledExecutorService scheduledExecutorService;
private final MessageService messageService;
/** /**
@@ -99,48 +91,46 @@ public class AuthController {
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) { } else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
return R.fail(MessageUtils.message("auth.grant.type.blocked")); return R.fail(MessageUtils.message("auth.grant.type.blocked"));
} }
// 校验租户
loginService.checkTenant(loginBody.getTenantId());
// 登录 // 登录
LoginVo loginVo = IAuthStrategy.login(body, client, grantType); LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
Long userId = LoginHelper.getUserId(); Long userId = LoginHelper.getUserId();
scheduledExecutorService.schedule(() -> { scheduledExecutorService.schedule(() -> {
SseMessageDto dto = new SseMessageDto(); messageService.publishMessage(
dto.setMessage(DateUtils.getTodayHour(new Date()) + "好,欢迎登录 RuoYi-Vue-Plus 后台管理系统"); List.of(userId),
dto.setUserIds(List.of(userId)); PushPayloadDTO.of(
SseMessageUtils.publishMessage(dto); PushTypeEnum.MESSAGE,
PushSourceEnum.BACKEND,
DateUtils.getTodayHour(new Date()) + "好,欢迎登录 RuoYi-Vue-Plus 后台管理系统",
null
)
);
}, 5, TimeUnit.SECONDS); }, 5, TimeUnit.SECONDS);
return R.ok(loginVo); return R.ok(loginVo);
} }
/** /**
* 获取跳转URL * 获取第三方绑定跳转地址。
* *
* @param source 登录来源 * @param source 登录来源
* @return 结果 * @return 跳转地址
*/ */
@GetMapping("/binding/{source}") @GetMapping("/binding/{source}")
public R<String> authBinding(@PathVariable("source") String source, public R<String> authBinding(@PathVariable("source") String source) {
@RequestParam String tenantId, @RequestParam String domain) {
SocialLoginConfigProperties obj = socialProperties.getType().get(source); SocialLoginConfigProperties obj = socialProperties.getType().get(source);
if (ObjectUtil.isNull(obj)) { if (ObjectUtil.isNull(obj)) {
return R.fail(source + "平台账号暂不支持"); return R.fail(source + "平台账号暂不支持");
} }
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties); AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
Map<String, String> map = new HashMap<>(); String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
map.put("tenantId", tenantId);
map.put("domain", domain);
map.put("state", AuthStateUtils.createState());
String authorizeUrl = authRequest.authorize(Base64.encode(JsonUtils.toJsonString(map), StandardCharsets.UTF_8));
return R.ok("操作成功", authorizeUrl); return R.ok("操作成功", authorizeUrl);
} }
/** /**
* 前端回调绑定授权(需要token) * 处理前端回调后的社交账号绑定。
* *
* @param loginBody 请求体 * @param loginBody 请求体
* @return 结果 * @return 操作结果
*/ */
@PostMapping("/social/callback") @PostMapping("/social/callback")
public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) { public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) {
@@ -161,9 +151,10 @@ public class AuthController {
/** /**
* 取消授权(需要token) * 取消当前用户的社交账号授权。
* *
* @param socialId socialId * @param socialId socialId
* @return 操作结果
*/ */
@DeleteMapping(value = "/unlock/{socialId}") @DeleteMapping(value = "/unlock/{socialId}")
public R<Void> unlockSocial(@PathVariable Long socialId) { public R<Void> unlockSocial(@PathVariable Long socialId) {
@@ -184,12 +175,15 @@ public class AuthController {
} }
/** /**
* 用户注册 * 用户注册
*
* @param user 注册信息
* @return 操作结果
*/ */
@ApiEncrypt @ApiEncrypt
@PostMapping("/register") @PostMapping("/register")
public R<Void> register(@Validated @RequestBody RegisterBody user) { public R<Void> register(@Validated @RequestBody RegisterBody user) {
if (!configService.selectRegisterEnabled(user.getTenantId())) { if (!configService.selectRegisterEnabled()) {
return R.fail("当前系统没有开启注册功能!"); return R.fail("当前系统没有开启注册功能!");
} }
registerService.register(user); registerService.register(user);
@@ -197,47 +191,24 @@ public class AuthController {
} }
/** /**
* 登录页租户下拉框 * 获取登录页租户下拉框数据,当前仅预留返回结构。
* *
* @param request 当前请求
* @return 租户列表 * @return 租户列表
* @throws Exception 异常
*/ */
@RateLimiter(time = 60, count = 20, limitType = LimitType.IP) @RateLimiter(time = 60, count = 20, limitType = LimitType.IP)
@GetMapping("/tenant/list") @GetMapping("/tenant/list")
public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception { public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
// 返回对象 // 暂时预留给前端使用 后续删除
LoginTenantVo result = new LoginTenantVo(); return R.ok(new LoginTenantVo(false));
boolean enable = TenantHelper.isEnable();
result.setTenantEnabled(enable);
// 如果未开启租户这直接返回
if (!enable) {
return R.ok(result);
}
List<SysTenantVo> tenantList = tenantService.queryList(new SysTenantBo());
List<TenantListVo> voList = MapstructUtils.convert(tenantList, TenantListVo.class);
try {
// 如果只超管返回所有租户
if (LoginHelper.isSuperAdmin()) {
result.setVoList(voList);
return R.ok(result);
}
} catch (NotLoginException ignored) {
}
// 获取域名
String host;
String referer = request.getHeader("referer");
if (StringUtils.isNotBlank(referer)) {
// 这里从referer中取值是为了本地使用hosts添加虚拟域名方便本地环境调试
host = referer.split("//")[1].split("/")[0];
} else {
host = new URL(request.getRequestURL().toString()).getHost();
}
// 根据域名进行筛选
List<TenantListVo> list = StreamUtils.filter(voList, vo ->
StringUtils.equalsIgnoreCase(vo.getDomain(), host));
result.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
return R.ok(result);
} }
/**
* 登录租户列表响应对象。
*
* @param tenantEnabled 是否启用租户
*/
public record LoginTenantVo(Boolean tenantEnabled) {
}
} }

View File

@@ -15,17 +15,17 @@ import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.regex.RegexValidator;
import org.dromara.common.mail.config.properties.MailProperties; import org.dromara.common.mail.config.properties.MailProperties;
import org.dromara.common.mail.utils.MailUtils; import org.dromara.common.mail.utils.MailUtils;
import org.dromara.common.ratelimiter.annotation.RateLimiter; import org.dromara.common.redis.annotation.RateLimiter;
import org.dromara.common.ratelimiter.enums.LimitType; import org.dromara.common.redis.enums.LimitType;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.web.core.WaveAndCircleCaptcha;
import org.dromara.common.web.config.properties.CaptchaProperties; import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.common.web.core.WaveAndCircleCaptcha;
import org.dromara.sms4j.api.SmsBlend; import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse; import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory; import org.dromara.sms4j.core.factory.SmsFactory;
import org.dromara.web.domain.vo.CaptchaVo;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -53,14 +53,18 @@ public class CaptchaController {
private final MailProperties mailProperties; private final MailProperties mailProperties;
/** /**
* 短信验证码 * 发送短信验证码
* *
* @param phonenumber 用户手机号 * @param phoneNumber 用户手机号
* @return 操作结果
*/ */
@RateLimiter(key = "#phonenumber", time = 60, count = 1) @RateLimiter(key = "#phonenumber", time = 60, count = 1)
@GetMapping("/resource/sms/code") @GetMapping("/resource/sms/code")
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) { public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber; if (!RegexValidator.isMobile(phoneNumber)) {
return R.fail("请输入正确的手机号!");
}
String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
String code = RandomUtil.randomNumbers(4); String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// 验证码模板id 自行处理 (查数据库或写死均可) // 验证码模板id 自行处理 (查数据库或写死均可)
@@ -68,7 +72,7 @@ public class CaptchaController {
LinkedHashMap<String, String> map = new LinkedHashMap<>(1); LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put("code", code); map.put("code", code);
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map); SmsResponse smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map);
if (!smsResponse.isSuccess()) { if (!smsResponse.isSuccess()) {
log.error("验证码短信发送异常 => {}", smsResponse); log.error("验证码短信发送异常 => {}", smsResponse);
return R.fail(smsResponse.getData().toString()); return R.fail(smsResponse.getData().toString());
@@ -77,22 +81,27 @@ public class CaptchaController {
} }
/** /**
* 邮箱验证码 * 发送邮箱验证码
* *
* @param email 邮箱 * @param email 邮箱
* @return 操作结果
*/ */
@GetMapping("/resource/email/code") @GetMapping("/resource/email/code")
public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) { public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
if (!mailProperties.getEnabled()) { if (!mailProperties.getEnabled()) {
return R.fail("当前系统没有开启邮箱功能!"); return R.fail("当前系统没有开启邮箱功能!");
} }
if (!RegexValidator.isEmail(email)) {
return R.fail("请输入正确的邮箱地址!");
}
SpringUtils.getAopProxy(this).emailCodeImpl(email); SpringUtils.getAopProxy(this).emailCodeImpl(email);
return R.ok(); return R.ok();
} }
/** /**
* 邮箱验证码 * 发送邮箱验证码的实际执行方法,拆分出来避免开关关闭时仍触发限流。
* 独立方法避免验证码关闭之后仍然走限流 *
* @param email 邮箱
*/ */
@RateLimiter(key = "#email", time = 60, count = 1) @RateLimiter(key = "#email", time = 60, count = 1)
public void emailCodeImpl(String email) { public void emailCodeImpl(String email) {
@@ -108,22 +117,23 @@ public class CaptchaController {
} }
/** /**
* 生成验证码 * 获取图片验证码
*
* @return 验证码信息
*/ */
@GetMapping("/auth/code") @GetMapping("/auth/code")
public R<CaptchaVo> getCode() { public R<CaptchaVo> getCode() {
boolean captchaEnabled = captchaProperties.getEnable(); boolean captchaEnabled = captchaProperties.getEnable();
if (!captchaEnabled) { if (!captchaEnabled) {
CaptchaVo captchaVo = new CaptchaVo(); return R.ok(new CaptchaVo(false, null, null));
captchaVo.setCaptchaEnabled(false);
return R.ok(captchaVo);
} }
return R.ok(SpringUtils.getAopProxy(this).getCodeImpl()); return R.ok(SpringUtils.getAopProxy(this).getCodeImpl());
} }
/** /**
* 生成验证码 * 实际生成图片验证码并缓存结果。
* 独立方法避免验证码关闭之后仍然走限流 *
* @return 验证码信息
*/ */
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP) @RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
public CaptchaVo getCodeImpl() { public CaptchaVo getCodeImpl() {
@@ -151,10 +161,17 @@ public class CaptchaController {
code = exp.getValue(String.class); code = exp.getValue(String.class);
} }
RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
CaptchaVo captchaVo = new CaptchaVo(); return new CaptchaVo(true, uuid, captcha.getImageBase64());
captchaVo.setUuid(uuid); }
captchaVo.setImg(captcha.getImageBase64());
return captchaVo; /**
* 图片验证码响应对象。
*
* @param captchaEnabled 是否启用验证码
* @param uuid 验证码标识
* @param img Base64 图片数据
*/
public record CaptchaVo(Boolean captchaEnabled, String uuid, String img) {
} }
} }

View File

@@ -18,7 +18,9 @@ import org.springframework.web.bind.annotation.RestController;
public class IndexController { public class IndexController {
/** /**
* 访问首页提示 * 访问首页时返回引导提示
*
* @return 首页提示语
*/ */
@GetMapping("/") @GetMapping("/")
public String index() { public String index() {

View File

@@ -1,25 +0,0 @@
package org.dromara.web.domain.vo;
import lombok.Data;
/**
* 验证码信息
*
* @author Michelle.Chung
*/
@Data
public class CaptchaVo {
/**
* 是否开启验证码
*/
private Boolean captchaEnabled = true;
private String uuid;
/**
* 验证码图片
*/
private String img;
}

View File

@@ -1,25 +0,0 @@
package org.dromara.web.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 登录租户对象
*
* @author Michelle.Chung
*/
@Data
public class LoginTenantVo {
/**
* 租户开关
*/
private Boolean tenantEnabled;
/**
* 租户对象列表
*/
private List<TenantListVo> voList;
}

View File

@@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
/** /**
* 登录验证信息 * 登录成功后的令牌信息返回对象。
* *
* @author Michelle.Chung * @author Michelle.Chung
*/ */

View File

@@ -1,31 +0,0 @@
package org.dromara.web.domain.vo;
import org.dromara.system.domain.vo.SysTenantVo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
/**
* 租户列表
*
* @author Lion Li
*/
@Data
@AutoMapper(target = SysTenantVo.class)
public class TenantListVo {
/**
* 租户编号
*/
private String tenantId;
/**
* 企业名称
*/
private String companyName;
/**
* 域名
*/
private String domain;
}

View File

@@ -1,32 +1,29 @@
package org.dromara.web.listener; package org.dromara.web.listener;
import cn.dev33.satoken.listener.SaTokenListener; import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.convert.Convert;
import cn.hutool.http.useragent.UserAgent; import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil; import cn.hutool.http.useragent.UserAgentUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.CacheConstants; import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.domain.dto.UserOnlineDTO;
import org.dromara.common.core.utils.MessageUtils; import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils; import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ip.AddressUtils; import org.dromara.common.core.utils.ip.AddressUtils;
import org.dromara.common.log.event.LogininforEvent; import org.dromara.common.log.event.LoginInfoEvent;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.system.api.domain.UserOnlineDTO;
import org.dromara.web.service.SysLoginService; import org.dromara.web.service.SysLoginService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.Duration; import java.time.Duration;
/** /**
* 用户行为 侦听器的实现 * 用户行为监听器,用于同步在线状态和登录日志。
* *
* @author Lion Li * @author Lion Li
*/ */
@@ -38,7 +35,7 @@ public class UserActionListener implements SaTokenListener {
private final SysLoginService loginService; private final SysLoginService loginService;
/** /**
* 每次登录时触发 * 登录成功后记录在线信息并写入登录日志。
*/ */
@Override @Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
@@ -52,64 +49,52 @@ public class UserActionListener implements SaTokenListener {
dto.setLoginTime(System.currentTimeMillis()); dto.setLoginTime(System.currentTimeMillis());
dto.setTokenId(tokenValue); dto.setTokenId(tokenValue);
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY); String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
String tenantId = (String) loginParameter.getExtra(LoginHelper.TENANT_KEY);
dto.setUserName(username); dto.setUserName(username);
dto.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY)); dto.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY));
dto.setDeviceType(loginParameter.getDeviceType()); dto.setDeviceType(loginParameter.getDeviceType());
dto.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY)); dto.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY));
TenantHelper.dynamic(tenantId, () -> { if (loginParameter.getTimeout() == -1) {
if(loginParameter.getTimeout() == -1) { RedisUtils.setCacheObject(CacheNames.ONLINE_TOKEN_KEY + tokenValue, dto);
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto); } else {
} else { RedisUtils.setCacheObject(CacheNames.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(loginParameter.getTimeout()));
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(loginParameter.getTimeout())); }
}
});
// 记录登录日志 // 记录登录日志
LogininforEvent logininforEvent = new LogininforEvent(); LoginInfoEvent loginInfoEvent = new LoginInfoEvent();
logininforEvent.setTenantId(tenantId); loginInfoEvent.setUsername(username);
logininforEvent.setUsername(username); loginInfoEvent.setStatus(Constants.LOGIN_SUCCESS);
logininforEvent.setStatus(Constants.LOGIN_SUCCESS); loginInfoEvent.setMessage(MessageUtils.message("user.login.success"));
logininforEvent.setMessage(MessageUtils.message("user.login.success")); loginInfoEvent.setRequest(ServletUtils.getRequest());
logininforEvent.setRequest(ServletUtils.getRequest()); SpringUtils.context().publishEvent(loginInfoEvent);
SpringUtils.context().publishEvent(logininforEvent);
// 更新登录信息 // 更新登录信息
loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip); loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
log.info("user doLogin, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8)); log.info("user doLogin, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8));
} }
/** /**
* 每次注销时触发 * 注销时清理在线缓存。
*/ */
@Override @Override
public void doLogout(String loginType, Object loginId, String tokenValue) { public void doLogout(String loginType, Object loginId, String tokenValue) {
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY)); RedisUtils.deleteObject(CacheNames.ONLINE_TOKEN_KEY + tokenValue);
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doLogout, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8)); log.info("user doLogout, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8));
} }
/** /**
* 每次被踢下线时触发 * 被踢下线时清理在线缓存。
*/ */
@Override @Override
public void doKickout(String loginType, Object loginId, String tokenValue) { public void doKickout(String loginType, Object loginId, String tokenValue) {
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY)); RedisUtils.deleteObject(CacheNames.ONLINE_TOKEN_KEY + tokenValue);
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doKickout, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8)); log.info("user doKickout, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8));
} }
/** /**
* 每次被顶下线时触发 * 被顶下线时清理在线缓存。
*/ */
@Override @Override
public void doReplaced(String loginType, Object loginId, String tokenValue) { public void doReplaced(String loginType, Object loginId, String tokenValue) {
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY)); RedisUtils.deleteObject(CacheNames.ONLINE_TOKEN_KEY + tokenValue);
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doReplaced, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8)); log.info("user doReplaced, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8));
} }

View File

@@ -1,12 +1,16 @@
package org.dromara.web.service; package org.dromara.web.service;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.system.domain.SysClient; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.web.domain.vo.LoginVo; import org.dromara.web.domain.vo.LoginVo;
import java.util.function.Consumer;
/** /**
* 授权策略 * 授权策略
* *
@@ -34,12 +38,43 @@ public interface IAuthStrategy {
return instance.login(body, client); return instance.login(body, client);
} }
/**
* 按客户端配置构建统一登录参数。
*
* @param client 客户端配置
* @return Sa-Token 登录参数
*/
static SaLoginParameter buildLoginParameter(SysClientVo client) {
return buildLoginParameter(client, null);
}
/**
* 按客户端配置构建统一登录参数,并预留自定义扩展入口。
*
* @param client 客户端配置
* @param customizer 自定义扩展逻辑
* @return Sa-Token 登录参数
*/
static SaLoginParameter buildLoginParameter(SysClientVo client, Consumer<SaLoginParameter> customizer) {
SaLoginParameter model = new SaLoginParameter();
model.setDeviceType(client.getDeviceType());
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
model.setExtra(LoginHelper.CLIENT_ACCESS_PATH_KEY, client.getAccessPath());
model.setExtra(LoginHelper.CLIENT_IP_WHITELIST_KEY, client.getIpWhitelist());
if (ObjectUtil.isNotNull(customizer)) {
customizer.accept(model);
}
return model;
}
/** /**
* 登录 * 登录
* *
* @param body 登录对象 * @param body 登录对象
* @param client 授权管理视图对象 * @param client 授权管理视图对象
* @return 登录验证信息 * @return 当前策略完成认证后的登录结果
*/ */
LoginVo login(String body, SysClientVo client); LoginVo login(String body, SysClientVo client);

View File

@@ -10,23 +10,19 @@ import com.baomidou.lock.annotation.Lock4j;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.model.AuthUser;
import org.dromara.common.core.constant.CacheConstants; import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.domain.dto.PostDTO;
import org.dromara.common.core.domain.dto.RoleDTO;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.enums.LoginType; import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.*; import org.dromara.common.core.utils.*;
import org.dromara.common.log.event.LogininforEvent; import org.dromara.common.log.event.LoginInfoEvent;
import org.dromara.common.mybatis.helper.DataPermissionHelper; import org.dromara.common.mybatis.helper.DataPermissionHelper;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.exception.TenantException; import org.dromara.system.api.domain.PostDTO;
import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.system.api.domain.RoleDTO;
import org.dromara.system.api.model.LoginUser;
import org.dromara.system.domain.SysUser; import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.bo.SysSocialBo; import org.dromara.system.domain.bo.SysSocialBo;
import org.dromara.system.domain.vo.*; import org.dromara.system.domain.vo.*;
@@ -36,7 +32,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Duration; import java.time.Duration;
import java.util.Date; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -56,7 +52,6 @@ public class SysLoginService {
@Value("${user.password.lockTime}") @Value("${user.password.lockTime}")
private Integer lockTime; private Integer lockTime;
private final ISysTenantService tenantService;
private final ISysPermissionService permissionService; private final ISysPermissionService permissionService;
private final ISysSocialService sysSocialService; private final ISysSocialService sysSocialService;
private final ISysRoleService roleService; private final ISysRoleService roleService;
@@ -113,11 +108,7 @@ public class SysLoginService {
if (ObjectUtil.isNull(loginUser)) { if (ObjectUtil.isNull(loginUser)) {
return; return;
} }
if (TenantHelper.isEnable() && LoginHelper.isSuperAdmin()) { recordLoginInfo(loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
// 超级管理员 登出清除动态租户
TenantHelper.clearDynamic();
}
recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
} catch (NotLoginException ignored) { } catch (NotLoginException ignored) {
} finally { } finally {
try { try {
@@ -130,73 +121,85 @@ public class SysLoginService {
/** /**
* 记录登录信息 * 记录登录信息
* *
* @param tenantId 租户ID
* @param username 用户名 * @param username 用户名
* @param status 状态 * @param status 状态
* @param message 消息内容 * @param message 消息内容
*/ */
public void recordLogininfor(String tenantId, String username, String status, String message) { public void recordLoginInfo(String username, String status, String message) {
LogininforEvent logininforEvent = new LogininforEvent(); LoginInfoEvent loginInfoEvent = new LoginInfoEvent();
logininforEvent.setTenantId(tenantId); loginInfoEvent.setUsername(username);
logininforEvent.setUsername(username); loginInfoEvent.setStatus(status);
logininforEvent.setStatus(status); loginInfoEvent.setMessage(message);
logininforEvent.setMessage(message); loginInfoEvent.setRequest(ServletUtils.getRequest());
logininforEvent.setRequest(ServletUtils.getRequest()); SpringUtils.context().publishEvent(loginInfoEvent);
SpringUtils.context().publishEvent(logininforEvent);
} }
/** /**
* 构建登录用户 * 根据用户视图对象组装登录态上下文。
*
* @param user 用户基础信息
* @return 包含部门、角色、岗位与权限数据的登录用户
*/ */
public LoginUser buildLoginUser(SysUserVo user) { public LoginUser buildLoginUser(SysUserVo user) {
LoginUser loginUser = new LoginUser(); LoginUser loginUser = new LoginUser();
Long userId = user.getUserId(); Long userId = user.getUserId();
loginUser.setTenantId(user.getTenantId());
loginUser.setUserId(userId); loginUser.setUserId(userId);
loginUser.setDeptId(user.getDeptId()); loginUser.setDeptId(user.getDeptId());
loginUser.setUsername(user.getUserName()); loginUser.setUsername(user.getUserName());
loginUser.setNickname(user.getNickName()); loginUser.setNickname(user.getNickName());
loginUser.setUserType(user.getUserType()); loginUser.setUserType(user.getUserType());
loginUser.setMenuPermission(permissionService.getMenuPermission(userId));
loginUser.setRolePermission(permissionService.getRolePermission(userId));
if (ObjectUtil.isNotNull(user.getDeptId())) { if (ObjectUtil.isNotNull(user.getDeptId())) {
Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById); Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY)); loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY)); loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
} }
List<SysRoleVo> roles = roleService.selectRolesByUserId(userId); ThreadUtils.virtualSubmit(() -> {
List<SysPostVo> posts = postService.selectPostsByUserId(userId); loginUser.setMenuPermission(permissionService.getMenuPermission(userId));
loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class)); }, () -> {
loginUser.setPosts(BeanUtil.copyToList(posts, PostDTO.class)); loginUser.setRolePermission(permissionService.getRolePermission(userId));
}, () -> {
List<SysRoleVo> roles = roleService.selectRolesByUserId(userId);
List<RoleDTO> roleDtos = BeanUtil.copyToList(roles, RoleDTO.class);
loginUser.setRoles(roleDtos);
loginUser.setDataScopeRoleMap(permissionService.getDataScopeRoleMap(roleDtos));
}, () -> {
List<SysPostVo> posts = postService.selectPostsByUserId(userId);
loginUser.setPosts(BeanUtil.copyToList(posts, PostDTO.class));
});
return loginUser; return loginUser;
} }
/** /**
* 记录登录信息 * 更新用户最近一次登录IP与登录时间。
* *
* @param userId 用户ID * @param userId 用户ID
* @param ip 登录IP
*/ */
public void recordLoginInfo(Long userId, String ip) { public void recordLoginInfo(Long userId, String ip) {
SysUser sysUser = new SysUser(); SysUser sysUser = new SysUser();
sysUser.setUserId(userId); sysUser.setUserId(userId);
sysUser.setLoginIp(ip); sysUser.setLoginIp(ip);
sysUser.setLoginDate(DateUtils.getNowDate()); sysUser.setLoginDate(LocalDateTime.now());
sysUser.setUpdateBy(userId); sysUser.setUpdateBy(userId);
DataPermissionHelper.ignore(() -> userMapper.updateById(sysUser)); DataPermissionHelper.ignore(() -> userMapper.updateById(sysUser));
} }
/** /**
* 登录校验 * 执行登录失败次数校验,并在成功后清空失败计数。
*
* @param loginType 登录类型
* @param username 登录标识
* @param supplier 返回 {@code true} 表示本次认证失败
*/ */
public void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) { public void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username; String errorKey = CacheNames.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL; String loginFail = Constants.LOGIN_FAIL;
// 获取用户登录错误次数默认为0 (可自定义限制策略 例如: key + username + ip) // 获取用户登录错误次数默认为0 (可自定义限制策略 例如: key + username + ip)
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0); int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
// 锁定时间内登录 则踢出 // 锁定时间内登录 则踢出
if (errorNumber >= maxRetryCount) { if (errorNumber >= maxRetryCount) {
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime)); recordLoginInfo(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime); throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} }
@@ -206,11 +209,11 @@ public class SysLoginService {
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime)); RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
// 达到规定错误次数 则锁定登录 // 达到规定错误次数 则锁定登录
if (errorNumber >= maxRetryCount) { if (errorNumber >= maxRetryCount) {
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime)); recordLoginInfo(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime); throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} else { } else {
// 未达到规定错误次数 // 未达到规定错误次数
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber)); recordLoginInfo(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
throw new UserException(loginType.getRetryLimitCount(), errorNumber); throw new UserException(loginType.getRetryLimitCount(), errorNumber);
} }
} }
@@ -219,33 +222,4 @@ public class SysLoginService {
RedisUtils.deleteObject(errorKey); RedisUtils.deleteObject(errorKey);
} }
/**
* 校验租户
*
* @param tenantId 租户ID
*/
public void checkTenant(String tenantId) {
if (!TenantHelper.isEnable()) {
return;
}
if (StringUtils.isBlank(tenantId)) {
throw new TenantException("tenant.number.not.blank");
}
if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
return;
}
SysTenantVo tenant = tenantService.queryByTenantId(tenantId);
if (ObjectUtil.isNull(tenant)) {
log.info("登录租户:{} 不存在.", tenantId);
throw new TenantException("tenant.not.exists");
} else if (SystemConstants.DISABLE.equals(tenant.getStatus())) {
log.info("登录租户:{} 已被停用.", tenantId);
throw new TenantException("tenant.blocked");
} else if (ObjectUtil.isNotNull(tenant.getExpireTime())
&& new Date().after(tenant.getExpireTime())) {
log.info("登录租户:{} 已超过有效期.", tenantId);
throw new TenantException("tenant.expired");
}
}
} }

View File

@@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.domain.model.RegisterBody;
import org.dromara.common.core.enums.UserType; import org.dromara.common.core.enums.UserType;
import org.dromara.common.core.exception.user.CaptchaException; import org.dromara.common.core.exception.user.CaptchaException;
import org.dromara.common.core.exception.user.CaptchaExpireException; import org.dromara.common.core.exception.user.CaptchaExpireException;
@@ -14,10 +13,10 @@ import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils; import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.log.event.LogininforEvent; import org.dromara.common.log.event.LoginInfoEvent;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.common.web.config.properties.CaptchaProperties; import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.system.api.model.RegisterBody;
import org.dromara.system.domain.SysUser; import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.bo.SysUserBo; import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.mapper.SysUserMapper; import org.dromara.system.mapper.SysUserMapper;
@@ -39,9 +38,10 @@ public class SysRegisterService {
/** /**
* 注册 * 注册
*
* @param registerBody 注册请求参数
*/ */
public void register(RegisterBody registerBody) { public void register(RegisterBody registerBody) {
String tenantId = registerBody.getTenantId();
String username = registerBody.getUsername(); String username = registerBody.getUsername();
String password = registerBody.getPassword(); String password = registerBody.getPassword();
// 校验用户类型是否存在 // 校验用户类型是否存在
@@ -50,7 +50,7 @@ public class SysRegisterService {
boolean captchaEnabled = captchaProperties.getEnable(); boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关 // 验证码开关
if (captchaEnabled) { if (captchaEnabled) {
validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid()); validateCaptcha(username, registerBody.getCode(), registerBody.getUuid());
} }
SysUserBo sysUser = new SysUserBo(); SysUserBo sysUser = new SysUserBo();
sysUser.setUserName(username); sysUser.setUserName(username);
@@ -58,18 +58,16 @@ public class SysRegisterService {
sysUser.setPassword(BCrypt.hashpw(password)); sysUser.setPassword(BCrypt.hashpw(password));
sysUser.setUserType(userType); sysUser.setUserType(userType);
boolean exist = TenantHelper.dynamic(tenantId, () -> { boolean exist = userMapper.exists(new LambdaQueryWrapper<SysUser>()
return userMapper.exists(new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUserName, sysUser.getUserName()));
.eq(SysUser::getUserName, sysUser.getUserName()));
});
if (exist) { if (exist) {
throw new UserException("user.register.save.error", username); throw new UserException("user.register.save.error", username);
} }
boolean regFlag = userService.registerUser(sysUser, tenantId); boolean regFlag = userService.registerUser(sysUser);
if (!regFlag) { if (!regFlag) {
throw new UserException("user.register.error"); throw new UserException("user.register.error");
} }
recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success")); recordLoginInfo(username, Constants.REGISTER, MessageUtils.message("user.register.success"));
} }
/** /**
@@ -79,16 +77,16 @@ public class SysRegisterService {
* @param code 验证码 * @param code 验证码
* @param uuid 唯一标识 * @param uuid 唯一标识
*/ */
public void validateCaptcha(String tenantId, String username, String code, String uuid) { public void validateCaptcha(String username, String code, String uuid) {
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, ""); String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
String captcha = RedisUtils.getCacheObject(verifyKey); String captcha = RedisUtils.getCacheObject(verifyKey);
RedisUtils.deleteObject(verifyKey); RedisUtils.deleteObject(verifyKey);
if (captcha == null) { if (captcha == null) {
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); recordLoginInfo(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
if (!StringUtils.equalsIgnoreCase(code, captcha)) { if (!StringUtils.equalsIgnoreCase(code, captcha)) {
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); recordLoginInfo(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException(); throw new CaptchaException();
} }
} }
@@ -96,20 +94,17 @@ public class SysRegisterService {
/** /**
* 记录登录信息 * 记录登录信息
* *
* @param tenantId 租户ID
* @param username 用户名 * @param username 用户名
* @param status 状态 * @param status 状态
* @param message 消息内容 * @param message 消息内容
* @return
*/ */
private void recordLogininfor(String tenantId, String username, String status, String message) { private void recordLoginInfo(String username, String status, String message) {
LogininforEvent logininforEvent = new LogininforEvent(); LoginInfoEvent loginInfoEvent = new LoginInfoEvent();
logininforEvent.setTenantId(tenantId); loginInfoEvent.setUsername(username);
logininforEvent.setUsername(username); loginInfoEvent.setStatus(status);
logininforEvent.setStatus(status); loginInfoEvent.setMessage(message);
logininforEvent.setMessage(message); loginInfoEvent.setRequest(ServletUtils.getRequest());
logininforEvent.setRequest(ServletUtils.getRequest()); SpringUtils.context().publishEvent(loginInfoEvent);
SpringUtils.context().publishEvent(logininforEvent);
} }
} }

View File

@@ -9,8 +9,6 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.EmailLoginBody;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.enums.LoginType; import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.exception.user.CaptchaExpireException; import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
@@ -20,7 +18,8 @@ import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.system.api.model.EmailLoginBody;
import org.dromara.system.api.model.LoginUser;
import org.dromara.system.domain.SysUser; import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.domain.vo.SysUserVo;
@@ -43,28 +42,26 @@ public class EmailAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService; private final SysLoginService loginService;
private final SysUserMapper userMapper; private final SysUserMapper userMapper;
/**
* 执行邮箱验证码登录,并按客户端配置生成访问令牌。
*
* @param body 登录请求体
* @param client 当前客户端配置
* @return 登录结果
*/
@Override @Override
public LoginVo login(String body, SysClientVo client) { public LoginVo login(String body, SysClientVo client) {
EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class); EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
ValidatorUtils.validate(loginBody); ValidatorUtils.validate(loginBody);
String tenantId = loginBody.getTenantId();
String email = loginBody.getEmail(); String email = loginBody.getEmail();
String emailCode = loginBody.getEmailCode(); String emailCode = loginBody.getEmailCode();
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> { SysUserVo user = loadUserByEmail(email);
SysUserVo user = loadUserByEmail(email); loginService.checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode)); // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了 LoginUser loginUser = loginService.buildLoginUser(user);
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter(); SaLoginParameter model = IAuthStrategy.buildLoginParameter(client);
model.setDeviceType(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token // 生成token
LoginHelper.login(loginUser, model); LoginHelper.login(loginUser, model);
@@ -76,17 +73,27 @@ public class EmailAuthStrategy implements IAuthStrategy {
} }
/** /**
* 校验邮箱验证码 * 校验邮箱验证码是否存在且匹配。
*
* @param email 邮箱地址
* @param emailCode 用户输入的邮箱验证码
* @return 是否校验通过
*/ */
private boolean validateEmailCode(String tenantId, String email, String emailCode) { private boolean validateEmailCode(String email, String emailCode) {
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email); String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
if (StringUtils.isBlank(code)) { if (StringUtils.isBlank(code)) {
loginService.recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); loginService.recordLoginInfo(email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
return code.equals(emailCode); return code.equals(emailCode);
} }
/**
* 按邮箱加载可登录用户,并校验是否存在或被停用。
*
* @param email 邮箱地址
* @return 用户信息
*/
private SysUserVo loadUserByEmail(String email) { private SysUserVo loadUserByEmail(String email) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email)); SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email));
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {

View File

@@ -10,8 +10,6 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.PasswordLoginBody;
import org.dromara.common.core.enums.LoginType; import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.exception.user.CaptchaException; import org.dromara.common.core.exception.user.CaptchaException;
import org.dromara.common.core.exception.user.CaptchaExpireException; import org.dromara.common.core.exception.user.CaptchaExpireException;
@@ -22,8 +20,9 @@ import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.common.web.config.properties.CaptchaProperties; import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.system.api.model.LoginUser;
import org.dromara.system.api.model.PasswordLoginBody;
import org.dromara.system.domain.SysUser; import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.domain.vo.SysUserVo;
@@ -47,11 +46,17 @@ public class PasswordAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService; private final SysLoginService loginService;
private final SysUserMapper userMapper; private final SysUserMapper userMapper;
/**
* 执行账号密码登录,并按客户端配置生成访问令牌。
*
* @param body 登录请求体
* @param client 当前客户端配置
* @return 登录结果
*/
@Override @Override
public LoginVo login(String body, SysClientVo client) { public LoginVo login(String body, SysClientVo client) {
PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class); PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
ValidatorUtils.validate(loginBody); ValidatorUtils.validate(loginBody);
String tenantId = loginBody.getTenantId();
String username = loginBody.getUsername(); String username = loginBody.getUsername();
String password = loginBody.getPassword(); String password = loginBody.getPassword();
String code = loginBody.getCode(); String code = loginBody.getCode();
@@ -60,23 +65,15 @@ public class PasswordAuthStrategy implements IAuthStrategy {
boolean captchaEnabled = captchaProperties.getEnable(); boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关 // 验证码开关
if (captchaEnabled) { if (captchaEnabled) {
validateCaptcha(tenantId, username, code, uuid); validateCaptcha(username, code, uuid);
} }
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> { SysUserVo user = loadUserByUsername(username);
SysUserVo user = loadUserByUsername(username); loginService.checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword())); // 此处可根据登录用户的数据不同 自行创建 loginUser
// 此处可根据登录用户的数据不同 自行创建 loginUser LoginUser loginUser = loginService.buildLoginUser(user);
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter(); SaLoginParameter model = IAuthStrategy.buildLoginParameter(client);
model.setDeviceType(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token // 生成token
LoginHelper.login(loginUser, model); LoginHelper.login(loginUser, model);
@@ -88,26 +85,32 @@ public class PasswordAuthStrategy implements IAuthStrategy {
} }
/** /**
* 校验验证码 * 校验图形验证码是否有效且匹配。
* *
* @param username 用户名 * @param username 用户名
* @param code 验证码 * @param code 用户输入的验证码
* @param uuid 唯一标识 * @param uuid 验证码缓存标识
*/ */
private void validateCaptcha(String tenantId, String username, String code, String uuid) { private void validateCaptcha(String username, String code, String uuid) {
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, ""); String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
String captcha = RedisUtils.getCacheObject(verifyKey); String captcha = RedisUtils.getCacheObject(verifyKey);
RedisUtils.deleteObject(verifyKey); RedisUtils.deleteObject(verifyKey);
if (captcha == null) { if (captcha == null) {
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); loginService.recordLoginInfo(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
if (!StringUtils.equalsIgnoreCase(code, captcha)) { if (!StringUtils.equalsIgnoreCase(code, captcha)) {
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); loginService.recordLoginInfo(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException(); throw new CaptchaException();
} }
} }
/**
* 按用户名加载可登录用户,并校验是否存在或被停用。
*
* @param username 用户名
* @return 用户信息
*/
private SysUserVo loadUserByUsername(String username) { private SysUserVo loadUserByUsername(String username) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username)); SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {

View File

@@ -9,8 +9,6 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.SmsLoginBody;
import org.dromara.common.core.enums.LoginType; import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.exception.user.CaptchaExpireException; import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
@@ -20,7 +18,8 @@ import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.system.api.model.LoginUser;
import org.dromara.system.api.model.SmsLoginBody;
import org.dromara.system.domain.SysUser; import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.domain.vo.SysUserVo;
@@ -43,28 +42,26 @@ public class SmsAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService; private final SysLoginService loginService;
private final SysUserMapper userMapper; private final SysUserMapper userMapper;
/**
* 执行短信验证码登录,并按客户端配置生成访问令牌。
*
* @param body 登录请求体
* @param client 当前客户端配置
* @return 登录结果
*/
@Override @Override
public LoginVo login(String body, SysClientVo client) { public LoginVo login(String body, SysClientVo client) {
SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class); SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
ValidatorUtils.validate(loginBody); ValidatorUtils.validate(loginBody);
String tenantId = loginBody.getTenantId(); String phoneNumber = loginBody.getPhoneNumber();
String phonenumber = loginBody.getPhonenumber();
String smsCode = loginBody.getSmsCode(); String smsCode = loginBody.getSmsCode();
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> { SysUserVo user = loadUserByPhoneNumber(phoneNumber);
SysUserVo user = loadUserByPhonenumber(phonenumber); loginService.checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phoneNumber, smsCode));
loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode)); // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了 LoginUser loginUser = loginService.buildLoginUser(user);
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter(); SaLoginParameter model = IAuthStrategy.buildLoginParameter(client);
model.setDeviceType(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token // 生成token
LoginHelper.login(loginUser, model); LoginHelper.login(loginUser, model);
@@ -76,25 +73,35 @@ public class SmsAuthStrategy implements IAuthStrategy {
} }
/** /**
* 校验短信验证码 * 校验短信验证码是否存在且匹配。
*
* @param phoneNumber 手机号
* @param smsCode 用户输入的短信验证码
* @return 是否校验通过
*/ */
private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) { private boolean validateSmsCode(String phoneNumber, String smsCode) {
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber); String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber);
if (StringUtils.isBlank(code)) { if (StringUtils.isBlank(code)) {
loginService.recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); loginService.recordLoginInfo(phoneNumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
return code.equals(smsCode); return code.equals(smsCode);
} }
private SysUserVo loadUserByPhonenumber(String phonenumber) { /**
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, phonenumber)); * 按手机号加载可登录用户,并校验是否存在或被停用。
*
* @param phoneNumber 手机号
* @return 用户信息
*/
private SysUserVo loadUserByPhoneNumber(String phoneNumber) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhoneNumber, phoneNumber));
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", phonenumber); log.info("登录用户:{} 不存在.", phoneNumber);
throw new UserException("user.not.exists", phonenumber); throw new UserException("user.not.exists", phoneNumber);
} else if (SystemConstants.DISABLE.equals(user.getStatus())) { } else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phonenumber); log.info("登录用户:{} 已被停用.", phoneNumber);
throw new UserException("user.blocked", phonenumber); throw new UserException("user.blocked", phoneNumber);
} }
return user; return user;
} }

View File

@@ -9,17 +9,15 @@ import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthResponse; import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.model.AuthUser;
import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.SocialLoginBody;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.ValidatorUtils; import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.social.config.properties.SocialProperties; import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.utils.SocialUtils; import org.dromara.common.social.utils.SocialUtils;
import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.system.api.model.LoginUser;
import org.dromara.system.api.model.SocialLoginBody;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysSocialVo; import org.dromara.system.domain.vo.SysSocialVo;
import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.domain.vo.SysUserVo;
@@ -31,7 +29,6 @@ import org.dromara.web.service.SysLoginService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* 第三方授权策略 * 第三方授权策略
@@ -49,10 +46,11 @@ public class SocialAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService; private final SysLoginService loginService;
/** /**
* 登录-第三方授权登录 * 执行第三方授权登录,并校验授权账号与系统账号的绑定关系。
* *
* @param body 登录信息 * @param body 登录信息
* @param client 客户端信息 * @param client 客户端信息
* @return 登录结果
*/ */
@Override @Override
public LoginVo login(String body, SysClientVo client) { public LoginVo login(String body, SysClientVo client) {
@@ -70,30 +68,12 @@ public class SocialAuthStrategy implements IAuthStrategy {
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!"); throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
} }
SysSocialVo social; SysUserVo user = loadUser(list.get(0).getUserId());
if (TenantHelper.isEnable()) { // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
Optional<SysSocialVo> opt = StreamUtils.findAny(list, x -> x.getTenantId().equals(loginBody.getTenantId())); LoginUser loginUser = loginService.buildLoginUser(user);
if (opt.isEmpty()) {
throw new ServiceException("对不起,你没有权限登录当前租户!");
}
social = opt.get();
} else {
social = list.get(0);
}
LoginUser loginUser = TenantHelper.dynamic(social.getTenantId(), () -> {
SysUserVo user = loadUser(social.getUserId());
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter(); SaLoginParameter model = IAuthStrategy.buildLoginParameter(client);
model.setDeviceType(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token // 生成token
LoginHelper.login(loginUser, model); LoginHelper.login(loginUser, model);
@@ -104,6 +84,12 @@ public class SocialAuthStrategy implements IAuthStrategy {
return loginVo; return loginVo;
} }
/**
* 根据用户ID加载用户并校验账号状态是否允许登录。
*
* @param userId 用户ID
* @return 用户信息
*/
private SysUserVo loadUser(Long userId) { private SysUserVo loadUser(Long userId) {
SysUserVo user = userMapper.selectVoById(userId); SysUserVo user = userMapper.selectVoById(userId);
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {

View File

@@ -13,12 +13,12 @@ import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest; import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.request.AuthWechatMiniProgramRequest; import me.zhyd.oauth.request.AuthWechatMiniProgramRequest;
import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.XcxLoginBody;
import org.dromara.common.core.domain.model.XcxLoginUser;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ValidatorUtils; import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.model.XcxLoginBody;
import org.dromara.system.api.model.XcxLoginUser;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.web.domain.vo.LoginVo; import org.dromara.web.domain.vo.LoginVo;
@@ -38,6 +38,13 @@ public class XcxAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService; private final SysLoginService loginService;
/**
* 执行微信小程序登录,并根据 openid 构建小程序用户登录态。
*
* @param body 登录请求体
* @param client 当前客户端配置
* @return 登录结果
*/
@Override @Override
public LoginVo login(String body, SysClientVo client) { public LoginVo login(String body, SysClientVo client) {
XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class); XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
@@ -67,7 +74,6 @@ public class XcxAuthStrategy implements IAuthStrategy {
SysUserVo user = loadUserByOpenid(openid); SysUserVo user = loadUserByOpenid(openid);
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了 // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
XcxLoginUser loginUser = new XcxLoginUser(); XcxLoginUser loginUser = new XcxLoginUser();
loginUser.setTenantId(user.getTenantId());
loginUser.setUserId(user.getUserId()); loginUser.setUserId(user.getUserId());
loginUser.setUsername(user.getUserName()); loginUser.setUsername(user.getUserName());
loginUser.setNickname(user.getNickName()); loginUser.setNickname(user.getNickName());
@@ -76,13 +82,7 @@ public class XcxAuthStrategy implements IAuthStrategy {
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
loginUser.setOpenid(openid); loginUser.setOpenid(openid);
SaLoginParameter model = new SaLoginParameter(); SaLoginParameter model = IAuthStrategy.buildLoginParameter(client);
model.setDeviceType(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token // 生成token
LoginHelper.login(loginUser, model); LoginHelper.login(loginUser, model);
@@ -94,6 +94,12 @@ public class XcxAuthStrategy implements IAuthStrategy {
return loginVo; return loginVo;
} }
/**
* 按 openid 查询小程序绑定用户。
*
* @param openid 小程序用户唯一标识
* @return 绑定的系统用户信息
*/
private SysUserVo loadUserByOpenid(String openid) { private SysUserVo loadUserByOpenid(String openid) {
// 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户 // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
// todo 自行实现 userService.selectUserByOpenid(openid); // todo 自行实现 userService.selectUserByOpenid(openid);

View File

@@ -5,20 +5,15 @@ server:
servlet: servlet:
# 应用的访问路径 # 应用的访问路径
context-path: / context-path: /
# undertow 配置 # jetty 配置
undertow: jetty:
# HTTP post内容的最大大小。当值为-1时默认值为大小是无限的 # HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
max-http-post-size: 1GB max-http-form-post-size: -1
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 512
# 是否分配的直接内存
direct-buffers: true
threads: threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 # 最小线程数
io: 8 min: 8
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载 # 最大线程数
worker: 256 max: 256
captcha: captcha:
# 是否启用验证码校验 # 是否启用验证码校验
@@ -81,8 +76,6 @@ spring:
format: format:
date-time: yyyy-MM-dd HH:mm:ss date-time: yyyy-MM-dd HH:mm:ss
jackson: jackson:
# 日期格式化
date-format: yyyy-MM-dd HH:mm:ss
serialization: serialization:
# 格式化输出 # 格式化输出
indent_output: false indent_output: false
@@ -117,23 +110,6 @@ security:
- /*/api-docs/** - /*/api-docs/**
- /warm-flow-ui/config - /warm-flow-ui/config
# 多租户配置
tenant:
# 是否开启
enable: true
# 排除表
excludes:
- sys_menu
- sys_tenant
- sys_tenant_package
- sys_role_dept
- sys_role_menu
- sys_user_post
- sys_user_role
- sys_client
- sys_oss_config
- flow_spel
# MyBatisPlus配置 # MyBatisPlus配置
# https://baomidou.com/config/ # https://baomidou.com/config/
mybatis-plus: mybatis-plus:
@@ -235,18 +211,14 @@ management:
logfile: logfile:
external-file: ./logs/sys-console.log external-file: ./logs/sys-console.log
--- # 默认/推荐使用sse推送 --- # 统一消息推送配置
sse: message:
enabled: true enabled: true
path: /resource/sse # sse / websocket
transport: sse
--- # websocket # 统一访问路径
websocket: path: /resource/message
# 如果关闭 需要和前端开关一起关闭 # websocket 允许的跨域来源
enabled: false
# 路径
path: /resource/websocket
# 设置访问源地址
allowedOrigins: '*' allowedOrigins: '*'
--- # warm-flow工作流配置 --- # warm-flow工作流配置
@@ -261,3 +233,43 @@ warm-flow:
node-tooltip: true node-tooltip: true
# 默认Authorization如果有多个token用逗号分隔 # 默认Authorization如果有多个token用逗号分隔
token-name: ${sa-token.token-name},clientid token-name: ${sa-token.token-name},clientid
--- # mqtt 配置
# 具体配置还需查看文档
# https://mica-mqtt.dreamlu.net/guide/spring/client.html
mqtt.client:
# 是否开启客户端默认true
enabled: false
# 连接的服务端 ip 默认127.0.0.1
ip: 127.0.0.1
# 端口默认1883
port: 1883
# 客户端名称
name: Mqtt-Client
# 客户端Id非常重要一般为设备 sn不可重复
client-id: 000001
username: ruoyi
password: 123456
# 超时时间单位默认5秒
timeout: 5
# 重连时间,默认 5000 毫秒
re-interval: 5000
# mqtt 协议版本,可选 MQTT_3_1、mqtt_3_1_1、mqtt_5默认mqtt_3_1_1
version: mqtt_3_1_1
# 接收数据的 buffer size默认8k
read-buffer-size: 8KB
# 消息解析最大 bytes 长度默认10M
max-bytes-in-message: 10MB
# keep-alive 时间,单位:秒
keep-alive-secs: 60
# 开启保留 session 时session 的有效期
session-expiry-interval-secs: 0
# 工作线程数,如果消息量比较大,例如做 emqx 的转发消息处理,可以调大此参数
mqtt-executor-size: 2
# 是否开启 ssl
ssl:
enabled: false
keystore-path:
keystore-pass:
truststore-path:
truststore-pass:

View File

@@ -55,8 +55,4 @@ xcx.code.not.blank=小程序[code]不能为空
social.source.not.blank=第三方登录平台[source]不能为空 social.source.not.blank=第三方登录平台[source]不能为空
social.code.not.blank=第三方登录平台[code]不能为空 social.code.not.blank=第三方登录平台[code]不能为空
social.state.not.blank=第三方登录平台[state]不能为空 social.state.not.blank=第三方登录平台[state]不能为空
##租户
tenant.number.not.blank=租户编号不能为空
tenant.not.exists=对不起, 您的租户不存在,请联系管理员
tenant.blocked=对不起,您的租户已禁用,请联系管理员
tenant.expired=对不起,您的租户已过期,请联系管理员

View File

@@ -55,8 +55,4 @@ xcx.code.not.blank=Mini program [code] cannot be blank
social.source.not.blank=Social login platform [source] cannot be blank social.source.not.blank=Social login platform [source] cannot be blank
social.code.not.blank=Social login platform [code] cannot be blank social.code.not.blank=Social login platform [code] cannot be blank
social.state.not.blank=Social login platform [state] cannot be blank social.state.not.blank=Social login platform [state] cannot be blank
##租户
tenant.number.not.blank=Tenant number cannot be blank
tenant.not.exists=Sorry, your tenant does not exist. Please contact the administrator
tenant.blocked=Sorry, your tenant is disabled. Please contact the administrator
tenant.expired=Sorry, your tenant has expired. Please contact the administrator.

View File

@@ -55,8 +55,4 @@ xcx.code.not.blank=小程序[code]不能为空
social.source.not.blank=第三方登录平台[source]不能为空 social.source.not.blank=第三方登录平台[source]不能为空
social.code.not.blank=第三方登录平台[code]不能为空 social.code.not.blank=第三方登录平台[code]不能为空
social.state.not.blank=第三方登录平台[state]不能为空 social.state.not.blank=第三方登录平台[state]不能为空
##租户
tenant.number.not.blank=租户编号不能为空
tenant.not.exists=对不起, 您的租户不存在,请联系管理员
tenant.blocked=对不起,您的租户已禁用,请联系管理员
tenant.expired=对不起,您的租户已过期,请联系管理员

View File

@@ -3,16 +3,16 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>ruoyi-vue-plus</artifactId>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version> <version>${revision}</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-common-ratelimiter</artifactId> <artifactId>ruoyi-api</artifactId>
<description> <description>
ruoyi-common-ratelimiter 限流功能 ruoyi-api系统接口
</description> </description>
<dependencies> <dependencies>
@@ -20,11 +20,6 @@
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId> <artifactId>ruoyi-common-core</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.service; package org.dromara.system.api;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Dict;

View File

@@ -1,7 +1,8 @@
package org.dromara.common.core.service; package org.dromara.system.api;
import org.dromara.common.core.domain.dto.DeptDTO; import org.dromara.system.api.domain.DeptDTO;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -41,6 +42,6 @@ public interface DeptService {
* @param deptIds 部门 ID 列表 * @param deptIds 部门 ID 列表
* @return Map其中 key 为部门 IDvalue 为对应的部门名称 * @return Map其中 key 为部门 IDvalue 为对应的部门名称
*/ */
Map<Long, String> selectDeptNamesByIds(List<Long> deptIds); Map<Long, String> selectDeptNamesByIds(Collection<Long> deptIds);
} }

View File

@@ -0,0 +1,66 @@
package org.dromara.system.api;
import org.dromara.system.api.domain.PushPayloadDTO;
import java.util.List;
/**
* 通用 消息服务
*
* @author Lion Li
*/
public interface MessageService {
/**
* 发送指定用户文本消息
*
* @param userId 目标用户ID
* @param message 文本消息内容
*/
void sendMessage(Long userId, String message);
/**
* 全局广播文本消息
*
* @param message 文本消息内容
*/
void sendMessage(String message);
/**
* 发送指定用户自定义消息体
*
* @param userId 目标用户ID
* @param payload 消息推送体
*/
void sendMessage(Long userId, PushPayloadDTO payload);
/**
* 全局广播自定义消息体
*
* @param payload 消息推送体
*/
void sendMessage(PushPayloadDTO payload);
/**
* 批量发布消息给指定用户列表
*
* @param userIds 用户ID集合
* @param payload 消息推送体
*/
void publishMessage(List<Long> userIds, PushPayloadDTO payload);
/**
* 发布全局广播文本消息
*
* @param message 文本消息内容
*/
void publishAll(String message);
/**
* 发布全局广播自定义消息体
*
* @param payload 消息推送体
*/
void publishAll(PushPayloadDTO payload);
}

View File

@@ -1,6 +1,6 @@
package org.dromara.common.core.service; package org.dromara.system.api;
import org.dromara.common.core.domain.dto.OssDTO; import org.dromara.system.api.domain.OssDTO;
import java.util.List; import java.util.List;

View File

@@ -1,6 +1,6 @@
package org.dromara.common.core.service; package org.dromara.system.api;
import java.util.List; import java.util.Collection;
import java.util.Map; import java.util.Map;
/** /**
@@ -16,6 +16,6 @@ public interface PostService {
* @param postIds 岗位 ID 列表 * @param postIds 岗位 ID 列表
* @return Map其中 key 为岗位 IDvalue 为对应的岗位名称 * @return Map其中 key 为岗位 IDvalue 为对应的岗位名称
*/ */
Map<Long, String> selectPostNamesByIds(List<Long> postIds); Map<Long, String> selectPostNamesByIds(Collection<Long> postIds);
} }

View File

@@ -1,6 +1,6 @@
package org.dromara.common.core.service; package org.dromara.system.api;
import java.util.List; import java.util.Collection;
import java.util.Map; import java.util.Map;
/** /**
@@ -16,6 +16,6 @@ public interface RoleService {
* @param roleIds 角色 ID 列表 * @param roleIds 角色 ID 列表
* @return Map其中 key 为角色 IDvalue 为对应的角色名称 * @return Map其中 key 为角色 IDvalue 为对应的角色名称
*/ */
Map<Long, String> selectRoleNamesByIds(List<Long> roleIds); Map<Long, String> selectRoleNamesByIds(Collection<Long> roleIds);
} }

View File

@@ -1,7 +1,7 @@
package org.dromara.common.core.service; package org.dromara.system.api;
import org.dromara.common.core.domain.dto.TaskAssigneeDTO; import org.dromara.system.api.domain.TaskAssigneeDTO;
import org.dromara.common.core.domain.model.TaskAssigneeBody; import org.dromara.system.api.model.TaskAssigneeBody;
/** /**
* 工作流设计器获取任务执行人 * 工作流设计器获取任务执行人

View File

@@ -1,7 +1,8 @@
package org.dromara.common.core.service; package org.dromara.system.api;
import org.dromara.common.core.domain.dto.UserDTO; import org.dromara.system.api.domain.UserDTO;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -52,13 +53,21 @@ public interface UserService {
*/ */
String selectEmailById(Long userId); String selectEmailById(Long userId);
/**
* 通过用户ID查询用户
*
* @param userId 用户id
* @return 用户列表
*/
UserDTO selectById(Long userId);
/** /**
* 通过用户ID查询用户列表 * 通过用户ID查询用户列表
* *
* @param userIds 用户ids * @param userIds 用户ids
* @return 用户列表 * @return 用户列表
*/ */
List<UserDTO> selectListByIds(List<Long> userIds); List<UserDTO> selectListByIds(Collection<Long> userIds);
/** /**
* 通过角色ID查询用户ID * 通过角色ID查询用户ID
@@ -66,7 +75,7 @@ public interface UserService {
* @param roleIds 角色ids * @param roleIds 角色ids
* @return 用户ids * @return 用户ids
*/ */
List<Long> selectUserIdsByRoleIds(List<Long> roleIds); List<Long> selectUserIdsByRoleIds(Collection<Long> roleIds);
/** /**
* 通过角色ID查询用户 * 通过角色ID查询用户
@@ -74,7 +83,7 @@ public interface UserService {
* @param roleIds 角色ids * @param roleIds 角色ids
* @return 用户 * @return 用户
*/ */
List<UserDTO> selectUsersByRoleIds(List<Long> roleIds); List<UserDTO> selectUsersByRoleIds(Collection<Long> roleIds);
/** /**
* 通过部门ID查询用户 * 通过部门ID查询用户
@@ -82,7 +91,7 @@ public interface UserService {
* @param deptIds 部门ids * @param deptIds 部门ids
* @return 用户 * @return 用户
*/ */
List<UserDTO> selectUsersByDeptIds(List<Long> deptIds); List<UserDTO> selectUsersByDeptIds(Collection<Long> deptIds);
/** /**
* 通过岗位ID查询用户 * 通过岗位ID查询用户
@@ -90,7 +99,7 @@ public interface UserService {
* @param postIds 岗位ids * @param postIds 岗位ids
* @return 用户 * @return 用户
*/ */
List<UserDTO> selectUsersByPostIds(List<Long> postIds); List<UserDTO> selectUsersByPostIds(Collection<Long> postIds);
/** /**
* 根据用户 ID 列表查询用户昵称映射关系 * 根据用户 ID 列表查询用户昵称映射关系
@@ -98,6 +107,6 @@ public interface UserService {
* @param userIds 用户 ID 列表 * @param userIds 用户 ID 列表
* @return Map其中 key 为用户 IDvalue 为对应的用户昵称 * @return Map其中 key 为用户 IDvalue 为对应的用户昵称
*/ */
Map<Long, String> selectUserNicksByIds(List<Long> userIds); Map<Long, String> selectUserNicksByIds(Collection<Long> userIds);
} }

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.dto; package org.dromara.system.api.domain;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.dto; package org.dromara.system.api.domain;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -7,7 +7,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
/** /**
* OSS对象 * OSS 文件简要信息对象
* *
* @author Lion Li * @author Lion Li
*/ */

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.dto; package org.dromara.system.api.domain;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -7,7 +7,7 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
/** /**
* 岗位 * 岗位简要信息对象
* *
* @author AprilWind * @author AprilWind
*/ */

View File

@@ -0,0 +1,81 @@
package org.dromara.system.api.domain;
import lombok.Data;
import org.dromara.common.core.enums.PushSourceEnum;
import org.dromara.common.core.enums.PushTypeEnum;
import org.dromara.common.core.utils.StringUtils;
import java.io.Serial;
import java.io.Serializable;
/**
* 推送给前端的统一消息体
*
* @author Lion Li
*/
@Data
public class PushPayloadDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 消息记录ID
*/
private Long messageId;
/**
* 消息类型
*/
private String type;
/**
* 消息来源
*/
private String source;
/**
* 文本消息
*/
private String message;
/**
* 扩展数据
*/
private Object data;
/**
* 前端跳转路径
*/
private String path;
/**
* 时间戳
*/
private Long timestamp;
public static PushPayloadDTO of(String type, String source, String message, Object data) {
PushPayloadDTO payload = new PushPayloadDTO();
payload.setType(StringUtils.defaultIfBlank(type, PushTypeEnum.MESSAGE.getType()));
payload.setSource(StringUtils.defaultIfBlank(source, PushSourceEnum.BACKEND.getSource()));
payload.setMessage(message);
payload.setData(data);
payload.setTimestamp(System.currentTimeMillis());
return payload;
}
public static PushPayloadDTO of(PushTypeEnum type, PushSourceEnum source, String message, Object data) {
return of(
type == null ? null : type.getType(),
source == null ? null : source.getSource(),
message,
data
);
}
public static PushPayloadDTO of(PushTypeEnum type, PushSourceEnum source, String message, Object data, String path) {
PushPayloadDTO payload = of(type, source, message, data);
payload.setPath(path);
return payload;
}
}

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.dto; package org.dromara.system.api.domain;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -7,11 +7,10 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
/** /**
* 角色 * 角色简要信息对象
* *
* @author Lion Li * @author Lion Li
*/ */
@Data @Data
@NoArgsConstructor @NoArgsConstructor
public class RoleDTO implements Serializable { public class RoleDTO implements Serializable {

View File

@@ -1,15 +1,16 @@
package org.dromara.common.core.domain.dto; package org.dromara.system.api.domain;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.StreamUtils;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
/** /**
* 任务受让人 * 任务受让人
@@ -29,7 +30,7 @@ public class TaskAssigneeDTO implements Serializable {
private Long total = 0L; private Long total = 0L;
/** /**
* * 受让人列表
*/ */
private List<TaskHandler> list; private List<TaskHandler> list;
@@ -42,7 +43,7 @@ public class TaskAssigneeDTO implements Serializable {
* 将源列表转换为 TaskHandler 列表 * 将源列表转换为 TaskHandler 列表
* *
* @param <T> 通用类型 * @param <T> 通用类型
* @param sourceList 待转换的源列表 * @param sourceCollection 待转换的源列表
* @param storageId 提取 storageId 的函数 * @param storageId 提取 storageId 的函数
* @param handlerCode 提取 handlerCode 的函数 * @param handlerCode 提取 handlerCode 的函数
* @param handlerName 提取 handlerName 的函数 * @param handlerName 提取 handlerName 的函数
@@ -51,22 +52,26 @@ public class TaskAssigneeDTO implements Serializable {
* @return 转换后的 TaskHandler 列表 * @return 转换后的 TaskHandler 列表
*/ */
public static <T> List<TaskHandler> convertToHandlerList( public static <T> List<TaskHandler> convertToHandlerList(
List<T> sourceList, Collection<T> sourceCollection,
Function<T, String> storageId, Function<T, String> storageId,
Function<T, String> handlerCode, Function<T, String> handlerCode,
Function<T, String> handlerName, Function<T, String> handlerName,
Function<T, String> groupName, Function<T, String> groupName,
Function<T, Date> createTimeMapper) { Function<T, LocalDateTime> createTimeMapper) {
return sourceList.stream() return StreamUtils.toList(sourceCollection, item ->
.map(item -> new TaskHandler( new TaskHandler(
storageId.apply(item), storageId.apply(item),
handlerCode.apply(item), handlerCode.apply(item),
handlerName.apply(item), handlerName.apply(item),
groupName.apply(item), groupName.apply(item),
createTimeMapper.apply(item) createTimeMapper.apply(item)
)).collect(Collectors.toList()); )
);
} }
/**
* 任务受让人明细对象
*/
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@@ -95,7 +100,7 @@ public class TaskAssigneeDTO implements Serializable {
/** /**
* 创建时间 * 创建时间
*/ */
private Date createTime; private LocalDateTime createTime;
} }
} }

View File

@@ -1,11 +1,11 @@
package org.dromara.common.core.domain.dto; package org.dromara.system.api.domain;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.time.LocalDateTime;
/** /**
@@ -53,7 +53,7 @@ public class UserDTO implements Serializable {
/** /**
* 手机号码 * 手机号码
*/ */
private String phonenumber; private String phoneNumber;
/** /**
* 用户性别0男 1女 2未知 * 用户性别0男 1女 2未知
@@ -68,6 +68,6 @@ public class UserDTO implements Serializable {
/** /**
* 创建时间 * 创建时间
*/ */
private Date createTime; private LocalDateTime createTime;
} }

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.dto; package org.dromara.system.api.domain;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -7,11 +7,10 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
/** /**
* 当前在线会话 * 当前在线会话信息对象
* *
* @author ruoyi * @author ruoyi
*/ */
@Data @Data
@NoArgsConstructor @NoArgsConstructor
public class UserOnlineDTO implements Serializable { public class UserOnlineDTO implements Serializable {

View File

@@ -1,16 +1,16 @@
package org.dromara.common.core.domain.model; package org.dromara.system.api.model;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.dromara.common.core.domain.model.LoginBody;
/** /**
* 件登录对象 * 箱验证码登录请求对象
* *
* @author Lion Li * @author Lion Li
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class EmailLoginBody extends LoginBody { public class EmailLoginBody extends LoginBody {

View File

@@ -1,17 +1,18 @@
package org.dromara.common.core.domain.model; package org.dromara.system.api.model;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.dromara.common.core.domain.dto.PostDTO; import org.dromara.system.api.domain.PostDTO;
import org.dromara.common.core.domain.dto.RoleDTO; import org.dromara.system.api.domain.RoleDTO;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
* 登录用户身份权限 * 登录用户上下文对象保存当前会话的身份权限和终端信息
* *
* @author Lion Li * @author Lion Li
*/ */
@@ -22,11 +23,6 @@ public class LoginUser implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/** /**
* 用户ID * 用户ID
*/ */
@@ -112,6 +108,11 @@ public class LoginUser implements Serializable {
*/ */
private List<RoleDTO> roles; private List<RoleDTO> roles;
/**
* 数据权限角色映射 key 为权限码 value 为可参与数据权限计算的角色ID列表
*/
private Map<String, List<Long>> dataScopeRoleMap;
/** /**
* 岗位对象 * 岗位对象
*/ */
@@ -133,7 +134,9 @@ public class LoginUser implements Serializable {
private String deviceType; private String deviceType;
/** /**
* 获取登录id * 获取 Sa-Token 使用的登录标识
*
* @return 登录标识
*/ */
public String getLoginId() { public String getLoginId() {
if (userType == null) { if (userType == null) {

View File

@@ -1,8 +1,9 @@
package org.dromara.common.core.domain.model; package org.dromara.system.api.model;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.dromara.common.core.domain.model.LoginBody;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;
/** /**

View File

@@ -1,8 +1,9 @@
package org.dromara.common.core.domain.model; package org.dromara.system.api.model;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.dromara.common.core.domain.model.LoginBody;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;
/** /**

View File

@@ -1,15 +1,15 @@
package org.dromara.common.core.domain.model; package org.dromara.system.api.model;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.dromara.common.core.domain.model.LoginBody;
/** /**
* 短信登录对象 * 短信验证码登录请求对象
* *
* @author Lion Li * @author Lion Li
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class SmsLoginBody extends LoginBody { public class SmsLoginBody extends LoginBody {
@@ -18,7 +18,7 @@ public class SmsLoginBody extends LoginBody {
* 手机号 * 手机号
*/ */
@NotBlank(message = "{user.phonenumber.not.blank}") @NotBlank(message = "{user.phonenumber.not.blank}")
private String phonenumber; private String phoneNumber;
/** /**
* 短信code * 短信code

View File

@@ -1,15 +1,15 @@
package org.dromara.common.core.domain.model; package org.dromara.system.api.model;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.dromara.common.core.domain.model.LoginBody;
/** /**
* 三方登录对象 * 三方平台登录绑定请求对象
* *
* @author Lion Li * @author Lion Li
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class SocialLoginBody extends LoginBody { public class SocialLoginBody extends LoginBody {

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.model; package org.dromara.system.api.model;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;

View File

@@ -1,15 +1,15 @@
package org.dromara.common.core.domain.model; package org.dromara.system.api.model;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.dromara.common.core.domain.model.LoginBody;
/** /**
* 三方登录对象 * 小程序登录请求对象
* *
* @author Lion Li * @author Lion Li
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class XcxLoginBody extends LoginBody { public class XcxLoginBody extends LoginBody {

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.model; package org.dromara.system.api.model;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -7,7 +7,7 @@ import lombok.NoArgsConstructor;
import java.io.Serial; import java.io.Serial;
/** /**
* 小程序登录用户身份权限 * 小程序登录用户上下文对象
* *
* @author Lion Li * @author Lion Li
*/ */
@@ -20,7 +20,7 @@ public class XcxLoginUser extends LoginUser {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* openid * 小程序 openid
*/ */
private String openid; private String openid;

View File

@@ -1,8 +1,8 @@
package org.dromara.common.core.service; package org.dromara.workflow.api;
import org.dromara.common.core.domain.dto.CompleteTaskDTO; import org.dromara.workflow.api.domain.CompleteTaskDTO;
import org.dromara.common.core.domain.dto.StartProcessDTO; import org.dromara.workflow.api.domain.StartProcessDTO;
import org.dromara.common.core.domain.dto.StartProcessReturnDTO; import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -50,6 +50,7 @@ public interface WorkflowService {
* 获取流程变量 * 获取流程变量
* *
* @param instanceId 流程实例id * @param instanceId 流程实例id
* @return 流程变量详情
*/ */
Map<String, Object> instanceVariable(Long instanceId); Map<String, Object> instanceVariable(Long instanceId);
@@ -61,18 +62,11 @@ public interface WorkflowService {
*/ */
Long getInstanceIdByBusinessId(String businessId); Long getInstanceIdByBusinessId(String businessId);
/**
* 新增租户流程定义
*
* @param tenantId 租户id
*/
void syncDef(String tenantId);
/** /**
* 启动流程 * 启动流程
* *
* @param startProcess 参数 * @param startProcess 参数
* @return 结果 * @return 启动后的流程实例与首任务信息
*/ */
StartProcessReturnDTO startWorkFlow(StartProcessDTO startProcess); StartProcessReturnDTO startWorkFlow(StartProcessDTO startProcess);
@@ -82,7 +76,7 @@ public interface WorkflowService {
* completeTask.getVariables().put("ignore", true); * completeTask.getVariables().put("ignore", true);
* *
* @param completeTask 参数 * @param completeTask 参数
* @return 结果 * @return 办理成功返回 {@code true}
*/ */
boolean completeTask(CompleteTaskDTO completeTask); boolean completeTask(CompleteTaskDTO completeTask);
@@ -91,7 +85,7 @@ public interface WorkflowService {
* *
* @param taskId 任务ID * @param taskId 任务ID
* @param message 办理意见 * @param message 办理意见
* @return 结果 * @return 办理成功返回 {@code true}
*/ */
boolean completeTask(Long taskId, String message); boolean completeTask(Long taskId, String message);
@@ -99,7 +93,7 @@ public interface WorkflowService {
* 启动流程并办理第一个任务 * 启动流程并办理第一个任务
* *
* @param startProcess 参数 * @param startProcess 参数
* @return 结果 * @return 首节点办理成功返回 {@code true}
*/ */
boolean startCompleteTask(StartProcessDTO startProcess); boolean startCompleteTask(StartProcessDTO startProcess);
} }

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.dto; package org.dromara.workflow.api.domain;
import lombok.Data; import lombok.Data;
@@ -65,6 +65,11 @@ public class CompleteTaskDTO implements Serializable {
*/ */
private String ext; private String ext;
/**
* 获取流程变量并自动剔除空值
*
* @return 流程变量
*/
public Map<String, Object> getVariables() { public Map<String, Object> getVariables() {
if (variables == null) { if (variables == null) {
variables = new HashMap<>(16); variables = new HashMap<>(16);

View File

@@ -0,0 +1,14 @@
package org.dromara.workflow.api.domain;
/**
* 抄送
*
* @param userId 抄送用户 ID
* @param nickName 抄送用户昵称
* @author may
*/
public record FlowCopyDTO(
Long userId,
String nickName
) {
}

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.dto; package org.dromara.workflow.api.domain;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.dto; package org.dromara.workflow.api.domain;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
@@ -11,7 +11,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
/** /**
* 启动流程对象 * 启动流程请求对象
* *
* @author may * @author may
*/ */
@@ -46,6 +46,11 @@ public class StartProcessDTO implements Serializable {
*/ */
private FlowInstanceBizExtDTO bizExt; private FlowInstanceBizExtDTO bizExt;
/**
* 获取流程变量并自动剔除空值
*
* @return 流程变量
*/
public Map<String, Object> getVariables() { public Map<String, Object> getVariables() {
if (variables == null) { if (variables == null) {
variables = new HashMap<>(16); variables = new HashMap<>(16);
@@ -55,6 +60,11 @@ public class StartProcessDTO implements Serializable {
return variables; return variables;
} }
/**
* 获取流程业务扩展信息为空时返回默认对象
*
* @return 业务扩展信息
*/
public FlowInstanceBizExtDTO getBizExt() { public FlowInstanceBizExtDTO getBizExt() {
if (ObjectUtil.isNull(bizExt)) { if (ObjectUtil.isNull(bizExt)) {
bizExt = new FlowInstanceBizExtDTO(); bizExt = new FlowInstanceBizExtDTO();

View File

@@ -0,0 +1,14 @@
package org.dromara.workflow.api.domain;
/**
* 启动流程后的返回结果对象。
*
* @param processInstanceId 流程实例 ID
* @param taskId 首个任务 ID
* @author Lion Li
*/
public record StartProcessReturnDTO(
Long processInstanceId,
Long taskId
) {
}

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.event; package org.dromara.workflow.api.event;
import lombok.Data; import lombok.Data;
@@ -16,11 +16,6 @@ public class ProcessDeleteEvent implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/** /**
* 流程定义编码 * 流程定义编码
*/ */

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.event; package org.dromara.workflow.api.event;
import lombok.Data; import lombok.Data;
@@ -17,11 +17,6 @@ public class ProcessEvent implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/** /**
* 流程定义编码 * 流程定义编码
*/ */

View File

@@ -1,4 +1,4 @@
package org.dromara.common.core.domain.event; package org.dromara.workflow.api.event;
import lombok.Data; import lombok.Data;
@@ -17,11 +17,6 @@ public class ProcessTaskEvent implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/** /**
* 流程定义编码 * 流程定义编码
*/ */

View File

@@ -8,6 +8,12 @@
<version>${revision}</version> <version>${revision}</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common</artifactId>
<packaging>pom</packaging>
<description>
common 通用模块
</description>
<modules> <modules>
<module>ruoyi-common-bom</module> <module>ruoyi-common-bom</module>
@@ -15,13 +21,11 @@
<module>ruoyi-common-core</module> <module>ruoyi-common-core</module>
<module>ruoyi-common-doc</module> <module>ruoyi-common-doc</module>
<module>ruoyi-common-excel</module> <module>ruoyi-common-excel</module>
<module>ruoyi-common-idempotent</module>
<module>ruoyi-common-job</module> <module>ruoyi-common-job</module>
<module>ruoyi-common-log</module> <module>ruoyi-common-log</module>
<module>ruoyi-common-mail</module> <module>ruoyi-common-mail</module>
<module>ruoyi-common-mybatis</module> <module>ruoyi-common-mybatis</module>
<module>ruoyi-common-oss</module> <module>ruoyi-common-oss</module>
<module>ruoyi-common-ratelimiter</module>
<module>ruoyi-common-redis</module> <module>ruoyi-common-redis</module>
<module>ruoyi-common-satoken</module> <module>ruoyi-common-satoken</module>
<module>ruoyi-common-security</module> <module>ruoyi-common-security</module>
@@ -31,16 +35,8 @@
<module>ruoyi-common-sensitive</module> <module>ruoyi-common-sensitive</module>
<module>ruoyi-common-json</module> <module>ruoyi-common-json</module>
<module>ruoyi-common-encrypt</module> <module>ruoyi-common-encrypt</module>
<module>ruoyi-common-tenant</module> <module>ruoyi-common-push</module>
<module>ruoyi-common-websocket</module> <module>ruoyi-common-mqtt</module>
<module>ruoyi-common-sse</module>
</modules> </modules>
<artifactId>ruoyi-common</artifactId>
<packaging>pom</packaging>
<description>
common 通用模块
</description>
</project> </project>

View File

@@ -14,7 +14,7 @@
</description> </description>
<properties> <properties>
<revision>5.6.0</revision> <revision>5.5.3</revision>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@@ -40,13 +40,6 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- 幂等 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-idempotent</artifactId>
<version>${revision}</version>
</dependency>
<!-- 调度模块 --> <!-- 调度模块 -->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
@@ -82,13 +75,6 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- 限流 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ratelimiter</artifactId>
<version>${revision}</version>
</dependency>
<!-- 缓存服务 --> <!-- 缓存服务 -->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
@@ -117,6 +103,7 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- 授权认证 -->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-social</artifactId> <artifactId>ruoyi-common-social</artifactId>
@@ -158,24 +145,17 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- 租户模块 --> <!-- 消息推送模块 -->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-tenant</artifactId> <artifactId>ruoyi-common-push</artifactId>
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- WebSocket模块 --> <!-- mqtt模块 -->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-websocket</artifactId> <artifactId>ruoyi-common-mqtt</artifactId>
<version>${revision}</version>
</dependency>
<!-- SSE模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sse</artifactId>
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>

View File

@@ -36,7 +36,7 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId> <artifactId>spring-boot-starter-aspectj</artifactId>
</dependency> </dependency>
<!--常用工具类 --> <!--常用工具类 -->

View File

@@ -28,6 +28,8 @@ public class ThreadPoolConfig {
/** /**
* 执行周期性或定时任务 * 执行周期性或定时任务
*
* @return 全局定时任务线程池
*/ */
@Bean(name = "scheduledExecutorService") @Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() { protected ScheduledExecutorService scheduledExecutorService() {
@@ -85,6 +87,9 @@ public class ThreadPoolConfig {
/** /**
* 打印线程异常信息 * 打印线程异常信息
*
* @param r 已执行的任务
* @param t 任务执行过程中抛出的异常
*/ */
public static void printException(Runnable r, Throwable t) { public static void printException(Runnable r, Throwable t) {
if (t == null && r instanceof Future<?>) { if (t == null && r instanceof Future<?>) {

View File

@@ -3,7 +3,7 @@ package org.dromara.common.core.config;
import jakarta.validation.Validator; import jakarta.validation.Validator;
import org.hibernate.validator.HibernateValidator; import org.hibernate.validator.HibernateValidator;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@@ -20,6 +20,9 @@ public class ValidatorConfig {
/** /**
* 配置校验框架 快速失败模式 * 配置校验框架 快速失败模式
*
* @param messageSource 国际化消息源
* @return 启用快速失败模式的校验器
*/ */
@Bean @Bean
public Validator validator(MessageSource messageSource) { public Validator validator(MessageSource messageSource) {

View File

@@ -1,30 +0,0 @@
package org.dromara.common.core.constant;
/**
* 缓存的key 常量
*
* @author Lion Li
*/
public interface CacheConstants {
/**
* 在线用户 redis key
*/
String ONLINE_TOKEN_KEY = "online_tokens:";
/**
* 参数管理 cache key
*/
String SYS_CONFIG_KEY = "sys_config:";
/**
* 字典管理 cache key
*/
String SYS_DICT_KEY = "sys_dict:";
/**
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
}

View File

@@ -1,7 +1,7 @@
package org.dromara.common.core.constant; package org.dromara.common.core.constant;
/** /**
* 缓存组名称常量 * 缓存组名称常量,统一约定缓存名和缓存策略配置格式。
* <p> * <p>
* key 格式为 cacheNames#ttl#maxIdleTime#maxSize#local * key 格式为 cacheNames#ttl#maxIdleTime#maxSize#local
* <p> * <p>
@@ -36,15 +36,10 @@ public interface CacheNames {
*/ */
String SYS_DICT_TYPE = "sys_dict_type"; String SYS_DICT_TYPE = "sys_dict_type";
/**
* 租户
*/
String SYS_TENANT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_tenant#30d";
/** /**
* 客户端 * 客户端
*/ */
String SYS_CLIENT = GlobalConstants.GLOBAL_REDIS_KEY + "sys_client#30d"; String SYS_CLIENT = "sys_client#30d";
/** /**
* 用户账户 * 用户账户
@@ -79,11 +74,16 @@ public interface CacheNames {
/** /**
* OSS配置 * OSS配置
*/ */
String SYS_OSS_CONFIG = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss_config"; String SYS_OSS_CONFIG = "sys_oss_config";
/** /**
* 在线用户 * 在线用户
*/ */
String ONLINE_TOKEN = "online_tokens"; String ONLINE_TOKEN_KEY = "online_tokens:";
/**
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
} }

View File

@@ -1,7 +1,7 @@
package org.dromara.common.core.constant; package org.dromara.common.core.constant;
/** /**
* 通用常量信息 * 通用基础常量定义。
* *
* @author ruoyi * @author ruoyi
*/ */

View File

@@ -1,7 +1,7 @@
package org.dromara.common.core.constant; package org.dromara.common.core.constant;
/** /**
* 全局的key常量 (业务无关的key) * 全局通用键常量,主要用于业务无关的 Redis Key 前缀定义。
* *
* @author Lion Li * @author Lion Li
*/ */

View File

@@ -7,7 +7,7 @@ import cn.hutool.core.lang.RegexPool;
* <p> * <p>
* 常用正则表达式集合,更多正则见: https://any86.github.io/any-rule/ * 常用正则表达式集合,更多正则见: https://any86.github.io/any-rule/
* *
* @author Feng * @author AprilWind
*/ */
public interface RegexConstants extends RegexPool { public interface RegexConstants extends RegexPool {

View File

@@ -18,25 +18,15 @@ public interface SystemConstants {
String DISABLE = "1"; String DISABLE = "1";
/** /**
* 是否为系统默认(是) * 是
*/ */
String YES = "Y"; String YES = "Y";
/** /**
* 是否为系统默认(否) *
*/ */
String NO = "N"; String NO = "N";
/**
* 是否菜单外链(是)
*/
String YES_FRAME = "0";
/**
* 是否菜单外链(否)
*/
String NO_FRAME = "1";
/** /**
* 菜单类型(目录) * 菜单类型(目录)
*/ */
@@ -68,9 +58,19 @@ public interface SystemConstants {
String INNER_LINK = "InnerLink"; String INNER_LINK = "InnerLink";
/** /**
* 超级管理员ID * 超级管理员用户ID
*/ */
Long SUPER_ADMIN_ID = 1L; Long SUPER_ADMIN_USER_ID = 1761100000000000001L;
/**
* 超级管理员角色ID
*/
Long SUPER_ADMIN_ROLE_ID = 1761300000000000001L;
/**
* 超级管理员角色 roleKey
*/
String SUPER_ADMIN_ROLE_KEY = "superadmin";
/** /**
* 根部门祖级列表 * 根部门祖级列表
@@ -80,7 +80,7 @@ public interface SystemConstants {
/** /**
* 默认部门 ID * 默认部门 ID
*/ */
Long DEFAULT_DEPT_ID = 100L; Long DEFAULT_DEPT_ID = 1761000000000000100L;
/** /**
* 排除敏感属性字段 * 排除敏感属性字段

View File

@@ -1,35 +0,0 @@
package org.dromara.common.core.constant;
/**
* 租户常量信息
*
* @author Lion Li
*/
public interface TenantConstants {
/**
* 超级管理员ID
*/
Long SUPER_ADMIN_ID = 1L;
/**
* 超级管理员角色 roleKey
*/
String SUPER_ADMIN_ROLE_KEY = "superadmin";
/**
* 租户管理员角色 roleKey
*/
String TENANT_ADMIN_ROLE_KEY = "admin";
/**
* 租户管理员角色名称
*/
String TENANT_ADMIN_ROLE_NAME = "管理员";
/**
* 默认租户ID
*/
String DEFAULT_TENANT_ID = "000000";
}

View File

@@ -0,0 +1,70 @@
package org.dromara.common.core.domain;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collection;
/**
* 表格分页数据对象
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class PageResult<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 总记录数
*/
private long total;
/**
* 列表数据
*/
private Collection<T> rows;
/**
* 分页
*
* @param list 列表数据
* @param total 总记录数
*/
public PageResult(Collection<T> list, long total) {
this.rows = list;
this.total = total;
}
/**
* 根据分页对象构建表格分页数据对象
*/
public static <T> PageResult<T> build(Collection<T> list, long total) {
PageResult<T> rspData = new PageResult<>();
rspData.setRows(list);
rspData.setTotal(total);
return rspData;
}
/**
* 根据数据列表构建表格分页数据对象
*/
public static <T> PageResult<T> build(Collection<T> list) {
PageResult<T> rspData = new PageResult<>();
rspData.setRows(list);
rspData.setTotal(list.size());
return rspData;
}
/**
* 构建表格分页数据对象
*/
public static <T> PageResult<T> build() {
return new PageResult<>();
}
}

View File

@@ -1,15 +1,19 @@
package org.dromara.common.core.domain; package org.dromara.common.core.domain;
import org.dromara.common.core.constant.HttpStatus;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.dromara.common.core.constant.HttpStatus;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import static org.dromara.common.core.constant.HttpStatus.ERROR;
import static org.dromara.common.core.constant.HttpStatus.SUCCESS;
/** /**
* 响应信息主体 * 响应信息主体
* *
* @param <T> 响应数据的泛型类型
* @author Lion Li * @author Lion Li
*/ */
@Data @Data
@@ -20,78 +24,152 @@ public class R<T> implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* 成功 * 响应状态码
*/ */
public static final int SUCCESS = 200;
/**
* 失败
*/
public static final int FAIL = 500;
private int code; private int code;
/**
* 响应提示信息
*/
private String msg; private String msg;
/**
* 响应业务数据
*/
private T data; private T data;
/**
* 构建成功响应结果
*
* @param <T> 响应数据的泛型类型
* @return 成功响应结果对象
*/
public static <T> R<T> ok() { public static <T> R<T> ok() {
return restResult(null, SUCCESS, "操作成功"); return restResult(null, SUCCESS, "操作成功");
} }
/**
* 构建成功响应结果(带业务数据)
*
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 成功响应结果对象
*/
public static <T> R<T> ok(T data) { public static <T> R<T> ok(T data) {
return restResult(data, SUCCESS, "操作成功"); return restResult(data, SUCCESS, "操作成功");
} }
/**
* 构建成功响应结果(自定义提示信息)
*
* @param msg 自定义提示信息
* @param <T> 响应数据的泛型类型
* @return 成功响应结果对象
*/
public static <T> R<T> ok(String msg) { public static <T> R<T> ok(String msg) {
return restResult(null, SUCCESS, msg); return restResult(null, SUCCESS, msg);
} }
/**
* 构建成功响应结果(自定义提示信息+业务数据)
*
* @param msg 自定义提示信息
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 成功响应结果对象
*/
public static <T> R<T> ok(String msg, T data) { public static <T> R<T> ok(String msg, T data) {
return restResult(data, SUCCESS, msg); return restResult(data, SUCCESS, msg);
} }
/**
* 构建失败响应结果
*
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail() { public static <T> R<T> fail() {
return restResult(null, FAIL, "操作失败"); return restResult(null, ERROR, "操作失败");
} }
/**
* 构建失败响应结果(自定义提示信息)
*
* @param msg 自定义提示信息
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail(String msg) { public static <T> R<T> fail(String msg) {
return restResult(null, FAIL, msg); return restResult(null, ERROR, msg);
} }
/**
* 构建失败响应结果(带业务数据)
*
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail(T data) { public static <T> R<T> fail(T data) {
return restResult(data, FAIL, "操作失败"); return restResult(data, ERROR, "操作失败");
} }
/**
* 构建失败响应结果(自定义提示信息+业务数据)
*
* @param msg 自定义提示信息
* @param data 业务数据
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail(String msg, T data) { public static <T> R<T> fail(String msg, T data) {
return restResult(data, FAIL, msg); return restResult(data, ERROR, msg);
} }
/**
* 构建失败响应结果(自定义状态码+提示信息)
*
* @param code 自定义状态码
* @param msg 自定义提示信息
* @param <T> 响应数据的泛型类型
* @return 失败响应结果对象
*/
public static <T> R<T> fail(int code, String msg) { public static <T> R<T> fail(int code, String msg) {
return restResult(null, code, msg); return restResult(null, code, msg);
} }
/** /**
* 返回警告消息 * 构建警告响应结果
* *
* @param msg 返回内容 * @param msg 自定义提示信息
* @return 警告消息 * @param <T> 响应数据的泛型类型
* @return 警告响应结果对象
*/ */
public static <T> R<T> warn(String msg) { public static <T> R<T> warn(String msg) {
return restResult(null, HttpStatus.WARN, msg); return restResult(null, HttpStatus.WARN, msg);
} }
/** /**
* 返回警告消息 * 构建警告响应结果(自定义提示信息+业务数据)
* *
* @param msg 返回内容 * @param msg 自定义提示信息
* @param data 数据对象 * @param data 业务数据
* @return 警告消息 * @param <T> 响应数据的泛型类型
* @return 警告响应结果对象
*/ */
public static <T> R<T> warn(String msg, T data) { public static <T> R<T> warn(String msg, T data) {
return restResult(data, HttpStatus.WARN, msg); return restResult(data, HttpStatus.WARN, msg);
} }
/**
* 核心构建方法
*
* @param data 业务数据
* @param code 响应状态码
* @param msg 提示信息
* @param <T> 响应数据的泛型类型
* @return 响应结果对象
*/
private static <T> R<T> restResult(T data, int code, String msg) { private static <T> R<T> restResult(T data, int code, String msg) {
R<T> r = new R<>(); R<T> r = new R<>();
r.setCode(code); r.setCode(code);
@@ -100,11 +178,26 @@ public class R<T> implements Serializable {
return r; return r;
} }
/**
* 判断响应结果是否为失败
*
* @param ret 响应结果对象
* @param <T> 响应数据的泛型类型
* @return true=失败false=成功
*/
public static <T> Boolean isError(R<T> ret) { public static <T> Boolean isError(R<T> ret) {
return !isSuccess(ret); return !isSuccess(ret);
} }
/**
* 判断响应结果是否为成功
*
* @param ret 响应结果对象
* @param <T> 响应数据的泛型类型
* @return true=成功false=失败
*/
public static <T> Boolean isSuccess(R<T> ret) { public static <T> Boolean isSuccess(R<T> ret) {
return R.SUCCESS == ret.getCode(); return SUCCESS == ret.getCode();
} }
} }

View File

@@ -1,30 +0,0 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 抄送
*
* @author may
*/
@Data
public class FlowCopyDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
private Long userId;
/**
* 用户昵称
*/
private String nickName;
}

View File

@@ -1,30 +0,0 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 启动流程返回对象
*
* @author Lion Li
*/
@Data
public class StartProcessReturnDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程实例id
*/
private Long processInstanceId;
/**
* 任务id
*/
private Long taskId;
}

View File

@@ -7,11 +7,10 @@ import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
/** /**
* 用户登录对象 * 通用登录请求对象,封装客户端、授权类型和验证码信息。
* *
* @author Lion Li * @author Lion Li
*/ */
@Data @Data
public class LoginBody implements Serializable { public class LoginBody implements Serializable {
@@ -30,18 +29,13 @@ public class LoginBody implements Serializable {
@NotBlank(message = "{auth.grant.type.not.blank}") @NotBlank(message = "{auth.grant.type.not.blank}")
private String grantType; private String grantType;
/**
* 租户ID
*/
private String tenantId;
/** /**
* 验证码 * 验证码
*/ */
private String code; private String code;
/** /**
* 唯一标识 * 验证码唯一标识
*/ */
private String uuid; private String uuid;

View File

@@ -13,7 +13,7 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 业务状态枚举 * 流程业务状态枚举,统一定义单据在审批流转中的状态。
* *
* @author may * @author may
*/ */

View File

@@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
/** /**
* 设备类型 * 登录设备类型枚举。
* *
* @author Lion Li * @author Lion Li
*/ */

View File

@@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
/** /**
* 登录类型 * 登录类型枚举,同时维护不同登录方式对应的重试提示配置。
* *
* @author Lion Li * @author Lion Li
*/ */

View File

@@ -0,0 +1,41 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 推送消息来源枚举
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum PushSourceEnum {
/**
* 后端系统消息
*/
BACKEND("backend"),
/**
* 通知公告
*/
NOTICE("notice"),
/**
* 工作流
*/
WORKFLOW("workflow"),
/**
* 大模型
*/
LLM("llm"),
/**
* 客户端消息
*/
CLIENT("client");
private final String source;
}

View File

@@ -0,0 +1,36 @@
package org.dromara.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 推送消息类型枚举
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum PushTypeEnum {
/**
* 通用消息
*/
MESSAGE("message"),
/**
* 通知公告
*/
NOTICE("notice"),
/**
* 大模型消息
*/
LLM("llm"),
/**
* 自定义消息
*/
CUSTOM("custom");
private final String type;
}

View File

@@ -24,7 +24,14 @@ public enum UserStatus {
*/ */
DELETED("2", "删除"); DELETED("2", "删除");
/**
* 状态编码。
*/
private final String code; private final String code;
/**
* 状态说明。
*/
private final String info; private final String info;
} }

View File

@@ -28,6 +28,12 @@ public enum UserType {
*/ */
private final String userType; private final String userType;
/**
* 根据字符串内容匹配用户类型。
*
* @param str 待匹配字符串
* @return 用户类型
*/
public static UserType getUserType(String str) { public static UserType getUserType(String str) {
for (UserType value : values()) { for (UserType value : values()) {
if (StringUtils.contains(str, value.getUserType())) { if (StringUtils.contains(str, value.getUserType())) {

View File

@@ -9,7 +9,7 @@ import lombok.NoArgsConstructor;
import java.io.Serial; import java.io.Serial;
/** /**
* 业务异常支持占位符 {} * 通用业务异常支持使用占位符拼接错误信息。
* *
* @author ruoyi * @author ruoyi
*/ */
@@ -37,15 +37,32 @@ public final class ServiceException extends RuntimeException {
*/ */
private String detailMessage; private String detailMessage;
/**
* 使用错误消息构造业务异常。
*
* @param message 错误消息
*/
public ServiceException(String message) { public ServiceException(String message) {
this.message = message; this.message = message;
} }
/**
* 使用错误消息和错误码构造业务异常。
*
* @param message 错误消息
* @param code 错误码
*/
public ServiceException(String message, Integer code) { public ServiceException(String message, Integer code) {
this.message = message; this.message = message;
this.code = code; this.code = code;
} }
/**
* 使用占位符参数格式化错误消息。
*
* @param message 模板消息
* @param args 参数
*/
public ServiceException(String message, Object... args) { public ServiceException(String message, Object... args) {
this.message = StrFormatter.format(message, args); this.message = StrFormatter.format(message, args);
} }
@@ -55,11 +72,23 @@ public final class ServiceException extends RuntimeException {
return message; return message;
} }
/**
* 设置错误消息并返回当前异常对象。
*
* @param message 错误消息
* @return 当前异常对象
*/
public ServiceException setMessage(String message) { public ServiceException setMessage(String message) {
this.message = message; this.message = message;
return this; return this;
} }
/**
* 设置错误明细并返回当前异常对象。
*
* @param detailMessage 错误明细
* @return 当前异常对象
*/
public ServiceException setDetailMessage(String detailMessage) { public ServiceException setDetailMessage(String detailMessage) {
this.detailMessage = detailMessage; this.detailMessage = detailMessage;
return this; return this;

View File

@@ -8,7 +8,7 @@ import lombok.NoArgsConstructor;
import java.io.Serial; import java.io.Serial;
/** /**
* sse 特制异常 * SSE 场景专用异常
* *
* @author LionLi * @author LionLi
*/ */
@@ -36,10 +36,21 @@ public final class SseException extends RuntimeException {
*/ */
private String detailMessage; private String detailMessage;
/**
* 使用错误消息构造 SSE 异常。
*
* @param message 错误消息
*/
public SseException(String message) { public SseException(String message) {
this.message = message; this.message = message;
} }
/**
* 使用错误消息和错误码构造 SSE 异常。
*
* @param message 错误消息
* @param code 错误码
*/
public SseException(String message, Integer code) { public SseException(String message, Integer code) {
this.message = message; this.message = message;
this.code = code; this.code = code;
@@ -50,11 +61,23 @@ public final class SseException extends RuntimeException {
return message; return message;
} }
/**
* 设置错误消息并返回当前异常对象。
*
* @param message 错误消息
* @return 当前异常对象
*/
public SseException setMessage(String message) { public SseException setMessage(String message) {
this.message = message; this.message = message;
return this; return this;
} }
/**
* 设置错误明细并返回当前异常对象。
*
* @param detailMessage 错误明细
* @return 当前异常对象
*/
public SseException setDetailMessage(String detailMessage) { public SseException setDetailMessage(String detailMessage) {
this.detailMessage = detailMessage; this.detailMessage = detailMessage;
return this; return this;

Some files were not shown because too many files have changed in this diff Show More