commit 4cb2b84df969cc0eb3a65299e0c0bb8b00790b38 Author: wangwei_123 <1255324804@qq.com> Date: Fri Jul 25 10:44:56 2025 +0800 初始化 diff --git a/ruoyi-vue-pro-master-jdk17/.gitignore b/ruoyi-vue-pro-master-jdk17/.gitignore new file mode 100644 index 0000000..49330ee --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/.gitignore @@ -0,0 +1,54 @@ +###################################################################### +# Build Tools + +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!.mvn/wrapper/maven-wrapper.jar + +.flattened-pom.xml + +###################################################################### +# IDE + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/* +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +###################################################################### +# Others +*.log +*.xml.versionsBackup +*.swp + +!*/build/*.java +!*/build/*.html +!*/build/*.xml + +### JRebel ### +rebel.xml + +application-my.yaml + +/yudao-ui-app/unpackage/ +**/.DS_Store diff --git a/ruoyi-vue-pro-master-jdk17/.image/Java监控.jpg b/ruoyi-vue-pro-master-jdk17/.image/Java监控.jpg new file mode 100644 index 0000000..6ad522a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/Java监控.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/MySQL.jpg b/ruoyi-vue-pro-master-jdk17/.image/MySQL.jpg new file mode 100644 index 0000000..64a1940 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/MySQL.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/OA请假-列表.jpg b/ruoyi-vue-pro-master-jdk17/.image/OA请假-列表.jpg new file mode 100644 index 0000000..787bb73 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/OA请假-列表.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/OA请假-发起.jpg b/ruoyi-vue-pro-master-jdk17/.image/OA请假-发起.jpg new file mode 100644 index 0000000..1a7342d Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/OA请假-发起.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/OA请假-详情.jpg b/ruoyi-vue-pro-master-jdk17/.image/OA请假-详情.jpg new file mode 100644 index 0000000..a83e7c1 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/OA请假-详情.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/Redis.jpg b/ruoyi-vue-pro-master-jdk17/.image/Redis.jpg new file mode 100644 index 0000000..9569352 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/Redis.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/01.png b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/01.png new file mode 100644 index 0000000..0f65d99 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/01.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/02.png b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/02.png new file mode 100644 index 0000000..05ec781 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/02.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/03.png b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/03.png new file mode 100644 index 0000000..f400c68 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/03.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/04.png b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/04.png new file mode 100644 index 0000000..d5d5ea0 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/04.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/05.png b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/05.png new file mode 100644 index 0000000..1de6d8a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/05.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/06.png b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/06.png new file mode 100644 index 0000000..400ae90 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/06.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/07.png b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/07.png new file mode 100644 index 0000000..2ed8c0f Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/07.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/08.png b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/08.png new file mode 100644 index 0000000..090e64a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/08.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/09.png b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/09.png new file mode 100644 index 0000000..f2032c8 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/admin-uniapp/09.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/ai-feature.png b/ruoyi-vue-pro-master-jdk17/.image/common/ai-feature.png new file mode 100644 index 0000000..7f8c92f Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/ai-feature.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/ai-preview.gif b/ruoyi-vue-pro-master-jdk17/.image/common/ai-preview.gif new file mode 100644 index 0000000..5f13ac4 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/ai-preview.gif differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/bpm-feature.png b/ruoyi-vue-pro-master-jdk17/.image/common/bpm-feature.png new file mode 100644 index 0000000..23787fb Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/bpm-feature.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/crm-feature.png b/ruoyi-vue-pro-master-jdk17/.image/common/crm-feature.png new file mode 100644 index 0000000..e1c9670 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/crm-feature.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/erp-feature.png b/ruoyi-vue-pro-master-jdk17/.image/common/erp-feature.png new file mode 100644 index 0000000..d30b30e Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/erp-feature.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/infra-feature.png b/ruoyi-vue-pro-master-jdk17/.image/common/infra-feature.png new file mode 100644 index 0000000..f5cef50 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/infra-feature.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/mall-feature.png b/ruoyi-vue-pro-master-jdk17/.image/common/mall-feature.png new file mode 100644 index 0000000..cca05c0 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/mall-feature.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/mall-preview.png b/ruoyi-vue-pro-master-jdk17/.image/common/mall-preview.png new file mode 100644 index 0000000..e164cd2 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/mall-preview.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/project-vs.png b/ruoyi-vue-pro-master-jdk17/.image/common/project-vs.png new file mode 100644 index 0000000..561e092 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/project-vs.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/ruoyi-vue-pro-architecture.png b/ruoyi-vue-pro-master-jdk17/.image/common/ruoyi-vue-pro-architecture.png new file mode 100644 index 0000000..7bd7d59 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/ruoyi-vue-pro-architecture.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/ruoyi-vue-pro-biz.png b/ruoyi-vue-pro-master-jdk17/.image/common/ruoyi-vue-pro-biz.png new file mode 100644 index 0000000..24a385a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/ruoyi-vue-pro-biz.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/system-feature.png b/ruoyi-vue-pro-master-jdk17/.image/common/system-feature.png new file mode 100644 index 0000000..366087c Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/system-feature.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/yudao-cloud-architecture.png b/ruoyi-vue-pro-master-jdk17/.image/common/yudao-cloud-architecture.png new file mode 100644 index 0000000..59416d8 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/yudao-cloud-architecture.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/common/yudao-roadmap.png b/ruoyi-vue-pro-master-jdk17/.image/common/yudao-roadmap.png new file mode 100644 index 0000000..f4becc9 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/common/yudao-roadmap.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/个人中心.jpg b/ruoyi-vue-pro-master-jdk17/.image/个人中心.jpg new file mode 100644 index 0000000..ce57f6e Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/个人中心.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/代码生成.jpg b/ruoyi-vue-pro-master-jdk17/.image/代码生成.jpg new file mode 100644 index 0000000..751603e Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/代码生成.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/令牌管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/令牌管理.jpg new file mode 100644 index 0000000..04abf4d Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/令牌管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/任务列表-审批.jpg b/ruoyi-vue-pro-master-jdk17/.image/任务列表-审批.jpg new file mode 100644 index 0000000..cba312a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/任务列表-审批.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/任务列表-已办.jpg b/ruoyi-vue-pro-master-jdk17/.image/任务列表-已办.jpg new file mode 100644 index 0000000..7a8d0fb Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/任务列表-已办.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/任务列表-待办.jpg b/ruoyi-vue-pro-master-jdk17/.image/任务列表-待办.jpg new file mode 100644 index 0000000..a90323f Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/任务列表-待办.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/任务日志.jpg b/ruoyi-vue-pro-master-jdk17/.image/任务日志.jpg new file mode 100644 index 0000000..599e50a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/任务日志.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/商户信息.jpg b/ruoyi-vue-pro-master-jdk17/.image/商户信息.jpg new file mode 100644 index 0000000..483eace Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/商户信息.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/在线用户.jpg b/ruoyi-vue-pro-master-jdk17/.image/在线用户.jpg new file mode 100644 index 0000000..b183009 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/在线用户.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/大屏设计器-列表.jpg b/ruoyi-vue-pro-master-jdk17/.image/大屏设计器-列表.jpg new file mode 100644 index 0000000..9a45c3b Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/大屏设计器-列表.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/大屏设计器-编辑.jpg b/ruoyi-vue-pro-master-jdk17/.image/大屏设计器-编辑.jpg new file mode 100644 index 0000000..63298a0 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/大屏设计器-编辑.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/大屏设计器-预览.jpg b/ruoyi-vue-pro-master-jdk17/.image/大屏设计器-预览.jpg new file mode 100644 index 0000000..501d9ea Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/大屏设计器-预览.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/字典数据.jpg b/ruoyi-vue-pro-master-jdk17/.image/字典数据.jpg new file mode 100644 index 0000000..8298c89 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/字典数据.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/字典类型.jpg b/ruoyi-vue-pro-master-jdk17/.image/字典类型.jpg new file mode 100644 index 0000000..6613392 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/字典类型.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/定时任务.jpg b/ruoyi-vue-pro-master-jdk17/.image/定时任务.jpg new file mode 100644 index 0000000..d5bbd85 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/定时任务.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/岗位管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/岗位管理.jpg new file mode 100644 index 0000000..42b64d2 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/岗位管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/工作流设计器-bpmn.jpg b/ruoyi-vue-pro-master-jdk17/.image/工作流设计器-bpmn.jpg new file mode 100644 index 0000000..2a61f60 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/工作流设计器-bpmn.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/工作流设计器-simple.jpg b/ruoyi-vue-pro-master-jdk17/.image/工作流设计器-simple.jpg new file mode 100644 index 0000000..9ef2c9e Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/工作流设计器-simple.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/应用信息-列表.jpg b/ruoyi-vue-pro-master-jdk17/.image/应用信息-列表.jpg new file mode 100644 index 0000000..da419a2 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/应用信息-列表.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/应用信息-编辑.jpg b/ruoyi-vue-pro-master-jdk17/.image/应用信息-编辑.jpg new file mode 100644 index 0000000..913cfbc Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/应用信息-编辑.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/应用管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/应用管理.jpg new file mode 100644 index 0000000..6e7789f Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/应用管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/我的流程-列表.jpg b/ruoyi-vue-pro-master-jdk17/.image/我的流程-列表.jpg new file mode 100644 index 0000000..223d17a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/我的流程-列表.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/我的流程-发起.jpg b/ruoyi-vue-pro-master-jdk17/.image/我的流程-发起.jpg new file mode 100644 index 0000000..7a83306 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/我的流程-发起.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/我的流程-详情.jpg b/ruoyi-vue-pro-master-jdk17/.image/我的流程-详情.jpg new file mode 100644 index 0000000..6a01541 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/我的流程-详情.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/报表设计器-图形报表.jpg b/ruoyi-vue-pro-master-jdk17/.image/报表设计器-图形报表.jpg new file mode 100644 index 0000000..681b318 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/报表设计器-图形报表.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/报表设计器-打印设计.jpg b/ruoyi-vue-pro-master-jdk17/.image/报表设计器-打印设计.jpg new file mode 100644 index 0000000..bb86da6 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/报表设计器-打印设计.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/报表设计器-数据报表.jpg b/ruoyi-vue-pro-master-jdk17/.image/报表设计器-数据报表.jpg new file mode 100644 index 0000000..9ca5b9b Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/报表设计器-数据报表.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/操作日志.jpg b/ruoyi-vue-pro-master-jdk17/.image/操作日志.jpg new file mode 100644 index 0000000..4a0611a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/操作日志.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/支付订单.jpg b/ruoyi-vue-pro-master-jdk17/.image/支付订单.jpg new file mode 100644 index 0000000..0a56dd7 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/支付订单.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/敏感词.jpg b/ruoyi-vue-pro-master-jdk17/.image/敏感词.jpg new file mode 100644 index 0000000..92a5397 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/敏感词.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/数据库文档.jpg b/ruoyi-vue-pro-master-jdk17/.image/数据库文档.jpg new file mode 100644 index 0000000..a4339d9 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/数据库文档.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/文件管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/文件管理.jpg new file mode 100644 index 0000000..054b19f Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/文件管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/文件管理2.jpg b/ruoyi-vue-pro-master-jdk17/.image/文件管理2.jpg new file mode 100644 index 0000000..b12e5c3 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/文件管理2.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/文件配置.jpg b/ruoyi-vue-pro-master-jdk17/.image/文件配置.jpg new file mode 100644 index 0000000..e618049 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/文件配置.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/日志中心.jpg b/ruoyi-vue-pro-master-jdk17/.image/日志中心.jpg new file mode 100644 index 0000000..27c1c6c Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/日志中心.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/流程模型-列表.jpg b/ruoyi-vue-pro-master-jdk17/.image/流程模型-列表.jpg new file mode 100644 index 0000000..ffdc584 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/流程模型-列表.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/流程模型-定义.jpg b/ruoyi-vue-pro-master-jdk17/.image/流程模型-定义.jpg new file mode 100644 index 0000000..18b316c Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/流程模型-定义.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/流程模型-设计.jpg b/ruoyi-vue-pro-master-jdk17/.image/流程模型-设计.jpg new file mode 100644 index 0000000..9614969 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/流程模型-设计.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/流程表单.jpg b/ruoyi-vue-pro-master-jdk17/.image/流程表单.jpg new file mode 100644 index 0000000..60669c1 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/流程表单.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/生成效果.jpg b/ruoyi-vue-pro-master-jdk17/.image/生成效果.jpg new file mode 100644 index 0000000..98ff2cc Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/生成效果.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/用户分组.jpg b/ruoyi-vue-pro-master-jdk17/.image/用户分组.jpg new file mode 100644 index 0000000..39af1cd Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/用户分组.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/用户管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/用户管理.jpg new file mode 100644 index 0000000..844604a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/用户管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/登录.jpg b/ruoyi-vue-pro-master-jdk17/.image/登录.jpg new file mode 100644 index 0000000..b782b98 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/登录.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/登录日志.jpg b/ruoyi-vue-pro-master-jdk17/.image/登录日志.jpg new file mode 100644 index 0000000..25662d9 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/登录日志.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/短信日志.jpg b/ruoyi-vue-pro-master-jdk17/.image/短信日志.jpg new file mode 100644 index 0000000..ada8e56 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/短信日志.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/短信模板.jpg b/ruoyi-vue-pro-master-jdk17/.image/短信模板.jpg new file mode 100644 index 0000000..09381cc Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/短信模板.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/短信渠道.jpg b/ruoyi-vue-pro-master-jdk17/.image/短信渠道.jpg new file mode 100644 index 0000000..df3a5c3 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/短信渠道.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/租户套餐.png b/ruoyi-vue-pro-master-jdk17/.image/租户套餐.png new file mode 100644 index 0000000..9663167 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/租户套餐.png differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/租户管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/租户管理.jpg new file mode 100644 index 0000000..647416a Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/租户管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/系统接口.jpg b/ruoyi-vue-pro-master-jdk17/.image/系统接口.jpg new file mode 100644 index 0000000..6d39d42 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/系统接口.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/菜单管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/菜单管理.jpg new file mode 100644 index 0000000..ad3b797 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/菜单管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/表单构建.jpg b/ruoyi-vue-pro-master-jdk17/.image/表单构建.jpg new file mode 100644 index 0000000..81f0374 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/表单构建.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/角色管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/角色管理.jpg new file mode 100644 index 0000000..eed776e Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/角色管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/访问日志.jpg b/ruoyi-vue-pro-master-jdk17/.image/访问日志.jpg new file mode 100644 index 0000000..ef301aa Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/访问日志.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/退款订单.jpg b/ruoyi-vue-pro-master-jdk17/.image/退款订单.jpg new file mode 100644 index 0000000..2c6c6c9 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/退款订单.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/通知公告.jpg b/ruoyi-vue-pro-master-jdk17/.image/通知公告.jpg new file mode 100644 index 0000000..97bb42f Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/通知公告.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/部门管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/部门管理.jpg new file mode 100644 index 0000000..6eab233 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/部门管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/配置管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/配置管理.jpg new file mode 100644 index 0000000..0abaec9 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/配置管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/链路追踪.jpg b/ruoyi-vue-pro-master-jdk17/.image/链路追踪.jpg new file mode 100644 index 0000000..12f7aa8 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/链路追踪.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/错误日志.jpg b/ruoyi-vue-pro-master-jdk17/.image/错误日志.jpg new file mode 100644 index 0000000..eb615ea Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/错误日志.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/错误码管理.jpg b/ruoyi-vue-pro-master-jdk17/.image/错误码管理.jpg new file mode 100644 index 0000000..ea91dde Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/错误码管理.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/.image/首页.jpg b/ruoyi-vue-pro-master-jdk17/.image/首页.jpg new file mode 100644 index 0000000..10a7fde Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/.image/首页.jpg differ diff --git a/ruoyi-vue-pro-master-jdk17/LICENSE b/ruoyi-vue-pro-master-jdk17/LICENSE new file mode 100644 index 0000000..bd9da62 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2021 ruoyi-vue-pro + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ruoyi-vue-pro-master-jdk17/README.md b/ruoyi-vue-pro-master-jdk17/README.md new file mode 100644 index 0000000..efdb44c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/README.md @@ -0,0 +1,391 @@ +

+ Downloads + Downloads + Downloads +

+ +**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!!** + +**「我喜欢写代码,乐此不疲」** +**「我喜欢做开源,以此为乐」** + +我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。 + +如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。 + +## 🐶 新手必读 + +* 演示地址【Vue3 + element-plus】: +* 演示地址【Vue3 + vben(ant-design-vue)】: +* 演示地址【Vue2 + element-ui】: +* 启动文档: +* 视频教程: + +## 🐰 版本说明 + +| 版本 | JDK 8 + Spring Boot 2.7 | JDK 17/21 + Spring Boot 3.2 | +|---------------------------------------------------------------------|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------| +| 【完整版】[ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [`master`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master-jdk17/) 分支 | +| 【精简版】[yudao-boot-mini](https://gitee.com/yudaocode/yudao-boot-mini) | [`master`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master-jdk17/) 分支 | + +* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能 +* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能 + +可参考 [《迁移文档》](https://doc.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】 + +## 🐯 平台简介 + +**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。 + +> 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。 +> +> 😜 给项目点点 Star 吧,这对我们真的很重要! + +![架构图](/.image/common/ruoyi-vue-pro-architecture.png) + +* Java 后端:`master` 分支为 JDK 8 + Spring Boot 2.7,`master-jdk17` 分支为 JDK 17/21 + Spring Boot 3.2 +* 管理后台的电脑端:Vue3 提供 `element-plus`、`vben(ant-design-vue)` 两个版本,Vue2 提供 `element-ui` 版本 +* 管理后台的移动端:采用 `uni-app` 方案,一份代码多终端适配,同时支持 APP、小程序、H5! +* 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson +* 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等 +* 消息队列可使用 Event、Redis、RabbitMQ、Kafka、RocketMQ 等 +* 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统,支持 SSO 单点登录 +* 支持加载动态权限菜单,按钮级别权限控制,Redis 缓存提升性能 +* 支持 SaaS 多租户,可自定义每个租户的权限,提供透明化的多租户底层封装 +* 工作流使用 Flowable,支持动态表单、在线设计流程、会签 / 或签、多种任务分配方式 +* 高效率开发,使用代码生成器可以一键生成 Java、Vue 前后端代码、SQL 脚本、接口文档,支持单表、树表、主子表 +* 实时通信,采用 Spring WebSocket 实现,内置 Token 身份校验,支持 WebSocket 集群 +* 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款 +* 集成阿里云、腾讯云等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务 +* 集成报表设计器、大屏设计器,通过拖拽即可生成酷炫的报表与大屏 + +## 🐳 项目关系 + +![架构演进](/.image/common/yudao-roadmap.png) + +三个项目的功能对比,可见社区共同整理的 [国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn) 表格。 + +### 后端项目 + +| 项目 | Star | 简介 | +|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| [ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro) | 基于 Spring Boot 多模块架构 | +| [yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [![Gitee star](https://gitee.com/zhijiantianya/yudao-cloud/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/yudao-cloud) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/yudao-cloud.svg?style=social&label=Stars)](https://github.com/YunaiV/yudao-cloud) | 基于 Spring Cloud 微服务架构 | +| [Spring-Boot-Labs](https://gitee.com/yudaocode/SpringBoot-Labs) | [![Gitee star](https://gitee.com/yudaocode/SpringBoot-Labs/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/yudao-cloud) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/SpringBoot-Labs.svg?style=social&label=Stars)](https://github.com/yudaocode/SpringBoot-Labs) | 系统学习 Spring Boot & Cloud 专栏 | + +### 前端项目 + +| 项目 | Star | 简介 | +|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------| +| [yudao-ui-admin-vue3](https://gitee.com/yudaocode/yudao-ui-admin-vue3) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue3/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue3) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue3.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue3) | 基于 Vue3 + element-plus 实现的管理后台 | +| [yudao-ui-admin-vben](https://gitee.com/yudaocode/yudao-ui-admin-vben) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vben/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vben) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vben.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vben) | 基于 Vue3 + vben(ant-design-vue) 实现的管理后台 | +| [yudao-mall-uniapp](https://gitee.com/yudaocode/yudao-mall-uniapp) | [![Gitee star](https://gitee.com/yudaocode/yudao-mall-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-mall-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-mall-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-mall-uniapp) | 基于 uni-app 实现的商城小程序 | +| [yudao-ui-admin-vue2](https://gitee.com/yudaocode/yudao-ui-admin-vue2) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue2/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue2) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue2.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue2) | 基于 Vue2 + element-ui 实现的管理后台 | +| [yudao-ui-admin-uniapp](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-uniapp) | 基于 Vue2 + element-ui 实现的管理后台 | +| [yudao-ui-go-view](https://gitee.com/yudaocode/yudao-ui-go-view) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-go-view/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-go-view) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-go-view.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-go-view) | 基于 Vue3 + naive-ui 实现的大屏报表 | + +## 😎 开源协议 + +**为什么推荐使用本项目?** + +① 本项目采用比 Apache 2.0 更宽松的 [MIT License](https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/LICENSE) 开源协议,个人与企业可 100% 免费使用,不用保留类作者、Copyright 信息。 + +② 代码全部开源,不会像其他项目一样,只开源部分代码,让你无法了解整个项目的架构设计。[国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn) + +![开源项目对比](/.image/common/project-vs.png) + +③ 代码整洁、架构整洁,遵循《阿里巴巴 Java 开发手册》规范,代码注释详细,113770 行 Java 代码,42462 行代码注释。 + +## 🤝 项目外包 + +我们也是接外包滴,如果你有项目想要外包,可以微信联系【**Aix9975**】。 + +团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。 + +项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 聊天、微信公众号、微信小程序等等。 + +## 🐼 内置功能 + +系统内置多种多种业务功能,可以用于快速你的业务系统: + +![功能分层](/.image/common/ruoyi-vue-pro-biz.png) + +* 通用模块(必选):系统功能、基础设施 +* 通用模块(可选):工作流程、支付系统、数据报表、会员中心 +* 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型 + +> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。 +> +> * 额外新增的功能,我们使用 🚀 标记。 +> * 重新实现的功能,我们使用 ⭐️ 标记。 + +🙂 所有功能,都通过 **单元测试** 保证高质量。 + +### 系统功能 + +| | 功能 | 描述 | +|-----|-------|---------------------------------| +| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 | +| ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 | +| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 | +| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 | +| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 | +| | 岗位管理 | 配置系统用户所属担任职务 | +| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 | +| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 | +| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 | +| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 | +| 🚀 | 邮件管理 | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台 | +| 🚀 | 站内信 | 系统内的消息通知,提供站内信模版、站内信消息 | +| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 | +| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 | +| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 | +| | 通知公告 | 系统通知公告信息发布维护 | +| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 | +| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 | +| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 | + +![功能图](/.image/common/system-feature.png) + +### 工作流程 + +![功能图](/.image/common/bpm-feature.png) + +基于 Flowable 构建,可支持信创(国产)数据库,满足中国特色流程操作: + +| BPMN 设计器 | 钉钉/飞书设计器 | +|------------------------------|--------------------------------| +| ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) | + +> 历经头部企业生产验证,工作流引擎须标配仿钉钉/飞书 + BPMN 双设计器!!! +> +> 前者支持轻量配置简单流程,后者实现复杂场景深度编排 + +| 功能列表 | 功能描述 | 是否完成 | +|------------|-------------------------------------------------------------------------------------|------| +| SIMPLE 设计器 | 仿钉钉/飞书设计器,支持拖拽搭建表单流程,10 分钟快速完成审批流程配置 | ✅ | +| BPMN 设计器 | 基于 BPMN 标准开发,适配复杂业务场景,满足多层级审批及流程自动化需求 | ✅ | +| 会签 | 同一个审批节点设置多个人(如 A、B、C 三人,三人会同时收到待办任务),需全部同意之后,审批才可到下一审批节点 | ✅ | +| 或签 | 同一个审批节点设置多个人,任意一个人处理后,就能进入下一个节点 | ✅ | +| 依次审批 | (顺序会签)同一个审批节点设置多个人(如 A、B、C 三人),三人按顺序依次收到待办,即 A 先审批,A 提交后 B 才能审批,需全部同意之后,审批才可到下一审批节点 | ✅ | +| 抄送 | 将审批结果通知给抄送人,同一个审批默认排重,不重复抄送给同一人 | ✅ | +| 驳回 | (退回)将审批重置发送给某节点,重新审批。可驳回至发起人、上一节点、任意节点 | ✅ | +| 转办 | A 转给其 B 审批,B 审批后,进入下一节点 | ✅ | +| 委派 | A 转给其 B 审批,B 审批后,转给 A,A 继续审批后进入下一节点 | ✅ | +| 加签 | 允许当前审批人根据需要,自行增加当前节点的审批人,支持向前、向后加签 | ✅ | +| 减签 | (取消加签)在当前审批人操作之前,减少审批人 | ✅ | +| 撤销 | (取消流程)流程发起人,可以对流程进行撤销处理 | ✅ | +| 终止 | 系统管理员,在任意节点终止流程实例 | ✅ | +| 表单权限 | 支持拖拉拽配置表单,每个审批节点可配置只读、编辑、隐藏权限 | ✅ | +| 超时审批 | 配置超时审批时间,超时后自动触发审批通过、不通过、驳回等操作 | ✅ | +| 自动提醒 | 配置提醒时间,到达时间后自动触发短信、邮箱、站内信等通知提醒,支持自定义重复提醒频次 | ✅ | +| 父子流程 | 主流程设置子流程节点,子流程节点会自动触发子流程。子流程结束后,主流程才会执行(继续往下下执行),支持同步子流程、异步子流程 | ✅ | +| 条件分支 | (排它分支)用于在流程中实现决策,即根据条件选择一个分支执行 | ✅ | +| 并行分支 | 允许将流程分成多条分支,不进行条件判断,所有分支都会执行 | ✅ | +| 包容分支 | (条件分支 + 并行分支的结合体)允许基于条件选择多条分支执行,但如果没有任何一个分支满足条件,则可以选择默认分支 | ✅ | +| 路由分支 | 根据条件选择一个分支执行(重定向到指定配置节点),也可以选择默认分支执行(继续往下执行) | ✅ | +| 触发节点 | 执行到该节点,触发 HTTP 请求、HTTP 回调、更新数据、删除数据等 | ✅ | +| 延迟节点 | 执行到该节点,审批等待一段时间再执行,支持固定时长、固定日期等 | ✅ | +| 拓展设置 | 流程前置/后置通知,节点(任务)前置、后置通知,流程报表,自动审批去重,自定流程编号、标题、摘要,流程报表等 | ✅ | + +### 支付系统 + +| | 功能 | 描述 | +|-----|------|---------------------------| +| 🚀 | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 | +| 🚀 | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单 | +| 🚀 | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单 | +| 🚀 | 回调通知 | 查看支付回调业务的【支付】【退款】的通知结果 | +| 🚀 | 接入示例 | 提供接入支付系统的【支付】【退款】的功能实战 | + +### 基础设施 + +| | 功能 | 描述 | +|-----|-----------|----------------------------------------------| +| 🚀 | 代码生成 | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 | +| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 | +| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 | +| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 | +| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 | +| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 | +| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 | +| 🚀 | WebSocket | 提供 WebSocket 接入示例,支持一对一、一对多发送方式 | +| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 | +| | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 | +| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 | +| 🚀 | 消息队列 | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 | +| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 | +| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 | +| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 | +| 🚀 | 服务保障 | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景 | +| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 | +| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 | + +![功能图](/.image/common/infra-feature.png) + +### 数据报表 + +| | 功能 | 描述 | +|-----|-------|--------------------| +| 🚀 | 报表设计器 | 支持数据报表、图形报表、打印设计等 | +| 🚀 | 大屏设计器 | 拖拽生成数据大屏,内置几十种图表组件 | + +### 微信公众号 + +| | 功能 | 描述 | +|-----|--------|-------------------------------| +| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 | +| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 | +| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 | +| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 | +| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 | +| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 | +| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 | +| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 | +| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 | +| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 | + +### 商城系统 + +演示地址: + +![功能图](/.image/common/mall-feature.png) + +![功能图](/.image/common/mall-preview.png) + +### 会员中心 + +| | 功能 | 描述 | +|-----|------|----------------------------------| +| 🚀 | 会员管理 | 会员是 C 端的消费者,该功能用于会员的搜索与管理 | +| 🚀 | 会员标签 | 对会员的标签进行创建、查询、修改、删除等操作 | +| 🚀 | 会员等级 | 对会员的等级、成长值进行管理,可用于订单折扣等会员权益 | +| 🚀 | 会员分组 | 对会员进行分组,用于用户画像、内容推送等运营手段 | +| 🚀 | 积分签到 | 回馈给签到、消费等行为的积分,会员可订单抵现、积分兑换等途径消耗 | + +### ERP 系统 + +演示地址: + +![功能图](/.image/common/erp-feature.png) + +### CRM 系统 + +演示地址: + +![功能图](/.image/common/crm-feature.png) + +### AI 大模型 + +演示地址: + +![功能图](/.image/common/ai-feature.png) + +![功能图](/.image/common/ai-preview.gif) + +## 🐨 技术栈 + +### 模块 + +| 项目 | 说明 | +|-----------------------|--------------------| +| `yudao-dependencies` | Maven 依赖版本管理 | +| `yudao-framework` | Java 框架拓展 | +| `yudao-server` | 管理后台 + 用户 APP 的服务端 | +| `yudao-module-system` | 系统功能的 Module 模块 | +| `yudao-module-member` | 会员中心的 Module 模块 | +| `yudao-module-infra` | 基础设施的 Module 模块 | +| `yudao-module-bpm` | 工作流程的 Module 模块 | +| `yudao-module-pay` | 支付系统的 Module 模块 | +| `yudao-module-mall` | 商城系统的 Module 模块 | +| `yudao-module-erp` | ERP 系统的 Module 模块 | +| `yudao-module-crm` | CRM 系统的 Module 模块 | +| `yudao-module-ai` | AI 大模型的 Module 模块 | +| `yudao-module-mp` | 微信公众号的 Module 模块 | +| `yudao-module-report` | 大屏报表 Module 模块 | + +### 框架 + +| 框架 | 说明 | 版本 | 学习指南 | +|---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------| +| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 3.4.5 | [文档](https://github.com/YunaiV/SpringBoot-Labs) | +| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | | +| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.23 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | +| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.7 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) | +| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 4.3.1 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | +| [Redis](https://redis.io/) | key-value 数据库 | 5.0 / 6.0 /7.0 | | +| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.32.0 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) | +| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 6.1.10 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) | +| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 6.3.1 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) | +| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 8.0.1 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) | +| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 7.0.0 | [文档](https://doc.iocoder.cn/bpm/) | +| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) | +| [Springdoc](https://springdoc.org/) | Swagger 文档 | 2.3.0 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) | +| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 9.0.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) | +| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 3.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) | +| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.17.1 | | +| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.6.3 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) | +| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.34 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) | +| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.10.1 | - | +| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 5.7.0 | - | + +## 🐷 演示图 + +### 系统功能 + +| 模块 | biu | biu | biu | +|----------|-----------------------------|---------------------------|--------------------------| +| 登录 & 首页 | ![登录](/.image/登录.jpg) | ![首页](/.image/首页.jpg) | ![个人中心](/.image/个人中心.jpg) | +| 用户 & 应用 | ![用户管理](/.image/用户管理.jpg) | ![令牌管理](/.image/令牌管理.jpg) | ![应用管理](/.image/应用管理.jpg) | +| 租户 & 套餐 | ![租户管理](/.image/租户管理.jpg) | ![租户套餐](/.image/租户套餐.png) | - | +| 部门 & 岗位 | ![部门管理](/.image/部门管理.jpg) | ![岗位管理](/.image/岗位管理.jpg) | - | +| 菜单 & 角色 | ![菜单管理](/.image/菜单管理.jpg) | ![角色管理](/.image/角色管理.jpg) | - | +| 审计日志 | ![操作日志](/.image/操作日志.jpg) | ![登录日志](/.image/登录日志.jpg) | - | +| 短信 | ![短信渠道](/.image/短信渠道.jpg) | ![短信模板](/.image/短信模板.jpg) | ![短信日志](/.image/短信日志.jpg) | +| 字典 & 敏感词 | ![字典类型](/.image/字典类型.jpg) | ![字典数据](/.image/字典数据.jpg) | ![敏感词](/.image/敏感词.jpg) | +| 错误码 & 通知 | ![错误码管理](/.image/错误码管理.jpg) | ![通知公告](/.image/通知公告.jpg) | - | + +### 工作流程 + +| 模块 | biu | biu | biu | +|---------|---------------------------------|---------------------------------|---------------------------------| +| 流程模型 | ![流程模型-列表](/.image/流程模型-列表.jpg) | ![流程模型-设计](/.image/流程模型-设计.jpg) | ![流程模型-定义](/.image/流程模型-定义.jpg) | +| 表单 & 分组 | ![流程表单](/.image/流程表单.jpg) | ![用户分组](/.image/用户分组.jpg) | - | +| 我的流程 | ![我的流程-列表](/.image/我的流程-列表.jpg) | ![我的流程-发起](/.image/我的流程-发起.jpg) | ![我的流程-详情](/.image/我的流程-详情.jpg) | +| 待办 & 已办 | ![任务列表-审批](/.image/任务列表-审批.jpg) | ![任务列表-待办](/.image/任务列表-待办.jpg) | ![任务列表-已办](/.image/任务列表-已办.jpg) | +| OA 请假 | ![OA请假-列表](/.image/OA请假-列表.jpg) | ![OA请假-发起](/.image/OA请假-发起.jpg) | ![OA请假-详情](/.image/OA请假-详情.jpg) | + +### 基础设施 + +| 模块 | biu | biu | biu | +|---------------|-------------------------------|-----------------------------|---------------------------| +| 代码生成 | ![代码生成](/.image/代码生成.jpg) | ![生成效果](/.image/生成效果.jpg) | - | +| 文档 | ![系统接口](/.image/系统接口.jpg) | ![数据库文档](/.image/数据库文档.jpg) | - | +| 文件 & 配置 | ![文件配置](/.image/文件配置.jpg) | ![文件管理](/.image/文件管理2.jpg) | ![配置管理](/.image/配置管理.jpg) | +| 定时任务 | ![定时任务](/.image/定时任务.jpg) | ![任务日志](/.image/任务日志.jpg) | - | +| API 日志 | ![访问日志](/.image/访问日志.jpg) | ![错误日志](/.image/错误日志.jpg) | - | +| MySQL & Redis | ![MySQL](/.image/MySQL.jpg) | ![Redis](/.image/Redis.jpg) | - | +| 监控平台 | ![Java监控](/.image/Java监控.jpg) | ![链路追踪](/.image/链路追踪.jpg) | ![日志中心](/.image/日志中心.jpg) | + +### 支付系统 + +| 模块 | biu | biu | biu | +|---------|---------------------------|---------------------------------|---------------------------------| +| 商家 & 应用 | ![商户信息](/.image/商户信息.jpg) | ![应用信息-列表](/.image/应用信息-列表.jpg) | ![应用信息-编辑](/.image/应用信息-编辑.jpg) | +| 支付 & 退款 | ![支付订单](/.image/支付订单.jpg) | ![退款订单](/.image/退款订单.jpg) | --- | +### 数据报表 + +| 模块 | biu | biu | biu | +|-------|---------------------------------|---------------------------------|---------------------------------------| +| 报表设计器 | ![数据报表](/.image/报表设计器-数据报表.jpg) | ![图形报表](/.image/报表设计器-图形报表.jpg) | ![报表设计器-打印设计](/.image/报表设计器-打印设计.jpg) | +| 大屏设计器 | ![大屏列表](/.image/大屏设计器-列表.jpg) | ![大屏预览](/.image/大屏设计器-预览.jpg) | ![大屏编辑](/.image/大屏设计器-编辑.jpg) | + +### 移动端(管理后台) + +| biu | biu | biu | +|----------------------------------|----------------------------------|----------------------------------| +| ![](/.image/admin-uniapp/01.png) | ![](/.image/admin-uniapp/02.png) | ![](/.image/admin-uniapp/03.png) | +| ![](/.image/admin-uniapp/04.png) | ![](/.image/admin-uniapp/05.png) | ![](/.image/admin-uniapp/06.png) | +| ![](/.image/admin-uniapp/07.png) | ![](/.image/admin-uniapp/08.png) | ![](/.image/admin-uniapp/09.png) | + +目前已经实现登录、我的、工作台、编辑资料、头像修改、密码修改、常见问题、关于我们等基础功能。 diff --git a/ruoyi-vue-pro-master-jdk17/lombok.config b/ruoyi-vue-pro-master-jdk17/lombok.config new file mode 100644 index 0000000..a8e8ce6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/lombok.config @@ -0,0 +1,4 @@ +config.stopBubbling = true +lombok.tostring.callsuper=CALL +lombok.equalsandhashcode.callsuper=CALL +lombok.accessors.chain=true diff --git a/ruoyi-vue-pro-master-jdk17/pom.xml b/ruoyi-vue-pro-master-jdk17/pom.xml new file mode 100644 index 0000000..36a4802 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/pom.xml @@ -0,0 +1,172 @@ + + + 4.0.0 + cn.iocoder.boot + yudao + ${revision} + pom + + yudao-dependencies + yudao-framework + + yudao-server + + yudao-module-system + yudao-module-infra + yudao-module-member + yudao-module-bpm + yudao-module-report + yudao-module-iot + + + ${project.artifactId} + 芋道项目基础脚手架 + https://github.com/YunaiV/ruoyi-vue-pro + + + 2.6.1-SNAPSHOT + + 21 + ${java.version} + ${java.version} + 3.2.2 + 3.14.0 + 1.6.0 + + 1.18.38 + 3.4.5 + 1.6.3 + UTF-8 + + + + + + cn.iocoder.boot + yudao-dependencies + ${revision} + pom + import + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + + org.springframework.boot + spring-boot-configuration-processor + ${spring.boot.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + false + + -parameters + + + + + org.codehaus.mojo + flatten-maven-plugin + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + ${flatten-maven-plugin.version} + + oss + true + + + + + flatten + + flatten + process-resources + + + + clean + + flatten.clean + clean + + + + + + + + + + huaweicloud + huawei + https://mirrors.huaweicloud.com/repository/maven/ + + + aliyunmaven + aliyun + https://maven.aliyun.com/repository/public + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-dependencies/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-dependencies/pom.xml new file mode 100644 index 0000000..3f05da8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-dependencies/pom.xml @@ -0,0 +1,674 @@ + + + 4.0.0 + + cn.iocoder.boot + yudao-dependencies + ${revision} + pom + + ${project.artifactId} + 基础 bom 文件,管理整个项目的依赖版本 + https://github.com/YunaiV/ruoyi-vue-pro + + + 2.6.1-SNAPSHOT + 1.6.0 + + 3.4.5 + + 2.8.3 + 4.6.0 + + 1.2.24 + 3.5.19 + 3.5.10.1 + 4.3.1 + 1.4.13 + 3.0.6 + 3.41.0 + 8.1.3.140 + 8.6.0 + 5.1.0 + 3.3.3 + + 2.3.2 + + 2.2.7 + + 9.0.0 + 3.4.5 + 0.33.0 + + 8.0.2.RELEASE + 1.1.8 + 5.2.0 + + 7.0.1 + + 1.4.0 + 1.18.3 + 1.18.38 + 1.6.3 + 5.8.35 + 6.0.0-M19 + 4.0.3 + 2.4.1 + 1.2.83 + 33.4.8-jre + 2.14.5 + 3.11.1 + 0.1.55 + 3.1.0 + 2.7.0 + 3.0.6 + 4.1.118.Final + 1.2.5 + 0.9.0 + 4.5.13 + + 2.17.0 + 1.27.1 + 2.30.14 + 1.16.7 + 1.4.0 + 2.0.0 + 1.9.5 + 4.7.5.B + + + + + + + io.netty + netty-bom + ${netty.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + io.github.mouzt + bizlog-sdk + ${bizlog-sdk.version} + + + org.springframework.boot + spring-boot-starter + + + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + ${revision} + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-data-permission + ${revision} + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-ip + ${revision} + + + + + + org.springframework.boot + spring-boot-configuration-processor + ${spring.boot.version} + + + + + cn.iocoder.boot + yudao-spring-boot-starter-web + ${revision} + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + ${revision} + + + + cn.iocoder.boot + yudao-spring-boot-starter-websocket + ${revision} + + + + com.github.xingfudeshi + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + org.springdoc + springdoc-openapi-starter-webmvc-api + ${springdoc.version} + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + ${revision} + + + + com.alibaba + druid-spring-boot-3-starter + ${druid.version} + + + + org.mybatis + mybatis + ${mybatis.version} + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-jsqlparser + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus.version} + + + com.baomidou + dynamic-datasource-spring-boot3-starter + ${dynamic-datasource.version} + + + com.github.yulichang + mybatis-plus-join-boot-starter + ${mybatis-plus-join.version} + + + + com.fhs-opensource + easy-trans-spring-boot-starter + ${easy-trans.version} + + + org.springframework + spring-context + + + org.springframework.cloud + spring-cloud-commons + + + + + com.fhs-opensource + easy-trans-mybatis-plus-extend + ${easy-trans.version} + + + com.fhs-opensource + easy-trans-anno + ${easy-trans.version} + + + + cn.iocoder.boot + yudao-spring-boot-starter-redis + ${revision} + + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + com.dameng + DmJdbcDriver18 + ${dm8.jdbc.version} + + + + org.opengauss + opengauss-jdbc + ${opengauss.jdbc.version} + + + + cn.com.kingbase + kingbase8 + ${kingbase.jdbc.version} + + + + com.taosdata.jdbc + taos-jdbcdriver + ${taos.version} + + + + + cn.iocoder.boot + yudao-spring-boot-starter-job + ${revision} + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mq + ${revision} + + + org.apache.rocketmq + rocketmq-spring-boot-starter + ${rocketmq-spring.version} + + + + + cn.iocoder.boot + yudao-spring-boot-starter-protection + ${revision} + + + + com.baomidou + lock4j-redisson-spring-boot-starter + ${lock4j.version} + + + redisson-spring-boot-starter + org.redisson + + + + + + + cn.iocoder.boot + yudao-spring-boot-starter-monitor + ${revision} + + + + org.apache.skywalking + apm-toolkit-trace + ${skywalking.version} + + + org.apache.skywalking + apm-toolkit-logback-1.x + ${skywalking.version} + + + org.apache.skywalking + apm-toolkit-opentracing + ${skywalking.version} + + + + + + + + + + + + + io.opentracing + opentracing-api + ${opentracing.version} + + + io.opentracing + opentracing-util + ${opentracing.version} + + + io.opentracing + opentracing-noop + ${opentracing.version} + + + + de.codecentric + spring-boot-admin-starter-server + ${spring-boot-admin.version} + + + de.codecentric + spring-boot-admin-server-cloud + + + + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + ${revision} + test + + + + org.mockito + mockito-inline + ${mockito-inline.version} + + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + + + asm + org.ow2.asm + + + org.mockito + mockito-core + + + + + + + org.flowable + flowable-spring-boot-starter-process + ${flowable.version} + + + org.flowable + flowable-spring-boot-starter-actuator + ${flowable.version} + + + + + + cn.iocoder.boot + yudao-common + ${revision} + + + + cn.iocoder.boot + yudao-spring-boot-starter-excel + ${revision} + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-jdk8 + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + cn.hutool + hutool-all + ${hutool-5.version} + + + org.dromara.hutool + hutool-extra + ${hutool-6.version} + + + + com.alibaba + easyexcel + ${easyexcel.version} + + + commons-io + commons-io + ${commons-io.version} + + + org.apache.commons + commons-compress + ${commons-compress.version} + + + org.apache.tika + tika-core + ${tika-core.version} + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + + com.alibaba + fastjson + ${fastjson.version} + + + + com.google.guava + guava + ${guava.version} + + + + com.alibaba + transmittable-thread-local + ${transmittable-thread-local.version} + + + + commons-net + commons-net + ${commons-net.version} + + + + com.jcraft + jsch + ${jsch.version} + + + + com.anji-plus + captcha-spring-boot-starter + ${anji-plus-captcha.version} + + + + org.lionsoul + ip2region + ${ip2region.version} + + + + org.jsoup + jsoup + ${jsoup.version} + + + + + software.amazon.awssdk + s3 + ${awssdk.version} + + + + me.zhyd.oauth + JustAuth + ${justauth.version} + + + com.xkcoding.justauth + justauth-spring-boot-starter + ${justauth-starter.version} + + + + cn.hutool + hutool-core + + + + + + com.github.binarywang + weixin-java-pay + ${weixin-java.version} + + + com.github.binarywang + wx-java-mp-spring-boot-starter + ${weixin-java.version} + + + com.github.binarywang + wx-java-miniapp-spring-boot-starter + ${weixin-java.version} + + + + + org.jeecgframework.jimureport + jimureport-spring-boot3-starter-fastjson2 + ${jimureport.version} + + + org.jeecgframework.jimureport + jimubi-spring-boot3-starter + ${jimubi.version} + + + com.github.jsqlparser + jsqlparser + + + + + + + org.pf4j + pf4j-spring + ${pf4j-spring.version} + + + org.slf4j + slf4j-log4j12 + + + + + + + io.vertx + vertx-core + ${vertx.version} + + + io.vertx + vertx-web + ${vertx.version} + + + io.vertx + vertx-mqtt + ${vertx.version} + + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + ${mqtt.version} + + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + ${flatten-maven-plugin.version} + + bom + true + + + + + flatten + + flatten + process-resources + + + + clean + + flatten.clean + clean + + + + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/pom.xml new file mode 100644 index 0000000..76e6740 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + yudao + cn.iocoder.boot + ${revision} + + pom + + yudao-common + yudao-spring-boot-starter-mybatis + yudao-spring-boot-starter-redis + yudao-spring-boot-starter-web + yudao-spring-boot-starter-security + yudao-spring-boot-starter-websocket + + yudao-spring-boot-starter-monitor + yudao-spring-boot-starter-protection + yudao-spring-boot-starter-job + yudao-spring-boot-starter-mq + + yudao-spring-boot-starter-excel + yudao-spring-boot-starter-test + + yudao-spring-boot-starter-biz-tenant + yudao-spring-boot-starter-biz-data-permission + yudao-spring-boot-starter-biz-ip + + + yudao-framework + + 该包是技术组件,每个子包,代表一个组件。每个组件包括两部分: + 1. core 包:是该组件的核心封装 + 2. config 包:是该组件基于 Spring 的配置 + + 技术组件,也分成两类: + 1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展 + 2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。 + 如果是业务组件,Maven 名字会包含 biz + + https://github.com/YunaiV/ruoyi-vue-pro + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/pom.xml new file mode 100644 index 0000000..b28da08 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/pom.xml @@ -0,0 +1,149 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-common + jar + + ${project.artifactId} + 定义基础 pojo 类、枚举、工具类等等 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + org.springframework + spring-core + provided + + + org.springframework + spring-expression + provided + + + org.springframework + spring-aop + provided + + + org.aspectj + aspectjweaver + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + org.springdoc + springdoc-openapi-starter-webmvc-api + provided + + + + + org.apache.skywalking + apm-toolkit-trace + + + + + org.projectlombok + lombok + + + + org.mapstruct + mapstruct + + + org.mapstruct + mapstruct-jdk8 + + + org.mapstruct + mapstruct-processor + + + + com.google.guava + guava + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + com.fasterxml.jackson.core + jackson-core + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + provided + + + + org.slf4j + slf4j-api + provided + + + + jakarta.validation + jakarta.validation-api + provided + + + + cn.hutool + hutool-all + + + + com.alibaba + transmittable-thread-local + + + + com.fhs-opensource + easy-trans-anno + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java new file mode 100644 index 0000000..efa9ca1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiAccessLogCommonApi.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.common.biz.infra.logger; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO; +import jakarta.validation.Valid; +import org.springframework.scheduling.annotation.Async; + +/** + * API 访问日志的 API 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogCommonApi { + + /** + * 创建 API 访问日志 + * + * @param createDTO 创建信息 + */ + void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO); + + /** + * 【异步】创建 API 访问日志 + * + * @param createDTO 访问日志 DTO + */ + @Async + default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) { + createApiAccessLog(createDTO); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java new file mode 100644 index 0000000..98743be --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/ApiErrorLogCommonApi.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.common.biz.infra.logger; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO; + +import jakarta.validation.Valid; +import org.springframework.scheduling.annotation.Async; + +/** + * API 错误日志的 API 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogCommonApi { + + /** + * 创建 API 错误日志 + * + * @param createDTO 创建信息 + */ + void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO); + + /** + * 【异步】创建 API 异常日志 + * + * @param createDTO 异常日志 DTO + */ + @Async + default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) { + createApiErrorLog(createDTO); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java new file mode 100644 index 0000000..b579cd3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiAccessLogCreateReqDTO.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.framework.common.biz.infra.logger.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * API 访问日志 + * + * @author 芋道源码 + */ +@Data +public class ApiAccessLogCreateReqDTO { + + /** + * 链路追踪编号 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + private String requestParams; + /** + * 响应结果 + */ + private String responseBody; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 操作模块 + */ + private String operateModule; + /** + * 操作名 + */ + private String operateName; + /** + * 操作分类 + * + * 枚举,参见 OperateTypeEnum 类 + */ + private Integer operateType; + + /** + * 开始请求时间 + */ + @NotNull(message = "开始请求时间不能为空") + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + @NotNull(message = "结束请求时间不能为空") + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + /** + * 结果码 + */ + @NotNull(message = "错误码不能为空") + private Integer resultCode; + /** + * 结果提示 + */ + private String resultMsg; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java new file mode 100644 index 0000000..ae5cd1d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/logger/dto/ApiErrorLogCreateReqDTO.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.framework.common.biz.infra.logger.dto; + +import lombok.Data; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 错误日志 + * + * @author 芋道源码 + */ +@Data +public class ApiErrorLogCreateReqDTO { + + /** + * 链路编号 + */ + private String traceId; + /** + * 账号编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 异常时间 + */ + @NotNull(message = "异常时间不能为空") + private LocalDateTime exceptionTime; + /** + * 异常名 + */ + @NotNull(message = "异常名不能为空") + private String exceptionName; + /** + * 异常发生的类全名 + */ + @NotNull(message = "异常发生的类全名不能为空") + private String exceptionClassName; + /** + * 异常发生的类文件 + */ + @NotNull(message = "异常发生的类文件不能为空") + private String exceptionFileName; + /** + * 异常发生的方法名 + */ + @NotNull(message = "异常发生的方法名不能为空") + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + */ + @NotNull(message = "异常发生的方法所在行不能为空") + private Integer exceptionLineNumber; + /** + * 异常的栈轨迹异常的栈轨迹 + */ + @NotNull(message = "异常的栈轨迹不能为空") + private String exceptionStackTrace; + /** + * 异常导致的根消息 + */ + @NotNull(message = "异常导致的根消息不能为空") + private String exceptionRootCauseMessage; + /** + * 异常导致的消息 + */ + @NotNull(message = "异常导致的消息不能为空") + private String exceptionMessage; + + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/package-info.java new file mode 100644 index 0000000..d7d8a3b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/infra/package-info.java @@ -0,0 +1,4 @@ +/** + * 针对 infra 模块的 api 包 + */ +package cn.iocoder.yudao.framework.common.biz.infra; \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/package-info.java new file mode 100644 index 0000000..2134e48 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/package-info.java @@ -0,0 +1,4 @@ +/** + * 特殊:用于 framework 下,starter 需要调用 biz 业务模块的接口定义! + */ +package cn.iocoder.yudao.framework.common.biz; \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/DictDataCommonApi.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/DictDataCommonApi.java new file mode 100644 index 0000000..92c87af --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/DictDataCommonApi.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.common.biz.system.dict; + +import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO; + +import java.util.List; + +/** + * 字典数据 API 接口 + * + * @author 芋道源码 + */ +public interface DictDataCommonApi { + + /** + * 获得指定字典类型的字典数据列表 + * + * @param dictType 字典类型 + * @return 字典数据列表 + */ + List getDictDataList(String dictType); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/dto/DictDataRespDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/dto/DictDataRespDTO.java new file mode 100644 index 0000000..bf32e7e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/dict/dto/DictDataRespDTO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.common.biz.system.dict.dto; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +/** + * 字典数据 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DictDataRespDTO { + + /** + * 字典标签 + */ + private String label; + /** + * 字典值 + */ + private String value; + /** + * 字典类型 + */ + private String dictType; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/OperateLogCommonApi.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/OperateLogCommonApi.java new file mode 100644 index 0000000..96753e0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/OperateLogCommonApi.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.common.biz.system.logger; + +import cn.iocoder.yudao.framework.common.biz.system.logger.dto.OperateLogCreateReqDTO; +import jakarta.validation.Valid; +import org.springframework.scheduling.annotation.Async; + +/** + * 操作日志 API 接口 + * + * @author 芋道源码 + */ +public interface OperateLogCommonApi { + + /** + * 创建操作日志 + * + * @param createReqDTO 请求 + */ + void createOperateLog(@Valid OperateLogCreateReqDTO createReqDTO); + + /** + * 【异步】创建操作日志 + * + * @param createReqDTO 请求 + */ + @Async + default void createOperateLogAsync(OperateLogCreateReqDTO createReqDTO) { + createOperateLog(createReqDTO); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java new file mode 100644 index 0000000..a2d7990 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/logger/dto/OperateLogCreateReqDTO.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.framework.common.biz.system.logger.dto; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 系统操作日志 Create Request DTO + * + * @author HUIHUI + */ +@Data +public class OperateLogCreateReqDTO { + + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + /** + * 操作模块类型 + */ + @NotEmpty(message = "操作模块类型不能为空") + private String type; + /** + * 操作名 + */ + @NotEmpty(message = "操作名不能为空") + private String subType; + /** + * 操作模块业务编号 + */ + @NotNull(message = "操作模块业务编号不能为空") + private Long bizId; + /** + * 操作内容,记录整个操作的明细 + * 例如说,修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。 + */ + @NotEmpty(message = "操作内容不能为空") + private String action; + /** + * 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ) + * 例如说,记录订单编号,{ orderId: "1"} + */ + private String extra; + + /** + * 请求方法名 + */ + @NotEmpty(message = "请求方法名不能为空") + private String requestMethod; + /** + * 请求地址 + */ + @NotEmpty(message = "请求地址不能为空") + private String requestUrl; + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotEmpty(message = "浏览器 UA 不能为空") + private String userAgent; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java new file mode 100644 index 0000000..c23d936 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/OAuth2TokenCommonApi.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.framework.common.biz.system.oauth2; + +import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO; +import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO; + +import jakarta.validation.Valid; + +/** + * OAuth2.0 Token API 接口 + * + * @author 芋道源码 + */ +public interface OAuth2TokenCommonApi { + + /** + * 创建访问令牌 + * + * @param reqDTO 访问令牌的创建信息 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO createAccessToken(@Valid OAuth2AccessTokenCreateReqDTO reqDTO); + + /** + * 校验访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken); + + /** + * 移除访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO removeAccessToken(String accessToken); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java new file mode 100644 index 0000000..a3c70ee --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.common.biz.system.oauth2.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * OAuth2.0 访问令牌的校验 Response DTO + * + * @author 芋道源码 + */ +@Data +public class OAuth2AccessTokenCheckRespDTO implements Serializable { + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 用户信息 + */ + private Map userInfo; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围的数组 + */ + private List scopes; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java new file mode 100644 index 0000000..72dcd9b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.common.biz.system.oauth2.dto; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * OAuth2.0 访问令牌创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class OAuth2AccessTokenCreateReqDTO implements Serializable { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + @InEnum(value = UserTypeEnum.class, message = "用户类型必须是 {value}") + private Integer userType; + /** + * 客户端编号 + */ + @NotNull(message = "客户端编号不能为空") + private String clientId; + /** + * 授权范围 + */ + private List scopes; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java new file mode 100644 index 0000000..c7b49f6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/oauth2/dto/OAuth2AccessTokenRespDTO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.common.biz.system.oauth2.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * OAuth2.0 访问令牌的信息 Response DTO + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class OAuth2AccessTokenRespDTO implements Serializable { + + /** + * 访问令牌 + */ + private String accessToken; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/package-info.java new file mode 100644 index 0000000..9589ee1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/package-info.java @@ -0,0 +1,4 @@ +/** + * 针对 system 模块的 api 包 + */ +package cn.iocoder.yudao.framework.common.biz.system; \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/PermissionCommonApi.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/PermissionCommonApi.java new file mode 100644 index 0000000..f842ed0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/PermissionCommonApi.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.framework.common.biz.system.permission; + +import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; + +/** + * 权限 API 接口 + * + * @author 芋道源码 + */ +public interface PermissionCommonApi { + + /** + * 判断是否有权限,任一一个即可 + * + * @param userId 用户编号 + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(Long userId, String... permissions); + + /** + * 判断是否有角色,任一一个即可 + * + * @param userId 用户编号 + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(Long userId, String... roles); + + /** + * 获得登陆用户的部门数据权限 + * + * @param userId 用户编号 + * @return 部门数据权限 + */ + DeptDataPermissionRespDTO getDeptDataPermission(Long userId); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/dto/DeptDataPermissionRespDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/dto/DeptDataPermissionRespDTO.java new file mode 100644 index 0000000..952c84d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/permission/dto/DeptDataPermissionRespDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.common.biz.system.permission.dto; + +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * 部门的数据权限 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DeptDataPermissionRespDTO { + + /** + * 是否可查看全部数据 + */ + private Boolean all; + /** + * 是否可查看自己的数据 + */ + private Boolean self; + /** + * 可查看的部门编号数组 + */ + private Set deptIds; + + public DeptDataPermissionRespDTO() { + this.all = false; + this.self = false; + this.deptIds = new HashSet<>(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/tenant/TenantCommonApi.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/tenant/TenantCommonApi.java new file mode 100644 index 0000000..809597e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/biz/system/tenant/TenantCommonApi.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.common.biz.system.tenant; + +import java.util.List; + +/** + * 多租户的 API 接口 + * + * @author 芋道源码 + */ +public interface TenantCommonApi { + + /** + * 获得所有租户 + * + * @return 租户编号数组 + */ + List getTenantIdList(); + + /** + * 校验租户是否合法 + * + * @param id 租户编号 + */ + void validateTenant(Long id); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/ArrayValuable.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/ArrayValuable.java new file mode 100644 index 0000000..b83451f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/ArrayValuable.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.framework.common.core; + +/** + * 可生成 T 数组的接口 + * + * @author HUIHUI + */ +public interface ArrayValuable { + + /** + * @return 数组 + */ + T[] array(); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/KeyValue.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/KeyValue.java new file mode 100644 index 0000000..07a8f39 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/KeyValue.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.common.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Key Value 的键值对 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KeyValue implements Serializable { + + private K key; + private V value; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/CommonStatusEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/CommonStatusEnum.java new file mode 100644 index 0000000..f2a63dc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/CommonStatusEnum.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.common.enums; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 通用状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum CommonStatusEnum implements ArrayValuable { + + ENABLE(0, "开启"), + DISABLE(1, "关闭"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(CommonStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static boolean isEnable(Integer status) { + return ObjUtil.equal(ENABLE.status, status); + } + + public static boolean isDisable(Integer status) { + return ObjUtil.equal(DISABLE.status, status); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java new file mode 100644 index 0000000..8d6a791 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.common.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 时间间隔的枚举 + * + * @author dhb52 + */ +@Getter +@AllArgsConstructor +public enum DateIntervalEnum implements ArrayValuable { + + DAY(1, "天"), + WEEK(2, "周"), + MONTH(3, "月"), + QUARTER(4, "季度"), + YEAR(5, "年") + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(DateIntervalEnum::getInterval).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer interval; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static DateIntervalEnum valueOf(Integer interval) { + return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values()); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DocumentEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DocumentEnum.java new file mode 100644 index 0000000..a96ee24 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DocumentEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 文档地址 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum DocumentEnum { + + REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"), + TENANT("https://doc.iocoder.cn", "SaaS 多租户文档"); + + private final String url; + private final String memo; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/RpcConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/RpcConstants.java new file mode 100644 index 0000000..b1c53db --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/RpcConstants.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.framework.common.enums; + +/** + * RPC 相关的枚举 + * + * 虽然放在 yudao-spring-boot-starter-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处 + * + * @author 芋道源码 + */ +public class RpcConstants { + + /** + * RPC API 的前缀 + */ + public static final String RPC_API_PREFIX = "/rpc-api"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java new file mode 100644 index 0000000..33482d4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.common.enums; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 终端的枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum TerminalEnum implements ArrayValuable { + + UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它 + WECHAT_MINI_PROGRAM(10, "微信小程序"), + WECHAT_WAP(11, "微信公众号"), + H5(20, "H5 网页"), + APP(31, "手机 App"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(TerminalEnum::getTerminal).toArray(Integer[]::new); + + /** + * 终端 + */ + private final Integer terminal; + /** + * 终端名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/UserTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/UserTypeEnum.java new file mode 100644 index 0000000..e00ec0b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/UserTypeEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.common.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 全局用户类型枚举 + */ +@AllArgsConstructor +@Getter +public enum UserTypeEnum implements ArrayValuable { + + MEMBER(1, "会员"), // 面向 c 端,普通用户 + ADMIN(2, "管理员"); // 面向 b 端,管理后台 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(UserTypeEnum::getValue).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer value; + /** + * 类型名 + */ + private final String name; + + public static UserTypeEnum valueOf(Integer value) { + return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java new file mode 100644 index 0000000..11a5ee0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.common.enums; + +/** + * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 + * + * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下 + * + * @author 芋道源码 + */ +public interface WebFilterOrderEnum { + + int CORS_FILTER = Integer.MIN_VALUE; + + int TRACE_FILTER = CORS_FILTER + 1; + + int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; + + // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 + + int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 + + int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 + + int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面 + + // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类 + + int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面 + + int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面 + + int DEMO_FILTER = Integer.MAX_VALUE; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ErrorCode.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ErrorCode.java new file mode 100644 index 0000000..45feaec --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ErrorCode.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.common.exception; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.exception.enums.ServiceErrorCodeRange; +import lombok.Data; + +/** + * 错误码对象 + * + * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} + * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} + * + * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 + */ +@Data +public class ErrorCode { + + /** + * 错误码 + */ + private final Integer code; + /** + * 错误提示 + */ + private final String msg; + + public ErrorCode(Integer code, String message) { + this.code = code; + this.msg = message; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServerException.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServerException.java new file mode 100644 index 0000000..fac56d5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServerException.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.framework.common.exception; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 服务器异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServerException extends RuntimeException { + + /** + * 全局错误码 + * + * @see GlobalErrorCodeConstants + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServerException() { + } + + public ServerException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServerException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServerException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServerException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServiceException.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServiceException.java new file mode 100644 index 0000000..5c6967e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/ServiceException.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.framework.common.exception; + +import cn.iocoder.yudao.framework.common.exception.enums.ServiceErrorCodeRange; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务逻辑异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServiceException extends RuntimeException { + + /** + * 业务错误码 + * + * @see ServiceErrorCodeRange + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() { + } + + public ServiceException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServiceException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServiceException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServiceException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java new file mode 100644 index 0000000..edf31f2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.common.exception.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * 全局错误码枚举 + * 0-999 系统异常编码保留 + * + * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status + * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 + * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 + * + * @author 芋道源码 + */ +public interface GlobalErrorCodeConstants { + + ErrorCode SUCCESS = new ErrorCode(0, "成功"); + + // ========== 客户端错误段 ========== + + ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); + ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); + ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); + ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); + ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); + ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 + ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); + + // ========== 服务端错误段 ========== + + ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); + ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启"); + ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项"); + + // ========== 自定义错误段 ========== + ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求 + ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); + + ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java new file mode 100644 index 0000000..950ab25 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/ServiceErrorCodeRange.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.framework.common.exception.enums; + +/** + * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 + * + * 一共 10 位,分成四段 + * + * 第一段,1 位,类型 + * 1 - 业务级别异常 + * x - 预留 + * 第二段,3 位,系统类型 + * 001 - 用户系统 + * 002 - 商品系统 + * 003 - 订单系统 + * 004 - 支付系统 + * 005 - 优惠劵系统 + * ... - ... + * 第三段,3 位,模块 + * 不限制规则。 + * 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子: + * 001 - OAuth2 模块 + * 002 - User 模块 + * 003 - MobileCode 模块 + * 第四段,3 位,错误码 + * 不限制规则。 + * 一般建议,每个模块自增。 + * + * @author 芋道源码 + */ +public class ServiceErrorCodeRange { + + // 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000) + // 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000) + // 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000) + // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000) + // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000) + // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000) + // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000) + + // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000) + // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000) + // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000) + + // 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000) + + // 模块 ai 错误码区间 [1-022-000-000 ~ 1-023-000-000) + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/util/ServiceExceptionUtil.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/util/ServiceExceptionUtil.java new file mode 100644 index 0000000..03c0859 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/util/ServiceExceptionUtil.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.framework.common.exception.util; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +/** + * {@link ServiceException} 工具类 + * + * 目的在于,格式化异常信息提示。 + * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 + * + */ +@Slf4j +public class ServiceExceptionUtil { + + // ========== 和 ServiceException 的集成 ========== + + public static ServiceException exception(ErrorCode errorCode) { + return exception0(errorCode.getCode(), errorCode.getMsg()); + } + + public static ServiceException exception(ErrorCode errorCode, Object... params) { + return exception0(errorCode.getCode(), errorCode.getMsg(), params); + } + + public static ServiceException exception0(Integer code, String messagePattern, Object... params) { + String message = doFormat(code, messagePattern, params); + return new ServiceException(code, message); + } + + public static ServiceException invalidParamException(String messagePattern, Object... params) { + return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params); + } + + // ========== 格式化方法 ========== + + /** + * 将错误编号对应的消息使用 params 进行格式化。 + * + * @param code 错误编号 + * @param messagePattern 消息模版 + * @param params 参数 + * @return 格式化后的提示 + */ + @VisibleForTesting + public static String doFormat(int code, String messagePattern, Object... params) { + StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); + int i = 0; + int j; + int l; + for (l = 0; l < params.length; l++) { + j = messagePattern.indexOf("{}", i); + if (j == -1) { + log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + if (i == 0) { + return messagePattern; + } else { + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + } else { + sbuf.append(messagePattern, i, j); + sbuf.append(params[l]); + i = j + 2; + } + } + if (messagePattern.indexOf("{}", i) != -1) { + log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + } + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java new file mode 100644 index 0000000..f3f2574 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/package-info.java @@ -0,0 +1,6 @@ +/** + * 基础的通用类,和框架无关 + * + * 例如说,CommonResult 为通用返回 + */ +package cn.iocoder.yudao.framework.common; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java new file mode 100644 index 0000000..ac74103 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java @@ -0,0 +1,121 @@ +package cn.iocoder.yudao.framework.common.pojo; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + * + * @see ErrorCode#getCode() + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + * + * @see ErrorCode#getMsg() () + */ + private String msg; + + /** + * 将传入的 result 对象,转换成另外一个泛型结果的对象 + * + * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 + * + * @param result 传入的 result 对象 + * @param 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + public static CommonResult error(ErrorCode errorCode, Object... params) { + Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = errorCode.getCode(); + result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params); + return result; + } + + public static CommonResult error(ErrorCode errorCode) { + return error(errorCode.getCode(), errorCode.getMsg()); + } + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); + result.data = data; + result.msg = ""; + return result; + } + + public static boolean isSuccess(Integer code) { + return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isSuccess() { + return isSuccess(code); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isError() { + return !isSuccess(); + } + + // ========= 和 Exception 异常体系集成 ========= + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + */ + public void checkError() throws ServiceException { + if (isSuccess()) { + return; + } + // 业务异常 + throw new ServiceException(code, msg); + } + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + * 如果没有,则返回 {@link #data} 数据 + */ + @JsonIgnore // 避免 jackson 序列化 + public T getCheckedData() { + checkError(); + return data; + } + + public static CommonResult error(ServiceException serviceException) { + return error(serviceException.getCode(), serviceException.getMessage()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java new file mode 100644 index 0000000..268d117 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.common.pojo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotNull; +import java.io.Serializable; + +@Schema(description="分页参数") +@Data +public class PageParam implements Serializable { + + private static final Integer PAGE_NO = 1; + private static final Integer PAGE_SIZE = 10; + + /** + * 每页条数 - 不分页 + * + * 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。 + */ + public static final Integer PAGE_SIZE_NONE = -1; + + @Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1") + @NotNull(message = "页码不能为空") + @Min(value = 1, message = "页码最小值为 1") + private Integer pageNo = PAGE_NO; + + @Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "每页条数不能为空") + @Min(value = 1, message = "每页条数最小值为 1") + @Max(value = 100, message = "每页条数最大值为 100") + private Integer pageSize = PAGE_SIZE; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java new file mode 100644 index 0000000..ff9087a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.common.pojo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "分页结果") +@Data +public final class PageResult implements Serializable { + + @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) + private List list; + + @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED) + private Long total; + + public PageResult() { + } + + public PageResult(List list, Long total) { + this.list = list; + this.total = total; + } + + public PageResult(Long total) { + this.list = new ArrayList<>(); + this.total = total; + } + + public static PageResult empty() { + return new PageResult<>(0L); + } + + public static PageResult empty(Long total) { + return new PageResult<>(total); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortablePageParam.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortablePageParam.java new file mode 100644 index 0000000..2365c41 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortablePageParam.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.common.pojo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "可排序的分页参数") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SortablePageParam extends PageParam { + + @Schema(description = "排序字段") + private List sortingFields; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortingField.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortingField.java new file mode 100644 index 0000000..07a68b0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/SortingField.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.common.pojo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 排序字段 DTO + * + * 类名加了 ing 的原因是,避免和 ES SortField 重名。 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SortingField implements Serializable { + + /** + * 顺序 - 升序 + */ + public static final String ORDER_ASC = "asc"; + /** + * 顺序 - 降序 + */ + public static final String ORDER_DESC = "desc"; + + /** + * 字段 + */ + private String field; + /** + * 顺序 + */ + private String order; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java new file mode 100644 index 0000000..12a6e17 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.framework.common.util.cache; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import java.time.Duration; +import java.util.concurrent.Executors; + +/** + * Cache 工具类 + * + * @author 芋道源码 + */ +public class CacheUtils { + + /** + * 构建异步刷新的 LoadingCache 对象 + * + * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法 + * + * 或者简单理解: + * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法 + * 2、和“全局”、“系统”相关的,使用当前缓存方法 + * + * @param duration 过期时间 + * @param loader CacheLoader 对象 + * @return LoadingCache 对象 + */ + public static LoadingCache buildAsyncReloadingCache(Duration duration, CacheLoader loader) { + return CacheBuilder.newBuilder() + // 只阻塞当前数据加载线程,其他线程返回旧值 + .refreshAfterWrite(duration) + // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 + .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置 + } + + /** + * 构建同步刷新的 LoadingCache 对象 + * + * @param duration 过期时间 + * @param loader CacheLoader 对象 + * @return LoadingCache 对象 + */ + public static LoadingCache buildCache(Duration duration, CacheLoader loader) { + return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/ArrayUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/ArrayUtils.java new file mode 100644 index 0000000..4285b8f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/ArrayUtils.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.common.util.collection; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.util.ArrayUtil; + +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.Function; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * Array 工具类 + * + * @author 芋道源码 + */ +public class ArrayUtils { + + /** + * 将 object 和 newElements 合并成一个数组 + * + * @param object 对象 + * @param newElements 数组 + * @param 泛型 + * @return 结果数组 + */ + @SafeVarargs + public static Consumer[] append(Consumer object, Consumer... newElements) { + if (object == null) { + return newElements; + } + Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); + result[0] = object; + System.arraycopy(newElements, 0, result, 1, newElements.length); + return result; + } + + public static V[] toArray(Collection from, Function mapper) { + return toArray(convertList(from, mapper)); + } + + @SuppressWarnings("unchecked") + public static T[] toArray(Collection from) { + if (CollectionUtil.isEmpty(from)) { + return (T[]) (new Object[0]); + } + return ArrayUtil.toArray(from, (Class) IterUtil.getElementType(from.iterator())); + } + + public static T get(T[] array, int index) { + if (null == array || index >= array.length) { + return null; + } + return array[index]; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java new file mode 100644 index 0000000..aa523b9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java @@ -0,0 +1,352 @@ +package cn.iocoder.yudao.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.google.common.collect.ImmutableMap; + +import java.util.*; +import java.util.function.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static cn.hutool.core.convert.Convert.toCollection; +import static java.util.Arrays.asList; + +/** + * Collection 工具类 + * + * @author 芋道源码 + */ +public class CollectionUtils { + + public static boolean containsAny(Object source, Object... targets) { + return asList(targets).contains(source); + } + + public static boolean isAnyEmpty(Collection... collections) { + return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); + } + + public static boolean anyMatch(Collection from, Predicate predicate) { + return from.stream().anyMatch(predicate); + } + + public static List filterList(Collection from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(predicate).collect(Collectors.toList()); + } + + public static List distinct(Collection from, Function keyMapper) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return distinct(from, keyMapper, (t1, t2) -> t1); + } + + public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); + } + + public static List convertList(T[] from, Function func) { + if (ArrayUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return convertList(Arrays.asList(from), func); + } + + public static List convertList(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertList(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static PageResult convertPage(PageResult from, Function func) { + if (ArrayUtil.isEmpty(from)) { + return new PageResult<>(from.getTotal()); + } + return new PageResult<>(convertList(from.getList(), func), from.getTotal()); + } + + public static List convertListByFlatMap(Collection from, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertListByFlatMap(Collection from, + Function mapper, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List mergeValuesFromMap(Map> map) { + return map.values() + .stream() + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + public static Set convertSet(Collection from) { + return convertSet(from, v -> v); + } + + public static Set convertSet(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Set convertSet(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Map convertMapByFilter(Collection from, Predicate filter, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v)); + } + + public static Set convertSetByFlatMap(Collection from, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Set convertSetByFlatMap(Collection from, + Function mapper, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Map convertMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, Function.identity()); + } + + public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, Function.identity(), supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream() + .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); + } + + // 暂时没想好名字,先以 2 结尾噶 + public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); + } + + public static Map convertImmutableMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return Collections.emptyMap(); + } + ImmutableMap.Builder builder = ImmutableMap.builder(); + from.forEach(item -> builder.put(keyFunc.apply(item), item)); + return builder.build(); + } + + /** + * 对比老、新两个列表,找出新增、修改、删除的数据 + * + * @param oldList 老列表 + * @param newList 新列表 + * @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同 + * 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据 + * @return [新增列表、修改列表、删除列表] + */ + public static List> diffList(Collection oldList, Collection newList, + BiFunction sameFunc) { + List createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除 + List updateList = new ArrayList<>(); + List deleteList = new ArrayList<>(); + + // 通过以 oldList 为主遍历,找出 updateList 和 deleteList + for (T oldObj : oldList) { + // 1. 寻找是否有匹配的 + T foundObj = null; + for (Iterator iterator = createList.iterator(); iterator.hasNext(); ) { + T newObj = iterator.next(); + // 1.1 不匹配,则直接跳过 + if (!sameFunc.apply(oldObj, newObj)) { + continue; + } + // 1.2 匹配,则移除,并结束寻找 + iterator.remove(); + foundObj = newObj; + break; + } + // 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中 + if (foundObj != null) { + updateList.add(foundObj); + } else { + deleteList.add(oldObj); + } + } + return asList(createList, updateList, deleteList); + } + + public static boolean containsAny(Collection source, Collection candidates) { + return org.springframework.util.CollectionUtils.containsAny(source, candidates); + } + + public static T getFirst(List from) { + return !CollectionUtil.isEmpty(from) ? from.get(0) : null; + } + + public static T findFirst(Collection from, Predicate predicate) { + return findFirst(from, predicate, Function.identity()); + } + + public static U findFirst(Collection from, Predicate predicate, Function func) { + if (CollUtil.isEmpty(from)) { + return null; + } + return from.stream().filter(predicate).findFirst().map(func).orElse(null); + } + + public static > V getMaxValue(Collection from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert !from.isEmpty(); // 断言,避免告警 + T t = from.stream().max(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > V getMinValue(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + T t = from.stream().min(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > T getMinObject(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + return from.stream().min(Comparator.comparing(valueFunc)).get(); + } + + public static > V getSumValue(Collection from, Function valueFunc, + BinaryOperator accumulator) { + return getSumValue(from, valueFunc, accumulator, null); + } + + public static > V getSumValue(Collection from, Function valueFunc, + BinaryOperator accumulator, V defaultValue) { + if (CollUtil.isEmpty(from)) { + return defaultValue; + } + assert !from.isEmpty(); // 断言,避免告警 + return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue); + } + + public static void addIfNotNull(Collection coll, T item) { + if (item == null) { + return; + } + coll.add(item); + } + + public static Collection singleton(T obj) { + return obj == null ? Collections.emptyList() : Collections.singleton(obj); + } + + public static List newArrayList(List> list) { + return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList()); + } + + /** + * 转换为 LinkedHashSet + * + * @param 元素类型 + * @param elementType 集合中元素类型 + * @param value 被转换的值 + * @return {@link LinkedHashSet} + */ + @SuppressWarnings("unchecked") + public static LinkedHashSet toLinkedHashSet(Class elementType, Object value) { + return (LinkedHashSet) toCollection(LinkedHashSet.class, elementType, value); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java new file mode 100644 index 0000000..a59b53f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Map 工具类 + * + * @author 芋道源码 + */ +public class MapUtils { + + /** + * 从哈希表表中,获得 keys 对应的所有 value 数组 + * + * @param multimap 哈希表 + * @param keys keys + * @return value 数组 + */ + public static List getList(Multimap multimap, Collection keys) { + List result = new ArrayList<>(); + keys.forEach(k -> { + Collection values = multimap.get(k); + if (CollectionUtil.isEmpty(values)) { + return; + } + result.addAll(values); + }); + return result; + } + + /** + * 从哈希表查找到 key 对应的 value,然后进一步处理 + * key 为 null 时, 不处理 + * 注意,如果查找到的 value 为 null 时,不进行处理 + * + * @param map 哈希表 + * @param key key + * @param consumer 进一步处理的逻辑 + */ + public static void findAndThen(Map map, K key, Consumer consumer) { + if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) { + return; + } + V value = map.get(key); + if (value == null) { + return; + } + consumer.accept(value); + } + + public static Map convertMap(List> keyValues) { + Map map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); + keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); + return map; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/SetUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/SetUtils.java new file mode 100644 index 0000000..e8eba66 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/SetUtils.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; + +import java.util.Set; + +/** + * Set 工具类 + * + * @author 芋道源码 + */ +public class SetUtils { + + @SafeVarargs + public static Set asSet(T... objs) { + return CollUtil.newHashSet(objs); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java new file mode 100644 index 0000000..b51a838 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java @@ -0,0 +1,149 @@ +package cn.iocoder.yudao.framework.common.util.date; + +import cn.hutool.core.date.LocalDateTimeUtil; + +import java.time.*; +import java.util.Calendar; +import java.util.Date; + +/** + * 时间工具类 + * + * @author 芋道源码 + */ +public class DateUtils { + + /** + * 时区 - 默认 + */ + public static final String TIME_ZONE_DEFAULT = "GMT+8"; + + /** + * 秒转换成毫秒 + */ + public static final long SECOND_MILLIS = 1000; + + public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd"; + + public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; + + /** + * 将 LocalDateTime 转换成 Date + * + * @param date LocalDateTime + * @return LocalDateTime + */ + public static Date of(LocalDateTime date) { + if (date == null) { + return null; + } + // 将此日期时间与时区相结合以创建 ZonedDateTime + ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault()); + // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳 + Instant instant = zonedDateTime.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return Date.from(instant); + } + + /** + * 将 Date 转换成 LocalDateTime + * + * @param date Date + * @return LocalDateTime + */ + public static LocalDateTime of(Date date) { + if (date == null) { + return null; + } + // 转为时间戳 + Instant instant = date.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + public static Date addTime(Duration duration) { + return new Date(System.currentTimeMillis() + duration.toMillis()); + } + + public static boolean isExpired(LocalDateTime time) { + LocalDateTime now = LocalDateTime.now(); + return now.isAfter(time); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @return 指定时间 + */ + public static Date buildTime(int year, int mouth, int day) { + return buildTime(year, mouth, day, 0, 0, 0); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @param hour 小时 + * @param minute 分钟 + * @param second 秒 + * @return 指定时间 + */ + public static Date buildTime(int year, int mouth, int day, + int hour, int minute, int second) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, mouth - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒 + return calendar.getTime(); + } + + public static Date max(Date a, Date b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.compareTo(b) > 0 ? a : b; + } + + public static LocalDateTime max(LocalDateTime a, LocalDateTime b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.isAfter(b) ? a : b; + } + + /** + * 是否今天 + * + * @param date 日期 + * @return 是否 + */ + public static boolean isToday(LocalDateTime date) { + return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now()); + } + + /** + * 是否昨天 + * + * @param date 日期 + * @return 是否 + */ + public static boolean isYesterday(LocalDateTime date) { + return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java new file mode 100644 index 0000000..8d18479 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java @@ -0,0 +1,328 @@ +package cn.iocoder.yudao.framework.common.util.date; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.date.TemporalAccessorUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.List; + +import static cn.hutool.core.date.DatePattern.UTC_MS_WITH_XXX_OFFSET_PATTERN; +import static cn.hutool.core.date.DatePattern.createFormatter; + +/** + * 时间工具类,用于 {@link LocalDateTime} + * + * @author 芋道源码 + */ +public class LocalDateTimeUtils { + + /** + * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值 + */ + public static LocalDateTime EMPTY = buildTime(1970, 1, 1); + + public static DateTimeFormatter UTC_MS_WITH_XXX_OFFSET_FORMATTER = createFormatter(UTC_MS_WITH_XXX_OFFSET_PATTERN); + + /** + * 解析时间 + * + * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功 + * + * @param time 时间 + * @return 时间字符串 + */ + public static LocalDateTime parse(String time) { + try { + return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN); + } catch (DateTimeParseException e) { + return LocalDateTimeUtil.parse(time); + } + } + + public static LocalDateTime addTime(Duration duration) { + return LocalDateTime.now().plus(duration); + } + + public static LocalDateTime minusTime(Duration duration) { + return LocalDateTime.now().minus(duration); + } + + public static boolean beforeNow(LocalDateTime date) { + return date.isBefore(LocalDateTime.now()); + } + + public static boolean afterNow(LocalDateTime date) { + return date.isAfter(LocalDateTime.now()); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @return 指定时间 + */ + public static LocalDateTime buildTime(int year, int mouth, int day) { + return LocalDateTime.of(year, mouth, day, 0, 0, 0); + } + + public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, + int year2, int mouth2, int day2) { + return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; + } + + /** + * 判指定断时间,是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param time 指定时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) { + if (startTime == null || endTime == null || time == null) { + return false; + } + return LocalDateTimeUtil.isIn(parse(time), startTime, endTime); + } + + /** + * 判断当前时间是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime == null || endTime == null) { + return false; + } + return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime); + } + + /** + * 判断当前时间是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 是否 + */ + public static boolean isBetween(String startTime, String endTime) { + if (startTime == null || endTime == null) { + return false; + } + LocalDate nowDate = LocalDate.now(); + return LocalDateTimeUtil.isIn(LocalDateTime.now(), + LocalDateTime.of(nowDate, LocalTime.parse(startTime)), + LocalDateTime.of(nowDate, LocalTime.parse(endTime))); + } + + /** + * 判断时间段是否重叠 + * + * @param startTime1 开始 time1 + * @param endTime1 结束 time1 + * @param startTime2 开始 time2 + * @param endTime2 结束 time2 + * @return 重叠:true 不重叠:false + */ + public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) { + LocalDate nowDate = LocalDate.now(); + return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1), + LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2)); + } + + /** + * 获取指定日期所在的月份的开始时间 + * 例如:2023-09-30 00:00:00,000 + * + * @param date 日期 + * @return 月份的开始时间 + */ + public static LocalDateTime beginOfMonth(LocalDateTime date) { + return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN); + } + + /** + * 获取指定日期所在的月份的最后时间 + * 例如:2023-09-30 23:59:59,999 + * + * @param date 日期 + * @return 月份的结束时间 + */ + public static LocalDateTime endOfMonth(LocalDateTime date) { + return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX); + } + + /** + * 获得指定日期所在季度 + * + * @param date 日期 + * @return 所在季度 + */ + public static int getQuarterOfYear(LocalDateTime date) { + return (date.getMonthValue() - 1) / 3 + 1; + } + + /** + * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负 + * + * @param dateTime 日期 + * @return 相差天数 + */ + public static Long between(LocalDateTime dateTime) { + return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS); + } + + /** + * 获取今天的开始时间 + * + * @return 今天 + */ + public static LocalDateTime getToday() { + return LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); + } + + /** + * 获取昨天的开始时间 + * + * @return 昨天 + */ + public static LocalDateTime getYesterday() { + return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1)); + } + + /** + * 获取本月的开始时间 + * + * @return 本月 + */ + public static LocalDateTime getMonth() { + return beginOfMonth(LocalDateTime.now()); + } + + /** + * 获取本年的开始时间 + * + * @return 本年 + */ + public static LocalDateTime getYear() { + return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN); + } + + public static List getDateRangeList(LocalDateTime startTime, + LocalDateTime endTime, + Integer interval) { + // 1.1 找到枚举 + DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); + Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); + // 1.2 将时间对齐 + startTime = LocalDateTimeUtil.beginOfDay(startTime); + endTime = LocalDateTimeUtil.endOfDay(endTime); + + // 2. 循环,生成时间范围 + List timeRanges = new ArrayList<>(); + switch (intervalEnum) { + case DAY: + while (startTime.isBefore(endTime)) { + timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)}); + startTime = startTime.plusDays(1); + } + break; + case WEEK: + while (startTime.isBefore(endTime)) { + LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, endOfWeek}); + startTime = endOfWeek.plusNanos(1); + } + break; + case MONTH: + while (startTime.isBefore(endTime)) { + LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, endOfMonth}); + startTime = endOfMonth.plusNanos(1); + } + break; + case QUARTER: + while (startTime.isBefore(endTime)) { + int quarterOfYear = getQuarterOfYear(startTime); + LocalDateTime quarterEnd = quarterOfYear == 4 + ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1) + : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, quarterEnd}); + startTime = quarterEnd.plusNanos(1); + } + break; + case YEAR: + while (startTime.isBefore(endTime)) { + LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, endOfYear}); + startTime = endOfYear.plusNanos(1); + } + break; + default: + throw new IllegalArgumentException("Invalid interval: " + interval); + } + // 3. 兜底,最后一个时间,需要保持在 endTime 之前 + LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges); + if (lastTimeRange != null) { + lastTimeRange[1] = endTime; + } + return timeRanges; + } + + /** + * 格式化时间范围 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param interval 时间间隔 + * @return 时间范围 + */ + public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) { + // 1. 找到枚举 + DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); + Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); + + // 2. 循环,生成时间范围 + switch (intervalEnum) { + case DAY: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN); + case WEEK: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN) + + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime)); + case MONTH: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN); + case QUARTER: + return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime)); + case YEAR: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN); + default: + throw new IllegalArgumentException("Invalid interval: " + interval); + } + } + + /** + * 将给定的 {@link LocalDateTime} 转换为自 Unix 纪元时间(1970-01-01T00:00:00Z)以来的秒数。 + * + * @param sourceDateTime 需要转换的本地日期时间,不能为空 + * @return 自 1970-01-01T00:00:00Z 起的秒数(epoch second) + * @throws NullPointerException 如果 {@code sourceDateTime} 为 {@code null} + * @throws DateTimeException 如果转换过程中发生时间超出范围或其他时间处理异常 + */ + public static Long toEpochSecond(LocalDateTime sourceDateTime) { + return TemporalAccessorUtil.toInstant(sourceDateTime).getEpochSecond(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java new file mode 100644 index 0000000..e1e0ac0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -0,0 +1,175 @@ +package cn.iocoder.yudao.framework.common.util.http; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.map.TableMap; +import cn.hutool.core.net.url.UrlBuilder; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * HTTP 工具类 + * + * @author 芋道源码 + */ +public class HttpUtils { + + /** + * 编码 URL 参数 + * + * @param value 参数 + * @return 编码后的参数 + */ + public static String encodeUtf8(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + @SuppressWarnings("unchecked") + public static String replaceUrlQuery(String url, String key, String value) { + UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); + // 先移除 + TableMap query = (TableMap) + ReflectUtil.getFieldValue(builder.getQuery(), "query"); + query.remove(key); + // 后添加 + builder.addQuery(key, value); + return builder.build(); + } + + private String append(String base, Map query, boolean fragment) { + return append(base, query, null, fragment); + } + + /** + * 拼接 URL + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法 + * + * @param base 基础 URL + * @param query 查询参数 + * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射 + * @param fragment URL 的 fragment,即拼接到 # 中 + * @return 拼接后的 URL + */ + public static String append(String base, Map query, Map keys, boolean fragment) { + UriComponentsBuilder template = UriComponentsBuilder.newInstance(); + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base); + URI redirectUri; + try { + // assume it's encoded to start with (if it came in over the wire) + redirectUri = builder.build(true).toUri(); + } catch (Exception e) { + // ... but allow client registrations to contain hard-coded non-encoded values + redirectUri = builder.build().toUri(); + builder = UriComponentsBuilder.fromUri(redirectUri); + } + template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost()) + .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath()); + + if (fragment) { + StringBuilder values = new StringBuilder(); + if (redirectUri.getFragment() != null) { + String append = redirectUri.getFragment(); + values.append(append); + } + for (String key : query.keySet()) { + if (values.length() > 0) { + values.append("&"); + } + String name = key; + if (keys != null && keys.containsKey(key)) { + name = keys.get(key); + } + values.append(name).append("={").append(key).append("}"); + } + if (values.length() > 0) { + template.fragment(values.toString()); + } + UriComponents encoded = template.build().expand(query).encode(); + builder.fragment(encoded.getFragment()); + } else { + for (String key : query.keySet()) { + String name = key; + if (keys != null && keys.containsKey(key)) { + name = keys.get(key); + } + template.queryParam(name, "{" + key + "}"); + } + template.fragment(redirectUri.getFragment()); + UriComponents encoded = template.build().expand(query).encode(); + builder.query(encoded.getQuery()); + } + return builder.build().toUriString(); + } + + public static String[] obtainBasicAuthorization(HttpServletRequest request) { + String clientId; + String clientSecret; + // 先从 Header 中获取 + String authorization = request.getHeader("Authorization"); + authorization = StrUtil.subAfter(authorization, "Basic ", true); + if (StringUtils.hasText(authorization)) { + authorization = Base64.decodeStr(authorization); + clientId = StrUtil.subBefore(authorization, ":", false); + clientSecret = StrUtil.subAfter(authorization, ":", false); + // 再从 Param 中获取 + } else { + clientId = request.getParameter("client_id"); + clientSecret = request.getParameter("client_secret"); + } + + // 如果两者非空,则返回 + if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) { + return new String[]{clientId, clientSecret}; + } + return null; + } + + /** + * HTTP post 请求,基于 {@link cn.hutool.http.HttpUtil} 实现 + * + * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数 + * + * @param url URL + * @param headers 请求头 + * @param requestBody 请求体 + * @return 请求结果 + */ + public static String post(String url, Map headers, String requestBody) { + try (HttpResponse response = HttpRequest.post(url) + .addHeaders(headers) + .body(requestBody) + .execute()) { + return response.body(); + } + } + + /** + * HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现 + * + * 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数 + * + * @param url URL + * @param headers 请求头 + * @return 请求结果 + */ + public static String get(String url, Map headers) { + try (HttpResponse response = HttpRequest.get(url) + .addHeaders(headers) + .execute()) { + return response.body(); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java new file mode 100644 index 0000000..63732f1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.framework.common.util.io; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import lombok.SneakyThrows; + +import java.io.File; + +/** + * 文件工具类 + * + * @author 芋道源码 + */ +public class FileUtils { + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(String data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeUtf8String(data, file); + return file; + } + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(byte[] data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeBytes(data, file); + return file; + } + + /** + * 创建临时文件,无内容 + * 该文件会在 JVM 退出时,进行删除 + * + * @return 文件 + */ + @SneakyThrows + public static File createTempFile() { + // 创建文件,通过 UUID 保证唯一 + File file = File.createTempFile(IdUtil.simpleUUID(), null); + // 标记 JVM 退出时,自动删除 + file.deleteOnExit(); + return file; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java new file mode 100644 index 0000000..4a19f47 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.common.util.io; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.InputStream; + +/** + * IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法 + * + * @author 芋道源码 + */ +public class IoUtils { + + /** + * 从流中读取 UTF8 编码的内容 + * + * @param in 输入流 + * @param isClose 是否关闭 + * @return 内容 + * @throws IORuntimeException IO 异常 + */ + public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException { + return StrUtil.utf8Str(IoUtil.read(in, isClose)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java new file mode 100644 index 0000000..8bb8765 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java @@ -0,0 +1,210 @@ +package cn.iocoder.yudao.framework.common.util.json; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * JSON 工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class JsonUtils { + + private static ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值 + objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化 + } + + /** + * 初始化 objectMapper 属性 + *

+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean + * + * @param objectMapper ObjectMapper 对象 + */ + public static void init(ObjectMapper objectMapper) { + JsonUtils.objectMapper = objectMapper; + } + + @SneakyThrows + public static String toJsonString(Object object) { + return objectMapper.writeValueAsString(object); + } + + @SneakyThrows + public static byte[] toJsonByte(Object object) { + return objectMapper.writeValueAsBytes(object); + } + + @SneakyThrows + public static String toJsonPrettyString(Object object) { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); + } + + public static T parseObject(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, String path, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + JsonNode treeNode = objectMapper.readTree(text); + JsonNode pathNode = treeNode.path(path); + return objectMapper.readValue(pathNode.toString(), clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, Type type) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 将字符串解析成指定类型的对象 + * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, + * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。 + * + * @param text 字符串 + * @param clazz 类型 + * @return 对象 + */ + public static T parseObject2(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + return JSONUtil.toBean(text, clazz); + } + + public static T parseObject(byte[] bytes, Class clazz) { + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + try { + return objectMapper.readValue(bytes, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", bytes, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null + * + * @param text 字符串 + * @param typeReference 类型引用 + * @return 指定类型的对象 + */ + public static T parseObjectQuietly(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + return null; + } + } + + public static List parseArray(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static List parseArray(String text, String path, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + JsonNode treeNode = objectMapper.readTree(text); + JsonNode pathNode = treeNode.path(path); + return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(String text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(byte[] text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static boolean isJson(String text) { + return JSONUtil.isTypeJSON(text); + } + + /** + * 判断字符串是否为 JSON 类型的字符串 + * @param str 字符串 + */ + public static boolean isJsonObject(String str) { + return JSONUtil.isTypeJSONObject(str); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/NumberSerializer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/NumberSerializer.java new file mode 100644 index 0000000..35fc9f7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/NumberSerializer.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.common.util.json.databind; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; + +import java.io.IOException; + +/** + * Long 序列化规则 + * + * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题 + * + * @author 星语 + */ +@JacksonStdImpl +public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer { + + private static final long MAX_SAFE_INTEGER = 9007199254740991L; + private static final long MIN_SAFE_INTEGER = -9007199254740991L; + + public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class); + + public NumberSerializer(Class rawType) { + super(rawType); + } + + @Override + public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 超出范围 序列化位字符串 + if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { + super.serialize(value, gen, serializers); + } else { + gen.writeString(value.toString()); + } + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java new file mode 100644 index 0000000..5bf5d6c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.common.util.json.databind; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 基于时间戳的 LocalDateTime 反序列化器 + * + * @author 老五 + */ +public class TimestampLocalDateTimeDeserializer extends JsonDeserializer { + + public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // 将 Long 时间戳,转换为 LocalDateTime 对象 + return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java new file mode 100644 index 0000000..ef767a5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeSerializer.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.common.util.json.databind; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 基于时间戳的 LocalDateTime 序列化器 + * + * @author 老五 + */ +public class TimestampLocalDateTimeSerializer extends JsonSerializer { + + public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer(); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 将 LocalDateTime 对象,转换为 Long 时间戳 + gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java new file mode 100644 index 0000000..81092b6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.framework.common.util.monitor; + +import org.apache.skywalking.apm.toolkit.trace.TraceContext; + +/** + * 链路追踪工具类 + * + * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下 + * + * @author 芋道源码 + */ +public class TracerUtils { + + /** + * 私有化构造方法 + */ + private TracerUtils() { + } + + /** + * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。 + * 如果不存在的话为空字符串!!! + * + * @return 链路追踪编号 + */ + public static String getTraceId() { + return TraceContext.traceId(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java new file mode 100644 index 0000000..90888d7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java @@ -0,0 +1,131 @@ +package cn.iocoder.yudao.framework.common.util.number; + +import cn.hutool.core.math.Money; +import cn.hutool.core.util.NumberUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 金额工具类 + * + * @author 芋道源码 + */ +public class MoneyUtils { + + /** + * 金额的小数位数 + */ + private static final int PRICE_SCALE = 2; + + /** + * 百分比对应的 BigDecimal 对象 + */ + public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100); + + /** + * 计算百分比金额,四舍五入 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @return 百分比金额 + */ + public static Integer calculateRatePrice(Integer price, Double rate) { + return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue(); + } + + /** + * 计算百分比金额,向下传入 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @return 百分比金额 + */ + public static Integer calculateRatePriceFloor(Integer price, Double rate) { + return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue(); + } + + /** + * 计算百分比金额 + * + * @param price 金额(单位分) + * @param count 数量 + * @param percent 折扣(单位分),列如 60.2%,则传入 6020 + * @return 商品总价 + */ + public static Integer calculator(Integer price, Integer count, Integer percent) { + price = price * count; + if (percent == null) { + return price; + } + return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100)); + } + + /** + * 计算百分比金额 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @param scale 保留小数位数 + * @param roundingMode 舍入模式 + */ + public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) { + return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以 + .divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100 + } + + /** + * 分转元 + * + * @param fen 分 + * @return 元 + */ + public static BigDecimal fenToYuan(int fen) { + return new Money(0, fen).getAmount(); + } + + /** + * 分转元(字符串) + * + * 例如说 fen 为 1 时,则结果为 0.01 + * + * @param fen 分 + * @return 元 + */ + public static String fenToYuanStr(int fen) { + return new Money(0, fen).toString(); + } + + /** + * 金额相乘,默认进行四舍五入 + * + * 位数:{@link #PRICE_SCALE} + * + * @param price 金额 + * @param count 数量 + * @return 金额相乘结果 + */ + public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) { + if (price == null || count == null) { + return null; + } + return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP); + } + + /** + * 金额相乘(百分比),默认进行四舍五入 + * + * 位数:{@link #PRICE_SCALE} + * + * @param price 金额 + * @param percent 百分比 + * @return 金额相乘结果 + */ + public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) { + if (price == null || percent == null) { + return null; + } + return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java new file mode 100644 index 0000000..c2787f4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.framework.common.util.number; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 + * + * @author 芋道源码 + */ +public class NumberUtils { + + public static Long parseLong(String str) { + return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; + } + + public static Integer parseInt(String str) { + return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null; + } + + public static boolean isAllNumber(List values) { + if (CollUtil.isEmpty(values)) { + return false; + } + for (String value : values) { + if (!NumberUtil.isNumber(value)) { + return false; + } + } + return true; + } + + /** + * 通过经纬度获取地球上两点之间的距离 + * + * 参考 <DistanceUtil> 实现,目前它已经被 hutool 删除 + * + * @param lat1 经度1 + * @param lng1 纬度1 + * @param lat2 经度2 + * @param lng2 纬度2 + * @return 距离,单位:千米 + */ + public static double getDistance(double lat1, double lng1, double lat2, double lng2) { + double radLat1 = lat1 * Math.PI / 180.0; + double radLat2 = lat2 * Math.PI / 180.0; + double a = radLat1 - radLat2; + double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; + double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + + Math.cos(radLat1) * Math.cos(radLat2) + * Math.pow(Math.sin(b / 2), 2))); + distance = distance * 6378.137; + distance = Math.round(distance * 10000d) / 10000d; + return distance; + } + + /** + * 提供精确的乘法运算 + * + * 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null + * + * @param values 多个被乘值 + * @return 积 + */ + public static BigDecimal mul(BigDecimal... values) { + for (BigDecimal value : values) { + if (value == null) { + return null; + } + } + return NumberUtil.mul(values); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java new file mode 100644 index 0000000..00ef7db --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.framework.common.util.object; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Bean 工具类 + * + * 1. 默认使用 {@link cn.hutool.core.bean.BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能 + * 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现 + * + * @author 芋道源码 + */ +public class BeanUtils { + + public static T toBean(Object source, Class targetClass) { + return BeanUtil.toBean(source, targetClass); + } + + public static T toBean(Object source, Class targetClass, Consumer peek) { + T target = toBean(source, targetClass); + if (target != null) { + peek.accept(target); + } + return target; + } + + public static List toBean(List source, Class targetType) { + if (source == null) { + return null; + } + return CollectionUtils.convertList(source, s -> toBean(s, targetType)); + } + + public static List toBean(List source, Class targetType, Consumer peek) { + List list = toBean(source, targetType); + if (list != null) { + list.forEach(peek); + } + return list; + } + + public static PageResult toBean(PageResult source, Class targetType) { + return toBean(source, targetType, null); + } + + public static PageResult toBean(PageResult source, Class targetType, Consumer peek) { + if (source == null) { + return null; + } + List list = toBean(source.getList(), targetType); + if (peek != null) { + list.forEach(peek); + } + return new PageResult<>(list, source.getTotal()); + } + + public static void copyProperties(Object source, Object target) { + if (source == null || target == null) { + return; + } + BeanUtil.copyProperties(source, target, false); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java new file mode 100644 index 0000000..c08316d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.common.util.object; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.function.Consumer; + +/** + * Object 工具类 + * + * @author 芋道源码 + */ +public class ObjectUtils { + + /** + * 复制对象,并忽略 Id 编号 + * + * @param object 被复制对象 + * @param consumer 消费者,可以二次编辑被复制对象 + * @return 复制后的对象 + */ + public static T cloneIgnoreId(T object, Consumer consumer) { + T result = ObjectUtil.clone(object); + // 忽略 id 编号 + Field field = ReflectUtil.getField(object.getClass(), "id"); + if (field != null) { + ReflectUtil.setFieldValue(result, field, null); + } + // 二次编辑 + if (result != null) { + consumer.accept(result); + } + return result; + } + + public static > T max(T obj1, T obj2) { + if (obj1 == null) { + return obj2; + } + if (obj2 == null) { + return obj1; + } + return obj1.compareTo(obj2) > 0 ? obj1 : obj2; + } + + @SafeVarargs + public static T defaultIfNull(T... array) { + for (T item : array) { + if (item != null) { + return item; + } + } + return null; + } + + @SafeVarargs + public static boolean equalsAny(T obj, T... array) { + return Arrays.asList(array).contains(obj); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java new file mode 100644 index 0000000..cf077db --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.framework.common.util.object; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.lang.func.LambdaUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam; +import cn.iocoder.yudao.framework.common.pojo.SortingField; +import org.springframework.util.Assert; + +import static java.util.Collections.singletonList; + +/** + * {@link cn.iocoder.yudao.framework.common.pojo.PageParam} 工具类 + * + * @author 芋道源码 + */ +public class PageUtils { + + private static final Object[] ORDER_TYPES = new String[]{SortingField.ORDER_ASC, SortingField.ORDER_DESC}; + + public static int getStart(PageParam pageParam) { + return (pageParam.getPageNo() - 1) * pageParam.getPageSize(); + } + + /** + * 构建排序字段(默认倒序) + * + * @param func 排序字段的 Lambda 表达式 + * @param 排序字段所属的类型 + * @return 排序字段 + */ + public static SortingField buildSortingField(Func1 func) { + return buildSortingField(func, SortingField.ORDER_DESC); + } + + /** + * 构建排序字段 + * + * @param func 排序字段的 Lambda 表达式 + * @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC} + * @param 排序字段所属的类型 + * @return 排序字段 + */ + public static SortingField buildSortingField(Func1 func, String order) { + Assert.isTrue(ArrayUtil.contains(ORDER_TYPES, order), String.format("字段的排序类型只能是 %s/%s", ORDER_TYPES)); + + String fieldName = LambdaUtil.getFieldName(func); + return new SortingField(fieldName, order); + } + + /** + * 构建默认的排序字段 + * 如果排序字段为空,则设置排序字段;否则忽略 + * + * @param sortablePageParam 排序分页查询参数 + * @param func 排序字段的 Lambda 表达式 + * @param 排序字段所属的类型 + */ + public static void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1 func) { + if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) { + sortablePageParam.setSortingFields(singletonList(buildSortingField(func))); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java new file mode 100644 index 0000000..cd8ae6b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java @@ -0,0 +1,7 @@ +/** + * 对于工具类的选择,优先查找 Hutool 中有没对应的方法 + * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分 + * + * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。 + */ +package cn.iocoder.yudao.framework.common.util; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java new file mode 100644 index 0000000..905f5d5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.framework.common.util.servlet; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.JakartaServletUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.Map; + +/** + * 客户端工具类 + * + * @author 芋道源码 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JsonUtils.toJsonString(object); + JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + /** + * @param request 请求 + * @return ua + */ + public static String getUserAgent(HttpServletRequest request) { + String ua = request.getHeader("User-Agent"); + return ua != null ? ua : ""; + } + + /** + * 获得请求 + * + * @return HttpServletRequest + */ + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + return ((ServletRequestAttributes) requestAttributes).getRequest(); + } + + public static String getUserAgent() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return getUserAgent(request); + } + + public static String getClientIP() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return JakartaServletUtil.getClientIP(request); + } + + public static boolean isJsonRequest(ServletRequest request) { + return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); + } + + public static String getBody(HttpServletRequest request) { + // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取 + if (isJsonRequest(request)) { + return JakartaServletUtil.getBody(request); + } + return null; + } + + public static byte[] getBodyBytes(HttpServletRequest request) { + // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取 + if (isJsonRequest(request)) { + return JakartaServletUtil.getBodyBytes(request); + } + return null; + } + + public static String getClientIP(HttpServletRequest request) { + return JakartaServletUtil.getClientIP(request); + } + + public static Map getParamMap(HttpServletRequest request) { + return JakartaServletUtil.getParamMap(request); + } + + public static Map getHeaderMap(HttpServletRequest request) { + return JakartaServletUtil.getHeaderMap(request); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java new file mode 100644 index 0000000..abb1cec --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java @@ -0,0 +1,123 @@ +package cn.iocoder.yudao.framework.common.util.spring; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Spring EL 表达式的工具类 + * + * @author mashu + */ +public class SpringExpressionUtils { + + /** + * Spring EL 表达式解析器 + */ + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + /** + * 参数名发现器 + */ + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + private SpringExpressionUtils() { + } + + /** + * 从切面中,单个解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionString EL 表达式数组 + * @return 执行界面 + */ + public static Object parseExpression(JoinPoint joinPoint, String expressionString) { + Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); + return result.get(expressionString); + } + + /** + * 从切面中,批量解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionStrings EL 表达式数组 + * @return 结果,key 为表达式,value 为对应值 + */ + public static Map parseExpressions(JoinPoint joinPoint, List expressionStrings) { + // 如果为空,则不进行解析 + if (CollUtil.isEmpty(expressionStrings)) { + return MapUtil.newHashMap(); + } + + // 第一步,构建解析的上下文 EvaluationContext + // 通过 joinPoint 获取被注解方法 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + Method method = methodSignature.getMethod(); + // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组 + String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + // Spring 的表达式上下文对象 + EvaluationContext context = new StandardEvaluationContext(); + // 给上下文赋值 + if (ArrayUtil.isNotEmpty(paramNames)) { + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < paramNames.length; i++) { + context.setVariable(paramNames[i], args[i]); + } + } + + // 第二步,逐个参数解析 + Map result = MapUtil.newHashMap(expressionStrings.size(), true); + expressionStrings.forEach(key -> { + Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); + result.put(key, value); + }); + return result; + } + + /** + * 从 Bean 工厂,解析 EL 表达式的结果 + * + * @param expressionString EL 表达式 + * @return 执行界面 + */ + public static Object parseExpression(String expressionString) { + return parseExpression(expressionString, null); + } + + /** + * 从 Bean 工厂,解析 EL 表达式的结果 + * + * @param expressionString EL 表达式 + * @param variables 变量 + * @return 执行界面 + */ + public static Object parseExpression(String expressionString, Map variables) { + if (StrUtil.isBlank(expressionString)) { + return null; + } + Expression expression = EXPRESSION_PARSER.parseExpression(expressionString); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext())); + if (MapUtil.isNotEmpty(variables)) { + context.setVariables(variables); + } + return expression.getValue(context); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringUtils.java new file mode 100644 index 0000000..a501a71 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringUtils.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.framework.common.util.spring; + +import cn.hutool.extra.spring.SpringUtil; + +import java.util.Objects; + +/** + * Spring 工具类 + * + * @author 芋道源码 + */ +public class SpringUtils extends SpringUtil { + + /** + * 是否为生产环境 + * + * @return 是否生产环境 + */ + public static boolean isProd() { + String activeProfile = getActiveProfile(); + return Objects.equals("prod", activeProfile); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java new file mode 100644 index 0000000..47a53a5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.framework.common.util.string; + +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import org.aspectj.lang.JoinPoint; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 字符串工具类 + * + * @author 芋道源码 + */ +public class StrUtils { + + public static String maxLength(CharSequence str, int maxLength) { + return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 + } + + /** + * 给定字符串是否以任何一个字符串开始 + * 给定字符串和数组为空都返回 false + * + * @param str 给定字符串 + * @param prefixes 需要检测的开始字符串 + * @since 3.0.6 + */ + public static boolean startWithAny(String str, Collection prefixes) { + if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { + return false; + } + + for (CharSequence suffix : prefixes) { + if (StrUtil.startWith(str, suffix, false)) { + return true; + } + } + return false; + } + + public static List splitToLong(String value, CharSequence separator) { + long[] longs = StrUtil.splitToLong(value, separator); + return Arrays.stream(longs).boxed().collect(Collectors.toList()); + } + + public static Set splitToLongSet(String value) { + return splitToLongSet(value, StrPool.COMMA); + } + + public static Set splitToLongSet(String value, CharSequence separator) { + long[] longs = StrUtil.splitToLong(value, separator); + return Arrays.stream(longs).boxed().collect(Collectors.toSet()); + } + + public static List splitToInteger(String value, CharSequence separator) { + int[] integers = StrUtil.splitToInt(value, separator); + return Arrays.stream(integers).boxed().collect(Collectors.toList()); + } + + /** + * 移除字符串中,包含指定字符串的行 + * + * @param content 字符串 + * @param sequence 包含的字符串 + * @return 移除后的字符串 + */ + public static String removeLineContains(String content, String sequence) { + if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) { + return content; + } + return Arrays.stream(content.split("\n")) + .filter(line -> !line.contains(sequence)) + .collect(Collectors.joining("\n")); + } + + /** + * 拼接方法的参数 + * + * 特殊:排除一些无法序列化的参数,如 ServletRequest、ServletResponse、MultipartFile + * + * @param joinPoint 连接点 + * @return 拼接后的参数 + */ + public static String joinMethodArgs(JoinPoint joinPoint) { + Object[] args = joinPoint.getArgs(); + if (ArrayUtil.isEmpty(args)) { + return ""; + } + return ArrayUtil.join(args, ",", item -> { + if (item == null) { + return ""; + } + // 讨论可见:https://t.zsxq.com/XUJVk、https://t.zsxq.com/MnKcL + String clazzName = item.getClass().getName(); + if (StrUtil.startWithAny(clazzName, "javax.servlet", "jakarta.servlet", "org.springframework.web")) { + return ""; + } + return item; + }); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java new file mode 100644 index 0000000..a71ebd3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.common.util.validation; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import org.springframework.util.StringUtils; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * 校验工具类 + * + * @author 芋道源码 + */ +public class ValidationUtils { + + private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$"); + + private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); + + private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*"); + + public static boolean isMobile(String mobile) { + return StringUtils.hasText(mobile) + && PATTERN_MOBILE.matcher(mobile).matches(); + } + + public static boolean isURL(String url) { + return StringUtils.hasText(url) + && PATTERN_URL.matcher(url).matches(); + } + + public static boolean isXmlNCName(String str) { + return StringUtils.hasText(str) + && PATTERN_XML_NCNAME.matcher(str).matches(); + } + + public static void validate(Object object, Class... groups) { + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Assert.notNull(validator); + validate(validator, object, groups); + } + + public static void validate(Validator validator, Object object, Class... groups) { + Set> constraintViolations = validator.validate(object, groups); + if (CollUtil.isNotEmpty(constraintViolations)) { + throw new ConstraintViolationException(constraintViolations); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java new file mode 100644 index 0000000..407d01b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.common.validation; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Target({ + ElementType.METHOD, + ElementType.FIELD, + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.PARAMETER, + ElementType.TYPE_USE +}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint( + validatedBy = {InEnumValidator.class, InEnumCollectionValidator.class} +) +public @interface InEnum { + + /** + * @return 实现 ArrayValuable 接口的类 + */ + Class> value(); + + String message() default "必须在指定范围 {value}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumCollectionValidator.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumCollectionValidator.java new file mode 100644 index 0000000..b21a929 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumCollectionValidator.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.common.validation; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class InEnumCollectionValidator implements ConstraintValidator> { + + private List values; + + @Override + public void initialize(InEnum annotation) { + ArrayValuable[] values = annotation.value().getEnumConstants(); + if (values.length == 0) { + this.values = Collections.emptyList(); + } else { + this.values = Arrays.asList(values[0].array()); + } + } + + @Override + public boolean isValid(Collection list, ConstraintValidatorContext context) { + if (list == null) { + return true; + } + // 校验通过 + if (CollUtil.containsAll(values, list)) { + return true; + } + // 校验不通过,自定义提示语句 + context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() + .replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句 + return false; + } + +} + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java new file mode 100644 index 0000000..e80eac4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.common.validation; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class InEnumValidator implements ConstraintValidator { + + private List values; + + @Override + public void initialize(InEnum annotation) { + ArrayValuable[] values = annotation.value().getEnumConstants(); + if (values.length == 0) { + this.values = Collections.emptyList(); + } else { + this.values = Arrays.asList(values[0].array()); + } + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + // 为空时,默认不校验,即认为通过 + if (value == null) { + return true; + } + // 校验通过 + if (values.contains(value)) { + return true; + } + // 校验不通过,自定义提示语句 + context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() + .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句 + return false; + } + +} + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java new file mode 100644 index 0000000..bcb75f6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.common.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.*; + +@Target({ + ElementType.METHOD, + ElementType.FIELD, + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.PARAMETER, + ElementType.TYPE_USE +}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint( + validatedBy = MobileValidator.class +) +public @interface Mobile { + + String message() default "手机号格式不正确"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java new file mode 100644 index 0000000..f2def56 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.common.validation; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class MobileValidator implements ConstraintValidator { + + @Override + public void initialize(Mobile annotation) { + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // 如果手机号为空,默认不校验,即校验通过 + if (StrUtil.isEmpty(value)) { + return true; + } + // 校验手机 + return ValidationUtils.isMobile(value); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java new file mode 100644 index 0000000..fa534e1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Telephone.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.common.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.*; + +@Target({ + ElementType.METHOD, + ElementType.FIELD, + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.PARAMETER, + ElementType.TYPE_USE +}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint( + validatedBy = TelephoneValidator.class +) +public @interface Telephone { + + String message() default "电话格式不正确"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java new file mode 100644 index 0000000..1406e07 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/TelephoneValidator.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.common.validation; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.PhoneUtil; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class TelephoneValidator implements ConstraintValidator { + + @Override + public void initialize(Telephone annotation) { + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // 如果手机号为空,默认不校验,即校验通过 + if (CharSequenceUtil.isEmpty(value)) { + return true; + } + // 校验手机 + return PhoneUtil.isTel(value) || PhoneUtil.isPhone(value); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java new file mode 100644 index 0000000..aa95c69 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Hibernate Validator 实现参数校验 + */ +package cn.iocoder.yudao.framework.common.validation; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtilsTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtilsTest.java new file mode 100644 index 0000000..0e44645 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtilsTest.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.framework.common.util.collection; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.BiFunction; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link CollectionUtils} 的单元测试 + */ +public class CollectionUtilsTest { + + @Data + @AllArgsConstructor + private static class Dog { + + private Integer id; + private String name; + private String code; + + } + + @Test + public void testDiffList() { + // 准备参数 + Collection oldList = Arrays.asList( + new Dog(1, "花花", "hh"), + new Dog(2, "旺财", "wc") + ); + Collection newList = Arrays.asList( + new Dog(null, "花花2", "hh"), + new Dog(null, "小白", "xb") + ); + BiFunction sameFunc = (oldObj, newObj) -> { + boolean same = oldObj.getCode().equals(newObj.getCode()); + // 如果相等的情况下,需要设置下 id,后续好更新 + if (same) { + newObj.setId(oldObj.getId()); + } + return same; + }; + + // 调用 + List> result = CollectionUtils.diffList(oldList, newList, sameFunc); + // 断言 + assertEquals(result.size(), 3); + // 断言 create + assertEquals(result.get(0).size(), 1); + assertEquals(result.get(0).get(0), new Dog(null, "小白", "xb")); + // 断言 update + assertEquals(result.get(1).size(), 1); + assertEquals(result.get(1).get(0), new Dog(1, "花花2", "hh")); + // 断言 delete + assertEquals(result.get(2).size(), 1); + assertEquals(result.get(2).get(0), new Dog(2, "旺财", "wc")); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md new file mode 100644 index 0000000..b2c2cf8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml new file mode 100644 index 0000000..802ff19 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml @@ -0,0 +1,45 @@ + + + + yudao-framework + cn.iocoder.boot + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-data-permission + jar + + ${project.artifactId} + 数据权限 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + true + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java new file mode 100644 index 0000000..519804c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.datapermission.config; + +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; +import cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionRuleHandler; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +/** + * 数据权限的自动配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class YudaoDataPermissionAutoConfiguration { + + @Bean + public DataPermissionRuleFactory dataPermissionRuleFactory(List rules) { + return new DataPermissionRuleFactoryImpl(rules); + } + + @Bean + public DataPermissionRuleHandler dataPermissionRuleHandler(MybatisPlusInterceptor interceptor, + DataPermissionRuleFactory ruleFactory) { + // 创建 DataPermissionInterceptor 拦截器 + DataPermissionRuleHandler handler = new DataPermissionRuleHandler(ruleFactory); + DataPermissionInterceptor inner = new DataPermissionInterceptor(handler); + // 添加到 interceptor 中 + // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 + MyBatisUtils.addInterceptor(interceptor, inner, 0); + return handler; + } + + @Bean + public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { + return new DataPermissionAnnotationAdvisor(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java new file mode 100644 index 0000000..ab5b3bc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.datapermission.config; + +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +/** + * 基于部门的数据权限 AutoConfiguration + * + * @author 芋道源码 + */ +@AutoConfiguration +@ConditionalOnClass(LoginUser.class) +@ConditionalOnBean(value = {PermissionCommonApi.class, DeptDataPermissionRuleCustomizer.class}) +public class YudaoDeptDataPermissionAutoConfiguration { + + @Bean + public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionApi, + List customizers) { + // 创建 DeptDataPermissionRule 对象 + DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); + // 补全表配置 + customizers.forEach(customizer -> customizer.customize(rule)); + return rule; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java new file mode 100644 index 0000000..4e8a921 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.datapermission.core.annotation; + +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; + +import java.lang.annotation.*; + +/** + * 数据权限注解 + * 可声明在类或者方法上,标识使用的数据权限规则 + * + * @author 芋道源码 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataPermission { + + /** + * 当前类或方法是否开启数据权限 + * 即使不添加 @DataPermission 注解,默认是开启状态 + * 可通过设置 enable 为 false 禁用 + */ + boolean enable() default true; + + /** + * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()} + */ + Class[] includeRules() default {}; + + /** + * 排除的数据权限规则数组,优先级最低 + */ + Class[] excludeRules() default {}; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java new file mode 100644 index 0000000..03d212c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.aopalliance.aop.Advice; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; + +/** + * {@link cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类 + * + * @author 芋道源码 + */ +@Getter +@EqualsAndHashCode(callSuper = true) +public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor { + + private final Advice advice; + + private final Pointcut pointcut; + + public DataPermissionAnnotationAdvisor() { + this.advice = new DataPermissionAnnotationInterceptor(); + this.pointcut = this.buildPointcut(); + } + + protected Pointcut buildPointcut() { + Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true); + Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); + return new ComposablePointcut(classPointcut).union(methodPointcut); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java new file mode 100644 index 0000000..48a0354 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import lombok.Getter; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.MethodClassKey; +import org.springframework.core.annotation.AnnotationUtils; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * {@link DataPermission} 注解的拦截器 + * 1. 在执行方法前,将 @DataPermission 注解入栈 + * 2. 在执行方法后,将 @DataPermission 注解出栈 + * + * @author 芋道源码 + */ +@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象 +public class DataPermissionAnnotationInterceptor implements MethodInterceptor { + + /** + * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位 + */ + static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class); + + @Getter + private final Map dataPermissionCache = new ConcurrentHashMap<>(); + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + // 入栈 + DataPermission dataPermission = this.findAnnotation(methodInvocation); + if (dataPermission != null) { + DataPermissionContextHolder.add(dataPermission); + } + try { + // 执行逻辑 + return methodInvocation.proceed(); + } finally { + // 出栈 + if (dataPermission != null) { + DataPermissionContextHolder.remove(); + } + } + } + + private DataPermission findAnnotation(MethodInvocation methodInvocation) { + // 1. 从缓存中获取 + Method method = methodInvocation.getMethod(); + Object targetObject = methodInvocation.getThis(); + Class clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass(); + MethodClassKey methodClassKey = new MethodClassKey(method, clazz); + DataPermission dataPermission = dataPermissionCache.get(methodClassKey); + if (dataPermission != null) { + return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null; + } + + // 2.1 从方法中获取 + dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class); + // 2.2 从类上获取 + if (dataPermission == null) { + dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class); + } + // 2.3 添加到缓存中 + dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL); + return dataPermission; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java new file mode 100644 index 0000000..9705d88 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import com.alibaba.ttl.TransmittableThreadLocal; + +import java.util.LinkedList; +import java.util.List; + +/** + * {@link DataPermission} 注解的 Context 上下文 + * + * @author 芋道源码 + */ +public class DataPermissionContextHolder { + + /** + * 使用 List 的原因,可能存在方法的嵌套调用 + */ + private static final ThreadLocal> DATA_PERMISSIONS = + TransmittableThreadLocal.withInitial(LinkedList::new); + + /** + * 获得当前的 DataPermission 注解 + * + * @return DataPermission 注解 + */ + public static DataPermission get() { + return DATA_PERMISSIONS.get().peekLast(); + } + + /** + * 入栈 DataPermission 注解 + * + * @param dataPermission DataPermission 注解 + */ + public static void add(DataPermission dataPermission) { + DATA_PERMISSIONS.get().addLast(dataPermission); + } + + /** + * 出栈 DataPermission 注解 + * + * @return DataPermission 注解 + */ + public static DataPermission remove() { + DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast(); + // 无元素时,清空 ThreadLocal + if (DATA_PERMISSIONS.get().isEmpty()) { + DATA_PERMISSIONS.remove(); + } + return dataPermission; + } + + /** + * 获得所有 DataPermission + * + * @return DataPermission 队列 + */ + public static List getAll() { + return DATA_PERMISSIONS.get(); + } + + /** + * 清空上下文 + * + * 目前仅仅用于单测 + */ + public static void clear() { + DATA_PERMISSIONS.remove(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java new file mode 100644 index 0000000..477bfd9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.framework.datapermission.core.db; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler; +import lombok.RequiredArgsConstructor; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.schema.Table; + +import java.util.List; + +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck; + +/** + * 基于 {@link DataPermissionRule} 的数据权限处理器 + * + * 它的底层,是基于 MyBatis Plus 的 数据权限插件 + * 核心原理:它会在 SQL 执行前拦截 SQL 语句,并根据用户权限动态添加权限相关的 SQL 片段。这样,只有用户有权限访问的数据才会被查询出来 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class DataPermissionRuleHandler implements MultiDataPermissionHandler { + + private final DataPermissionRuleFactory ruleFactory; + + @Override + public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return null; + } + + // 获得 Mapper 对应的数据权限的规则 + List rules = ruleFactory.getDataPermissionRule(mappedStatementId); + if (CollUtil.isEmpty(rules)) { + return null; + } + + // 生成条件 + Expression allExpression = null; + for (DataPermissionRule rule : rules) { + // 判断表名是否匹配 + String tableName = MyBatisUtils.getTableName(table); + if (!rule.getTableNames().contains(tableName)) { + continue; + } + + // 单条规则的条件 + Expression oneExpress = rule.getExpression(tableName, table.getAlias()); + if (oneExpress == null) { + continue; + } + // 拼接到 allExpression 中 + allExpression = allExpression == null ? oneExpress + : new AndExpression(allExpression, oneExpress); + } + return allExpression; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java new file mode 100644 index 0000000..2bccde8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; + +import java.util.Set; + +/** + * 数据权限规则接口 + * 通过实现接口,自定义数据规则。例如说, + * + * @author 芋道源码 + */ +public interface DataPermissionRule { + + /** + * 返回需要生效的表名数组 + * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 + * + * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 + * + * @return 表名数组 + */ + Set getTableNames(); + + /** + * 根据表名和别名,生成对应的 WHERE / OR 过滤条件 + * + * @param tableName 表名 + * @param tableAlias 别名,可能为空 + * @return 过滤条件 Expression 表达式 + */ + Expression getExpression(String tableName, Alias tableAlias); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java new file mode 100644 index 0000000..166dfea --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import java.util.List; + +/** + * {@link DataPermissionRule} 工厂接口 + * 作为 {@link DataPermissionRule} 的容器,提供管理能力 + * + * @author 芋道源码 + */ +public interface DataPermissionRuleFactory { + + /** + * 获得所有数据权限规则数组 + * + * @return 数据权限规则数组 + */ + List getDataPermissionRules(); + + /** + * 获得指定 Mapper 的数据权限规则数组 + * + * @param mappedStatementId 指定 Mapper 的编号 + * @return 数据权限规则数组 + */ + List getDataPermissionRule(String mappedStatementId); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java new file mode 100644 index 0000000..eaa6e6a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 默认的 DataPermissionRuleFactoryImpl 实现类 + * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory { + + /** + * 数据权限规则数组 + */ + private final List rules; + + @Override + public List getDataPermissionRules() { + return rules; + } + + @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存 + public List getDataPermissionRule(String mappedStatementId) { + // 1. 无数据权限 + if (CollUtil.isEmpty(rules)) { + return Collections.emptyList(); + } + // 2. 未配置,则默认开启 + DataPermission dataPermission = DataPermissionContextHolder.get(); + if (dataPermission == null) { + return rules; + } + // 3. 已配置,但禁用 + if (!dataPermission.enable()) { + return Collections.emptyList(); + } + + // 4. 已配置,只选择部分规则 + if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) { + return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass())) + .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 + } + // 5. 已配置,只排除部分规则 + if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) { + return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass())) + .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 + } + // 6. 已配置,全部规则 + return rules; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java new file mode 100644 index 0000000..56b6ea4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java @@ -0,0 +1,210 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi; +import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 + * + * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 + * + * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? + * 1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【yudao-server 采用该方案】 + * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 + * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 + * 最终过滤条件是 WHERE dept_id = ? + * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; + * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) + * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; + * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Slf4j +public class DeptDataPermissionRule implements DataPermissionRule { + + /** + * LoginUser 的 Context 缓存 Key + */ + protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName(); + + private static final String DEPT_COLUMN_NAME = "dept_id"; + private static final String USER_COLUMN_NAME = "user_id"; + + static final Expression EXPRESSION_NULL = new NullValue(); + + private final PermissionCommonApi permissionApi; + + /** + * 基于部门的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map deptColumns = new HashMap<>(); + /** + * 基于用户的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map userColumns = new HashMap<>(); + /** + * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 + */ + private final Set TABLE_NAMES = new HashSet<>(); + + @Override + public Set getTableNames() { + return TABLE_NAMES; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + // 只有有登陆用户的情况下,才进行数据权限的处理 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return null; + } + // 只有管理员类型的用户,才进行数据权限的处理 + if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) { + return null; + } + + // 获得数据权限 + DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); + // 从上下文中拿不到,则调用逻辑进行获取 + if (deptDataPermission == null) { + deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId()); + if (deptDataPermission == null) { + log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); + throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", + loginUser.getId(), tableName, tableAlias.getName())); + } + // 添加到上下文中,避免重复计算 + loginUser.setContext(CONTEXT_KEY, deptDataPermission); + } + + // 情况一,如果是 ALL 可查看全部,则无需拼接条件 + if (deptDataPermission.getAll()) { + return null; + } + + // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限 + if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) + && Boolean.FALSE.equals(deptDataPermission.getSelf())) { + return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 + } + + // 情况三,拼接 Dept 和 User 的条件,最后组合 + Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); + Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); + if (deptExpression == null && userExpression == null) { + // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据 + log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", + JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); +// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空", +// loginUser.getId(), tableName, tableAlias.getName())); + return EXPRESSION_NULL; + } + if (deptExpression == null) { + return userExpression; + } + if (userExpression == null) { + return deptExpression; + } + // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?) + return new ParenthesedExpressionList(new OrExpression(deptExpression, userExpression)); + } + + private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) { + // 如果不存在配置,则无需作为条件 + String columnName = deptColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 如果为空,则无条件 + if (CollUtil.isEmpty(deptIds)) { + return null; + } + // 拼接条件 + return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), + // Parenthesis 的目的,是提供 (1,2,3) 的 () 左右括号 + new ParenthesedExpressionList(new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new)))); + } + + private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { + // 如果不查看自己,则无需作为条件 + if (Boolean.FALSE.equals(self)) { + return null; + } + String columnName = userColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 拼接条件 + return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); + } + + // ==================== 添加配置 ==================== + + public void addDeptColumn(Class entityClass) { + addDeptColumn(entityClass, DEPT_COLUMN_NAME); + } + + public void addDeptColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addDeptColumn(tableName, columnName); + } + + public void addDeptColumn(String tableName, String columnName) { + deptColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + + public void addUserColumn(Class entityClass) { + addUserColumn(entityClass, USER_COLUMN_NAME); + } + + public void addUserColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addUserColumn(tableName, columnName); + } + + public void addUserColumn(String tableName, String columnName) { + userColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java new file mode 100644 index 0000000..e5e4f57 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; + +/** + * {@link DeptDataPermissionRule} 的自定义配置接口 + * + * @author 芋道源码 + */ +@FunctionalInterface +public interface DeptDataPermissionRuleCustomizer { + + /** + * 自定义该权限规则 + * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 + * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 + * + * @param rule 权限规则 + */ + void customize(DeptDataPermissionRule rule); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java new file mode 100644 index 0000000..66e9326 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于部门的数据权限规则 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java new file mode 100644 index 0000000..eb7b223 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.framework.datapermission.core.util; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; +import lombok.SneakyThrows; + +import java.util.concurrent.Callable; + +/** + * 数据权限 Util + * + * @author 芋道源码 + */ +public class DataPermissionUtils { + + private static DataPermission DATA_PERMISSION_DISABLE; + + @DataPermission(enable = false) + @SneakyThrows + private static DataPermission getDisableDataPermissionDisable() { + if (DATA_PERMISSION_DISABLE == null) { + DATA_PERMISSION_DISABLE = DataPermissionUtils.class + .getDeclaredMethod("getDisableDataPermissionDisable") + .getAnnotation(DataPermission.class); + } + return DATA_PERMISSION_DISABLE; + } + + /** + * 忽略数据权限,执行对应的逻辑 + * + * @param runnable 逻辑 + */ + public static void executeIgnore(Runnable runnable) { + addDisableDataPermission(); + try { + // 执行 runnable + runnable.run(); + } finally { + removeDataPermission(); + } + } + + /** + * 忽略数据权限,执行对应的逻辑 + * + * @param callable 逻辑 + * @return 执行结果 + */ + @SneakyThrows + public static T executeIgnore(Callable callable) { + addDisableDataPermission(); + try { + // 执行 callable + return callable.call(); + } finally { + removeDataPermission(); + } + } + + /** + * 添加忽略数据权限 + */ + public static void addDisableDataPermission(){ + DataPermission dataPermission = getDisableDataPermissionDisable(); + DataPermissionContextHolder.add(dataPermission); + } + + public static void removeDataPermission(){ + DataPermissionContextHolder.remove(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java new file mode 100644 index 0000000..831aa7c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 + */ +package cn.iocoder.yudao.framework.datapermission; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..c5d0cd3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration +cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java new file mode 100644 index 0000000..ba97ede --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +/** + * {@link DataPermissionAnnotationInterceptor} 的单元测试 + * + * @author 芋道源码 + */ +public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionAnnotationInterceptor interceptor; + + @Mock + private MethodInvocation methodInvocation; + + @BeforeEach + public void setUp() { + interceptor.getDataPermissionCache().clear(); + } + + @Test // 无 @DataPermission 注解 + public void testInvoke_none() throws Throwable { + // 参数 + mockMethodInvocation(TestNone.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("none", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + @Test // 在 Method 上有 @DataPermission 注解 + public void testInvoke_method() throws Throwable { + // 参数 + mockMethodInvocation(TestMethod.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("method", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + @Test // 在 Class 上有 @DataPermission 注解 + public void testInvoke_class() throws Throwable { + // 参数 + mockMethodInvocation(TestClass.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("class", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + private void mockMethodInvocation(Class clazz) throws Throwable { + Object targetObject = clazz.newInstance(); + Method method = targetObject.getClass().getMethod("echo"); + when(methodInvocation.getThis()).thenReturn(targetObject); + when(methodInvocation.getMethod()).thenReturn(method); + when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject)); + } + + static class TestMethod { + + @DataPermission(enable = false) + public String echo() { + return "method"; + } + + } + + @DataPermission(enable = false) + static class TestClass { + + public String echo() { + return "class"; + } + + } + + static class TestNone { + + public String echo() { + return "none"; + } + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java new file mode 100644 index 0000000..688b92d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; + +/** + * {@link DataPermissionContextHolder} 的单元测试 + * + * @author 芋道源码 + */ +class DataPermissionContextHolderTest { + + @BeforeEach + public void setUp() { + DataPermissionContextHolder.clear(); + } + + @Test + public void testGet() { + // mock 方法 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + + // 调用 + DataPermission result = DataPermissionContextHolder.get(); + // 断言 + assertSame(result, dataPermission02); + } + + @Test + public void testPush() { + // 调用 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + // 断言 + DataPermission first = DataPermissionContextHolder.getAll().get(0); + DataPermission second = DataPermissionContextHolder.getAll().get(1); + assertSame(dataPermission01, first); + assertSame(dataPermission02, second); + } + + @Test + public void testRemove() { + // mock 方法 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + + // 调用 + DataPermission result = DataPermissionContextHolder.remove(); + // 断言 + assertSame(result, dataPermission02); + assertEquals(1, DataPermissionContextHolder.getAll().size()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandlerTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandlerTest.java new file mode 100644 index 0000000..c7a0d08 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandlerTest.java @@ -0,0 +1,540 @@ +package cn.iocoder.yudao.framework.datapermission.core.db; + +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.schema.Column; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Arrays; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * {@link DataPermissionRuleHandler} 的单元测试 + * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试 + * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~ + * + * @author 芋道源码 + */ +public class DataPermissionRuleHandlerTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionRuleHandler handler; + + @Mock + private DataPermissionRuleFactory ruleFactory; + + private DataPermissionInterceptor interceptor; + + @BeforeEach + public void setUp() { + interceptor = new DataPermissionInterceptor(handler); + + // 租户的数据权限规则 + DataPermissionRule tenantRule = new DataPermissionRule() { + + private static final String COLUMN = "tenant_id"; + + @Override + public Set getTableNames() { + return asSet("entity", "entity1", "entity2", "entity3", "t1", "t2", "sys_dict_item", // 支持 MyBatis Plus 的单元测试 + "t_user", "t_role"); // 满足自己的单元测试 + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + LongValue value = new LongValue(1L); + return new EqualsTo(column, value); + } + + }; + // 部门的数据权限规则 + DataPermissionRule deptRule = new DataPermissionRule() { + + private static final String COLUMN = "dept_id"; + + @Override + public Set getTableNames() { + return asSet("t_user"); // 满足自己的单元测试 + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + ExpressionList values = new ExpressionList<>(new LongValue(10L), + new LongValue(20L)); + return new InExpression(column, new ParenthesedExpressionList((values))); + } + + }; + // 设置到上下文 + when(ruleFactory.getDataPermissionRule(any())).thenReturn(Arrays.asList(tenantRule, deptRule)); + } + + @Test + void delete() { + assertSql("delete from entity where id = ?", + "DELETE FROM entity WHERE id = ? AND entity.tenant_id = 1"); + } + + @Test + void update() { + assertSql("update entity set name = ? where id = ?", + "UPDATE entity SET name = ? WHERE id = ? AND entity.tenant_id = 1"); + } + + @Test + void selectSingle() { + // 单表 + assertSql("select * from entity where id = ?", + "SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1"); + + assertSql("select * from entity where id = ? or name = ?", + "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1"); + + assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)", + "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1"); + + /* not */ + assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)", + "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND entity.tenant_id = 1"); + } + + @Test + void selectSubSelectIn() { + /* in */ + assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + // 在最前 + assertSql("SELECT * FROM entity e WHERE e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", + "SELECT * FROM entity e WHERE e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); + // 在最后 + assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + // 在中间 + assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", + "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectEq() { + /* = */ + assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectInnerNotEq() { + /* inner not = */ + assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))", + "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)", + "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectExists() { + /* EXISTS */ + assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + + /* NOT EXISTS */ + assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelect() { + /* >= */ + assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + + /* <= */ + assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + + /* <> */ + assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectFromSelect() { + assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))", + "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)"); + } + + @Test + void selectBodySubSelect() { + assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1", + "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1"); + } + + @Test + void selectLeftJoin() { + // left join + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "left join entity2 e2 on e1.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " + + "WHERE e.tenant_id = 1"); + } + + @Test + void selectRightJoin() { + // right join + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + + "WHERE e1.tenant_id = 1"); + + assertSql("SELECT * FROM with_as_1 e " + + "right join entity1 e1 on e1.id = e.id", + "SELECT * FROM with_as_1 e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id " + + "WHERE e1.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id " + + "right join entity2 e2 on e1.id = e2.id ", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + + "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " + + "WHERE e2.tenant_id = 1"); + } + + @Test + void selectMixJoin() { + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id " + + "left join entity2 e2 on e1.id = e2.id", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + + "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " + + "WHERE e1.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "right join entity2 e2 on e1.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 " + + "WHERE e2.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "inner join entity2 e2 on e1.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "INNER JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 AND e2.tenant_id = 1"); + } + + + @Test + void selectJoinSubSelect() { + assertSql("select * from (select * from entity) e1 " + + "left join entity2 e2 on e1.id = e2.id", + "SELECT * FROM (SELECT * FROM entity WHERE entity.tenant_id = 1) e1 " + + "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1"); + + assertSql("select * from entity1 e1 " + + "left join (select * from entity2) e2 " + + "on e1.id = e2.id", + "SELECT * FROM entity1 e1 " + + "LEFT JOIN (SELECT * FROM entity2 WHERE entity2.tenant_id = 1) e2 " + + "ON e1.id = e2.id " + + "WHERE e1.tenant_id = 1"); + } + + @Test + void selectSubJoin() { + + assertSql("select * FROM " + + "(entity1 e1 right JOIN entity2 e2 ON e1.id = e2.id)", + "SELECT * FROM " + + "(entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " + + "WHERE e2.tenant_id = 1"); + + assertSql("select * FROM " + + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id)", + "SELECT * FROM " + + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + + "WHERE e1.tenant_id = 1"); + + + assertSql("select * FROM " + + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id) " + + "right join entity3 e3 on e1.id = e3.id", + "SELECT * FROM " + + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + + "RIGHT JOIN entity3 e3 ON e1.id = e3.id AND e1.tenant_id = 1 " + + "WHERE e3.tenant_id = 1"); + + + assertSql("select * FROM entity e " + + "LEFT JOIN (entity1 e1 right join entity2 e2 ON e1.id = e2.id) " + + "on e.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN (entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " + + "ON e.id = e2.id AND e2.tenant_id = 1 " + + "WHERE e.tenant_id = 1"); + + assertSql("select * FROM entity e " + + "LEFT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " + + "on e.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + + "ON e.id = e2.id AND e1.tenant_id = 1 " + + "WHERE e.tenant_id = 1"); + + assertSql("select * FROM entity e " + + "RIGHT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " + + "on e.id = e2.id", + "SELECT * FROM entity e " + + "RIGHT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + + "ON e.id = e2.id AND e.tenant_id = 1 " + + "WHERE e1.tenant_id = 1"); + } + + + @Test + void selectLeftJoinMultipleTrailingOn() { + // 多个 on 尾缀的 + assertSql("SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN entity2 e2 ON e2.id = e1.id " + + "ON e1.id = e.id " + + "WHERE (e.id = ? OR e.NAME = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " + + "ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + + "ON e1.id = e.id " + + "WHERE (e.id = ? OR e.NAME = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + + "ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); + } + + @Test + void selectInnerJoin() { + // inner join + assertSql("SELECT * FROM entity e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " + + "WHERE e.id = ? OR e.name = ?"); + + assertSql("SELECT * FROM entity e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM entity e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?)"); + + // 隐式内连接 + assertSql("SELECT * FROM entity,entity1 " + + "WHERE entity.id = entity1.id", + "SELECT * FROM entity, entity1 " + + "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); + + // 隐式内连接 + assertSql("SELECT * FROM entity a, with_as_entity1 b " + + "WHERE a.id = b.id", + "SELECT * FROM entity a, with_as_entity1 b " + + "WHERE a.id = b.id AND a.tenant_id = 1"); + + assertSql("SELECT * FROM with_as_entity a, with_as_entity1 b " + + "WHERE a.id = b.id", + "SELECT * FROM with_as_entity a, with_as_entity1 b " + + "WHERE a.id = b.id"); + + // SubJoin with 隐式内连接 + assertSql("SELECT * FROM (entity,entity1) " + + "WHERE entity.id = entity1.id", + "SELECT * FROM (entity, entity1) " + + "WHERE entity.id = entity1.id " + + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); + + assertSql("SELECT * FROM ((entity,entity1),entity2) " + + "WHERE entity.id = entity1.id and entity.id = entity2.id", + "SELECT * FROM ((entity, entity1), entity2) " + + "WHERE entity.id = entity1.id AND entity.id = entity2.id " + + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1"); + + assertSql("SELECT * FROM (entity,(entity1,entity2)) " + + "WHERE entity.id = entity1.id and entity.id = entity2.id", + "SELECT * FROM (entity, (entity1, entity2)) " + + "WHERE entity.id = entity1.id AND entity.id = entity2.id " + + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1"); + + // 沙雕的括号写法 + assertSql("SELECT * FROM (((entity,entity1))) " + + "WHERE entity.id = entity1.id", + "SELECT * FROM (((entity, entity1))) " + + "WHERE entity.id = entity1.id " + + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); + + } + + + @Test + void selectWithAs() { + assertSql("with with_as_A as (select * from entity) select * from with_as_A", + "WITH with_as_A AS (SELECT * FROM entity WHERE entity.tenant_id = 1) SELECT * FROM with_as_A"); + } + + + @Test + void selectIgnoreTable() { + assertSql(" SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)", + "SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id AND item.tenant_id = 1 WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)"); + } + + private void assertSql(String sql, String targetSql) { + assertEquals(targetSql, interceptor.parserSingle(sql, null)); + } + + // ========== 额外的测试 ========== + + @Test + public void testSelectSingle() { + // 单表 + assertSql("select * from t_user where id = ?", + "SELECT * FROM t_user WHERE id = ? AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); + + assertSql("select * from t_user where id = ? or name = ?", + "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); + + assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)", + "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); + + /* not */ + assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)", + "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); + } + + @Test + public void testSelectLeftJoin() { + // left join + assertSql("SELECT * FROM t_user e " + + "left join t_role e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "left join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + } + + @Test + public void testSelectRightJoin() { + // right join + assertSql("SELECT * FROM t_user e " + + "right join t_role e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " + + "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "right join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " + + "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); + } + + @Test + public void testSelectInnerJoin() { + // inner join + assertSql("SELECT * FROM t_user e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " + + "WHERE e.id = ? OR e.name = ?"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?)"); + + // 没有 On 的 inner join + assertSql("SELECT * FROM entity,entity1 " + + "WHERE entity.id = entity1.id", + "SELECT * FROM entity, entity1 " + + "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java new file mode 100644 index 0000000..17dddc9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.springframework.core.annotation.AnnotationUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DataPermissionRuleFactoryImpl} 单元测试 + * + * @author 芋道源码 + */ +class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionRuleFactoryImpl dataPermissionRuleFactory; + + @Spy + private List rules = Arrays.asList(new DataPermissionRule01(), + new DataPermissionRule02()); + + @BeforeEach + public void setUp() { + DataPermissionContextHolder.clear(); + } + + @Test + public void testGetDataPermissionRule_02() { + // 准备参数 + String mappedStatementId = randomString(); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertSame(rules, result); + } + + @Test + public void testGetDataPermissionRule_03() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertTrue(result.isEmpty()); + } + + @Test + public void testGetDataPermissionRule_04() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertEquals(1, result.size()); + assertEquals(DataPermissionRule01.class, result.get(0).getClass()); + } + + @Test + public void testGetDataPermissionRule_05() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertEquals(1, result.size()); + assertEquals(DataPermissionRule02.class, result.get(0).getClass()); + } + + @Test + public void testGetDataPermissionRule_06() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertSame(rules, result); + } + + @DataPermission(enable = false) + static class TestClass03 {} + + @DataPermission(includeRules = DataPermissionRule01.class) + static class TestClass04 {} + + @DataPermission(excludeRules = DataPermissionRule01.class) + static class TestClass05 {} + + @DataPermission + static class TestClass06 {} + + static class DataPermissionRule01 implements DataPermissionRule { + + @Override + public Set getTableNames() { + return null; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + + static class DataPermissionRule02 implements DataPermissionRule { + + @Override + public Set getTableNames() { + return null; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java new file mode 100644 index 0000000..7faae00 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java @@ -0,0 +1,238 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi; +import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * {@link DeptDataPermissionRule} 的单元测试 + * + * @author 芋道源码 + */ +class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { + + @InjectMocks + private DeptDataPermissionRule rule; + + @Mock + private PermissionCommonApi permissionApi; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setUp() { + // 清空 rule + rule.getTableNames().clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + } + + @Test // 无 LoginUser + public void testGetExpression_noLoginUser() { + // 准备参数 + String tableName = randomString(); + Alias tableAlias = new Alias(randomString()); + // mock 方法 + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + } + + @Test // 无数据权限时 + public void testGetExpression_noDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法 + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(permissionApi 返回 null) + when(permissionApi.getDeptDataPermission(eq(loginUser.getId()))).thenReturn(null); + + // 调用 + NullPointerException exception = assertThrows(NullPointerException.class, + () -> rule.getExpression(tableName, tableAlias)); + // 断言 + assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); + } + } + + @Test // 全部数据权限 + public void testGetExpression_allDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限 + public void testGetExpression_noDept_noSelf() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("null = null", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(字段都不符合) + public void testGetExpression_noDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertSame(EXPRESSION_NULL, expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(self 符合) + public void testGetExpression_noDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.id = 1", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept 符合) + public void testGetExpression_yesDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.dept_id IN (10, 20)", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept + self 符合) + public void testGetExpression_yesDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtilsTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtilsTest.java new file mode 100644 index 0000000..1cc57c2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtilsTest.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.framework.datapermission.core.util; + +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class DataPermissionUtilsTest { + + @Test + public void testExecuteIgnore() { + DataPermissionUtils.executeIgnore(() -> assertFalse(DataPermissionContextHolder.get().enable())); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml new file mode 100644 index 0000000..bc800c8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml @@ -0,0 +1,54 @@ + + + + yudao-framework + cn.iocoder.boot + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-ip + jar + + ${project.artifactId} + IP 拓展,支持如下功能: + 1. IP 功能:查询 IP 对应的城市信息 + 基于 https://gitee.com/lionsoul/ip2region 实现 + 2. 城市功能:查询城市编码对应的城市信息 + 基于 https://github.com/modood/Administrative-divisions-of-China 实现 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + org.lionsoul + ip2region + + + + org.projectlombok + lombok + + + + org.slf4j + slf4j-api + provided + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java new file mode 100644 index 0000000..3027085 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.framework.ip.core; + +import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum; +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +/** + * 区域节点,包括国家、省份、城市、地区等信息 + * + * 数据可见 resources/area.csv 文件 + * + * @author 芋道源码 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@ToString(exclude = {"parent"}) // 参见 https://gitee.com/yudaocode/yudao-cloud-mini/pulls/2 原因 +public class Area { + + /** + * 编号 - 全球,即根目录 + */ + public static final Integer ID_GLOBAL = 0; + /** + * 编号 - 中国 + */ + public static final Integer ID_CHINA = 1; + + /** + * 编号 + */ + private Integer id; + /** + * 名字 + */ + private String name; + /** + * 类型 + * + * 枚举 {@link AreaTypeEnum} + */ + private Integer type; + + /** + * 父节点 + */ + @JsonManagedReference + private Area parent; + /** + * 子节点 + */ + @JsonBackReference + private List children; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/enums/AreaTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/enums/AreaTypeEnum.java new file mode 100644 index 0000000..1698ad2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/enums/AreaTypeEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.ip.core.enums; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 区域类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum AreaTypeEnum implements ArrayValuable { + + COUNTRY(1, "国家"), + PROVINCE(2, "省份"), + CITY(3, "城市"), + DISTRICT(4, "地区"), // 县、镇、区等 + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(AreaTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java new file mode 100644 index 0000000..8e27a31 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java @@ -0,0 +1,214 @@ +package cn.iocoder.yudao.framework.ip.core.utils; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.csv.CsvRow; +import cn.hutool.core.text.csv.CsvUtil; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.ip.core.Area; +import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.findFirst; + +/** + * 区域工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class AreaUtils { + + /** + * 初始化 SEARCHER + */ + @SuppressWarnings("InstantiationOfUtilityClass") + private final static AreaUtils INSTANCE = new AreaUtils(); + + /** + * Area 内存缓存,提升访问速度 + */ + private static Map areas; + + private AreaUtils() { + long now = System.currentTimeMillis(); + areas = new HashMap<>(); + areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, + null, new ArrayList<>())); + // 从 csv 中加载数据 + List rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows(); + rows.remove(0); // 删除 header + for (CsvRow row : rows) { + // 创建 Area 对象 + Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)), + null, new ArrayList<>()); + // 添加到 areas 中 + areas.put(area.getId(), area); + } + + // 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取 + for (CsvRow row : rows) { + Area area = areas.get(Integer.valueOf(row.get(0))); // 自己 + Area parent = areas.get(Integer.valueOf(row.get(3))); // 父 + Assert.isTrue(area != parent, "{}:父子节点相同", area.getName()); + area.setParent(parent); + parent.getChildren().add(area); + } + log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); + } + + /** + * 获得指定编号对应的区域 + * + * @param id 区域编号 + * @return 区域 + */ + public static Area getArea(Integer id) { + return areas.get(id); + } + + /** + * 获得指定区域对应的编号 + * + * @param pathStr 区域路径,例如说:河南省/石家庄市/新华区 + * @return 区域 + */ + public static Area parseArea(String pathStr) { + String[] paths = pathStr.split("/"); + Area area = null; + for (String path : paths) { + if (area == null) { + area = findFirst(areas.values(), item -> item.getName().equals(path)); + } else { + area = findFirst(area.getChildren(), item -> item.getName().equals(path)); + } + } + return area; + } + + /** + * 获取所有节点的全路径名称如:河南省/石家庄市/新华区 + * + * @param areas 地区树 + * @return 所有节点的全路径名称 + */ + public static List getAreaNodePathList(List areas) { + List paths = new ArrayList<>(); + areas.forEach(area -> getAreaNodePathList(area, "", paths)); + return paths; + } + + /** + * 构建一棵树的所有节点的全路径名称,并将其存储为 "祖先/父级/子级" 的形式 + * + * @param node 父节点 + * @param path 全路径名称 + * @param paths 全路径名称列表,省份/城市/地区 + */ + private static void getAreaNodePathList(Area node, String path, List paths) { + if (node == null) { + return; + } + // 构建当前节点的路径 + String currentPath = path.isEmpty() ? node.getName() : path + "/" + node.getName(); + paths.add(currentPath); + // 递归遍历子节点 + for (Area child : node.getChildren()) { + getAreaNodePathList(child, currentPath, paths); + } + } + + /** + * 格式化区域 + * + * @param id 区域编号 + * @return 格式化后的区域 + */ + public static String format(Integer id) { + return format(id, " "); + } + + /** + * 格式化区域 + * + * 例如说: + * 1. id = “静安区”时:上海 上海市 静安区 + * 2. id = “上海市”时:上海 上海市 + * 3. id = “上海”时:上海 + * 4. id = “美国”时:美国 + * 当区域在中国时,默认不显示中国 + * + * @param id 区域编号 + * @param separator 分隔符 + * @return 格式化后的区域 + */ + public static String format(Integer id, String separator) { + // 获得区域 + Area area = areas.get(id); + if (area == null) { + return null; + } + + // 格式化 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < AreaTypeEnum.values().length; i++) { // 避免死循环 + sb.insert(0, area.getName()); + // “递归”父节点 + area = area.getParent(); + if (area == null + || ObjectUtils.equalsAny(area.getId(), Area.ID_GLOBAL, Area.ID_CHINA)) { // 跳过父节点为中国的情况 + break; + } + sb.insert(0, separator); + } + return sb.toString(); + } + + /** + * 获取指定类型的区域列表 + * + * @param type 区域类型 + * @param func 转换函数 + * @param 结果类型 + * @return 区域列表 + */ + public static List getByType(AreaTypeEnum type, Function func) { + return convertList(areas.values(), func, area -> type.getType().equals(area.getType())); + } + + /** + * 根据区域编号、上级区域类型,获取上级区域编号 + * + * @param id 区域编号 + * @param type 区域类型 + * @return 上级区域编号 + */ + public static Integer getParentIdByType(Integer id, @NonNull AreaTypeEnum type) { + for (int i = 0; i < Byte.MAX_VALUE; i++) { + Area area = AreaUtils.getArea(id); + if (area == null) { + return null; + } + // 情况一:匹配到,返回它 + if (type.getType().equals(area.getType())) { + return area.getId(); + } + // 情况二:找到根节点,返回空 + if (area.getParent() == null || area.getParent().getId() == null) { + return null; + } + // 其它:继续向上查找 + id = area.getParent().getId(); + } + return null; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java new file mode 100644 index 0000000..f74f848 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtils.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.framework.ip.core.utils; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.iocoder.yudao.framework.ip.core.Area; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; + +import java.io.IOException; + +/** + * IP 工具类 + * + * IP 数据源来自 ip2region.xdb 精简版,基于 项目 + * + * @author wanglhup + */ +@Slf4j +public class IPUtils { + + /** + * 初始化 SEARCHER + */ + @SuppressWarnings("InstantiationOfUtilityClass") + private final static IPUtils INSTANCE = new IPUtils(); + + /** + * IP 查询器,启动加载到内存中 + */ + private static Searcher SEARCHER; + + /** + * 私有化构造 + */ + private IPUtils() { + try { + long now = System.currentTimeMillis(); + byte[] bytes = ResourceUtil.readBytes("ip2region.xdb"); + SEARCHER = Searcher.newWithBuffer(bytes); + log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); + } catch (IOException e) { + log.error("启动加载 IPUtils 失败", e); + } + } + + /** + * 查询 IP 对应的地区编号 + * + * @param ip IP 地址,格式为 127.0.0.1 + * @return 地区id + */ + @SneakyThrows + public static Integer getAreaId(String ip) { + return Integer.parseInt(SEARCHER.search(ip.trim())); + } + + /** + * 查询 IP 对应的地区编号 + * + * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 + * @return 地区编号 + */ + @SneakyThrows + public static Integer getAreaId(long ip) { + return Integer.parseInt(SEARCHER.search(ip)); + } + + /** + * 查询 IP 对应的地区 + * + * @param ip IP 地址,格式为 127.0.0.1 + * @return 地区 + */ + public static Area getArea(String ip) { + return AreaUtils.getArea(getAreaId(ip)); + } + + /** + * 查询 IP 对应的地区 + * + * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 + * @return 地区 + */ + public static Area getArea(long ip) { + return AreaUtils.getArea(getAreaId(ip)); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/package-info.java new file mode 100644 index 0000000..9d422f5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/package-info.java @@ -0,0 +1,11 @@ +/** + * IP 拓展,支持如下功能: + * + * 1. IP 功能:查询 IP 对应的城市信息 + * 基于 https://gitee.com/lionsoul/ip2region 实现 + * 2. 城市功能:查询城市编码对应的城市信息 + * 基于 https://github.com/modood/Administrative-divisions-of-China 实现 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.ip; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv new file mode 100644 index 0000000..0dd830e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv @@ -0,0 +1,3662 @@ +id,name,type,parentId +1,中国,1,0 +2,蒙古,1,0 +3,朝鲜,1,0 +4,韩国,1,0 +5,日本,1,0 +6,菲律宾,1,0 +7,越南,1,0 +8,老挝,1,0 +9,柬埔寨,1,0 +10,缅甸,1,0 +11,泰国,1,0 +12,马来西亚,1,0 +13,文莱,1,0 +14,新加坡,1,0 +15,印度尼西亚,1,0 +16,东帝汶,1,0 +17,尼泊尔,1,0 +18,不丹,1,0 +19,孟加拉国,1,0 +20,印度,1,0 +21,巴基斯坦,1,0 +22,斯里兰卡,1,0 +23,马尔代夫,1,0 +24,哈萨克斯坦,1,0 +25,吉尔吉斯斯坦,1,0 +26,塔吉克斯坦,1,0 +27,乌兹别克斯坦,1,0 +28,土库曼斯坦,1,0 +29,阿富汗,1,0 +30,伊拉克,1,0 +31,伊朗,1,0 +32,叙利亚,1,0 +33,约旦,1,0 +34,黎巴嫩,1,0 +35,以色列,1,0 +36,巴勒斯坦,1,0 +37,沙特阿拉伯,1,0 +38,巴林,1,0 +39,卡塔尔,1,0 +40,科威特,1,0 +41,阿拉伯联合酋长国,1,0 +42,阿曼,1,0 +43,也门,1,0 +44,格鲁吉亚,1,0 +45,亚美尼亚,1,0 +46,阿塞拜疆,1,0 +47,土耳其,1,0 +48,塞浦路斯,1,0 +49,芬兰,1,0 +50,瑞典,1,0 +51,挪威,1,0 +52,冰岛,1,0 +53,丹麦,1,0 +54,爱沙尼亚,1,0 +55,拉脱维亚,1,0 +56,立陶宛,1,0 +57,白俄罗斯,1,0 +58,俄罗斯,1,0 +59,乌克兰,1,0 +60,摩尔多瓦,1,0 +61,波兰,1,0 +62,捷克,1,0 +63,斯洛伐克,1,0 +64,匈牙利,1,0 +65,德国,1,0 +66,奥地利,1,0 +67,瑞士,1,0 +68,列支敦士登,1,0 +69,英国,1,0 +70,爱尔兰,1,0 +71,荷兰,1,0 +72,比利时,1,0 +73,卢森堡,1,0 +74,法国,1,0 +75,摩纳哥,1,0 +76,罗马尼亚,1,0 +77,保加利亚,1,0 +78,塞尔维亚,1,0 +79,马其顿,1,0 +80,阿尔巴尼亚,1,0 +81,希腊,1,0 +82,斯洛文尼亚,1,0 +83,克罗地亚,1,0 +84,波斯尼亚和墨塞哥维那,1,0 +85,意大利,1,0 +86,梵蒂冈,1,0 +87,圣马力诺,1,0 +88,马耳他,1,0 +89,西班牙,1,0 +90,葡萄牙,1,0 +91,安道尔共和国,1,0 +92,埃及,1,0 +93,利比亚,1,0 +94,苏丹,1,0 +95,突尼斯,1,0 +96,阿尔及利亚,1,0 +97,摩洛哥,1,0 +98,亚速尔群岛,1,0 +99,马德拉群岛,1,0 +100,埃塞俄比亚,1,0 +101,厄立特里亚,1,0 +102,索马里,1,0 +103,吉布提,1,0 +104,肯尼亚,1,0 +105,坦桑尼亚,1,0 +106,乌干达,1,0 +107,卢旺达,1,0 +108,布隆迪,1,0 +109,塞舌尔,1,0 +110,圣多美及普林西比,1,0 +111,塞内加尔,1,0 +112,冈比亚,1,0 +113,马里,1,0 +114,布基纳法索,1,0 +115,几内亚,1,0 +116,几内亚比绍,1,0 +117,佛得角,1,0 +118,塞拉利昂,1,0 +119,利比里亚,1,0 +120,科特迪瓦,1,0 +121,加纳,1,0 +122,多哥,1,0 +123,贝宁,1,0 +124,尼日尔,1,0 +125,加那利群岛,1,0 +126,赞比亚,1,0 +127,安哥拉,1,0 +128,津巴布韦,1,0 +129,马拉维,1,0 +130,莫桑比克,1,0 +131,博茨瓦纳,1,0 +132,纳米比亚,1,0 +133,南非,1,0 +134,斯威士兰,1,0 +135,莱索托,1,0 +136,马达加斯加,1,0 +137,科摩罗,1,0 +138,毛里求斯,1,0 +139,留尼旺,1,0 +140,圣赫勒拿,1,0 +141,澳大利亚,1,0 +142,新西兰,1,0 +143,巴布亚新几内亚,1,0 +144,所罗门群岛,1,0 +145,瓦努阿图共和国,1,0 +146,密克罗尼西亚,1,0 +147,马绍尔群岛,1,0 +148,帕劳,1,0 +149,瑙鲁,1,0 +150,基里巴斯,1,0 +151,图瓦卢,1,0 +152,萨摩亚,1,0 +153,斐济,1,0 +154,汤加,1,0 +155,库克群岛,1,0 +156,关岛,1,0 +157,新喀里多尼亚,1,0 +158,法属波利尼西亚,1,0 +159,皮特凯恩岛,1,0 +160,瓦利斯与富图纳,1,0 +161,纽埃,1,0 +162,托克劳,1,0 +163,美属萨摩亚,1,0 +164,北马里亚纳,1,0 +165,加拿大,1,0 +166,美国,1,0 +167,墨西哥,1,0 +168,格陵兰,1,0 +169,危地马拉,1,0 +170,伯利兹,1,0 +171,萨尔瓦多,1,0 +172,洪都拉斯,1,0 +173,尼加拉瓜,1,0 +174,哥斯达黎加,1,0 +175,巴拿马,1,0 +176,巴哈马,1,0 +177,古巴,1,0 +178,牙买加,1,0 +179,海地,1,0 +180,多米尼加共和国,1,0 +181,安提瓜和巴布达,1,0 +182,圣基茨和尼维斯,1,0 +183,多米尼克,1,0 +184,圣卢西亚,1,0 +185,圣文森特和格林纳丁斯,1,0 +186,格林纳达,1,0 +187,巴巴多斯,1,0 +188,特立尼达和多巴哥,1,0 +189,波多黎各,1,0 +190,英属维尔京群岛,1,0 +191,美属维尔京群岛,1,0 +192,安圭拉,1,0 +193,蒙特塞拉特岛,1,0 +194,瓜德罗普,1,0 +195,马提尼克,1,0 +196,荷属安的列斯,1,0 +197,阿鲁巴,1,0 +198,特克斯和凯科斯群岛,1,0 +199,开曼群岛,1,0 +200,百慕大,1,0 +201,哥伦比亚,1,0 +202,委内瑞拉,1,0 +203,圭亚那,1,0 +204,法属圭亚那,1,0 +205,苏里南,1,0 +206,厄瓜多尔,1,0 +207,秘鲁,1,0 +208,玻利维亚,1,0 +209,巴西,1,0 +210,智利,1,0 +211,阿根廷,1,0 +212,乌拉圭,1,0 +213,巴拉圭,1,0 +214,波黑,1,0 +215,直布罗陀,1,0 +216,新喀里多尼亚群岛,1,0 +217,瓦利斯和富图纳群岛,1,0 +218,泽西岛,1,0 +219,黑山,1,0 +220,英属马恩岛,1,0 +221,尼日利亚,1,0 +222,喀麦隆,1,0 +223,加蓬,1,0 +224,乍得,1,0 +225,刚果共和国,1,0 +226,中非共和国,1,0 +227,南苏丹,1,0 +228,赤道几内亚,1,0 +229,毛里塔尼亚,1,0 +230,刚果民主共和国,1,0 +231,留尼汪岛,1,0 +232,格陵兰岛,1,0 +233,法罗群岛,1,0 +234,根西岛,1,0 +235,百慕大群岛,1,0 +236,圣皮埃尔和密克隆群岛,1,0 +237,法属圣马丁,1,0 +238,奥兰群岛,1,0 +239,北马里亚纳群岛,1,0 +240,库拉索,1,0 +241,博内尔岛,1,0 +242,圣马丁岛,1,0 +243,圣巴泰勒米岛,1,0 +244,福克兰群岛,1,0 +245,圣多美和普林西比,1,0 +246,英属印度洋领地,1,0 +247,东萨摩亚,1,0 +248,诺福克岛,1,0 +110000,北京市,2,1 +120000,天津市,2,1 +130000,河北省,2,1 +140000,山西省,2,1 +150000,内蒙古自治区,2,1 +210000,辽宁省,2,1 +220000,吉林省,2,1 +230000,黑龙江省,2,1 +310000,上海市,2,1 +320000,江苏省,2,1 +330000,浙江省,2,1 +340000,安徽省,2,1 +350000,福建省,2,1 +360000,江西省,2,1 +370000,山东省,2,1 +410000,河南省,2,1 +420000,湖北省,2,1 +430000,湖南省,2,1 +440000,广东省,2,1 +450000,广西壮族自治区,2,1 +460000,海南省,2,1 +500000,重庆市,2,1 +510000,四川省,2,1 +520000,贵州省,2,1 +530000,云南省,2,1 +540000,西藏自治区,2,1 +610000,陕西省,2,1 +620000,甘肃省,2,1 +630000,青海省,2,1 +640000,宁夏回族自治区,2,1 +650000,新疆维吾尔自治区,2,1 +110100,北京市,3,110000 +120100,天津市,3,120000 +130100,石家庄市,3,130000 +130200,唐山市,3,130000 +130300,秦皇岛市,3,130000 +130400,邯郸市,3,130000 +130500,邢台市,3,130000 +130600,保定市,3,130000 +130700,张家口市,3,130000 +130800,承德市,3,130000 +130900,沧州市,3,130000 +131000,廊坊市,3,130000 +131100,衡水市,3,130000 +140100,太原市,3,140000 +140200,大同市,3,140000 +140300,阳泉市,3,140000 +140400,长治市,3,140000 +140500,晋城市,3,140000 +140600,朔州市,3,140000 +140700,晋中市,3,140000 +140800,运城市,3,140000 +140900,忻州市,3,140000 +141000,临汾市,3,140000 +141100,吕梁市,3,140000 +150100,呼和浩特市,3,150000 +150200,包头市,3,150000 +150300,乌海市,3,150000 +150400,赤峰市,3,150000 +150500,通辽市,3,150000 +150600,鄂尔多斯市,3,150000 +150700,呼伦贝尔市,3,150000 +150800,巴彦淖尔市,3,150000 +150900,乌兰察布市,3,150000 +152200,兴安盟,3,150000 +152500,锡林郭勒盟,3,150000 +152900,阿拉善盟,3,150000 +210100,沈阳市,3,210000 +210200,大连市,3,210000 +210300,鞍山市,3,210000 +210400,抚顺市,3,210000 +210500,本溪市,3,210000 +210600,丹东市,3,210000 +210700,锦州市,3,210000 +210800,营口市,3,210000 +210900,阜新市,3,210000 +211000,辽阳市,3,210000 +211100,盘锦市,3,210000 +211200,铁岭市,3,210000 +211300,朝阳市,3,210000 +211400,葫芦岛市,3,210000 +220100,长春市,3,220000 +220200,吉林市,3,220000 +220300,四平市,3,220000 +220400,辽源市,3,220000 +220500,通化市,3,220000 +220600,白山市,3,220000 +220700,松原市,3,220000 +220800,白城市,3,220000 +222400,延边朝鲜族自治州,3,220000 +230100,哈尔滨市,3,230000 +230200,齐齐哈尔市,3,230000 +230300,鸡西市,3,230000 +230400,鹤岗市,3,230000 +230500,双鸭山市,3,230000 +230600,大庆市,3,230000 +230700,伊春市,3,230000 +230800,佳木斯市,3,230000 +230900,七台河市,3,230000 +231000,牡丹江市,3,230000 +231100,黑河市,3,230000 +231200,绥化市,3,230000 +232700,大兴安岭地区,3,230000 +310100,上海市,3,310000 +320100,南京市,3,320000 +320200,无锡市,3,320000 +320300,徐州市,3,320000 +320400,常州市,3,320000 +320500,苏州市,3,320000 +320600,南通市,3,320000 +320700,连云港市,3,320000 +320800,淮安市,3,320000 +320900,盐城市,3,320000 +321000,扬州市,3,320000 +321100,镇江市,3,320000 +321200,泰州市,3,320000 +321300,宿迁市,3,320000 +330100,杭州市,3,330000 +330200,宁波市,3,330000 +330300,温州市,3,330000 +330400,嘉兴市,3,330000 +330500,湖州市,3,330000 +330600,绍兴市,3,330000 +330700,金华市,3,330000 +330800,衢州市,3,330000 +330900,舟山市,3,330000 +331000,台州市,3,330000 +331100,丽水市,3,330000 +340100,合肥市,3,340000 +340200,芜湖市,3,340000 +340300,蚌埠市,3,340000 +340400,淮南市,3,340000 +340500,马鞍山市,3,340000 +340600,淮北市,3,340000 +340700,铜陵市,3,340000 +340800,安庆市,3,340000 +341000,黄山市,3,340000 +341100,滁州市,3,340000 +341200,阜阳市,3,340000 +341300,宿州市,3,340000 +341500,六安市,3,340000 +341600,亳州市,3,340000 +341700,池州市,3,340000 +341800,宣城市,3,340000 +350100,福州市,3,350000 +350200,厦门市,3,350000 +350300,莆田市,3,350000 +350400,三明市,3,350000 +350500,泉州市,3,350000 +350600,漳州市,3,350000 +350700,南平市,3,350000 +350800,龙岩市,3,350000 +350900,宁德市,3,350000 +360100,南昌市,3,360000 +360200,景德镇市,3,360000 +360300,萍乡市,3,360000 +360400,九江市,3,360000 +360500,新余市,3,360000 +360600,鹰潭市,3,360000 +360700,赣州市,3,360000 +360800,吉安市,3,360000 +360900,宜春市,3,360000 +361000,抚州市,3,360000 +361100,上饶市,3,360000 +370100,济南市,3,370000 +370200,青岛市,3,370000 +370300,淄博市,3,370000 +370400,枣庄市,3,370000 +370500,东营市,3,370000 +370600,烟台市,3,370000 +370700,潍坊市,3,370000 +370800,济宁市,3,370000 +370900,泰安市,3,370000 +371000,威海市,3,370000 +371100,日照市,3,370000 +371300,临沂市,3,370000 +371400,德州市,3,370000 +371500,聊城市,3,370000 +371600,滨州市,3,370000 +371700,菏泽市,3,370000 +410100,郑州市,3,410000 +410200,开封市,3,410000 +410300,洛阳市,3,410000 +410400,平顶山市,3,410000 +410500,安阳市,3,410000 +410600,鹤壁市,3,410000 +410700,新乡市,3,410000 +410800,焦作市,3,410000 +410900,濮阳市,3,410000 +411000,许昌市,3,410000 +411100,漯河市,3,410000 +411200,三门峡市,3,410000 +411300,南阳市,3,410000 +411400,商丘市,3,410000 +411500,信阳市,3,410000 +411600,周口市,3,410000 +411700,驻马店市,3,410000 +419000,省直辖县级行政区划,3,410000 +420100,武汉市,3,420000 +420200,黄石市,3,420000 +420300,十堰市,3,420000 +420500,宜昌市,3,420000 +420600,襄阳市,3,420000 +420700,鄂州市,3,420000 +420800,荆门市,3,420000 +420900,孝感市,3,420000 +421000,荆州市,3,420000 +421100,黄冈市,3,420000 +421200,咸宁市,3,420000 +421300,随州市,3,420000 +422800,恩施土家族苗族自治州,3,420000 +429000,省直辖县级行政区划,3,420000 +430100,长沙市,3,430000 +430200,株洲市,3,430000 +430300,湘潭市,3,430000 +430400,衡阳市,3,430000 +430500,邵阳市,3,430000 +430600,岳阳市,3,430000 +430700,常德市,3,430000 +430800,张家界市,3,430000 +430900,益阳市,3,430000 +431000,郴州市,3,430000 +431100,永州市,3,430000 +431200,怀化市,3,430000 +431300,娄底市,3,430000 +433100,湘西土家族苗族自治州,3,430000 +440100,广州市,3,440000 +440200,韶关市,3,440000 +440300,深圳市,3,440000 +440400,珠海市,3,440000 +440500,汕头市,3,440000 +440600,佛山市,3,440000 +440700,江门市,3,440000 +440800,湛江市,3,440000 +440900,茂名市,3,440000 +441200,肇庆市,3,440000 +441300,惠州市,3,440000 +441400,梅州市,3,440000 +441500,汕尾市,3,440000 +441600,河源市,3,440000 +441700,阳江市,3,440000 +441800,清远市,3,440000 +441900,东莞市,3,440000 +441901,莞城区,4,441900 +441902,南城区,4,441900 +441904,万江区,4,441900 +441905,石碣镇,4,441900 +441906,石龙镇,4,441900 +441907,茶山镇,4,441900 +441908,石排镇,4,441900 +441909,企石镇,4,441900 +441910,横沥镇,4,441900 +441911,桥头镇,4,441900 +441912,谢岗镇,4,441900 +441913,东坑镇,4,441900 +441914,常平镇,4,441900 +441915,寮步镇,4,441900 +441916,大朗镇,4,441900 +441917,麻涌镇,4,441900 +441918,中堂镇,4,441900 +441919,高埗镇,4,441900 +441920,樟木头镇,4,441900 +441921,大岭山镇,4,441900 +441922,望牛墩镇,4,441900 +441923,黄江镇,4,441900 +441924,洪梅镇,4,441900 +441925,清溪镇,4,441900 +441926,沙田镇,4,441900 +441927,道滘镇,4,441900 +441928,塘厦镇,4,441900 +441929,虎门镇,4,441900 +441930,厚街镇,4,441900 +441931,凤岗镇,4,441900 +441932,长安镇,4,441900 +442000,中山市,3,440000 +442001,石岐街道,4,442000 +442002,东区街道,4,442000 +442003,中山港街道,4,442000 +442004,西区街道,4,442000 +442005,南区街道,4,442000 +442006,五桂山街道,4,442000 +442007,民众街道,4,442000 +442008,南朗街道,4,442000 +442009,黄圃镇,4,442000 +442010,东凤镇,4,442000 +442011,古镇镇,4,442000 +442012,沙溪镇,4,442000 +442013,坦洲镇,4,442000 +442014,港口镇,4,442000 +442015,三角镇,4,442000 +442016,横栏镇,4,442000 +442017,南头镇,4,442000 +442018,阜沙镇,4,442000 +442019,三乡镇,4,442000 +442020,板芙镇,4,442000 +442021,大涌镇,4,442000 +442022,神湾镇,4,442000 +442023,小榄镇,4,442000 +445100,潮州市,3,440000 +445200,揭阳市,3,440000 +445300,云浮市,3,440000 +450100,南宁市,3,450000 +450200,柳州市,3,450000 +450300,桂林市,3,450000 +450400,梧州市,3,450000 +450500,北海市,3,450000 +450600,防城港市,3,450000 +450700,钦州市,3,450000 +450800,贵港市,3,450000 +450900,玉林市,3,450000 +451000,百色市,3,450000 +451100,贺州市,3,450000 +451200,河池市,3,450000 +451300,来宾市,3,450000 +451400,崇左市,3,450000 +460100,海口市,3,460000 +460200,三亚市,3,460000 +460300,三沙市,3,460000 +460400,儋州市,3,460000 +469000,省直辖县级行政区划,3,460000 +500100,重庆市,3,500000 +510100,成都市,3,510000 +510300,自贡市,3,510000 +510400,攀枝花市,3,510000 +510500,泸州市,3,510000 +510600,德阳市,3,510000 +510700,绵阳市,3,510000 +510800,广元市,3,510000 +510900,遂宁市,3,510000 +511000,内江市,3,510000 +511100,乐山市,3,510000 +511300,南充市,3,510000 +511400,眉山市,3,510000 +511500,宜宾市,3,510000 +511600,广安市,3,510000 +511700,达州市,3,510000 +511800,雅安市,3,510000 +511900,巴中市,3,510000 +512000,资阳市,3,510000 +513200,阿坝藏族羌族自治州,3,510000 +513300,甘孜藏族自治州,3,510000 +513400,凉山彝族自治州,3,510000 +520100,贵阳市,3,520000 +520200,六盘水市,3,520000 +520300,遵义市,3,520000 +520400,安顺市,3,520000 +520500,毕节市,3,520000 +520600,铜仁市,3,520000 +522300,黔西南布依族苗族自治州,3,520000 +522600,黔东南苗族侗族自治州,3,520000 +522700,黔南布依族苗族自治州,3,520000 +530100,昆明市,3,530000 +530300,曲靖市,3,530000 +530400,玉溪市,3,530000 +530500,保山市,3,530000 +530600,昭通市,3,530000 +530700,丽江市,3,530000 +530800,普洱市,3,530000 +530900,临沧市,3,530000 +532300,楚雄彝族自治州,3,530000 +532500,红河哈尼族彝族自治州,3,530000 +532600,文山壮族苗族自治州,3,530000 +532800,西双版纳傣族自治州,3,530000 +532900,大理白族自治州,3,530000 +533100,德宏傣族景颇族自治州,3,530000 +533300,怒江傈僳族自治州,3,530000 +533400,迪庆藏族自治州,3,530000 +540100,拉萨市,3,540000 +540200,日喀则市,3,540000 +540300,昌都市,3,540000 +540400,林芝市,3,540000 +540500,山南市,3,540000 +540600,那曲市,3,540000 +542500,阿里地区,3,540000 +610100,西安市,3,610000 +610200,铜川市,3,610000 +610300,宝鸡市,3,610000 +610400,咸阳市,3,610000 +610500,渭南市,3,610000 +610600,延安市,3,610000 +610700,汉中市,3,610000 +610800,榆林市,3,610000 +610900,安康市,3,610000 +611000,商洛市,3,610000 +620100,兰州市,3,620000 +620200,嘉峪关市,3,620000 +620300,金昌市,3,620000 +620400,白银市,3,620000 +620500,天水市,3,620000 +620600,武威市,3,620000 +620700,张掖市,3,620000 +620800,平凉市,3,620000 +620900,酒泉市,3,620000 +621000,庆阳市,3,620000 +621100,定西市,3,620000 +621200,陇南市,3,620000 +622900,临夏回族自治州,3,620000 +623000,甘南藏族自治州,3,620000 +630100,西宁市,3,630000 +630200,海东市,3,630000 +632200,海北藏族自治州,3,630000 +632300,黄南藏族自治州,3,630000 +632500,海南藏族自治州,3,630000 +632600,果洛藏族自治州,3,630000 +632700,玉树藏族自治州,3,630000 +632800,海西蒙古族藏族自治州,3,630000 +640100,银川市,3,640000 +640200,石嘴山市,3,640000 +640300,吴忠市,3,640000 +640400,固原市,3,640000 +640500,中卫市,3,640000 +650100,乌鲁木齐市,3,650000 +650200,克拉玛依市,3,650000 +650400,吐鲁番市,3,650000 +650500,哈密市,3,650000 +652300,昌吉回族自治州,3,650000 +652700,博尔塔拉蒙古自治州,3,650000 +652800,巴音郭楞蒙古自治州,3,650000 +652900,阿克苏地区,3,650000 +653000,克孜勒苏柯尔克孜自治州,3,650000 +653100,喀什地区,3,650000 +653200,和田地区,3,650000 +654000,伊犁哈萨克自治州,3,650000 +654200,塔城地区,3,650000 +654300,阿勒泰地区,3,650000 +659000,自治区直辖县级行政区划,3,650000 +110101,东城区,4,110100 +110102,西城区,4,110100 +110105,朝阳区,4,110100 +110106,丰台区,4,110100 +110107,石景山区,4,110100 +110108,海淀区,4,110100 +110109,门头沟区,4,110100 +110111,房山区,4,110100 +110112,通州区,4,110100 +110113,顺义区,4,110100 +110114,昌平区,4,110100 +110115,大兴区,4,110100 +110116,怀柔区,4,110100 +110117,平谷区,4,110100 +110118,密云区,4,110100 +110119,延庆区,4,110100 +120101,和平区,4,120100 +120102,河东区,4,120100 +120103,河西区,4,120100 +120104,南开区,4,120100 +120105,河北区,4,120100 +120106,红桥区,4,120100 +120110,东丽区,4,120100 +120111,西青区,4,120100 +120112,津南区,4,120100 +120113,北辰区,4,120100 +120114,武清区,4,120100 +120115,宝坻区,4,120100 +120116,滨海新区,4,120100 +120117,宁河区,4,120100 +120118,静海区,4,120100 +120119,蓟州区,4,120100 +130102,长安区,4,130100 +130104,桥西区,4,130100 +130105,新华区,4,130100 +130107,井陉矿区,4,130100 +130108,裕华区,4,130100 +130109,藁城区,4,130100 +130110,鹿泉区,4,130100 +130111,栾城区,4,130100 +130121,井陉县,4,130100 +130123,正定县,4,130100 +130125,行唐县,4,130100 +130126,灵寿县,4,130100 +130127,高邑县,4,130100 +130128,深泽县,4,130100 +130129,赞皇县,4,130100 +130130,无极县,4,130100 +130131,平山县,4,130100 +130132,元氏县,4,130100 +130133,赵县,4,130100 +130171,石家庄高新技术产业开发区,4,130100 +130172,石家庄循环化工园区,4,130100 +130181,辛集市,4,130100 +130183,晋州市,4,130100 +130184,新乐市,4,130100 +130202,路南区,4,130200 +130203,路北区,4,130200 +130204,古冶区,4,130200 +130205,开平区,4,130200 +130207,丰南区,4,130200 +130208,丰润区,4,130200 +130209,曹妃甸区,4,130200 +130224,滦南县,4,130200 +130225,乐亭县,4,130200 +130227,迁西县,4,130200 +130229,玉田县,4,130200 +130271,河北唐山芦台经济开发区,4,130200 +130272,唐山市汉沽管理区,4,130200 +130273,唐山高新技术产业开发区,4,130200 +130274,河北唐山海港经济开发区,4,130200 +130281,遵化市,4,130200 +130283,迁安市,4,130200 +130284,滦州市,4,130200 +130302,海港区,4,130300 +130303,山海关区,4,130300 +130304,北戴河区,4,130300 +130306,抚宁区,4,130300 +130321,青龙满族自治县,4,130300 +130322,昌黎县,4,130300 +130324,卢龙县,4,130300 +130371,秦皇岛市经济技术开发区,4,130300 +130372,北戴河新区,4,130300 +130402,邯山区,4,130400 +130403,丛台区,4,130400 +130404,复兴区,4,130400 +130406,峰峰矿区,4,130400 +130407,肥乡区,4,130400 +130408,永年区,4,130400 +130423,临漳县,4,130400 +130424,成安县,4,130400 +130425,大名县,4,130400 +130426,涉县,4,130400 +130427,磁县,4,130400 +130430,邱县,4,130400 +130431,鸡泽县,4,130400 +130432,广平县,4,130400 +130433,馆陶县,4,130400 +130434,魏县,4,130400 +130435,曲周县,4,130400 +130471,邯郸经济技术开发区,4,130400 +130473,邯郸冀南新区,4,130400 +130481,武安市,4,130400 +130502,襄都区,4,130500 +130503,信都区,4,130500 +130505,任泽区,4,130500 +130506,南和区,4,130500 +130522,临城县,4,130500 +130523,内丘县,4,130500 +130524,柏乡县,4,130500 +130525,隆尧县,4,130500 +130528,宁晋县,4,130500 +130529,巨鹿县,4,130500 +130530,新河县,4,130500 +130531,广宗县,4,130500 +130532,平乡县,4,130500 +130533,威县,4,130500 +130534,清河县,4,130500 +130535,临西县,4,130500 +130571,河北邢台经济开发区,4,130500 +130581,南宫市,4,130500 +130582,沙河市,4,130500 +130602,竞秀区,4,130600 +130606,莲池区,4,130600 +130607,满城区,4,130600 +130608,清苑区,4,130600 +130609,徐水区,4,130600 +130623,涞水县,4,130600 +130624,阜平县,4,130600 +130626,定兴县,4,130600 +130627,唐县,4,130600 +130628,高阳县,4,130600 +130629,容城县,4,130600 +130630,涞源县,4,130600 +130631,望都县,4,130600 +130632,安新县,4,130600 +130633,易县,4,130600 +130634,曲阳县,4,130600 +130635,蠡县,4,130600 +130636,顺平县,4,130600 +130637,博野县,4,130600 +130638,雄县,4,130600 +130671,保定高新技术产业开发区,4,130600 +130672,保定白沟新城,4,130600 +130681,涿州市,4,130600 +130682,定州市,4,130600 +130683,安国市,4,130600 +130684,高碑店市,4,130600 +130702,桥东区,4,130700 +130703,桥西区,4,130700 +130705,宣化区,4,130700 +130706,下花园区,4,130700 +130708,万全区,4,130700 +130709,崇礼区,4,130700 +130722,张北县,4,130700 +130723,康保县,4,130700 +130724,沽源县,4,130700 +130725,尚义县,4,130700 +130726,蔚县,4,130700 +130727,阳原县,4,130700 +130728,怀安县,4,130700 +130730,怀来县,4,130700 +130731,涿鹿县,4,130700 +130732,赤城县,4,130700 +130771,张家口经济开发区,4,130700 +130772,张家口市察北管理区,4,130700 +130773,张家口市塞北管理区,4,130700 +130802,双桥区,4,130800 +130803,双滦区,4,130800 +130804,鹰手营子矿区,4,130800 +130821,承德县,4,130800 +130822,兴隆县,4,130800 +130824,滦平县,4,130800 +130825,隆化县,4,130800 +130826,丰宁满族自治县,4,130800 +130827,宽城满族自治县,4,130800 +130828,围场满族蒙古族自治县,4,130800 +130871,承德高新技术产业开发区,4,130800 +130881,平泉市,4,130800 +130902,新华区,4,130900 +130903,运河区,4,130900 +130921,沧县,4,130900 +130922,青县,4,130900 +130923,东光县,4,130900 +130924,海兴县,4,130900 +130925,盐山县,4,130900 +130926,肃宁县,4,130900 +130927,南皮县,4,130900 +130928,吴桥县,4,130900 +130929,献县,4,130900 +130930,孟村回族自治县,4,130900 +130971,河北沧州经济开发区,4,130900 +130972,沧州高新技术产业开发区,4,130900 +130973,沧州渤海新区,4,130900 +130981,泊头市,4,130900 +130982,任丘市,4,130900 +130983,黄骅市,4,130900 +130984,河间市,4,130900 +131002,安次区,4,131000 +131003,广阳区,4,131000 +131022,固安县,4,131000 +131023,永清县,4,131000 +131024,香河县,4,131000 +131025,大城县,4,131000 +131026,文安县,4,131000 +131028,大厂回族自治县,4,131000 +131071,廊坊经济技术开发区,4,131000 +131081,霸州市,4,131000 +131082,三河市,4,131000 +131102,桃城区,4,131100 +131103,冀州区,4,131100 +131121,枣强县,4,131100 +131122,武邑县,4,131100 +131123,武强县,4,131100 +131124,饶阳县,4,131100 +131125,安平县,4,131100 +131126,故城县,4,131100 +131127,景县,4,131100 +131128,阜城县,4,131100 +131171,河北衡水高新技术产业开发区,4,131100 +131172,衡水滨湖新区,4,131100 +131182,深州市,4,131100 +140105,小店区,4,140100 +140106,迎泽区,4,140100 +140107,杏花岭区,4,140100 +140108,尖草坪区,4,140100 +140109,万柏林区,4,140100 +140110,晋源区,4,140100 +140121,清徐县,4,140100 +140122,阳曲县,4,140100 +140123,娄烦县,4,140100 +140171,山西转型综合改革示范区,4,140100 +140181,古交市,4,140100 +140212,新荣区,4,140200 +140213,平城区,4,140200 +140214,云冈区,4,140200 +140215,云州区,4,140200 +140221,阳高县,4,140200 +140222,天镇县,4,140200 +140223,广灵县,4,140200 +140224,灵丘县,4,140200 +140225,浑源县,4,140200 +140226,左云县,4,140200 +140271,山西大同经济开发区,4,140200 +140302,城区,4,140300 +140303,矿区,4,140300 +140311,郊区,4,140300 +140321,平定县,4,140300 +140322,盂县,4,140300 +140403,潞州区,4,140400 +140404,上党区,4,140400 +140405,屯留区,4,140400 +140406,潞城区,4,140400 +140423,襄垣县,4,140400 +140425,平顺县,4,140400 +140426,黎城县,4,140400 +140427,壶关县,4,140400 +140428,长子县,4,140400 +140429,武乡县,4,140400 +140430,沁县,4,140400 +140431,沁源县,4,140400 +140471,山西长治高新技术产业园区,4,140400 +140502,城区,4,140500 +140521,沁水县,4,140500 +140522,阳城县,4,140500 +140524,陵川县,4,140500 +140525,泽州县,4,140500 +140581,高平市,4,140500 +140602,朔城区,4,140600 +140603,平鲁区,4,140600 +140621,山阴县,4,140600 +140622,应县,4,140600 +140623,右玉县,4,140600 +140671,山西朔州经济开发区,4,140600 +140681,怀仁市,4,140600 +140702,榆次区,4,140700 +140703,太谷区,4,140700 +140721,榆社县,4,140700 +140722,左权县,4,140700 +140723,和顺县,4,140700 +140724,昔阳县,4,140700 +140725,寿阳县,4,140700 +140727,祁县,4,140700 +140728,平遥县,4,140700 +140729,灵石县,4,140700 +140781,介休市,4,140700 +140802,盐湖区,4,140800 +140821,临猗县,4,140800 +140822,万荣县,4,140800 +140823,闻喜县,4,140800 +140824,稷山县,4,140800 +140825,新绛县,4,140800 +140826,绛县,4,140800 +140827,垣曲县,4,140800 +140828,夏县,4,140800 +140829,平陆县,4,140800 +140830,芮城县,4,140800 +140881,永济市,4,140800 +140882,河津市,4,140800 +140902,忻府区,4,140900 +140921,定襄县,4,140900 +140922,五台县,4,140900 +140923,代县,4,140900 +140924,繁峙县,4,140900 +140925,宁武县,4,140900 +140926,静乐县,4,140900 +140927,神池县,4,140900 +140928,五寨县,4,140900 +140929,岢岚县,4,140900 +140930,河曲县,4,140900 +140931,保德县,4,140900 +140932,偏关县,4,140900 +140971,五台山风景名胜区,4,140900 +140981,原平市,4,140900 +141002,尧都区,4,141000 +141021,曲沃县,4,141000 +141022,翼城县,4,141000 +141023,襄汾县,4,141000 +141024,洪洞县,4,141000 +141025,古县,4,141000 +141026,安泽县,4,141000 +141027,浮山县,4,141000 +141028,吉县,4,141000 +141029,乡宁县,4,141000 +141030,大宁县,4,141000 +141031,隰县,4,141000 +141032,永和县,4,141000 +141033,蒲县,4,141000 +141034,汾西县,4,141000 +141081,侯马市,4,141000 +141082,霍州市,4,141000 +141102,离石区,4,141100 +141121,文水县,4,141100 +141122,交城县,4,141100 +141123,兴县,4,141100 +141124,临县,4,141100 +141125,柳林县,4,141100 +141126,石楼县,4,141100 +141127,岚县,4,141100 +141128,方山县,4,141100 +141129,中阳县,4,141100 +141130,交口县,4,141100 +141181,孝义市,4,141100 +141182,汾阳市,4,141100 +150102,新城区,4,150100 +150103,回民区,4,150100 +150104,玉泉区,4,150100 +150105,赛罕区,4,150100 +150121,土默特左旗,4,150100 +150122,托克托县,4,150100 +150123,和林格尔县,4,150100 +150124,清水河县,4,150100 +150125,武川县,4,150100 +150172,呼和浩特经济技术开发区,4,150100 +150202,东河区,4,150200 +150203,昆都仑区,4,150200 +150204,青山区,4,150200 +150205,石拐区,4,150200 +150206,白云鄂博矿区,4,150200 +150207,九原区,4,150200 +150221,土默特右旗,4,150200 +150222,固阳县,4,150200 +150223,达尔罕茂明安联合旗,4,150200 +150271,包头稀土高新技术产业开发区,4,150200 +150302,海勃湾区,4,150300 +150303,海南区,4,150300 +150304,乌达区,4,150300 +150402,红山区,4,150400 +150403,元宝山区,4,150400 +150404,松山区,4,150400 +150421,阿鲁科尔沁旗,4,150400 +150422,巴林左旗,4,150400 +150423,巴林右旗,4,150400 +150424,林西县,4,150400 +150425,克什克腾旗,4,150400 +150426,翁牛特旗,4,150400 +150428,喀喇沁旗,4,150400 +150429,宁城县,4,150400 +150430,敖汉旗,4,150400 +150502,科尔沁区,4,150500 +150521,科尔沁左翼中旗,4,150500 +150522,科尔沁左翼后旗,4,150500 +150523,开鲁县,4,150500 +150524,库伦旗,4,150500 +150525,奈曼旗,4,150500 +150526,扎鲁特旗,4,150500 +150571,通辽经济技术开发区,4,150500 +150581,霍林郭勒市,4,150500 +150602,东胜区,4,150600 +150603,康巴什区,4,150600 +150621,达拉特旗,4,150600 +150622,准格尔旗,4,150600 +150623,鄂托克前旗,4,150600 +150624,鄂托克旗,4,150600 +150625,杭锦旗,4,150600 +150626,乌审旗,4,150600 +150627,伊金霍洛旗,4,150600 +150702,海拉尔区,4,150700 +150703,扎赉诺尔区,4,150700 +150721,阿荣旗,4,150700 +150722,莫力达瓦达斡尔族自治旗,4,150700 +150723,鄂伦春自治旗,4,150700 +150724,鄂温克族自治旗,4,150700 +150725,陈巴尔虎旗,4,150700 +150726,新巴尔虎左旗,4,150700 +150727,新巴尔虎右旗,4,150700 +150781,满洲里市,4,150700 +150782,牙克石市,4,150700 +150783,扎兰屯市,4,150700 +150784,额尔古纳市,4,150700 +150785,根河市,4,150700 +150802,临河区,4,150800 +150821,五原县,4,150800 +150822,磴口县,4,150800 +150823,乌拉特前旗,4,150800 +150824,乌拉特中旗,4,150800 +150825,乌拉特后旗,4,150800 +150826,杭锦后旗,4,150800 +150902,集宁区,4,150900 +150921,卓资县,4,150900 +150922,化德县,4,150900 +150923,商都县,4,150900 +150924,兴和县,4,150900 +150925,凉城县,4,150900 +150926,察哈尔右翼前旗,4,150900 +150927,察哈尔右翼中旗,4,150900 +150928,察哈尔右翼后旗,4,150900 +150929,四子王旗,4,150900 +150981,丰镇市,4,150900 +152201,乌兰浩特市,4,152200 +152202,阿尔山市,4,152200 +152221,科尔沁右翼前旗,4,152200 +152222,科尔沁右翼中旗,4,152200 +152223,扎赉特旗,4,152200 +152224,突泉县,4,152200 +152501,二连浩特市,4,152500 +152502,锡林浩特市,4,152500 +152522,阿巴嘎旗,4,152500 +152523,苏尼特左旗,4,152500 +152524,苏尼特右旗,4,152500 +152525,东乌珠穆沁旗,4,152500 +152526,西乌珠穆沁旗,4,152500 +152527,太仆寺旗,4,152500 +152528,镶黄旗,4,152500 +152529,正镶白旗,4,152500 +152530,正蓝旗,4,152500 +152531,多伦县,4,152500 +152571,乌拉盖管委会,4,152500 +152921,阿拉善左旗,4,152900 +152922,阿拉善右旗,4,152900 +152923,额济纳旗,4,152900 +152971,内蒙古阿拉善高新技术产业开发区,4,152900 +210102,和平区,4,210100 +210103,沈河区,4,210100 +210104,大东区,4,210100 +210105,皇姑区,4,210100 +210106,铁西区,4,210100 +210111,苏家屯区,4,210100 +210112,浑南区,4,210100 +210113,沈北新区,4,210100 +210114,于洪区,4,210100 +210115,辽中区,4,210100 +210123,康平县,4,210100 +210124,法库县,4,210100 +210181,新民市,4,210100 +210202,中山区,4,210200 +210203,西岗区,4,210200 +210204,沙河口区,4,210200 +210211,甘井子区,4,210200 +210212,旅顺口区,4,210200 +210213,金州区,4,210200 +210214,普兰店区,4,210200 +210224,长海县,4,210200 +210281,瓦房店市,4,210200 +210283,庄河市,4,210200 +210302,铁东区,4,210300 +210303,铁西区,4,210300 +210304,立山区,4,210300 +210311,千山区,4,210300 +210321,台安县,4,210300 +210323,岫岩满族自治县,4,210300 +210381,海城市,4,210300 +210402,新抚区,4,210400 +210403,东洲区,4,210400 +210404,望花区,4,210400 +210411,顺城区,4,210400 +210421,抚顺县,4,210400 +210422,新宾满族自治县,4,210400 +210423,清原满族自治县,4,210400 +210502,平山区,4,210500 +210503,溪湖区,4,210500 +210504,明山区,4,210500 +210505,南芬区,4,210500 +210521,本溪满族自治县,4,210500 +210522,桓仁满族自治县,4,210500 +210602,元宝区,4,210600 +210603,振兴区,4,210600 +210604,振安区,4,210600 +210624,宽甸满族自治县,4,210600 +210681,东港市,4,210600 +210682,凤城市,4,210600 +210702,古塔区,4,210700 +210703,凌河区,4,210700 +210711,太和区,4,210700 +210726,黑山县,4,210700 +210727,义县,4,210700 +210781,凌海市,4,210700 +210782,北镇市,4,210700 +210802,站前区,4,210800 +210803,西市区,4,210800 +210804,鲅鱼圈区,4,210800 +210811,老边区,4,210800 +210881,盖州市,4,210800 +210882,大石桥市,4,210800 +210902,海州区,4,210900 +210903,新邱区,4,210900 +210904,太平区,4,210900 +210905,清河门区,4,210900 +210911,细河区,4,210900 +210921,阜新蒙古族自治县,4,210900 +210922,彰武县,4,210900 +211002,白塔区,4,211000 +211003,文圣区,4,211000 +211004,宏伟区,4,211000 +211005,弓长岭区,4,211000 +211011,太子河区,4,211000 +211021,辽阳县,4,211000 +211081,灯塔市,4,211000 +211102,双台子区,4,211100 +211103,兴隆台区,4,211100 +211104,大洼区,4,211100 +211122,盘山县,4,211100 +211202,银州区,4,211200 +211204,清河区,4,211200 +211221,铁岭县,4,211200 +211223,西丰县,4,211200 +211224,昌图县,4,211200 +211281,调兵山市,4,211200 +211282,开原市,4,211200 +211302,双塔区,4,211300 +211303,龙城区,4,211300 +211321,朝阳县,4,211300 +211322,建平县,4,211300 +211324,喀喇沁左翼蒙古族自治县,4,211300 +211381,北票市,4,211300 +211382,凌源市,4,211300 +211402,连山区,4,211400 +211403,龙港区,4,211400 +211404,南票区,4,211400 +211421,绥中县,4,211400 +211422,建昌县,4,211400 +211481,兴城市,4,211400 +220102,南关区,4,220100 +220103,宽城区,4,220100 +220104,朝阳区,4,220100 +220105,二道区,4,220100 +220106,绿园区,4,220100 +220112,双阳区,4,220100 +220113,九台区,4,220100 +220122,农安县,4,220100 +220171,长春经济技术开发区,4,220100 +220172,长春净月高新技术产业开发区,4,220100 +220173,长春高新技术产业开发区,4,220100 +220174,长春汽车经济技术开发区,4,220100 +220182,榆树市,4,220100 +220183,德惠市,4,220100 +220184,公主岭市,4,220100 +220202,昌邑区,4,220200 +220203,龙潭区,4,220200 +220204,船营区,4,220200 +220211,丰满区,4,220200 +220221,永吉县,4,220200 +220271,吉林经济开发区,4,220200 +220272,吉林高新技术产业开发区,4,220200 +220273,吉林中国新加坡食品区,4,220200 +220281,蛟河市,4,220200 +220282,桦甸市,4,220200 +220283,舒兰市,4,220200 +220284,磐石市,4,220200 +220302,铁西区,4,220300 +220303,铁东区,4,220300 +220322,梨树县,4,220300 +220323,伊通满族自治县,4,220300 +220382,双辽市,4,220300 +220402,龙山区,4,220400 +220403,西安区,4,220400 +220421,东丰县,4,220400 +220422,东辽县,4,220400 +220502,东昌区,4,220500 +220503,二道江区,4,220500 +220521,通化县,4,220500 +220523,辉南县,4,220500 +220524,柳河县,4,220500 +220581,梅河口市,4,220500 +220582,集安市,4,220500 +220602,浑江区,4,220600 +220605,江源区,4,220600 +220621,抚松县,4,220600 +220622,靖宇县,4,220600 +220623,长白朝鲜族自治县,4,220600 +220681,临江市,4,220600 +220702,宁江区,4,220700 +220721,前郭尔罗斯蒙古族自治县,4,220700 +220722,长岭县,4,220700 +220723,乾安县,4,220700 +220771,吉林松原经济开发区,4,220700 +220781,扶余市,4,220700 +220802,洮北区,4,220800 +220821,镇赉县,4,220800 +220822,通榆县,4,220800 +220871,吉林白城经济开发区,4,220800 +220881,洮南市,4,220800 +220882,大安市,4,220800 +222401,延吉市,4,222400 +222402,图们市,4,222400 +222403,敦化市,4,222400 +222404,珲春市,4,222400 +222405,龙井市,4,222400 +222406,和龙市,4,222400 +222424,汪清县,4,222400 +222426,安图县,4,222400 +230102,道里区,4,230100 +230103,南岗区,4,230100 +230104,道外区,4,230100 +230108,平房区,4,230100 +230109,松北区,4,230100 +230110,香坊区,4,230100 +230111,呼兰区,4,230100 +230112,阿城区,4,230100 +230113,双城区,4,230100 +230123,依兰县,4,230100 +230124,方正县,4,230100 +230125,宾县,4,230100 +230126,巴彦县,4,230100 +230127,木兰县,4,230100 +230128,通河县,4,230100 +230129,延寿县,4,230100 +230183,尚志市,4,230100 +230184,五常市,4,230100 +230202,龙沙区,4,230200 +230203,建华区,4,230200 +230204,铁锋区,4,230200 +230205,昂昂溪区,4,230200 +230206,富拉尔基区,4,230200 +230207,碾子山区,4,230200 +230208,梅里斯达斡尔族区,4,230200 +230221,龙江县,4,230200 +230223,依安县,4,230200 +230224,泰来县,4,230200 +230225,甘南县,4,230200 +230227,富裕县,4,230200 +230229,克山县,4,230200 +230230,克东县,4,230200 +230231,拜泉县,4,230200 +230281,讷河市,4,230200 +230302,鸡冠区,4,230300 +230303,恒山区,4,230300 +230304,滴道区,4,230300 +230305,梨树区,4,230300 +230306,城子河区,4,230300 +230307,麻山区,4,230300 +230321,鸡东县,4,230300 +230381,虎林市,4,230300 +230382,密山市,4,230300 +230402,向阳区,4,230400 +230403,工农区,4,230400 +230404,南山区,4,230400 +230405,兴安区,4,230400 +230406,东山区,4,230400 +230407,兴山区,4,230400 +230421,萝北县,4,230400 +230422,绥滨县,4,230400 +230502,尖山区,4,230500 +230503,岭东区,4,230500 +230505,四方台区,4,230500 +230506,宝山区,4,230500 +230521,集贤县,4,230500 +230522,友谊县,4,230500 +230523,宝清县,4,230500 +230524,饶河县,4,230500 +230602,萨尔图区,4,230600 +230603,龙凤区,4,230600 +230604,让胡路区,4,230600 +230605,红岗区,4,230600 +230606,大同区,4,230600 +230621,肇州县,4,230600 +230622,肇源县,4,230600 +230623,林甸县,4,230600 +230624,杜尔伯特蒙古族自治县,4,230600 +230671,大庆高新技术产业开发区,4,230600 +230717,伊美区,4,230700 +230718,乌翠区,4,230700 +230719,友好区,4,230700 +230722,嘉荫县,4,230700 +230723,汤旺县,4,230700 +230724,丰林县,4,230700 +230725,大箐山县,4,230700 +230726,南岔县,4,230700 +230751,金林区,4,230700 +230781,铁力市,4,230700 +230803,向阳区,4,230800 +230804,前进区,4,230800 +230805,东风区,4,230800 +230811,郊区,4,230800 +230822,桦南县,4,230800 +230826,桦川县,4,230800 +230828,汤原县,4,230800 +230881,同江市,4,230800 +230882,富锦市,4,230800 +230883,抚远市,4,230800 +230902,新兴区,4,230900 +230903,桃山区,4,230900 +230904,茄子河区,4,230900 +230921,勃利县,4,230900 +231002,东安区,4,231000 +231003,阳明区,4,231000 +231004,爱民区,4,231000 +231005,西安区,4,231000 +231025,林口县,4,231000 +231071,牡丹江经济技术开发区,4,231000 +231081,绥芬河市,4,231000 +231083,海林市,4,231000 +231084,宁安市,4,231000 +231085,穆棱市,4,231000 +231086,东宁市,4,231000 +231102,爱辉区,4,231100 +231123,逊克县,4,231100 +231124,孙吴县,4,231100 +231181,北安市,4,231100 +231182,五大连池市,4,231100 +231183,嫩江市,4,231100 +231202,北林区,4,231200 +231221,望奎县,4,231200 +231222,兰西县,4,231200 +231223,青冈县,4,231200 +231224,庆安县,4,231200 +231225,明水县,4,231200 +231226,绥棱县,4,231200 +231281,安达市,4,231200 +231282,肇东市,4,231200 +231283,海伦市,4,231200 +232701,漠河市,4,232700 +232721,呼玛县,4,232700 +232722,塔河县,4,232700 +232761,加格达奇区,4,232700 +232762,松岭区,4,232700 +232763,新林区,4,232700 +232764,呼中区,4,232700 +310101,黄浦区,4,310100 +310104,徐汇区,4,310100 +310105,长宁区,4,310100 +310106,静安区,4,310100 +310107,普陀区,4,310100 +310109,虹口区,4,310100 +310110,杨浦区,4,310100 +310112,闵行区,4,310100 +310113,宝山区,4,310100 +310114,嘉定区,4,310100 +310115,浦东新区,4,310100 +310116,金山区,4,310100 +310117,松江区,4,310100 +310118,青浦区,4,310100 +310120,奉贤区,4,310100 +310151,崇明区,4,310100 +320102,玄武区,4,320100 +320104,秦淮区,4,320100 +320105,建邺区,4,320100 +320106,鼓楼区,4,320100 +320111,浦口区,4,320100 +320113,栖霞区,4,320100 +320114,雨花台区,4,320100 +320115,江宁区,4,320100 +320116,六合区,4,320100 +320117,溧水区,4,320100 +320118,高淳区,4,320100 +320205,锡山区,4,320200 +320206,惠山区,4,320200 +320211,滨湖区,4,320200 +320213,梁溪区,4,320200 +320214,新吴区,4,320200 +320281,江阴市,4,320200 +320282,宜兴市,4,320200 +320302,鼓楼区,4,320300 +320303,云龙区,4,320300 +320305,贾汪区,4,320300 +320311,泉山区,4,320300 +320312,铜山区,4,320300 +320321,丰县,4,320300 +320322,沛县,4,320300 +320324,睢宁县,4,320300 +320371,徐州经济技术开发区,4,320300 +320381,新沂市,4,320300 +320382,邳州市,4,320300 +320402,天宁区,4,320400 +320404,钟楼区,4,320400 +320411,新北区,4,320400 +320412,武进区,4,320400 +320413,金坛区,4,320400 +320481,溧阳市,4,320400 +320505,虎丘区,4,320500 +320506,吴中区,4,320500 +320507,相城区,4,320500 +320508,姑苏区,4,320500 +320509,吴江区,4,320500 +320571,苏州工业园区,4,320500 +320581,常熟市,4,320500 +320582,张家港市,4,320500 +320583,昆山市,4,320500 +320585,太仓市,4,320500 +320612,通州区,4,320600 +320613,崇川区,4,320600 +320614,海门区,4,320600 +320623,如东县,4,320600 +320671,南通经济技术开发区,4,320600 +320681,启东市,4,320600 +320682,如皋市,4,320600 +320685,海安市,4,320600 +320703,连云区,4,320700 +320706,海州区,4,320700 +320707,赣榆区,4,320700 +320722,东海县,4,320700 +320723,灌云县,4,320700 +320724,灌南县,4,320700 +320771,连云港经济技术开发区,4,320700 +320772,连云港高新技术产业开发区,4,320700 +320803,淮安区,4,320800 +320804,淮阴区,4,320800 +320812,清江浦区,4,320800 +320813,洪泽区,4,320800 +320826,涟水县,4,320800 +320830,盱眙县,4,320800 +320831,金湖县,4,320800 +320871,淮安经济技术开发区,4,320800 +320902,亭湖区,4,320900 +320903,盐都区,4,320900 +320904,大丰区,4,320900 +320921,响水县,4,320900 +320922,滨海县,4,320900 +320923,阜宁县,4,320900 +320924,射阳县,4,320900 +320925,建湖县,4,320900 +320971,盐城经济技术开发区,4,320900 +320981,东台市,4,320900 +321002,广陵区,4,321000 +321003,邗江区,4,321000 +321012,江都区,4,321000 +321023,宝应县,4,321000 +321071,扬州经济技术开发区,4,321000 +321081,仪征市,4,321000 +321084,高邮市,4,321000 +321102,京口区,4,321100 +321111,润州区,4,321100 +321112,丹徒区,4,321100 +321171,镇江新区,4,321100 +321181,丹阳市,4,321100 +321182,扬中市,4,321100 +321183,句容市,4,321100 +321202,海陵区,4,321200 +321203,高港区,4,321200 +321204,姜堰区,4,321200 +321271,泰州医药高新技术产业开发区,4,321200 +321281,兴化市,4,321200 +321282,靖江市,4,321200 +321283,泰兴市,4,321200 +321302,宿城区,4,321300 +321311,宿豫区,4,321300 +321322,沭阳县,4,321300 +321323,泗阳县,4,321300 +321324,泗洪县,4,321300 +321371,宿迁经济技术开发区,4,321300 +330102,上城区,4,330100 +330105,拱墅区,4,330100 +330106,西湖区,4,330100 +330108,滨江区,4,330100 +330109,萧山区,4,330100 +330110,余杭区,4,330100 +330111,富阳区,4,330100 +330112,临安区,4,330100 +330113,临平区,4,330100 +330114,钱塘区,4,330100 +330122,桐庐县,4,330100 +330127,淳安县,4,330100 +330182,建德市,4,330100 +330203,海曙区,4,330200 +330205,江北区,4,330200 +330206,北仑区,4,330200 +330211,镇海区,4,330200 +330212,鄞州区,4,330200 +330213,奉化区,4,330200 +330225,象山县,4,330200 +330226,宁海县,4,330200 +330281,余姚市,4,330200 +330282,慈溪市,4,330200 +330302,鹿城区,4,330300 +330303,龙湾区,4,330300 +330304,瓯海区,4,330300 +330305,洞头区,4,330300 +330324,永嘉县,4,330300 +330326,平阳县,4,330300 +330327,苍南县,4,330300 +330328,文成县,4,330300 +330329,泰顺县,4,330300 +330371,温州经济技术开发区,4,330300 +330381,瑞安市,4,330300 +330382,乐清市,4,330300 +330383,龙港市,4,330300 +330402,南湖区,4,330400 +330411,秀洲区,4,330400 +330421,嘉善县,4,330400 +330424,海盐县,4,330400 +330481,海宁市,4,330400 +330482,平湖市,4,330400 +330483,桐乡市,4,330400 +330502,吴兴区,4,330500 +330503,南浔区,4,330500 +330521,德清县,4,330500 +330522,长兴县,4,330500 +330523,安吉县,4,330500 +330602,越城区,4,330600 +330603,柯桥区,4,330600 +330604,上虞区,4,330600 +330624,新昌县,4,330600 +330681,诸暨市,4,330600 +330683,嵊州市,4,330600 +330702,婺城区,4,330700 +330703,金东区,4,330700 +330723,武义县,4,330700 +330726,浦江县,4,330700 +330727,磐安县,4,330700 +330781,兰溪市,4,330700 +330782,义乌市,4,330700 +330783,东阳市,4,330700 +330784,永康市,4,330700 +330802,柯城区,4,330800 +330803,衢江区,4,330800 +330822,常山县,4,330800 +330824,开化县,4,330800 +330825,龙游县,4,330800 +330881,江山市,4,330800 +330902,定海区,4,330900 +330903,普陀区,4,330900 +330921,岱山县,4,330900 +330922,嵊泗县,4,330900 +331002,椒江区,4,331000 +331003,黄岩区,4,331000 +331004,路桥区,4,331000 +331022,三门县,4,331000 +331023,天台县,4,331000 +331024,仙居县,4,331000 +331081,温岭市,4,331000 +331082,临海市,4,331000 +331083,玉环市,4,331000 +331102,莲都区,4,331100 +331121,青田县,4,331100 +331122,缙云县,4,331100 +331123,遂昌县,4,331100 +331124,松阳县,4,331100 +331125,云和县,4,331100 +331126,庆元县,4,331100 +331127,景宁畲族自治县,4,331100 +331181,龙泉市,4,331100 +340102,瑶海区,4,340100 +340103,庐阳区,4,340100 +340104,蜀山区,4,340100 +340111,包河区,4,340100 +340121,长丰县,4,340100 +340122,肥东县,4,340100 +340123,肥西县,4,340100 +340124,庐江县,4,340100 +340171,合肥高新技术产业开发区,4,340100 +340172,合肥经济技术开发区,4,340100 +340173,合肥新站高新技术产业开发区,4,340100 +340181,巢湖市,4,340100 +340202,镜湖区,4,340200 +340207,鸠江区,4,340200 +340209,弋江区,4,340200 +340210,湾沚区,4,340200 +340212,繁昌区,4,340200 +340223,南陵县,4,340200 +340271,芜湖经济技术开发区,4,340200 +340272,安徽芜湖三山经济开发区,4,340200 +340281,无为市,4,340200 +340302,龙子湖区,4,340300 +340303,蚌山区,4,340300 +340304,禹会区,4,340300 +340311,淮上区,4,340300 +340321,怀远县,4,340300 +340322,五河县,4,340300 +340323,固镇县,4,340300 +340371,蚌埠市高新技术开发区,4,340300 +340372,蚌埠市经济开发区,4,340300 +340402,大通区,4,340400 +340403,田家庵区,4,340400 +340404,谢家集区,4,340400 +340405,八公山区,4,340400 +340406,潘集区,4,340400 +340421,凤台县,4,340400 +340422,寿县,4,340400 +340503,花山区,4,340500 +340504,雨山区,4,340500 +340506,博望区,4,340500 +340521,当涂县,4,340500 +340522,含山县,4,340500 +340523,和县,4,340500 +340602,杜集区,4,340600 +340603,相山区,4,340600 +340604,烈山区,4,340600 +340621,濉溪县,4,340600 +340705,铜官区,4,340700 +340706,义安区,4,340700 +340711,郊区,4,340700 +340722,枞阳县,4,340700 +340802,迎江区,4,340800 +340803,大观区,4,340800 +340811,宜秀区,4,340800 +340822,怀宁县,4,340800 +340825,太湖县,4,340800 +340826,宿松县,4,340800 +340827,望江县,4,340800 +340828,岳西县,4,340800 +340871,安徽安庆经济开发区,4,340800 +340881,桐城市,4,340800 +340882,潜山市,4,340800 +341002,屯溪区,4,341000 +341003,黄山区,4,341000 +341004,徽州区,4,341000 +341021,歙县,4,341000 +341022,休宁县,4,341000 +341023,黟县,4,341000 +341024,祁门县,4,341000 +341102,琅琊区,4,341100 +341103,南谯区,4,341100 +341122,来安县,4,341100 +341124,全椒县,4,341100 +341125,定远县,4,341100 +341126,凤阳县,4,341100 +341171,中新苏滁高新技术产业开发区,4,341100 +341172,滁州经济技术开发区,4,341100 +341181,天长市,4,341100 +341182,明光市,4,341100 +341202,颍州区,4,341200 +341203,颍东区,4,341200 +341204,颍泉区,4,341200 +341221,临泉县,4,341200 +341222,太和县,4,341200 +341225,阜南县,4,341200 +341226,颍上县,4,341200 +341271,阜阳合肥现代产业园区,4,341200 +341272,阜阳经济技术开发区,4,341200 +341282,界首市,4,341200 +341302,埇桥区,4,341300 +341321,砀山县,4,341300 +341322,萧县,4,341300 +341323,灵璧县,4,341300 +341324,泗县,4,341300 +341371,宿州马鞍山现代产业园区,4,341300 +341372,宿州经济技术开发区,4,341300 +341502,金安区,4,341500 +341503,裕安区,4,341500 +341504,叶集区,4,341500 +341522,霍邱县,4,341500 +341523,舒城县,4,341500 +341524,金寨县,4,341500 +341525,霍山县,4,341500 +341602,谯城区,4,341600 +341621,涡阳县,4,341600 +341622,蒙城县,4,341600 +341623,利辛县,4,341600 +341702,贵池区,4,341700 +341721,东至县,4,341700 +341722,石台县,4,341700 +341723,青阳县,4,341700 +341802,宣州区,4,341800 +341821,郎溪县,4,341800 +341823,泾县,4,341800 +341824,绩溪县,4,341800 +341825,旌德县,4,341800 +341871,宣城市经济开发区,4,341800 +341881,宁国市,4,341800 +341882,广德市,4,341800 +350102,鼓楼区,4,350100 +350103,台江区,4,350100 +350104,仓山区,4,350100 +350105,马尾区,4,350100 +350111,晋安区,4,350100 +350112,长乐区,4,350100 +350121,闽侯县,4,350100 +350122,连江县,4,350100 +350123,罗源县,4,350100 +350124,闽清县,4,350100 +350125,永泰县,4,350100 +350128,平潭县,4,350100 +350181,福清市,4,350100 +350203,思明区,4,350200 +350205,海沧区,4,350200 +350206,湖里区,4,350200 +350211,集美区,4,350200 +350212,同安区,4,350200 +350213,翔安区,4,350200 +350302,城厢区,4,350300 +350303,涵江区,4,350300 +350304,荔城区,4,350300 +350305,秀屿区,4,350300 +350322,仙游县,4,350300 +350404,三元区,4,350400 +350405,沙县区,4,350400 +350421,明溪县,4,350400 +350423,清流县,4,350400 +350424,宁化县,4,350400 +350425,大田县,4,350400 +350426,尤溪县,4,350400 +350428,将乐县,4,350400 +350429,泰宁县,4,350400 +350430,建宁县,4,350400 +350481,永安市,4,350400 +350502,鲤城区,4,350500 +350503,丰泽区,4,350500 +350504,洛江区,4,350500 +350505,泉港区,4,350500 +350521,惠安县,4,350500 +350524,安溪县,4,350500 +350525,永春县,4,350500 +350526,德化县,4,350500 +350527,金门县,4,350500 +350581,石狮市,4,350500 +350582,晋江市,4,350500 +350583,南安市,4,350500 +350602,芗城区,4,350600 +350603,龙文区,4,350600 +350604,龙海区,4,350600 +350605,长泰区,4,350600 +350622,云霄县,4,350600 +350623,漳浦县,4,350600 +350624,诏安县,4,350600 +350626,东山县,4,350600 +350627,南靖县,4,350600 +350628,平和县,4,350600 +350629,华安县,4,350600 +350702,延平区,4,350700 +350703,建阳区,4,350700 +350721,顺昌县,4,350700 +350722,浦城县,4,350700 +350723,光泽县,4,350700 +350724,松溪县,4,350700 +350725,政和县,4,350700 +350781,邵武市,4,350700 +350782,武夷山市,4,350700 +350783,建瓯市,4,350700 +350802,新罗区,4,350800 +350803,永定区,4,350800 +350821,长汀县,4,350800 +350823,上杭县,4,350800 +350824,武平县,4,350800 +350825,连城县,4,350800 +350881,漳平市,4,350800 +350902,蕉城区,4,350900 +350921,霞浦县,4,350900 +350922,古田县,4,350900 +350923,屏南县,4,350900 +350924,寿宁县,4,350900 +350925,周宁县,4,350900 +350926,柘荣县,4,350900 +350981,福安市,4,350900 +350982,福鼎市,4,350900 +360102,东湖区,4,360100 +360103,西湖区,4,360100 +360104,青云谱区,4,360100 +360111,青山湖区,4,360100 +360112,新建区,4,360100 +360113,红谷滩区,4,360100 +360121,南昌县,4,360100 +360123,安义县,4,360100 +360124,进贤县,4,360100 +360202,昌江区,4,360200 +360203,珠山区,4,360200 +360222,浮梁县,4,360200 +360281,乐平市,4,360200 +360302,安源区,4,360300 +360313,湘东区,4,360300 +360321,莲花县,4,360300 +360322,上栗县,4,360300 +360323,芦溪县,4,360300 +360402,濂溪区,4,360400 +360403,浔阳区,4,360400 +360404,柴桑区,4,360400 +360423,武宁县,4,360400 +360424,修水县,4,360400 +360425,永修县,4,360400 +360426,德安县,4,360400 +360428,都昌县,4,360400 +360429,湖口县,4,360400 +360430,彭泽县,4,360400 +360481,瑞昌市,4,360400 +360482,共青城市,4,360400 +360483,庐山市,4,360400 +360502,渝水区,4,360500 +360521,分宜县,4,360500 +360602,月湖区,4,360600 +360603,余江区,4,360600 +360681,贵溪市,4,360600 +360702,章贡区,4,360700 +360703,南康区,4,360700 +360704,赣县区,4,360700 +360722,信丰县,4,360700 +360723,大余县,4,360700 +360724,上犹县,4,360700 +360725,崇义县,4,360700 +360726,安远县,4,360700 +360728,定南县,4,360700 +360729,全南县,4,360700 +360730,宁都县,4,360700 +360731,于都县,4,360700 +360732,兴国县,4,360700 +360733,会昌县,4,360700 +360734,寻乌县,4,360700 +360735,石城县,4,360700 +360781,瑞金市,4,360700 +360783,龙南市,4,360700 +360802,吉州区,4,360800 +360803,青原区,4,360800 +360821,吉安县,4,360800 +360822,吉水县,4,360800 +360823,峡江县,4,360800 +360824,新干县,4,360800 +360825,永丰县,4,360800 +360826,泰和县,4,360800 +360827,遂川县,4,360800 +360828,万安县,4,360800 +360829,安福县,4,360800 +360830,永新县,4,360800 +360881,井冈山市,4,360800 +360902,袁州区,4,360900 +360921,奉新县,4,360900 +360922,万载县,4,360900 +360923,上高县,4,360900 +360924,宜丰县,4,360900 +360925,靖安县,4,360900 +360926,铜鼓县,4,360900 +360981,丰城市,4,360900 +360982,樟树市,4,360900 +360983,高安市,4,360900 +361002,临川区,4,361000 +361003,东乡区,4,361000 +361021,南城县,4,361000 +361022,黎川县,4,361000 +361023,南丰县,4,361000 +361024,崇仁县,4,361000 +361025,乐安县,4,361000 +361026,宜黄县,4,361000 +361027,金溪县,4,361000 +361028,资溪县,4,361000 +361030,广昌县,4,361000 +361102,信州区,4,361100 +361103,广丰区,4,361100 +361104,广信区,4,361100 +361123,玉山县,4,361100 +361124,铅山县,4,361100 +361125,横峰县,4,361100 +361126,弋阳县,4,361100 +361127,余干县,4,361100 +361128,鄱阳县,4,361100 +361129,万年县,4,361100 +361130,婺源县,4,361100 +361181,德兴市,4,361100 +370102,历下区,4,370100 +370103,市中区,4,370100 +370104,槐荫区,4,370100 +370105,天桥区,4,370100 +370112,历城区,4,370100 +370113,长清区,4,370100 +370114,章丘区,4,370100 +370115,济阳区,4,370100 +370116,莱芜区,4,370100 +370117,钢城区,4,370100 +370124,平阴县,4,370100 +370126,商河县,4,370100 +370171,济南高新技术产业开发区,4,370100 +370202,市南区,4,370200 +370203,市北区,4,370200 +370211,黄岛区,4,370200 +370212,崂山区,4,370200 +370213,李沧区,4,370200 +370214,城阳区,4,370200 +370215,即墨区,4,370200 +370271,青岛高新技术产业开发区,4,370200 +370281,胶州市,4,370200 +370283,平度市,4,370200 +370285,莱西市,4,370200 +370302,淄川区,4,370300 +370303,张店区,4,370300 +370304,博山区,4,370300 +370305,临淄区,4,370300 +370306,周村区,4,370300 +370321,桓台县,4,370300 +370322,高青县,4,370300 +370323,沂源县,4,370300 +370402,市中区,4,370400 +370403,薛城区,4,370400 +370404,峄城区,4,370400 +370405,台儿庄区,4,370400 +370406,山亭区,4,370400 +370481,滕州市,4,370400 +370502,东营区,4,370500 +370503,河口区,4,370500 +370505,垦利区,4,370500 +370522,利津县,4,370500 +370523,广饶县,4,370500 +370571,东营经济技术开发区,4,370500 +370572,东营港经济开发区,4,370500 +370602,芝罘区,4,370600 +370611,福山区,4,370600 +370612,牟平区,4,370600 +370613,莱山区,4,370600 +370614,蓬莱区,4,370600 +370671,烟台高新技术产业开发区,4,370600 +370672,烟台经济技术开发区,4,370600 +370681,龙口市,4,370600 +370682,莱阳市,4,370600 +370683,莱州市,4,370600 +370685,招远市,4,370600 +370686,栖霞市,4,370600 +370687,海阳市,4,370600 +370702,潍城区,4,370700 +370703,寒亭区,4,370700 +370704,坊子区,4,370700 +370705,奎文区,4,370700 +370724,临朐县,4,370700 +370725,昌乐县,4,370700 +370772,潍坊滨海经济技术开发区,4,370700 +370781,青州市,4,370700 +370782,诸城市,4,370700 +370783,寿光市,4,370700 +370784,安丘市,4,370700 +370785,高密市,4,370700 +370786,昌邑市,4,370700 +370811,任城区,4,370800 +370812,兖州区,4,370800 +370826,微山县,4,370800 +370827,鱼台县,4,370800 +370828,金乡县,4,370800 +370829,嘉祥县,4,370800 +370830,汶上县,4,370800 +370831,泗水县,4,370800 +370832,梁山县,4,370800 +370871,济宁高新技术产业开发区,4,370800 +370881,曲阜市,4,370800 +370883,邹城市,4,370800 +370902,泰山区,4,370900 +370911,岱岳区,4,370900 +370921,宁阳县,4,370900 +370923,东平县,4,370900 +370982,新泰市,4,370900 +370983,肥城市,4,370900 +371002,环翠区,4,371000 +371003,文登区,4,371000 +371071,威海火炬高技术产业开发区,4,371000 +371072,威海经济技术开发区,4,371000 +371073,威海临港经济技术开发区,4,371000 +371082,荣成市,4,371000 +371083,乳山市,4,371000 +371102,东港区,4,371100 +371103,岚山区,4,371100 +371121,五莲县,4,371100 +371122,莒县,4,371100 +371171,日照经济技术开发区,4,371100 +371302,兰山区,4,371300 +371311,罗庄区,4,371300 +371312,河东区,4,371300 +371321,沂南县,4,371300 +371322,郯城县,4,371300 +371323,沂水县,4,371300 +371324,兰陵县,4,371300 +371325,费县,4,371300 +371326,平邑县,4,371300 +371327,莒南县,4,371300 +371328,蒙阴县,4,371300 +371329,临沭县,4,371300 +371371,临沂高新技术产业开发区,4,371300 +371402,德城区,4,371400 +371403,陵城区,4,371400 +371422,宁津县,4,371400 +371423,庆云县,4,371400 +371424,临邑县,4,371400 +371425,齐河县,4,371400 +371426,平原县,4,371400 +371427,夏津县,4,371400 +371428,武城县,4,371400 +371471,德州经济技术开发区,4,371400 +371472,德州运河经济开发区,4,371400 +371481,乐陵市,4,371400 +371482,禹城市,4,371400 +371502,东昌府区,4,371500 +371503,茌平区,4,371500 +371521,阳谷县,4,371500 +371522,莘县,4,371500 +371524,东阿县,4,371500 +371525,冠县,4,371500 +371526,高唐县,4,371500 +371581,临清市,4,371500 +371602,滨城区,4,371600 +371603,沾化区,4,371600 +371621,惠民县,4,371600 +371622,阳信县,4,371600 +371623,无棣县,4,371600 +371625,博兴县,4,371600 +371681,邹平市,4,371600 +371702,牡丹区,4,371700 +371703,定陶区,4,371700 +371721,曹县,4,371700 +371722,单县,4,371700 +371723,成武县,4,371700 +371724,巨野县,4,371700 +371725,郓城县,4,371700 +371726,鄄城县,4,371700 +371728,东明县,4,371700 +371771,菏泽经济技术开发区,4,371700 +371772,菏泽高新技术开发区,4,371700 +410102,中原区,4,410100 +410103,二七区,4,410100 +410104,管城回族区,4,410100 +410105,金水区,4,410100 +410106,上街区,4,410100 +410108,惠济区,4,410100 +410122,中牟县,4,410100 +410171,郑州经济技术开发区,4,410100 +410172,郑州高新技术产业开发区,4,410100 +410173,郑州航空港经济综合实验区,4,410100 +410181,巩义市,4,410100 +410182,荥阳市,4,410100 +410183,新密市,4,410100 +410184,新郑市,4,410100 +410185,登封市,4,410100 +410202,龙亭区,4,410200 +410203,顺河回族区,4,410200 +410204,鼓楼区,4,410200 +410205,禹王台区,4,410200 +410212,祥符区,4,410200 +410221,杞县,4,410200 +410222,通许县,4,410200 +410223,尉氏县,4,410200 +410225,兰考县,4,410200 +410302,老城区,4,410300 +410303,西工区,4,410300 +410304,瀍河回族区,4,410300 +410305,涧西区,4,410300 +410307,偃师区,4,410300 +410308,孟津区,4,410300 +410311,洛龙区,4,410300 +410323,新安县,4,410300 +410324,栾川县,4,410300 +410325,嵩县,4,410300 +410326,汝阳县,4,410300 +410327,宜阳县,4,410300 +410328,洛宁县,4,410300 +410329,伊川县,4,410300 +410371,洛阳高新技术产业开发区,4,410300 +410402,新华区,4,410400 +410403,卫东区,4,410400 +410404,石龙区,4,410400 +410411,湛河区,4,410400 +410421,宝丰县,4,410400 +410422,叶县,4,410400 +410423,鲁山县,4,410400 +410425,郏县,4,410400 +410471,平顶山高新技术产业开发区,4,410400 +410472,平顶山市城乡一体化示范区,4,410400 +410481,舞钢市,4,410400 +410482,汝州市,4,410400 +410502,文峰区,4,410500 +410503,北关区,4,410500 +410505,殷都区,4,410500 +410506,龙安区,4,410500 +410522,安阳县,4,410500 +410523,汤阴县,4,410500 +410526,滑县,4,410500 +410527,内黄县,4,410500 +410571,安阳高新技术产业开发区,4,410500 +410581,林州市,4,410500 +410602,鹤山区,4,410600 +410603,山城区,4,410600 +410611,淇滨区,4,410600 +410621,浚县,4,410600 +410622,淇县,4,410600 +410671,鹤壁经济技术开发区,4,410600 +410702,红旗区,4,410700 +410703,卫滨区,4,410700 +410704,凤泉区,4,410700 +410711,牧野区,4,410700 +410721,新乡县,4,410700 +410724,获嘉县,4,410700 +410725,原阳县,4,410700 +410726,延津县,4,410700 +410727,封丘县,4,410700 +410771,新乡高新技术产业开发区,4,410700 +410772,新乡经济技术开发区,4,410700 +410773,新乡市平原城乡一体化示范区,4,410700 +410781,卫辉市,4,410700 +410782,辉县市,4,410700 +410783,长垣市,4,410700 +410802,解放区,4,410800 +410803,中站区,4,410800 +410804,马村区,4,410800 +410811,山阳区,4,410800 +410821,修武县,4,410800 +410822,博爱县,4,410800 +410823,武陟县,4,410800 +410825,温县,4,410800 +410871,焦作城乡一体化示范区,4,410800 +410882,沁阳市,4,410800 +410883,孟州市,4,410800 +410902,华龙区,4,410900 +410922,清丰县,4,410900 +410923,南乐县,4,410900 +410926,范县,4,410900 +410927,台前县,4,410900 +410928,濮阳县,4,410900 +410971,河南濮阳工业园区,4,410900 +410972,濮阳经济技术开发区,4,410900 +411002,魏都区,4,411000 +411003,建安区,4,411000 +411024,鄢陵县,4,411000 +411025,襄城县,4,411000 +411071,许昌经济技术开发区,4,411000 +411081,禹州市,4,411000 +411082,长葛市,4,411000 +411102,源汇区,4,411100 +411103,郾城区,4,411100 +411104,召陵区,4,411100 +411121,舞阳县,4,411100 +411122,临颍县,4,411100 +411171,漯河经济技术开发区,4,411100 +411202,湖滨区,4,411200 +411203,陕州区,4,411200 +411221,渑池县,4,411200 +411224,卢氏县,4,411200 +411271,河南三门峡经济开发区,4,411200 +411281,义马市,4,411200 +411282,灵宝市,4,411200 +411302,宛城区,4,411300 +411303,卧龙区,4,411300 +411321,南召县,4,411300 +411322,方城县,4,411300 +411323,西峡县,4,411300 +411324,镇平县,4,411300 +411325,内乡县,4,411300 +411326,淅川县,4,411300 +411327,社旗县,4,411300 +411328,唐河县,4,411300 +411329,新野县,4,411300 +411330,桐柏县,4,411300 +411371,南阳高新技术产业开发区,4,411300 +411372,南阳市城乡一体化示范区,4,411300 +411381,邓州市,4,411300 +411402,梁园区,4,411400 +411403,睢阳区,4,411400 +411421,民权县,4,411400 +411422,睢县,4,411400 +411423,宁陵县,4,411400 +411424,柘城县,4,411400 +411425,虞城县,4,411400 +411426,夏邑县,4,411400 +411471,豫东综合物流产业聚集区,4,411400 +411472,河南商丘经济开发区,4,411400 +411481,永城市,4,411400 +411502,浉河区,4,411500 +411503,平桥区,4,411500 +411521,罗山县,4,411500 +411522,光山县,4,411500 +411523,新县,4,411500 +411524,商城县,4,411500 +411525,固始县,4,411500 +411526,潢川县,4,411500 +411527,淮滨县,4,411500 +411528,息县,4,411500 +411571,信阳高新技术产业开发区,4,411500 +411602,川汇区,4,411600 +411603,淮阳区,4,411600 +411621,扶沟县,4,411600 +411622,西华县,4,411600 +411623,商水县,4,411600 +411624,沈丘县,4,411600 +411625,郸城县,4,411600 +411627,太康县,4,411600 +411628,鹿邑县,4,411600 +411671,河南周口经济开发区,4,411600 +411681,项城市,4,411600 +411702,驿城区,4,411700 +411721,西平县,4,411700 +411722,上蔡县,4,411700 +411723,平舆县,4,411700 +411724,正阳县,4,411700 +411725,确山县,4,411700 +411726,泌阳县,4,411700 +411727,汝南县,4,411700 +411728,遂平县,4,411700 +411729,新蔡县,4,411700 +411771,河南驻马店经济开发区,4,411700 +419001,济源市,4,419000 +420102,江岸区,4,420100 +420103,江汉区,4,420100 +420104,硚口区,4,420100 +420105,汉阳区,4,420100 +420106,武昌区,4,420100 +420107,青山区,4,420100 +420111,洪山区,4,420100 +420112,东西湖区,4,420100 +420113,汉南区,4,420100 +420114,蔡甸区,4,420100 +420115,江夏区,4,420100 +420116,黄陂区,4,420100 +420117,新洲区,4,420100 +420202,黄石港区,4,420200 +420203,西塞山区,4,420200 +420204,下陆区,4,420200 +420205,铁山区,4,420200 +420222,阳新县,4,420200 +420281,大冶市,4,420200 +420302,茅箭区,4,420300 +420303,张湾区,4,420300 +420304,郧阳区,4,420300 +420322,郧西县,4,420300 +420323,竹山县,4,420300 +420324,竹溪县,4,420300 +420325,房县,4,420300 +420381,丹江口市,4,420300 +420502,西陵区,4,420500 +420503,伍家岗区,4,420500 +420504,点军区,4,420500 +420505,猇亭区,4,420500 +420506,夷陵区,4,420500 +420525,远安县,4,420500 +420526,兴山县,4,420500 +420527,秭归县,4,420500 +420528,长阳土家族自治县,4,420500 +420529,五峰土家族自治县,4,420500 +420581,宜都市,4,420500 +420582,当阳市,4,420500 +420583,枝江市,4,420500 +420602,襄城区,4,420600 +420606,樊城区,4,420600 +420607,襄州区,4,420600 +420624,南漳县,4,420600 +420625,谷城县,4,420600 +420626,保康县,4,420600 +420682,老河口市,4,420600 +420683,枣阳市,4,420600 +420684,宜城市,4,420600 +420702,梁子湖区,4,420700 +420703,华容区,4,420700 +420704,鄂城区,4,420700 +420802,东宝区,4,420800 +420804,掇刀区,4,420800 +420822,沙洋县,4,420800 +420881,钟祥市,4,420800 +420882,京山市,4,420800 +420902,孝南区,4,420900 +420921,孝昌县,4,420900 +420922,大悟县,4,420900 +420923,云梦县,4,420900 +420981,应城市,4,420900 +420982,安陆市,4,420900 +420984,汉川市,4,420900 +421002,沙市区,4,421000 +421003,荆州区,4,421000 +421022,公安县,4,421000 +421024,江陵县,4,421000 +421071,荆州经济技术开发区,4,421000 +421081,石首市,4,421000 +421083,洪湖市,4,421000 +421087,松滋市,4,421000 +421088,监利市,4,421000 +421102,黄州区,4,421100 +421121,团风县,4,421100 +421122,红安县,4,421100 +421123,罗田县,4,421100 +421124,英山县,4,421100 +421125,浠水县,4,421100 +421126,蕲春县,4,421100 +421127,黄梅县,4,421100 +421171,龙感湖管理区,4,421100 +421181,麻城市,4,421100 +421182,武穴市,4,421100 +421202,咸安区,4,421200 +421221,嘉鱼县,4,421200 +421222,通城县,4,421200 +421223,崇阳县,4,421200 +421224,通山县,4,421200 +421281,赤壁市,4,421200 +421303,曾都区,4,421300 +421321,随县,4,421300 +421381,广水市,4,421300 +422801,恩施市,4,422800 +422802,利川市,4,422800 +422822,建始县,4,422800 +422823,巴东县,4,422800 +422825,宣恩县,4,422800 +422826,咸丰县,4,422800 +422827,来凤县,4,422800 +422828,鹤峰县,4,422800 +429004,仙桃市,4,429000 +429005,潜江市,4,429000 +429006,天门市,4,429000 +429021,神农架林区,4,429000 +430102,芙蓉区,4,430100 +430103,天心区,4,430100 +430104,岳麓区,4,430100 +430105,开福区,4,430100 +430111,雨花区,4,430100 +430112,望城区,4,430100 +430121,长沙县,4,430100 +430181,浏阳市,4,430100 +430182,宁乡市,4,430100 +430202,荷塘区,4,430200 +430203,芦淞区,4,430200 +430204,石峰区,4,430200 +430211,天元区,4,430200 +430212,渌口区,4,430200 +430223,攸县,4,430200 +430224,茶陵县,4,430200 +430225,炎陵县,4,430200 +430271,云龙示范区,4,430200 +430281,醴陵市,4,430200 +430302,雨湖区,4,430300 +430304,岳塘区,4,430300 +430321,湘潭县,4,430300 +430371,湖南湘潭高新技术产业园区,4,430300 +430372,湘潭昭山示范区,4,430300 +430373,湘潭九华示范区,4,430300 +430381,湘乡市,4,430300 +430382,韶山市,4,430300 +430405,珠晖区,4,430400 +430406,雁峰区,4,430400 +430407,石鼓区,4,430400 +430408,蒸湘区,4,430400 +430412,南岳区,4,430400 +430421,衡阳县,4,430400 +430422,衡南县,4,430400 +430423,衡山县,4,430400 +430424,衡东县,4,430400 +430426,祁东县,4,430400 +430471,衡阳综合保税区,4,430400 +430472,湖南衡阳高新技术产业园区,4,430400 +430473,湖南衡阳松木经济开发区,4,430400 +430481,耒阳市,4,430400 +430482,常宁市,4,430400 +430502,双清区,4,430500 +430503,大祥区,4,430500 +430511,北塔区,4,430500 +430522,新邵县,4,430500 +430523,邵阳县,4,430500 +430524,隆回县,4,430500 +430525,洞口县,4,430500 +430527,绥宁县,4,430500 +430528,新宁县,4,430500 +430529,城步苗族自治县,4,430500 +430581,武冈市,4,430500 +430582,邵东市,4,430500 +430602,岳阳楼区,4,430600 +430603,云溪区,4,430600 +430611,君山区,4,430600 +430621,岳阳县,4,430600 +430623,华容县,4,430600 +430624,湘阴县,4,430600 +430626,平江县,4,430600 +430671,岳阳市屈原管理区,4,430600 +430681,汨罗市,4,430600 +430682,临湘市,4,430600 +430702,武陵区,4,430700 +430703,鼎城区,4,430700 +430721,安乡县,4,430700 +430722,汉寿县,4,430700 +430723,澧县,4,430700 +430724,临澧县,4,430700 +430725,桃源县,4,430700 +430726,石门县,4,430700 +430771,常德市西洞庭管理区,4,430700 +430781,津市市,4,430700 +430802,永定区,4,430800 +430811,武陵源区,4,430800 +430821,慈利县,4,430800 +430822,桑植县,4,430800 +430902,资阳区,4,430900 +430903,赫山区,4,430900 +430921,南县,4,430900 +430922,桃江县,4,430900 +430923,安化县,4,430900 +430971,益阳市大通湖管理区,4,430900 +430972,湖南益阳高新技术产业园区,4,430900 +430981,沅江市,4,430900 +431002,北湖区,4,431000 +431003,苏仙区,4,431000 +431021,桂阳县,4,431000 +431022,宜章县,4,431000 +431023,永兴县,4,431000 +431024,嘉禾县,4,431000 +431025,临武县,4,431000 +431026,汝城县,4,431000 +431027,桂东县,4,431000 +431028,安仁县,4,431000 +431081,资兴市,4,431000 +431102,零陵区,4,431100 +431103,冷水滩区,4,431100 +431122,东安县,4,431100 +431123,双牌县,4,431100 +431124,道县,4,431100 +431125,江永县,4,431100 +431126,宁远县,4,431100 +431127,蓝山县,4,431100 +431128,新田县,4,431100 +431129,江华瑶族自治县,4,431100 +431171,永州经济技术开发区,4,431100 +431173,永州市回龙圩管理区,4,431100 +431181,祁阳市,4,431100 +431202,鹤城区,4,431200 +431221,中方县,4,431200 +431222,沅陵县,4,431200 +431223,辰溪县,4,431200 +431224,溆浦县,4,431200 +431225,会同县,4,431200 +431226,麻阳苗族自治县,4,431200 +431227,新晃侗族自治县,4,431200 +431228,芷江侗族自治县,4,431200 +431229,靖州苗族侗族自治县,4,431200 +431230,通道侗族自治县,4,431200 +431271,怀化市洪江管理区,4,431200 +431281,洪江市,4,431200 +431302,娄星区,4,431300 +431321,双峰县,4,431300 +431322,新化县,4,431300 +431381,冷水江市,4,431300 +431382,涟源市,4,431300 +433101,吉首市,4,433100 +433122,泸溪县,4,433100 +433123,凤凰县,4,433100 +433124,花垣县,4,433100 +433125,保靖县,4,433100 +433126,古丈县,4,433100 +433127,永顺县,4,433100 +433130,龙山县,4,433100 +440103,荔湾区,4,440100 +440104,越秀区,4,440100 +440105,海珠区,4,440100 +440106,天河区,4,440100 +440111,白云区,4,440100 +440112,黄埔区,4,440100 +440113,番禺区,4,440100 +440114,花都区,4,440100 +440115,南沙区,4,440100 +440117,从化区,4,440100 +440118,增城区,4,440100 +440203,武江区,4,440200 +440204,浈江区,4,440200 +440205,曲江区,4,440200 +440222,始兴县,4,440200 +440224,仁化县,4,440200 +440229,翁源县,4,440200 +440232,乳源瑶族自治县,4,440200 +440233,新丰县,4,440200 +440281,乐昌市,4,440200 +440282,南雄市,4,440200 +440303,罗湖区,4,440300 +440304,福田区,4,440300 +440305,南山区,4,440300 +440306,宝安区,4,440300 +440307,龙岗区,4,440300 +440308,盐田区,4,440300 +440309,龙华区,4,440300 +440310,坪山区,4,440300 +440311,光明区,4,440300 +440402,香洲区,4,440400 +440403,斗门区,4,440400 +440404,金湾区,4,440400 +440507,龙湖区,4,440500 +440511,金平区,4,440500 +440512,濠江区,4,440500 +440513,潮阳区,4,440500 +440514,潮南区,4,440500 +440515,澄海区,4,440500 +440523,南澳县,4,440500 +440604,禅城区,4,440600 +440605,南海区,4,440600 +440606,顺德区,4,440600 +440607,三水区,4,440600 +440608,高明区,4,440600 +440703,蓬江区,4,440700 +440704,江海区,4,440700 +440705,新会区,4,440700 +440781,台山市,4,440700 +440783,开平市,4,440700 +440784,鹤山市,4,440700 +440785,恩平市,4,440700 +440802,赤坎区,4,440800 +440803,霞山区,4,440800 +440804,坡头区,4,440800 +440811,麻章区,4,440800 +440823,遂溪县,4,440800 +440825,徐闻县,4,440800 +440881,廉江市,4,440800 +440882,雷州市,4,440800 +440883,吴川市,4,440800 +440902,茂南区,4,440900 +440904,电白区,4,440900 +440981,高州市,4,440900 +440982,化州市,4,440900 +440983,信宜市,4,440900 +441202,端州区,4,441200 +441203,鼎湖区,4,441200 +441204,高要区,4,441200 +441223,广宁县,4,441200 +441224,怀集县,4,441200 +441225,封开县,4,441200 +441226,德庆县,4,441200 +441284,四会市,4,441200 +441302,惠城区,4,441300 +441303,惠阳区,4,441300 +441322,博罗县,4,441300 +441323,惠东县,4,441300 +441324,龙门县,4,441300 +441402,梅江区,4,441400 +441403,梅县区,4,441400 +441422,大埔县,4,441400 +441423,丰顺县,4,441400 +441424,五华县,4,441400 +441426,平远县,4,441400 +441427,蕉岭县,4,441400 +441481,兴宁市,4,441400 +441502,城区,4,441500 +441521,海丰县,4,441500 +441523,陆河县,4,441500 +441581,陆丰市,4,441500 +441602,源城区,4,441600 +441621,紫金县,4,441600 +441622,龙川县,4,441600 +441623,连平县,4,441600 +441624,和平县,4,441600 +441625,东源县,4,441600 +441702,江城区,4,441700 +441704,阳东区,4,441700 +441721,阳西县,4,441700 +441781,阳春市,4,441700 +441802,清城区,4,441800 +441803,清新区,4,441800 +441821,佛冈县,4,441800 +441823,阳山县,4,441800 +441825,连山壮族瑶族自治县,4,441800 +441826,连南瑶族自治县,4,441800 +441881,英德市,4,441800 +441882,连州市,4,441800 +445102,湘桥区,4,445100 +445103,潮安区,4,445100 +445122,饶平县,4,445100 +445202,榕城区,4,445200 +445203,揭东区,4,445200 +445222,揭西县,4,445200 +445224,惠来县,4,445200 +445281,普宁市,4,445200 +445302,云城区,4,445300 +445303,云安区,4,445300 +445321,新兴县,4,445300 +445322,郁南县,4,445300 +445381,罗定市,4,445300 +450102,兴宁区,4,450100 +450103,青秀区,4,450100 +450105,江南区,4,450100 +450107,西乡塘区,4,450100 +450108,良庆区,4,450100 +450109,邕宁区,4,450100 +450110,武鸣区,4,450100 +450123,隆安县,4,450100 +450124,马山县,4,450100 +450125,上林县,4,450100 +450126,宾阳县,4,450100 +450181,横州市,4,450100 +450202,城中区,4,450200 +450203,鱼峰区,4,450200 +450204,柳南区,4,450200 +450205,柳北区,4,450200 +450206,柳江区,4,450200 +450222,柳城县,4,450200 +450223,鹿寨县,4,450200 +450224,融安县,4,450200 +450225,融水苗族自治县,4,450200 +450226,三江侗族自治县,4,450200 +450302,秀峰区,4,450300 +450303,叠彩区,4,450300 +450304,象山区,4,450300 +450305,七星区,4,450300 +450311,雁山区,4,450300 +450312,临桂区,4,450300 +450321,阳朔县,4,450300 +450323,灵川县,4,450300 +450324,全州县,4,450300 +450325,兴安县,4,450300 +450326,永福县,4,450300 +450327,灌阳县,4,450300 +450328,龙胜各族自治县,4,450300 +450329,资源县,4,450300 +450330,平乐县,4,450300 +450332,恭城瑶族自治县,4,450300 +450381,荔浦市,4,450300 +450403,万秀区,4,450400 +450405,长洲区,4,450400 +450406,龙圩区,4,450400 +450421,苍梧县,4,450400 +450422,藤县,4,450400 +450423,蒙山县,4,450400 +450481,岑溪市,4,450400 +450502,海城区,4,450500 +450503,银海区,4,450500 +450512,铁山港区,4,450500 +450521,合浦县,4,450500 +450602,港口区,4,450600 +450603,防城区,4,450600 +450621,上思县,4,450600 +450681,东兴市,4,450600 +450702,钦南区,4,450700 +450703,钦北区,4,450700 +450721,灵山县,4,450700 +450722,浦北县,4,450700 +450802,港北区,4,450800 +450803,港南区,4,450800 +450804,覃塘区,4,450800 +450821,平南县,4,450800 +450881,桂平市,4,450800 +450902,玉州区,4,450900 +450903,福绵区,4,450900 +450921,容县,4,450900 +450922,陆川县,4,450900 +450923,博白县,4,450900 +450924,兴业县,4,450900 +450981,北流市,4,450900 +451002,右江区,4,451000 +451003,田阳区,4,451000 +451022,田东县,4,451000 +451024,德保县,4,451000 +451026,那坡县,4,451000 +451027,凌云县,4,451000 +451028,乐业县,4,451000 +451029,田林县,4,451000 +451030,西林县,4,451000 +451031,隆林各族自治县,4,451000 +451081,靖西市,4,451000 +451082,平果市,4,451000 +451102,八步区,4,451100 +451103,平桂区,4,451100 +451121,昭平县,4,451100 +451122,钟山县,4,451100 +451123,富川瑶族自治县,4,451100 +451202,金城江区,4,451200 +451203,宜州区,4,451200 +451221,南丹县,4,451200 +451222,天峨县,4,451200 +451223,凤山县,4,451200 +451224,东兰县,4,451200 +451225,罗城仫佬族自治县,4,451200 +451226,环江毛南族自治县,4,451200 +451227,巴马瑶族自治县,4,451200 +451228,都安瑶族自治县,4,451200 +451229,大化瑶族自治县,4,451200 +451302,兴宾区,4,451300 +451321,忻城县,4,451300 +451322,象州县,4,451300 +451323,武宣县,4,451300 +451324,金秀瑶族自治县,4,451300 +451381,合山市,4,451300 +451402,江州区,4,451400 +451421,扶绥县,4,451400 +451422,宁明县,4,451400 +451423,龙州县,4,451400 +451424,大新县,4,451400 +451425,天等县,4,451400 +451481,凭祥市,4,451400 +460105,秀英区,4,460100 +460106,龙华区,4,460100 +460107,琼山区,4,460100 +460108,美兰区,4,460100 +460202,海棠区,4,460200 +460203,吉阳区,4,460200 +460204,天涯区,4,460200 +460205,崖州区,4,460200 +460321,西沙群岛,4,460300 +460322,南沙群岛,4,460300 +460323,中沙群岛的岛礁及其海域,4,460300 +469001,五指山市,4,469000 +469002,琼海市,4,469000 +469005,文昌市,4,469000 +469006,万宁市,4,469000 +469007,东方市,4,469000 +469021,定安县,4,469000 +469022,屯昌县,4,469000 +469023,澄迈县,4,469000 +469024,临高县,4,469000 +469025,白沙黎族自治县,4,469000 +469026,昌江黎族自治县,4,469000 +469027,乐东黎族自治县,4,469000 +469028,陵水黎族自治县,4,469000 +469029,保亭黎族苗族自治县,4,469000 +469030,琼中黎族苗族自治县,4,469000 +500101,万州区,4,500100 +500102,涪陵区,4,500100 +500103,渝中区,4,500100 +500104,大渡口区,4,500100 +500105,江北区,4,500100 +500106,沙坪坝区,4,500100 +500107,九龙坡区,4,500100 +500108,南岸区,4,500100 +500109,北碚区,4,500100 +500110,綦江区,4,500100 +500111,大足区,4,500100 +500112,渝北区,4,500100 +500113,巴南区,4,500100 +500114,黔江区,4,500100 +500115,长寿区,4,500100 +500116,江津区,4,500100 +500117,合川区,4,500100 +500118,永川区,4,500100 +500119,南川区,4,500100 +500120,璧山区,4,500100 +500151,铜梁区,4,500100 +500152,潼南区,4,500100 +500153,荣昌区,4,500100 +500154,开州区,4,500100 +500155,梁平区,4,500100 +500156,武隆区,4,500100 +500229,城口县,4,500100 +500230,丰都县,4,500100 +500231,垫江县,4,500100 +500233,忠县,4,500100 +500235,云阳县,4,500100 +500236,奉节县,4,500100 +500237,巫山县,4,500100 +500238,巫溪县,4,500100 +500240,石柱土家族自治县,4,500100 +500241,秀山土家族苗族自治县,4,500100 +500242,酉阳土家族苗族自治县,4,500100 +500243,彭水苗族土家族自治县,4,500100 +510104,锦江区,4,510100 +510105,青羊区,4,510100 +510106,金牛区,4,510100 +510107,武侯区,4,510100 +510108,成华区,4,510100 +510112,龙泉驿区,4,510100 +510113,青白江区,4,510100 +510114,新都区,4,510100 +510115,温江区,4,510100 +510116,双流区,4,510100 +510117,郫都区,4,510100 +510118,新津区,4,510100 +510121,金堂县,4,510100 +510129,大邑县,4,510100 +510131,蒲江县,4,510100 +510181,都江堰市,4,510100 +510182,彭州市,4,510100 +510183,邛崃市,4,510100 +510184,崇州市,4,510100 +510185,简阳市,4,510100 +510302,自流井区,4,510300 +510303,贡井区,4,510300 +510304,大安区,4,510300 +510311,沿滩区,4,510300 +510321,荣县,4,510300 +510322,富顺县,4,510300 +510402,东区,4,510400 +510403,西区,4,510400 +510411,仁和区,4,510400 +510421,米易县,4,510400 +510422,盐边县,4,510400 +510502,江阳区,4,510500 +510503,纳溪区,4,510500 +510504,龙马潭区,4,510500 +510521,泸县,4,510500 +510522,合江县,4,510500 +510524,叙永县,4,510500 +510525,古蔺县,4,510500 +510603,旌阳区,4,510600 +510604,罗江区,4,510600 +510623,中江县,4,510600 +510681,广汉市,4,510600 +510682,什邡市,4,510600 +510683,绵竹市,4,510600 +510703,涪城区,4,510700 +510704,游仙区,4,510700 +510705,安州区,4,510700 +510722,三台县,4,510700 +510723,盐亭县,4,510700 +510725,梓潼县,4,510700 +510726,北川羌族自治县,4,510700 +510727,平武县,4,510700 +510781,江油市,4,510700 +510802,利州区,4,510800 +510811,昭化区,4,510800 +510812,朝天区,4,510800 +510821,旺苍县,4,510800 +510822,青川县,4,510800 +510823,剑阁县,4,510800 +510824,苍溪县,4,510800 +510903,船山区,4,510900 +510904,安居区,4,510900 +510921,蓬溪县,4,510900 +510923,大英县,4,510900 +510981,射洪市,4,510900 +511002,市中区,4,511000 +511011,东兴区,4,511000 +511024,威远县,4,511000 +511025,资中县,4,511000 +511071,内江经济开发区,4,511000 +511083,隆昌市,4,511000 +511102,市中区,4,511100 +511111,沙湾区,4,511100 +511112,五通桥区,4,511100 +511113,金口河区,4,511100 +511123,犍为县,4,511100 +511124,井研县,4,511100 +511126,夹江县,4,511100 +511129,沐川县,4,511100 +511132,峨边彝族自治县,4,511100 +511133,马边彝族自治县,4,511100 +511181,峨眉山市,4,511100 +511302,顺庆区,4,511300 +511303,高坪区,4,511300 +511304,嘉陵区,4,511300 +511321,南部县,4,511300 +511322,营山县,4,511300 +511323,蓬安县,4,511300 +511324,仪陇县,4,511300 +511325,西充县,4,511300 +511381,阆中市,4,511300 +511402,东坡区,4,511400 +511403,彭山区,4,511400 +511421,仁寿县,4,511400 +511423,洪雅县,4,511400 +511424,丹棱县,4,511400 +511425,青神县,4,511400 +511502,翠屏区,4,511500 +511503,南溪区,4,511500 +511504,叙州区,4,511500 +511523,江安县,4,511500 +511524,长宁县,4,511500 +511525,高县,4,511500 +511526,珙县,4,511500 +511527,筠连县,4,511500 +511528,兴文县,4,511500 +511529,屏山县,4,511500 +511602,广安区,4,511600 +511603,前锋区,4,511600 +511621,岳池县,4,511600 +511622,武胜县,4,511600 +511623,邻水县,4,511600 +511681,华蓥市,4,511600 +511702,通川区,4,511700 +511703,达川区,4,511700 +511722,宣汉县,4,511700 +511723,开江县,4,511700 +511724,大竹县,4,511700 +511725,渠县,4,511700 +511771,达州经济开发区,4,511700 +511781,万源市,4,511700 +511802,雨城区,4,511800 +511803,名山区,4,511800 +511822,荥经县,4,511800 +511823,汉源县,4,511800 +511824,石棉县,4,511800 +511825,天全县,4,511800 +511826,芦山县,4,511800 +511827,宝兴县,4,511800 +511902,巴州区,4,511900 +511903,恩阳区,4,511900 +511921,通江县,4,511900 +511922,南江县,4,511900 +511923,平昌县,4,511900 +511971,巴中经济开发区,4,511900 +512002,雁江区,4,512000 +512021,安岳县,4,512000 +512022,乐至县,4,512000 +513201,马尔康市,4,513200 +513221,汶川县,4,513200 +513222,理县,4,513200 +513223,茂县,4,513200 +513224,松潘县,4,513200 +513225,九寨沟县,4,513200 +513226,金川县,4,513200 +513227,小金县,4,513200 +513228,黑水县,4,513200 +513230,壤塘县,4,513200 +513231,阿坝县,4,513200 +513232,若尔盖县,4,513200 +513233,红原县,4,513200 +513301,康定市,4,513300 +513322,泸定县,4,513300 +513323,丹巴县,4,513300 +513324,九龙县,4,513300 +513325,雅江县,4,513300 +513326,道孚县,4,513300 +513327,炉霍县,4,513300 +513328,甘孜县,4,513300 +513329,新龙县,4,513300 +513330,德格县,4,513300 +513331,白玉县,4,513300 +513332,石渠县,4,513300 +513333,色达县,4,513300 +513334,理塘县,4,513300 +513335,巴塘县,4,513300 +513336,乡城县,4,513300 +513337,稻城县,4,513300 +513338,得荣县,4,513300 +513401,西昌市,4,513400 +513402,会理市,4,513400 +513422,木里藏族自治县,4,513400 +513423,盐源县,4,513400 +513424,德昌县,4,513400 +513426,会东县,4,513400 +513427,宁南县,4,513400 +513428,普格县,4,513400 +513429,布拖县,4,513400 +513430,金阳县,4,513400 +513431,昭觉县,4,513400 +513432,喜德县,4,513400 +513433,冕宁县,4,513400 +513434,越西县,4,513400 +513435,甘洛县,4,513400 +513436,美姑县,4,513400 +513437,雷波县,4,513400 +520102,南明区,4,520100 +520103,云岩区,4,520100 +520111,花溪区,4,520100 +520112,乌当区,4,520100 +520113,白云区,4,520100 +520115,观山湖区,4,520100 +520121,开阳县,4,520100 +520122,息烽县,4,520100 +520123,修文县,4,520100 +520181,清镇市,4,520100 +520201,钟山区,4,520200 +520203,六枝特区,4,520200 +520204,水城区,4,520200 +520281,盘州市,4,520200 +520302,红花岗区,4,520300 +520303,汇川区,4,520300 +520304,播州区,4,520300 +520322,桐梓县,4,520300 +520323,绥阳县,4,520300 +520324,正安县,4,520300 +520325,道真仡佬族苗族自治县,4,520300 +520326,务川仡佬族苗族自治县,4,520300 +520327,凤冈县,4,520300 +520328,湄潭县,4,520300 +520329,余庆县,4,520300 +520330,习水县,4,520300 +520381,赤水市,4,520300 +520382,仁怀市,4,520300 +520402,西秀区,4,520400 +520403,平坝区,4,520400 +520422,普定县,4,520400 +520423,镇宁布依族苗族自治县,4,520400 +520424,关岭布依族苗族自治县,4,520400 +520425,紫云苗族布依族自治县,4,520400 +520502,七星关区,4,520500 +520521,大方县,4,520500 +520523,金沙县,4,520500 +520524,织金县,4,520500 +520525,纳雍县,4,520500 +520526,威宁彝族回族苗族自治县,4,520500 +520527,赫章县,4,520500 +520581,黔西市,4,520500 +520602,碧江区,4,520600 +520603,万山区,4,520600 +520621,江口县,4,520600 +520622,玉屏侗族自治县,4,520600 +520623,石阡县,4,520600 +520624,思南县,4,520600 +520625,印江土家族苗族自治县,4,520600 +520626,德江县,4,520600 +520627,沿河土家族自治县,4,520600 +520628,松桃苗族自治县,4,520600 +522301,兴义市,4,522300 +522302,兴仁市,4,522300 +522323,普安县,4,522300 +522324,晴隆县,4,522300 +522325,贞丰县,4,522300 +522326,望谟县,4,522300 +522327,册亨县,4,522300 +522328,安龙县,4,522300 +522601,凯里市,4,522600 +522622,黄平县,4,522600 +522623,施秉县,4,522600 +522624,三穗县,4,522600 +522625,镇远县,4,522600 +522626,岑巩县,4,522600 +522627,天柱县,4,522600 +522628,锦屏县,4,522600 +522629,剑河县,4,522600 +522630,台江县,4,522600 +522631,黎平县,4,522600 +522632,榕江县,4,522600 +522633,从江县,4,522600 +522634,雷山县,4,522600 +522635,麻江县,4,522600 +522636,丹寨县,4,522600 +522701,都匀市,4,522700 +522702,福泉市,4,522700 +522722,荔波县,4,522700 +522723,贵定县,4,522700 +522725,瓮安县,4,522700 +522726,独山县,4,522700 +522727,平塘县,4,522700 +522728,罗甸县,4,522700 +522729,长顺县,4,522700 +522730,龙里县,4,522700 +522731,惠水县,4,522700 +522732,三都水族自治县,4,522700 +530102,五华区,4,530100 +530103,盘龙区,4,530100 +530111,官渡区,4,530100 +530112,西山区,4,530100 +530113,东川区,4,530100 +530114,呈贡区,4,530100 +530115,晋宁区,4,530100 +530124,富民县,4,530100 +530125,宜良县,4,530100 +530126,石林彝族自治县,4,530100 +530127,嵩明县,4,530100 +530128,禄劝彝族苗族自治县,4,530100 +530129,寻甸回族彝族自治县,4,530100 +530181,安宁市,4,530100 +530302,麒麟区,4,530300 +530303,沾益区,4,530300 +530304,马龙区,4,530300 +530322,陆良县,4,530300 +530323,师宗县,4,530300 +530324,罗平县,4,530300 +530325,富源县,4,530300 +530326,会泽县,4,530300 +530381,宣威市,4,530300 +530402,红塔区,4,530400 +530403,江川区,4,530400 +530423,通海县,4,530400 +530424,华宁县,4,530400 +530425,易门县,4,530400 +530426,峨山彝族自治县,4,530400 +530427,新平彝族傣族自治县,4,530400 +530428,元江哈尼族彝族傣族自治县,4,530400 +530481,澄江市,4,530400 +530502,隆阳区,4,530500 +530521,施甸县,4,530500 +530523,龙陵县,4,530500 +530524,昌宁县,4,530500 +530581,腾冲市,4,530500 +530602,昭阳区,4,530600 +530621,鲁甸县,4,530600 +530622,巧家县,4,530600 +530623,盐津县,4,530600 +530624,大关县,4,530600 +530625,永善县,4,530600 +530626,绥江县,4,530600 +530627,镇雄县,4,530600 +530628,彝良县,4,530600 +530629,威信县,4,530600 +530681,水富市,4,530600 +530702,古城区,4,530700 +530721,玉龙纳西族自治县,4,530700 +530722,永胜县,4,530700 +530723,华坪县,4,530700 +530724,宁蒗彝族自治县,4,530700 +530802,思茅区,4,530800 +530821,宁洱哈尼族彝族自治县,4,530800 +530822,墨江哈尼族自治县,4,530800 +530823,景东彝族自治县,4,530800 +530824,景谷傣族彝族自治县,4,530800 +530825,镇沅彝族哈尼族拉祜族自治县,4,530800 +530826,江城哈尼族彝族自治县,4,530800 +530827,孟连傣族拉祜族佤族自治县,4,530800 +530828,澜沧拉祜族自治县,4,530800 +530829,西盟佤族自治县,4,530800 +530902,临翔区,4,530900 +530921,凤庆县,4,530900 +530922,云县,4,530900 +530923,永德县,4,530900 +530924,镇康县,4,530900 +530925,双江拉祜族佤族布朗族傣族自治县,4,530900 +530926,耿马傣族佤族自治县,4,530900 +530927,沧源佤族自治县,4,530900 +532301,楚雄市,4,532300 +532302,禄丰市,4,532300 +532322,双柏县,4,532300 +532323,牟定县,4,532300 +532324,南华县,4,532300 +532325,姚安县,4,532300 +532326,大姚县,4,532300 +532327,永仁县,4,532300 +532328,元谋县,4,532300 +532329,武定县,4,532300 +532501,个旧市,4,532500 +532502,开远市,4,532500 +532503,蒙自市,4,532500 +532504,弥勒市,4,532500 +532523,屏边苗族自治县,4,532500 +532524,建水县,4,532500 +532525,石屏县,4,532500 +532527,泸西县,4,532500 +532528,元阳县,4,532500 +532529,红河县,4,532500 +532530,金平苗族瑶族傣族自治县,4,532500 +532531,绿春县,4,532500 +532532,河口瑶族自治县,4,532500 +532601,文山市,4,532600 +532622,砚山县,4,532600 +532623,西畴县,4,532600 +532624,麻栗坡县,4,532600 +532625,马关县,4,532600 +532626,丘北县,4,532600 +532627,广南县,4,532600 +532628,富宁县,4,532600 +532801,景洪市,4,532800 +532822,勐海县,4,532800 +532823,勐腊县,4,532800 +532901,大理市,4,532900 +532922,漾濞彝族自治县,4,532900 +532923,祥云县,4,532900 +532924,宾川县,4,532900 +532925,弥渡县,4,532900 +532926,南涧彝族自治县,4,532900 +532927,巍山彝族回族自治县,4,532900 +532928,永平县,4,532900 +532929,云龙县,4,532900 +532930,洱源县,4,532900 +532931,剑川县,4,532900 +532932,鹤庆县,4,532900 +533102,瑞丽市,4,533100 +533103,芒市,4,533100 +533122,梁河县,4,533100 +533123,盈江县,4,533100 +533124,陇川县,4,533100 +533301,泸水市,4,533300 +533323,福贡县,4,533300 +533324,贡山独龙族怒族自治县,4,533300 +533325,兰坪白族普米族自治县,4,533300 +533401,香格里拉市,4,533400 +533422,德钦县,4,533400 +533423,维西傈僳族自治县,4,533400 +540102,城关区,4,540100 +540103,堆龙德庆区,4,540100 +540104,达孜区,4,540100 +540121,林周县,4,540100 +540122,当雄县,4,540100 +540123,尼木县,4,540100 +540124,曲水县,4,540100 +540127,墨竹工卡县,4,540100 +540171,格尔木藏青工业园区,4,540100 +540172,拉萨经济技术开发区,4,540100 +540173,西藏文化旅游创意园区,4,540100 +540174,达孜工业园区,4,540100 +540202,桑珠孜区,4,540200 +540221,南木林县,4,540200 +540222,江孜县,4,540200 +540223,定日县,4,540200 +540224,萨迦县,4,540200 +540225,拉孜县,4,540200 +540226,昂仁县,4,540200 +540227,谢通门县,4,540200 +540228,白朗县,4,540200 +540229,仁布县,4,540200 +540230,康马县,4,540200 +540231,定结县,4,540200 +540232,仲巴县,4,540200 +540233,亚东县,4,540200 +540234,吉隆县,4,540200 +540235,聂拉木县,4,540200 +540236,萨嘎县,4,540200 +540237,岗巴县,4,540200 +540302,卡若区,4,540300 +540321,江达县,4,540300 +540322,贡觉县,4,540300 +540323,类乌齐县,4,540300 +540324,丁青县,4,540300 +540325,察雅县,4,540300 +540326,八宿县,4,540300 +540327,左贡县,4,540300 +540328,芒康县,4,540300 +540329,洛隆县,4,540300 +540330,边坝县,4,540300 +540402,巴宜区,4,540400 +540421,工布江达县,4,540400 +540422,米林县,4,540400 +540423,墨脱县,4,540400 +540424,波密县,4,540400 +540425,察隅县,4,540400 +540426,朗县,4,540400 +540502,乃东区,4,540500 +540521,扎囊县,4,540500 +540522,贡嘎县,4,540500 +540523,桑日县,4,540500 +540524,琼结县,4,540500 +540525,曲松县,4,540500 +540526,措美县,4,540500 +540527,洛扎县,4,540500 +540528,加查县,4,540500 +540529,隆子县,4,540500 +540530,错那县,4,540500 +540531,浪卡子县,4,540500 +540602,色尼区,4,540600 +540621,嘉黎县,4,540600 +540622,比如县,4,540600 +540623,聂荣县,4,540600 +540624,安多县,4,540600 +540625,申扎县,4,540600 +540626,索县,4,540600 +540627,班戈县,4,540600 +540628,巴青县,4,540600 +540629,尼玛县,4,540600 +540630,双湖县,4,540600 +542521,普兰县,4,542500 +542522,札达县,4,542500 +542523,噶尔县,4,542500 +542524,日土县,4,542500 +542525,革吉县,4,542500 +542526,改则县,4,542500 +542527,措勤县,4,542500 +610102,新城区,4,610100 +610103,碑林区,4,610100 +610104,莲湖区,4,610100 +610111,灞桥区,4,610100 +610112,未央区,4,610100 +610113,雁塔区,4,610100 +610114,阎良区,4,610100 +610115,临潼区,4,610100 +610116,长安区,4,610100 +610117,高陵区,4,610100 +610118,鄠邑区,4,610100 +610122,蓝田县,4,610100 +610124,周至县,4,610100 +610202,王益区,4,610200 +610203,印台区,4,610200 +610204,耀州区,4,610200 +610222,宜君县,4,610200 +610302,渭滨区,4,610300 +610303,金台区,4,610300 +610304,陈仓区,4,610300 +610305,凤翔区,4,610300 +610323,岐山县,4,610300 +610324,扶风县,4,610300 +610326,眉县,4,610300 +610327,陇县,4,610300 +610328,千阳县,4,610300 +610329,麟游县,4,610300 +610330,凤县,4,610300 +610331,太白县,4,610300 +610402,秦都区,4,610400 +610403,杨陵区,4,610400 +610404,渭城区,4,610400 +610422,三原县,4,610400 +610423,泾阳县,4,610400 +610424,乾县,4,610400 +610425,礼泉县,4,610400 +610426,永寿县,4,610400 +610428,长武县,4,610400 +610429,旬邑县,4,610400 +610430,淳化县,4,610400 +610431,武功县,4,610400 +610481,兴平市,4,610400 +610482,彬州市,4,610400 +610502,临渭区,4,610500 +610503,华州区,4,610500 +610522,潼关县,4,610500 +610523,大荔县,4,610500 +610524,合阳县,4,610500 +610525,澄城县,4,610500 +610526,蒲城县,4,610500 +610527,白水县,4,610500 +610528,富平县,4,610500 +610581,韩城市,4,610500 +610582,华阴市,4,610500 +610602,宝塔区,4,610600 +610603,安塞区,4,610600 +610621,延长县,4,610600 +610622,延川县,4,610600 +610625,志丹县,4,610600 +610626,吴起县,4,610600 +610627,甘泉县,4,610600 +610628,富县,4,610600 +610629,洛川县,4,610600 +610630,宜川县,4,610600 +610631,黄龙县,4,610600 +610632,黄陵县,4,610600 +610681,子长市,4,610600 +610702,汉台区,4,610700 +610703,南郑区,4,610700 +610722,城固县,4,610700 +610723,洋县,4,610700 +610724,西乡县,4,610700 +610725,勉县,4,610700 +610726,宁强县,4,610700 +610727,略阳县,4,610700 +610728,镇巴县,4,610700 +610729,留坝县,4,610700 +610730,佛坪县,4,610700 +610802,榆阳区,4,610800 +610803,横山区,4,610800 +610822,府谷县,4,610800 +610824,靖边县,4,610800 +610825,定边县,4,610800 +610826,绥德县,4,610800 +610827,米脂县,4,610800 +610828,佳县,4,610800 +610829,吴堡县,4,610800 +610830,清涧县,4,610800 +610831,子洲县,4,610800 +610881,神木市,4,610800 +610902,汉滨区,4,610900 +610921,汉阴县,4,610900 +610922,石泉县,4,610900 +610923,宁陕县,4,610900 +610924,紫阳县,4,610900 +610925,岚皋县,4,610900 +610926,平利县,4,610900 +610927,镇坪县,4,610900 +610929,白河县,4,610900 +610981,旬阳市,4,610900 +611002,商州区,4,611000 +611021,洛南县,4,611000 +611022,丹凤县,4,611000 +611023,商南县,4,611000 +611024,山阳县,4,611000 +611025,镇安县,4,611000 +611026,柞水县,4,611000 +620102,城关区,4,620100 +620103,七里河区,4,620100 +620104,西固区,4,620100 +620105,安宁区,4,620100 +620111,红古区,4,620100 +620121,永登县,4,620100 +620122,皋兰县,4,620100 +620123,榆中县,4,620100 +620171,兰州新区,4,620100 +620201,嘉峪关市,4,620200 +620302,金川区,4,620300 +620321,永昌县,4,620300 +620402,白银区,4,620400 +620403,平川区,4,620400 +620421,靖远县,4,620400 +620422,会宁县,4,620400 +620423,景泰县,4,620400 +620502,秦州区,4,620500 +620503,麦积区,4,620500 +620521,清水县,4,620500 +620522,秦安县,4,620500 +620523,甘谷县,4,620500 +620524,武山县,4,620500 +620525,张家川回族自治县,4,620500 +620602,凉州区,4,620600 +620621,民勤县,4,620600 +620622,古浪县,4,620600 +620623,天祝藏族自治县,4,620600 +620702,甘州区,4,620700 +620721,肃南裕固族自治县,4,620700 +620722,民乐县,4,620700 +620723,临泽县,4,620700 +620724,高台县,4,620700 +620725,山丹县,4,620700 +620802,崆峒区,4,620800 +620821,泾川县,4,620800 +620822,灵台县,4,620800 +620823,崇信县,4,620800 +620825,庄浪县,4,620800 +620826,静宁县,4,620800 +620881,华亭市,4,620800 +620902,肃州区,4,620900 +620921,金塔县,4,620900 +620922,瓜州县,4,620900 +620923,肃北蒙古族自治县,4,620900 +620924,阿克塞哈萨克族自治县,4,620900 +620981,玉门市,4,620900 +620982,敦煌市,4,620900 +621002,西峰区,4,621000 +621021,庆城县,4,621000 +621022,环县,4,621000 +621023,华池县,4,621000 +621024,合水县,4,621000 +621025,正宁县,4,621000 +621026,宁县,4,621000 +621027,镇原县,4,621000 +621102,安定区,4,621100 +621121,通渭县,4,621100 +621122,陇西县,4,621100 +621123,渭源县,4,621100 +621124,临洮县,4,621100 +621125,漳县,4,621100 +621126,岷县,4,621100 +621202,武都区,4,621200 +621221,成县,4,621200 +621222,文县,4,621200 +621223,宕昌县,4,621200 +621224,康县,4,621200 +621225,西和县,4,621200 +621226,礼县,4,621200 +621227,徽县,4,621200 +621228,两当县,4,621200 +622901,临夏市,4,622900 +622921,临夏县,4,622900 +622922,康乐县,4,622900 +622923,永靖县,4,622900 +622924,广河县,4,622900 +622925,和政县,4,622900 +622926,东乡族自治县,4,622900 +622927,积石山保安族东乡族撒拉族自治县,4,622900 +623001,合作市,4,623000 +623021,临潭县,4,623000 +623022,卓尼县,4,623000 +623023,舟曲县,4,623000 +623024,迭部县,4,623000 +623025,玛曲县,4,623000 +623026,碌曲县,4,623000 +623027,夏河县,4,623000 +630102,城东区,4,630100 +630103,城中区,4,630100 +630104,城西区,4,630100 +630105,城北区,4,630100 +630106,湟中区,4,630100 +630121,大通回族土族自治县,4,630100 +630123,湟源县,4,630100 +630202,乐都区,4,630200 +630203,平安区,4,630200 +630222,民和回族土族自治县,4,630200 +630223,互助土族自治县,4,630200 +630224,化隆回族自治县,4,630200 +630225,循化撒拉族自治县,4,630200 +632221,门源回族自治县,4,632200 +632222,祁连县,4,632200 +632223,海晏县,4,632200 +632224,刚察县,4,632200 +632301,同仁市,4,632300 +632322,尖扎县,4,632300 +632323,泽库县,4,632300 +632324,河南蒙古族自治县,4,632300 +632521,共和县,4,632500 +632522,同德县,4,632500 +632523,贵德县,4,632500 +632524,兴海县,4,632500 +632525,贵南县,4,632500 +632621,玛沁县,4,632600 +632622,班玛县,4,632600 +632623,甘德县,4,632600 +632624,达日县,4,632600 +632625,久治县,4,632600 +632626,玛多县,4,632600 +632701,玉树市,4,632700 +632722,杂多县,4,632700 +632723,称多县,4,632700 +632724,治多县,4,632700 +632725,囊谦县,4,632700 +632726,曲麻莱县,4,632700 +632801,格尔木市,4,632800 +632802,德令哈市,4,632800 +632803,茫崖市,4,632800 +632821,乌兰县,4,632800 +632822,都兰县,4,632800 +632823,天峻县,4,632800 +632857,大柴旦行政委员会,4,632800 +640104,兴庆区,4,640100 +640105,西夏区,4,640100 +640106,金凤区,4,640100 +640121,永宁县,4,640100 +640122,贺兰县,4,640100 +640181,灵武市,4,640100 +640202,大武口区,4,640200 +640205,惠农区,4,640200 +640221,平罗县,4,640200 +640302,利通区,4,640300 +640303,红寺堡区,4,640300 +640323,盐池县,4,640300 +640324,同心县,4,640300 +640381,青铜峡市,4,640300 +640402,原州区,4,640400 +640422,西吉县,4,640400 +640423,隆德县,4,640400 +640424,泾源县,4,640400 +640425,彭阳县,4,640400 +640502,沙坡头区,4,640500 +640521,中宁县,4,640500 +640522,海原县,4,640500 +650102,天山区,4,650100 +650103,沙依巴克区,4,650100 +650104,新市区,4,650100 +650105,水磨沟区,4,650100 +650106,头屯河区,4,650100 +650107,达坂城区,4,650100 +650109,米东区,4,650100 +650121,乌鲁木齐县,4,650100 +650202,独山子区,4,650200 +650203,克拉玛依区,4,650200 +650204,白碱滩区,4,650200 +650205,乌尔禾区,4,650200 +650402,高昌区,4,650400 +650421,鄯善县,4,650400 +650422,托克逊县,4,650400 +650502,伊州区,4,650500 +650521,巴里坤哈萨克自治县,4,650500 +650522,伊吾县,4,650500 +652301,昌吉市,4,652300 +652302,阜康市,4,652300 +652323,呼图壁县,4,652300 +652324,玛纳斯县,4,652300 +652325,奇台县,4,652300 +652327,吉木萨尔县,4,652300 +652328,木垒哈萨克自治县,4,652300 +652701,博乐市,4,652700 +652702,阿拉山口市,4,652700 +652722,精河县,4,652700 +652723,温泉县,4,652700 +652801,库尔勒市,4,652800 +652822,轮台县,4,652800 +652823,尉犁县,4,652800 +652824,若羌县,4,652800 +652825,且末县,4,652800 +652826,焉耆回族自治县,4,652800 +652827,和静县,4,652800 +652828,和硕县,4,652800 +652829,博湖县,4,652800 +652871,库尔勒经济技术开发区,4,652800 +652901,阿克苏市,4,652900 +652902,库车市,4,652900 +652922,温宿县,4,652900 +652924,沙雅县,4,652900 +652925,新和县,4,652900 +652926,拜城县,4,652900 +652927,乌什县,4,652900 +652928,阿瓦提县,4,652900 +652929,柯坪县,4,652900 +653001,阿图什市,4,653000 +653022,阿克陶县,4,653000 +653023,阿合奇县,4,653000 +653024,乌恰县,4,653000 +653101,喀什市,4,653100 +653121,疏附县,4,653100 +653122,疏勒县,4,653100 +653123,英吉沙县,4,653100 +653124,泽普县,4,653100 +653125,莎车县,4,653100 +653126,叶城县,4,653100 +653127,麦盖提县,4,653100 +653128,岳普湖县,4,653100 +653129,伽师县,4,653100 +653130,巴楚县,4,653100 +653131,塔什库尔干塔吉克自治县,4,653100 +653201,和田市,4,653200 +653221,和田县,4,653200 +653222,墨玉县,4,653200 +653223,皮山县,4,653200 +653224,洛浦县,4,653200 +653225,策勒县,4,653200 +653226,于田县,4,653200 +653227,民丰县,4,653200 +654002,伊宁市,4,654000 +654003,奎屯市,4,654000 +654004,霍尔果斯市,4,654000 +654021,伊宁县,4,654000 +654022,察布查尔锡伯自治县,4,654000 +654023,霍城县,4,654000 +654024,巩留县,4,654000 +654025,新源县,4,654000 +654026,昭苏县,4,654000 +654027,特克斯县,4,654000 +654028,尼勒克县,4,654000 +654201,塔城市,4,654200 +654202,乌苏市,4,654200 +654203,沙湾市,4,654200 +654221,额敏县,4,654200 +654224,托里县,4,654200 +654225,裕民县,4,654200 +654226,和布克赛尔蒙古自治县,4,654200 +654301,阿勒泰市,4,654300 +654321,布尔津县,4,654300 +654322,富蕴县,4,654300 +654323,福海县,4,654300 +654324,哈巴河县,4,654300 +654325,青河县,4,654300 +654326,吉木乃县,4,654300 +659001,石河子市,4,659000 +659002,阿拉尔市,4,659000 +659003,图木舒克市,4,659000 +659004,五家渠市,4,659000 +659005,北屯市,4,659000 +659006,铁门关市,4,659000 +659007,双河市,4,659000 +659008,可克达拉市,4,659000 +659009,昆玉市,4,659000 +659010,胡杨河市,4,659000 +659011,新星市,4,659000 \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb new file mode 100644 index 0000000..58596a5 Binary files /dev/null and b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb differ diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/test/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtilsTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/test/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtilsTest.java new file mode 100644 index 0000000..cc23819 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/test/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtilsTest.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.ip.core.utils; + + +import cn.iocoder.yudao.framework.ip.core.Area; +import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link AreaUtils} 的单元测试 + * + * @author 芋道源码 + */ +public class AreaUtilsTest { + + @Test + public void testGetArea() { + // 调用:北京 + Area area = AreaUtils.getArea(110100); + // 断言 + assertEquals(area.getId(), 110100); + assertEquals(area.getName(), "北京市"); + assertEquals(area.getType(), AreaTypeEnum.CITY.getType()); + assertEquals(area.getParent().getId(), 110000); + assertEquals(area.getChildren().size(), 16); + } + + @Test + public void testFormat() { + assertEquals(AreaUtils.format(110105), "北京市 北京市 朝阳区"); + assertEquals(AreaUtils.format(1), "中国"); + assertEquals(AreaUtils.format(2), "蒙古"); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/test/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtilsTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/test/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtilsTest.java new file mode 100644 index 0000000..761a1aa --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-ip/src/test/java/cn/iocoder/yudao/framework/ip/core/utils/IPUtilsTest.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.ip.core.utils; + +import cn.iocoder.yudao.framework.ip.core.Area; +import org.junit.jupiter.api.Test; +import org.lionsoul.ip2region.xdb.Searcher; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link IPUtils} 的单元测试 + * + * @author wanglhup + */ +public class IPUtilsTest { + + @Test + public void testGetAreaId_string() { + // 120.202.4.0|120.202.4.255|420600 + Integer areaId = IPUtils.getAreaId("120.202.4.50"); + assertEquals(420600, areaId); + } + + @Test + public void testGetAreaId_long() throws Exception { + // 120.203.123.0|120.203.133.255|360900 + long ip = Searcher.checkIP("120.203.123.250"); + Integer areaId = IPUtils.getAreaId(ip); + assertEquals(360900, areaId); + } + + @Test + public void testGetArea_string() { + // 120.202.4.0|120.202.4.255|420600 + Area area = IPUtils.getArea("120.202.4.50"); + assertEquals("襄阳市", area.getName()); + } + + @Test + public void testGetArea_long() throws Exception { + // 120.203.123.0|120.203.133.255|360900 + long ip = Searcher.checkIP("120.203.123.252"); + Area area = IPUtils.getArea(ip); + assertEquals("宜春市", area.getName()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml new file mode 100644 index 0000000..c1a5376 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml @@ -0,0 +1,83 @@ + + + + yudao-framework + cn.iocoder.boot + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-tenant + jar + + ${project.artifactId} + 多租户 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + + + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + + + + cn.iocoder.boot + yudao-spring-boot-starter-job + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mq + true + + + org.springframework.kafka + spring-kafka + true + + + org.springframework.amqp + spring-rabbit + true + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + + com.google.guava + guava + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java new file mode 100644 index 0000000..e059e28 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * 多租户配置 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yudao.tenant") +@Data +public class TenantProperties { + + /** + * 租户是否开启 + */ + private static final Boolean ENABLE_DEFAULT = true; + + /** + * 是否开启 + */ + private Boolean enable = ENABLE_DEFAULT; + + /** + * 需要忽略多租户的请求 + * + * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API! + */ + private Set ignoreUrls = new HashSet<>(); + + /** + * 需要忽略跨(切换)租户访问的请求 + * + * 原因是:某些接口,访问的是个人信息,在跨租户是获取不到的! + */ + private Set ignoreVisitUrls = Collections.emptySet(); + + /** + * 需要忽略多租户的表 + * + * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 + */ + private Set ignoreTables = Collections.emptySet(); + + /** + * 需要忽略多租户的 Spring Cache 缓存 + * + * 即默认所有缓存都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 + */ + private Set ignoreCaches = Collections.emptySet(); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java new file mode 100644 index 0000000..0d9783e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java @@ -0,0 +1,197 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi; +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect; +import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJobAspect; +import cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer; +import cn.iocoder.yudao.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor; +import cn.iocoder.yudao.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer; +import cn.iocoder.yudao.framework.tenant.core.redis.TenantRedisCacheManager; +import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl; +import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; +import cn.iocoder.yudao.framework.tenant.core.web.TenantVisitContextInterceptor; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import jakarta.annotation.Resource; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.BatchStrategies; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.pattern.PathPattern; + +import java.util.Map; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@AutoConfiguration +@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户 +@EnableConfigurationProperties(TenantProperties.class) +public class YudaoTenantAutoConfiguration { + + @Resource + private ApplicationContext applicationContext; + + @Bean + public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) { + return new TenantFrameworkServiceImpl(tenantApi); + } + + // ========== AOP ========== + + @Bean + public TenantIgnoreAspect tenantIgnoreAspect() { + return new TenantIgnoreAspect(); + } + + // ========== DB ========== + + @Bean + public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, + MybatisPlusInterceptor interceptor) { + TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); + // 添加到 interceptor 中 + // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 + MyBatisUtils.addInterceptor(interceptor, inner, 0); + return inner; + } + + // ========== WEB ========== + + @Bean + public FilterRegistrationBean tenantContextWebFilter(TenantProperties tenantProperties) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantContextWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); + addIgnoreUrls(tenantProperties); + return registrationBean; + } + + /** + * 如果 Controller 接口上,有 {@link TenantIgnore} 注解,那么添加到忽略的 URL 中 + * + * @param tenantProperties 租户配置 + */ + private void addIgnoreUrls(TenantProperties tenantProperties) { + // 获得接口对应的 HandlerMethod 集合 + RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) + applicationContext.getBean("requestMappingHandlerMapping"); + Map handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); + // 获得有 @TenantIgnore 注解的接口 + for (Map.Entry entry : handlerMethodMap.entrySet()) { + HandlerMethod handlerMethod = entry.getValue(); + if (!handlerMethod.hasMethodAnnotation(TenantIgnore.class)) { + continue; + } + // 添加到忽略的 URL 中 + if (entry.getKey().getPatternsCondition() != null) { + tenantProperties.getIgnoreUrls().addAll(entry.getKey().getPatternsCondition().getPatterns()); + } + if (entry.getKey().getPathPatternsCondition() != null) { + tenantProperties.getIgnoreUrls().addAll( + convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString)); + } + } + } + + @Bean + public TenantVisitContextInterceptor tenantVisitContextInterceptor(TenantProperties tenantProperties, + SecurityFrameworkService securityFrameworkService) { + return new TenantVisitContextInterceptor(tenantProperties, securityFrameworkService); + } + + @Bean + public WebMvcConfigurer tenantWebMvcConfigurer(TenantProperties tenantProperties, + TenantVisitContextInterceptor tenantVisitContextInterceptor) { + return new WebMvcConfigurer() { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(tenantVisitContextInterceptor) + .excludePathPatterns(tenantProperties.getIgnoreVisitUrls().toArray(new String[0])); + } + }; + } + + // ========== Security ========== + + @Bean + public FilterRegistrationBean tenantSecurityWebFilter(TenantProperties tenantProperties, + WebProperties webProperties, + GlobalExceptionHandler globalExceptionHandler, + TenantFrameworkService tenantFrameworkService) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties, + globalExceptionHandler, tenantFrameworkService)); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); + return registrationBean; + } + + // ========== MQ ========== + + @Bean + public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { + return new TenantRedisMessageInterceptor(); + } + + @Bean + @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") + public TenantRabbitMQInitializer tenantRabbitMQInitializer() { + return new TenantRabbitMQInitializer(); + } + + @Bean + @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate") + public TenantRocketMQInitializer tenantRocketMQInitializer() { + return new TenantRocketMQInitializer(); + } + + // ========== Job ========== + + @Bean + public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) { + return new TenantJobAspect(tenantFrameworkService); + } + + // ========== Redis ========== + + @Bean + @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean + public RedisCacheManager tenantRedisCacheManager(RedisTemplate redisTemplate, + RedisCacheConfiguration redisCacheConfiguration, + YudaoCacheProperties yudaoCacheProperties, + TenantProperties tenantProperties) { + // 创建 RedisCacheWriter 对象 + RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, + BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize())); + // 创建 TenantRedisCacheManager 对象 + return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java new file mode 100644 index 0000000..7ed9ade --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnore.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.tenant.core.aop; + +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; + +import java.lang.annotation.*; + +/** + * 忽略租户,标记指定方法不进行租户的自动过滤 + * + * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤: + * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的 + * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略 + * + * 特殊: + * 1、如果添加到 Controller 类上,则该 URL 自动添加到 {@link TenantProperties#getIgnoreUrls()} 中 + * 2、如果添加到 DO 实体类上,则它对应的表名“相当于”自动添加到 {@link TenantProperties#getIgnoreTables()} 中 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface TenantIgnore { + + /** + * 是否开启忽略租户,默认为 true 开启 + * + * 支持 Spring EL 表达式,如果返回 true 则满足条件,进行租户的忽略 + */ + String enable() default "true"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java new file mode 100644 index 0000000..91514d5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/aop/TenantIgnoreAspect.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.tenant.core.aop; + +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +/** + * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。 + * 例如说,一个定时任务,读取所有数据,进行处理。 + * 又例如说,读取所有数据,进行缓存。 + * + * 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class TenantIgnoreAspect { + + @Around("@annotation(tenantIgnore)") + public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable { + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + // 计算条件,满足的情况下,才进行忽略 + Object enable = SpringExpressionUtils.parseExpression(tenantIgnore.enable()); + if (Boolean.TRUE.equals(enable)) { + TenantContextHolder.setIgnore(true); + } + + // 执行逻辑 + return joinPoint.proceed(); + } finally { + TenantContextHolder.setIgnore(oldIgnore); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java new file mode 100644 index 0000000..4e7b5e0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.framework.tenant.core.context; + +import cn.iocoder.yudao.framework.common.enums.DocumentEnum; +import com.alibaba.ttl.TransmittableThreadLocal; + +/** + * 多租户上下文 Holder + * + * @author 芋道源码 + */ +public class TenantContextHolder { + + /** + * 当前租户编号 + */ + private static final ThreadLocal TENANT_ID = new TransmittableThreadLocal<>(); + + /** + * 是否忽略租户 + */ + private static final ThreadLocal IGNORE = new TransmittableThreadLocal<>(); + + /** + * 获得租户编号 + * + * @return 租户编号 + */ + public static Long getTenantId() { + return TENANT_ID.get(); + } + + /** + * 获得租户编号。如果不存在,则抛出 NullPointerException 异常 + * + * @return 租户编号 + */ + public static Long getRequiredTenantId() { + Long tenantId = getTenantId(); + if (tenantId == null) { + throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:" + + DocumentEnum.TENANT.getUrl()); + } + return tenantId; + } + + public static void setTenantId(Long tenantId) { + TENANT_ID.set(tenantId); + } + + public static void setIgnore(Boolean ignore) { + IGNORE.set(ignore); + } + + /** + * 当前是否忽略租户 + * + * @return 是否忽略 + */ + public static boolean isIgnore() { + return Boolean.TRUE.equals(IGNORE.get()); + } + + public static void clear() { + TENANT_ID.remove(); + IGNORE.remove(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java new file mode 100644 index 0000000..f4f0ea5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.tenant.core.db; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 拓展多租户的 BaseDO 基类 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public abstract class TenantBaseDO extends BaseDO { + + /** + * 多租户编号 + */ + private Long tenantId; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java new file mode 100644 index 0000000..47e5df0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.framework.tenant.core.db; + +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; + +import java.util.HashMap; +import java.util.Map; + +/** + * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 + * + * @author 芋道源码 + */ +public class TenantDatabaseInterceptor implements TenantLineHandler { + + /** + * 忽略的表 + * + * KEY:表名 + * VALUE:是否忽略 + */ + private final Map ignoreTables = new HashMap<>(); + + public TenantDatabaseInterceptor(TenantProperties properties) { + // 不同 DB 下,大小写的习惯不同,所以需要都添加进去 + properties.getIgnoreTables().forEach(table -> { + addIgnoreTable(table, true); + }); + // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错 + addIgnoreTable("DUAL", true); + } + + @Override + public Expression getTenantId() { + return new LongValue(TenantContextHolder.getRequiredTenantId()); + } + + @Override + public boolean ignoreTable(String tableName) { + // 情况一,全局忽略多租户 + if (TenantContextHolder.isIgnore()) { + return true; + } + // 情况二,忽略多租户的表 + tableName = SqlParserUtils.removeWrapperSymbol(tableName); + Boolean ignore = ignoreTables.get(tableName.toLowerCase()); + if (ignore == null) { + ignore = computeIgnoreTable(tableName); + synchronized (ignoreTables) { + addIgnoreTable(tableName, ignore); + } + } + return ignore; + } + + private void addIgnoreTable(String tableName, boolean ignore) { + ignoreTables.put(tableName.toLowerCase(), ignore); + ignoreTables.put(tableName.toUpperCase(), ignore); + } + + private boolean computeIgnoreTable(String tableName) { + // 找不到的表,说明不是 yudao 项目里的,不进行拦截(忽略租户) + TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName); + if (tableInfo == null) { + return true; + } + // 如果继承了 TenantBaseDO 基类,显然不忽略租户 + if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) { + return false; + } + // 如果添加了 @TenantIgnore 注解,显然也不忽略租户 + TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class); + return tenantIgnore != null; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java new file mode 100644 index 0000000..23474bb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.framework.tenant.core.job; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 多租户 Job 注解 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TenantJob { +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java new file mode 100644 index 0000000..a58887a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobAspect.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.framework.tenant.core.job; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 多租户 JobHandler AOP + * 任务执行时,会按照租户逐个执行 Job 的逻辑 + * + * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 + * + * @author 芋道源码 + */ +@Aspect +@RequiredArgsConstructor +@Slf4j +public class TenantJobAspect { + + private final TenantFrameworkService tenantFrameworkService; + + @Around("@annotation(tenantJob)") + public String around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) { + // 获得租户列表 + List tenantIds = tenantFrameworkService.getTenantIds(); + if (CollUtil.isEmpty(tenantIds)) { + return null; + } + + // 逐个租户,执行 Job + Map results = new ConcurrentHashMap<>(); + tenantIds.parallelStream().forEach(tenantId -> { + // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况 + TenantUtils.execute(tenantId, () -> { + try { + Object result = joinPoint.proceed(); + results.put(tenantId, StrUtil.toStringOrEmpty(result)); + } catch (Throwable e) { + log.error("[execute][租户({}) 执行 Job 发生异常", tenantId, e); + results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); + } + }); + }); + return JsonUtils.toJsonString(results); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java new file mode 100644 index 0000000..8bf7cc1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.tenant.core.mq.kafka; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类 + * + * Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器 + * + * @author 芋道源码 + */ +@Slf4j +public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor { + + private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes"; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + // 添加 TenantKafkaProducerInterceptor 拦截器 + try { + String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES); + if (StrUtil.isEmpty(value)) { + value = TenantKafkaProducerInterceptor.class.getName(); + } else { + value += "," + TenantKafkaProducerInterceptor.class.getName(); + } + environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value); + } catch (NoClassDefFoundError ignore) { + // 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖 + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java new file mode 100644 index 0000000..8ded801 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.tenant.core.mq.kafka; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import org.apache.kafka.clients.producer.ProducerInterceptor; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; +import org.apache.kafka.common.header.Headers; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类 + * + * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 + * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 + * + * @author 芋道源码 + */ +public class TenantKafkaProducerInterceptor implements ProducerInterceptor { + + @Override + public ProducerRecord onSend(ProducerRecord record) { + Long tenantId = TenantContextHolder.getTenantId(); + if (tenantId != null) { + Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射 + headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes()); + } + return record; + } + + @Override + public void onAcknowledgement(RecordMetadata metadata, Exception exception) { + } + + @Override + public void close() { + } + + @Override + public void configure(Map configs) { + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java new file mode 100644 index 0000000..b856ce9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq; + +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * 多租户的 RabbitMQ 初始化器 + * + * @author 芋道源码 + */ +public class TenantRabbitMQInitializer implements BeanPostProcessor { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof RabbitTemplate) { + RabbitTemplate rabbitTemplate = (RabbitTemplate) bean; + rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor()); + } + return bean; + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java new file mode 100644 index 0000000..3e6969c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq; + +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import org.apache.kafka.clients.producer.ProducerInterceptor; +import org.springframework.amqp.AmqpException; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类 + * + * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 + * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 + * + * @author 芋道源码 + */ +public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor { + + @Override + public Message postProcessMessage(Message message) throws AmqpException { + Long tenantId = TenantContextHolder.getTenantId(); + if (tenantId != null) { + message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId); + } + return message; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java new file mode 100644 index 0000000..f6b7747 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.tenant.core.mq.redis; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * 多租户 {@link AbstractRedisMessage} 拦截器 + * + * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 + * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中 + * + * @author 芋道源码 + */ +public class TenantRedisMessageInterceptor implements RedisMessageInterceptor { + + @Override + public void sendMessageBefore(AbstractRedisMessage message) { + Long tenantId = TenantContextHolder.getTenantId(); + if (tenantId != null) { + message.addHeader(HEADER_TENANT_ID, tenantId.toString()); + } + } + + @Override + public void consumeMessageBefore(AbstractRedisMessage message) { + String tenantIdStr = message.getHeader(HEADER_TENANT_ID); + if (StrUtil.isNotEmpty(tenantIdStr)) { + TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr)); + } + } + + @Override + public void consumeMessageAfter(AbstractRedisMessage message) { + // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况 + TenantContextHolder.clear(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java new file mode 100644 index 0000000..d9d7334 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import org.apache.rocketmq.client.hook.ConsumeMessageContext; +import org.apache.rocketmq.client.hook.ConsumeMessageHook; +import org.apache.rocketmq.common.message.MessageExt; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; + +import java.util.List; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类 + * + * Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 + * + * @author 芋道源码 + */ +public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook { + + @Override + public String hookName() { + return getClass().getSimpleName(); + } + + @Override + public void consumeMessageBefore(ConsumeMessageContext context) { + // 校验,消息必须是单条,不然设置租户可能不正确 + List messages = context.getMsgList(); + Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size()); + // 设置租户编号 + String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID); + if (StrUtil.isNotEmpty(tenantId)) { + TenantContextHolder.setTenantId(Long.parseLong(tenantId)); + } + } + + @Override + public void consumeMessageAfter(ConsumeMessageContext context) { + TenantContextHolder.clear(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java new file mode 100644 index 0000000..7f12ac5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * 多租户的 RocketMQ 初始化器 + * + * @author 芋道源码 + */ +public class TenantRocketMQInitializer implements BeanPostProcessor { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof DefaultRocketMQListenerContainer) { + DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean; + initTenantConsumer(container.getConsumer()); + } else if (bean instanceof RocketMQTemplate) { + RocketMQTemplate template = (RocketMQTemplate) bean; + initTenantProducer(template.getProducer()); + } + return bean; + } + + private void initTenantProducer(DefaultMQProducer producer) { + if (producer == null) { + return; + } + DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl(); + if (producerImpl == null) { + return; + } + producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook()); + } + + private void initTenantConsumer(DefaultMQPushConsumer consumer) { + if (consumer == null) { + return; + } + DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl(); + if (consumerImpl == null) { + return; + } + consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook()); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java new file mode 100644 index 0000000..4f03074 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq; + +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.hook.SendMessageHook; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类 + * + * Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 + * + * @author 芋道源码 + */ +public class TenantRocketMQSendMessageHook implements SendMessageHook { + + @Override + public String hookName() { + return getClass().getSimpleName(); + } + + @Override + public void sendMessageBefore(SendMessageContext sendMessageContext) { + Long tenantId = TenantContextHolder.getTenantId(); + if (tenantId == null) { + return; + } + sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString()); + } + + @Override + public void sendMessageAfter(SendMessageContext sendMessageContext) { + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisCacheManager.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisCacheManager.java new file mode 100644 index 0000000..aeea4b5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisCacheManager.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.tenant.core.redis; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; + +import java.util.Set; + +/** + * 多租户的 {@link RedisCacheManager} 实现类 + * + * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀 + * + * @author airhead + */ +@Slf4j +public class TenantRedisCacheManager extends TimeoutRedisCacheManager { + + private final Set ignoreCaches; + + public TenantRedisCacheManager(RedisCacheWriter cacheWriter, + RedisCacheConfiguration defaultCacheConfiguration, + Set ignoreCaches) { + super(cacheWriter, defaultCacheConfiguration); + this.ignoreCaches = ignoreCaches; + } + + @Override + public Cache getCache(String name) { + // 如果开启多租户,则 name 拼接租户后缀 + if (!TenantContextHolder.isIgnore() + && TenantContextHolder.getTenantId() != null + && !CollUtil.contains(ignoreCaches, name)) { + name = name + ":" + TenantContextHolder.getTenantId(); + } + + // 继续基于父方法 + return super.getCache(name); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java new file mode 100644 index 0000000..c6d4d5e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.framework.tenant.core.security; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.AntPathMatcher; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * 多租户 Security Web 过滤器 + * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。 + * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。 + * 3. 校验租户是合法,例如说被禁用、到期 + * + * @author 芋道源码 + */ +@Slf4j +public class TenantSecurityWebFilter extends ApiRequestFilter { + + private final TenantProperties tenantProperties; + + private final AntPathMatcher pathMatcher; + + private final GlobalExceptionHandler globalExceptionHandler; + private final TenantFrameworkService tenantFrameworkService; + + public TenantSecurityWebFilter(TenantProperties tenantProperties, + WebProperties webProperties, + GlobalExceptionHandler globalExceptionHandler, + TenantFrameworkService tenantFrameworkService) { + super(webProperties); + this.tenantProperties = tenantProperties; + this.pathMatcher = new AntPathMatcher(); + this.globalExceptionHandler = globalExceptionHandler; + this.tenantFrameworkService = tenantFrameworkService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + Long tenantId = TenantContextHolder.getTenantId(); + // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。 + LoginUser user = SecurityFrameworkUtils.getLoginUser(); + if (user != null) { + // 如果获取不到租户编号,则尝试使用登陆用户的租户编号 + if (tenantId == null) { + tenantId = user.getTenantId(); + TenantContextHolder.setTenantId(tenantId); + // 如果传递了租户编号,则进行比对租户编号,避免越权问题 + } else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { + log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", + user.getTenantId(), user.getId(), user.getUserType(), + TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); + ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), + "您无权访问该租户的数据")); + return; + } + } + + // 如果非允许忽略租户的 URL,则校验租户是否合法 + if (!isIgnoreUrl(request)) { + // 2. 如果请求未带租户的编号,不允许访问。 + if (tenantId == null) { + log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod()); + ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), + "请求的租户标识未传递,请进行排查")); + return; + } + // 3. 校验租户是合法,例如说被禁用、到期 + try { + tenantFrameworkService.validTenant(tenantId); + } catch (Throwable ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错 + if (tenantId == null) { + TenantContextHolder.setIgnore(true); + } + } + + // 继续过滤 + chain.doFilter(request, response); + } + + private boolean isIgnoreUrl(HttpServletRequest request) { + // 快速匹配,保证性能 + if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) { + return true; + } + // 逐个 Ant 路径匹配 + for (String url : tenantProperties.getIgnoreUrls()) { + if (pathMatcher.match(url, request.getRequestURI())) { + return true; + } + } + return false; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java new file mode 100644 index 0000000..2ca474d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.tenant.core.service; + +import java.util.List; + +/** + * Tenant 框架 Service 接口,定义获取租户信息 + * + * @author 芋道源码 + */ +public interface TenantFrameworkService { + + /** + * 获得所有租户 + * + * @return 租户编号数组 + */ + List getTenantIds(); + + /** + * 校验租户是否合法 + * + * @param id 租户编号 + */ + void validTenant(Long id); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkServiceImpl.java new file mode 100644 index 0000000..970b88a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkServiceImpl.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.framework.tenant.core.service; + +import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.util.cache.CacheUtils; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +import java.time.Duration; +import java.util.List; + +/** + * Tenant 框架 Service 实现类 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class TenantFrameworkServiceImpl implements TenantFrameworkService { + + private static final ServiceException SERVICE_EXCEPTION_NULL = new ServiceException(); + + private final TenantCommonApi tenantApi; + + /** + * 针对 {@link #getTenantIds()} 的缓存 + */ + private final LoadingCache> getTenantIdsCache = CacheUtils.buildAsyncReloadingCache( + Duration.ofMinutes(1L), // 过期时间 1 分钟 + new CacheLoader>() { + + @Override + public List load(Object key) { + return tenantApi.getTenantIdList(); + } + + }); + + /** + * 针对 {@link #validTenant(Long)} 的缓存 + */ + private final LoadingCache validTenantCache = CacheUtils.buildAsyncReloadingCache( + Duration.ofMinutes(1L), // 过期时间 1 分钟 + new CacheLoader() { + + @Override + public ServiceException load(Long id) { + try { + tenantApi.validateTenant(id); + return SERVICE_EXCEPTION_NULL; + } catch (ServiceException ex) { + return ex; + } + } + + }); + + @Override + @SneakyThrows + public List getTenantIds() { + return getTenantIdsCache.get(Boolean.TRUE); + } + + @Override + public void validTenant(Long id) { + ServiceException serviceException = validTenantCache.getUnchecked(id); + if (serviceException != SERVICE_EXCEPTION_NULL) { + throw serviceException; + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java new file mode 100644 index 0000000..b05b3c0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.framework.tenant.core.util; + +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; + +import java.util.Map; +import java.util.concurrent.Callable; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * 多租户 Util + * + * @author 芋道源码 + */ +public class TenantUtils { + + /** + * 使用指定租户,执行对应的逻辑 + * + * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 + * 当然,执行完成后,还是会恢复回去 + * + * @param tenantId 租户编号 + * @param runnable 逻辑 + */ + public static void execute(Long tenantId, Runnable runnable) { + Long oldTenantId = TenantContextHolder.getTenantId(); + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setTenantId(tenantId); + TenantContextHolder.setIgnore(false); + // 执行逻辑 + runnable.run(); + } finally { + TenantContextHolder.setTenantId(oldTenantId); + TenantContextHolder.setIgnore(oldIgnore); + } + } + + /** + * 使用指定租户,执行对应的逻辑 + * + * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 + * 当然,执行完成后,还是会恢复回去 + * + * @param tenantId 租户编号 + * @param callable 逻辑 + * @return 结果 + */ + public static V execute(Long tenantId, Callable callable) { + Long oldTenantId = TenantContextHolder.getTenantId(); + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setTenantId(tenantId); + TenantContextHolder.setIgnore(false); + // 执行逻辑 + return callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + TenantContextHolder.setTenantId(oldTenantId); + TenantContextHolder.setIgnore(oldIgnore); + } + } + + /** + * 忽略租户,执行对应的逻辑 + * + * @param runnable 逻辑 + */ + public static void executeIgnore(Runnable runnable) { + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setIgnore(true); + // 执行逻辑 + runnable.run(); + } finally { + TenantContextHolder.setIgnore(oldIgnore); + } + } + + /** + * 忽略租户,执行对应的逻辑 + * + * @param callable 逻辑 + * @return 结果 + */ + public static V executeIgnore(Callable callable) { + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setIgnore(true); + // 执行逻辑 + return callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + TenantContextHolder.setIgnore(oldIgnore); + } + } + + /** + * 将多租户编号,添加到 header 中 + * + * @param headers HTTP 请求 headers + * @param tenantId 租户编号 + */ + public static void addTenantHeader(Map headers, Long tenantId) { + if (tenantId != null) { + headers.put(HEADER_TENANT_ID, tenantId.toString()); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java new file mode 100644 index 0000000..41b7b4d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.tenant.core.web; + +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 多租户 Context Web 过滤器 + * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 + * + * @author 芋道源码 + */ +public class TenantContextWebFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + // 设置 + Long tenantId = WebFrameworkUtils.getTenantId(request); + if (tenantId != null) { + TenantContextHolder.setTenantId(tenantId); + } + try { + chain.doFilter(request, response); + } finally { + // 清理 + TenantContextHolder.clear(); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java new file mode 100644 index 0000000..9ac70d8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.framework.tenant.core.web; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.HandlerInterceptor; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; + +@RequiredArgsConstructor +@Slf4j +public class TenantVisitContextInterceptor implements HandlerInterceptor { + + private static final String PERMISSION = "system:tenant:visit"; + + private final TenantProperties tenantProperties; + + private final SecurityFrameworkService securityFrameworkService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 如果和当前租户编号一致,则直接跳过 + Long visitTenantId = WebFrameworkUtils.getVisitTenantId(request); + if (visitTenantId == null) { + return true; + } + if (ObjUtil.equal(visitTenantId, TenantContextHolder.getTenantId())) { + return true; + } + // 必须是登录用户 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return true; + } + + // 校验用户是否可切换租户 + if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) { + throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换租户"); + } + + // 【重点】切换租户编号 + loginUser.setVisitTenantId(visitTenantId); + TenantContextHolder.setTenantId(visitTenantId); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + // 【重点】清理切换,换回原租户编号 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser != null && loginUser.getTenantId() != null) { + TenantContextHolder.setTenantId(loginUser.getTenantId()); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java new file mode 100644 index 0000000..aa22cdb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java @@ -0,0 +1,17 @@ +/** + * 多租户,支持如下层面: + * 1. DB:基于 MyBatis Plus 多租户的功能实现。 + * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。 + * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。 + * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。 + * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见: + * 1)Spring Async: + * {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} + * 2)Spring Security: + * TransmittableThreadLocalSecurityContextHolderStrategy + * 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 + * + */ +package cn.iocoder.yudao.framework.tenant; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java new file mode 100644 index 0000000..de330fd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java @@ -0,0 +1,275 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed 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 + * + * https://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. + */ + +package org.springframework.messaging.handler.invocation; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; + +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.HandlerMethod; +import org.springframework.util.ObjectUtils; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * Extension of {@link HandlerMethod} that invokes the underlying method with + * argument values resolved from the current HTTP request through a list of + * {@link HandlerMethodArgumentResolver}. + * + * 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中 + * TODO 芋艿:持续跟进,看看有没新的拓展点 + * + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 4.0 + */ +public class InvocableHandlerMethod extends HandlerMethod { + + private static final Object[] EMPTY_ARGS = new Object[0]; + + + private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); + + private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + + /** + * Create an instance from a {@code HandlerMethod}. + */ + public InvocableHandlerMethod(HandlerMethod handlerMethod) { + super(handlerMethod); + } + + /** + * Create an instance from a bean instance and a method. + */ + public InvocableHandlerMethod(Object bean, Method method) { + super(bean, method); + } + + /** + * Construct a new handler method with the given bean instance, method name and parameters. + * @param bean the object bean + * @param methodName the method name + * @param parameterTypes the method parameter types + * @throws NoSuchMethodException when the method cannot be found + */ + public InvocableHandlerMethod(Object bean, String methodName, Class... parameterTypes) + throws NoSuchMethodException { + + super(bean, methodName, parameterTypes); + } + + + /** + * Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values. + */ + public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) { + this.resolvers = argumentResolvers; + } + + /** + * Set the ParameterNameDiscoverer for resolving parameter names when needed + * (e.g. default request attribute name). + *

Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}. + */ + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + + /** + * Invoke the method after resolving its argument values in the context of the given message. + *

Argument values are commonly resolved through + * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. + * The {@code providedArgs} parameter however may supply argument values to be used directly, + * i.e. without argument resolution. + *

Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the + * resolved arguments. + * @param message the current message being processed + * @param providedArgs "given" arguments matched by type, not resolved + * @return the raw value returned by the invoked method + * @throws Exception raised if no suitable argument resolver can be found, + * or if the method raised an exception + * @see #getMethodArgumentValues + * @see #doInvoke + */ + @Nullable + public Object invoke(Message message, Object... providedArgs) throws Exception { + Object[] args = getMethodArgumentValues(message, providedArgs); + if (logger.isTraceEnabled()) { + logger.trace("Arguments: " + Arrays.toString(args)); + } + // 注意:如下是本类的改动点!!! + // 情况一:无租户编号的情况 + Long tenantId= parseTenantId(message); + if (tenantId == null) { + return doInvoke(args); + } + // 情况二:有租户的情况下 + return TenantUtils.execute(tenantId, () -> doInvoke(args)); + } + + private Long parseTenantId(Message message) { + Object tenantId = message.getHeaders().get(HEADER_TENANT_ID); + if (tenantId == null) { + return null; + } + if (tenantId instanceof Long) { + return (Long) tenantId; + } + if (tenantId instanceof Number) { + return ((Number) tenantId).longValue(); + } + if (tenantId instanceof String) { + return Long.parseLong((String) tenantId); + } + if (tenantId instanceof byte[]) { + return Long.parseLong(new String((byte[]) tenantId)); + } + throw new IllegalArgumentException("未知的数据类型:" + tenantId); + } + + /** + * Get the method argument values for the current message, checking the provided + * argument values and falling back to the configured argument resolvers. + *

The resulting array will be passed into {@link #doInvoke}. + * @since 5.1.2 + */ + protected Object[] getMethodArgumentValues(Message message, Object... providedArgs) throws Exception { + MethodParameter[] parameters = getMethodParameters(); + if (ObjectUtils.isEmpty(parameters)) { + return EMPTY_ARGS; + } + + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + args[i] = findProvidedArgument(parameter, providedArgs); + if (args[i] != null) { + continue; + } + if (!this.resolvers.supportsParameter(parameter)) { + throw new MethodArgumentResolutionException( + message, parameter, formatArgumentError(parameter, "No suitable resolver")); + } + try { + args[i] = this.resolvers.resolveArgument(parameter, message); + } + catch (Exception ex) { + // Leave stack trace for later, exception may actually be resolved and handled... + if (logger.isDebugEnabled()) { + String exMsg = ex.getMessage(); + if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { + logger.debug(formatArgumentError(parameter, exMsg)); + } + } + throw ex; + } + } + return args; + } + + /** + * Invoke the handler method with the given argument values. + */ + @Nullable + protected Object doInvoke(Object... args) throws Exception { + try { + return getBridgedMethod().invoke(getBean(), args); + } + catch (IllegalArgumentException ex) { + assertTargetBean(getBridgedMethod(), getBean(), args); + String text = (ex.getMessage() == null || ex.getCause() instanceof NullPointerException) ? + "Illegal argument": ex.getMessage(); + throw new IllegalStateException(formatInvokeError(text, args), ex); + } + catch (InvocationTargetException ex) { + // Unwrap for HandlerExceptionResolvers ... + Throwable targetException = ex.getTargetException(); + if (targetException instanceof RuntimeException runtimeException) { + throw runtimeException; + } + else if (targetException instanceof Error error) { + throw error; + } + else if (targetException instanceof Exception exception) { + throw exception; + } + else { + throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException); + } + } + } + + MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) { + return new AsyncResultMethodParameter(returnValue); + } + + + private class AsyncResultMethodParameter extends AnnotatedMethodParameter { + + @Nullable + private final Object returnValue; + + private final ResolvableType returnType; + + public AsyncResultMethodParameter(@Nullable Object returnValue) { + super(-1); + this.returnValue = returnValue; + this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(); + } + + protected AsyncResultMethodParameter(AsyncResultMethodParameter original) { + super(original); + this.returnValue = original.returnValue; + this.returnType = original.returnType; + } + + @Override + public Class getParameterType() { + if (this.returnValue != null) { + return this.returnValue.getClass(); + } + if (!ResolvableType.NONE.equals(this.returnType)) { + return this.returnType.toClass(); + } + return super.getParameterType(); + } + + @Override + public Type getGenericParameterType() { + return this.returnType.getType(); + } + + @Override + public AsyncResultMethodParameter clone() { + return new AsyncResultMethodParameter(this); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..a495842 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ + cn.iocoder.yudao.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..603831e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.framework.tenant.config.YudaoTenantAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/pom.xml new file mode 100644 index 0000000..0413986 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/pom.xml @@ -0,0 +1,73 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-excel + jar + + ${project.artifactId} + Excel 拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + + com.alibaba + easyexcel + + + + com.google.guava + guava + + + + org.apache.commons + commons-compress + + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-ip + true + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/config/YudaoDictAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/config/YudaoDictAutoConfiguration.java new file mode 100644 index 0000000..5dea47f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/config/YudaoDictAutoConfiguration.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.framework.dict.config; + +import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class YudaoDictAutoConfiguration { + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public DictFrameworkUtils dictUtils(DictDataCommonApi dictDataApi) { + DictFrameworkUtils.init(dictDataApi); + return new DictFrameworkUtils(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java new file mode 100644 index 0000000..9fc67bf --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.framework.dict.core; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi; +import cn.iocoder.yudao.framework.common.util.cache.CacheUtils; +import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 字典工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class DictFrameworkUtils { + + private static DictDataCommonApi dictDataApi; + + /** + * 针对 dictType 的字段数据缓存 + */ + private static final LoadingCache> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( + Duration.ofMinutes(1L), // 过期时间 1 分钟 + new CacheLoader>() { + + @Override + public List load(String dictType) { + return dictDataApi.getDictDataList(dictType); + } + + }); + + public static void init(DictDataCommonApi dictDataApi) { + DictFrameworkUtils.dictDataApi = dictDataApi; + log.info("[init][初始化 DictFrameworkUtils 成功]"); + } + + public static void clearCache() { + GET_DICT_DATA_CACHE.invalidateAll(); + } + + @SneakyThrows + public static String parseDictDataLabel(String dictType, Integer value) { + if (value == null) { + return null; + } + return parseDictDataLabel(dictType, String.valueOf(value)); + } + + @SneakyThrows + public static String parseDictDataLabel(String dictType, String value) { + List dictDatas = GET_DICT_DATA_CACHE.get(dictType); + DictDataRespDTO dictData = CollUtil.findOne(dictDatas, data -> Objects.equals(data.getValue(), value)); + return dictData != null ? dictData.getLabel(): null; + } + + @SneakyThrows + public static List getDictDataLabelList(String dictType) { + List dictDatas = GET_DICT_DATA_CACHE.get(dictType); + return convertList(dictDatas, DictDataRespDTO::getLabel); + } + + @SneakyThrows + public static String parseDictDataValue(String dictType, String label) { + List dictDatas = GET_DICT_DATA_CACHE.get(dictType); + DictDataRespDTO dictData = CollUtil.findOne(dictDatas, data -> Objects.equals(data.getLabel(), label)); + return dictData!= null ? dictData.getValue(): null; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/package-info.java new file mode 100644 index 0000000..fd87fda --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/package-info.java @@ -0,0 +1,6 @@ +/** + * 字典数据模块,提供 {@link cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils} 工具类 + * + * 通过将字典缓存在内存中,保证性能 + */ +package cn.iocoder.yudao.framework.dict; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/DictFormat.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/DictFormat.java new file mode 100644 index 0000000..0d898b4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/DictFormat.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.excel.core.annotations; + +import java.lang.annotation.*; + +/** + * 字典格式化 + * + * 实现将字典数据的值,格式化成字典数据的标签 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface DictFormat { + + /** + * 例如说,SysDictTypeConstants、InfDictTypeConstants + * + * @return 字典类型 + */ + String value(); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/ExcelColumnSelect.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/ExcelColumnSelect.java new file mode 100644 index 0000000..b4ff140 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/annotations/ExcelColumnSelect.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.excel.core.annotations; + +import java.lang.annotation.*; + +/** + * 给 Excel 列添加下拉选择数据 + * + * 其中 {@link #dictType()} 和 {@link #functionName()} 二选一 + * + * @author HUIHUI + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface ExcelColumnSelect { + + /** + * @return 字典类型 + */ + String dictType() default ""; + + /** + * @return 获取下拉数据源的方法名称 + */ + String functionName() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java new file mode 100644 index 0000000..9778b17 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.excel.core.convert; + +import cn.hutool.core.convert.Convert; +import cn.iocoder.yudao.framework.ip.core.Area; +import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; +import lombok.extern.slf4j.Slf4j; + +/** + * Excel 数据地区转换器 + * + * @author HUIHUI + */ +@Slf4j +public class AreaConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 解析地区编号 + String label = readCellData.getStringValue(); + Area area = AreaUtils.parseArea(label); + if (area == null) { + log.error("[convertToJavaData][label({}) 解析不掉]", label); + return null; + } + // 将 value 转换成对应的属性 + Class fieldClazz = contentProperty.getField().getType(); + return Convert.convert(fieldClazz, area.getId()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java new file mode 100644 index 0000000..e393195 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.framework.excel.core.convert; + +import cn.hutool.core.convert.Convert; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; +import lombok.extern.slf4j.Slf4j; + +/** + * Excel 数据字典转换器 + * + * @author 芋道源码 + */ +@Slf4j +public class DictConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 使用字典解析 + String type = getType(contentProperty); + String label = readCellData.getStringValue(); + String value = DictFrameworkUtils.parseDictDataValue(type, label); + if (value == null) { + log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label); + return null; + } + // 将 String 的 value 转换成对应的属性 + Class fieldClazz = contentProperty.getField().getType(); + return Convert.convert(fieldClazz, value); + } + + @Override + public WriteCellData convertToExcelData(Object object, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 空时,返回空 + if (object == null) { + return new WriteCellData<>(""); + } + + // 使用字典格式化 + String type = getType(contentProperty); + String value = String.valueOf(object); + String label = DictFrameworkUtils.parseDictDataLabel(type, value); + if (label == null) { + log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value); + return new WriteCellData<>(""); + } + // 生成 Excel 小表格 + return new WriteCellData<>(label); + } + + private static String getType(ExcelContentProperty contentProperty) { + return contentProperty.getField().getAnnotation(DictFormat.class).value(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java new file mode 100644 index 0000000..0d4794e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.excel.core.convert; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +/** + * Excel Json 转换器 + * + * @author 芋道源码 + */ +public class JsonConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public WriteCellData convertToExcelData(Object value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 生成 Excel 小表格 + return new WriteCellData<>(JsonUtils.toJsonString(value)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java new file mode 100644 index 0000000..ee66fe7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.excel.core.convert; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 金额转换器 + * + * 金额单位:分 + * + * @author 芋道源码 + */ +public class MoneyConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public WriteCellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + BigDecimal result = BigDecimal.valueOf(value) + .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP); + return new WriteCellData<>(result.toString()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/function/ExcelColumnSelectFunction.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/function/ExcelColumnSelectFunction.java new file mode 100644 index 0000000..51953c4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/function/ExcelColumnSelectFunction.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.excel.core.function; + +import java.util.List; + +/** + * Excel 列下拉数据源获取接口 + * + * 为什么不直接解析字典还搞个接口?考虑到有的下拉数据不是从字典中获取的所有需要做一个兼容 + + * @author HUIHUI + */ +public interface ExcelColumnSelectFunction { + + /** + * 获得方法名称 + * + * @return 方法名称 + */ + String getName(); + + /** + * 获得列下拉数据源 + * + * @return 下拉数据源 + */ + List getOptions(); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java new file mode 100644 index 0000000..8e3e28e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java @@ -0,0 +1,188 @@ +package cn.iocoder.yudao.framework.excel.core.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect; +import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction; +import com.alibaba.excel.annotation.ExcelIgnore; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.write.handler.SheetWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.hssf.usermodel.HSSFDataValidation; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddressList; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 基于固定 sheet 实现下拉框 + * + * @author HUIHUI + */ +@Slf4j +public class SelectSheetWriteHandler implements SheetWriteHandler { + + /** + * 数据起始行从 0 开始 + * + * 约定:本项目第一行有标题所以从 1 开始如果您的 Excel 有多行标题请自行更改 + */ + public static final int FIRST_ROW = 1; + /** + * 下拉列需要创建下拉框的行数,默认两千行如需更多请自行调整 + */ + public static final int LAST_ROW = 2000; + + private static final String DICT_SHEET_NAME = "字典sheet"; + + /** + * key: 列 value: 下拉数据源 + */ + private final Map> selectMap = new HashMap<>(); + + public SelectSheetWriteHandler(Class head) { + // 解析下拉数据 + int colIndex = 0; + boolean ignoreUnannotated = head.isAnnotationPresent(ExcelIgnoreUnannotated.class); + for (Field field : head.getDeclaredFields()) { + // 关联 https://github.com/YunaiV/ruoyi-vue-pro/pull/853 + // 1.1 忽略 static final 或 transient 的字段 + if (isStaticFinalOrTransient(field) ) { + continue; + } + // 1.2 忽略的字段跳过 + if ((ignoreUnannotated && !field.isAnnotationPresent(ExcelProperty.class)) + || field.isAnnotationPresent(ExcelIgnore.class)) { + continue; + } + + // 2. 核心:处理有 ExcelColumnSelect 注解的字段 + if (field.isAnnotationPresent(ExcelColumnSelect.class)) { + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + if (excelProperty != null && excelProperty.index() != -1) { + colIndex = excelProperty.index(); + } + getSelectDataList(colIndex, field); + } + colIndex++; + } + } + + /** + * 判断字段是否是静态的、最终的、 transient 的 + * 原因:EasyExcel 默认是忽略 static final 或 transient 的字段,所以需要判断 + * + * @param field 字段 + * @return 是否是静态的、最终的、transient 的 + */ + private boolean isStaticFinalOrTransient(Field field) { + return (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) + || Modifier.isTransient(field.getModifiers()); + } + + + /** + * 获得下拉数据,并添加到 {@link #selectMap} 中 + * + * @param colIndex 列索引 + * @param field 字段 + */ + private void getSelectDataList(int colIndex, Field field) { + ExcelColumnSelect columnSelect = field.getAnnotation(ExcelColumnSelect.class); + String dictType = columnSelect.dictType(); + String functionName = columnSelect.functionName(); + Assert.isTrue(ObjectUtil.isNotEmpty(dictType) || ObjectUtil.isNotEmpty(functionName), + "Field({}) 的 @ExcelColumnSelect 注解,dictType 和 functionName 不能同时为空", field.getName()); + + // 情况一:使用 dictType 获得下拉数据 + if (StrUtil.isNotEmpty(dictType)) { // 情况一: 字典数据 (默认) + selectMap.put(colIndex, DictFrameworkUtils.getDictDataLabelList(dictType)); + return; + } + + // 情况二:使用 functionName 获得下拉数据 + Map functionMap = SpringUtil.getApplicationContext().getBeansOfType(ExcelColumnSelectFunction.class); + ExcelColumnSelectFunction function = CollUtil.findOne(functionMap.values(), item -> item.getName().equals(functionName)); + Assert.notNull(function, "未找到对应的 function({})", functionName); + selectMap.put(colIndex, function.getOptions()); + } + + @Override + public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { + if (CollUtil.isEmpty(selectMap)) { + return; + } + + // 1. 获取相应操作对象 + DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); // 需要设置下拉框的 sheet 页的数据验证助手 + Workbook workbook = writeWorkbookHolder.getWorkbook(); // 获得工作簿 + List>> keyValues = convertList(selectMap.entrySet(), entry -> new KeyValue<>(entry.getKey(), entry.getValue())); + keyValues.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错 + + // 2. 创建数据字典的 sheet 页 + Sheet dictSheet = workbook.createSheet(DICT_SHEET_NAME); + for (KeyValue> keyValue : keyValues) { + int rowLength = keyValue.getValue().size(); + // 2.1 设置字典 sheet 页的值,每一列一个字典项 + for (int i = 0; i < rowLength; i++) { + Row row = dictSheet.getRow(i); + if (row == null) { + row = dictSheet.createRow(i); + } + row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i)); + } + // 2.2 设置单元格下拉选择 + setColumnSelect(writeSheetHolder, workbook, helper, keyValue); + } + } + + /** + * 设置单元格下拉选择 + */ + private static void setColumnSelect(WriteSheetHolder writeSheetHolder, Workbook workbook, DataValidationHelper helper, + KeyValue> keyValue) { + // 1.1 创建可被其他单元格引用的名称 + Name name = workbook.createName(); + String excelColumn = ExcelUtil.indexToColName(keyValue.getKey()); + // 1.2 下拉框数据来源 eg:字典sheet!$B1:$B2 + String refers = DICT_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + keyValue.getValue().size(); + name.setNameName("dict" + keyValue.getKey()); // 设置名称的名字 + name.setRefersToFormula(refers); // 设置公式 + + // 2.1 设置约束 + DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); // 设置引用约束 + // 设置下拉单元格的首行、末行、首列、末列 + CellRangeAddressList rangeAddressList = new CellRangeAddressList(FIRST_ROW, LAST_ROW, + keyValue.getKey(), keyValue.getKey()); + DataValidation validation = helper.createValidation(constraint, rangeAddressList); + if (validation instanceof HSSFDataValidation) { + validation.setSuppressDropDownArrow(false); + } else { + validation.setSuppressDropDownArrow(true); + validation.setShowErrorBox(true); + } + // 2.2 阻止输入非下拉框的值 + validation.setErrorStyle(DataValidation.ErrorStyle.STOP); + validation.createErrorBox("提示", "此值不存在于下拉选择中!"); + // 2.3 添加下拉框约束 + writeSheetHolder.getSheet().addValidationData(validation); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java new file mode 100644 index 0000000..eb037d9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.excel.core.util; + +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.converters.longconverter.LongStringConverter; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +/** + * Excel 工具类 + * + * @author 芋道源码 + */ +public class ExcelUtils { + + /** + * 将列表以 Excel 响应给前端 + * + * @param response 响应 + * @param filename 文件名 + * @param sheetName Excel sheet 名 + * @param head Excel head 头 + * @param data 数据列表哦 + * @param 泛型,保证 head 和 data 类型的一致性 + * @throws IOException 写入失败的情况 + */ + public static void write(HttpServletResponse response, String filename, String sheetName, + Class head, List data) throws IOException { + // 输出 Excel + EasyExcel.write(response.getOutputStream(), head) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框 + .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 + .sheet(sheetName).doWrite(data); + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 + response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); + response.setContentType("application/vnd.ms-excel;charset=UTF-8"); + } + + public static List read(MultipartFile file, Class head) throws IOException { + return EasyExcel.read(file.getInputStream(), head, null) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .doReadAllSync(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java new file mode 100644 index 0000000..53bc5c0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 EasyExcel 实现 Excel 相关的操作 + */ +package cn.iocoder.yudao.framework.excel; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..2125cee --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.framework.dict.config.YudaoDictAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/dict/core/util/DictFrameworkUtilsTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/dict/core/util/DictFrameworkUtilsTest.java new file mode 100644 index 0000000..fa0d172 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/dict/core/util/DictFrameworkUtilsTest.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.framework.dict.core.util; + +import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi; +import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link DictFrameworkUtils} 的单元测试 + */ +public class DictFrameworkUtilsTest extends BaseMockitoUnitTest { + + @Mock + private DictDataCommonApi dictDataApi; + + @BeforeEach + public void setUp() { + DictFrameworkUtils.init(dictDataApi); + DictFrameworkUtils.clearCache(); + } + + @Test + public void testParseDictDataLabel() { + // mock 数据 + List dictDatas = List.of( + randomPojo(DictDataRespDTO.class, o -> o.setDictType("animal").setValue("cat").setLabel("猫")), + randomPojo(DictDataRespDTO.class, o -> o.setDictType("animal").setValue("dog").setLabel("狗")) + ); + // mock 方法 + when(dictDataApi.getDictDataList(eq("animal"))).thenReturn(dictDatas); + + // 断言返回值 + assertEquals("狗", DictFrameworkUtils.parseDictDataLabel("animal", "dog")); + } + + @Test + public void testParseDictDataValue() { + // mock 数据 + List dictDatas = List.of( + randomPojo(DictDataRespDTO.class, o -> o.setDictType("animal").setValue("cat").setLabel("猫")), + randomPojo(DictDataRespDTO.class, o -> o.setDictType("animal").setValue("dog").setLabel("狗")) + ); + // mock 方法 + when(dictDataApi.getDictDataList(eq("animal"))).thenReturn(dictDatas); + + // 断言返回值 + assertEquals("dog", DictFrameworkUtils.parseDictDataValue("animal", "狗")); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/pom.xml new file mode 100644 index 0000000..c17baf4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/pom.xml @@ -0,0 +1,41 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-job + jar + + ${project.artifactId} + 任务拓展 + 1. 定时任务,基于 Quartz 拓展 + 2. 异步任务,基于 Spring Async 拓展 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + org.springframework.boot + spring-boot-starter-quartz + + + + + jakarta.validation + jakarta.validation-api + + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java new file mode 100644 index 0000000..4b08210 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.quartz.config; + +import com.alibaba.ttl.TtlRunnable; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; + +/** + * 异步任务 Configuration + */ +@AutoConfiguration +@EnableAsync +public class YudaoAsyncAutoConfiguration { + + @Bean + public BeanPostProcessor threadPoolTaskExecutorBeanPostProcessor() { + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + // 处理 ThreadPoolTaskExecutor + if (bean instanceof ThreadPoolTaskExecutor) { + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; + executor.setTaskDecorator(TtlRunnable::get); + return executor; + } + // 处理 SimpleAsyncTaskExecutor + // 参考 https://t.zsxq.com/CBoks 增加 + if (bean instanceof SimpleAsyncTaskExecutor) { + SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) bean; + executor.setTaskDecorator(TtlRunnable::get); + return executor; + } + return bean; + } + + }; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoQuartzAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoQuartzAutoConfiguration.java new file mode 100644 index 0000000..aaf4fe5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoQuartzAutoConfiguration.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.quartz.config; + +import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; +import lombok.extern.slf4j.Slf4j; +import org.quartz.Scheduler; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.util.Optional; + +/** + * 定时任务 Configuration + */ +@AutoConfiguration +@EnableScheduling // 开启 Spring 自带的定时任务 +@Slf4j +public class YudaoQuartzAutoConfiguration { + + @Bean + public SchedulerManager schedulerManager(Optional scheduler) { + if (!scheduler.isPresent()) { + log.info("[定时任务 - 已禁用][参考 https://doc.iocoder.cn/job/ 开启]"); + return new SchedulerManager(null); + } + return new SchedulerManager(scheduler.get()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/enums/JobDataKeyEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/enums/JobDataKeyEnum.java new file mode 100644 index 0000000..3154c40 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/enums/JobDataKeyEnum.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.framework.quartz.core.enums; + +/** + * Quartz Job Data 的 key 枚举 + */ +public enum JobDataKeyEnum { + + JOB_ID, + JOB_HANDLER_NAME, + JOB_HANDLER_PARAM, + JOB_RETRY_COUNT, // 最大重试次数 + JOB_RETRY_INTERVAL, // 每次重试间隔 + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandler.java new file mode 100644 index 0000000..381e132 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandler.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.quartz.core.handler; + +/** + * 任务处理器 + * + * @author 芋道源码 + */ +public interface JobHandler { + + /** + * 执行任务 + * + * @param param 参数 + * @return 结果 + * @throws Exception 异常 + */ + String execute(String param) throws Exception; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandlerInvoker.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandlerInvoker.java new file mode 100644 index 0000000..d9b2990 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/handler/JobHandlerInvoker.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.framework.quartz.core.handler; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; +import cn.iocoder.yudao.framework.quartz.core.enums.JobDataKeyEnum; +import cn.iocoder.yudao.framework.quartz.core.service.JobLogFrameworkService; +import lombok.extern.slf4j.Slf4j; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.PersistJobDataAfterExecution; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.quartz.QuartzJobBean; + +import jakarta.annotation.Resource; +import java.time.LocalDateTime; + +import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; + +/** + * 基础 Job 调用者,负责调用 {@link JobHandler#execute(String)} 执行任务 + * + * @author 芋道源码 + */ +@DisallowConcurrentExecution +@PersistJobDataAfterExecution +@Slf4j +public class JobHandlerInvoker extends QuartzJobBean { + + @Resource + private ApplicationContext applicationContext; + + @Resource + private JobLogFrameworkService jobLogFrameworkService; + + @Override + protected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException { + // 第一步,获得 Job 数据 + Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name()); + String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name()); + String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name()); + int refireCount = executionContext.getRefireCount(); + int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0); + int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0); + + // 第二步,执行任务 + Long jobLogId = null; + LocalDateTime startTime = LocalDateTime.now(); + String data = null; + Throwable exception = null; + try { + // 记录 Job 日志(初始) + jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1); + // 执行任务 + data = this.executeInternal(jobHandlerName, jobHandlerParam); + } catch (Throwable ex) { + exception = ex; + } + + // 第三步,记录执行日志 + this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext); + + // 第四步,处理有异常的情况 + handleException(exception, refireCount, retryCount, retryInterval); + } + + private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception { + // 获得 JobHandler 对象 + JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class); + Assert.notNull(jobHandler, "JobHandler 不会为空"); + // 执行任务 + return jobHandler.execute(jobHandlerParam); + } + + private void updateJobLogResultAsync(Long jobLogId, LocalDateTime startTime, String data, Throwable exception, + JobExecutionContext executionContext) { + LocalDateTime endTime = LocalDateTime.now(); + // 处理是否成功 + boolean success = exception == null; + if (!success) { + data = getRootCauseMessage(exception); + } + // 更新日志 + try { + jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) LocalDateTimeUtil.between(startTime, endTime).toMillis(), success, data); + } catch (Exception ex) { + log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]", + executionContext.getJobDetail().getKey(), jobLogId, success, data); + } + } + + private void handleException(Throwable exception, + int refireCount, int retryCount, int retryInterval) throws JobExecutionException { + // 如果有异常,则进行重试 + if (exception == null) { + return; + } + // 情况一:如果到达重试上限,则直接抛出异常即可 + if (refireCount >= retryCount) { + throw new JobExecutionException(exception); + } + + // 情况二:如果未到达重试上限,则 sleep 一定间隔时间,然后重试 + // 这里使用 sleep 来实现,主要还是希望实现比较简单。因为,同一时间,不会存在大量失败的 Job。 + if (retryInterval > 0) { + ThreadUtil.sleep(retryInterval); + } + // 第二个参数,refireImmediately = true,表示立即重试 + throw new JobExecutionException(exception, true); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java new file mode 100644 index 0000000..d56682e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java @@ -0,0 +1,150 @@ +package cn.iocoder.yudao.framework.quartz.core.scheduler; + +import cn.iocoder.yudao.framework.quartz.core.enums.JobDataKeyEnum; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker; +import org.quartz.*; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; + +/** + * {@link org.quartz.Scheduler} 的管理器,负责创建任务 + * + * 考虑到实现的简洁性,我们使用 jobHandlerName 作为唯一标识,即: + * 1. Job 的 {@link JobDetail#getKey()} + * 2. Trigger 的 {@link Trigger#getKey()} + * + * 另外,jobHandlerName 对应到 Spring Bean 的名字,直接调用 + * + * @author 芋道源码 + */ +public class SchedulerManager { + + private final Scheduler scheduler; + + public SchedulerManager(Scheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * 添加 Job 到 Quartz 中 + * + * @param jobId 任务编号 + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @param cronExpression CRON 表达式 + * @param retryCount 重试次数 + * @param retryInterval 重试间隔 + * @throws SchedulerException 添加异常 + */ + public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) + throws SchedulerException { + validateScheduler(); + // 创建 JobDetail 对象 + JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class) + .usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId) + .usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName) + .withIdentity(jobHandlerName).build(); + // 创建 Trigger 对象 + Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); + // 新增 Job 调度 + scheduler.scheduleJob(jobDetail, trigger); + } + + /** + * 更新 Job 到 Quartz + * + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @param cronExpression CRON 表达式 + * @param retryCount 重试次数 + * @param retryInterval 重试间隔 + * @throws SchedulerException 更新异常 + */ + public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) + throws SchedulerException { + validateScheduler(); + // 创建新 Trigger 对象 + Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); + // 修改调度 + scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger); + } + + /** + * 删除 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 删除异常 + */ + public void deleteJob(String jobHandlerName) throws SchedulerException { + validateScheduler(); + // 暂停 Trigger 对象 + scheduler.pauseTrigger(new TriggerKey(jobHandlerName)); + // 取消并删除 Job 调度 + scheduler.unscheduleJob(new TriggerKey(jobHandlerName)); + scheduler.deleteJob(new JobKey(jobHandlerName)); + } + + /** + * 暂停 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 暂停异常 + */ + public void pauseJob(String jobHandlerName) throws SchedulerException { + validateScheduler(); + scheduler.pauseJob(new JobKey(jobHandlerName)); + } + + /** + * 启动 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 启动异常 + */ + public void resumeJob(String jobHandlerName) throws SchedulerException { + validateScheduler(); + scheduler.resumeJob(new JobKey(jobHandlerName)); + scheduler.resumeTrigger(new TriggerKey(jobHandlerName)); + } + + /** + * 立即触发一次 Quartz 中的 Job + * + * @param jobId 任务编号 + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @throws SchedulerException 触发异常 + */ + public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam) + throws SchedulerException { + validateScheduler(); + // 触发任务 + JobDataMap data = new JobDataMap(); // 无需重试,所以不设置 retryCount 和 retryInterval + data.put(JobDataKeyEnum.JOB_ID.name(), jobId); + data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName); + data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam); + scheduler.triggerJob(new JobKey(jobHandlerName), data); + } + + private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) { + return TriggerBuilder.newTrigger() + .withIdentity(jobHandlerName) + .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) + .usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam) + .usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount) + .usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval) + .build(); + } + + private void validateScheduler() { + if (scheduler == null) { + throw exception0(NOT_IMPLEMENTED.getCode(), + "[定时任务 - 已禁用][参考 https://doc.iocoder.cn/job/ 开启]"); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/service/JobLogFrameworkService.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/service/JobLogFrameworkService.java new file mode 100644 index 0000000..cd99cad --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/service/JobLogFrameworkService.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.quartz.core.service; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * Job 日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface JobLogFrameworkService { + + /** + * 创建 Job 日志 + * + * @param jobId 任务编号 + * @param beginTime 开始时间 + * @param jobHandlerName Job 处理器的名字 + * @param jobHandlerParam Job 处理器的参数 + * @param executeIndex 第几次执行 + * @return Job 日志的编号 + */ + Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId, + @NotNull(message = "开始时间") LocalDateTime beginTime, + @NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName, + String jobHandlerParam, + @NotNull(message = "第几次执行不能为空") Integer executeIndex); + + /** + * 更新 Job 日志的执行结果 + * + * @param logId 日志编号 + * @param endTime 结束时间。因为是异步,避免记录时间不准去 + * @param duration 运行时长,单位:毫秒 + * @param success 是否成功 + * @param result 成功数据 + */ + void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId, + @NotNull(message = "结束时间不能为空") LocalDateTime endTime, + @NotNull(message = "运行时长不能为空") Integer duration, + boolean success, String result); +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/util/CronUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/util/CronUtils.java new file mode 100644 index 0000000..5658fa3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/util/CronUtils.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.framework.quartz.core.util; + +import cn.hutool.core.date.LocalDateTimeUtil; +import org.quartz.CronExpression; + +import java.text.ParseException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Quartz Cron 表达式的工具类 + * + * @author 芋道源码 + */ +public class CronUtils { + + /** + * 校验 CRON 表达式是否有效 + * + * @param cronExpression CRON 表达式 + * @return 是否有效 + */ + public static boolean isValid(String cronExpression) { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 基于 CRON 表达式,获得下 n 个满足执行的时间 + * + * @param cronExpression CRON 表达式 + * @param n 数量 + * @return 满足条件的执行时间 + */ + public static List getNextTimes(String cronExpression, int n) { + // 1. 获得 CronExpression 对象 + CronExpression cron; + try { + cron = new CronExpression(cronExpression); + } catch (ParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + // 2. 从当前开始计算,n 个满足条件的 + Date now = new Date(); + List nextTimes = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + Date nextTime = cron.getNextValidTimeAfter(now); + // 2.1 如果 nextTime 为 null,说明没有更多的有效时间,退出循环 + if (nextTime == null) { + break; + } + nextTimes.add(LocalDateTimeUtil.of(nextTime)); + // 2.2 切换现在,为下一个触发时间; + now = nextTime; + } + return nextTimes; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/package-info.java new file mode 100644 index 0000000..cfd237b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/package-info.java @@ -0,0 +1,7 @@ +/** + * 1. 定时任务,采用 Quartz 实现进程内的任务执行。 + * 考虑到高可用,使用 Quartz 自带的 MySQL 集群方案。 + * + * 2. 异步任务,采用 Spring Async 异步执行。 + */ +package cn.iocoder.yudao.framework.quartz; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..4086f7a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +cn.iocoder.yudao.framework.quartz.config.YudaoQuartzAutoConfiguration +cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md new file mode 100644 index 0000000..6564701 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md new file mode 100644 index 0000000..5822b83 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml new file mode 100644 index 0000000..ebd1210 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml @@ -0,0 +1,73 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-monitor + jar + + ${project.artifactId} + 服务监控,提供链路追踪、日志服务、指标收集等等功能 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + + io.opentracing + opentracing-util + + + org.apache.skywalking + apm-toolkit-trace + + + org.apache.skywalking + apm-toolkit-logback-1.x + + + org.apache.skywalking + apm-toolkit-opentracing + + + + + io.micrometer + micrometer-registry-prometheus + + + + de.codecentric + spring-boot-admin-starter-client + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/TracerProperties.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/TracerProperties.java new file mode 100644 index 0000000..11d25a8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/TracerProperties.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.framework.tracer.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * BizTracer配置类 + * + * @author 麻薯 + */ +@ConfigurationProperties("yudao.tracer") +@Data +public class TracerProperties { +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoMetricsAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoMetricsAutoConfiguration.java new file mode 100644 index 0000000..cc2a0df --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoMetricsAutoConfiguration.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.tracer.config; + +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; + +/** + * Metrics 配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@ConditionalOnClass({MeterRegistryCustomizer.class}) +@ConditionalOnProperty(prefix = "yudao.metrics", value = "enable", matchIfMissing = true) // 允许使用 yudao.metrics.enable=false 禁用 Metrics +public class YudaoMetricsAutoConfiguration { + + @Bean + public MeterRegistryCustomizer metricsCommonTags( + @Value("${spring.application.name}") String applicationName) { + return registry -> registry.config().commonTags("application", applicationName); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java new file mode 100644 index 0000000..c7d9e2c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.tracer.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.tracer.core.aop.BizTraceAspect; +import cn.iocoder.yudao.framework.tracer.core.filter.TraceFilter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; + +/** + * Tracer 配置类 + * + * @author mashu + */ +@AutoConfiguration +@ConditionalOnClass({BizTraceAspect.class}) +@EnableConfigurationProperties(TracerProperties.class) +@ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true) +public class YudaoTracerAutoConfiguration { + + // TODO @芋艿:重要。目前 opentracing 版本存在冲突,要么保证 skywalking,要么保证阿里云短信 sdk +// @Bean +// public TracerProperties bizTracerProperties() { +// return new TracerProperties(); +// } +// +// @Bean +// public BizTraceAspect bizTracingAop() { +// return new BizTraceAspect(tracer()); +// } +// +// @Bean +// public Tracer tracer() { +// // 创建 SkywalkingTracer 对象 +// SkywalkingTracer tracer = new SkywalkingTracer(); +// // 设置为 GlobalTracer 的追踪器 +// GlobalTracer.register(tracer); +// return tracer; +// } + + /** + * 创建 TraceFilter 过滤器,响应 header 设置 traceId + */ + @Bean + public FilterRegistrationBean traceFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TraceFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TRACE_FILTER); + return registrationBean; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/annotation/BizTrace.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/annotation/BizTrace.java new file mode 100644 index 0000000..8b12140 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/annotation/BizTrace.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.tracer.core.annotation; + +import java.lang.annotation.*; + +/** + * 打印业务编号 / 业务类型注解 + * + * 使用时,需要设置 SkyWalking OAP Server 的 application.yaml 配置文件,修改 SW_SEARCHABLE_TAG_KEYS 配置项, + * 增加 biz.type 和 biz.id 两值,然后重启 SkyWalking OAP Server 服务器。 + * + * @author 麻薯 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface BizTrace { + + /** + * 业务编号 tag 名 + */ + String ID_TAG = "biz.id"; + /** + * 业务类型 tag 名 + */ + String TYPE_TAG = "biz.type"; + + /** + * @return 操作名 + */ + String operationName() default ""; + + /** + * @return 业务编号 + */ + String id(); + + /** + * @return 业务类型 + */ + String type(); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/aop/BizTraceAspect.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/aop/BizTraceAspect.java new file mode 100644 index 0000000..3c3b9f3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/aop/BizTraceAspect.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.framework.tracer.core.aop; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tracer.core.annotation.BizTrace; +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; +import cn.iocoder.yudao.framework.tracer.core.util.TracerFrameworkUtils; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import java.util.Map; + +import static java.util.Arrays.asList; + +/** + * {@link BizTrace} 切面,记录业务链路 + * + * @author mashu + */ +@Aspect +@AllArgsConstructor +@Slf4j +public class BizTraceAspect { + + private static final String BIZ_OPERATION_NAME_PREFIX = "Biz/"; + + private final Tracer tracer; + + @Around(value = "@annotation(trace)") + public Object around(ProceedingJoinPoint joinPoint, BizTrace trace) throws Throwable { + // 创建 span + String operationName = getOperationName(joinPoint, trace); + Span span = tracer.buildSpan(operationName) + .withTag(Tags.COMPONENT.getKey(), "biz") + .start(); + try { + // 执行原有方法 + return joinPoint.proceed(); + } catch (Throwable throwable) { + TracerFrameworkUtils.onError(throwable, span); + throw throwable; + } finally { + // 设置 Span 的 biz 属性 + setBizTag(span, joinPoint, trace); + // 完成 Span + span.finish(); + } + } + + private String getOperationName(ProceedingJoinPoint joinPoint, BizTrace trace) { + // 自定义操作名 + if (StrUtil.isNotEmpty(trace.operationName())) { + return BIZ_OPERATION_NAME_PREFIX + trace.operationName(); + } + // 默认操作名,使用方法名 + return BIZ_OPERATION_NAME_PREFIX + + joinPoint.getSignature().getDeclaringType().getSimpleName() + + "/" + joinPoint.getSignature().getName(); + } + + private void setBizTag(Span span, ProceedingJoinPoint joinPoint, BizTrace trace) { + try { + Map result = SpringExpressionUtils.parseExpressions(joinPoint, asList(trace.type(), trace.id())); + span.setTag(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type())); + span.setTag(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id())); + } catch (Exception ex) { + log.error("[setBizTag][解析 bizType 与 bizId 发生异常]", ex); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/filter/TraceFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/filter/TraceFilter.java new file mode 100644 index 0000000..e8d7ca1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/filter/TraceFilter.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.tracer.core.filter; + +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Trace 过滤器,打印 traceId 到 header 中返回 + * + * @author 芋道源码 + */ +public class TraceFilter extends OncePerRequestFilter { + + /** + * Header 名 - 链路追踪编号 + */ + private static final String HEADER_NAME_TRACE_ID = "trace-id"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + // 设置响应 traceId + response.addHeader(HEADER_NAME_TRACE_ID, TracerUtils.getTraceId()); + // 继续过滤 + chain.doFilter(request, response); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/util/TracerFrameworkUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/util/TracerFrameworkUtils.java new file mode 100644 index 0000000..51323e7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/core/util/TracerFrameworkUtils.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.tracer.core.util; + +import io.opentracing.Span; +import io.opentracing.tag.Tags; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * 链路追踪 Util + * + * @author 芋道源码 + */ +public class TracerFrameworkUtils { + + /** + * 将异常记录到 Span 中,参考自 com.aliyuncs.utils.TraceUtils + * + * @param throwable 异常 + * @param span Span + */ + public static void onError(Throwable throwable, Span span) { + Tags.ERROR.set(span, Boolean.TRUE); + if (throwable != null) { + span.log(errorLogs(throwable)); + } + } + + private static Map errorLogs(Throwable throwable) { + Map errorLogs = new HashMap(10); + errorLogs.put("event", Tags.ERROR.getKey()); + errorLogs.put("error.object", throwable); + errorLogs.put("error.kind", throwable.getClass().getName()); + String message = throwable.getCause() != null ? throwable.getCause().getMessage() : throwable.getMessage(); + if (message != null) { + errorLogs.put("message", message); + } + StringWriter sw = new StringWriter(); + throwable.printStackTrace(new PrintWriter(sw)); + errorLogs.put("stack", sw.toString()); + return errorLogs; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/package-info.java new file mode 100644 index 0000000..43ee5e4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/package-info.java @@ -0,0 +1,6 @@ +/** + * 使用 SkyWalking 组件,作为链路追踪、日志中心。 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.tracer; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..218ee31 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +cn.iocoder.yudao.framework.tracer.config.YudaoTracerAutoConfiguration +cn.iocoder.yudao.framework.tracer.config.YudaoMetricsAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md new file mode 100644 index 0000000..7f898b9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md new file mode 100644 index 0000000..b73a460 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md new file mode 100644 index 0000000..ed9d4fe --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/pom.xml new file mode 100644 index 0000000..c8972f1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/pom.xml @@ -0,0 +1,43 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-mq + jar + + ${project.artifactId} + 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + + + + org.springframework.kafka + spring-kafka + true + + + org.springframework.amqp + spring-rabbit + true + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + + \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/package-info.java new file mode 100644 index 0000000..3b716cb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种 + */ +package cn.iocoder.yudao.framework.mq; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java new file mode 100644 index 0000000..af14673 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.mq.rabbitmq.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; + +/** + * RabbitMQ 消息队列配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@Slf4j +@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") +public class YudaoRabbitMQAutoConfiguration { + + /** + * Jackson2JsonMessageConverter Bean:使用 jackson 序列化消息 + */ + @Bean + public MessageConverter createMessageConverter() { + return new Jackson2JsonMessageConverter(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/core/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/core/package-info.java new file mode 100644 index 0000000..2773b58 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,无特殊逻辑 + */ +package cn.iocoder.yudao.framework.mq.rabbitmq.core; \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/package-info.java new file mode 100644 index 0000000..9f6032c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列,基于 RabbitMQ 提供 + */ +package cn.iocoder.yudao.framework.mq.rabbitmq; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java new file mode 100644 index 0000000..c9ab3e5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java @@ -0,0 +1,163 @@ +package cn.iocoder.yudao.framework.mq.redis.config; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.framework.common.enums.DocumentEnum; +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob; +import cn.iocoder.yudao.framework.mq.redis.core.job.RedisStreamMessageCleanupJob; +import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; +import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.stream.StreamMessageListenerContainer; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.util.List; +import java.util.Properties; + +/** + * Redis 消息队列 Consumer 配置类 + * + * @author 芋道源码 + */ +@Slf4j +@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息 +@AutoConfiguration(after = YudaoRedisAutoConfiguration.class) +public class YudaoRedisMQConsumerAutoConfiguration { + + /** + * 创建 Redis Pub/Sub 广播消费的容器 + */ + @Bean + @ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听 + public RedisMessageListenerContainer redisMessageListenerContainer( + RedisMQTemplate redisMQTemplate, List> listeners) { + // 创建 RedisMessageListenerContainer 对象 + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + // 设置 RedisConnection 工厂。 + container.setConnectionFactory(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory()); + // 添加监听器 + listeners.forEach(listener -> { + listener.setRedisMQTemplate(redisMQTemplate); + container.addMessageListener(listener, new ChannelTopic(listener.getChannel())); + log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]", + listener.getChannel(), listener.getClass().getName()); + }); + return container; + } + + /** + * 创建 Redis Stream 重新消费的任务 + */ + @Bean + @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听 + public RedisPendingMessageResendJob redisPendingMessageResendJob(List> listeners, + RedisMQTemplate redisTemplate, + @Value("${spring.application.name}") String groupName, + RedissonClient redissonClient) { + return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient); + } + + /** + * 创建 Redis Stream 消息清理任务 + */ + @Bean + @ConditionalOnBean(AbstractRedisStreamMessageListener.class) + public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List> listeners, + RedisMQTemplate redisTemplate, + RedissonClient redissonClient) { + return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient); + } + + /** + * 创建 Redis Stream 集群消费的容器 + * + * 基础知识:Redis Stream 的 xreadgroup 命令 + */ + @Bean(initMethod = "start", destroyMethod = "stop") + @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听 + public StreamMessageListenerContainer> redisStreamMessageListenerContainer( + RedisMQTemplate redisMQTemplate, List> listeners) { + RedisTemplate redisTemplate = redisMQTemplate.getRedisTemplate(); + checkRedisVersion(redisTemplate); + // 第一步,创建 StreamMessageListenerContainer 容器 + // 创建 options 配置 + StreamMessageListenerContainer.StreamMessageListenerContainerOptions> containerOptions = + StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() + .batchSize(10) // 一次性最多拉取多少条消息 + .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化 + .build(); + // 创建 container 对象 + StreamMessageListenerContainer> container = + StreamMessageListenerContainer.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions); + + // 第二步,注册监听器,消费对应的 Stream 主题 + String consumerName = buildConsumerName(); + listeners.parallelStream().forEach(listener -> { + log.info("[redisStreamMessageListenerContainer][开始注册 StreamKey({}) 对应的监听器({})]", + listener.getStreamKey(), listener.getClass().getName()); + // 创建 listener 对应的消费者分组 + try { + redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); + } catch (Exception ignore) { + } + // 设置 listener 对应的 redisTemplate + listener.setRedisMQTemplate(redisMQTemplate); + // 创建 Consumer 对象 + Consumer consumer = Consumer.from(listener.getGroup(), consumerName); + // 设置 Consumer 消费进度,以最小消费进度为准 + StreamOffset streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()); + // 设置 Consumer 监听 + StreamMessageListenerContainer.StreamReadRequestBuilder builder = StreamMessageListenerContainer.StreamReadRequest + .builder(streamOffset).consumer(consumer) + .autoAcknowledge(false) // 不自动 ack + .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false + container.register(builder.build(), listener); + log.info("[redisStreamMessageListenerContainer][完成注册 StreamKey({}) 对应的监听器({})]", + listener.getStreamKey(), listener.getClass().getName()); + }); + return container; + } + + /** + * 构建消费者名字,使用本地 IP + 进程编号的方式。 + * 参考自 RocketMQ clientId 的实现 + * + * @return 消费者名字 + */ + private static String buildConsumerName() { + return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); + } + + /** + * 校验 Redis 版本号,是否满足最低的版本号要求! + */ + private static void checkRedisVersion(RedisTemplate redisTemplate) { + // 获得 Redis 版本 + Properties info = redisTemplate.execute((RedisCallback) RedisServerCommands::info); + String version = MapUtil.getStr(info, "redis_version"); + // 校验最低版本必须大于等于 5.0.0 + int majorVersion = Integer.parseInt(StrUtil.subBefore(version, '.', false)); + if (majorVersion < 5) { + throw new IllegalStateException(StrUtil.format("您当前的 Redis 版本为 {},小于最低要求的 5.0.0 版本!" + + "请参考 {} 文档进行安装。", version, DocumentEnum.REDIS_INSTALL.getUrl())); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java new file mode 100644 index 0000000..c1950c4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.mq.redis.config; + +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.List; + +/** + * Redis 消息队列 Producer 配置类 + * + * @author 芋道源码 + */ +@Slf4j +@AutoConfiguration(after = YudaoRedisAutoConfiguration.class) +public class YudaoRedisMQProducerAutoConfiguration { + + @Bean + public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate, + List interceptors) { + RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate); + // 添加拦截器 + interceptors.forEach(redisMQTemplate::addInterceptor); + return redisMQTemplate; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/RedisMQTemplate.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/RedisMQTemplate.java new file mode 100644 index 0000000..5755ffa --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/RedisMQTemplate.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.framework.mq.redis.core; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage; +import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage; +import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.ArrayList; +import java.util.List; + +/** + * Redis MQ 操作模板类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class RedisMQTemplate { + + @Getter + private final RedisTemplate redisTemplate; + /** + * 拦截器数组 + */ + @Getter + private final List interceptors = new ArrayList<>(); + + /** + * 发送 Redis 消息,基于 Redis pub/sub 实现 + * + * @param message 消息 + */ + public void send(T message) { + try { + sendMessageBefore(message); + // 发送消息 + redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message)); + } finally { + sendMessageAfter(message); + } + } + + /** + * 发送 Redis 消息,基于 Redis Stream 实现 + * + * @param message 消息 + * @return 消息记录的编号对象 + */ + public RecordId send(T message) { + try { + sendMessageBefore(message); + // 发送消息 + return redisTemplate.opsForStream().add(StreamRecords.newRecord() + .ofObject(JsonUtils.toJsonString(message)) // 设置内容 + .withStreamKey(message.getStreamKey())); // 设置 stream key + } finally { + sendMessageAfter(message); + } + } + + /** + * 添加拦截器 + * + * @param interceptor 拦截器 + */ + public void addInterceptor(RedisMessageInterceptor interceptor) { + interceptors.add(interceptor); + } + + private void sendMessageBefore(AbstractRedisMessage message) { + // 正序 + interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message)); + } + + private void sendMessageAfter(AbstractRedisMessage message) { + // 倒序 + for (int i = interceptors.size() - 1; i >= 0; i--) { + interceptors.get(i).sendMessageAfter(message); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java new file mode 100644 index 0000000..dbcee7f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.mq.redis.core.interceptor; + +import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage; + +/** + * {@link AbstractRedisMessage} 消息拦截器 + * 通过拦截器,作为插件机制,实现拓展。 + * 例如说,多租户场景下的 MQ 消息处理 + * + * @author 芋道源码 + */ +public interface RedisMessageInterceptor { + + default void sendMessageBefore(AbstractRedisMessage message) { + } + + default void sendMessageAfter(AbstractRedisMessage message) { + } + + default void consumeMessageBefore(AbstractRedisMessage message) { + } + + default void consumeMessageAfter(AbstractRedisMessage message) { + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java new file mode 100644 index 0000000..cb4e399 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.framework.mq.redis.core.job; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.core.StreamOperations; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 这个任务用于处理,crash 之后的消费者未消费完的消息 + */ +@Slf4j +@AllArgsConstructor +public class RedisPendingMessageResendJob { + + private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock"; + + /** + * 消息超时时间,默认 5 分钟 + * + * 1. 超时的消息才会被重新投递 + * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息 5 分钟过期后,再等 1 分钟才会被扫瞄到 + */ + private static final int EXPIRE_TIME = 5 * 60; + + private final List> listeners; + private final RedisMQTemplate redisTemplate; + private final String groupName; + private final RedissonClient redissonClient; + + /** + * 一分钟执行一次,这里选择每分钟的 35 秒执行,是为了避免整点任务过多的问题 + */ + @Scheduled(cron = "35 * * * * ?") + public void messageResend() { + RLock lock = redissonClient.getLock(LOCK_KEY); + // 尝试加锁 + if (lock.tryLock()) { + try { + execute(); + } catch (Exception ex) { + log.error("[messageResend][执行异常]", ex); + } finally { + lock.unlock(); + } + } + } + + /** + * 执行清理逻辑 + * + * @see 讨论 + */ + private void execute() { + StreamOperations ops = redisTemplate.getRedisTemplate().opsForStream(); + listeners.forEach(listener -> { + PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), groupName)); + // 每个消费者的 pending 队列消息数量 + Map pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer(); + pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> { + log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount); + // 每个消费者的 pending消息的详情信息 + PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(groupName, consumerName), Range.unbounded(), pendingMessageCount); + if (pendingMessages.isEmpty()) { + return; + } + pendingMessages.forEach(pendingMessage -> { + // 获取消息上一次传递到 consumer 的时间, + long lastDelivery = pendingMessage.getElapsedTimeSinceLastDelivery().getSeconds(); + if (lastDelivery < EXPIRE_TIME){ + return; + } + // 获取指定 id 的消息体 + List> records = ops.range(listener.getStreamKey(), + Range.of(Range.Bound.inclusive(pendingMessage.getIdAsString()), Range.Bound.inclusive(pendingMessage.getIdAsString()))); + if (CollUtil.isEmpty(records)) { + return; + } + // 重新投递消息 + redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord() + .ofObject(records.get(0).getValue()) // 设置内容 + .withStreamKey(listener.getStreamKey())); + // ack 消息消费完成 + redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, records.get(0)); + log.info("[processPendingMessage][消息({})重新投递成功]", records.get(0).getId()); + }); + }); + }); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java new file mode 100644 index 0000000..19da845 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisStreamMessageCleanupJob.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.framework.mq.redis.core.job; + +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.data.redis.core.StreamOperations; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.List; + +/** + * Redis Stream 消息清理任务 + * 用于定期清理已消费的消息,防止内存占用过大 + * + * @see 记一次 redis stream 数据类型内存不释放问题 + * + * @author 芋道源码 + */ +@Slf4j +@AllArgsConstructor +public class RedisStreamMessageCleanupJob { + + private static final String LOCK_KEY = "redis:stream:message-cleanup:lock"; + + /** + * 保留的消息数量,默认保留最近 10000 条消息 + */ + private static final long MAX_COUNT = 10000; + + private final List> listeners; + private final RedisMQTemplate redisTemplate; + private final RedissonClient redissonClient; + + /** + * 每小时执行一次清理任务 + */ + @Scheduled(cron = "0 0 * * * ?") + public void cleanup() { + RLock lock = redissonClient.getLock(LOCK_KEY); + // 尝试加锁 + if (lock.tryLock()) { + try { + execute(); + } catch (Exception ex) { + log.error("[cleanup][执行异常]", ex); + } finally { + lock.unlock(); + } + } + } + + /** + * 执行清理逻辑 + */ + private void execute() { + StreamOperations ops = redisTemplate.getRedisTemplate().opsForStream(); + listeners.forEach(listener -> { + try { + // 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息 + Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true); + if (trimCount != null && trimCount > 0) { + log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount); + } + } catch (Exception ex) { + log.error("[execute][Stream({}) 清理异常]", listener.getStreamKey(), ex); + } + }); + } +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/message/AbstractRedisMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/message/AbstractRedisMessage.java new file mode 100644 index 0000000..ee40814 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/message/AbstractRedisMessage.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.mq.redis.core.message; + +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +/** + * Redis 消息抽象基类 + * + * @author 芋道源码 + */ +@Data +public abstract class AbstractRedisMessage { + + /** + * 头 + */ + private Map headers = new HashMap<>(); + + public String getHeader(String key) { + return headers.get(key); + } + + public void addHeader(String key, String value) { + headers.put(key, value); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java new file mode 100644 index 0000000..d5ea5b9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.framework.mq.redis.core.pubsub; + +import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Redis Channel Message 抽象类 + * + * @author 芋道源码 + */ +public abstract class AbstractRedisChannelMessage extends AbstractRedisMessage { + + /** + * 获得 Redis Channel,默认使用类名 + * + * @return Channel + */ + @JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。 + public String getChannel() { + return getClass().getSimpleName(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java new file mode 100644 index 0000000..fd7c910 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.framework.mq.redis.core.pubsub; + +import cn.hutool.core.util.TypeUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage; +import lombok.Setter; +import lombok.SneakyThrows; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * Redis Pub/Sub 监听器抽象类,用于实现广播消费 + * + * @param 消息类型。一定要填写噢,不然会报错 + * + * @author 芋道源码 + */ +public abstract class AbstractRedisChannelMessageListener implements MessageListener { + + /** + * 消息类型 + */ + private final Class messageType; + /** + * Redis Channel + */ + private final String channel; + /** + * RedisMQTemplate + */ + @Setter + private RedisMQTemplate redisMQTemplate; + + @SneakyThrows + protected AbstractRedisChannelMessageListener() { + this.messageType = getMessageClass(); + this.channel = messageType.getDeclaredConstructor().newInstance().getChannel(); + } + + /** + * 获得 Sub 订阅的 Redis Channel 通道 + * + * @return channel + */ + public final String getChannel() { + return channel; + } + + @Override + public final void onMessage(Message message, byte[] bytes) { + T messageObj = JsonUtils.parseObject(message.getBody(), messageType); + try { + consumeMessageBefore(messageObj); + // 消费消息 + this.onMessage(messageObj); + } finally { + consumeMessageAfter(messageObj); + } + } + + /** + * 处理消息 + * + * @param message 消息 + */ + public abstract void onMessage(T message); + + /** + * 通过解析类上的泛型,获得消息类型 + * + * @return 消息类型 + */ + @SuppressWarnings("unchecked") + private Class getMessageClass() { + Type type = TypeUtil.getTypeArgument(getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + return (Class) type; + } + + private void consumeMessageBefore(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 正序 + interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); + } + + private void consumeMessageAfter(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 倒序 + for (int i = interceptors.size() - 1; i >= 0; i--) { + interceptors.get(i).consumeMessageAfter(message); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java new file mode 100644 index 0000000..9017e08 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.framework.mq.redis.core.stream; + +import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Redis Stream Message 抽象类 + * + * @author 芋道源码 + */ +public abstract class AbstractRedisStreamMessage extends AbstractRedisMessage { + + /** + * 获得 Redis Stream Key,默认使用类名 + * + * @return Channel + */ + @JsonIgnore // 避免序列化 + public String getStreamKey() { + return getClass().getSimpleName(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java new file mode 100644 index 0000000..3e656af --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.framework.mq.redis.core.stream; + +import cn.hutool.core.util.TypeUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.redis.core.message.AbstractRedisMessage; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.stream.StreamListener; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * Redis Stream 监听器抽象类,用于实现集群消费 + * + * @param 消息类型。一定要填写噢,不然会报错 + * + * @author 芋道源码 + */ +public abstract class AbstractRedisStreamMessageListener + implements StreamListener> { + + /** + * 消息类型 + */ + private final Class messageType; + /** + * Redis Channel + */ + @Getter + private final String streamKey; + + /** + * Redis 消费者分组,默认使用 spring.application.name 名字 + */ + @Value("${spring.application.name}") + @Getter + private String group; + /** + * RedisMQTemplate + */ + @Setter + private RedisMQTemplate redisMQTemplate; + + @SneakyThrows + protected AbstractRedisStreamMessageListener() { + this.messageType = getMessageClass(); + this.streamKey = messageType.getDeclaredConstructor().newInstance().getStreamKey(); + } + + @Override + public void onMessage(ObjectRecord message) { + // 消费消息 + T messageObj = JsonUtils.parseObject(message.getValue(), messageType); + try { + consumeMessageBefore(messageObj); + // 消费消息 + this.onMessage(messageObj); + // ack 消息消费完成 + redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message); + // TODO 芋艿:需要额外考虑以下几个点: + // 1. 处理异常的情况 + // 2. 发送日志;以及事务的结合 + // 3. 消费日志;以及通用的幂等性 + // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638 + } finally { + consumeMessageAfter(messageObj); + } + } + + /** + * 处理消息 + * + * @param message 消息 + */ + public abstract void onMessage(T message); + + /** + * 通过解析类上的泛型,获得消息类型 + * + * @return 消息类型 + */ + @SuppressWarnings("unchecked") + private Class getMessageClass() { + Type type = TypeUtil.getTypeArgument(getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + return (Class) type; + } + + private void consumeMessageBefore(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 正序 + interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); + } + + private void consumeMessageAfter(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 倒序 + for (int i = interceptors.size() - 1; i >= 0; i--) { + interceptors.get(i).consumeMessageAfter(message); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/package-info.java new file mode 100644 index 0000000..6621fc1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/package-info.java @@ -0,0 +1,6 @@ +/** + * 消息队列,基于 Redis 提供: + * 1. 基于 Pub/Sub 实现广播消费 + * 2. 基于 Stream 实现集群消费 + */ +package cn.iocoder.yudao.framework.mq.redis; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..213850b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQProducerAutoConfiguration +cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration +cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 事件机制 Event 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 事件机制 Event 入门》.md new file mode 100644 index 0000000..08586b3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 事件机制 Event 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 Kafka 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 Kafka 入门》.md new file mode 100644 index 0000000..b66d633 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 Kafka 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RabbitMQ 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RabbitMQ 入门》.md new file mode 100644 index 0000000..eff46e2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RabbitMQ 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RocketMQ 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RocketMQ 入门》.md new file mode 100644 index 0000000..08586b3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RocketMQ 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml new file mode 100644 index 0000000..3272059 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml @@ -0,0 +1,110 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-mybatis + jar + + ${project.artifactId} + 数据库连接池、多数据源、事务、MyBatis 拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + provided + + + + + com.mysql + mysql-connector-j + + + com.oracle.database.jdbc + ojdbc8 + true + + + org.postgresql + postgresql + true + + + com.microsoft.sqlserver + mssql-jdbc + true + + + com.dameng + DmJdbcDriver18 + true + + + cn.com.kingbase + kingbase8 + true + + + org.opengauss + opengauss-jdbc + true + + + com.taosdata.jdbc + taos-jdbcdriver + true + + + + com.alibaba + druid-spring-boot-3-starter + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + com.baomidou + mybatis-plus-jsqlparser + + + com.baomidou + dynamic-datasource-spring-boot3-starter + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + com.github.yulichang + mybatis-plus-join-boot-starter + + + + com.fhs-opensource + easy-trans-spring-boot-starter + + + com.fhs-opensource + easy-trans-mybatis-plus-extend + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/config/YudaoDataSourceAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/config/YudaoDataSourceAutoConfiguration.java new file mode 100644 index 0000000..879a19a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/config/YudaoDataSourceAutoConfiguration.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.datasource.config; + +import cn.iocoder.yudao.framework.datasource.core.filter.DruidAdRemoveFilter; +import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * 数据库配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理 +@EnableConfigurationProperties(DruidStatProperties.class) +public class YudaoDataSourceAutoConfiguration { + + /** + * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 + */ + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true") + public FilterRegistrationBean druidAdRemoveFilterFilter(DruidStatProperties properties) { + // 获取 druid web 监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取 common.js 的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + // 创建 DruidAdRemoveFilter Bean + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new DruidAdRemoveFilter()); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/enums/DataSourceEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/enums/DataSourceEnum.java new file mode 100644 index 0000000..c99a256 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/enums/DataSourceEnum.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.datasource.core.enums; + +/** + * 对应于多数据源中不同数据源配置 + * + * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。 + * 注意,默认是 {@link #MASTER} 数据源 + * + * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html + */ +public interface DataSourceEnum { + + /** + * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解 + */ + String MASTER = "master"; + /** + * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解 + */ + String SLAVE = "slave"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/filter/DruidAdRemoveFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/filter/DruidAdRemoveFilter.java new file mode 100644 index 0000000..18a86a8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/core/filter/DruidAdRemoveFilter.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.framework.datasource.core.filter; + +import com.alibaba.druid.util.Utils; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Druid 底部广告过滤器 + * + * @author 芋道源码 + */ +public class DruidAdRemoveFilter extends OncePerRequestFilter { + + /** + * common.js 的路径 + */ + private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取 common.js + String text = Utils.readFromResource(COMMON_JS_ILE_PATH); + // 正则替换 banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/package-info.java new file mode 100644 index 0000000..8512891 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/datasource/package-info.java @@ -0,0 +1,5 @@ +/** + * 数据库连接池,采用 Druid + * 多数据源,采用爆米花 + */ +package cn.iocoder.yudao.framework.datasource; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java new file mode 100644 index 0000000..3a67b90 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.framework.mybatis.config; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.annotation.IdType; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.util.Set; + +/** + * 当 IdType 为 {@link IdType#NONE} 时,根据 PRIMARY 数据源所使用的数据库,自动设置 + * + * @author 芋道源码 + */ +@Slf4j +public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor { + + private static final String ID_TYPE_KEY = "mybatis-plus.global-config.db-config.id-type"; + + private static final String DATASOURCE_DYNAMIC_KEY = "spring.datasource.dynamic"; + + private static final String QUARTZ_JOB_STORE_DRIVER_KEY = "spring.quartz.properties.org.quartz.jobStore.driverDelegateClass"; + + private static final Set INPUT_ID_TYPES = SetUtils.asSet(DbType.ORACLE, DbType.ORACLE_12C, + DbType.POSTGRE_SQL, DbType.KINGBASE_ES, DbType.DB2, DbType.H2); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + // 如果获取不到 DbType,则不进行处理 + DbType dbType = getDbType(environment); + if (dbType == null) { + return; + } + + // 设置 Quartz JobStore 对应的 Driver + // TODO 芋艿:暂时没有找到特别合适的地方,先放在这里 + setJobStoreDriverIfPresent(environment, dbType); + + // 如果非 NONE,则不进行处理 + IdType idType = getIdType(environment); + if (idType != IdType.NONE) { + return; + } + // 情况一,用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 + if (INPUT_ID_TYPES.contains(dbType)) { + setIdType(environment, IdType.INPUT); + return; + } + // 情况二,自增 ID,适合 MySQL、DM 达梦等直接自增的数据库 + setIdType(environment, IdType.AUTO); + } + + public IdType getIdType(ConfigurableEnvironment environment) { + return environment.getProperty(ID_TYPE_KEY, IdType.class); + } + + public void setIdType(ConfigurableEnvironment environment, IdType idType) { + environment.getSystemProperties().put(ID_TYPE_KEY, idType); + log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType); + } + + public void setJobStoreDriverIfPresent(ConfigurableEnvironment environment, DbType dbType) { + String driverClass = environment.getProperty(QUARTZ_JOB_STORE_DRIVER_KEY); + if (StrUtil.isNotEmpty(driverClass)) { + return; + } + // 根据 dbType 类型,获取对应的 driverClass + switch (dbType) { + case POSTGRE_SQL: + driverClass = "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"; + break; + case ORACLE: + case ORACLE_12C: + driverClass = "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate"; + break; + case SQL_SERVER: + case SQL_SERVER2005: + driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate"; + break; + case DM: + case KINGBASE_ES: + driverClass = "org.quartz.impl.jdbcjobstore.StdJDBCDelegate"; + break; + } + // 设置 driverClass 变量 + if (StrUtil.isNotEmpty(driverClass)) { + environment.getSystemProperties().put(QUARTZ_JOB_STORE_DRIVER_KEY, driverClass); + } + } + + public static DbType getDbType(ConfigurableEnvironment environment) { + String primary = environment.getProperty(DATASOURCE_DYNAMIC_KEY + "." + "primary"); + if (StrUtil.isEmpty(primary)) { + return null; + } + String url = environment.getProperty(DATASOURCE_DYNAMIC_KEY + ".datasource." + primary + ".url"); + if (StrUtil.isEmpty(url)) { + return null; + } + return JdbcUtils.getDbType(url); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java new file mode 100644 index 0000000..ab29921 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.framework.mybatis.config; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; +import com.baomidou.mybatisplus.extension.incrementer.*; +import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal; +import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.apache.ibatis.annotations.Mapper; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.util.concurrent.TimeUnit; + +/** + * MyBaits 配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的:先于 MyBatis Plus 自动配置,避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志 +@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class, + lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试 +public class YudaoMybatisAutoConfiguration { + + static { + // 动态 SQL 智能优化支持本地缓存加速解析,更完善的租户复杂 XML 动态 SQL 支持,静态注入缓存 + JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache( + (cache) -> cache.maximumSize(1024) + .expireAfterWrite(5, TimeUnit.SECONDS)) + ); + } + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); + mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 + return mybatisPlusInterceptor; + } + + @Bean + public MetaObjectHandler defaultMetaObjectHandler() { + return new DefaultDBFieldHandler(); // 自动填充参数类 + } + + @Bean + @ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT") + public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) { + DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment); + if (dbType != null) { + switch (dbType) { + case POSTGRE_SQL: + return new PostgreKeyGenerator(); + case ORACLE: + case ORACLE_12C: + return new OracleKeyGenerator(); + case H2: + return new H2KeyGenerator(); + case KINGBASE_ES: + return new KingbaseKeyGenerator(); + case DM: + return new DmKeyGenerator(); + } + } + // 找不到合适的 IKeyGenerator 实现类 + throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BaseDO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BaseDO.java new file mode 100644 index 0000000..7e07fd8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BaseDO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.framework.mybatis.core.dataobject; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fhs.core.trans.vo.TransPojo; +import lombok.Data; +import org.apache.ibatis.type.JdbcType; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 基础实体对象 + * + * 为什么实现 {@link TransPojo} 接口? + * 因为使用 Easy-Trans TransType.SIMPLE 模式,集成 MyBatis Plus 查询 + * + * @author 芋道源码 + */ +@Data +@JsonIgnoreProperties(value = "transMap") // 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错 +public abstract class BaseDO implements Serializable, TransPojo { + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + /** + * 最后更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + /** + * 创建者,目前使用 SysUser 的 id 编号 + * + * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 + */ + @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR) + private String creator; + /** + * 更新者,目前使用 SysUser 的 id 编号 + * + * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 + */ + @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR) + private String updater; + /** + * 是否删除 + */ + @TableLogic + private Boolean deleted; + + /** + * 把 creator、createTime、updateTime、updater 都清空,避免前端直接传递 creator 之类的字段,直接就被更新了 + */ + public void clean(){ + this.creator = null; + this.createTime = null; + this.updater = null; + this.updateTime = null; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java new file mode 100644 index 0000000..3929b71 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.framework.mybatis.core.enums; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.DbType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 针对 MyBatis Plus 的 {@link DbType} 增强,补充更多信息 + */ +@Getter +@AllArgsConstructor +public enum DbTypeEnum { + + /** + * H2 + * + * 注意:H2 不支持 find_in_set 函数 + */ + H2(DbType.H2, "H2", ""), + + /** + * MySQL + */ + MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"), + + /** + * Oracle + */ + ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"), + + /** + * PostgreSQL + * + * 华为 openGauss 使用 ProductName 与 PostgreSQL 相同 + */ + POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"), + + /** + * SQL Server + */ + SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"), + /** + * SQL Server 2005 + */ + SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"), + + /** + * 达梦 + */ + DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"), + + /** + * 人大金仓 + */ + KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"), + ; + + public static final Map MAP_BY_NAME = Arrays.stream(values()) + .collect(Collectors.toMap(DbTypeEnum::getProductName, Function.identity())); + + public static final Map MAP_BY_MP = Arrays.stream(values()) + .collect(Collectors.toMap(DbTypeEnum::getMpDbType, Function.identity())); + + /** + * MyBatis Plus 类型 + */ + private final DbType mpDbType; + /** + * 数据库产品名 + */ + private final String productName; + /** + * SQL FIND_IN_SET 模板 + */ + private final String findInSetTemplate; + + public static DbType find(String databaseProductName) { + if (StrUtil.isBlank(databaseProductName)) { + return null; + } + return MAP_BY_NAME.get(databaseProductName).getMpDbType(); + } + + public static String getFindInSetTemplate(DbType dbType) { + return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate()) + .orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported")); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java new file mode 100644 index 0000000..3c35999 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.mybatis.core.handler; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * 通用参数填充实现类 + * + * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值 + * + * @author hexiaowu + */ +public class DefaultDBFieldHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) { + BaseDO baseDO = (BaseDO) metaObject.getOriginalObject(); + + LocalDateTime current = LocalDateTime.now(); + // 创建时间为空,则以当前时间为插入时间 + if (Objects.isNull(baseDO.getCreateTime())) { + baseDO.setCreateTime(current); + } + // 更新时间为空,则以当前时间为更新时间 + if (Objects.isNull(baseDO.getUpdateTime())) { + baseDO.setUpdateTime(current); + } + + Long userId = SecurityFrameworkUtils.getLoginUserId(); + // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 + if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { + baseDO.setCreator(userId.toString()); + } + // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 + if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) { + baseDO.setUpdater(userId.toString()); + } + } + } + + @Override + public void updateFill(MetaObject metaObject) { + // 更新时间为空,则以当前时间为更新时间 + Object modifyTime = getFieldValByName("updateTime", metaObject); + if (Objects.isNull(modifyTime)) { + setFieldValByName("updateTime", LocalDateTime.now(), metaObject); + } + + // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 + Object modifier = getFieldValByName("updater", metaObject); + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (Objects.nonNull(userId) && Objects.isNull(modifier)) { + setFieldValByName("updater", userId.toString(), metaObject); + } + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java new file mode 100644 index 0000000..52ca947 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java @@ -0,0 +1,226 @@ +package cn.iocoder.yudao.framework.mybatis.core.mapper; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.SortablePageParam; +import cn.iocoder.yudao.framework.common.pojo.SortingField; +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import com.github.yulichang.base.MPJBaseMapper; +import com.github.yulichang.interfaces.MPJBaseJoin; +import com.github.yulichang.wrapper.MPJLambdaWrapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 + * + * 1. {@link BaseMapper} 为 MyBatis Plus 的基础接口,提供基础的 CRUD 能力 + * 2. {@link MPJBaseMapper} 为 MyBatis Plus Join 的基础接口,提供连表 Join 能力 + */ +public interface BaseMapperX extends MPJBaseMapper { + + default PageResult selectPage(SortablePageParam pageParam, @Param("ew") Wrapper queryWrapper) { + return selectPage(pageParam, pageParam.getSortingFields(), queryWrapper); + } + + default PageResult selectPage(PageParam pageParam, @Param("ew") Wrapper queryWrapper) { + return selectPage(pageParam, null, queryWrapper); + } + + default PageResult selectPage(PageParam pageParam, Collection sortingFields, @Param("ew") Wrapper queryWrapper) { + // 特殊:不分页,直接查询全部 + if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { + MyBatisUtils.addOrder(queryWrapper, sortingFields); + List list = selectList(queryWrapper); + return new PageResult<>(list, (long) list.size()); + } + + // MyBatis Plus 查询 + IPage mpPage = MyBatisUtils.buildPage(pageParam, sortingFields); + selectPage(mpPage, queryWrapper); + // 转换返回 + return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); + } + + default PageResult selectJoinPage(PageParam pageParam, Class clazz, MPJLambdaWrapper lambdaWrapper) { + // 特殊:不分页,直接查询全部 + if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { + List list = selectJoinList(clazz, lambdaWrapper); + return new PageResult<>(list, (long) list.size()); + } + + // MyBatis Plus Join 查询 + IPage mpPage = MyBatisUtils.buildPage(pageParam); + mpPage = selectJoinPage(mpPage, clazz, lambdaWrapper); + // 转换返回 + return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); + } + + default PageResult selectJoinPage(PageParam pageParam, Class resultTypeClass, MPJBaseJoin joinQueryWrapper) { + IPage mpPage = MyBatisUtils.buildPage(pageParam); + selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper); + // 转换返回 + return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); + } + + default T selectOne(String field, Object value) { + return selectOne(new QueryWrapper().eq(field, value)); + } + + default T selectOne(SFunction field, Object value) { + return selectOne(new LambdaQueryWrapper().eq(field, value)); + } + + default T selectOne(String field1, Object value1, String field2, Object value2) { + return selectOne(new QueryWrapper().eq(field1, value1).eq(field2, value2)); + } + + default T selectOne(SFunction field1, Object value1, SFunction field2, Object value2) { + return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); + } + + default T selectOne(SFunction field1, Object value1, SFunction field2, Object value2, + SFunction field3, Object value3) { + return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2).eq(field3, value3)); + } + + /** + * 获取满足条件的第 1 条记录 + * + * 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题 + * + * @param field 字段名 + * @param value 字段值 + * @return 实体 + */ + default T selectFirstOne(SFunction field, Object value) { + // 如果明确使用 MySQL 等场景,可以考虑使用 LIMIT 1 进行优化 + List list = selectList(new LambdaQueryWrapper().eq(field, value)); + return CollUtil.getFirst(list); + } + + default T selectFirstOne(SFunction field1, Object value1, SFunction field2, Object value2) { + List list = selectList(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); + return CollUtil.getFirst(list); + } + + default T selectFirstOne(SFunction field1, Object value1, SFunction field2, Object value2, + SFunction field3, Object value3) { + List list = selectList(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2).eq(field3, value3)); + return CollUtil.getFirst(list); + } + + + default Long selectCount() { + return selectCount(new QueryWrapper<>()); + } + + default Long selectCount(String field, Object value) { + return selectCount(new QueryWrapper().eq(field, value)); + } + + default Long selectCount(SFunction field, Object value) { + return selectCount(new LambdaQueryWrapper().eq(field, value)); + } + + default List selectList() { + return selectList(new QueryWrapper<>()); + } + + default List selectList(String field, Object value) { + return selectList(new QueryWrapper().eq(field, value)); + } + + default List selectList(SFunction field, Object value) { + return selectList(new LambdaQueryWrapper().eq(field, value)); + } + + default List selectList(String field, Collection values) { + if (CollUtil.isEmpty(values)) { + return CollUtil.newArrayList(); + } + return selectList(new QueryWrapper().in(field, values)); + } + + default List selectList(SFunction field, Collection values) { + if (CollUtil.isEmpty(values)) { + return CollUtil.newArrayList(); + } + return selectList(new LambdaQueryWrapper().in(field, values)); + } + + default List selectList(SFunction field1, Object value1, SFunction field2, Object value2) { + return selectList(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); + } + + /** + * 批量插入,适合大量数据插入 + * + * @param entities 实体们 + */ + default Boolean insertBatch(Collection entities) { + // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理 + DbType dbType = JdbcUtils.getDbType(); + if (JdbcUtils.isSQLServer(dbType)) { + entities.forEach(this::insert); + return CollUtil.isNotEmpty(entities); + } + return Db.saveBatch(entities); + } + + /** + * 批量插入,适合大量数据插入 + * + * @param entities 实体们 + * @param size 插入数量 Db.saveBatch 默认为 1000 + */ + default Boolean insertBatch(Collection entities, int size) { + // 特殊:SQL Server 批量插入后,获取 id 会报错,因此通过循环处理 + DbType dbType = JdbcUtils.getDbType(); + if (JdbcUtils.isSQLServer(dbType)) { + entities.forEach(this::insert); + return CollUtil.isNotEmpty(entities); + } + return Db.saveBatch(entities, size); + } + + default int updateBatch(T update) { + return update(update, new QueryWrapper<>()); + } + + default Boolean updateBatch(Collection entities) { + return Db.updateBatchById(entities); + } + + default Boolean updateBatch(Collection entities, int size) { + return Db.updateBatchById(entities, size); + } + + default int delete(String field, String value) { + return delete(new QueryWrapper().eq(field, value)); + } + + default int delete(SFunction field, Object value) { + return delete(new LambdaQueryWrapper().eq(field, value)); + } + + default int deleteBatch(SFunction field, Collection values) { + if (CollUtil.isEmpty(values)) { + return 0; + } + return delete(new LambdaQueryWrapper().in(field, values)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java new file mode 100644 index 0000000..a728365 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/LambdaQueryWrapperX.java @@ -0,0 +1,135 @@ +package cn.iocoder.yudao.framework.mybatis.core.query; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: + *

+ * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class LambdaQueryWrapperX extends LambdaQueryWrapper { + + public LambdaQueryWrapperX likeIfPresent(SFunction column, String val) { + if (StringUtils.hasText(val)) { + return (LambdaQueryWrapperX) super.like(column, val); + } + return this; + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Collection values) { + if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { + return (LambdaQueryWrapperX) super.in(column, values); + } + return this; + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Object... values) { + if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { + return (LambdaQueryWrapperX) super.in(column, values); + } + return this; + } + + public LambdaQueryWrapperX eqIfPresent(SFunction column, Object val) { + if (ObjectUtil.isNotEmpty(val)) { + return (LambdaQueryWrapperX) super.eq(column, val); + } + return this; + } + + public LambdaQueryWrapperX neIfPresent(SFunction column, Object val) { + if (ObjectUtil.isNotEmpty(val)) { + return (LambdaQueryWrapperX) super.ne(column, val); + } + return this; + } + + public LambdaQueryWrapperX gtIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.gt(column, val); + } + return this; + } + + public LambdaQueryWrapperX geIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.ge(column, val); + } + return this; + } + + public LambdaQueryWrapperX ltIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.lt(column, val); + } + return this; + } + + public LambdaQueryWrapperX leIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.le(column, val); + } + return this; + } + + public LambdaQueryWrapperX betweenIfPresent(SFunction column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (LambdaQueryWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (LambdaQueryWrapperX) ge(column, val1); + } + if (val2 != null) { + return (LambdaQueryWrapperX) le(column, val2); + } + return this; + } + + public LambdaQueryWrapperX betweenIfPresent(SFunction column, Object[] values) { + Object val1 = ArrayUtils.get(values, 0); + Object val2 = ArrayUtils.get(values, 1); + return betweenIfPresent(column, val1, val2); + } + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public LambdaQueryWrapperX eq(boolean condition, SFunction column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public LambdaQueryWrapperX eq(SFunction column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public LambdaQueryWrapperX orderByDesc(SFunction column) { + super.orderByDesc(true, column); + return this; + } + + @Override + public LambdaQueryWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public LambdaQueryWrapperX in(SFunction column, Collection coll) { + super.in(column, coll); + return this; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java new file mode 100644 index 0000000..48e901d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java @@ -0,0 +1,349 @@ +package cn.iocoder.yudao.framework.mybatis.core.query; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.github.yulichang.wrapper.MPJLambdaWrapper; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.function.Consumer; + +/** + * 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能: + *

+ * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * 2. SFunction column + 泛型:支持任意类字段(主表、子表、三表),推荐写法, 让编译器自动推断 S 类型 + * @param 数据类型 + */ +public class MPJLambdaWrapperX extends MPJLambdaWrapper { + + public MPJLambdaWrapperX likeIfPresent(SFunction column, String val) { + if (StringUtils.hasText(val)) { + return (MPJLambdaWrapperX) super.like(column, val); + } + return this; + } + + public MPJLambdaWrapperX inIfPresent(SFunction column, Collection values) { + if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { + return (MPJLambdaWrapperX) super.in(column, values); + } + return this; + } + + public MPJLambdaWrapperX inIfPresent(SFunction column, Object... values) { + if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { + return (MPJLambdaWrapperX) super.in(column, values); + } + return this; + } + + public MPJLambdaWrapperX eqIfPresent(SFunction column, Object val) { + if (ObjectUtil.isNotEmpty(val)) { + return (MPJLambdaWrapperX) super.eq(column, val); + } + return this; + } + + public MPJLambdaWrapperX neIfPresent(SFunction column, Object val) { + if (ObjectUtil.isNotEmpty(val)) { + return (MPJLambdaWrapperX) super.ne(column, val); + } + return this; + } + + public MPJLambdaWrapperX gtIfPresent(SFunction column, Object val) { + if (val != null) { + return (MPJLambdaWrapperX) super.gt(column, val); + } + return this; + } + + public MPJLambdaWrapperX geIfPresent(SFunction column, Object val) { + if (val != null) { + return (MPJLambdaWrapperX) super.ge(column, val); + } + return this; + } + + public MPJLambdaWrapperX ltIfPresent(SFunction column, Object val) { + if (val != null) { + return (MPJLambdaWrapperX) super.lt(column, val); + } + return this; + } + + public MPJLambdaWrapperX leIfPresent(SFunction column, Object val) { + if (val != null) { + return (MPJLambdaWrapperX) super.le(column, val); + } + return this; + } + + public MPJLambdaWrapperX betweenIfPresent(SFunction column, Object[] values) { + Object val1 = ArrayUtils.get(values, 0); + Object val2 = ArrayUtils.get(values, 1); + return betweenIfPresent(column, val1, val2); + } + + public MPJLambdaWrapperX betweenIfPresent(SFunction column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (MPJLambdaWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (MPJLambdaWrapperX) super.ge(column, val1); + } + if (val2 != null) { + return (MPJLambdaWrapperX) super.le(column, val2); + } + return this; + } + + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public MPJLambdaWrapperX eq(boolean condition, SFunction column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public MPJLambdaWrapperX eq(SFunction column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public MPJLambdaWrapperX orderByDesc(SFunction column) { + //noinspection unchecked + super.orderByDesc(true, column); + return this; + } + + @Override + public MPJLambdaWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public MPJLambdaWrapperX in(SFunction column, Collection coll) { + super.in(column, coll); + return this; + } + + @Override + public MPJLambdaWrapperX selectAll(Class clazz) { + super.selectAll(clazz); + return this; + } + + @Override + public MPJLambdaWrapperX selectAll(Class clazz, String prefix) { + super.selectAll(clazz, prefix); + return this; + } + + @Override + public MPJLambdaWrapperX selectAs(SFunction column, String alias) { + super.selectAs(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectAs(String column, SFunction alias) { + super.selectAs(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectAs(SFunction column, SFunction alias) { + super.selectAs(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectAs(String index, SFunction column, SFunction alias) { + super.selectAs(index, column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectAsClass(Class source, Class tag) { + super.selectAsClass(source, tag); + return this; + } + + @Override + public MPJLambdaWrapperX selectSub(Class clazz, Consumer> consumer, SFunction alias) { + super.selectSub(clazz, consumer, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectSub(Class clazz, String st, Consumer> consumer, SFunction alias) { + super.selectSub(clazz, st, consumer, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectCount(SFunction column) { + super.selectCount(column); + return this; + } + + @Override + public MPJLambdaWrapperX selectCount(Object column, String alias) { + super.selectCount(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectCount(Object column, SFunction alias) { + super.selectCount(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectCount(SFunction column, String alias) { + super.selectCount(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectCount(SFunction column, SFunction alias) { + super.selectCount(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectSum(SFunction column) { + super.selectSum(column); + return this; + } + + @Override + public MPJLambdaWrapperX selectSum(SFunction column, String alias) { + super.selectSum(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectSum(SFunction column, SFunction alias) { + super.selectSum(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectMax(SFunction column) { + super.selectMax(column); + return this; + } + + @Override + public MPJLambdaWrapperX selectMax(SFunction column, String alias) { + super.selectMax(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectMax(SFunction column, SFunction alias) { + super.selectMax(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectMin(SFunction column) { + super.selectMin(column); + return this; + } + + @Override + public MPJLambdaWrapperX selectMin(SFunction column, String alias) { + super.selectMin(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectMin(SFunction column, SFunction alias) { + super.selectMin(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectAvg(SFunction column) { + super.selectAvg(column); + return this; + } + + @Override + public MPJLambdaWrapperX selectAvg(SFunction column, String alias) { + super.selectAvg(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectAvg(SFunction column, SFunction alias) { + super.selectAvg(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectLen(SFunction column) { + super.selectLen(column); + return this; + } + + @Override + public MPJLambdaWrapperX selectLen(SFunction column, String alias) { + super.selectLen(column, alias); + return this; + } + + @Override + public MPJLambdaWrapperX selectLen(SFunction column, SFunction alias) { + super.selectLen(column, alias); + return this; + } + + // ========== 关键重写:使 leftJoin 返回当前类型 this ========== + @Override + public MPJLambdaWrapperX leftJoin(Class clazz, SFunction left, SFunction right) { + super.leftJoin(clazz, left, right); + return this; + } + + @Override + public MPJLambdaWrapperX rightJoin(Class clazz, SFunction left, SFunction right) { + super.rightJoin(clazz, left, right); + return this; + } + + @Override + public MPJLambdaWrapperX innerJoin(Class clazz, SFunction left, SFunction right) { + super.innerJoin(clazz, left, right); + return this; + } + + // ========== 添加扩展 Join 支持 ext 函数式参数 ========== + public MPJLambdaWrapperX leftJoin(Class clazz, SFunction left, SFunction right, Consumer> ext) { + super.leftJoin(clazz, left, right); + if (ext != null) ext.accept(this); + return this; + } + + public MPJLambdaWrapperX rightJoin(Class clazz, SFunction left, SFunction right, Consumer> ext) { + super.rightJoin(clazz, left, right); + if (ext != null) ext.accept(this); + return this; + } + + public MPJLambdaWrapperX innerJoin(Class clazz, SFunction left, SFunction right, Consumer> ext) { + super.innerJoin(clazz, left, right); + if (ext != null) ext.accept(this); + return this; + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java new file mode 100644 index 0000000..087b1b8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java @@ -0,0 +1,166 @@ +package cn.iocoder.yudao.framework.mybatis.core.query; + +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: + * + * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class QueryWrapperX extends QueryWrapper { + + public QueryWrapperX likeIfPresent(String column, String val) { + if (StringUtils.hasText(val)) { + return (QueryWrapperX) super.like(column, val); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Collection values) { + if (!CollectionUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Object... values) { + if (!ArrayUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX eqIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.eq(column, val); + } + return this; + } + + public QueryWrapperX neIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ne(column, val); + } + return this; + } + + public QueryWrapperX gtIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.gt(column, val); + } + return this; + } + + public QueryWrapperX geIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ge(column, val); + } + return this; + } + + public QueryWrapperX ltIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.lt(column, val); + } + return this; + } + + public QueryWrapperX leIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.le(column, val); + } + return this; + } + + public QueryWrapperX betweenIfPresent(String column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (QueryWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (QueryWrapperX) ge(column, val1); + } + if (val2 != null) { + return (QueryWrapperX) le(column, val2); + } + return this; + } + + public QueryWrapperX betweenIfPresent(String column, Object[] values) { + if (values!= null && values.length != 0 && values[0] != null && values[1] != null) { + return (QueryWrapperX) super.between(column, values[0], values[1]); + } + if (values!= null && values.length != 0 && values[0] != null) { + return (QueryWrapperX) ge(column, values[0]); + } + if (values!= null && values.length != 0 && values[1] != null) { + return (QueryWrapperX) le(column, values[1]); + } + return this; + } + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public QueryWrapperX eq(boolean condition, String column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public QueryWrapperX eq(String column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public QueryWrapperX orderByDesc(String column) { + super.orderByDesc(true, column); + return this; + } + + @Override + public QueryWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public QueryWrapperX in(String column, Collection coll) { + super.in(column, coll); + return this; + } + + /** + * 设置只返回最后一条 + * + * TODO 芋艿:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同 + * + * @return this + */ + public QueryWrapperX limitN(int n) { + DbType dbType = JdbcUtils.getDbType(); + switch (dbType) { + case ORACLE: + case ORACLE_12C: + super.le("ROWNUM", n); + break; + case SQL_SERVER: + case SQL_SERVER2005: + super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段 + break; + default: // MySQL、PostgreSQL、DM 达梦、KingbaseES 大金都是采用 LIMIT 实现 + super.last("LIMIT " + n); + } + return this; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java new file mode 100644 index 0000000..9327ebb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.framework.mybatis.core.type; + +import cn.hutool.core.lang.Assert; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.symmetric.AES; +import cn.hutool.extra.spring.SpringUtil; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 字段字段的 TypeHandler 实现类,基于 {@link AES} 实现 + * 可通过 jasypt.encryptor.password 配置项,设置密钥 + * + * @author 芋道源码 + */ +public class EncryptTypeHandler extends BaseTypeHandler { + + private static final String ENCRYPTOR_PROPERTY_NAME = "mybatis-plus.encryptor.password"; + + private static AES aes; + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, encrypt(parameter)); + } + + @Override + public String getNullableResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return decrypt(value); + } + + @Override + public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return decrypt(value); + } + + @Override + public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return decrypt(value); + } + + private static String decrypt(String value) { + if (value == null) { + return null; + } + return getEncryptor().decryptStr(value); + } + + public static String encrypt(String rawValue) { + if (rawValue == null) { + return null; + } + return getEncryptor().encryptBase64(rawValue); + } + + private static AES getEncryptor() { + if (aes != null) { + return aes; + } + // 构建 AES + String password = SpringUtil.getProperty(ENCRYPTOR_PROPERTY_NAME); + Assert.notEmpty(password, "配置项({}) 不能为空", ENCRYPTOR_PROPERTY_NAME); + aes = SecureUtil.aes(password.getBytes()); + return aes; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/IntegerListTypeHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/IntegerListTypeHandler.java new file mode 100644 index 0000000..18455ba --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/IntegerListTypeHandler.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * List 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author jason + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class IntegerListTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public List getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public List getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public List getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private List getResult(String value) { + if (value == null) { + return null; + } + return StrUtils.splitToInteger(value, COMMA); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongListTypeHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongListTypeHandler.java new file mode 100644 index 0000000..8c30f3d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongListTypeHandler.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * List 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author 芋道源码 + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class LongListTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { + // 设置占位符 + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public List getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public List getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public List getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private List getResult(String value) { + if (value == null) { + return null; + } + return StrUtils.splitToLong(value, COMMA); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java new file mode 100644 index 0000000..58d82ec --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Set; + +/** + * Set 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author 芋道源码 + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class LongSetTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, Set strings, JdbcType jdbcType) throws SQLException { + // 设置占位符 + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public Set getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public Set getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public Set getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private Set getResult(String value) { + if (value == null) { + return null; + } + return StrUtils.splitToLongSet(value, COMMA); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/StringListTypeHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/StringListTypeHandler.java new file mode 100644 index 0000000..598a15e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/StringListTypeHandler.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * List 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author 永不言败 + * @since 2022 3/23 12:50:15 + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class StringListTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { + // 设置占位符 + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public List getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public List getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public List getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private List getResult(String value) { + if (value == null) { + return null; + } + return StrUtil.splitTrim(value, COMMA); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java new file mode 100644 index 0000000..0ee22db --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.framework.mybatis.core.util; + +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; +import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.mybatisplus.annotation.DbType; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +/** + * JDBC 工具类 + * + * @author 芋道源码 + */ +public class JdbcUtils { + + /** + * 判断连接是否正确 + * + * @param url 数据源连接 + * @param username 账号 + * @param password 密码 + * @return 是否正确 + */ + public static boolean isConnectionOK(String url, String username, String password) { + try (Connection ignored = DriverManager.getConnection(url, username, password)) { + return true; + } catch (Exception ex) { + return false; + } + } + + /** + * 获得 URL 对应的 DB 类型 + * + * @param url URL + * @return DB 类型 + */ + public static DbType getDbType(String url) { + return com.baomidou.mybatisplus.extension.toolkit.JdbcUtils.getDbType(url); + } + + /** + * 通过当前数据库连接获得对应的 DB 类型 + * + * @return DB 类型 + */ + public static DbType getDbType() { + DataSource dataSource; + try { + DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class); + dataSource = dynamicRoutingDataSource.determineDataSource(); + } catch (NoSuchBeanDefinitionException e) { + dataSource = SpringUtils.getBean(DataSource.class); + } + try (Connection conn = dataSource.getConnection()) { + return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName()); + } catch (SQLException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + /** + * 判断 JDBC 连接是否为 SQLServer 数据库 + * + * @param url JDBC 连接 + * @return 是否为 SQLServer 数据库 + */ + public static boolean isSQLServer(String url) { + DbType dbType = getDbType(url); + return isSQLServer(dbType); + } + + /** + * 判断 JDBC 连接是否为 SQLServer 数据库 + * + * @param dbType DB 类型 + * @return 是否为 SQLServer 数据库 + */ + public static boolean isSQLServer(DbType dbType) { + return ObjectUtils.equalsAny(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java new file mode 100644 index 0000000..ac33ba8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java @@ -0,0 +1,135 @@ +package cn.iocoder.yudao.framework.mybatis.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.lang.func.LambdaUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.SortingField; +import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * MyBatis 工具类 + */ +public class MyBatisUtils { + + private static final String MYSQL_ESCAPE_CHARACTER = "`"; + + public static Page buildPage(PageParam pageParam) { + return buildPage(pageParam, null); + } + + public static Page buildPage(PageParam pageParam, Collection sortingFields) { + // 页码 + 数量 + Page page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); + // 排序字段 + if (CollUtil.isNotEmpty(sortingFields)) { + for (SortingField sortingField : sortingFields) { + page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder())) + .setColumn(StrUtil.toUnderlineCase(sortingField.getField()))); + } + } + return page; + } + + public static void addOrder(Wrapper wrapper, Collection sortingFields) { + if (CollUtil.isEmpty(sortingFields)) { + return; + } + QueryWrapper query = (QueryWrapper) wrapper; + for (SortingField sortingField : sortingFields) { + query.orderBy(true, + SortingField.ORDER_ASC.equals(sortingField.getOrder()), + StrUtil.toUnderlineCase(sortingField.getField())); + } + } + + /** + * 将拦截器添加到链中 + * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 + * + * @param interceptor 链 + * @param inner 拦截器 + * @param index 位置 + */ + public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) { + List inners = new ArrayList<>(interceptor.getInterceptors()); + inners.add(index, inner); + interceptor.setInterceptors(inners); + } + + /** + * 获得 Table 对应的表名 + *

+ * 兼容 MySQL 转义表名 `t_xxx` + * + * @param table 表 + * @return 去除转移字符后的表名 + */ + public static String getTableName(Table table) { + String tableName = table.getName(); + if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) { + tableName = tableName.substring(1, tableName.length() - 1); + } + return tableName; + } + + /** + * 构建 Column 对象 + * + * @param tableName 表名 + * @param tableAlias 别名 + * @param column 字段名 + * @return Column 对象 + */ + public static Column buildColumn(String tableName, Alias tableAlias, String column) { + if (tableAlias != null) { + tableName = tableAlias.getName(); + } + return new Column(tableName + StringPool.DOT + column); + } + + /** + * 跨数据库的 find_in_set 实现 + * + * @param column 字段名称 + * @param value 查询值(不带单引号) + * @return sql + */ + public static String findInSet(String column, Object value) { + DbType dbType = JdbcUtils.getDbType(); + return DbTypeEnum.getFindInSetTemplate(dbType) + .replace("#{column}", column) + .replace("#{value}", StrUtil.toString(value)); + } + + /** + * 将驼峰命名转换为下划线命名 + * + * 使用场景: + * 1. fix:修复"商品统计聚合函数的别名与排序字段不符"导致的 SQL 异常 + * + * @param func 字段名函数(驼峰命名) + * @return 字段名(下划线命名) + */ + public static String toUnderlineCase(Func1 func) { + String fieldName = LambdaUtil.getFieldName(func); + return StrUtil.toUnderlineCase(fieldName); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/package-info.java new file mode 100644 index 0000000..50b1e85 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 MyBatis Plus 提升使用 MyBatis 的开发效率 + */ +package cn.iocoder.yudao.framework.mybatis; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/config/YudaoTranslateAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/config/YudaoTranslateAutoConfiguration.java new file mode 100644 index 0000000..bcc46b8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/config/YudaoTranslateAutoConfiguration.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.framework.translate.config; + +import cn.iocoder.yudao.framework.translate.core.TranslateUtils; +import com.fhs.trans.service.impl.TransService; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class YudaoTranslateAutoConfiguration { + + @Bean + @SuppressWarnings({"InstantiationOfUtilityClass", "SpringJavaInjectionPointsAutowiringInspection"}) + public TranslateUtils translateUtils(TransService transService) { + TranslateUtils.init(transService); + return new TranslateUtils(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/core/TranslateUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/core/TranslateUtils.java new file mode 100644 index 0000000..2974b0c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/core/TranslateUtils.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.translate.core; + +import cn.hutool.core.collection.CollUtil; +import com.fhs.core.trans.vo.VO; +import com.fhs.trans.service.impl.TransService; + +import java.util.List; + +/** + * VO 数据翻译 Utils + * + * @author 芋道源码 + */ +public class TranslateUtils { + + private static TransService transService; + + public static void init(TransService transService) { + TranslateUtils.transService = transService; + } + + /** + * 数据翻译 + * + * 使用场景:无法使用 @TransMethodResult 注解的场景,只能通过手动触发翻译 + * + * @param data 数据 + * @return 翻译结果 + */ + public static List translate(List data) { + if (CollUtil.isNotEmpty((data))) { + transService.transBatch(data); + } + return data; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/package-info.java new file mode 100644 index 0000000..f14914a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Easy-Trans 提升使用 VO 数据翻译的开发效率 + */ +package cn.iocoder.yudao.framework.translate; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..eb172e4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ + cn.iocoder.yudao.framework.mybatis.config.IdTypeEnvironmentPostProcessor diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..4a4eb95 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration +cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration +cn.iocoder.yudao.framework.translate.config.YudaoTranslateAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md new file mode 100644 index 0000000..d684e1d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md new file mode 100644 index 0000000..2113374 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md new file mode 100644 index 0000000..21b5075 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/pom.xml new file mode 100644 index 0000000..7e7279e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/pom.xml @@ -0,0 +1,47 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-protection + jar + + ${project.artifactId} + 服务保证,提供分布式锁、幂等、限流、熔断、API 签名等等功能 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + cn.iocoder.boot + yudao-spring-boot-starter-web + provided + + + + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + + + + com.baomidou + lock4j-redisson-spring-boot-starter + true + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/config/YudaoIdempotentConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/config/YudaoIdempotentConfiguration.java new file mode 100644 index 0000000..de74963 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/config/YudaoIdempotentConfiguration.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.idempotent.config; + +import cn.iocoder.yudao.framework.idempotent.core.aop.IdempotentAspect; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.List; + +@AutoConfiguration(after = YudaoRedisAutoConfiguration.class) +public class YudaoIdempotentConfiguration { + + @Bean + public IdempotentAspect idempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { + return new IdempotentAspect(keyResolvers, idempotentRedisDAO); + } + + @Bean + public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) { + return new IdempotentRedisDAO(stringRedisTemplate); + } + + // ========== 各种 IdempotentKeyResolver Bean ========== + + @Bean + public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() { + return new DefaultIdempotentKeyResolver(); + } + + @Bean + public UserIdempotentKeyResolver userIdempotentKeyResolver() { + return new UserIdempotentKeyResolver(); + } + + @Bean + public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { + return new ExpressionIdempotentKeyResolver(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/annotation/Idempotent.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/annotation/Idempotent.java new file mode 100644 index 0000000..cd6add8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/annotation/Idempotent.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.idempotent.core.annotation; + +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * 幂等注解 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Idempotent { + + /** + * 幂等的超时时间,默认为 1 秒 + * + * 注意,如果执行时间超过它,请求还是会进来 + */ + int timeout() default 1; + /** + * 时间单位,默认为 SECONDS 秒 + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * 提示信息,正在执行中的提示 + */ + String message() default "重复请求,请稍后重试"; + + /** + * 使用的 Key 解析器 + * + * @see DefaultIdempotentKeyResolver 全局级别 + * @see UserIdempotentKeyResolver 用户级别 + * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算 + */ + Class keyResolver() default DefaultIdempotentKeyResolver.class; + /** + * 使用的 Key 参数 + */ + String keyArg() default ""; + + /** + * 删除 Key,当发生异常时候 + * + * 问题:为什么发生异常时,需要删除 Key 呢? + * 回答:发生异常时,说明业务发生错误,此时需要删除 Key,避免下次请求无法正常执行。 + * + * 问题:为什么不搞 deleteWhenSuccess 执行成功时,需要删除 Key 呢? + * 回答:这种情况下,本质上是分布式锁,推荐使用 @Lock4j 注解 + */ + boolean deleteKeyWhenException() default true; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/aop/IdempotentAspect.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/aop/IdempotentAspect.java new file mode 100644 index 0000000..11ff765 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/aop/IdempotentAspect.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.framework.idempotent.core.aop; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Map; + +/** + * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class IdempotentAspect { + + /** + * IdempotentKeyResolver 集合 + */ + private final Map, IdempotentKeyResolver> keyResolvers; + + private final IdempotentRedisDAO idempotentRedisDAO; + + public IdempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { + this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass); + this.idempotentRedisDAO = idempotentRedisDAO; + } + + @Around(value = "@annotation(idempotent)") + public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { + // 获得 IdempotentKeyResolver + IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); + Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); + // 解析 Key + String key = keyResolver.resolver(joinPoint, idempotent); + + // 1. 锁定 Key + boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); + // 锁定失败,抛出异常 + if (!success) { + log.info("[aroundPointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); + throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); + } + + // 2. 执行逻辑 + try { + return joinPoint.proceed(); + } catch (Throwable throwable) { + // 3. 异常时,删除 Key + // 参考美团 GTIS 思路:https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html + if (idempotent.deleteKeyWhenException()) { + idempotentRedisDAO.delete(key); + } + throw throwable; + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java new file mode 100644 index 0000000..1617570 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.idempotent.core.keyresolver; + +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; +import org.aspectj.lang.JoinPoint; + +/** + * 幂等 Key 解析器接口 + * + * @author 芋道源码 + */ +public interface IdempotentKeyResolver { + + /** + * 解析一个 Key + * + * @param idempotent 幂等注解 + * @param joinPoint AOP 切面 + * @return Key + */ + String resolver(JoinPoint joinPoint, Idempotent idempotent); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java new file mode 100644 index 0000000..4021848 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.crypto.SecureUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import org.aspectj.lang.JoinPoint; + +/** + * 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key + * + * 为了避免 Key 过长,使用 MD5 进行“压缩” + * + * @author 芋道源码 + */ +public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver { + + @Override + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { + String methodName = joinPoint.getSignature().toString(); + String argsStr = StrUtils.joinMethodArgs(joinPoint); + return SecureUtil.md5(methodName + argsStr); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java new file mode 100644 index 0000000..665d327 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * 基于 Spring EL 表达式, + * + * @author 芋道源码 + */ +public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { + + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + private final ExpressionParser expressionParser = new SpelExpressionParser(); + + @Override + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { + // 获得被拦截方法参数名列表 + Method method = getMethod(joinPoint); + Object[] args = joinPoint.getArgs(); + String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); + // 准备 Spring EL 表达式解析的上下文 + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + if (ArrayUtil.isNotEmpty(parameterNames)) { + for (int i = 0; i < parameterNames.length; i++) { + evaluationContext.setVariable(parameterNames[i], args[i]); + } + } + + // 解析参数 + Expression expression = expressionParser.parseExpression(idempotent.keyArg()); + return expression.getValue(evaluationContext, String.class); + } + + private static Method getMethod(JoinPoint point) { + // 处理,声明在类上的情况 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + if (!method.getDeclaringClass().isInterface()) { + return method; + } + + // 处理,声明在接口上的情况 + try { + return point.getTarget().getClass().getDeclaredMethod( + point.getSignature().getName(), method.getParameterTypes()); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java new file mode 100644 index 0000000..dd89617 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.crypto.SecureUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.aspectj.lang.JoinPoint; + +/** + * 用户级别的幂等 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key + * + * 为了避免 Key 过长,使用 MD5 进行“压缩” + * + * @author 芋道源码 + */ +public class UserIdempotentKeyResolver implements IdempotentKeyResolver { + + @Override + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { + String methodName = joinPoint.getSignature().toString(); + String argsStr = StrUtils.joinMethodArgs(joinPoint); + Long userId = WebFrameworkUtils.getLoginUserId(); + Integer userType = WebFrameworkUtils.getLoginUserType(); + return SecureUtil.md5(methodName + argsStr + userId + userType); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/redis/IdempotentRedisDAO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/redis/IdempotentRedisDAO.java new file mode 100644 index 0000000..a8d981d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/redis/IdempotentRedisDAO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.idempotent.core.redis; + +import lombok.AllArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 幂等 Redis DAO + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class IdempotentRedisDAO { + + /** + * 幂等操作 + * + * KEY 格式:idempotent:%s // 参数为 uuid + * VALUE 格式:String + * 过期时间:不固定 + */ + private static final String IDEMPOTENT = "idempotent:%s"; + + private final StringRedisTemplate redisTemplate; + + public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) { + String redisKey = formatKey(key); + return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit); + } + + public void delete(String key) { + String redisKey = formatKey(key); + redisTemplate.delete(redisKey); + } + + private static String formatKey(String key) { + return String.format(IDEMPOTENT, key); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/package-info.java new file mode 100644 index 0000000..8cad5fc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/package-info.java @@ -0,0 +1,12 @@ +/** + * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现 + * 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。 + * + * 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。 + * + * 和 it4alla/idempotent 组件的差异点,主要体现在两点: + * 1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力 + * 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。 + * 2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。 + */ +package cn.iocoder.yudao.framework.idempotent; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/config/YudaoLock4jConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/config/YudaoLock4jConfiguration.java new file mode 100644 index 0000000..f9b883c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/config/YudaoLock4jConfiguration.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.framework.lock4j.config; + +import cn.iocoder.yudao.framework.lock4j.core.DefaultLockFailureStrategy; +import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration(before = LockAutoConfiguration.class) +@ConditionalOnClass(name = "com.baomidou.lock.annotation.Lock4j") +public class YudaoLock4jConfiguration { + + @Bean + public DefaultLockFailureStrategy lockFailureStrategy() { + return new DefaultLockFailureStrategy(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java new file mode 100644 index 0000000..2cea08b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.lock4j.core; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.baomidou.lock.LockFailureStrategy; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; + +/** + * 自定义获取锁失败策略,抛出 {@link ServiceException} 异常 + */ +@Slf4j +public class DefaultLockFailureStrategy implements LockFailureStrategy { + + @Override + public void onLockFailure(String key, Method method, Object[] arguments) { + log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments); + throw new ServiceException(GlobalErrorCodeConstants.LOCKED); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/Lock4jRedisKeyConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/Lock4jRedisKeyConstants.java new file mode 100644 index 0000000..693d052 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/Lock4jRedisKeyConstants.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.lock4j.core; + +/** + * Lock4j Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface Lock4jRedisKeyConstants { + + /** + * 分布式锁 + * + * KEY 格式:lock4j:%s // 参数来自 DefaultLockKeyBuilder 类 + * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构 + * 过期时间:不固定 + */ + String LOCK4J = "lock4j:%s"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/package-info.java new file mode 100644 index 0000000..267b951 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/package-info.java @@ -0,0 +1,4 @@ +/** + * 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目 + */ +package cn.iocoder.yudao.framework.lock4j; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/config/YudaoRateLimiterConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/config/YudaoRateLimiterConfiguration.java new file mode 100644 index 0000000..68b910f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/config/YudaoRateLimiterConfiguration.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.ratelimiter.config; + +import cn.iocoder.yudao.framework.ratelimiter.core.aop.RateLimiterAspect; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.*; +import cn.iocoder.yudao.framework.ratelimiter.core.redis.RateLimiterRedisDAO; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import org.redisson.api.RedissonClient; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +@AutoConfiguration(after = YudaoRedisAutoConfiguration.class) +public class YudaoRateLimiterConfiguration { + + @Bean + public RateLimiterAspect rateLimiterAspect(List keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) { + return new RateLimiterAspect(keyResolvers, rateLimiterRedisDAO); + } + + @Bean + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + public RateLimiterRedisDAO rateLimiterRedisDAO(RedissonClient redissonClient) { + return new RateLimiterRedisDAO(redissonClient); + } + + // ========== 各种 RateLimiterRedisDAO Bean ========== + + @Bean + public DefaultRateLimiterKeyResolver defaultRateLimiterKeyResolver() { + return new DefaultRateLimiterKeyResolver(); + } + + @Bean + public UserRateLimiterKeyResolver userRateLimiterKeyResolver() { + return new UserRateLimiterKeyResolver(); + } + + @Bean + public ClientIpRateLimiterKeyResolver clientIpRateLimiterKeyResolver() { + return new ClientIpRateLimiterKeyResolver(); + } + + @Bean + public ServerNodeRateLimiterKeyResolver serverNodeRateLimiterKeyResolver() { + return new ServerNodeRateLimiterKeyResolver(); + } + + @Bean + public ExpressionRateLimiterKeyResolver expressionRateLimiterKeyResolver() { + return new ExpressionRateLimiterKeyResolver(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/annotation/RateLimiter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/annotation/RateLimiter.java new file mode 100644 index 0000000..417c4d6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/annotation/RateLimiter.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.ratelimiter.core.annotation; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.ClientIpRateLimiterKeyResolver; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.DefaultRateLimiterKeyResolver; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.ServerNodeRateLimiterKeyResolver; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.UserRateLimiterKeyResolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * 限流注解 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RateLimiter { + + /** + * 限流的时间,默认为 1 秒 + */ + int time() default 1; + /** + * 时间单位,默认为 SECONDS 秒 + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * 限流次数 + */ + int count() default 100; + + /** + * 提示信息,请求过快的提示 + * + * @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS + */ + String message() default ""; // 为空时,使用 TOO_MANY_REQUESTS 错误提示 + + /** + * 使用的 Key 解析器 + * + * @see DefaultRateLimiterKeyResolver 全局级别 + * @see UserRateLimiterKeyResolver 用户 ID 级别 + * @see ClientIpRateLimiterKeyResolver 用户 IP 级别 + * @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别 + * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算 + */ + Class keyResolver() default DefaultRateLimiterKeyResolver.class; + /** + * 使用的 Key 参数 + */ + String keyArg() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/aop/RateLimiterAspect.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/aop/RateLimiterAspect.java new file mode 100644 index 0000000..6ede62b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/aop/RateLimiterAspect.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.framework.ratelimiter.core.aop; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; +import cn.iocoder.yudao.framework.ratelimiter.core.redis.RateLimiterRedisDAO; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Map; + +/** + * 拦截声明了 {@link RateLimiter} 注解的方法,实现限流操作 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class RateLimiterAspect { + + /** + * RateLimiterKeyResolver 集合 + */ + private final Map, RateLimiterKeyResolver> keyResolvers; + + private final RateLimiterRedisDAO rateLimiterRedisDAO; + + public RateLimiterAspect(List keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) { + this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass); + this.rateLimiterRedisDAO = rateLimiterRedisDAO; + } + + @Before("@annotation(rateLimiter)") + public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) { + // 获得 IdempotentKeyResolver 对象 + RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver()); + Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver"); + // 解析 Key + String key = keyResolver.resolver(joinPoint, rateLimiter); + + // 获取 1 次限流 + boolean success = rateLimiterRedisDAO.tryAcquire(key, + rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit()); + if (!success) { + log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs()); + String message = StrUtil.blankToDefault(rateLimiter.message(), + GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg()); + throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message); + } + } + +} + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java new file mode 100644 index 0000000..44d7bdf --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/RateLimiterKeyResolver.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver; + +import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; +import org.aspectj.lang.JoinPoint; + +/** + * 限流 Key 解析器接口 + * + * @author 芋道源码 + */ +public interface RateLimiterKeyResolver { + + /** + * 解析一个 Key + * + * @param rateLimiter 限流注解 + * @param joinPoint AOP 切面 + * @return Key + */ + String resolver(JoinPoint joinPoint, RateLimiter rateLimiter); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java new file mode 100644 index 0000000..4ed68dd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ClientIpRateLimiterKeyResolver.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl; + +import cn.hutool.crypto.SecureUtil; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; +import org.aspectj.lang.JoinPoint; + +/** + * IP 级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key + * + * 为了避免 Key 过长,使用 MD5 进行“压缩” + * + * @author 芋道源码 + */ +public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver { + + @Override + public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { + String methodName = joinPoint.getSignature().toString(); + String argsStr = StrUtils.joinMethodArgs(joinPoint); + String clientIp = ServletUtils.getClientIP(); + return SecureUtil.md5(methodName + argsStr + clientIp); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java new file mode 100644 index 0000000..9ae2ff3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/DefaultRateLimiterKeyResolver.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl; + +import cn.hutool.crypto.SecureUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; +import org.aspectj.lang.JoinPoint; + +/** + * 默认(全局级别)限流 Key 解析器,使用方法名 + 方法参数,组装成一个 Key + * + * 为了避免 Key 过长,使用 MD5 进行“压缩” + * + * @author 芋道源码 + */ +public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver { + + @Override + public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { + String methodName = joinPoint.getSignature().toString(); + String argsStr = StrUtils.joinMethodArgs(joinPoint); + return SecureUtil.md5(methodName + argsStr); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java new file mode 100644 index 0000000..118581e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ExpressionRateLimiterKeyResolver.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * 基于 Spring EL 表达式的 {@link RateLimiterKeyResolver} 实现类 + * + * @author 芋道源码 + */ +public class ExpressionRateLimiterKeyResolver implements RateLimiterKeyResolver { + + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + private final ExpressionParser expressionParser = new SpelExpressionParser(); + + @Override + public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { + // 获得被拦截方法参数名列表 + Method method = getMethod(joinPoint); + Object[] args = joinPoint.getArgs(); + String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); + // 准备 Spring EL 表达式解析的上下文 + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + if (ArrayUtil.isNotEmpty(parameterNames)) { + for (int i = 0; i < parameterNames.length; i++) { + evaluationContext.setVariable(parameterNames[i], args[i]); + } + } + + // 解析参数 + Expression expression = expressionParser.parseExpression(rateLimiter.keyArg()); + return expression.getValue(evaluationContext, String.class); + } + + private static Method getMethod(JoinPoint point) { + // 处理,声明在类上的情况 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + if (!method.getDeclaringClass().isInterface()) { + return method; + } + + // 处理,声明在接口上的情况 + try { + return point.getTarget().getClass().getDeclaredMethod( + point.getSignature().getName(), method.getParameterTypes()); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java new file mode 100644 index 0000000..653790d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/ServerNodeRateLimiterKeyResolver.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl; + +import cn.hutool.crypto.SecureUtil; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; +import org.aspectj.lang.JoinPoint; + +/** + * Server 节点级别的限流 Key 解析器,使用方法名 + 方法参数 + IP,组装成一个 Key + * + * 为了避免 Key 过长,使用 MD5 进行“压缩” + * + * @author 芋道源码 + */ +public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver { + + @Override + public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { + String methodName = joinPoint.getSignature().toString(); + String argsStr = StrUtils.joinMethodArgs(joinPoint); + String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); + return SecureUtil.md5(methodName + argsStr + serverNode); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java new file mode 100644 index 0000000..eccabf1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/keyresolver/impl/UserRateLimiterKeyResolver.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl; + +import cn.hutool.crypto.SecureUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter; +import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.aspectj.lang.JoinPoint; + +/** + * 用户级别的限流 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key + * + * 为了避免 Key 过长,使用 MD5 进行“压缩” + * + * @author 芋道源码 + */ +public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver { + + @Override + public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) { + String methodName = joinPoint.getSignature().toString(); + String argsStr = StrUtils.joinMethodArgs(joinPoint); + Long userId = WebFrameworkUtils.getLoginUserId(); + Integer userType = WebFrameworkUtils.getLoginUserType(); + return SecureUtil.md5(methodName + argsStr + userId + userType); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java new file mode 100644 index 0000000..2bcce37 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/core/redis/RateLimiterRedisDAO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.framework.ratelimiter.core.redis; + +import lombok.AllArgsConstructor; +import org.redisson.api.*; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 限流 Redis DAO + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class RateLimiterRedisDAO { + + /** + * 限流操作 + * + * KEY 格式:rate_limiter:%s // 参数为 uuid + * VALUE 格式:String + * 过期时间:不固定 + */ + private static final String RATE_LIMITER = "rate_limiter:%s"; + + private final RedissonClient redissonClient; + + public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) { + // 1. 获得 RRateLimiter,并设置 rate 速率 + RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit); + // 2. 尝试获取 1 个 + return rateLimiter.tryAcquire(); + } + + private static String formatKey(String key) { + return String.format(RATE_LIMITER, key); + } + + private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) { + String redisKey = formatKey(key); + RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey); + long rateInterval = timeUnit.toSeconds(time); + Duration duration = Duration.ofSeconds(rateInterval); + // 1. 如果不存在,设置 rate 速率 + RateLimiterConfig config = rateLimiter.getConfig(); + if (config == null) { + rateLimiter.trySetRate(RateType.OVERALL, count, duration); + // 原因参见 https://t.zsxq.com/lcR0W + rateLimiter.expire(duration); + return rateLimiter; + } + // 2. 如果存在,并且配置相同,则直接返回 + if (config.getRateType() == RateType.OVERALL + && Objects.equals(config.getRate(), count) + && Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) { + return rateLimiter; + } + // 3. 如果存在,并且配置不同,则进行新建 + rateLimiter.setRate(RateType.OVERALL, count, duration); + // 原因参见 https://t.zsxq.com/lcR0W + rateLimiter.expire(duration); + return rateLimiter; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/package-info.java new file mode 100644 index 0000000..36a408c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/ratelimiter/package-info.java @@ -0,0 +1,4 @@ +/** + * 限流组件,基于 Redisson {@link org.redisson.api.RRateLimiter} 限流实现 + */ +package cn.iocoder.yudao.framework.ratelimiter; \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java new file mode 100644 index 0000000..7c68424 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.signature.config; + +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import cn.iocoder.yudao.framework.signature.core.aop.ApiSignatureAspect; +import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * HTTP API 签名的自动配置类 + * + * @author Zhougang + */ +@AutoConfiguration(after = YudaoRedisAutoConfiguration.class) +public class YudaoApiSignatureAutoConfiguration { + + @Bean + public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) { + return new ApiSignatureAspect(signatureRedisDAO); + } + + @Bean + public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) { + return new ApiSignatureRedisDAO(stringRedisTemplate); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java new file mode 100644 index 0000000..281bcec --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.framework.signature.core.annotation; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + + +/** + * HTTP API 签名注解 + * + * @author Zhougang + */ +@Inherited +@Documented +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiSignature { + + /** + * 同一个请求多长时间内有效 默认 60 秒 + */ + int timeout() default 60; + + /** + * 时间单位,默认为 SECONDS 秒 + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + // ========================== 签名参数 ========================== + + /** + * 提示信息,签名失败的提示 + * + * @see GlobalErrorCodeConstants#BAD_REQUEST + */ + String message() default "签名不正确"; // 为空时,使用 BAD_REQUEST 错误提示 + + /** + * 签名字段:appId 应用ID + */ + String appId() default "appId"; + + /** + * 签名字段:timestamp 时间戳 + */ + String timestamp() default "timestamp"; + + /** + * 签名字段:nonce 随机数,10 位以上 + */ + String nonce() default "nonce"; + + /** + * sign 客户端签名 + */ + String sign() default "sign"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java new file mode 100644 index 0000000..e568346 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java @@ -0,0 +1,174 @@ +package cn.iocoder.yudao.framework.signature.core.aop; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature; +import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO; +import jakarta.servlet.http.HttpServletRequest; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +import java.util.Map; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; + +/** + * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名 + * + * @author Zhougang + */ +@Aspect +@Slf4j +@AllArgsConstructor +public class ApiSignatureAspect { + + private final ApiSignatureRedisDAO signatureRedisDAO; + + @Before("@annotation(signature)") + public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) { + // 1. 验证通过,直接结束 + if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) { + return; + } + + // 2. 验证不通过,抛出异常 + log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(), + joinPoint.getArgs()); + throw new ServiceException(BAD_REQUEST.getCode(), + StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg())); + } + + public boolean verifySignature(ApiSignature signature, HttpServletRequest request) { + // 1.1 校验 Header + if (!verifyHeaders(signature, request)) { + return false; + } + // 1.2 校验 appId 是否能获取到对应的 appSecret + String appId = request.getHeader(signature.appId()); + String appSecret = signatureRedisDAO.getAppSecret(appId); + Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId); + + // 2. 校验签名【重要!】 + String clientSignature = request.getHeader(signature.sign()); // 客户端签名 + String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串 + String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名 + if (ObjUtil.notEqual(clientSignature, serverSignature)) { + return false; + } + + // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) + String nonce = request.getHeader(signature.nonce()); + if (BooleanUtil.isFalse(signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit()))) { + String timestamp = request.getHeader(signature.timestamp()); + log.info("[verifySignature][appId({}) timestamp({}) nonce({}) sign({}) 存在重复请求]", appId, timestamp, nonce, clientSignature); + throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), "存在重复请求"); + } + return true; + } + + /** + * 校验请求头加签参数 + *

+ * 1. appId 是否为空 + * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟 + * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了 + * 4. sign 是否为空 + * + * @param signature signature + * @param request request + * @return 是否校验 Header 通过 + */ + private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) { + // 1. 非空校验 + String appId = request.getHeader(signature.appId()); + if (StrUtil.isBlank(appId)) { + return false; + } + String timestamp = request.getHeader(signature.timestamp()); + if (StrUtil.isBlank(timestamp)) { + return false; + } + String nonce = request.getHeader(signature.nonce()); + if (StrUtil.length(nonce) < 10) { + return false; + } + String sign = request.getHeader(signature.sign()); + if (StrUtil.isBlank(sign)) { + return false; + } + + // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值) + long expireTime = signature.timeUnit().toMillis(signature.timeout()); + long requestTimestamp = Long.parseLong(timestamp); + long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp); + if (timestampDisparity > expireTime) { + return false; + } + + // 3. 检查 nonce 是否存在,有且仅能使用一次 + return signatureRedisDAO.getNonce(appId, nonce) == null; + } + + /** + * 构建签名字符串 + *

+ * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥 + * + * @param signature signature + * @param request request + * @param appSecret appSecret + * @return 签名字符串 + */ + private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) { + SortedMap parameterMap = getRequestParameterMap(request); // 请求头 + SortedMap headerMap = getRequestHeaderMap(signature, request); // 请求参数 + String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体 + return MapUtil.join(parameterMap, "&", "=") + + requestBody + + MapUtil.join(headerMap, "&", "=") + + appSecret; + } + + /** + * 获取请求头加签参数 Map + * + * @param request 请求 + * @param signature 签名注解 + * @return signature params + */ + private static SortedMap getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) { + SortedMap sortedMap = new TreeMap<>(); + sortedMap.put(signature.appId(), request.getHeader(signature.appId())); + sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp())); + sortedMap.put(signature.nonce(), request.getHeader(signature.nonce())); + return sortedMap; + } + + /** + * 获取请求参数 Map + * + * @param request 请求 + * @return queryParams + */ + private static SortedMap getRequestParameterMap(HttpServletRequest request) { + SortedMap sortedMap = new TreeMap<>(); + for (Map.Entry entry : request.getParameterMap().entrySet()) { + sortedMap.put(entry.getKey(), entry.getValue()[0]); + } + return sortedMap; + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java new file mode 100644 index 0000000..7f3b119 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.framework.signature.core.redis; + +import lombok.AllArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * HTTP API 签名 Redis DAO + * + * @author Zhougang + */ +@AllArgsConstructor +public class ApiSignatureRedisDAO { + + private final StringRedisTemplate stringRedisTemplate; + + /** + * 验签随机数 + *

+ * KEY 格式:signature_nonce:%s // 参数为 随机数 + * VALUE 格式:String + * 过期时间:不固定 + */ + private static final String SIGNATURE_NONCE = "api_signature_nonce:%s:%s"; + + /** + * 签名密钥 + *

+ * HASH 结构 + * KEY 格式:%s // 参数为 appid + * VALUE 格式:String + * 过期时间:永不过期(预加载到 Redis) + */ + private static final String SIGNATURE_APPID = "api_signature_app"; + + // ========== 验签随机数 ========== + + public String getNonce(String appId, String nonce) { + return stringRedisTemplate.opsForValue().get(formatNonceKey(appId, nonce)); + } + + public Boolean setNonce(String appId, String nonce, int time, TimeUnit timeUnit) { + return stringRedisTemplate.opsForValue().setIfAbsent(formatNonceKey(appId, nonce), "", time, timeUnit); + } + + private static String formatNonceKey(String appId, String nonce) { + return String.format(SIGNATURE_NONCE, appId, nonce); + } + + // ========== 签名密钥 ========== + + public String getAppSecret(String appId) { + return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java new file mode 100644 index 0000000..4ebd87a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java @@ -0,0 +1,6 @@ +/** + * HTTP API 签名,校验安全性 + * + * @see builder() + .put("v1", new String[]{"k1"}).put("k1", new String[]{"v1"}).build()); + when(request.getContentType()).thenReturn("application/json"); + when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test"))); + // mock 方法 + when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret); + when(signatureRedisDAO.setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS))).thenReturn(true); + + // 调用 + boolean result = apiSignatureAspect.verifySignature(apiSignature, request); + // 断言结果 + assertTrue(result); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/pom.xml new file mode 100644 index 0000000..8f4e580 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/pom.xml @@ -0,0 +1,41 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-redis + jar + + ${project.artifactId} + Redis 封装拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + org.redisson + redisson-spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-cache + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java new file mode 100644 index 0000000..9e84c4a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.framework.redis.config; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.cache.CacheProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.BatchStrategies; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.util.StringUtils; + +import java.util.Objects; + +import static cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration.buildRedisSerializer; + +/** + * Cache 配置类,基于 Redis 实现 + */ +@AutoConfiguration +@EnableConfigurationProperties({CacheProperties.class, YudaoCacheProperties.class}) +@EnableCaching +public class YudaoCacheAutoConfiguration { + + /** + * RedisCacheConfiguration Bean + *

+ * 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 的 createConfiguration 方法 + */ + @Bean + @Primary + public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); + // 设置使用 : 单冒号,而不是双 :: 冒号,避免 Redis Desktop Manager 多余空格 + // 详细可见 https://blog.csdn.net/chuixue24/article/details/103928965 博客 + // 再次修复单冒号,而不是双 :: 冒号问题,Issues 详情:https://gitee.com/zhijiantianya/yudao-cloud/issues/I86VY2 + config = config.computePrefixWith(cacheName -> { + String keyPrefix = cacheProperties.getRedis().getKeyPrefix(); + if (StringUtils.hasText(keyPrefix)) { + keyPrefix = keyPrefix.lastIndexOf(StrUtil.COLON) == -1 ? keyPrefix + StrUtil.COLON : keyPrefix; + return keyPrefix + cacheName + StrUtil.COLON; + } + return cacheName + StrUtil.COLON; + }); + // 设置使用 JSON 序列化方式 + config = config.serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(buildRedisSerializer())); + + // 设置 CacheProperties.Redis 的属性 + CacheProperties.Redis redisProperties = cacheProperties.getRedis(); + if (redisProperties.getTimeToLive() != null) { + config = config.entryTtl(redisProperties.getTimeToLive()); + } + if (!redisProperties.isCacheNullValues()) { + config = config.disableCachingNullValues(); + } + if (!redisProperties.isUseKeyPrefix()) { + config = config.disableKeyPrefix(); + } + return config; + } + + @Bean + public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate, + RedisCacheConfiguration redisCacheConfiguration, + YudaoCacheProperties yudaoCacheProperties) { + // 创建 RedisCacheWriter 对象 + RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, + BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize())); + // 创建 TenantRedisCacheManager 对象 + return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheProperties.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheProperties.java new file mode 100644 index 0000000..c51219d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheProperties.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.redis.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * Cache 配置项 + * + * @author Wanwan + */ +@ConfigurationProperties("yudao.cache") +@Data +@Validated +public class YudaoCacheProperties { + + /** + * {@link #redisScanBatchSize} 默认值 + */ + private static final Integer REDIS_SCAN_BATCH_SIZE_DEFAULT = 30; + + /** + * redis scan 一次返回数量 + */ + private Integer redisScanBatchSize = REDIS_SCAN_BATCH_SIZE_DEFAULT; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoRedisAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoRedisAutoConfiguration.java new file mode 100644 index 0000000..01eeb04 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoRedisAutoConfiguration.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.redis.config; + +import cn.hutool.core.util.ReflectUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.redisson.spring.starter.RedissonAutoConfigurationV2; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * Redis 配置类 + */ +@AutoConfiguration(before = RedissonAutoConfigurationV2.class) // 目的:使用自己定义的 RedisTemplate Bean +public class YudaoRedisAutoConfiguration { + + /** + * 创建 RedisTemplate Bean,使用 JSON 序列化方式 + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + // 创建 RedisTemplate 对象 + RedisTemplate template = new RedisTemplate<>(); + // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 + template.setConnectionFactory(factory); + // 使用 String 序列化方式,序列化 KEY 。 + template.setKeySerializer(RedisSerializer.string()); + template.setHashKeySerializer(RedisSerializer.string()); + // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 + template.setValueSerializer(buildRedisSerializer()); + template.setHashValueSerializer(buildRedisSerializer()); + return template; + } + + public static RedisSerializer buildRedisSerializer() { + RedisSerializer json = RedisSerializer.json(); + // 解决 LocalDateTime 的序列化 + ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper"); + objectMapper.registerModules(new JavaTimeModule()); + return json; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java new file mode 100644 index 0000000..a835835 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.framework.redis.core; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.cache.RedisCache; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; + +import java.time.Duration; + +/** + * 支持自定义过期时间的 {@link RedisCacheManager} 实现类 + * + * 在 {@link Cacheable#cacheNames()} 格式为 "key#ttl" 时,# 后面的 ttl 为过期时间。 + * 单位为最后一个字母(支持的单位有:d 天,h 小时,m 分钟,s 秒),默认单位为 s 秒 + * + * @author 芋道源码 + */ +public class TimeoutRedisCacheManager extends RedisCacheManager { + + private static final String SPLIT = "#"; + + public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { + super(cacheWriter, defaultCacheConfiguration); + } + + @Override + protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) { + if (StrUtil.isEmpty(name)) { + return super.createRedisCache(name, cacheConfig); + } + // 如果使用 # 分隔,大小不为 2,则说明不使用自定义过期时间 + String[] names = StrUtil.splitToArray(name, SPLIT); + if (names.length != 2) { + return super.createRedisCache(name, cacheConfig); + } + + // 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间 + if (cacheConfig != null) { + // 移除 # 后面的 : 以及后面的内容,避免影响解析 + String ttlStr = StrUtil.subBefore(names[1], StrUtil.COLON, false); // 获得 ttlStr 时间部分 + names[1] = StrUtil.subAfter(names[1], ttlStr, false); // 移除掉 ttlStr 时间部分 + // 解析时间 + Duration duration = parseDuration(ttlStr); + cacheConfig = cacheConfig.entryTtl(duration); + } + + // 创建 RedisCache 对象,需要忽略掉 ttlStr + return super.createRedisCache(names[0] + names[1], cacheConfig); + } + + /** + * 解析过期时间 Duration + * + * @param ttlStr 过期时间字符串 + * @return 过期时间 Duration + */ + private Duration parseDuration(String ttlStr) { + String timeUnit = StrUtil.subSuf(ttlStr, -1); + switch (timeUnit) { + case "d": + return Duration.ofDays(removeDurationSuffix(ttlStr)); + case "h": + return Duration.ofHours(removeDurationSuffix(ttlStr)); + case "m": + return Duration.ofMinutes(removeDurationSuffix(ttlStr)); + case "s": + return Duration.ofSeconds(removeDurationSuffix(ttlStr)); + default: + return Duration.ofSeconds(Long.parseLong(ttlStr)); + } + } + + /** + * 移除多余的后缀,返回具体的时间 + * + * @param ttlStr 过期时间字符串 + * @return 时间 + */ + private Long removeDurationSuffix(String ttlStr) { + return NumberUtil.parseLong(StrUtil.sub(ttlStr, 0, ttlStr.length() - 1)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/package-info.java new file mode 100644 index 0000000..bd8a5d3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/package-info.java @@ -0,0 +1,4 @@ +/** + * 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端 + */ +package cn.iocoder.yudao.framework.redis; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..02d4a68 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration +cn.iocoder.yudao.framework.redis.config.YudaoCacheAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md new file mode 100644 index 0000000..5c27083 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md new file mode 100644 index 0000000..80c5445 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/pom.xml new file mode 100644 index 0000000..a3bdfda --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/pom.xml @@ -0,0 +1,64 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-security + jar + + ${project.artifactId} + + 1. security:用户的认证、权限的校验,实现「谁」可以做「什么事」 + 2. operatelog:操作日志,实现「谁」在「什么时间」对「什么」做了「什么事」 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + cn.iocoder.boot + yudao-spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.google.guava + guava + + + + + + io.github.mouzt + bizlog-sdk + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogConfiguration.java new file mode 100644 index 0000000..eda6a14 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/config/YudaoOperateLogConfiguration.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.operatelog.config; + +import cn.iocoder.yudao.framework.operatelog.core.service.LogRecordServiceImpl; +import com.mzt.logapi.service.ILogRecordService; +import com.mzt.logapi.starter.annotation.EnableLogRecord; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +/** + * 操作日志配置类 + * + * @author HUIHUI + */ +@EnableLogRecord(tenant = "") // 貌似用不上 tenant 这玩意给个空好啦 +@AutoConfiguration +@Slf4j +public class YudaoOperateLogConfiguration { + + @Bean + @Primary + public ILogRecordService iLogRecordServiceImpl() { + return new LogRecordServiceImpl(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/package-info.java new file mode 100644 index 0000000..97ce4ea --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,无特殊作用 + */ +package cn.iocoder.yudao.framework.operatelog.core; \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java new file mode 100644 index 0000000..7000f6e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.framework.operatelog.core.service; + +import cn.iocoder.yudao.framework.common.biz.system.logger.OperateLogCommonApi; +import cn.iocoder.yudao.framework.common.biz.system.logger.dto.OperateLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import com.mzt.logapi.beans.LogRecord; +import com.mzt.logapi.service.ILogRecordService; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * 操作日志 ILogRecordService 实现类 + * + * 基于 {@link OperateLogCommonApi} 实现,记录操作日志 + * + * @author HUIHUI + */ +@Slf4j +public class LogRecordServiceImpl implements ILogRecordService { + + @Resource + private OperateLogCommonApi operateLogApi; + + @Override + public void record(LogRecord logRecord) { + OperateLogCreateReqDTO reqDTO = new OperateLogCreateReqDTO(); + try { + reqDTO.setTraceId(TracerUtils.getTraceId()); + // 补充用户信息 + fillUserFields(reqDTO); + // 补全模块信息 + fillModuleFields(reqDTO, logRecord); + // 补全请求信息 + fillRequestFields(reqDTO); + + // 2. 异步记录日志 + operateLogApi.createOperateLogAsync(reqDTO); + } catch (Throwable ex) { + // 由于 @Async 异步调用,这里打印下日志,更容易跟进 + log.error("[record][url({}) log({}) 发生异常]", reqDTO.getRequestUrl(), reqDTO, ex); + } + } + + private static void fillUserFields(OperateLogCreateReqDTO reqDTO) { + // 使用 SecurityFrameworkUtils。因为要考虑,rpc、mq、job,它其实不是 web; + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return; + } + reqDTO.setUserId(loginUser.getId()); + reqDTO.setUserType(loginUser.getUserType()); + } + + public static void fillModuleFields(OperateLogCreateReqDTO reqDTO, LogRecord logRecord) { + reqDTO.setType(logRecord.getType()); // 大模块类型,例如:CRM 客户 + reqDTO.setSubType(logRecord.getSubType());// 操作名称,例如:转移客户 + reqDTO.setBizId(Long.parseLong(logRecord.getBizNo())); // 业务编号,例如:客户编号 + reqDTO.setAction(logRecord.getAction());// 操作内容,例如:修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。 + reqDTO.setExtra(logRecord.getExtra()); // 拓展字段,有些复杂的业务,需要记录一些字段 ( JSON 格式 ),例如说,记录订单编号,{ orderId: "1"} + } + + private static void fillRequestFields(OperateLogCreateReqDTO reqDTO) { + // 获得 Request 对象 + HttpServletRequest request = ServletUtils.getRequest(); + if (request == null) { + return; + } + // 补全请求信息 + reqDTO.setRequestMethod(request.getMethod()); + reqDTO.setRequestUrl(request.getRequestURI()); + reqDTO.setUserIp(ServletUtils.getClientIP(request)); + reqDTO.setUserAgent(ServletUtils.getUserAgent(request)); + } + + @Override + public List queryLog(String bizNo, String type) { + throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询"); + } + + @Override + public List queryLogByBizNo(String bizNo, String type, String subType) { + throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询"); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/package-info.java new file mode 100644 index 0000000..c90139b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于 mzt-log 框架 + * 实现操作日志功能 + * + * @author HUIHUI + */ +package cn.iocoder.yudao.framework.operatelog; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/AuthorizeRequestsCustomizer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/AuthorizeRequestsCustomizer.java new file mode 100644 index 0000000..1b6bf68 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/AuthorizeRequestsCustomizer.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.security.config; + +import cn.iocoder.yudao.framework.web.config.WebProperties; +import jakarta.annotation.Resource; +import org.springframework.core.Ordered; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * 自定义的 URL 的安全配置 + * 目的:每个 Maven Module 可以自定义规则! + * + * @author 芋道源码 + */ +public abstract class AuthorizeRequestsCustomizer + implements Customizer.AuthorizationManagerRequestMatcherRegistry>, Ordered { + + @Resource + private WebProperties webProperties; + + protected String buildAdminApi(String url) { + return webProperties.getAdminApi().getPrefix() + url; + } + + protected String buildAppApi(String url) { + return webProperties.getAppApi().getPrefix() + url; + } + + @Override + public int getOrder() { + return 0; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java new file mode 100644 index 0000000..936e447 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.framework.security.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.Collections; +import java.util.List; + +@ConfigurationProperties(prefix = "yudao.security") +@Validated +@Data +public class SecurityProperties { + + /** + * HTTP 请求时,访问令牌的请求 Header + */ + @NotEmpty(message = "Token Header 不能为空") + private String tokenHeader = "Authorization"; + /** + * HTTP 请求时,访问令牌的请求参数 + * + * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接 + */ + @NotEmpty(message = "Token Parameter 不能为空") + private String tokenParameter = "token"; + + /** + * mock 模式的开关 + */ + @NotNull(message = "mock 模式的开关不能为空") + private Boolean mockEnable = false; + /** + * mock 模式的密钥 + * 一定要配置密钥,保证安全性 + */ + @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。 + private String mockSecret = "test"; + + /** + * 免登录的 URL 列表 + */ + private List permitAllUrls = Collections.emptyList(); + + /** + * PasswordEncoder 加密复杂度,越高开销越大 + */ + private Integer passwordEncoderLength = 4; +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java new file mode 100644 index 0000000..8874b15 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.framework.security.config; + +import cn.iocoder.yudao.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi; +import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; +import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl; +import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkServiceImpl; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.config.MethodInvokingFactoryBean; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; + +/** + * Spring Security 自动配置类,主要用于相关组件的配置 + * + * 注意,不能和 {@link YudaoWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。 + * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。 + * + * @author 芋道源码 + */ +@AutoConfiguration +@AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效 +@EnableConfigurationProperties(SecurityProperties.class) +public class YudaoSecurityAutoConfiguration { + + @Resource + private SecurityProperties securityProperties; + + /** + * 认证失败处理类 Bean + */ + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new AuthenticationEntryPointImpl(); + } + + /** + * 权限不够处理器 Bean + */ + @Bean + public AccessDeniedHandler accessDeniedHandler() { + return new AccessDeniedHandlerImpl(); + } + + /** + * Spring Security 加密器 + * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器 + * + * @see Password Encoding with Spring Security + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(securityProperties.getPasswordEncoderLength()); + } + + /** + * Token 认证过滤器 Bean + */ + @Bean + public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler, + OAuth2TokenCommonApi oauth2TokenApi) { + return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi); + } + + @Bean("ss") // 使用 Spring Security 的缩写,方便使用 + public SecurityFrameworkService securityFrameworkService(PermissionCommonApi permissionApi) { + return new SecurityFrameworkServiceImpl(permissionApi); + } + + /** + * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法, + * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略 + */ + @Bean + public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() { + MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean(); + methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class); + methodInvokingFactoryBean.setTargetMethod("setStrategyName"); + methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName()); + return methodInvokingFactoryBean; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java new file mode 100644 index 0000000..8c2d93b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java @@ -0,0 +1,220 @@ +package cn.iocoder.yudao.framework.security.config; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.servlet.DispatcherType; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.pattern.PathPattern; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 自定义的 Spring Security 配置适配器实现 + * + * @author 芋道源码 + */ +@AutoConfiguration +@AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效 +@EnableMethodSecurity(securedEnabled = true) +public class YudaoWebSecurityConfigurerAdapter { + + @Resource + private WebProperties webProperties; + @Resource + private SecurityProperties securityProperties; + + /** + * 认证失败处理类 Bean + */ + @Resource + private AuthenticationEntryPoint authenticationEntryPoint; + /** + * 权限不够处理器 Bean + */ + @Resource + private AccessDeniedHandler accessDeniedHandler; + /** + * Token 认证过滤器 Bean + */ + @Resource + private TokenAuthenticationFilter authenticationTokenFilter; + + /** + * 自定义的权限映射 Bean 们 + * + * @see #filterChain(HttpSecurity) + */ + @Resource + private List authorizeRequestsCustomizers; + + @Resource + private ApplicationContext applicationContext; + + /** + * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入 + * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题 + */ + @Bean + public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + /** + * 配置 URL 的安全配置 + * + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Bean + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + // 登出 + httpSecurity + // 开启跨域 + .cors(Customizer.withDefaults()) + // CSRF 禁用,因为不使用 Session + .csrf(AbstractHttpConfigurer::disable) + // 基于 token 机制,所以不需要 Session + .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) + // 一堆自定义的 Spring Security 处理器 + .exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler)); + // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 + + // 获得 @PermitAll 带来的 URL 列表,免登录 + Multimap permitAllUrls = getPermitAllUrlsFromAnnotations(); + // 设置每个请求的权限 + httpSecurity + // ①:全局共享规则 + .authorizeHttpRequests(c -> c + // 1.1 静态资源,可匿名访问 + .requestMatchers(HttpMethod.GET, "/*.html", "/*.css", "/*.js").permitAll() + // 1.2 设置 @PermitAll 无需认证 + .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll() + // 1.3 基于 yudao.security.permit-all-urls 无需认证 + .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() + ) + // ②:每个项目的自定义规则 + .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c))) + // ③:兜底规则,必须认证 + .authorizeHttpRequests(c -> c + .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() // WebFlux 异步请求,无需认证,目的:SSE 场景 + .anyRequest().authenticated()); + + // 添加 Token Filter + httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + return httpSecurity.build(); + } + + private String buildAppApi(String url) { + return webProperties.getAppApi().getPrefix() + url; + } + + private Multimap getPermitAllUrlsFromAnnotations() { + Multimap result = HashMultimap.create(); + // 获得接口对应的 HandlerMethod 集合 + RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) + applicationContext.getBean("requestMappingHandlerMapping"); + Map handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); + // 获得有 @PermitAll 注解的接口 + for (Map.Entry entry : handlerMethodMap.entrySet()) { + HandlerMethod handlerMethod = entry.getValue(); + if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) { + continue; + } + Set urls = new HashSet<>(); + if (entry.getKey().getPatternsCondition() != null) { + urls.addAll(entry.getKey().getPatternsCondition().getPatterns()); + } + if (entry.getKey().getPathPatternsCondition() != null) { + urls.addAll(convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString)); + } + if (urls.isEmpty()) { + continue; + } + + // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录 + Set methods = entry.getKey().getMethodsCondition().getMethods(); + if (CollUtil.isEmpty(methods)) { + result.putAll(HttpMethod.GET, urls); + result.putAll(HttpMethod.POST, urls); + result.putAll(HttpMethod.PUT, urls); + result.putAll(HttpMethod.DELETE, urls); + result.putAll(HttpMethod.HEAD, urls); + result.putAll(HttpMethod.PATCH, urls); + continue; + } + // 根据请求方法,添加到 result 结果 + entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> { + switch (requestMethod) { + case GET: + result.putAll(HttpMethod.GET, urls); + break; + case POST: + result.putAll(HttpMethod.POST, urls); + break; + case PUT: + result.putAll(HttpMethod.PUT, urls); + break; + case DELETE: + result.putAll(HttpMethod.DELETE, urls); + break; + case HEAD: + result.putAll(HttpMethod.HEAD, urls); + break; + case PATCH: + result.putAll(HttpMethod.PATCH, urls); + break; + } + }); + } + return result; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java new file mode 100644 index 0000000..a2cd9e5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.framework.security.core; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 登录用户信息 + * + * @author 芋道源码 + */ +@Data +public class LoginUser { + + public static final String INFO_KEY_NICKNAME = "nickname"; + public static final String INFO_KEY_DEPT_ID = "deptId"; + + /** + * 用户编号 + */ + private Long id; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 额外的用户信息 + */ + private Map info; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围 + */ + private List scopes; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + + // ========== 上下文 ========== + /** + * 上下文字段,不进行持久化 + * + * 1. 用于基于 LoginUser 维度的临时缓存 + */ + @JsonIgnore + private Map context; + /** + * 访问的租户编号 + */ + private Long visitTenantId; + + public void setContext(String key, Object value) { + if (context == null) { + context = new HashMap<>(); + } + context.put(key, value); + } + + public T getContext(String key, Class type) { + return MapUtil.get(context, key, type); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java new file mode 100644 index 0000000..1bdbe71 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.framework.security.core.context; + +import com.alibaba.ttl.TransmittableThreadLocal; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.util.Assert; + +/** + * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略 + * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题 + * + * @author 芋道源码 + */ +public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { + + /** + * 使用 TransmittableThreadLocal 作为上下文 + */ + private static final ThreadLocal CONTEXT_HOLDER = new TransmittableThreadLocal<>(); + + @Override + public void clearContext() { + CONTEXT_HOLDER.remove(); + } + + @Override + public SecurityContext getContext() { + SecurityContext ctx = CONTEXT_HOLDER.get(); + if (ctx == null) { + ctx = createEmptyContext(); + CONTEXT_HOLDER.set(ctx); + } + return ctx; + } + + @Override + public void setContext(SecurityContext context) { + Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); + CONTEXT_HOLDER.set(context); + } + + @Override + public SecurityContext createEmptyContext() { + return new SecurityContextImpl(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 0000000..8aaea2e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,119 @@ +package cn.iocoder.yudao.framework.security.core.filter; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; +import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.security.config.SecurityProperties; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * Token 过滤器,验证 token 的有效性 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + private final SecurityProperties securityProperties; + + private final GlobalExceptionHandler globalExceptionHandler; + + private final OAuth2TokenCommonApi oauth2TokenApi; + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + String token = SecurityFrameworkUtils.obtainAuthorization(request, + securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); + if (StrUtil.isNotEmpty(token)) { + Integer userType = WebFrameworkUtils.getLoginUserType(request); + try { + // 1.1 基于 token 构建登录用户 + LoginUser loginUser = buildLoginUserByToken(token, userType); + // 1.2 模拟 Login 功能,方便日常开发调试 + if (loginUser == null) { + loginUser = mockLoginUser(request, token, userType); + } + + // 2. 设置当前用户 + if (loginUser != null) { + SecurityFrameworkUtils.setLoginUser(loginUser, request); + } + } catch (Throwable ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } + + // 继续过滤链 + chain.doFilter(request, response); + } + + private LoginUser buildLoginUserByToken(String token, Integer userType) { + try { + OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token); + if (accessToken == null) { + return null; + } + // 用户类型不匹配,无权限 + // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型 + // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的 + if (userType != null + && ObjectUtil.notEqual(accessToken.getUserType(), userType)) { + throw new AccessDeniedException("错误的用户类型"); + } + // 构建登录用户 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setInfo(accessToken.getUserInfo()) // 额外的用户信息 + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) + .setExpiresTime(accessToken.getExpiresTime()); + } catch (ServiceException serviceException) { + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 + return null; + } + } + + /** + * 模拟登录用户,方便日常开发调试 + * + * 注意,在线上环境下,一定要关闭该功能!!! + * + * @param request 请求 + * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号 + * @param userType 用户类型 + * @return 模拟的 LoginUser + */ + private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) { + if (!securityProperties.getMockEnable()) { + return null; + } + // 必须以 mockSecret 开头 + if (!token.startsWith(securityProperties.getMockSecret())) { + return null; + } + // 构建模拟用户 + Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); + return new LoginUser().setId(userId).setUserType(userType) + .setTenantId(WebFrameworkUtils.getTenantId(request)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AccessDeniedHandlerImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 0000000..0ec5262 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.security.core.handler; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; + +/** + * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 + * + * @author 芋道源码 + */ +@Slf4j +@SuppressWarnings("JavadocReference") +public class AccessDeniedHandlerImpl implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException, ServletException { + // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 + log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), + SecurityFrameworkUtils.getLoginUserId(), e); + // 返回 403 + ServletUtils.writeJSON(response, CommonResult.error(FORBIDDEN)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AuthenticationEntryPointImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..9d9caa6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.security.core.handler; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.ExceptionTranslationFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; + +/** + * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 + * + * @author ruoyi + */ +@Slf4j +@SuppressWarnings("JavadocReference") // 忽略文档引用报错 +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); + // 返回 401 + ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java new file mode 100644 index 0000000..bf2f7f3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.framework.security.core.service; + +/** + * Security 框架 Service 接口,定义权限相关的校验操作 + * + * @author 芋道源码 + */ +public interface SecurityFrameworkService { + + /** + * 判断是否有权限 + * + * @param permission 权限 + * @return 是否 + */ + boolean hasPermission(String permission); + + /** + * 判断是否有权限,任一一个即可 + * + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(String... permissions); + + /** + * 判断是否有角色 + * + * 注意,角色使用的是 SysRoleDO 的 code 标识 + * + * @param role 角色 + * @return 是否 + */ + boolean hasRole(String role); + + /** + * 判断是否有角色,任一一个即可 + * + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(String... roles); + + /** + * 判断是否有授权 + * + * @param scope 授权 + * @return 是否 + */ + boolean hasScope(String scope); + + /** + * 判断是否有授权范围,任一一个即可 + * + * @param scope 授权范围数组 + * @return 是否 + */ + boolean hasAnyScopes(String... scope); +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java new file mode 100644 index 0000000..197118a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.framework.security.core.service; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import lombok.AllArgsConstructor; + +import java.util.Arrays; + +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck; + +/** + * 默认的 {@link SecurityFrameworkService} 实现类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { + + private final PermissionCommonApi permissionApi; + + @Override + public boolean hasPermission(String permission) { + return hasAnyPermissions(permission); + } + + @Override + public boolean hasAnyPermissions(String... permissions) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return true; + } + + // 权限校验 + Long userId = getLoginUserId(); + if (userId == null) { + return false; + } + return permissionApi.hasAnyPermissions(userId, permissions); + } + + @Override + public boolean hasRole(String role) { + return hasAnyRoles(role); + } + + @Override + public boolean hasAnyRoles(String... roles) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return true; + } + + // 权限校验 + Long userId = getLoginUserId(); + if (userId == null) { + return false; + } + return permissionApi.hasAnyRoles(userId, roles); + } + + @Override + public boolean hasScope(String scope) { + return hasAnyScopes(scope); + } + + @Override + public boolean hasAnyScopes(String... scope) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return true; + } + + // 权限校验 + LoginUser user = SecurityFrameworkUtils.getLoginUser(); + if (user == null) { + return false; + } + return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java new file mode 100644 index 0000000..4f80244 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.framework.security.core.util; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Collections; + +/** + * 安全服务工具类 + * + * @author 芋道源码 + */ +public class SecurityFrameworkUtils { + + /** + * HEADER 认证头 value 的前缀 + */ + public static final String AUTHORIZATION_BEARER = "Bearer"; + + private SecurityFrameworkUtils() {} + + /** + * 从请求中,获得认证 Token + * + * @param request 请求 + * @param headerName 认证 Token 对应的 Header 名字 + * @param parameterName 认证 Token 对应的 Parameter 名字 + * @return 认证 Token + */ + public static String obtainAuthorization(HttpServletRequest request, + String headerName, String parameterName) { + // 1. 获得 Token。优先级:Header > Parameter + String token = request.getHeader(headerName); + if (StrUtil.isEmpty(token)) { + token = request.getParameter(parameterName); + } + if (!StringUtils.hasText(token)) { + return null; + } + // 2. 去除 Token 中带的 Bearer + int index = token.indexOf(AUTHORIZATION_BEARER + " "); + return index >= 0 ? token.substring(index + 7).trim() : token; + } + + /** + * 获得当前认证信息 + * + * @return 认证信息 + */ + public static Authentication getAuthentication() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + return context.getAuthentication(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + @Nullable + public static LoginUser getLoginUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获得当前用户的编号,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 获得当前用户的昵称,从上下文中 + * + * @return 昵称 + */ + @Nullable + public static String getLoginUserNickname() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? MapUtil.getStr(loginUser.getInfo(), LoginUser.INFO_KEY_NICKNAME) : null; + } + + /** + * 获得当前用户的部门编号,从上下文中 + * + * @return 部门编号 + */ + @Nullable + public static Long getLoginUserDeptId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? MapUtil.getLong(loginUser.getInfo(), LoginUser.INFO_KEY_DEPT_ID) : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 Authentication,并设置到上下文 + Authentication authentication = buildAuthentication(loginUser, request); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; + // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 + if (request != null) { + WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); + WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); + } + } + + private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { + // 创建 UsernamePasswordAuthenticationToken 对象 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginUser, null, Collections.emptyList()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + return authenticationToken; + } + + /** + * 是否条件跳过权限校验,包括数据权限、功能权限 + * + * @return 是否跳过 + */ + public static boolean skipPermissionCheck() { + LoginUser loginUser = getLoginUser(); + if (loginUser == null) { + return false; + } + if (loginUser.getVisitTenantId() == null) { + return false; + } + // 重点:跨租户访问时,无法进行权限校验 + return ObjUtil.notEqual(loginUser.getVisitTenantId(), loginUser.getTenantId()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/package-info.java new file mode 100644 index 0000000..dc95b4e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于 Spring Security 框架 + * 实现安全认证功能 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.security; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..f5a1f81 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +cn.iocoder.yudao.framework.security.config.YudaoSecurityAutoConfiguration +cn.iocoder.yudao.framework.security.config.YudaoWebSecurityConfigurerAdapter +cn.iocoder.yudao.framework.operatelog.config.YudaoOperateLogConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md new file mode 100644 index 0000000..b8a8931 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md @@ -0,0 +1,2 @@ +* 芋道 Spring Security 入门: +* Spring Security 基本概念: diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/pom.xml new file mode 100644 index 0000000..04576fe --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/pom.xml @@ -0,0 +1,60 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-test + jar + + ${project.artifactId} + 测试组件,用于单元测试、集成测试 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + + + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + + + + org.mockito + mockito-inline + + + org.springframework.boot + spring-boot-starter-test + + + + com.h2database + h2 + + + + com.github.fppt + jedis-mock + + + + uk.co.jemos.podam + podam + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/RedisTestConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/RedisTestConfiguration.java new file mode 100644 index 0000000..4622291 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/RedisTestConfiguration.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.test.config; + +import com.github.fppt.jedismock.RedisServer; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import java.io.IOException; + +/** + * Redis 测试 Configuration,主要实现内嵌 Redis 的启动 + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@Lazy(false) // 禁止延迟加载 +@EnableConfigurationProperties(RedisProperties.class) +public class RedisTestConfiguration { + + /** + * 创建模拟的 Redis Server 服务器 + */ + @Bean + public RedisServer redisServer(RedisProperties properties) throws IOException { + RedisServer redisServer = new RedisServer(properties.getPort()); + // 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。 + try { + redisServer.start(); + } catch (Exception ignore) {} + return redisServer; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/SqlInitializationTestConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/SqlInitializationTestConfiguration.java new file mode 100644 index 0000000..abaec9d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/config/SqlInitializationTestConfiguration.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.test.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import javax.sql.DataSource; + +/** + * SQL 初始化的测试 Configuration + * + * 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢? + * 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化 + * 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈! + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class) +@ConditionalOnSingleCandidate(DataSource.class) +@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator") +@Lazy(value = false) // 禁止延迟加载 +@EnableConfigurationProperties(SqlInitializationProperties.class) +public class SqlInitializationTestConfiguration { + + @Bean + public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource, + SqlInitializationProperties initializationProperties) { + DatabaseInitializationSettings settings = createFrom(initializationProperties); + return new DataSourceScriptDatabaseInitializer(dataSource, settings); + } + + static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) { + DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); + settings.setSchemaLocations(properties.getSchemaLocations()); + settings.setDataLocations(properties.getDataLocations()); + settings.setContinueOnError(properties.isContinueOnError()); + settings.setSeparator(properties.getSeparator()); + settings.setEncoding(properties.getEncoding()); + settings.setMode(properties.getMode()); + return settings; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbAndRedisUnitTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbAndRedisUnitTest.java new file mode 100644 index 0000000..46a6927 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbAndRedisUnitTest.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.test.core.ut; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration; +import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration; +import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +/** + * 依赖内存 DB + Redis 的单元测试 + * + * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB +public class BaseDbAndRedisUnitTest { + + @Import({ + // DB 配置类 + YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类 + SqlInitializationTestConfiguration.class, // SQL 初始化 + // MyBatis 配置类 + YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + + // Redis 配置类 + RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + RedissonAutoConfiguration.class, // Redisson 自动配置类 + + // 其它配置类 + SpringUtil.class + }) + public static class Application { + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbUnitTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbUnitTest.java new file mode 100644 index 0000000..98b06f9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseDbUnitTest.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.test.core.ut; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; +import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; +import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration; +import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +/** + * 依赖内存 DB 的单元测试 + * + * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法 + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB +public class BaseDbUnitTest { + + @Import({ + // DB 配置类 + YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类 + SqlInitializationTestConfiguration.class, // SQL 初始化 + // MyBatis 配置类 + YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类 + + // 其它配置类 + SpringUtil.class + }) + public static class Application { + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseMockitoUnitTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseMockitoUnitTest.java new file mode 100644 index 0000000..2604869 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseMockitoUnitTest.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.framework.test.core.ut; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * 纯 Mockito 的单元测试 + * + * @author 芋道源码 + */ +@ExtendWith(MockitoExtension.class) +public class BaseMockitoUnitTest { +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseRedisUnitTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseRedisUnitTest.java new file mode 100644 index 0000000..ff6315a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/BaseRedisUnitTest.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.test.core.ut; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; +import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +/** + * 依赖内存 Redis 的单元测试 + * + * 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +public class BaseRedisUnitTest { + + @Import({ + // Redis 配置类 + RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动配置类 + + // 其它配置类 + SpringUtil.class + }) + public static class Application { + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/package-info.java new file mode 100644 index 0000000..bda7aad --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/ut/package-info.java @@ -0,0 +1,4 @@ +/** + * 提供单元测试 Unit Test 的基类 + */ +package cn.iocoder.yudao.framework.test.core.ut; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/AssertUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/AssertUtils.java new file mode 100644 index 0000000..e98f498 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/AssertUtils.java @@ -0,0 +1,101 @@ +package cn.iocoder.yudao.framework.test.core.util; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * 单元测试,assert 断言工具类 + * + * @author 芋道源码 + */ +public class AssertUtils { + + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + * @param ignoreFields 忽略的属性数组 + */ + public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + Arrays.stream(expectedFields).forEach(expectedField -> { + // 忽略 jacoco 自动生成的 $jacocoData 属性的情况 + if (expectedField.isSynthetic()) { + return; + } + // 如果是忽略的属性,则不进行比对 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { + return; + } + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return; + } + // 比对 + Assertions.assertEquals( + ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField), + String.format("Field(%s) 不匹配", expectedField.getName()) + ); + }); + } + + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + * @param ignoreFields 忽略的属性数组 + * @return 是否一致 + */ + public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + return Arrays.stream(expectedFields).allMatch(expectedField -> { + // 如果是忽略的属性,则不进行比对 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { + return true; + } + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return true; + } + return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField)); + }); + } + + /** + * 执行方法,校验抛出的 Service 是否符合条件 + * + * @param executable 业务异常 + * @param errorCode 错误码对象 + * @param messageParams 消息参数 + */ + public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) { + // 调用方法 + ServiceException serviceException = assertThrows(ServiceException.class, executable); + // 校验错误码 + Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); + String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams); + Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配"); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java new file mode 100644 index 0000000..1cafbcd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java @@ -0,0 +1,146 @@ +package cn.iocoder.yudao.framework.test.core.util; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import uk.co.jemos.podam.api.PodamFactory; +import uk.co.jemos.podam.api.PodamFactoryImpl; + +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 随机工具类 + * + * @author 芋道源码 + */ +public class RandomUtils { + + private static final int RANDOM_STRING_LENGTH = 10; + + private static final int TINYINT_MAX = 127; + + private static final int RANDOM_DATE_MAX = 30; + + private static final int RANDOM_COLLECTION_LENGTH = 5; + + private static final PodamFactory PODAM_FACTORY = new PodamFactoryImpl(); + + static { + // 字符串 + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class, + (dataProviderStrategy, attributeMetadata, map) -> randomString()); + // Integer + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Integer.class, (dataProviderStrategy, attributeMetadata, map) -> { + // 如果是 status 的字段,返回 0 或 1 + if ("status".equals(attributeMetadata.getAttributeName())) { + return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); + } + // 如果是 type、status 结尾的字段,返回 tinyint 范围 + if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(), + "type", "status", "category", "scope", "result")) { + return RandomUtil.randomInt(0, TINYINT_MAX + 1); + } + return RandomUtil.randomInt(); + }); + // LocalDateTime + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class, + (dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime()); + // Boolean + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> { + // 如果是 deleted 的字段,返回非删除 + if ("deleted".equals(attributeMetadata.getAttributeName())) { + return false; + } + return RandomUtil.randomBoolean(); + }); + } + + public static String randomString() { + return RandomUtil.randomString(RANDOM_STRING_LENGTH); + } + + public static Long randomLongId() { + return RandomUtil.randomLong(0, Long.MAX_VALUE); + } + + public static Integer randomInteger() { + return RandomUtil.randomInt(0, Integer.MAX_VALUE); + } + + public static Date randomDate() { + return RandomUtil.randomDay(0, RANDOM_DATE_MAX); + } + + public static LocalDateTime randomLocalDateTime() { + // 设置 Nano 为零的原因,避免 MySQL、H2 存储不到时间戳 + return LocalDateTimeUtil.of(randomDate()).withNano(0); + } + + public static Short randomShort() { + return (short) RandomUtil.randomInt(0, Short.MAX_VALUE); + } + + public static Set randomSet(Class clazz) { + return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH)) + .map(i -> randomPojo(clazz)).collect(Collectors.toSet()); + } + + public static Integer randomCommonStatus() { + return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); + } + + public static String randomEmail() { + return randomString() + "@qq.com"; + } + + public static String randomMobile() { + return "13800138" + RandomUtil.randomNumbers(3); + } + + public static String randomURL() { + return "https://www.iocoder.cn/" + randomString(); + } + + @SafeVarargs + public static T randomPojo(Class clazz, Consumer... consumers) { + T pojo = PODAM_FACTORY.manufacturePojo(clazz); + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 + if (ArrayUtil.isNotEmpty(consumers)) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); + } + return pojo; + } + + @SafeVarargs + public static T randomPojo(Class clazz, Type type, Consumer... consumers) { + T pojo = PODAM_FACTORY.manufacturePojo(clazz, type); + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 + if (ArrayUtil.isNotEmpty(consumers)) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); + } + return pojo; + } + + @SafeVarargs + public static List randomPojoList(Class clazz, Consumer... consumers) { + int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH); + return randomPojoList(clazz, size, consumers); + } + + @SafeVarargs + public static List randomPojoList(Class clazz, int size, Consumer... consumers) { + return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers)) + .collect(Collectors.toList()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/package-info.java new file mode 100644 index 0000000..3a17f51 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/package-info.java @@ -0,0 +1,4 @@ +/** + * 测试组件,用于单元测试、集成测试等等 + */ +package cn.iocoder.yudao.framework.test; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/《芋道 Spring Boot 单元测试 Test 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/《芋道 Spring Boot 单元测试 Test 入门》.md new file mode 100644 index 0000000..c6d0e9a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-test/《芋道 Spring Boot 单元测试 Test 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/pom.xml new file mode 100644 index 0000000..d04db57 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/pom.xml @@ -0,0 +1,75 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-web + jar + + ${project.artifactId} + Web 框架,全局异常、API 日志、脱敏、错误码等 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.aspectj + aspectjweaver + provided + + + + com.github.xingfudeshi + knife4j-openapi3-jakarta-spring-boot-starter + + + org.springdoc + springdoc-openapi-starter-webmvc-api + + + + org.springframework.security + spring-security-core + provided + + + + + org.jsoup + jsoup + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.mockito + mockito-inline + test + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java new file mode 100644 index 0000000..156004b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/config/YudaoApiLogAutoConfiguration.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.apilog.config; + +import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter; +import cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor; +import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiAccessLogCommonApi; +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration; +import jakarta.servlet.Filter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@AutoConfiguration(after = YudaoWebAutoConfiguration.class) +public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer { + + /** + * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 + */ + @Bean + @ConditionalOnProperty(prefix = "yudao.access-log", value = "enable", matchIfMissing = true) // 允许使用 yudao.access-log.enable=false 禁用访问日志 + public FilterRegistrationBean apiAccessLogFilter(WebProperties webProperties, + @Value("${spring.application.name}") String applicationName, + ApiAccessLogCommonApi apiAccessLogApi) { + ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogApi); + return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new ApiAccessLogInterceptor()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/annotation/ApiAccessLog.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/annotation/ApiAccessLog.java new file mode 100644 index 0000000..fe93ef6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/annotation/ApiAccessLog.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.framework.apilog.core.annotation; + +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 访问日志注解 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiAccessLog { + + // ========== 开关字段 ========== + + /** + * 是否记录访问日志 + */ + boolean enable() default true; + /** + * 是否记录请求参数 + * + * 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭 + */ + boolean requestEnable() default true; + /** + * 是否记录响应结果 + * + * 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开 + */ + boolean responseEnable() default false; + /** + * 敏感参数数组 + * + * 添加后,请求参数、响应结果不会记录该参数 + */ + String[] sanitizeKeys() default {}; + + // ========== 模块字段 ========== + + /** + * 操作模块 + * + * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.tags.Tag#name()} 属性 + */ + String operateModule() default ""; + /** + * 操作名 + * + * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.Operation#summary()} 属性 + */ + String operateName() default ""; + /** + * 操作分类 + * + * 实际并不是数组,因为枚举不能设置 null 作为默认值 + */ + OperateTypeEnum[] operateType() default {}; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/enums/OperateTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/enums/OperateTypeEnum.java new file mode 100644 index 0000000..a7f0055 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/enums/OperateTypeEnum.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.framework.apilog.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作日志的操作类型 + * + * @author ruoyi + */ +@Getter +@AllArgsConstructor +public enum OperateTypeEnum { + + /** + * 查询 + */ + GET(1), + /** + * 新增 + */ + CREATE(2), + /** + * 修改 + */ + UPDATE(3), + /** + * 删除 + */ + DELETE(4), + /** + * 导出 + */ + EXPORT(5), + /** + * 导入 + */ + IMPORT(6), + /** + * 其它 + * + * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 + */ + OTHER(0); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java new file mode 100644 index 0000000..1094ebd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/filter/ApiAccessLogFilter.java @@ -0,0 +1,252 @@ +package cn.iocoder.yudao.framework.apilog.core.filter; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiAccessLogCommonApi; +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Iterator; +import java.util.Map; + +import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * API 访问日志 Filter + * + * 目的:记录 API 访问日志到数据库中 + * + * @author 芋道源码 + */ +@Slf4j +public class ApiAccessLogFilter extends ApiRequestFilter { + + private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"}; + + private final String applicationName; + + private final ApiAccessLogCommonApi apiAccessLogApi; + + public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogCommonApi apiAccessLogApi) { + super(webProperties); + this.applicationName = applicationName; + this.apiAccessLogApi = apiAccessLogApi; + } + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 获得开始时间 + LocalDateTime beginTime = LocalDateTime.now(); + // 提前获得参数,避免 XssFilter 过滤处理 + Map queryString = ServletUtils.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; + + try { + // 继续过滤器 + filterChain.doFilter(request, response); + // 正常执行,记录日志 + createApiAccessLog(request, beginTime, queryString, requestBody, null); + } catch (Exception ex) { + // 异常执行,记录日志 + createApiAccessLog(request, beginTime, queryString, requestBody, ex); + throw ex; + } + } + + private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime, + Map queryString, String requestBody, Exception ex) { + ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO(); + try { + boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex); + if (!enable) { + return; + } + apiAccessLogApi.createApiAccessLogAsync(accessLog); + } catch (Throwable th) { + log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th); + } + } + + private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime, + Map queryString, String requestBody, Exception ex) { + // 判断:是否要记录操作日志 + HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ATTRIBUTE_HANDLER_METHOD); + ApiAccessLog accessLogAnnotation = null; + if (handlerMethod != null) { + accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class); + if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) { + return false; + } + } + + // 处理用户信息 + accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)) + .setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置访问结果 + CommonResult result = WebFrameworkUtils.getCommonResult(request); + if (result != null) { + accessLog.setResultCode(result.getCode()).setResultMsg(result.getMsg()); + } else if (ex != null) { + accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()) + .setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); + } else { + accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()).setResultMsg(""); + } + // 设置请求字段 + accessLog.setTraceId(TracerUtils.getTraceId()).setApplicationName(applicationName) + .setRequestUrl(request.getRequestURI()).setRequestMethod(request.getMethod()) + .setUserAgent(ServletUtils.getUserAgent(request)).setUserIp(ServletUtils.getClientIP(request)); + String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null; + Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE; + if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false + Map requestParams = MapUtil.builder() + .put("query", sanitizeMap(queryString, sanitizeKeys)) + .put("body", sanitizeJson(requestBody, sanitizeKeys)).build(); + accessLog.setRequestParams(toJsonString(requestParams)); + } + Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE; + if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true + accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys)); + } + // 持续时间 + accessLog.setBeginTime(beginTime).setEndTime(LocalDateTime.now()) + .setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS)); + + // 操作模块 + if (handlerMethod != null) { + Tag tagAnnotation = handlerMethod.getBeanType().getAnnotation(Tag.class); + Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class); + String operateModule = accessLogAnnotation != null && StrUtil.isNotBlank(accessLogAnnotation.operateModule()) ? + accessLogAnnotation.operateModule() : + tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null; + String operateName = accessLogAnnotation != null && StrUtil.isNotBlank(accessLogAnnotation.operateName()) ? + accessLogAnnotation.operateName() : + operationAnnotation != null ? operationAnnotation.summary() : null; + OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ? + accessLogAnnotation.operateType()[0] : parseOperateLogType(request); + accessLog.setOperateModule(operateModule).setOperateName(operateName).setOperateType(operateType.getType()); + } + return true; + } + + // ========== 解析 @ApiAccessLog、@Swagger 注解 ========== + + private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) { + RequestMethod requestMethod = RequestMethod.resolve(request.getMethod()); + if (requestMethod == null) { + return OperateTypeEnum.OTHER; + } + switch (requestMethod) { + case GET: + return OperateTypeEnum.GET; + case POST: + return OperateTypeEnum.CREATE; + case PUT: + return OperateTypeEnum.UPDATE; + case DELETE: + return OperateTypeEnum.DELETE; + default: + return OperateTypeEnum.OTHER; + } + } + + // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ========== + + private static String sanitizeMap(Map map, String[] sanitizeKeys) { + if (CollUtil.isEmpty(map)) { + return null; + } + if (sanitizeKeys != null) { + MapUtil.removeAny(map, sanitizeKeys); + } + MapUtil.removeAny(map, SANITIZE_KEYS); + return JsonUtils.toJsonString(map); + } + + private static String sanitizeJson(String jsonString, String[] sanitizeKeys) { + if (StrUtil.isEmpty(jsonString)) { + return null; + } + try { + JsonNode rootNode = JsonUtils.parseTree(jsonString); + sanitizeJson(rootNode, sanitizeKeys); + return JsonUtils.toJsonString(rootNode); + } catch (Exception e) { + // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 + log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); + return jsonString; + } + } + + private static String sanitizeJson(CommonResult commonResult, String[] sanitizeKeys) { + if (commonResult == null) { + return null; + } + String jsonString = toJsonString(commonResult); + try { + JsonNode rootNode = JsonUtils.parseTree(jsonString); + sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉 + return JsonUtils.toJsonString(rootNode); + } catch (Exception e) { + // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 + log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); + return jsonString; + } + } + + private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) { + // 情况一:数组,遍历处理 + if (node.isArray()) { + for (JsonNode childNode : node) { + sanitizeJson(childNode, sanitizeKeys); + } + return; + } + // 情况二:非 Object,只是某个值,直接返回 + if (!node.isObject()) { + return; + } + // 情况三:Object,遍历处理 + Iterator> iterator = node.properties().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (ArrayUtil.contains(sanitizeKeys, entry.getKey()) + || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) { + iterator.remove(); + continue; + } + sanitizeJson(entry.getValue(), sanitizeKeys); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java new file mode 100644 index 0000000..7d6b456 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.framework.apilog.core.interceptor; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StopWatch; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.IntStream; + +/** + * API 访问日志 Interceptor + * + * 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。 + * + * @author 芋道源码 + */ +@Slf4j +public class ApiAccessLogInterceptor implements HandlerInterceptor { + + public static final String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD"; + + private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用 + HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null; + if (handlerMethod != null) { + request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod); + } + + // 打印 request 日志 + if (!SpringUtils.isProd()) { + Map queryString = ServletUtils.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; + if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) { + log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI()); + } else { + log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(), + StrUtil.blankToDefault(requestBody, queryString.toString())); + } + // 计时 + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch); + // 打印 Controller 路径 + printHandlerMethodPosition(handlerMethod); + } + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + // 打印 response 日志 + if (!SpringUtils.isProd()) { + StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH); + stopWatch.stop(); + log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]", + request.getRequestURI(), stopWatch.getTotalTimeMillis()); + } + } + + /** + * 打印 Controller 方法路径 + */ + private void printHandlerMethodPosition(HandlerMethod handlerMethod) { + if (handlerMethod == null) { + return; + } + Method method = handlerMethod.getMethod(); + Class clazz = method.getDeclaringClass(); + try { + // 获取 method 的 lineNumber + List clazzContents = FileUtil.readUtf8Lines( + ResourceUtil.getResource(null, clazz).getPath().replace("/target/classes/", "/src/main/java/") + + clazz.getSimpleName() + ".java"); + Optional lineNumber = IntStream.range(0, clazzContents.size()) + .filter(i -> clazzContents.get(i).contains(" " + method.getName() + "(")) // 简单匹配,不考虑方法重名 + .mapToObj(i -> i + 1) // 行号从 1 开始 + .findFirst(); + if (!lineNumber.isPresent()) { + return; + } + // 打印结果 + System.out.printf("\tController 方法路径:%s(%s.java:%d)\n", clazz.getName(), clazz.getSimpleName(), lineNumber.get()); + } catch (Exception ignore) { + // 忽略异常。原因:仅仅打印,非重要逻辑 + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/package-info.java new file mode 100644 index 0000000..cb93f3d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/package-info.java @@ -0,0 +1,8 @@ +/** + * API 日志:包含两类 + * 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。 + * 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.apilog; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/config/YudaoBannerAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/config/YudaoBannerAutoConfiguration.java new file mode 100644 index 0000000..a8b6678 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/config/YudaoBannerAutoConfiguration.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.banner.config; + +import cn.iocoder.yudao.framework.banner.core.BannerApplicationRunner; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * Banner 的自动配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class YudaoBannerAutoConfiguration { + + @Bean + public BannerApplicationRunner bannerApplicationRunner() { + return new BannerApplicationRunner(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java new file mode 100644 index 0000000..d07c4aa --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.framework.banner.core; + +import cn.hutool.core.thread.ThreadUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.util.ClassUtils; + +import java.util.concurrent.TimeUnit; + +/** + * 项目启动成功后,提供文档相关的地址 + * + * @author 芋道源码 + */ +@Slf4j +public class BannerApplicationRunner implements ApplicationRunner { + + @Override + public void run(ApplicationArguments args) { + ThreadUtil.execute(() -> { + ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒,保证输出到结尾 + log.info("\n----------------------------------------------------------\n\t" + + "项目启动成功!\n\t" + + "接口文档: \t{} \n\t" + + "开发文档: \t{} \n\t" + + "视频教程: \t{} \n" + + "----------------------------------------------------------", + "https://doc.iocoder.cn/api-doc/", + "https://doc.iocoder.cn", + "https://t.zsxq.com/02Yf6M7Qn"); + + // 数据报表 + if (isNotPresent("cn.iocoder.yudao.module.report.framework.security.config.SecurityConfiguration")) { + System.out.println("[报表模块 yudao-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]"); + } + // 工作流 + if (isNotPresent("cn.iocoder.yudao.module.bpm.framework.flowable.config.BpmFlowableConfiguration")) { + System.out.println("[工作流模块 yudao-module-bpm - 已禁用][参考 https://doc.iocoder.cn/bpm/ 开启]"); + } + // 商城系统 + if (isNotPresent("cn.iocoder.yudao.module.trade.framework.web.config.TradeWebConfiguration")) { + System.out.println("[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + } + // ERP 系统 + if (isNotPresent("cn.iocoder.yudao.module.erp.framework.web.config.ErpWebConfiguration")) { + System.out.println("[ERP 系统 yudao-module-erp - 已禁用][参考 https://doc.iocoder.cn/erp/build/ 开启]"); + } + // CRM 系统 + if (isNotPresent("cn.iocoder.yudao.module.crm.framework.web.config.CrmWebConfiguration")) { + System.out.println("[CRM 系统 yudao-module-crm - 已禁用][参考 https://doc.iocoder.cn/crm/build/ 开启]"); + } + // 微信公众号 + if (isNotPresent("cn.iocoder.yudao.module.mp.framework.mp.config.MpConfiguration")) { + System.out.println("[微信公众号 yudao-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + } + // 支付平台 + if (isNotPresent("cn.iocoder.yudao.module.pay.framework.pay.config.PayConfiguration")) { + System.out.println("[支付系统 yudao-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + } + // AI 大模型 + if (isNotPresent("cn.iocoder.yudao.module.ai.framework.web.config.AiWebConfiguration")) { + System.out.println("[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]"); + } + // IoT 物联网 + if (isNotPresent("cn.iocoder.yudao.module.iot.framework.web.config.IotWebConfiguration")) { + System.out.println("[IoT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + } + }); + } + + private static boolean isNotPresent(String className) { + return !ClassUtils.isPresent(className, ClassUtils.getDefaultClassLoader()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/package-info.java new file mode 100644 index 0000000..aba7268 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/package-info.java @@ -0,0 +1,6 @@ +/** + * Banner 用于在 console 控制台,打印开发文档、接口文档等 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.banner; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/annotation/DesensitizeBy.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/annotation/DesensitizeBy.java new file mode 100644 index 0000000..0166cb6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/annotation/DesensitizeBy.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.desensitize.core.base.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler; +import cn.iocoder.yudao.framework.desensitize.core.base.serializer.StringDesensitizeSerializer; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 顶级脱敏注解,自定义注解需要使用此注解 + * + * @author gaibu + */ +@Documented +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside // 此注解是其他所有 jackson 注解的元注解,打上了此注解的注解表明是 jackson 注解的一部分 +@JsonSerialize(using = StringDesensitizeSerializer.class) // 指定序列化器 +public @interface DesensitizeBy { + + /** + * 脱敏处理器 + */ + @SuppressWarnings("rawtypes") + Class handler(); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/handler/DesensitizationHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/handler/DesensitizationHandler.java new file mode 100644 index 0000000..b15e356 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/handler/DesensitizationHandler.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.desensitize.core.base.handler; + +import cn.hutool.core.util.ReflectUtil; + +import java.lang.annotation.Annotation; + +/** + * 脱敏处理器接口 + * + * @author gaibu + */ +public interface DesensitizationHandler { + + /** + * 脱敏 + * + * @param origin 原始字符串 + * @param annotation 注解信息 + * @return 脱敏后的字符串 + */ + String desensitize(String origin, T annotation); + + /** + * 是否禁用脱敏的 Spring EL 表达式 + * + * 如果返回 true 则跳过脱敏 + * + * @param annotation 注解信息 + * @return 是否禁用脱敏的 Spring EL 表达式 + */ + default String getDisable(T annotation) { + // 约定:默认就是 enable() 属性。如果不符合,子类重写 + try { + return (String) ReflectUtil.invoke(annotation, "disable"); + } catch (Exception ex) { + return ""; + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java new file mode 100644 index 0000000..566b0d1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.framework.desensitize.core.base.serializer; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.lang.Singleton; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import lombok.Getter; +import lombok.Setter; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +/** + * 脱敏序列化器 + * + * 实现 JSON 返回数据时,使用 {@link DesensitizationHandler} 对声明脱敏注解的字段,进行脱敏处理。 + * + * @author gaibu + */ +@SuppressWarnings("rawtypes") +public class StringDesensitizeSerializer extends StdSerializer implements ContextualSerializer { + + @Getter + @Setter + private DesensitizationHandler desensitizationHandler; + + protected StringDesensitizeSerializer() { + super(String.class); + } + + @Override + public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) { + DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class); + if (annotation == null) { + return this; + } + // 创建一个 StringDesensitizeSerializer 对象,使用 DesensitizeBy 对应的处理器 + StringDesensitizeSerializer serializer = new StringDesensitizeSerializer(); + serializer.setDesensitizationHandler(Singleton.get(annotation.handler())); + return serializer; + } + + @Override + @SuppressWarnings("unchecked") + public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException { + if (StrUtil.isBlank(value)) { + gen.writeNull(); + return; + } + // 获取序列化字段 + Field field = getField(gen); + + // 自定义处理器 + DesensitizeBy[] annotations = AnnotationUtil.getCombinationAnnotations(field, DesensitizeBy.class); + if (ArrayUtil.isEmpty(annotations)) { + gen.writeString(value); + return; + } + for (Annotation annotation : field.getAnnotations()) { + if (AnnotationUtil.hasAnnotation(annotation.annotationType(), DesensitizeBy.class)) { + value = this.desensitizationHandler.desensitize(value, annotation); + gen.writeString(value); + return; + } + } + gen.writeString(value); + } + + /** + * 获取字段 + * + * @param generator JsonGenerator + * @return 字段 + */ + private Field getField(JsonGenerator generator) { + String currentName = generator.getOutputContext().getCurrentName(); + Object currentValue = generator.currentValue(); + Class currentValueClass = currentValue.getClass(); + return ReflectUtil.getField(currentValueClass, currentName); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/EmailDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/EmailDesensitize.java new file mode 100644 index 0000000..cb838de --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/EmailDesensitize.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.desensitize.core.regex.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.regex.handler.EmailDesensitizationHandler; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 邮箱脱敏注解 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = EmailDesensitizationHandler.class) +public @interface EmailDesensitize { + + /** + * 匹配的正则表达式 + */ + String regex() default "(^.)[^@]*(@.*$)"; + + /** + * 替换规则,邮箱; + * + * 比如:example@gmail.com 脱敏之后为 e****@gmail.com + */ + String replacer() default "$1****$2"; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/RegexDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/RegexDesensitize.java new file mode 100644 index 0000000..49d002a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/annotation/RegexDesensitize.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.desensitize.core.regex.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.regex.handler.DefaultRegexDesensitizationHandler; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 正则脱敏注解 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = DefaultRegexDesensitizationHandler.class) +public @interface RegexDesensitize { + + /** + * 匹配的正则表达式(默认匹配所有) + */ + String regex() default "^[\\s\\S]*$"; + + /** + * 替换规则,会将匹配到的字符串全部替换成 replacer + * + * 例如:regex=123; replacer=****** + * 原始字符串 123456789 + * 脱敏后字符串 ******456789 + */ + String replacer() default "******"; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java new file mode 100644 index 0000000..6ae1a50 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.desensitize.core.regex.handler; + +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; +import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler; + +import java.lang.annotation.Annotation; + +/** + * 正则表达式脱敏处理器抽象类,已实现通用的方法 + * + * @author gaibu + */ +public abstract class AbstractRegexDesensitizationHandler + implements DesensitizationHandler { + + @Override + public String desensitize(String origin, T annotation) { + // 1. 判断是否禁用脱敏 + Object disable = SpringExpressionUtils.parseExpression(getDisable(annotation)); + if (Boolean.TRUE.equals(disable)) { + return origin; + } + + // 2. 执行脱敏 + String regex = getRegex(annotation); + String replacer = getReplacer(annotation); + return origin.replaceAll(regex, replacer); + } + + /** + * 获取注解上的 regex 参数 + * + * @param annotation 注解信息 + * @return 正则表达式 + */ + abstract String getRegex(T annotation); + + /** + * 获取注解上的 replacer 参数 + * + * @param annotation 注解信息 + * @return 待替换的字符串 + */ + abstract String getReplacer(T annotation); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java new file mode 100644 index 0000000..debbe63 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.desensitize.core.regex.handler; + +import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.RegexDesensitize; + +/** + * {@link RegexDesensitize} 的正则脱敏处理器 + * + * @author gaibu + */ +public class DefaultRegexDesensitizationHandler extends AbstractRegexDesensitizationHandler { + + @Override + String getRegex(RegexDesensitize annotation) { + return annotation.regex(); + } + + @Override + String getReplacer(RegexDesensitize annotation) { + return annotation.replacer(); + } + + @Override + public String getDisable(RegexDesensitize annotation) { + return annotation.disable(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java new file mode 100644 index 0000000..8d1867a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.desensitize.core.regex.handler; + +import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.EmailDesensitize; + +/** + * {@link EmailDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class EmailDesensitizationHandler extends AbstractRegexDesensitizationHandler { + + @Override + String getRegex(EmailDesensitize annotation) { + return annotation.regex(); + } + + @Override + String getReplacer(EmailDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/BankCardDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/BankCardDesensitize.java new file mode 100644 index 0000000..7f08a43 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/BankCardDesensitize.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.slider.handler.BankCardDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 银行卡号 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = BankCardDesensitization.class) +public @interface BankCardDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 6; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 2; + + /** + * 替换规则,银行卡号; 比如:9988002866797031 脱敏之后为 998800********31 + */ + String replacer() default "*"; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java new file mode 100644 index 0000000..91dae86 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.slider.handler.CarLicenseDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 车牌号 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = CarLicenseDesensitization.class) +public @interface CarLicenseDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 3; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 1; + + /** + * 替换规则,车牌号;比如:粤A66666 脱敏之后为粤A6***6 + */ + String replacer() default "*"; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java new file mode 100644 index 0000000..866ec22 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.slider.handler.ChineseNameDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 中文名 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = ChineseNameDesensitization.class) +public @interface ChineseNameDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 1; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 0; + + /** + * 替换规则,中文名;比如:刘子豪脱敏之后为刘** + */ + String replacer() default "*"; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java new file mode 100644 index 0000000..23273b8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.slider.handler.FixedPhoneDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 固定电话 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = FixedPhoneDesensitization.class) +public @interface FixedPhoneDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 4; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 2; + + /** + * 替换规则,固定电话;比如:01086551122 脱敏之后为 0108*****22 + */ + String replacer() default "*"; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/IdCardDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/IdCardDesensitize.java new file mode 100644 index 0000000..6d97e10 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/IdCardDesensitize.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.slider.handler.IdCardDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 身份证 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = IdCardDesensitization.class) +public @interface IdCardDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 6; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 2; + + /** + * 替换规则,身份证号码;比如:530321199204074611 脱敏之后为 530321**********11 + */ + String replacer() default "*"; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/MobileDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/MobileDesensitize.java new file mode 100644 index 0000000..c0f5211 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/MobileDesensitize.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.slider.handler.MobileDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 手机号 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = MobileDesensitization.class) +public @interface MobileDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 3; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 4; + + /** + * 替换规则,手机号;比如:13248765917 脱敏之后为 132****5917 + */ + String replacer() default "*"; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/PasswordDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/PasswordDesensitize.java new file mode 100644 index 0000000..e37c473 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/PasswordDesensitize.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.slider.handler.PasswordDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 密码 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = PasswordDesensitization.class) +public @interface PasswordDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 0; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 0; + + /** + * 替换规则,密码; + * + * 比如:123456 脱敏之后为 ****** + */ + String replacer() default "*"; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/SliderDesensitize.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/SliderDesensitize.java new file mode 100644 index 0000000..ee5d867 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/annotation/SliderDesensitize.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import cn.iocoder.yudao.framework.desensitize.core.slider.handler.DefaultDesensitizationHandler; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 滑动脱敏注解 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = DefaultDesensitizationHandler.class) +public @interface SliderDesensitize { + + /** + * 后缀保留长度 + */ + int suffixKeep() default 0; + + /** + * 替换规则,会将前缀后缀保留后,全部替换成 replacer + * + * 例如:prefixKeep = 1; suffixKeep = 2; replacer = "*"; + * 原始字符串 123456 + * 脱敏后 1***56 + */ + String replacer() default "*"; + + /** + * 前缀保留长度 + */ + int prefixKeep() default 0; + + /** + * 是否禁用脱敏 + * + * 支持 Spring EL 表达式,如果返回 true 则跳过脱敏 + */ + String disable() default ""; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java new file mode 100644 index 0000000..7b56329 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.handler; + +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; +import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler; + +import java.lang.annotation.Annotation; + +/** + * 滑动脱敏处理器抽象类,已实现通用的方法 + * + * @author gaibu + */ +public abstract class AbstractSliderDesensitizationHandler + implements DesensitizationHandler { + + @Override + public String desensitize(String origin, T annotation) { + // 1. 判断是否禁用脱敏 + Object disable = SpringExpressionUtils.parseExpression(getDisable(annotation)); + if (Boolean.TRUE.equals(disable)) { + return origin; + } + + // 2. 执行脱敏 + int prefixKeep = getPrefixKeep(annotation); + int suffixKeep = getSuffixKeep(annotation); + String replacer = getReplacer(annotation); + int length = origin.length(); + int interval = length - prefixKeep - suffixKeep; + + // 情况一:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换 + if (interval <= 0) { + return buildReplacerByLength(replacer, length); + } + + // 情况二:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串 + return origin.substring(0, prefixKeep) + + buildReplacerByLength(replacer, interval) + + origin.substring(prefixKeep + interval); + } + + /** + * 根据长度循环构建替换符 + * + * @param replacer 替换符 + * @param length 长度 + * @return 构建后的替换符 + */ + private String buildReplacerByLength(String replacer, int length) { + return replacer.repeat(length); + } + + /** + * 前缀保留长度 + * + * @param annotation 注解信息 + * @return 前缀保留长度 + */ + abstract Integer getPrefixKeep(T annotation); + + /** + * 后缀保留长度 + * + * @param annotation 注解信息 + * @return 后缀保留长度 + */ + abstract Integer getSuffixKeep(T annotation); + + /** + * 替换符 + * + * @param annotation 注解信息 + * @return 替换符 + */ + abstract String getReplacer(T annotation); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/BankCardDesensitization.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/BankCardDesensitization.java new file mode 100644 index 0000000..79797e5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/BankCardDesensitization.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.handler; + +import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.BankCardDesensitize; + +/** + * {@link BankCardDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class BankCardDesensitization extends AbstractSliderDesensitizationHandler { + + @Override + Integer getPrefixKeep(BankCardDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(BankCardDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(BankCardDesensitize annotation) { + return annotation.replacer(); + } + + @Override + public String getDisable(BankCardDesensitize annotation) { + return ""; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java new file mode 100644 index 0000000..1029ee2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.handler; + +import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.CarLicenseDesensitize; + +/** + * {@link CarLicenseDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class CarLicenseDesensitization extends AbstractSliderDesensitizationHandler { + + @Override + Integer getPrefixKeep(CarLicenseDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(CarLicenseDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(CarLicenseDesensitize annotation) { + return annotation.replacer(); + } + + @Override + public String getDisable(CarLicenseDesensitize annotation) { + return annotation.disable(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java new file mode 100644 index 0000000..f71dac0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.handler; + +import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.ChineseNameDesensitize; + +/** + * {@link ChineseNameDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class ChineseNameDesensitization extends AbstractSliderDesensitizationHandler { + + @Override + Integer getPrefixKeep(ChineseNameDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(ChineseNameDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(ChineseNameDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java new file mode 100644 index 0000000..bdb282d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.handler; + +import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.SliderDesensitize; + +/** + * {@link SliderDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class DefaultDesensitizationHandler extends AbstractSliderDesensitizationHandler { + + @Override + Integer getPrefixKeep(SliderDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(SliderDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(SliderDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java new file mode 100644 index 0000000..53412e4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.handler; + +import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize; + +/** + * {@link FixedPhoneDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class FixedPhoneDesensitization extends AbstractSliderDesensitizationHandler { + + @Override + Integer getPrefixKeep(FixedPhoneDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(FixedPhoneDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(FixedPhoneDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/IdCardDesensitization.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/IdCardDesensitization.java new file mode 100644 index 0000000..4bb8915 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/IdCardDesensitization.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.handler; + +import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.IdCardDesensitize; + +/** + * {@link IdCardDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class IdCardDesensitization extends AbstractSliderDesensitizationHandler { + @Override + Integer getPrefixKeep(IdCardDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(IdCardDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(IdCardDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/MobileDesensitization.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/MobileDesensitization.java new file mode 100644 index 0000000..5796d13 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/MobileDesensitization.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.handler; + +import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.MobileDesensitize; + +/** + * {@link MobileDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class MobileDesensitization extends AbstractSliderDesensitizationHandler { + + @Override + Integer getPrefixKeep(MobileDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(MobileDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(MobileDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/PasswordDesensitization.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/PasswordDesensitization.java new file mode 100644 index 0000000..8c6d120 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/core/slider/handler/PasswordDesensitization.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.desensitize.core.slider.handler; + +import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.PasswordDesensitize; + +/** + * {@link PasswordDesensitize} 的码脱敏处理器 + * + * @author gaibu + */ +public class PasswordDesensitization extends AbstractSliderDesensitizationHandler { + @Override + Integer getPrefixKeep(PasswordDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(PasswordDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(PasswordDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/package-info.java new file mode 100644 index 0000000..bfe0187 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/desensitize/package-info.java @@ -0,0 +1,4 @@ +/** + * 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏 + */ +package cn.iocoder.yudao.framework.desensitize; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/config/YudaoJacksonAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/config/YudaoJacksonAutoConfiguration.java new file mode 100644 index 0000000..c62f0a0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/config/YudaoJacksonAutoConfiguration.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.jackson.config; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.json.databind.NumberSerializer; +import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer; +import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +@AutoConfiguration +@Slf4j +public class YudaoJacksonAutoConfiguration { + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public JsonUtils jsonUtils(List objectMappers) { + // 1.1 创建 SimpleModule 对象 + SimpleModule simpleModule = new SimpleModule(); + simpleModule + // 新增 Long 类型序列化规则,数值超过 2^53-1,在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型 + .addSerializer(Long.class, NumberSerializer.INSTANCE) + .addSerializer(Long.TYPE, NumberSerializer.INSTANCE) + .addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE) + .addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE) + .addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE) + .addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE) + // 新增 LocalDateTime 序列化、反序列化规则,使用 Long 时间戳 + .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE) + .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); + // 1.2 注册到 objectMapper + objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule)); + + // 2. 设置 objectMapper 到 JsonUtils + JsonUtils.init(CollUtil.getFirst(objectMappers)); + log.info("[init][初始化 JsonUtils 成功]"); + return new JsonUtils(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/package-info.java new file mode 100644 index 0000000..261b508 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/jackson/core/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework.jackson.core; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/package-info.java new file mode 100644 index 0000000..2dc5316 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/package-info.java @@ -0,0 +1,4 @@ +/** + * Web 框架,全局异常、API 日志等 + */ +package cn.iocoder.yudao.framework; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/SwaggerProperties.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/SwaggerProperties.java new file mode 100644 index 0000000..5374d3a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/SwaggerProperties.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.framework.swagger.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import jakarta.validation.constraints.NotEmpty; + +/** + * Swagger 配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties("yudao.swagger") +@Data +public class SwaggerProperties { + + /** + * 标题 + */ + @NotEmpty(message = "标题不能为空") + private String title; + /** + * 描述 + */ + @NotEmpty(message = "描述不能为空") + private String description; + /** + * 作者 + */ + @NotEmpty(message = "作者不能为空") + private String author; + /** + * 版本 + */ + @NotEmpty(message = "版本不能为空") + private String version; + /** + * url + */ + @NotEmpty(message = "扫描的 package 不能为空") + private String url; + /** + * email + */ + @NotEmpty(message = "扫描的 email 不能为空") + private String email; + + /** + * license + */ + @NotEmpty(message = "扫描的 license 不能为空") + private String license; + + /** + * license-url + */ + @NotEmpty(message = "扫描的 license-url 不能为空") + private String licenseUrl; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java new file mode 100644 index 0000000..295e323 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.framework.swagger.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.media.IntegerSchema; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.customizers.OpenApiBuilderCustomizer; +import org.springdoc.core.customizers.ServerBaseUrlCustomizer; +import org.springdoc.core.models.GroupedOpenApi; +import org.springdoc.core.properties.SpringDocConfigProperties; +import org.springdoc.core.providers.JavadocProvider; +import org.springdoc.core.service.OpenAPIService; +import org.springdoc.core.service.SecurityService; +import org.springdoc.core.utils.PropertyResolverUtils; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpHeaders; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * Swagger 自动配置类,基于 OpenAPI + Springdoc 实现。 + * + * 友情提示: + * 1. Springdoc 文档地址:仓库 + * 2. Swagger 规范,于 2015 更名为 OpenAPI 规范,本质是一个东西 + * + * @author 芋道源码 + */ +@AutoConfiguration +@ConditionalOnClass({OpenAPI.class}) +@EnableConfigurationProperties(SwaggerProperties.class) +@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 +public class YudaoSwaggerAutoConfiguration { + + // ========== 全局 OpenAPI 配置 ========== + + @Bean + public OpenAPI createApi(SwaggerProperties properties) { + Map securitySchemas = buildSecuritySchemes(); + OpenAPI openAPI = new OpenAPI() + // 接口信息 + .info(buildInfo(properties)) + // 接口安全配置 + .components(new Components().securitySchemes(securitySchemas)) + .addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)); + securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key))); + return openAPI; + } + + /** + * API 摘要信息 + */ + private Info buildInfo(SwaggerProperties properties) { + return new Info() + .title(properties.getTitle()) + .description(properties.getDescription()) + .version(properties.getVersion()) + .contact(new Contact().name(properties.getAuthor()).url(properties.getUrl()).email(properties.getEmail())) + .license(new License().name(properties.getLicense()).url(properties.getLicenseUrl())); + } + + /** + * 安全模式,这里配置通过请求头 Authorization 传递 token 参数 + */ + private Map buildSecuritySchemes() { + Map securitySchemes = new HashMap<>(); + SecurityScheme securityScheme = new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) // 类型 + .name(HttpHeaders.AUTHORIZATION) // 请求头的 name + .in(SecurityScheme.In.HEADER); // token 所在位置 + securitySchemes.put(HttpHeaders.AUTHORIZATION, securityScheme); + return securitySchemes; + } + + /** + * 自定义 OpenAPI 处理器 + */ + @Bean + @Primary // 目的:以我们创建的 OpenAPIService Bean 为主,避免一键改包后,启动报错! + public OpenAPIService openApiBuilder(Optional openAPI, + SecurityService securityParser, + SpringDocConfigProperties springDocConfigProperties, + PropertyResolverUtils propertyResolverUtils, + Optional> openApiBuilderCustomizers, + Optional> serverBaseUrlCustomizers, + Optional javadocProvider) { + return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, + propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider); + } + + // ========== 分组 OpenAPI 配置 ========== + + /** + * 所有模块的 API 分组 + */ + @Bean + public GroupedOpenApi allGroupedOpenApi() { + return buildGroupedOpenApi("all", ""); + } + + public static GroupedOpenApi buildGroupedOpenApi(String group) { + return buildGroupedOpenApi(group, group); + } + + public static GroupedOpenApi buildGroupedOpenApi(String group, String path) { + return GroupedOpenApi.builder() + .group(group) + .pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**") + .addOperationCustomizer((operation, handlerMethod) -> operation + .addParametersItem(buildTenantHeaderParameter()) + .addParametersItem(buildSecurityHeaderParameter())) + .build(); + } + + /** + * 构建 Tenant 租户编号请求头参数 + * + * @return 多租户参数 + */ + private static Parameter buildTenantHeaderParameter() { + return new Parameter() + .name(HEADER_TENANT_ID) // header 名 + .description("租户编号") // 描述 + .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header + .schema(new IntegerSchema()._default(1L).name(HEADER_TENANT_ID).description("租户编号")); // 默认:使用租户编号为 1 + } + + /** + * 构建 Authorization 认证请求头参数 + * + * 解决 Knife4j Authorize 未生效,请求header里未包含参数 + * + * @return 认证参数 + */ + private static Parameter buildSecurityHeaderParameter() { + return new Parameter() + .name(HttpHeaders.AUTHORIZATION) // header 名 + .description("认证 Token") // 描述 + .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header + .schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1 + } + +} + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/package-info.java new file mode 100644 index 0000000..5eb7822 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于 Swagger + Knife4j 实现 API 接口文档 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.swagger; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/WebProperties.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/WebProperties.java new file mode 100644 index 0000000..8091e9a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/WebProperties.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.framework.web.config; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +@ConfigurationProperties(prefix = "yudao.web") +@Validated +@Data +public class WebProperties { + + @NotNull(message = "APP API 不能为空") + private Api appApi = new Api("/app-api", "**.controller.app.**"); + @NotNull(message = "Admin API 不能为空") + private Api adminApi = new Api("/admin-api", "**.controller.admin.**"); + + @NotNull(message = "Admin UI 不能为空") + private Ui adminUi; + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Valid + public static class Api { + + /** + * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀 + * + * + * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题 + * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。 + * + * @see YudaoWebAutoConfiguration#configurePathMatch(PathMatchConfigurer) + */ + @NotEmpty(message = "API 前缀不能为空") + private String prefix; + + /** + * Controller 所在包的 Ant 路径规则 + * + * 主要目的是,给该 Controller 设置指定的 {@link #prefix} + */ + @NotEmpty(message = "Controller 所在包不能为空") + private String controller; + + } + + @Data + @Valid + public static class Ui { + + /** + * 访问地址 + */ + private String url; + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java new file mode 100644 index 0000000..9126c7e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java @@ -0,0 +1,131 @@ +package cn.iocoder.yudao.framework.web.config; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter; +import cn.iocoder.yudao.framework.web.core.filter.DemoFilter; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import jakarta.annotation.Resource; +import jakarta.servlet.Filter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@AutoConfiguration +@EnableConfigurationProperties(WebProperties.class) +public class YudaoWebAutoConfiguration implements WebMvcConfigurer { + + @Resource + private WebProperties webProperties; + /** + * 应用名 + */ + @Value("${spring.application.name}") + private String applicationName; + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurePathMatch(configurer, webProperties.getAdminApi()); + configurePathMatch(configurer, webProperties.getAppApi()); + } + + /** + * 设置 API 前缀,仅仅匹配 controller 包下的 + * + * @param configurer 配置 + * @param api API 配置 + */ + private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) { + AntPathMatcher antPathMatcher = new AntPathMatcher("."); + configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class) + && antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包 + } + + @Bean + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogCommonApi apiErrorLogApi) { + return new GlobalExceptionHandler(applicationName, apiErrorLogApi); + } + + @Bean + public GlobalResponseBodyHandler globalResponseBodyHandler() { + return new GlobalResponseBodyHandler(); + } + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) { + // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean + return new WebFrameworkUtils(webProperties); + } + + // ========== Filter 相关 ========== + + /** + * 创建 CorsFilter Bean,解决跨域问题 + */ + @Bean + public FilterRegistrationBean corsFilterBean() { + // 创建 CorsConfiguration 对象 + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); // 设置访问源地址 + config.addAllowedHeader("*"); // 设置访问源请求头 + config.addAllowedMethod("*"); // 设置访问源请求方法 + // 创建 UrlBasedCorsConfigurationSource 对象 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 + return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER); + } + + /** + * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 + */ + @Bean + public FilterRegistrationBean requestBodyCacheFilter() { + return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER); + } + + /** + * 创建 DemoFilter Bean,演示模式 + */ + @Bean + @ConditionalOnProperty(value = "yudao.demo", havingValue = "true") + public FilterRegistrationBean demoFilter() { + return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER); + } + + public static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + + /** + * 创建 RestTemplate 实例 + * + * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder} + */ + @Bean + @ConditionalOnMissingBean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.build(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java new file mode 100644 index 0000000..7b064e8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 过滤 /admin-api、/app-api 等 API 请求的过滤器 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public abstract class ApiRequestFilter extends OncePerRequestFilter { + + protected final WebProperties webProperties; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 只过滤 API 请求的地址 + String apiUri = request.getRequestURI().substring(request.getContextPath().length()); + return !StrUtil.startWithAny(apiUri, webProperties.getAdminApi().getPrefix(), webProperties.getAppApi().getPrefix()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java new file mode 100644 index 0000000..9071998 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Request Body 缓存 Filter,实现它的可重复读取 + * + * @author 芋道源码 + */ +public class CacheRequestBodyFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new CacheRequestBodyWrapper(request), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 只处理 json 请求内容 + return !ServletUtils.isJsonRequest(request); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java new file mode 100644 index 0000000..8e80fa5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Request Body 缓存 Wrapper + * + * @author 芋道源码 + */ +public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { + + /** + * 缓存的内容 + */ + private final byte[] body; + + public CacheRequestBodyWrapper(HttpServletRequest request) { + super(request); + body = ServletUtils.getBodyBytes(request); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + // 返回 ServletInputStream + return new ServletInputStream() { + + @Override + public int read() { + return inputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) {} + + @Override + public int available() { + return body.length; + } + + }; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/DemoFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/DemoFilter.java new file mode 100644 index 0000000..eece304 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/DemoFilter.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.web.core.filter; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY; + +/** + * 演示 Filter,禁止用户发起写操作,避免影响测试数据 + * + * @author 芋道源码 + */ +public class DemoFilter extends OncePerRequestFilter { + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String method = request.getMethod(); + return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率 + || WebFrameworkUtils.getLoginUserId(request) == null; // 非登录用户时,不进行过滤 + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { + // 直接返回 DEMO_DENY 的结果。即,请求不继续 + ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..e27d04e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -0,0 +1,407 @@ +package cn.iocoder.yudao.framework.web.core.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.JakartaServletUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.ValidationException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.Assert; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; + +/** + * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 + * + * @author 芋道源码 + */ +@RestControllerAdvice +@AllArgsConstructor +@Slf4j +public class GlobalExceptionHandler { + + /** + * 忽略的 ServiceException 错误提示,避免打印过多 logger + */ + public static final Set IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌"); + + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + private final String applicationName; + + private final ApiErrorLogCommonApi apiErrorLogApi; + + /** + * 处理所有异常,主要是提供给 Filter 使用 + * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 + * + * @param request 请求 + * @param ex 异常 + * @return 通用返回 + */ + public CommonResult allExceptionHandler(HttpServletRequest request, Throwable ex) { + if (ex instanceof MissingServletRequestParameterException) { + return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); + } + if (ex instanceof MethodArgumentTypeMismatchException) { + return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); + } + if (ex instanceof MethodArgumentNotValidException) { + return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); + } + if (ex instanceof BindException) { + return bindExceptionHandler((BindException) ex); + } + if (ex instanceof ConstraintViolationException) { + return constraintViolationExceptionHandler((ConstraintViolationException) ex); + } + if (ex instanceof ValidationException) { + return validationException((ValidationException) ex); + } + if (ex instanceof NoHandlerFoundException) { + return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); + } + if (ex instanceof NoResourceFoundException) { + return noResourceFoundExceptionHandler(request, (NoResourceFoundException) ex); + } + if (ex instanceof HttpRequestMethodNotSupportedException) { + return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); + } + if (ex instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex); + } + if (ex instanceof AccessDeniedException) { + return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); + } + return defaultExceptionHandler(request, ex); + } + + /** + * 处理 SpringMVC 请求参数缺失 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 + */ + @ExceptionHandler(value = MissingServletRequestParameterException.class) + public CommonResult missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { + log.warn("[missingServletRequestParameterExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); + } + + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { + log.warn("[methodArgumentTypeMismatchExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); + } + + /** + * 处理 SpringMVC 参数校验不正确 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { + log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); + // 获取 errorMessage + String errorMessage = null; + FieldError fieldError = ex.getBindingResult().getFieldError(); + if (fieldError == null) { + // 组合校验,参考自 https://t.zsxq.com/3HVTx + List allErrors = ex.getBindingResult().getAllErrors(); + if (CollUtil.isNotEmpty(allErrors)) { + errorMessage = allErrors.get(0).getDefaultMessage(); + } + } else { + errorMessage = fieldError.getDefaultMessage(); + } + // 转换 CommonResult + if (StrUtil.isEmpty(errorMessage)) { + return CommonResult.error(BAD_REQUEST); + } + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage)); + } + + /** + * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 + */ + @ExceptionHandler(BindException.class) + public CommonResult bindExceptionHandler(BindException ex) { + log.warn("[handleBindException]", ex); + FieldError fieldError = ex.getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestBody实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public CommonResult methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { + log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); + if(ex.getCause() instanceof InvalidFormatException) { + InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); + }else { + return defaultExceptionHandler(ServletUtils.getRequest(), ex); + } + } + + /** + * 处理 Validator 校验不通过产生的异常 + */ + @ExceptionHandler(value = ConstraintViolationException.class) + public CommonResult constraintViolationExceptionHandler(ConstraintViolationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + ConstraintViolation constraintViolation = ex.getConstraintViolations().iterator().next(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); + } + + /** + * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 + */ + @ExceptionHandler(value = ValidationException.class) + public CommonResult validationException(ValidationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 + return CommonResult.error(BAD_REQUEST); + } + + /** + * 处理 SpringMVC 请求地址不存在 + * + * 注意,它需要设置如下两个配置项: + * 1. spring.mvc.throw-exception-if-no-handler-found 为 true + * 2. spring.mvc.static-path-pattern 为 /statics/** + */ + @ExceptionHandler(NoHandlerFoundException.class) + public CommonResult noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { + log.warn("[noHandlerFoundExceptionHandler]", ex); + return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); + } + + /** + * 处理 SpringMVC 请求地址不存在 + */ + @ExceptionHandler(NoResourceFoundException.class) + private CommonResult noResourceFoundExceptionHandler(HttpServletRequest req, NoResourceFoundException ex) { + log.warn("[noResourceFoundExceptionHandler]", ex); + return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getResourcePath())); + } + + /** + * 处理 SpringMVC 请求方法不正确 + * + * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public CommonResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { + log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); + return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); + } + + /** + * 处理 Spring Security 权限不足的异常 + * + * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 + */ + @ExceptionHandler(value = AccessDeniedException.class) + public CommonResult accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { + log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), + req.getRequestURL(), ex); + return CommonResult.error(FORBIDDEN); + } + + /** + * 处理业务异常 ServiceException + * + * 例如说,商品库存不足,用户手机号已存在。 + */ + @ExceptionHandler(value = ServiceException.class) + public CommonResult serviceExceptionHandler(ServiceException ex) { + // 不包含的时候,才进行打印,避免 ex 堆栈过多 + if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) { + // 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到 + try { + StackTraceElement[] stackTraces = ex.getStackTrace(); + for (StackTraceElement stackTrace : stackTraces) { + if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceExceptionUtil.class.getName())) { + log.warn("[serviceExceptionHandler]\n\t{}", stackTrace); + break; + } + } + } catch (Exception ignored) { + // 忽略日志,避免影响主流程 + } + } + return CommonResult.error(ex.getCode(), ex.getMessage()); + } + + /** + * 处理系统异常,兜底处理所有的一切 + */ + @ExceptionHandler(value = Exception.class) + public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { + // 情况一:处理表不存在的异常 + CommonResult tableNotExistsResult = handleTableNotExists(ex); + if (tableNotExistsResult != null) { + return tableNotExistsResult; + } + + // 情况二:处理异常 + log.error("[defaultExceptionHandler]", ex); + // 插入异常日志 + createExceptionLog(req, ex); + // 返回 ERROR CommonResult + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + + private void createExceptionLog(HttpServletRequest req, Throwable e) { + // 插入错误日志 + ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO(); + try { + // 初始化 errorLog + buildExceptionLog(errorLog, req, e); + // 执行插入 errorLog + apiErrorLogApi.createApiErrorLogAsync(errorLog); + } catch (Throwable th) { + log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); + } + } + + private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) { + // 处理用户信息 + errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置异常字段 + errorLog.setExceptionName(e.getClass().getName()); + errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); + errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); + errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e)); + StackTraceElement[] stackTraceElements = e.getStackTrace(); + Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); + StackTraceElement stackTraceElement = stackTraceElements[0]; + errorLog.setExceptionClassName(stackTraceElement.getClassName()); + errorLog.setExceptionFileName(stackTraceElement.getFileName()); + errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); + errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); + // 设置其它字段 + errorLog.setTraceId(TracerUtils.getTraceId()); + errorLog.setApplicationName(applicationName); + errorLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder() + .put("query", JakartaServletUtil.getParamMap(request)) + .put("body", JakartaServletUtil.getBody(request)).build(); + errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + errorLog.setRequestMethod(request.getMethod()); + errorLog.setUserAgent(ServletUtils.getUserAgent(request)); + errorLog.setUserIp(JakartaServletUtil.getClientIP(request)); + errorLog.setExceptionTime(LocalDateTime.now()); + } + + /** + * 处理 Table 不存在的异常情况 + * + * @param ex 异常 + * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult + */ + private CommonResult handleTableNotExists(Throwable ex) { + String message = ExceptionUtil.getRootCauseMessage(ex); + if (!message.contains("doesn't exist")) { + return null; + } + // 1. 数据报表 + if (message.contains("report_")) { + log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); + } + // 2. 工作流 + if (message.contains("bpm_")) { + log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); + } + // 3. 微信公众号 + if (message.contains("mp_")) { + log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); + } + // 4. 商城系统 + if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) { + log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); + } + // 5. ERP 系统 + if (message.contains("erp_")) { + log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); + } + // 6. CRM 系统 + if (message.contains("crm_")) { + log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); + } + // 7. 支付平台 + if (message.contains("pay_")) { + log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); + } + // 8. AI 大模型 + if (message.contains("ai_")) { + log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); + } + // 9. IoT 物联网 + if (message.contains("iot_")) { + log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + } + return null; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalResponseBodyHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalResponseBodyHandler.java new file mode 100644 index 0000000..90da742 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalResponseBodyHandler.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.web.core.handler; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * 全局响应结果(ResponseBody)处理器 + * + * 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult}, + * 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。 + * 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构 + * + * 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果, + * 方便 {@link cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter} 记录访问日志 + */ +@ControllerAdvice +public class GlobalResponseBodyHandler implements ResponseBodyAdvice { + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public boolean supports(MethodParameter returnType, Class converterType) { + if (returnType.getMethod() == null) { + return false; + } + // 只拦截返回结果为 CommonResult 类型 + return returnType.getMethod().getReturnType() == CommonResult.class; + } + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + // 记录 Controller 结果 + WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body); + return body; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java new file mode 100644 index 0000000..b4aa301 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java @@ -0,0 +1,156 @@ +package cn.iocoder.yudao.framework.web.core.util; + +import cn.hutool.core.util.NumberUtil; +import cn.iocoder.yudao.framework.common.enums.TerminalEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * 专属于 web 包的工具类 + * + * @author 芋道源码 + */ +public class WebFrameworkUtils { + + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type"; + + private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; + + public static final String HEADER_TENANT_ID = "tenant-id"; + public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id"; + + /** + * 终端的 Header + * + * @see cn.iocoder.yudao.framework.common.enums.TerminalEnum + */ + public static final String HEADER_TERMINAL = "terminal"; + + private static WebProperties properties; + + public WebFrameworkUtils(WebProperties webProperties) { + WebFrameworkUtils.properties = webProperties; + } + + /** + * 获得租户编号,从 header 中 + * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供 + * + * @param request 请求 + * @return 租户编号 + */ + public static Long getTenantId(HttpServletRequest request) { + String tenantId = request.getHeader(HEADER_TENANT_ID); + return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null; + } + + /** + * 获得访问的租户编号,从 header 中 + * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供 + * + * @param request 请求 + * @return 租户编号 + */ + public static Long getVisitTenantId(HttpServletRequest request) { + String tenantId = request.getHeader(HEADER_VISIT_TENANT_ID); + return NumberUtil.isNumber(tenantId)? Long.valueOf(tenantId) : null; + } + + public static void setLoginUserId(ServletRequest request, Long userId) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); + } + + /** + * 设置用户类型 + * + * @param request 请求 + * @param userType 用户类型 + */ + public static void setLoginUserType(ServletRequest request, Integer userType) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType); + } + + /** + * 获得当前用户的编号,从请求中 + * 注意:该方法仅限于 framework 框架使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Long getLoginUserId(HttpServletRequest request) { + if (request == null) { + return null; + } + return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); + } + + /** + * 获得当前用户的类型 + * 注意:该方法仅限于 web 相关的 framework 组件使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Integer getLoginUserType(HttpServletRequest request) { + if (request == null) { + return null; + } + // 1. 优先,从 Attribute 中获取 + Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); + if (userType != null) { + return userType; + } + // 2. 其次,基于 URL 前缀的约定 + if (request.getServletPath().startsWith(properties.getAdminApi().getPrefix())) { + return UserTypeEnum.ADMIN.getValue(); + } + if (request.getServletPath().startsWith(properties.getAppApi().getPrefix())) { + return UserTypeEnum.MEMBER.getValue(); + } + return null; + } + + public static Integer getLoginUserType() { + HttpServletRequest request = getRequest(); + return getLoginUserType(request); + } + + public static Long getLoginUserId() { + HttpServletRequest request = getRequest(); + return getLoginUserId(request); + } + + public static Integer getTerminal() { + HttpServletRequest request = getRequest(); + if (request == null) { + return TerminalEnum.UNKNOWN.getTerminal(); + } + String terminalValue = request.getHeader(HEADER_TERMINAL); + return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal()); + } + + public static void setCommonResult(ServletRequest request, CommonResult result) { + request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); + } + + public static CommonResult getCommonResult(ServletRequest request) { + return (CommonResult) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); + } + + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; + return servletRequestAttributes.getRequest(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/package-info.java new file mode 100644 index 0000000..43b70eb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * 针对 SpringMVC 的基础封装 + */ +package cn.iocoder.yudao.framework.web; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/XssProperties.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/XssProperties.java new file mode 100644 index 0000000..c914b04 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/XssProperties.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.xss.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import java.util.Collections; +import java.util.List; + +/** + * Xss 配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yudao.xss") +@Validated +@Data +public class XssProperties { + + /** + * 是否开启,默认为 true + */ + private boolean enable = true; + /** + * 需要排除的 URL,默认为空 + */ + private List excludeUrls = Collections.emptyList(); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java new file mode 100644 index 0000000..99b6a44 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.xss.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.xss.core.clean.JsoupXssCleaner; +import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner; +import cn.iocoder.yudao.framework.xss.core.filter.XssFilter; +import cn.iocoder.yudao.framework.xss.core.json.XssStringJsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.util.PathMatcher; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import static cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration.createFilterBean; + +@AutoConfiguration +@EnableConfigurationProperties(XssProperties.class) +@ConditionalOnProperty(prefix = "yudao.xss", name = "enable", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 +public class YudaoXssAutoConfiguration implements WebMvcConfigurer { + + /** + * Xss 清理者 + * + * @return XssCleaner + */ + @Bean + @ConditionalOnMissingBean(XssCleaner.class) + public XssCleaner xssCleaner() { + return new JsoupXssCleaner(); + } + + /** + * 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤 + * + * @return Jackson2ObjectMapperBuilderCustomizer + */ + @Bean + @ConditionalOnMissingBean(name = "xssJacksonCustomizer") + @ConditionalOnBean(ObjectMapper.class) + @ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true") + public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties, + PathMatcher pathMatcher, + XssCleaner xssCleaner) { + // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理 + return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)); + } + + /** + * 创建 XssFilter Bean,解决 Xss 安全问题 + */ + @Bean + @ConditionalOnBean(XssCleaner.class) + public FilterRegistrationBean xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) { + return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/clean/JsoupXssCleaner.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/clean/JsoupXssCleaner.java new file mode 100644 index 0000000..4cbc907 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/clean/JsoupXssCleaner.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.framework.xss.core.clean; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Safelist; + +/** + * 基于 JSONP 实现 XSS 过滤字符串 + */ +public class JsoupXssCleaner implements XssCleaner { + + private final Safelist safelist; + + /** + * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分) + */ + private final String baseUri; + + /** + * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表 + */ + public JsoupXssCleaner() { + this.safelist = buildSafelist(); + this.baseUri = ""; + } + + /** + * 构建一个 Xss 清理的 Safelist 规则。 + * 基于 Safelist#relaxed() 的基础上: + * 1. 扩展支持了 style 和 class 属性 + * 2. a 标签额外支持了 target 属性 + * 3. img 标签额外支持了 data 协议,便于支持 base64 + * + * @return Safelist + */ + private Safelist buildSafelist() { + // 使用 jsoup 提供的默认的 + Safelist relaxedSafelist = Safelist.relaxed(); + // 富文本编辑时一些样式是使用 style 来进行实现的 + // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性 + // 注意:style 属性会有注入风险 + relaxedSafelist.addAttributes(":all", "style", "class"); + // 保留 a 标签的 target 属性 + relaxedSafelist.addAttributes("a", "target"); + // 支持img 为base64 + relaxedSafelist.addProtocols("img", "src", "data"); + + // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除 + // WHITELIST.preserveRelativeLinks(false); + + // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 + // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径 + // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto"); + // WHITELIST.removeProtocols("img", "src", "http", "https"); + return relaxedSafelist; + } + + @Override + public String clean(String html) { + return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false)); + } + +} + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/clean/XssCleaner.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/clean/XssCleaner.java new file mode 100644 index 0000000..529bc64 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/clean/XssCleaner.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.framework.xss.core.clean; + +/** + * 对 html 文本中的有 Xss 风险的数据进行清理 + */ +public interface XssCleaner { + + /** + * 清理有 Xss 风险的文本 + * + * @param html 原 html + * @return 清理后的 html + */ + String clean(String html); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/filter/XssFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/filter/XssFilter.java new file mode 100644 index 0000000..d33086a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/filter/XssFilter.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.xss.core.filter; + +import cn.iocoder.yudao.framework.xss.config.XssProperties; +import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner; +import lombok.AllArgsConstructor; +import org.springframework.util.PathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Xss 过滤器 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class XssFilter extends OncePerRequestFilter { + + /** + * 属性 + */ + private final XssProperties properties; + /** + * 路径匹配器 + */ + private final PathMatcher pathMatcher; + + private final XssCleaner xssCleaner; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 如果关闭,则不过滤 + if (!properties.isEnable()) { + return true; + } + + // 如果匹配到无需过滤,则不过滤 + String uri = request.getRequestURI(); + return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/filter/XssRequestWrapper.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/filter/XssRequestWrapper.java new file mode 100644 index 0000000..427eb9b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/filter/XssRequestWrapper.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.framework.xss.core.filter; + +import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Xss 请求 Wrapper + * + * @author 芋道源码 + */ +public class XssRequestWrapper extends HttpServletRequestWrapper { + + private final XssCleaner xssCleaner; + + public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) { + super(request); + this.xssCleaner = xssCleaner; + } + + // ============================ parameter ============================ + @Override + public Map getParameterMap() { + Map map = new LinkedHashMap<>(); + Map parameters = super.getParameterMap(); + for (Map.Entry entry : parameters.entrySet()) { + String[] values = entry.getValue(); + for (int i = 0; i < values.length; i++) { + values[i] = xssCleaner.clean(values[i]); + } + map.put(entry.getKey(), values); + } + return map; + } + + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (values == null) { + return null; + } + int count = values.length; + String[] encodedValues = new String[count]; + for (int i = 0; i < count; i++) { + encodedValues[i] = xssCleaner.clean(values[i]); + } + return encodedValues; + } + + @Override + public String getParameter(String name) { + String value = super.getParameter(name); + if (value == null) { + return null; + } + return xssCleaner.clean(value); + } + + // ============================ attribute ============================ + @Override + public Object getAttribute(String name) { + Object value = super.getAttribute(name); + if (value instanceof String) { + return xssCleaner.clean((String) value); + } + return value; + } + + // ============================ header ============================ + @Override + public String getHeader(String name) { + String value = super.getHeader(name); + if (value == null) { + return null; + } + return xssCleaner.clean(value); + } + + // ============================ queryString ============================ + @Override + public String getQueryString() { + String value = super.getQueryString(); + if (value == null) { + return null; + } + return xssCleaner.clean(value); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/json/XssStringJsonDeserializer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/json/XssStringJsonDeserializer.java new file mode 100644 index 0000000..047b19d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/core/json/XssStringJsonDeserializer.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.framework.xss.core.json; + +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.xss.config.XssProperties; +import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StringDeserializer; +import jakarta.servlet.http.HttpServletRequest; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.PathMatcher; + +import java.io.IOException; + +/** + * XSS 过滤 jackson 反序列化器。 + * 在反序列化的过程中,会对字符串进行 XSS 过滤。 + * + * @author Hccake + */ +@Slf4j +@AllArgsConstructor +public class XssStringJsonDeserializer extends StringDeserializer { + + /** + * 属性 + */ + private final XssProperties properties; + /** + * 路径匹配器 + */ + private final PathMatcher pathMatcher; + + private final XssCleaner xssCleaner; + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // 1. 白名单 URL 的处理 + HttpServletRequest request = ServletUtils.getRequest(); + if (request != null) { + String uri = ServletUtils.getRequest().getRequestURI(); + if (properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri))) { + return p.getText(); + } + } + + // 2. 真正使用 xssCleaner 进行过滤 + if (p.hasToken(JsonToken.VALUE_STRING)) { + return xssCleaner.clean(p.getText()); + } + JsonToken t = p.currentToken(); + // [databind#381] + if (t == JsonToken.START_ARRAY) { + return _deserializeFromArray(p, ctxt); + } + // need to gracefully handle byte[] data, as base64 + if (t == JsonToken.VALUE_EMBEDDED_OBJECT) { + Object ob = p.getEmbeddedObject(); + if (ob == null) { + return null; + } + if (ob instanceof byte[]) { + return ctxt.getBase64Variant().encode((byte[]) ob, false); + } + // otherwise, try conversion using toString()... + return ob.toString(); + } + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (t == JsonToken.START_OBJECT) { + return ctxt.extractScalarFromObject(p, this, _valueClass); + } + + if (t.isScalarValue()) { + String text = p.getValueAsString(); + return xssCleaner.clean(text); + } + return (String) ctxt.handleUnexpectedToken(_valueClass, p); + } +} + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/package-info.java new file mode 100644 index 0000000..c6e46f0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/package-info.java @@ -0,0 +1,6 @@ +/** + * 针对 XSS 的基础封装 + * + * XSS 说明:https://tech.meituan.com/2018/09/27/fe-security.html + */ +package cn.iocoder.yudao.framework.xss; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..9cdcd09 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,6 @@ +cn.iocoder.yudao.framework.apilog.config.YudaoApiLogAutoConfiguration +cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration +cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration +cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration +cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration +cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/banner.txt b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/banner.txt new file mode 100644 index 0000000..39a441d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/banner.txt @@ -0,0 +1,17 @@ +芋道源码 http://www.iocoder.cn +Application Version: ${yudao.info.version} +Spring Boot Version: ${spring-boot.version} + +.__ __. ______ .______ __ __ _______ +| \ | | / __ \ | _ \ | | | | / _____| +| \| | | | | | | |_) | | | | | | | __ +| . ` | | | | | | _ < | | | | | | |_ | +| |\ | | `--' | | |_) | | `--' | | |__| | +|__| \__| \______/ |______/ \______/ \______| + +███╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ +████╗ ██║██╔═══██╗ ██╔══██╗██║ ██║██╔════╝ +██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║██║ ███╗ +██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║██║ ██║ +██║ ╚████║╚██████╔╝ ██████╔╝╚██████╔╝╚██████╔╝ +╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/desensitize/core/DesensitizeTest.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/desensitize/core/DesensitizeTest.java new file mode 100644 index 0000000..dd214f0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/desensitize/core/DesensitizeTest.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.framework.desensitize.core; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.desensitize.core.annotation.Address; +import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.EmailDesensitize; +import cn.iocoder.yudao.framework.desensitize.core.regex.annotation.RegexDesensitize; +import cn.iocoder.yudao.framework.desensitize.core.slider.annotation.*; +import lombok.Data; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * {@link DesensitizeTest} 的单元测试 + */ +@ExtendWith(MockitoExtension.class) +public class DesensitizeTest { + + @Test + public void test() { + // 准备参数 + DesensitizeDemo desensitizeDemo = new DesensitizeDemo(); + desensitizeDemo.setNickname("芋道源码"); + desensitizeDemo.setBankCard("9988002866797031"); + desensitizeDemo.setCarLicense("粤A66666"); + desensitizeDemo.setFixedPhone("01086551122"); + desensitizeDemo.setIdCard("530321199204074611"); + desensitizeDemo.setPassword("123456"); + desensitizeDemo.setPhoneNumber("13248765917"); + desensitizeDemo.setSlider1("ABCDEFG"); + desensitizeDemo.setSlider2("ABCDEFG"); + desensitizeDemo.setSlider3("ABCDEFG"); + desensitizeDemo.setEmail("1@email.com"); + desensitizeDemo.setRegex("你好,我是芋道源码"); + desensitizeDemo.setAddress("北京市海淀区上地十街10号"); + desensitizeDemo.setOrigin("芋道源码"); + + // 调用 + DesensitizeDemo d = JsonUtils.parseObject(JsonUtils.toJsonString(desensitizeDemo), DesensitizeDemo.class); + // 断言 + assertNotNull(d); + assertEquals("芋***", d.getNickname()); + assertEquals("998800********31", d.getBankCard()); + assertEquals("粤A6***6", d.getCarLicense()); + assertEquals("0108*****22", d.getFixedPhone()); + assertEquals("530321**********11", d.getIdCard()); + assertEquals("******", d.getPassword()); + assertEquals("132****5917", d.getPhoneNumber()); + assertEquals("#######", d.getSlider1()); + assertEquals("ABC*EFG", d.getSlider2()); + assertEquals("*******", d.getSlider3()); + assertEquals("1****@email.com", d.getEmail()); + assertEquals("你好,我是*", d.getRegex()); + assertEquals("北京市海淀区上地十街10号*", d.getAddress()); + assertEquals("芋道源码", d.getOrigin()); + } + + @Data + public static class DesensitizeDemo { + + @ChineseNameDesensitize + private String nickname; + @BankCardDesensitize + private String bankCard; + @CarLicenseDesensitize + private String carLicense; + @FixedPhoneDesensitize + private String fixedPhone; + @IdCardDesensitize + private String idCard; + @PasswordDesensitize + private String password; + @MobileDesensitize + private String phoneNumber; + @SliderDesensitize(prefixKeep = 6, suffixKeep = 1, replacer = "#") + private String slider1; + @SliderDesensitize(prefixKeep = 3, suffixKeep = 3) + private String slider2; + @SliderDesensitize(prefixKeep = 10) + private String slider3; + @EmailDesensitize + private String email; + @RegexDesensitize(regex = "芋道源码", replacer = "*") + private String regex; + @Address + private String address; + private String origin; + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/desensitize/core/annotation/Address.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/desensitize/core/annotation/Address.java new file mode 100644 index 0000000..1301209 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/desensitize/core/annotation/Address.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.framework.desensitize.core.annotation; + +import cn.iocoder.yudao.framework.desensitize.core.DesensitizeTest; +import cn.iocoder.yudao.framework.desensitize.core.handler.AddressHandler; +import cn.iocoder.yudao.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 地址 + * + * 用于 {@link DesensitizeTest} 测试使用 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = AddressHandler.class) +public @interface Address { + + String replacer() default "*"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/desensitize/core/handler/AddressHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/desensitize/core/handler/AddressHandler.java new file mode 100644 index 0000000..7a8455f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/desensitize/core/handler/AddressHandler.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.desensitize.core.handler; + +import cn.iocoder.yudao.framework.desensitize.core.DesensitizeTest; +import cn.iocoder.yudao.framework.desensitize.core.base.handler.DesensitizationHandler; +import cn.iocoder.yudao.framework.desensitize.core.annotation.Address; + +/** + * {@link Address} 的脱敏处理器 + * + * 用于 {@link DesensitizeTest} 测试使用 + */ +public class AddressHandler implements DesensitizationHandler
{ + + @Override + public String desensitize(String origin, Address annotation) { + return origin + annotation.replacer(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md new file mode 100644 index 0000000..7ed5ebd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md new file mode 100644 index 0000000..7c36a15 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml new file mode 100644 index 0000000..b534f10 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml @@ -0,0 +1,73 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-websocket + jar + + ${project.artifactId} + WebSocket 框架,支持多节点的广播 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + cn.iocoder.boot + yudao-common + + + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + provided + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mq + + + org.springframework.kafka + spring-kafka + true + + + org.springframework.amqp + spring-rabbit + true + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + provided + + + + \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java new file mode 100644 index 0000000..f1d84f7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.websocket.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * WebSocket 配置项 + * + * @author xingyu4j + */ +@ConfigurationProperties("yudao.websocket") +@Data +@Validated +public class WebSocketProperties { + + /** + * WebSocket 的连接路径 + */ + @NotEmpty(message = "WebSocket 的连接路径不能为空") + private String path = "/ws"; + + /** + * 消息发送器的类型 + * + * 可选值:local、redis、rocketmq、kafka、rabbitmq + */ + @NotNull(message = "WebSocket 的消息发送者不能为空") + private String senderType = "local"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java new file mode 100644 index 0000000..3aded88 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java @@ -0,0 +1,183 @@ +package cn.iocoder.yudao.framework.websocket.config; + +import cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration; +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler; +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; +import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor; +import cn.iocoder.yudao.framework.websocket.core.security.WebSocketAuthorizeRequestsCustomizer; +import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer; +import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer; +import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer; +import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer; +import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator; +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.List; + +/** + * WebSocket 自动配置 + * + * @author xingyu4j + */ +@AutoConfiguration(before = YudaoRedisMQConsumerAutoConfiguration.class) // before YudaoRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer +@EnableWebSocket // 开启 websocket +@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) // 允许使用 yudao.websocket.enable=false 禁用 websocket +@EnableConfigurationProperties(WebSocketProperties.class) +public class YudaoWebSocketAutoConfiguration { + + @Bean + public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors, + WebSocketHandler webSocketHandler, + WebSocketProperties webSocketProperties) { + return registry -> registry + // 添加 WebSocketHandler + .addHandler(webSocketHandler, webSocketProperties.getPath()) + .addInterceptors(handshakeInterceptors) + // 允许跨域,否则前端连接会直接断开 + .setAllowedOriginPatterns("*"); + } + + @Bean + public HandshakeInterceptor handshakeInterceptor() { + return new LoginUserHandshakeInterceptor(); + } + + @Bean + public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager, + List> messageListeners) { + // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息 + JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners); + // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接 + return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager); + } + + @Bean + public WebSocketSessionManager webSocketSessionManager() { + return new WebSocketSessionManagerImpl(); + } + + @Bean + public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) { + return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties); + } + + // ==================== Sender 相关 ==================== + + @Configuration + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local") + public class LocalWebSocketMessageSenderConfiguration { + + @Bean + public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) { + return new LocalWebSocketMessageSender(sessionManager); + } + + } + + @Configuration + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis") + public class RedisWebSocketMessageSenderConfiguration { + + @Bean + public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager, + RedisMQTemplate redisMQTemplate) { + return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate); + } + + @Bean + public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer( + RedisWebSocketMessageSender redisWebSocketMessageSender) { + return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender); + } + + } + + @Configuration + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq") + public class RocketMQWebSocketMessageSenderConfiguration { + + @Bean + public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender( + WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate, + @Value("${yudao.websocket.sender-rocketmq.topic}") String topic) { + return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic); + } + + @Bean + public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer( + RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) { + return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender); + } + + } + + @Configuration + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq") + public class RabbitMQWebSocketMessageSenderConfiguration { + + @Bean + public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender( + WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate, + TopicExchange websocketTopicExchange) { + return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange); + } + + @Bean + public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer( + RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) { + return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender); + } + + /** + * 创建 Topic Exchange + */ + @Bean + public TopicExchange websocketTopicExchange(@Value("${yudao.websocket.sender-rabbitmq.exchange}") String exchange) { + return new TopicExchange(exchange, + true, // durable: 是否持久化 + false); // exclusive: 是否排它 + } + + } + + @Configuration + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka") + public class KafkaWebSocketMessageSenderConfiguration { + + @Bean + public KafkaWebSocketMessageSender kafkaWebSocketMessageSender( + WebSocketSessionManager sessionManager, KafkaTemplate kafkaTemplate, + @Value("${yudao.websocket.sender-kafka.topic}") String topic) { + return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic); + } + + @Bean + public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer( + KafkaWebSocketMessageSender kafkaWebSocketMessageSender) { + return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender); + } + + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java new file mode 100644 index 0000000..120f529 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.framework.websocket.core.handler; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage; +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * JSON 格式 {@link WebSocketHandler} 实现类 + * + * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。 + * + * @author 芋道源码 + */ +@Slf4j +public class JsonWebSocketMessageHandler extends TextWebSocketHandler { + + /** + * type 与 WebSocketMessageListener 的映射 + */ + private final Map> listeners = new HashMap<>(); + + @SuppressWarnings({"rawtypes", "unchecked"}) + public JsonWebSocketMessageHandler(List listenersList) { + listenersList.forEach((Consumer) + listener -> listeners.put(listener.getType(), listener)); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + // 1.1 空消息,跳过 + if (message.getPayloadLength() == 0) { + return; + } + // 1.2 ping 心跳消息,直接返回 pong 消息。 + if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) { + session.sendMessage(new TextMessage("pong")); + return; + } + + // 2.1 解析消息 + try { + JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class); + if (jsonMessage == null) { + log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload()); + return; + } + if (StrUtil.isEmpty(jsonMessage.getType())) { + log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload()); + return; + } + // 2.2 获得对应的 WebSocketMessageListener + WebSocketMessageListener messageListener = listeners.get(jsonMessage.getType()); + if (messageListener == null) { + log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload()); + return; + } + // 2.3 处理消息 + Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0); + Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type); + Long tenantId = WebSocketFrameworkUtils.getTenantId(session); + TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj)); + } catch (Throwable ex) { + log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload()); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java new file mode 100644 index 0000000..f3a62cc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.websocket.core.listener; + +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage; +import org.springframework.web.socket.WebSocketSession; + +/** + * WebSocket 消息监听器接口 + * + * 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息 + * + * @param 泛型,消息类型 + */ +public interface WebSocketMessageListener { + + /** + * 处理消息 + * + * @param session Session + * @param message 消息 + */ + void onMessage(WebSocketSession session, T message); + + /** + * 获得消息类型 + * + * @see JsonWebSocketMessage#getType() + * @return 消息类型 + */ + String getType(); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java new file mode 100644 index 0000000..0a55cd6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.websocket.core.message; + +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; +import lombok.Data; + +import java.io.Serializable; + +/** + * JSON 格式的 WebSocket 消息帧 + * + * @author 芋道源码 + */ +@Data +public class JsonWebSocketMessage implements Serializable { + + /** + * 消息类型 + * + * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类 + */ + private String type; + /** + * 消息内容 + * + * 要求 JSON 对象 + */ + private String content; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java new file mode 100644 index 0000000..3a31825 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.websocket.core.security; + +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +/** + * 登录用户的 {@link HandshakeInterceptor} 实现类 + * + * 流程如下: + * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过 + * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中 + * + * @author 芋道源码 + */ +public class LoginUserHandshakeInterceptor implements HandshakeInterceptor { + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Map attributes) { + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser != null) { + WebSocketFrameworkUtils.setLoginUser(loginUser, attributes); + } + return true; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Exception exception) { + // do nothing + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java new file mode 100644 index 0000000..f260dfc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.framework.websocket.core.security; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import cn.iocoder.yudao.framework.websocket.config.WebSocketProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * WebSocket 的权限自定义 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { + + private final WebSocketProperties webSocketProperties; + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + registry.requestMatchers(webSocketProperties.getPath()).permitAll(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java new file mode 100644 index 0000000..9309fd2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.framework.websocket.core.sender; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage; +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * WebSocketMessageSender 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender { + + private final WebSocketSessionManager sessionManager; + + @Override + public void send(Integer userType, Long userId, String messageType, String messageContent) { + send(null, userType, userId, messageType, messageContent); + } + + @Override + public void send(Integer userType, String messageType, String messageContent) { + send(null, userType, null, messageType, messageContent); + } + + @Override + public void send(String sessionId, String messageType, String messageContent) { + send(sessionId, null, null, messageType, messageContent); + } + + /** + * 发送消息 + * + * @param sessionId Session 编号 + * @param userType 用户类型 + * @param userId 用户编号 + * @param messageType 消息类型 + * @param messageContent 消息内容 + */ + public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) { + // 1. 获得 Session 列表 + List sessions = Collections.emptyList(); + if (StrUtil.isNotEmpty(sessionId)) { + WebSocketSession session = sessionManager.getSession(sessionId); + if (session != null) { + sessions = Collections.singletonList(session); + } + } else if (userType != null && userId != null) { + sessions = (List) sessionManager.getSessionList(userType, userId); + } else if (userType != null) { + sessions = (List) sessionManager.getSessionList(userType); + } + if (CollUtil.isEmpty(sessions)) { + if (log.isDebugEnabled()) { + log.debug("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]", + sessionId, userType, userId, messageType, messageContent); + } + } + // 2. 执行发送 + doSend(sessions, messageType, messageContent); + } + + /** + * 发送消息的具体实现 + * + * @param sessions Session 列表 + * @param messageType 消息类型 + * @param messageContent 消息内容 + */ + public void doSend(Collection sessions, String messageType, String messageContent) { + JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent); + String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化 + sessions.forEach(session -> { + // 1. 各种校验,保证 Session 可以被发送 + if (session == null) { + log.error("[doSend][session 为空, message({})]", message); + return; + } + if (!session.isOpen()) { + log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message); + return; + } + // 2. 执行发送 + try { + session.sendMessage(new TextMessage(payload)); + log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message); + } catch (IOException ex) { + log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex); + } + }); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java new file mode 100644 index 0000000..9f75ad5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.websocket.core.sender; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; + +/** + * WebSocket 消息的发送器接口 + * + * @author 芋道源码 + */ +public interface WebSocketMessageSender { + + /** + * 发送消息给指定用户 + * + * @param userType 用户类型 + * @param userId 用户编号 + * @param messageType 消息类型 + * @param messageContent 消息内容,JSON 格式 + */ + void send(Integer userType, Long userId, String messageType, String messageContent); + + /** + * 发送消息给指定用户类型 + * + * @param userType 用户类型 + * @param messageType 消息类型 + * @param messageContent 消息内容,JSON 格式 + */ + void send(Integer userType, String messageType, String messageContent); + + /** + * 发送消息给指定 Session + * + * @param sessionId Session 编号 + * @param messageType 消息类型 + * @param messageContent 消息内容,JSON 格式 + */ + void send(String sessionId, String messageType, String messageContent); + + default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) { + send(userType, userId, messageType, JsonUtils.toJsonString(messageContent)); + } + + default void sendObject(Integer userType, String messageType, Object messageContent) { + send(userType, messageType, JsonUtils.toJsonString(messageContent)); + } + + default void sendObject(String sessionId, String messageType, Object messageContent) { + send(sessionId, messageType, JsonUtils.toJsonString(messageContent)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java new file mode 100644 index 0000000..5a4cf53 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.kafka; + +import lombok.Data; + +/** + * Kafka 广播 WebSocket 的消息 + * + * @author 芋道源码 + */ +@Data +public class KafkaWebSocketMessage { + + /** + * Session 编号 + */ + private String sessionId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 用户编号 + */ + private Long userId; + + /** + * 消息类型 + */ + private String messageType; + /** + * 消息内容 + */ + private String messageContent; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java new file mode 100644 index 0000000..4b255b9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.kafka; + +import lombok.RequiredArgsConstructor; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.kafka.annotation.KafkaListener; + +/** + * {@link KafkaWebSocketMessage} 广播消息的消费者,真正把消息发送出去 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class KafkaWebSocketMessageConsumer { + + private final KafkaWebSocketMessageSender kafkaWebSocketMessageSender; + + @RabbitHandler + @KafkaListener( + topics = "${yudao.websocket.sender-kafka.topic}", + // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的 + groupId = "${yudao.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}") + public void onMessage(KafkaWebSocketMessage message) { + kafkaWebSocketMessageSender.send(message.getSessionId(), + message.getUserType(), message.getUserId(), + message.getMessageType(), message.getMessageContent()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java new file mode 100644 index 0000000..47bb598 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.kafka; + +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; + +import java.util.concurrent.ExecutionException; + +/** + * 基于 Kafka 的 {@link WebSocketMessageSender} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender { + + private final KafkaTemplate kafkaTemplate; + + private final String topic; + + public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager, + KafkaTemplate kafkaTemplate, + String topic) { + super(sessionManager); + this.kafkaTemplate = kafkaTemplate; + this.topic = topic; + } + + @Override + public void send(Integer userType, Long userId, String messageType, String messageContent) { + sendKafkaMessage(null, userId, userType, messageType, messageContent); + } + + @Override + public void send(Integer userType, String messageType, String messageContent) { + sendKafkaMessage(null, null, userType, messageType, messageContent); + } + + @Override + public void send(String sessionId, String messageType, String messageContent) { + sendKafkaMessage(sessionId, null, null, messageType, messageContent); + } + + /** + * 通过 Kafka 广播消息 + * + * @param sessionId Session 编号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param messageType 消息类型 + * @param messageContent 消息内容 + */ + private void sendKafkaMessage(String sessionId, Long userId, Integer userType, + String messageType, String messageContent) { + KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage() + .setSessionId(sessionId).setUserId(userId).setUserType(userType) + .setMessageType(messageType).setMessageContent(messageContent); + try { + kafkaTemplate.send(topic, mqMessage).get(); + } catch (InterruptedException | ExecutionException e) { + log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java new file mode 100644 index 0000000..66640ef --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.local; + +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; + +/** + * 本地的 {@link WebSocketMessageSender} 实现类 + * + * 注意:仅仅适合单机场景!!! + * + * @author 芋道源码 + */ +public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender { + + public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) { + super(sessionManager); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java new file mode 100644 index 0000000..80a4bc1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq; + +import lombok.Data; + +import java.io.Serializable; + +/** + * RabbitMQ 广播 WebSocket 的消息 + * + * @author 芋道源码 + */ +@Data +public class RabbitMQWebSocketMessage implements Serializable { + + /** + * Session 编号 + */ + private String sessionId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 用户编号 + */ + private Long userId; + + /** + * 消息类型 + */ + private String messageType; + /** + * 消息内容 + */ + private String messageContent; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java new file mode 100644 index 0000000..59e3824 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq; + +import lombok.RequiredArgsConstructor; +import org.springframework.amqp.core.ExchangeTypes; +import org.springframework.amqp.rabbit.annotation.*; + +/** + * {@link RabbitMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去 + * + * @author 芋道源码 + */ +@RabbitListener( + bindings = @QueueBinding( + value = @Queue( + // 在 Queue 的名字上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Queue 不同,以达到广播消费的目的 + name = "${yudao.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}", + // Consumer 关闭时,该队列就可以被自动删除了 + autoDelete = "true" + ), + exchange = @Exchange( + name = "${yudao.websocket.sender-rabbitmq.exchange}", + type = ExchangeTypes.TOPIC, + declare = "false" + ) + ) +) +@RequiredArgsConstructor +public class RabbitMQWebSocketMessageConsumer { + + private final RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender; + + @RabbitHandler + public void onMessage(RabbitMQWebSocketMessage message) { + rabbitMQWebSocketMessageSender.send(message.getSessionId(), + message.getUserType(), message.getUserId(), + message.getMessageType(), message.getMessageContent()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java new file mode 100644 index 0000000..065a5d6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq; + +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.core.RabbitTemplate; + +/** + * 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender { + + private final RabbitTemplate rabbitTemplate; + + private final TopicExchange topicExchange; + + public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager, + RabbitTemplate rabbitTemplate, + TopicExchange topicExchange) { + super(sessionManager); + this.rabbitTemplate = rabbitTemplate; + this.topicExchange = topicExchange; + } + + @Override + public void send(Integer userType, Long userId, String messageType, String messageContent) { + sendRabbitMQMessage(null, userId, userType, messageType, messageContent); + } + + @Override + public void send(Integer userType, String messageType, String messageContent) { + sendRabbitMQMessage(null, null, userType, messageType, messageContent); + } + + @Override + public void send(String sessionId, String messageType, String messageContent) { + sendRabbitMQMessage(sessionId, null, null, messageType, messageContent); + } + + /** + * 通过 RabbitMQ 广播消息 + * + * @param sessionId Session 编号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param messageType 消息类型 + * @param messageContent 消息内容 + */ + private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType, + String messageType, String messageContent) { + RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage() + .setSessionId(sessionId).setUserId(userId).setUserType(userType) + .setMessageType(messageType).setMessageContent(messageContent); + rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java new file mode 100644 index 0000000..fb9ea0c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.redis; + +import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage; +import lombok.Data; + +/** + * Redis 广播 WebSocket 的消息 + */ +@Data +public class RedisWebSocketMessage extends AbstractRedisChannelMessage { + + /** + * Session 编号 + */ + private String sessionId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 用户编号 + */ + private Long userId; + + /** + * 消息类型 + */ + private String messageType; + /** + * 消息内容 + */ + private String messageContent; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java new file mode 100644 index 0000000..abce006 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.redis; + +import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; +import lombok.RequiredArgsConstructor; + +/** + * {@link RedisWebSocketMessage} 广播消息的消费者,真正把消息发送出去 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class RedisWebSocketMessageConsumer extends AbstractRedisChannelMessageListener { + + private final RedisWebSocketMessageSender redisWebSocketMessageSender; + + @Override + public void onMessage(RedisWebSocketMessage message) { + redisWebSocketMessageSender.send(message.getSessionId(), + message.getUserType(), message.getUserId(), + message.getMessageType(), message.getMessageContent()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java new file mode 100644 index 0000000..d6004ac --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.redis; + +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; +import lombok.extern.slf4j.Slf4j; + +/** + * 基于 Redis 的 {@link WebSocketMessageSender} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender { + + private final RedisMQTemplate redisMQTemplate; + + public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager, + RedisMQTemplate redisMQTemplate) { + super(sessionManager); + this.redisMQTemplate = redisMQTemplate; + } + + @Override + public void send(Integer userType, Long userId, String messageType, String messageContent) { + sendRedisMessage(null, userId, userType, messageType, messageContent); + } + + @Override + public void send(Integer userType, String messageType, String messageContent) { + sendRedisMessage(null, null, userType, messageType, messageContent); + } + + @Override + public void send(String sessionId, String messageType, String messageContent) { + sendRedisMessage(sessionId, null, null, messageType, messageContent); + } + + /** + * 通过 Redis 广播消息 + * + * @param sessionId Session 编号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param messageType 消息类型 + * @param messageContent 消息内容 + */ + private void sendRedisMessage(String sessionId, Long userId, Integer userType, + String messageType, String messageContent) { + RedisWebSocketMessage mqMessage = new RedisWebSocketMessage() + .setSessionId(sessionId).setUserId(userId).setUserType(userType) + .setMessageType(messageType).setMessageContent(messageContent); + redisMQTemplate.send(mqMessage); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java new file mode 100644 index 0000000..91570e3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq; + +import lombok.Data; + +/** + * RocketMQ 广播 WebSocket 的消息 + * + * @author 芋道源码 + */ +@Data +public class RocketMQWebSocketMessage { + + /** + * Session 编号 + */ + private String sessionId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 用户编号 + */ + private Long userId; + + /** + * 消息类型 + */ + private String messageType; + /** + * 消息内容 + */ + private String messageContent; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java new file mode 100644 index 0000000..ab2e2c4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq; + +import lombok.RequiredArgsConstructor; +import org.apache.rocketmq.spring.annotation.MessageModel; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; + +/** + * {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去 + * + * @author 芋道源码 + */ +@RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic + topic = "${yudao.websocket.sender-rocketmq.topic}", + consumerGroup = "${yudao.websocket.sender-rocketmq.consumer-group}", + messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息 +) +@RequiredArgsConstructor +public class RocketMQWebSocketMessageConsumer implements RocketMQListener { + + private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender; + + @Override + public void onMessage(RocketMQWebSocketMessage message) { + rocketMQWebSocketMessageSender.send(message.getSessionId(), + message.getUserType(), message.getUserId(), + message.getMessageType(), message.getMessageContent()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java new file mode 100644 index 0000000..ed059ba --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq; + +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.core.RocketMQTemplate; + +/** + * 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender { + + private final RocketMQTemplate rocketMQTemplate; + + private final String topic; + + public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager, + RocketMQTemplate rocketMQTemplate, + String topic) { + super(sessionManager); + this.rocketMQTemplate = rocketMQTemplate; + this.topic = topic; + } + + @Override + public void send(Integer userType, Long userId, String messageType, String messageContent) { + sendRocketMQMessage(null, userId, userType, messageType, messageContent); + } + + @Override + public void send(Integer userType, String messageType, String messageContent) { + sendRocketMQMessage(null, null, userType, messageType, messageContent); + } + + @Override + public void send(String sessionId, String messageType, String messageContent) { + sendRocketMQMessage(sessionId, null, null, messageType, messageContent); + } + + /** + * 通过 RocketMQ 广播消息 + * + * @param sessionId Session 编号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param messageType 消息类型 + * @param messageContent 消息内容 + */ + private void sendRocketMQMessage(String sessionId, Long userId, Integer userType, + String messageType, String messageContent) { + RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage() + .setSessionId(sessionId).setUserId(userId).setUserType(userType) + .setMessageType(messageType).setMessageContent(messageContent); + rocketMQTemplate.syncSend(topic, mqMessage); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java new file mode 100644 index 0000000..600a4dd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.framework.websocket.core.session; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; +import org.springframework.web.socket.handler.WebSocketHandlerDecorator; + +/** + * {@link WebSocketHandler} 的装饰类,实现了以下功能: + * + * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理 + * 2. 封装 {@link WebSocketSession} 支持并发操作 + * + * @author 芋道源码 + */ +public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator { + + /** + * 发送时间的限制,单位:毫秒 + */ + private static final Integer SEND_TIME_LIMIT = 1000 * 5; + /** + * 发送消息缓冲上线,单位:bytes + */ + private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100; + + private final WebSocketSessionManager sessionManager; + + public WebSocketSessionHandlerDecorator(WebSocketHandler delegate, + WebSocketSessionManager sessionManager) { + super(delegate); + this.sessionManager = sessionManager; + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) { + // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149 + session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT); + // 添加到 WebSocketSessionManager 中 + sessionManager.addSession(session); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) { + sessionManager.removeSession(session); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java new file mode 100644 index 0000000..ad1de23 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.framework.websocket.core.session; + +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collection; + +/** + * {@link WebSocketSession} 管理器的接口 + * + * @author 芋道源码 + */ +public interface WebSocketSessionManager { + + /** + * 添加 Session + * + * @param session Session + */ + void addSession(WebSocketSession session); + + /** + * 移除 Session + * + * @param session Session + */ + void removeSession(WebSocketSession session); + + /** + * 获得指定编号的 Session + * + * @param id Session 编号 + * @return Session + */ + WebSocketSession getSession(String id); + + /** + * 获得指定用户类型的 Session 列表 + * + * @param userType 用户类型 + * @return Session 列表 + */ + Collection getSessionList(Integer userType); + + /** + * 获得指定用户编号的 Session 列表 + * + * @param userType 用户类型 + * @param userId 用户编号 + * @return Session 列表 + */ + Collection getSessionList(Integer userType, Long userId); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java new file mode 100644 index 0000000..6dba898 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java @@ -0,0 +1,125 @@ +package cn.iocoder.yudao.framework.websocket.core.session; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; +import org.springframework.web.socket.WebSocketSession; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * 默认的 {@link WebSocketSessionManager} 实现类 + * + * @author 芋道源码 + */ +public class WebSocketSessionManagerImpl implements WebSocketSessionManager { + + /** + * id 与 WebSocketSession 映射 + * + * key:Session 编号 + */ + private final ConcurrentMap idSessions = new ConcurrentHashMap<>(); + + /** + * user 与 WebSocketSession 映射 + * + * key1:用户类型 + * key2:用户编号 + */ + private final ConcurrentMap>> userSessions + = new ConcurrentHashMap<>(); + + @Override + public void addSession(WebSocketSession session) { + // 添加到 idSessions 中 + idSessions.put(session.getId(), session); + // 添加到 userSessions 中 + LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); + if (user == null) { + return; + } + ConcurrentMap> userSessionsMap = userSessions.get(user.getUserType()); + if (userSessionsMap == null) { + userSessionsMap = new ConcurrentHashMap<>(); + if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) { + userSessionsMap = userSessions.get(user.getUserType()); + } + } + CopyOnWriteArrayList sessions = userSessionsMap.get(user.getId()); + if (sessions == null) { + sessions = new CopyOnWriteArrayList<>(); + if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) { + sessions = userSessionsMap.get(user.getId()); + } + } + sessions.add(session); + } + + @Override + public void removeSession(WebSocketSession session) { + // 移除从 idSessions 中 + idSessions.remove(session.getId()); + // 移除从 idSessions 中 + LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); + if (user == null) { + return; + } + ConcurrentMap> userSessionsMap = userSessions.get(user.getUserType()); + if (userSessionsMap == null) { + return; + } + CopyOnWriteArrayList sessions = userSessionsMap.get(user.getId()); + sessions.removeIf(session0 -> session0.getId().equals(session.getId())); + if (CollUtil.isEmpty(sessions)) { + userSessionsMap.remove(user.getId(), sessions); + } + } + + @Override + public WebSocketSession getSession(String id) { + return idSessions.get(id); + } + + @Override + public Collection getSessionList(Integer userType) { + ConcurrentMap> userSessionsMap = userSessions.get(userType); + if (CollUtil.isEmpty(userSessionsMap)) { + return new ArrayList<>(); + } + LinkedList result = new LinkedList<>(); // 避免扩容 + Long contextTenantId = TenantContextHolder.getTenantId(); + for (List sessions : userSessionsMap.values()) { + if (CollUtil.isEmpty(sessions)) { + continue; + } + // 特殊:如果租户不匹配,则直接排除 + if (contextTenantId != null) { + Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0)); + if (!contextTenantId.equals(userTenantId)) { + continue; + } + } + result.addAll(sessions); + } + return result; + } + + @Override + public Collection getSessionList(Integer userType, Long userId) { + ConcurrentMap> userSessionsMap = userSessions.get(userType); + if (CollUtil.isEmpty(userSessionsMap)) { + return new ArrayList<>(); + } + CopyOnWriteArrayList sessions = userSessionsMap.get(userId); + return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java new file mode 100644 index 0000000..58cdedc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.framework.websocket.core.util; + +import cn.iocoder.yudao.framework.security.core.LoginUser; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; + +/** + * 专属于 web 包的工具类 + * + * @author 芋道源码 + */ +public class WebSocketFrameworkUtils { + + public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER"; + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param attributes Session + */ + public static void setLoginUser(LoginUser loginUser, Map attributes) { + attributes.put(ATTRIBUTE_LOGIN_USER, loginUser); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + public static LoginUser getLoginUser(WebSocketSession session) { + return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER); + } + + /** + * 获得当前用户的编号 + * + * @return 用户编号 + */ + public static Long getLoginUserId(WebSocketSession session) { + LoginUser loginUser = getLoginUser(session); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 获得当前用户的类型 + * + * @return 用户编号 + */ + public static Integer getLoginUserType(WebSocketSession session) { + LoginUser loginUser = getLoginUser(session); + return loginUser != null ? loginUser.getUserType() : null; + } + + /** + * 获得当前用户的租户编号 + * + * @param session Session + * @return 租户编号 + */ + public static Long getTenantId(WebSocketSession session) { + LoginUser loginUser = getLoginUser(session); + return loginUser != null ? loginUser.getTenantId() : null; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java new file mode 100644 index 0000000..97bc5f9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java @@ -0,0 +1,4 @@ +/** + * WebSocket 框架,支持多节点的广播 + */ +package cn.iocoder.yudao.framework.websocket; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..6260e40 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.framework.websocket.config.YudaoWebSocketAutoConfiguration \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md new file mode 100644 index 0000000..8df5a77 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/pom.xml new file mode 100644 index 0000000..d44b5a5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/pom.xml @@ -0,0 +1,79 @@ + + + + yudao + cn.iocoder.boot + ${revision} + + 4.0.0 + yudao-module-bpm + jar + + ${project.artifactId} + + bpm 包下,业务流程管理(Business Process Management),我们放工作流的功能。 + 例如说:流程定义、表单配置、审核中心(我的申请、我的待办、我的已办)等等 + bpm 解释:https://baike.baidu.com/item/BPM/1933 + + 工作流基于 Flowable 6 实现,分成流程定义、流程表单、流程实例、流程任务等功能模块。 + + + + + cn.iocoder.boot + yudao-module-system + ${revision} + + + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-data-permission + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + + + + + cn.iocoder.boot + yudao-spring-boot-starter-web + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + + + + + cn.iocoder.boot + yudao-spring-boot-starter-excel + + + + + org.flowable + flowable-spring-boot-starter-process + + + org.flowable + flowable-spring-boot-starter-actuator + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/event/BpmProcessInstanceStatusEvent.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/event/BpmProcessInstanceStatusEvent.java new file mode 100644 index 0000000..5522d01 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/event/BpmProcessInstanceStatusEvent.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.bpm.api.event; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.context.ApplicationEvent; + +/** + * 流程实例的状态(结果)发生变化的 Event + * + * @author 芋道源码 + */ +@SuppressWarnings("ALL") +@Data +public class BpmProcessInstanceStatusEvent extends ApplicationEvent { + + /** + * 流程实例的编号 + */ + @NotNull(message = "流程实例的编号不能为空") + private String id; + /** + * 流程实例的 key + */ + @NotNull(message = "流程实例的 key 不能为空") + private String processDefinitionKey; + /** + * 流程实例的结果 + */ + @NotNull(message = "流程实例的状态不能为空") + private Integer status; + /** + * 流程实例对应的业务标识 + * 例如说,请假 + */ + private String businessKey; + + public BpmProcessInstanceStatusEvent(Object source) { + super(source); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/event/BpmProcessInstanceStatusEventListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/event/BpmProcessInstanceStatusEventListener.java new file mode 100644 index 0000000..fbaba2e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/event/BpmProcessInstanceStatusEventListener.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.bpm.api.event; + +import cn.hutool.core.util.StrUtil; +import org.springframework.context.ApplicationListener; + +/** + * {@link BpmProcessInstanceStatusEvent} 的监听器 + * + * @author 芋道源码 + */ +public abstract class BpmProcessInstanceStatusEventListener + implements ApplicationListener { + + @Override + public final void onApplicationEvent(BpmProcessInstanceStatusEvent event) { + if (!StrUtil.equals(event.getProcessDefinitionKey(), getProcessDefinitionKey())) { + return; + } + onEvent(event); + } + + /** + * @return 返回监听的流程定义 Key + */ + protected abstract String getProcessDefinitionKey(); + + /** + * 处理事件 + * + * @param event 事件 + */ + protected abstract void onEvent(BpmProcessInstanceStatusEvent event); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/package-info.java new file mode 100644 index 0000000..def88bd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/package-info.java @@ -0,0 +1,4 @@ +/** + * bpm API 包,定义并实现提供给其它模块的 API + */ +package cn.iocoder.yudao.module.bpm.api; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java new file mode 100644 index 0000000..b8ccad6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.bpm.api.task; + +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import jakarta.validation.Valid; + +/** + * 流程实例 Api 接口 + * + * @author 芋道源码 + */ +public interface BpmProcessInstanceApi { + + /** + * 创建流程实例(提供给内部) + * + * @param userId 用户编号 + * @param reqDTO 创建信息 + * @return 实例的编号 + */ + String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO); + + + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java new file mode 100644 index 0000000..5ebb12f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.bpm.api.task; + +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * Flowable 流程实例 Api 实现类 + * + * @author 芋道源码 + * @author jason + */ +@Service +@Validated +public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO) { + return processInstanceService.createProcessInstance(userId, reqDTO); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java new file mode 100644 index 0000000..8f6f425 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.bpm.api.task; + +import jakarta.validation.constraints.NotEmpty; + +/** + * 流程任务 Api 接口 + * + * @author jason + */ +public interface BpmProcessTaskApi { + + /** + * 触发流程任务的执行 + * + * @param processInstanceId 流程实例编号 + * @param taskDefineKey 任务 Key + */ + void triggerTask(@NotEmpty(message = "流程实例的编号不能为空") String processInstanceId, + @NotEmpty(message = "任务 Key 不能为空") String taskDefineKey); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java new file mode 100644 index 0000000..d99aa03 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.bpm.api.task; + +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * 流程任务 Api 实现类 + * + * @author jason + */ +@Service +@Validated +public class BpmProcessTaskApiImpl implements BpmProcessTaskApi { + + @Resource + private BpmTaskService bpmTaskService; + + @Override + public void triggerTask(String processInstanceId, String taskDefineKey) { + bpmTaskService.triggerTask(processInstanceId, taskDefineKey); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java new file mode 100644 index 0000000..b1ac353 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.bpm.api.task.dto; + +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Map; + +/** + * 流程实例的创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class BpmProcessInstanceCreateReqDTO { + + /** + * 流程定义的标识 + */ + @NotEmpty(message = "流程定义的标识不能为空") + private String processDefinitionKey; + /** + * 变量实例(动态表单) + */ + private Map variables; + + /** + * 业务的唯一标识 + * + * 例如说,请假申请的编号。通过它,可以查询到对应的实例 + */ + @NotEmpty(message = "业务的唯一标识") + private String businessKey; + + /** + * 发起人自选审批人 Map + * + * key:taskKey 任务编码 + * value:审批人的数组 + * 例如:{ taskKey1 :[1, 2] },则表示 taskKey1 这个任务,提前设定了,由 userId 为 1,2 的用户进行审批 + */ + private Map> startUserSelectAssignees; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/dept/DeptSimpleBaseVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/dept/DeptSimpleBaseVO.java new file mode 100644 index 0000000..ba8049a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/dept/DeptSimpleBaseVO.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.base.dept; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "部门精简信息 VO") +@Data +public class DeptSimpleBaseVO { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "技术部") + private String name; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/package-info.java new file mode 100644 index 0000000..41ce650 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/package-info.java @@ -0,0 +1,4 @@ +/** + * 基础包,放一些通用的 VO 类 + */ +package cn.iocoder.yudao.module.bpm.controller.admin.base; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java new file mode 100644 index 0000000..b2fb016 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.base.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户精简信息 VO") +@Data +public class UserSimpleBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String nickname; + @Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png") + private String avatar; + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long deptId; + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") + private String deptName; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmCategoryController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmCategoryController.java new file mode 100644 index 0000000..05e571f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmCategoryController.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategoryPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategoryRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategorySaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Comparator; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - BPM 流程分类") +@RestController +@RequestMapping("/bpm/category") +@Validated +public class BpmCategoryController { + + @Resource + private BpmCategoryService categoryService; + + @PostMapping("/create") + @Operation(summary = "创建流程分类") + @PreAuthorize("@ss.hasPermission('bpm:category:create')") + public CommonResult createCategory(@Valid @RequestBody BpmCategorySaveReqVO createReqVO) { + return success(categoryService.createCategory(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新流程分类") + @PreAuthorize("@ss.hasPermission('bpm:category:update')") + public CommonResult updateCategory(@Valid @RequestBody BpmCategorySaveReqVO updateReqVO) { + categoryService.updateCategory(updateReqVO); + return success(true); + } + + @PutMapping("/update-sort-batch") + @Operation(summary = "批量更新流程分类的排序") + @Parameter(name = "ids", description = "分类编号列表", required = true, example = "1,2,3") + @PreAuthorize("@ss.hasPermission('bpm:category:update')") + public CommonResult updateCategorySortBatch(@RequestParam("ids") List ids) { + categoryService.updateCategorySortBatch(ids); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除流程分类") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:category:delete')") + public CommonResult deleteCategory(@RequestParam("id") Long id) { + categoryService.deleteCategory(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得流程分类") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:category:query')") + public CommonResult getCategory(@RequestParam("id") Long id) { + BpmCategoryDO category = categoryService.getCategory(id); + return success(BeanUtils.toBean(category, BpmCategoryRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得流程分类分页") + @PreAuthorize("@ss.hasPermission('bpm:category:query')") + public CommonResult> getCategoryPage(@Valid BpmCategoryPageReqVO pageReqVO) { + PageResult pageResult = categoryService.getCategoryPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, BpmCategoryRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取流程分类的精简信息列表", description = "只包含被开启的分类,主要用于前端的下拉选项") + public CommonResult> getCategorySimpleList() { + List list = categoryService.getCategoryListByStatus(CommonStatusEnum.ENABLE.getStatus()); + list.sort(Comparator.comparingInt(BpmCategoryDO::getSort)); + return success(convertList(list, category -> new BpmCategoryRespVO().setId(category.getId()) + .setName(category.getName()).setCode(category.getCode()))); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmFormController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmFormController.java new file mode 100644 index 0000000..b957a75 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmFormController.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormSaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - 动态表单") +@RestController +@RequestMapping("/bpm/form") +@Validated +public class BpmFormController { + + @Resource + private BpmFormService formService; + + @PostMapping("/create") + @Operation(summary = "创建动态表单") + @PreAuthorize("@ss.hasPermission('bpm:form:create')") + public CommonResult createForm(@Valid @RequestBody BpmFormSaveReqVO createReqVO) { + return success(formService.createForm(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新动态表单") + @PreAuthorize("@ss.hasPermission('bpm:form:update')") + public CommonResult updateForm(@Valid @RequestBody BpmFormSaveReqVO updateReqVO) { + formService.updateForm(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除动态表单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:form:delete')") + public CommonResult deleteForm(@RequestParam("id") Long id) { + formService.deleteForm(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得动态表单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:form:query')") + public CommonResult getForm(@RequestParam("id") Long id) { + BpmFormDO form = formService.getForm(id); + return success(BeanUtils.toBean(form, BpmFormRespVO.class)); + } + + @GetMapping({"/list-all-simple", "/simple-list"}) + @Operation(summary = "获得动态表单的精简列表", description = "用于表单下拉框") + public CommonResult> getFormSimpleList() { + List list = formService.getFormList(); + return success(convertList(list, formDO -> // 只返回 id、name 字段 + new BpmFormRespVO().setId(formDO.getId()).setName(formDO.getName()))); + } + + @GetMapping("/page") + @Operation(summary = "获得动态表单分页") + @PreAuthorize("@ss.hasPermission('bpm:form:query')") + public CommonResult> getFormPage(@Valid BpmFormPageReqVO pageVO) { + PageResult pageResult = formService.getFormPage(pageVO); + return success(BeanUtils.toBean(pageResult, BpmFormRespVO.class)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java new file mode 100644 index 0000000..aa4c484 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java @@ -0,0 +1,200 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO; +import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.Model; +import org.flowable.engine.repository.ProcessDefinition; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 流程模型") +@RestController +@RequestMapping("/bpm/model") +@Validated +public class BpmModelController { + + @Resource + private BpmModelService modelService; + @Resource + private BpmFormService formService; + @Resource + private BpmCategoryService categoryService; + @Resource + private BpmProcessDefinitionService processDefinitionService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @GetMapping("/list") + @Operation(summary = "获得模型分页") + @Parameter(name = "name", description = "模型名称", example = "芋艿") + public CommonResult> getModelList(@RequestParam(value = "name", required = false) String name) { + List list = modelService.getModelList(name); + if (CollUtil.isEmpty(list)) { + return success(Collections.emptyList()); + } + + // 获得 Form 表单 + Set formIds = convertSet(list, model -> { + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); + return metaInfo != null ? metaInfo.getFormId() : null; + }); + Map formMap = formService.getFormMap(formIds); + // 获得 Category Map + Map categoryMap = categoryService.getCategoryMap( + convertSet(list, Model::getCategory)); + // 获得 Deployment Map + Map deploymentMap = processDefinitionService.getDeploymentMap( + convertSet(list, Model::getDeploymentId)); + // 获得 ProcessDefinition Map + List processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds( + deploymentMap.keySet()); + Map processDefinitionMap = convertMap(processDefinitions, ProcessDefinition::getDeploymentId); + // 获得 User Map、Dept Map + Set userIds = convertSetByFlatMap(list, model -> { + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); + return metaInfo != null ? metaInfo.getStartUserIds().stream() : Stream.empty(); + }); + Map userMap = adminUserApi.getUserMap(userIds); + Set deptIds = convertSetByFlatMap(list, model -> { + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); + return metaInfo != null && metaInfo.getStartDeptIds() != null ? metaInfo.getStartDeptIds().stream() : Stream.empty(); + }); + Map deptMap = deptApi.getDeptMap(deptIds); + return success(BpmModelConvert.INSTANCE.buildModelList(list, + formMap, categoryMap, deploymentMap, processDefinitionMap, userMap, deptMap)); + } + + @GetMapping("/get") + @Operation(summary = "获得模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:model:query')") + public CommonResult getModel(@RequestParam("id") String id) { + Model model = modelService.getModel(id); + if (model == null) { + return null; + } + byte[] bpmnBytes = modelService.getModelBpmnXML(id); + BpmSimpleModelNodeVO simpleModel = modelService.getSimpleModel(id); + return success(BpmModelConvert.INSTANCE.buildModel(model, bpmnBytes, simpleModel)); + } + + @PostMapping("/create") + @Operation(summary = "新建模型") + @PreAuthorize("@ss.hasPermission('bpm:model:create')") + public CommonResult createModel(@Valid @RequestBody BpmModelSaveReqVO createRetVO) { + return success(modelService.createModel(createRetVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改模型") + @PreAuthorize("@ss.hasPermission('bpm:model:update')") + public CommonResult updateModel(@Valid @RequestBody BpmModelSaveReqVO modelVO) { + modelService.updateModel(getLoginUserId(), modelVO); + return success(true); + } + + @PutMapping("/update-sort-batch") + @Operation(summary = "批量修改模型排序") + @Parameter(name = "ids", description = "编号数组", required = true, example = "1,2,3") + public CommonResult updateModelSortBatch(@RequestParam("ids") List ids) { + modelService.updateModelSortBatch(getLoginUserId(), ids); + return success(true); + } + + @PostMapping("/deploy") + @Operation(summary = "部署模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:model:deploy')") + public CommonResult deployModel(@RequestParam("id") String id) { + modelService.deployModel(getLoginUserId(), id); + return success(true); + } + + @PutMapping("/update-state") + @Operation(summary = "修改模型的状态", description = "实际更新的部署的流程定义的状态") + @PreAuthorize("@ss.hasPermission('bpm:model:update')") + public CommonResult updateModelState(@Valid @RequestBody BpmModelUpdateStateReqVO reqVO) { + modelService.updateModelState(getLoginUserId(), reqVO.getId(), reqVO.getState()); + return success(true); + } + + @Deprecated + @PutMapping("/update-bpmn") + @Operation(summary = "修改模型的 BPMN") + @PreAuthorize("@ss.hasPermission('bpm:model:update')") + public CommonResult updateModelBpmn(@Valid @RequestBody BpmModeUpdateBpmnReqVO reqVO) { + modelService.updateModelBpmnXml(reqVO.getId(), reqVO.getBpmnXml()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:model:delete')") + public CommonResult deleteModel(@RequestParam("id") String id) { + modelService.deleteModel(getLoginUserId(), id); + return success(true); + } + + @DeleteMapping("/clean") + @Operation(summary = "清理模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:model:clean')") + public CommonResult cleanModel(@RequestParam("id") String id) { + modelService.cleanModel(getLoginUserId(), id); + return success(true); + } + + // ========== 仿钉钉/飞书的精简模型 ========= + + @GetMapping("/simple/get") + @Operation(summary = "获得仿钉钉流程设计模型") + @Parameter(name = "modelId", description = "流程模型编号", required = true, example = "a2c5eee0-eb6c-11ee-abf4-0c37967c420a") + public CommonResult getSimpleModel(@RequestParam("id") String modelId){ + return success(modelService.getSimpleModel(modelId)); + } + + @Deprecated + @PostMapping("/simple/update") + @Operation(summary = "保存仿钉钉流程设计模型") + @PreAuthorize("@ss.hasPermission('bpm:model:update')") + public CommonResult updateSimpleModel(@Valid @RequestBody BpmSimpleModelUpdateReqVO reqVO) { + modelService.updateSimpleModel(getLoginUserId(), reqVO); + return success(Boolean.TRUE); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java new file mode 100644 index 0000000..90c3888 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java @@ -0,0 +1,133 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.ProcessDefinition; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 流程定义") +@RestController +@RequestMapping("/bpm/process-definition") +@Validated +public class BpmProcessDefinitionController { + + @Resource + private BpmProcessDefinitionService processDefinitionService; + @Resource + private BpmFormService formService; + @Resource + private BpmCategoryService categoryService; + + @GetMapping("/page") + @Operation(summary = "获得流程定义分页") + @PreAuthorize("@ss.hasPermission('bpm:process-definition:query')") + public CommonResult> getProcessDefinitionPage( + BpmProcessDefinitionPageReqVO pageReqVO) { + PageResult pageResult = processDefinitionService.getProcessDefinitionPage(pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + + // 获得 Category Map + Map categoryMap = categoryService.getCategoryMap( + convertSet(pageResult.getList(), ProcessDefinition::getCategory)); + // 获得 Deployment Map + Map deploymentMap = processDefinitionService.getDeploymentMap( + convertSet(pageResult.getList(), ProcessDefinition::getDeploymentId)); + // 获得 BpmProcessDefinitionInfoDO Map + Map processDefinitionMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), ProcessDefinition::getId)); + // 获得 Form Map + Map formMap = formService.getFormMap( + convertSet(processDefinitionMap.values(), BpmProcessDefinitionInfoDO::getFormId)); + return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinitionPage( + pageResult, deploymentMap, processDefinitionMap, formMap, categoryMap)); + } + + @GetMapping ("/list") + @Operation(summary = "获得流程定义列表") + @Parameter(name = "suspensionState", description = "挂起状态", required = true, example = "1") // 参见 Flowable SuspensionState 枚举 + public CommonResult> getProcessDefinitionList( + @RequestParam("suspensionState") Integer suspensionState) { + // 1.1 获得开启的流程定义 + List list = processDefinitionService.getProcessDefinitionListBySuspensionState(suspensionState); + if (CollUtil.isEmpty(list)) { + return success(Collections.emptyList()); + } + // 1.2 移除不可见的流程定义 + Map processDefinitionMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(list, ProcessDefinition::getId)); + Long userId = getLoginUserId(); + list.removeIf(processDefinition -> { + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionMap.get(processDefinition.getId()); + return processDefinitionInfo == null // 不存在 + || Boolean.FALSE.equals(processDefinitionInfo.getVisible()) // visible 不可见 + || !processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, userId); // 无权限发起 + }); + + // 2. 拼接 VO 返回 + return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinitionList( + list, null, processDefinitionMap, null, null)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得流程定义精简列表", description = "只包含未挂起的流程,主要用于前端的下拉选项") + public CommonResult> getSimpleProcessDefinitionList() { + // 只查询未挂起的流程 + List list = processDefinitionService.getProcessDefinitionListBySuspensionState( + SuspensionState.ACTIVE.getStateCode()); + // 拼接 VO 返回,只返回 id、name、key + return success(convertList(list, definition -> new BpmProcessDefinitionRespVO() + .setId(definition.getId()).setName(definition.getName()).setKey(definition.getKey()))); + } + + @GetMapping ("/get") + @Operation(summary = "获得流程定义") + @Parameter(name = "id", description = "流程编号", required = true, example = "1024") + @Parameter(name = "key", description = "流程定义标识", required = true, example = "1024") + public CommonResult getProcessDefinition( + @RequestParam(value = "id", required = false) String id, + @RequestParam(value = "key", required = false) String key) { + ProcessDefinition processDefinition = id != null ? processDefinitionService.getProcessDefinition(id) + : processDefinitionService.getActiveProcessDefinition(key); + if (processDefinition == null) { + return success(null); + } + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(processDefinition.getId()); + BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId()); + return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinition( + processDefinition, null, processDefinitionInfo, null, null, bpmnModel)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessExpressionController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessExpressionController.java new file mode 100644 index 0000000..4b119f7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessExpressionController.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionSaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessExpressionDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessExpressionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - BPM 流程表达式") +@RestController +@RequestMapping("/bpm/process-expression") +@Validated +public class BpmProcessExpressionController { + + @Resource + private BpmProcessExpressionService processExpressionService; + + @PostMapping("/create") + @Operation(summary = "创建流程表达式") + @PreAuthorize("@ss.hasPermission('bpm:process-expression:create')") + public CommonResult createProcessExpression(@Valid @RequestBody BpmProcessExpressionSaveReqVO createReqVO) { + return success(processExpressionService.createProcessExpression(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新流程表达式") + @PreAuthorize("@ss.hasPermission('bpm:process-expression:update')") + public CommonResult updateProcessExpression(@Valid @RequestBody BpmProcessExpressionSaveReqVO updateReqVO) { + processExpressionService.updateProcessExpression(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除流程表达式") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:process-expression:delete')") + public CommonResult deleteProcessExpression(@RequestParam("id") Long id) { + processExpressionService.deleteProcessExpression(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得流程表达式") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:process-expression:query')") + public CommonResult getProcessExpression(@RequestParam("id") Long id) { + BpmProcessExpressionDO processExpression = processExpressionService.getProcessExpression(id); + return success(BeanUtils.toBean(processExpression, BpmProcessExpressionRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得流程表达式分页") + @PreAuthorize("@ss.hasPermission('bpm:process-expression:query')") + public CommonResult> getProcessExpressionPage( + @Valid BpmProcessExpressionPageReqVO pageReqVO) { + PageResult pageResult = processExpressionService.getProcessExpressionPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, BpmProcessExpressionRespVO.class)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessListenerController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessListenerController.java new file mode 100644 index 0000000..843f53d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessListenerController.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerSaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessListenerDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessListenerService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - BPM 流程监听器") +@RestController +@RequestMapping("/bpm/process-listener") +@Validated +public class BpmProcessListenerController { + + @Resource + private BpmProcessListenerService processListenerService; + + @PostMapping("/create") + @Operation(summary = "创建流程监听器") + @PreAuthorize("@ss.hasPermission('bpm:process-listener:create')") + public CommonResult createProcessListener(@Valid @RequestBody BpmProcessListenerSaveReqVO createReqVO) { + return success(processListenerService.createProcessListener(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新流程监听器") + @PreAuthorize("@ss.hasPermission('bpm:process-listener:update')") + public CommonResult updateProcessListener(@Valid @RequestBody BpmProcessListenerSaveReqVO updateReqVO) { + processListenerService.updateProcessListener(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除流程监听器") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:process-listener:delete')") + public CommonResult deleteProcessListener(@RequestParam("id") Long id) { + processListenerService.deleteProcessListener(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得流程监听器") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:process-listener:query')") + public CommonResult getProcessListener(@RequestParam("id") Long id) { + BpmProcessListenerDO processListener = processListenerService.getProcessListener(id); + return success(BeanUtils.toBean(processListener, BpmProcessListenerRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得流程监听器分页") + @PreAuthorize("@ss.hasPermission('bpm:process-listener:query')") + public CommonResult> getProcessListenerPage( + @Valid BpmProcessListenerPageReqVO pageReqVO) { + PageResult pageResult = processListenerService.getProcessListenerPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, BpmProcessListenerRespVO.class)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmUserGroupController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmUserGroupController.java new file mode 100644 index 0000000..64b8034 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmUserGroupController.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupSaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - 用户组") +@RestController +@RequestMapping("/bpm/user-group") +@Validated +public class BpmUserGroupController { + + @Resource + private BpmUserGroupService userGroupService; + + @PostMapping("/create") + @Operation(summary = "创建用户组") + @PreAuthorize("@ss.hasPermission('bpm:user-group:create')") + public CommonResult createUserGroup(@Valid @RequestBody BpmUserGroupSaveReqVO createReqVO) { + return success(userGroupService.createUserGroup(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新用户组") + @PreAuthorize("@ss.hasPermission('bpm:user-group:update')") + public CommonResult updateUserGroup(@Valid @RequestBody BpmUserGroupSaveReqVO updateReqVO) { + userGroupService.updateUserGroup(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户组") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:user-group:delete')") + public CommonResult deleteUserGroup(@RequestParam("id") Long id) { + userGroupService.deleteUserGroup(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得用户组") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:user-group:query')") + public CommonResult getUserGroup(@RequestParam("id") Long id) { + BpmUserGroupDO userGroup = userGroupService.getUserGroup(id); + return success(BeanUtils.toBean(userGroup, BpmUserGroupRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得用户组分页") + @PreAuthorize("@ss.hasPermission('bpm:user-group:query')") + public CommonResult> getUserGroupPage(@Valid BpmUserGroupPageReqVO pageVO) { + PageResult pageResult = userGroupService.getUserGroupPage(pageVO); + return success(BeanUtils.toBean(pageResult, BpmUserGroupRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取用户组精简信息列表", description = "只包含被开启的用户组,主要用于前端的下拉选项") + public CommonResult> getUserGroupSimpleList() { + List list = userGroupService.getUserGroupListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, group -> new BpmUserGroupRespVO().setId(group.getId()).setName(group.getName()))); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/category/BpmCategoryPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/category/BpmCategoryPageReqVO.java new file mode 100644 index 0000000..b76e96c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/category/BpmCategoryPageReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - BPM 流程分类分页 Request VO") +@Data +public class BpmCategoryPageReqVO extends PageParam { + + @Schema(description = "分类名", example = "王五") + private String name; + + @Schema(description = "分类标志", example = "OA") + private String code; + + @Schema(description = "分类状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/category/BpmCategoryRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/category/BpmCategoryRespVO.java new file mode 100644 index 0000000..7ada55a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/category/BpmCategoryRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - BPM 流程分类 Response VO") +@Data +public class BpmCategoryRespVO { + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167") + private Long id; + + @Schema(description = "分类名", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + @Schema(description = "分类标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "OA") + private String code; + + @Schema(description = "分类描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + private String description; + + @Schema(description = "分类状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "分类排序", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/category/BpmCategorySaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/category/BpmCategorySaveReqVO.java new file mode 100644 index 0000000..d1a175e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/category/BpmCategorySaveReqVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - BPM 流程分类新增/修改 Request VO") +@Data +public class BpmCategorySaveReqVO { + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3167") + private Long id; + + @Schema(description = "分类名", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @NotEmpty(message = "分类名不能为空") + private String name; + + @Schema(description = "分类描述", example = "你猜") + private String description; + + @Schema(description = "分类标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "OA") + @NotEmpty(message = "分类标志不能为空") + private String code; + + @Schema(description = "分类状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "分类状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "分类排序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "分类排序不能为空") + private Integer sort; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionPageReqVO.java new file mode 100644 index 0000000..37b02f0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionPageReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - BPM 流程表达式分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmProcessExpressionPageReqVO extends PageParam { + + @Schema(description = "表达式名字", example = "李四") + private String name; + + @Schema(description = "表达式状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java new file mode 100644 index 0000000..d877f60 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression; + +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - BPM 流程表达式 Response VO") +@Data +public class BpmProcessExpressionRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3870") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "表达式名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @ExcelProperty("表达式名字") + private String name; + + @Schema(description = "表达式状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "表达式", requiredMode = Schema.RequiredMode.REQUIRED) + private String expression; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionSaveReqVO.java new file mode 100644 index 0000000..9c7301a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionSaveReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - BPM 流程表达式新增/修改 Request VO") +@Data +public class BpmProcessExpressionSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3870") + private Long id; + + @Schema(description = "表达式名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotEmpty(message = "表达式名字不能为空") + private String name; + + @Schema(description = "表达式状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "表达式状态不能为空") + private Integer status; + + @Schema(description = "表达式", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "表达式不能为空") + private String expression; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormFieldVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormFieldVO.java new file mode 100644 index 0000000..65fb305 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormFieldVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form; + +import lombok.Data; + +/** + * 流程表单字段 VO + */ +@Data +public class BpmFormFieldVO { + + /** + * 字段类型 + */ + private String type; + /** + * 字段标识 + */ + private String field; + /** + * 字段标题 + */ + private String title; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormPageReqVO.java new file mode 100644 index 0000000..1657f02 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormPageReqVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 动态表单分页 Request VO") +@Data +public class BpmFormPageReqVO extends PageParam { + + @Schema(description = "表单名称", example = "芋道") + private String name; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormRespVO.java new file mode 100644 index 0000000..1c4eb76 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormRespVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 动态表单 Response VO") +@Data +public class BpmFormRespVO { + + @Schema(description = "表单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "表单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "表单名称不能为空") + private String name; + + @Schema(description = "表单的配置-JSON 字符串", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单的配置不能为空") + private String conf; + + @Schema(description = "表单项的数组-JSON 字符串的数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单项的数组不能为空") + private List fields; + + @Schema(description = "表单状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "表单状态不能为空") + private Integer status; // 参见 CommonStatusEnum 枚举 + + @Schema(description = "备注", example = "我是备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormSaveReqVO.java new file mode 100644 index 0000000..4da13aa --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormSaveReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 动态表单创建/更新 Request VO") +@Data +public class BpmFormSaveReqVO { + + @Schema(description = "表单编号", example = "1024") + private Long id; + + @Schema(description = "表单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "表单名称不能为空") + private String name; + + @Schema(description = "表单的配置-JSON 字符串", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单的配置不能为空") + private String conf; + + @Schema(description = "表单项的数组-JSON 字符串的数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单项的数组不能为空") + private List fields; + + @Schema(description = "表单状态-参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "表单状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "我是备注") + private String remark; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupPageReqVO.java new file mode 100644 index 0000000..234ff28 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupPageReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户组分页 Request VO") +@Data +public class BpmUserGroupPageReqVO extends PageParam { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "组名", example = "芋道") + private String name; + + @Schema(description = "状态", example = "1") + private Integer status; + + @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java new file mode 100644 index 0000000..f20722a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Set; + +@Schema(description = "管理后台 - 用户组 Response VO") +@Data +public class BpmUserGroupRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "组名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String description; + + @Schema(description = "成员编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + private Set userIds; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSaveReqVO.java new file mode 100644 index 0000000..1993cdb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSaveReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +import java.util.Set; + +@Schema(description = "管理后台 - 用户组创建/修改 Request VO") +@Data +public class BpmUserGroupSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "组名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "组名不能为空") + private String name; + + @Schema(description = "描述", example = "芋道源码") + private String description; + + @Schema(description = "成员编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + @NotNull(message = "成员编号数组不能为空") + private Set userIds; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/listener/BpmProcessListenerPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/listener/BpmProcessListenerPageReqVO.java new file mode 100644 index 0000000..d3b9746 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/listener/BpmProcessListenerPageReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - BPM 流程监听器分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmProcessListenerPageReqVO extends PageParam { + + @Schema(description = "监听器名字", example = "赵六") + private String name; + + @Schema(description = "监听器类型", example = "execution") + private String type; + + @Schema(description = "监听事件", example = "start") + private String event; + + @Schema(description = "状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/listener/BpmProcessListenerRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/listener/BpmProcessListenerRespVO.java new file mode 100644 index 0000000..f7a4842 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/listener/BpmProcessListenerRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - BPM 流程监听器 Response VO") +@Data +public class BpmProcessListenerRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13089") + private Long id; + + @Schema(description = "监听器名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "监听器类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "execution") + private String type; + + @Schema(description = "监听器状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "监听事件", requiredMode = Schema.RequiredMode.REQUIRED, example = "start") + private String event; + + @Schema(description = "监听器值类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "class") + private String valueType; + + @Schema(description = "监听器值", requiredMode = Schema.RequiredMode.REQUIRED) + private String value; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/listener/BpmProcessListenerSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/listener/BpmProcessListenerSaveReqVO.java new file mode 100644 index 0000000..f69d0df --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/listener/BpmProcessListenerSaveReqVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - BPM 流程监听器新增/修改 Request VO") +@Data +public class BpmProcessListenerSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13089") + private Long id; + + @Schema(description = "监听器名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @NotEmpty(message = "监听器名字不能为空") + private String name; + + @Schema(description = "监听器类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "execution") + @NotEmpty(message = "监听器类型不能为空") + private String type; + + @Schema(description = "监听器状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "监听器状态不能为空") + private Integer status; + + @Schema(description = "监听事件", requiredMode = Schema.RequiredMode.REQUIRED, example = "start") + @NotEmpty(message = "监听事件不能为空") + private String event; + + @Schema(description = "监听器值类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "class") + @NotEmpty(message = "监听器值类型不能为空") + private String valueType; + + @Schema(description = "监听器值", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "监听器值不能为空") + private String value; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeUpdateBpmnReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeUpdateBpmnReqVO.java new file mode 100644 index 0000000..55d7533 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModeUpdateBpmnReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Schema(description = "管理后台 - 流程模型的更新 BPMN XML Request VO") +@Data +public class BpmModeUpdateBpmnReqVO { + + @Schema(description = "流程编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "流程编号不能为空") + private String id; + + @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "BPMN XML 不能为空") + private String bpmnXml; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java new file mode 100644 index 0000000..d2316f5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java @@ -0,0 +1,180 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import java.util.List; + +/** + * BPM 流程 MetaInfo Response DTO + * 主要用于 { Model#setMetaInfo(String)} 的存储 + * + * 最终,它的字段和 + * {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} + * 是一致的 + * + * @author 芋道源码 + */ +@Data +public class BpmModelMetaInfoVO { + + @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg") + @URL(message = "流程图标格式不正确") + private String icon; + + @Schema(description = "流程描述", example = "我是描述") + private String description; + + @Schema(description = "流程类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(BpmModelTypeEnum.class) + @NotNull(message = "流程类型不能为空") + private Integer type; + + @Schema(description = "表单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(BpmModelFormTypeEnum.class) + @NotNull(message = "表单类型不能为空") + private Integer formType; + @Schema(description = "表单编号", example = "1024") + private Long formId; // formType 为 NORMAL 使用,必须非空 + + @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create") + private String formCustomCreatePath; // 表单类型为 CUSTOM 时,必须非空 + @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view") + private String formCustomViewPath; // 表单类型为 CUSTOM 时,必须非空 + + @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否可见不能为空") + private Boolean visible; + + @Schema(description = "可发起用户编号数组", example = "[1,2,3]") + private List startUserIds; + + @Schema(description = "可发起部门编号数组", example = "[2,4,6]") + private List startDeptIds; + + @Schema(description = "可管理用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[2,4,6]") + @NotEmpty(message = "可管理用户编号数组不能为空") + private List managerUserIds; + + @Schema(description = "排序", example = "1") + private Long sort; // 创建时,后端自动生成 + + @Schema(description = "允许撤销审批中的申请", example = "true") + private Boolean allowCancelRunningProcess; + + @Schema(description = "流程 ID 规则", example = "{}") + private ProcessIdRule processIdRule; + + @Schema(description = "自动去重类型", example = "1") + @InEnum(BpmAutoApproveTypeEnum.class) + private Integer autoApprovalType; + + @Schema(description = "标题设置", example = "{}") + private TitleSetting titleSetting; + + @Schema(description = "摘要设置", example = "{}") + private SummarySetting summarySetting; + + @Schema(description = "流程前置通知设置", example = "{}") + private HttpRequestSetting processBeforeTriggerSetting; + + @Schema(description = "流程后置通知设置", example = "{}") + private HttpRequestSetting processAfterTriggerSetting; + + @Schema(description = "任务前置通知设置", example = "{}") + private HttpRequestSetting taskBeforeTriggerSetting; + + @Schema(description = "任务后置通知设置", example = "{}") + private HttpRequestSetting taskAfterTriggerSetting; + + @Schema(description = "流程 ID 规则") + @Data + @Valid + public static class ProcessIdRule { + + @Schema(description = "是否启用", example = "false") + @NotNull(message = "是否启用不能为空") + private Boolean enable; + + @Schema(description = "前缀", example = "XX") + private String prefix; + + @Schema(description = "中缀", example = "20250120") + private String infix; // 精确到日、精确到时、精确到分、精确到秒 + + @Schema(description = "后缀", example = "YY") + private String postfix; + + @Schema(description = "序列长度", example = "5") + @NotNull(message = "序列长度不能为空") + private Integer length; + + } + + @Schema(description = "标题设置") + @Data + @Valid + public static class TitleSetting { + + @Schema(description = "是否自定义", example = "false") + @NotNull(message = "是否自定义不能为空") + private Boolean enable; + + @Schema(description = "标题", example = "流程标题") + private String title; + + } + + @Schema(description = "摘要设置") + @Data + @Valid + public static class SummarySetting { + + @Schema(description = "是否自定义", example = "false") + @NotNull(message = "是否自定义不能为空") + private Boolean enable; + + @Schema(description = "摘要字段数组", example = "[]") + private List summary; + + } + + @Schema(description = "http 请求通知设置", example = "{}") + @Data + public static class HttpRequestSetting { + + @Schema(description = "请求路径", example = "http://127.0.0.1") + @NotEmpty(message = "请求 URL 不能为空") + @URL(message = "请求 URL 格式不正确") + private String url; + + @Schema(description = "请求头参数设置", example = "[]") + @Valid + private List header; + + @Schema(description = "请求头参数设置", example = "[]") + @Valid + private List body; + + /** + * 请求返回处理设置,用于修改流程表单值 + *

+ * key:表示要修改的流程表单字段名(name) + * value:接口返回的字段名 + */ + @Schema(description = "请求返回处理设置", example = "[]") + private List> response; + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java new file mode 100644 index 0000000..6a9150a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model; + +import cn.iocoder.yudao.module.bpm.controller.admin.base.dept.DeptSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 流程模型 Response VO") +@Data +public class BpmModelRespVO extends BpmModelMetaInfoVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "process_yudao") + private String key; + + @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg") + private String icon; + + @Schema(description = "流程分类编号", example = "1") + private String category; + @Schema(description = "流程分类名字", example = "请假") + private String categoryName; + + @Schema(description = "表单名字", example = "请假表单") + private String formName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "可发起的用户数组") + private List startUsers; + + @Schema(description = "可发起的部门数组") + private List startDepts; + + @Schema(description = "BPMN XML") + private String bpmnXml; + + @Schema(description = "仿钉钉流程设计模型对象") + private BpmSimpleModelNodeVO simpleModel; + + /** + * 最新部署的流程定义 + */ + private BpmProcessDefinitionRespVO processDefinition; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelSaveReqVO.java new file mode 100644 index 0000000..7e32596 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelSaveReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Schema(description = "管理后台 - 流程模型的保存 Request VO") +@Data +public class BpmModelSaveReqVO extends BpmModelMetaInfoVO { + + @Schema(description = "编号", example = "1024") + private String id; + + @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "process_yudao") + @NotEmpty(message = "流程标识不能为空") + private String key; + + @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotEmpty(message = "流程名称不能为空") + private String name; + + @Schema(description = "流程分类", example = "1") + private String category; + + @Schema(description = "BPMN XML") + private String bpmnXml; + + @Schema(description = "仿钉钉流程设计模型对象") + @Valid + private BpmSimpleModelNodeVO simpleModel; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateStateReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateStateReqVO.java new file mode 100644 index 0000000..7c5a356 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateStateReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 流程模型更新状态 Request VO") +@Data +public class BpmModelUpdateStateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private String id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer state; // 参见 Flowable SuspensionState 枚举 + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java new file mode 100644 index 0000000..1cbebe0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java @@ -0,0 +1,526 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.*; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.flowable.bpmn.model.IOParameter; +import org.hibernate.validator.constraints.URL; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO") +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BpmSimpleModelNodeVO { + + @Schema(description = "模型节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartEvent_1") + @NotEmpty(message = "模型节点编号不能为空") + private String id; + + @Schema(description = "模型节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模型节点类型不能为空") + @InEnum(BpmSimpleModelNodeTypeEnum.class) + private Integer type; + + @Schema(description = "模型节点名称", example = "领导审批") + private String name; + + @Schema(description = "节点展示内容", example = "指定成员: 芋道源码") + private String showText; + + @Schema(description = "子节点") + private BpmSimpleModelNodeVO childNode; // 补充说明:在该模型下,子节点有且仅有一个,不会有多个 + + @Schema(description = "候选人策略", example = "30") + @InEnum(BpmTaskCandidateStrategyEnum.class) + private Integer candidateStrategy; // 用于审批,抄送节点 + + @Schema(description = "候选人参数") + private String candidateParam; // 用于审批,抄送节点 + + @Schema(description = "审批节点类型", example = "1") + @InEnum(BpmUserTaskApproveTypeEnum.class) + private Integer approveType; // 用于审批节点 + + @Schema(description = "多人审批方式", example = "1") + @InEnum(BpmUserTaskApproveMethodEnum.class) + private Integer approveMethod; // 用于审批节点 + + @Schema(description = "通过比例", example = "100") + private Integer approveRatio; // 通过比例,当多人审批方式为:多人会签(按通过比例) 需要设置 + + @Schema(description = "表单权限", example = "[]") + private List> fieldsPermission; + + @Schema(description = "操作按钮设置", example = "[]") + private List buttonsSetting; // 用于审批节点 + + @Schema(description = "是否需要签名", example = "false") + private Boolean signEnable; + + @Schema(description = "是否填写审批意见", example = "false") + private Boolean reasonRequire; + + /** + * 审批节点拒绝处理 + */ + private RejectHandler rejectHandler; + + /** + * 审批节点超时处理 + */ + private TimeoutHandler timeoutHandler; + + @Schema(description = "审批节点的审批人与发起人相同时,对应的处理类型", example = "1") + @InEnum(BpmUserTaskAssignStartUserHandlerTypeEnum.class) + private Integer assignStartUserHandlerType; + + /** + * 空处理策略 + */ + private AssignEmptyHandler assignEmptyHandler; + + /** + * 创建任务监听器 + */ + private ListenerHandler taskCreateListener; + /** + * 指派任务监听器 + */ + private ListenerHandler taskAssignListener; + /** + * 完成任务监听器 + */ + private ListenerHandler taskCompleteListener; + + @Schema(description = "延迟器设置", example = "{}") + private DelaySetting delaySetting; + + @Schema(description = "条件节点") + private List conditionNodes; // 补充说明:有且仅有条件、并行、包容分支会使用 + + /** + * 条件节点设置 + */ + private ConditionSetting conditionSetting; // 仅用于条件节点 BpmSimpleModelNodeTypeEnum.CONDITION_NODE + + @Schema(description = "路由分支组", example = "[]") + private List routerGroups; + + @Schema(description = "路由分支默认分支 ID", example = "Flow_xxx", hidden = true) // 由后端生成(不从前端传递),所以 hidden = true + @JsonIgnore + private String routerDefaultFlowId; // 仅用于路由分支节点 BpmSimpleModelNodeType.ROUTER_BRANCH_NODE + + /** + * 触发器节点设置 + */ + private TriggerSetting triggerSetting; + + @Schema(description = "附加节点 Id", example = "UserTask_xxx", hidden = true) // 由后端生成(不从前端传递),所以 hidden = true + @JsonIgnore + private String attachNodeId; // 目前用于触发器节点(HTTP 回调)。需要 UserTask 和 ReceiveTask(附加节点) 来完成 + + /** + * 子流程设置 + */ + private ChildProcessSetting childProcessSetting; + + @Schema(description = "任务监听器") + @Valid + @Data + public static class ListenerHandler { + + @Schema(description = "是否开启任务监听器", example = "false") + @NotNull(message = "是否开启任务监听器不能为空") + private Boolean enable; + + @Schema(description = "请求路径", example = "http://xxxxx") + private String path; + + @Schema(description = "请求头", example = "[]") + private List header; + + @Schema(description = "请求体", example = "[]") + private List body; + + } + + @Schema(description = "HTTP 请求参数设置") + @Data + public static class HttpRequestParam { + + @Schema(description = "值类型", example = "1") + @InEnum(BpmHttpRequestParamTypeEnum.class) + @NotNull(message = "值类型不能为空") + private Integer type; + + @Schema(description = "键", example = "xxx") + @NotEmpty(message = "键不能为空") + private String key; + + @Schema(description = "值", example = "xxx") + @NotEmpty(message = "值不能为空") + private String value; + + } + + @Schema(description = "审批节点拒绝处理策略") + @Data + public static class RejectHandler { + + @Schema(description = "拒绝处理类型", example = "1") + @InEnum(BpmUserTaskRejectHandlerTypeEnum.class) + private Integer type; + + @Schema(description = "任务拒绝后驳回的节点 Id", example = "Activity_1") + private String returnNodeId; + } + + @Schema(description = "审批节点超时处理策略") + @Valid + @Data + public static class TimeoutHandler { + + @Schema(description = "是否开启超时处理", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否开启超时处理不能为空") + private Boolean enable; + + @Schema(description = "任务超时未处理的行为", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "任务超时未处理的行为不能为空") + @InEnum(BpmUserTaskTimeoutHandlerTypeEnum.class) + private Integer type; + + @Schema(description = "超时时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "PT6H") + @NotEmpty(message = "超时时间不能为空") + private String timeDuration; + + @Schema(description = "最大提醒次数", example = "1") + private Integer maxRemindCount; + } + + @Schema(description = "空处理策略") + @Data + @Valid + public static class AssignEmptyHandler { + + @Schema(description = "空处理类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "空处理类型不能为空") + @InEnum(BpmUserTaskAssignEmptyHandlerTypeEnum.class) + private Integer type; + + @Schema(description = "指定人员审批的用户编号数组", example = "1") + private List userIds; + } + + @Schema(description = "操作按钮设置") + @Data + @Valid + public static class OperationButtonSetting { + + // TODO @jason:是不是按钮的标识?id 会和数据库的 id 自增有点模糊,key 标识会更合理一点点哈。 + @Schema(description = "按钮 Id", example = "1") + private Integer id; + + @Schema(description = "显示名称", example = "审批") + private String displayName; + + @Schema(description = "是否启用", example = "true") + private Boolean enable; + } + + @Schema(description = "条件设置") + @Data + @Valid + // 仅用于条件节点 BpmSimpleModelNodeTypeEnum.CONDITION_NODE + public static class ConditionSetting { + + @Schema(description = "条件类型", example = "1") + @InEnum(BpmSimpleModeConditionTypeEnum.class) + private Integer conditionType; + + @Schema(description = "条件表达式", example = "${day>3}") + private String conditionExpression; + + @Schema(description = "是否默认条件", example = "true") + private Boolean defaultFlow; + + /** + * 条件组 + */ + private ConditionGroups conditionGroups; + } + + @Schema(description = "条件组") + @Data + @Valid + public static class ConditionGroups { + + @Schema(description = "条件组下的条件关系是否为与关系", example = "true") + @NotNull(message = "条件关系不能为空") + private Boolean and; + + @Schema(description = "条件组下的条件", example = "[]") + @NotEmpty(message = "条件不能为空") + private List conditions; + } + + @Schema(description = "条件") + @Data + @Valid + public static class Condition { + + @Schema(description = "条件下的规则关系是否为与关系", example = "true") + @NotNull(message = "规则关系不能为空") + private Boolean and; + + @Schema(description = "条件下的规则", example = "[]") + @NotEmpty(message = "规则不能为空") + private List rules; + } + + @Schema(description = "条件规则") + @Data + @Valid + public static class ConditionRule { + + @Schema(description = "运行符号", example = "==") + @NotEmpty(message = "运行符号不能为空") + private String opCode; + + @Schema(description = "运算符左边的值,例如某个流程变量", example = "startUserId") + @NotEmpty(message = "运算符左边的值不能为空") + private String leftSide; + + @Schema(description = "运算符右边的值", example = "1") + @NotEmpty(message = "运算符右边的值不能为空") + private String rightSide; + } + + @Schema(description = "延迟器") + @Data + @Valid + public static class DelaySetting { + + @Schema(description = "延迟时间类型", example = "1") + @NotNull(message = "延迟时间类型不能为空") + @InEnum(BpmDelayTimerTypeEnum.class) + private Integer delayType; + + @Schema(description = "延迟时间表达式", example = "PT1H,2025-01-01T00:00:00") + @NotEmpty(message = "延迟时间表达式不能为空") + private String delayTime; + } + + @Schema(description = "路由分支") + @Data + @Valid + public static class RouterSetting { + + @Schema(description = "节点 Id", example = "Activity_xxx") // 跳转到该节点 + @NotEmpty(message = "节点 Id 不能为空") + private String nodeId; + + @Schema(description = "条件类型", example = "1") + @InEnum(BpmSimpleModeConditionTypeEnum.class) + @NotNull(message = "条件类型不能为空") + private Integer conditionType; + + @Schema(description = "条件表达式", example = "${day>3}") + private String conditionExpression; + + @Schema(description = "条件组", example = "{}") + private ConditionGroups conditionGroups; + } + + @Schema(description = "触发器节点配置") + @Data + @Valid + public static class TriggerSetting { + + @Schema(description = "触发器类型", example = "1") + @InEnum(BpmTriggerTypeEnum.class) + @NotNull(message = "触发器类型不能为空") + private Integer type; + + /** + * http 请求触发器设置 + */ + @Valid + private HttpRequestTriggerSetting httpRequestSetting; + + /** + * 流程表单触发器设置 + */ + private List formSettings; + + @Schema(description = "http 请求触发器设置", example = "{}") + @Data + public static class HttpRequestTriggerSetting { + + @Schema(description = "请求路径", example = "http://127.0.0.1") + @NotEmpty(message = "请求 URL 不能为空") + @URL(message = "请求 URL 格式不正确") + private String url; + + @Schema(description = "请求头参数设置", example = "[]") + @Valid + private List header; + + @Schema(description = "请求头参数设置", example = "[]") + @Valid + private List body; + + /** + * 请求返回处理设置,用于修改流程表单值 + *

+ * key:表示要修改的流程表单字段名(name) + * value:接口返回的字段名 + */ + @Schema(description = "请求返回处理设置", example = "[]") + private List> response; + + /** + * Http 回调请求,需要指定回调任务 Key,用于回调执行 + */ + @Schema(description = "回调任务 Key", example = "xxx", hidden = true) + private String callbackTaskDefineKey; + + } + + @Schema(description = "流程表单触发器设置", example = "{}") + @Data + public static class FormTriggerSetting { + + @Schema(description = "条件类型", example = "1") + @InEnum(BpmSimpleModeConditionTypeEnum.class) + private Integer conditionType; + + @Schema(description = "条件表达式", example = "${day>3}") + private String conditionExpression; + + @Schema(description = "条件组", example = "{}") + private ConditionGroups conditionGroups; + + @Schema(description = "修改的表单字段", example = "{}") + private Map updateFormFields; + + @Schema(description = "删除表单字段", example = "[]") + private Set deleteFields; + } + } + + @Schema(description = "子流程节点配置") + @Data + @Valid + public static class ChildProcessSetting { + + @Schema(description = "被调用流程", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx") + @NotEmpty(message = "被调用流程不能为空") + private String calledProcessDefinitionKey; + + @Schema(description = "被调用流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx") + @NotEmpty(message = "被调用流程名称不能为空") + private String calledProcessDefinitionName; + + @Schema(description = "是否异步", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否异步不能为空") + private Boolean async; + + @Schema(description = "输入参数(主->子)", example = "[]") + private List inVariables; + + @Schema(description = "输出参数(子->主)", example = "[]") + private List outVariables; + + @Schema(description = "是否自动跳过子流程发起节点", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否自动跳过子流程发起节点不能为空") + private Boolean skipStartUserNode; + + @Schema(description = "子流程发起人配置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + @NotNull(message = "子流程发起人配置不能为空") + private StartUserSetting startUserSetting; + + @Schema(description = "超时设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private TimeoutSetting timeoutSetting; + + @Schema(description = "多实例设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private MultiInstanceSetting multiInstanceSetting; + + @Schema(description = "子流程发起人配置") + @Data + @Valid + public static class StartUserSetting { + + @Schema(description = "子流程发起人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "子流程发起人类型") + @InEnum(BpmChildProcessStartUserTypeEnum.class) + private Integer type; + + @Schema(description = "表单", example = "xxx") + private String formField; + + @Schema(description = "当子流程发起人为空时类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "当子流程发起人为空时类型不能为空") + @InEnum(BpmChildProcessStartUserEmptyTypeEnum.class) + private Integer emptyType; + + } + + @Schema(description = "超时设置") + @Data + @Valid + public static class TimeoutSetting { + + @Schema(description = "是否开启超时设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否开启超时设置不能为空") + private Boolean enable; + + @Schema(description = "时间类型", example = "1") + @InEnum(BpmDelayTimerTypeEnum.class) + private Integer type; + + @Schema(description = "时间表达式", example = "PT1H,2025-01-01T00:00:00") + private String timeExpression; + + } + + @Schema(description = "多实例设置") + @Data + @Valid + public static class MultiInstanceSetting { + + @Schema(description = "是否开启多实例", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否开启多实例不能为空") + private Boolean enable; + + @Schema(description = "是否串行", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否串行不能为空") + private Boolean sequential; + + @Schema(description = "完成比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "完成比例不能为空") + private Integer approveRatio; + + @Schema(description = "多实例来源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "多实例来源类型不能为空") + @InEnum(BpmChildProcessMultiInstanceSourceTypeEnum.class) + private Integer sourceType; + + @Schema(description = "多实例来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "多实例来源不能为空") + private String source; + + } + + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelUpdateReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelUpdateReqVO.java new file mode 100644 index 0000000..10d9672 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelUpdateReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +// TODO @jason:需要考虑,如果某个节点的配置不正确,需要有提示;具体怎么实现,可以讨论下; +@Schema(description = "管理后台 - 仿钉钉流程设计模型的新增/修改 Request VO") +@Data +public class BpmSimpleModelUpdateReqVO { + + @Schema(description = "流程模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotEmpty(message = "流程模型编号不能为空") + private String id; // 对应 Flowable act_re_model 表 ID_ 字段 + + @Schema(description = "仿钉钉流程设计模型对象", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "仿钉钉流程设计模型对象不能为空") + @Valid + private BpmSimpleModelNodeVO simpleModel; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionPageReqVO.java new file mode 100644 index 0000000..828654f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionPageReqVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 流程定义分页 Request VO") +@Data +public class BpmProcessDefinitionPageReqVO extends PageParam { + + @Schema(description = "标识-精准匹配", example = "process1641042089407") + private String key; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java new file mode 100644 index 0000000..0ec7371 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 流程定义 Response VO") +@Data +public class BpmProcessDefinitionRespVO extends BpmModelMetaInfoVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer version; + + @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "youdao") + private String key; + + @Schema(description = "流程分类", example = "1") + private String category; + @Schema(description = "流程分类名字", example = "请假") + private String categoryName; + + @Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer modelType; // 参见 BpmModelTypeEnum 枚举类 + + @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC") + private String modelId; + + @Schema(description = "表单的配置-JSON 字符串。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) + private String formConf; + @Schema(description = "表单项的数组-JSON 字符串的数组。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) + private List formFields; + @Schema(description = "表单名字", example = "请假表单") + private String formName; + + @Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer suspensionState; // 参见 SuspensionState 枚举 + + @Schema(description = "部署时间") + private LocalDateTime deploymentTime; // 需要从对应的 Deployment 读取,非必须返回 + + @Schema(description = "BPMN XML") + private String bpmnXml; // 需要从对应的 BpmnModel 读取,非必须返回 + + @Schema(description = "SIMPLE 设计器模型数据 json 格式") + private String simpleModel; // 非必须返回 + + @Schema(description = "流程定义排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long sort; + + @Schema(description = "BPMN UserTask 用户任务") + @Data + public static class UserTask { + + @Schema(description = "任务标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "sudo") + private String id; + + @Schema(description = "任务名", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/BpmOALeaveController.http b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/BpmOALeaveController.http new file mode 100644 index 0000000..96bbf96 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/BpmOALeaveController.http @@ -0,0 +1,12 @@ +### 请求 /bpm/oa/leave/create 接口 => 成功 +POST {{baseUrl}}/bpm/oa/leave/create +Content-Type: application/json +tenant-id: 1 +Authorization: Bearer {{token}} + +{ + "startTime": "2022-03-01", + "endTime": "2022-03-05", + "type": 1, + "reason": "我要请假啦啦啦!" +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/BpmOALeaveController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/BpmOALeaveController.java new file mode 100644 index 0000000..1447494 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/BpmOALeaveController.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.oa; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeaveCreateReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeavePageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeaveRespVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.BpmOALeaveDO; +import cn.iocoder.yudao.module.bpm.service.oa.BpmOALeaveService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * OA 请假申请 Controller,用于演示自己存储数据,接入工作流的例子 + * + * @author jason + * @author 芋道源码 + */ +@Tag(name = "管理后台 - OA 请假申请") +@RestController +@RequestMapping("/bpm/oa/leave") +@Validated +public class BpmOALeaveController { + + @Resource + private BpmOALeaveService leaveService; + + @PostMapping("/create") + @PreAuthorize("@ss.hasPermission('bpm:oa-leave:create')") + @Operation(summary = "创建请求申请") + public CommonResult createLeave(@Valid @RequestBody BpmOALeaveCreateReqVO createReqVO) { + return success(leaveService.createLeave(getLoginUserId(), createReqVO)); + } + + @GetMapping("/get") + @PreAuthorize("@ss.hasPermission('bpm:oa-leave:query')") + @Operation(summary = "获得请假申请") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult getLeave(@RequestParam("id") Long id) { + BpmOALeaveDO leave = leaveService.getLeave(id); + return success(BeanUtils.toBean(leave, BpmOALeaveRespVO.class)); + } + + @GetMapping("/page") + @PreAuthorize("@ss.hasPermission('bpm:oa-leave:query')") + @Operation(summary = "获得请假申请分页") + public CommonResult> getLeavePage(@Valid BpmOALeavePageReqVO pageVO) { + PageResult pageResult = leaveService.getLeavePage(getLoginUserId(), pageVO); + return success(BeanUtils.toBean(pageResult, BpmOALeaveRespVO.class)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/package-info.java new file mode 100644 index 0000000..7028708 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/package-info.java @@ -0,0 +1,5 @@ +/** + * OA 示例,用于演示外部业务接入 BPM 工作流的示例 + * 一般的接入方式,只需要调用 接口,后续 Admin 用户在管理后台的【待办事务】进行审批 + */ +package cn.iocoder.yudao.module.bpm.controller.admin.oa; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/BpmOALeaveCreateReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/BpmOALeaveCreateReqVO.java new file mode 100644 index 0000000..856a225 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/BpmOALeaveCreateReqVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.oa.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 请假申请创建 Request VO") +@Data +public class BpmOALeaveCreateReqVO { + + @Schema(description = "请假的开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开始时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime startTime; + + @Schema(description = "请假的结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "结束时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime endTime; + + @Schema(description = "请假类型-参见 bpm_oa_type 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "阅读芋道源码") + private String reason; + + @Schema(description = "发起人自选审批人 Map", example = "{taskKey1: [1, 2]}") + private Map> startUserSelectAssignees; + + @AssertTrue(message = "结束时间,需要在开始时间之后") + public boolean isEndTimeValid() { + return !getEndTime().isBefore(getStartTime()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/BpmOALeavePageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/BpmOALeavePageReqVO.java new file mode 100644 index 0000000..3777564 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/BpmOALeavePageReqVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.oa.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 请假申请分页 Request VO") +@Data +public class BpmOALeavePageReqVO extends PageParam { + + @Schema(description = "状态", example = "1") + private Integer status; // 参见 BpmProcessInstanceResultEnum 枚举 + + @Schema(description = "请假类型,参见 bpm_oa_type", example = "1") + private Integer type; + + @Schema(description = "原因,模糊匹配", example = "阅读芋道源码") + private String reason; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "申请时间") + private LocalDateTime[] createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/BpmOALeaveRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/BpmOALeaveRespVO.java new file mode 100644 index 0000000..dbfe9d9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/BpmOALeaveRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.oa.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 请假申请 Response VO") +@Data +public class BpmOALeaveRespVO { + + @Schema(description = "请假表单主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "请假类型,参见 bpm_oa_type 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "阅读芋道源码") + private String reason; + + @Schema(description = "申请时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "请假的开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime startTime; + + @Schema(description = "请假的结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + @Schema(description = "流程编号") + private String processInstanceId; + + @Schema(description = "审批结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; // 参见 BpmProcessInstanceStatusEnum 枚举 + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.http b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.http new file mode 100644 index 0000000..c690827 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.http @@ -0,0 +1,16 @@ +### 请求 /bpm/process-instance/get-bpmn 接口 => 成功 +GET {{baseUrl}}/bpm/process-instance/get-bpmn-model-view?id=1d5fb5a6-85f8-11ef-b717-7e93075f94e3 +Content-Type: application/json +tenant-id: 1 +Authorization: Bearer {{token}} + +### 请求 /bpm/process-instance/get-bpmn 接口 => 失败 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=1d5fb5a6-85f8-11ef-b717-7e93075f94e3 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=3ee5c5ba-904a-11ef-a76e-b2ed5d6ef911 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=f630dfa2-8f92-11ef-947c-ba5e239a6eb4 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=9de8bdbf-9133-11ef-ae97-eaf49df1f932 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=dd2188eb-9394-11ef-a039-7a9ac3d9eb6b +GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processDefinitionId=test-auto:1:c70a799a-9394-11ef-a039-7a9ac3d9eb6b +Content-Type: application/json +tenant-id: 1 +Authorization: Bearer {{token}} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java new file mode 100644 index 0000000..8a37777 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java @@ -0,0 +1,199 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; +import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.task.api.Task; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请” +@RestController +@RequestMapping("/bpm/process-instance") +@Validated +public class BpmProcessInstanceController { + + @Resource + private BpmProcessInstanceService processInstanceService; + @Resource + private BpmTaskService taskService; + @Resource + private BpmProcessDefinitionService processDefinitionService; + @Resource + private BpmCategoryService categoryService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @GetMapping("/my-page") + @Operation(summary = "获得我的实例分页列表", description = "在【我的流程】菜单中,进行调用") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + public CommonResult> getProcessInstanceMyPage( + @Valid BpmProcessInstancePageReqVO pageReqVO) { + PageResult pageResult = processInstanceService.getProcessInstancePage( + getLoginUserId(), pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + + // 拼接返回 + Map> taskMap = taskService.getTaskMapByProcessInstanceIds( + convertList(pageResult.getList(), HistoricProcessInstance::getId)); + Map processDefinitionMap = processDefinitionService.getProcessDefinitionMap( + convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); + Map categoryMap = categoryService.getCategoryMap( + convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory)); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); + Set userIds = convertSet(pageResult.getList(), processInstance -> NumberUtils.parseLong(processInstance.getStartUserId())); + userIds.addAll(convertSetByFlatMap(taskMap.values(), + tasks -> tasks.stream().map(Task::getAssignee).filter(StrUtil::isNotBlank).map(Long::parseLong))); + Map userMap = adminUserApi.getUserMap(userIds); + Map deptMap = deptApi.getDeptMap( + convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult, + processDefinitionMap, categoryMap, taskMap, userMap, deptMap, processDefinitionInfoMap)); + } + + @GetMapping("/manager-page") + @Operation(summary = "获得管理流程实例的分页列表", description = "在【流程实例】菜单中,进行调用") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:manager-query')") + public CommonResult> getProcessInstanceManagerPage( + @Valid BpmProcessInstancePageReqVO pageReqVO) { + PageResult pageResult = processInstanceService.getProcessInstancePage( + null, pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty(pageResult.getTotal())); + } + + // 拼接返回 + Map> taskMap = taskService.getTaskMapByProcessInstanceIds( + convertList(pageResult.getList(), HistoricProcessInstance::getId)); + Map processDefinitionMap = processDefinitionService.getProcessDefinitionMap( + convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); + Map categoryMap = categoryService.getCategoryMap( + convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory)); + // 发起人信息 + Map userMap = adminUserApi.getUserMap( + convertSet(pageResult.getList(), processInstance -> NumberUtils.parseLong(processInstance.getStartUserId()))); + Map deptMap = deptApi.getDeptMap( + convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); + return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult, + processDefinitionMap, categoryMap, taskMap, userMap, deptMap, processDefinitionInfoMap)); + } + + @PostMapping("/create") + @Operation(summary = "新建流程实例") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + public CommonResult createProcessInstance(@Valid @RequestBody BpmProcessInstanceCreateReqVO createReqVO) { + return success(processInstanceService.createProcessInstance(getLoginUserId(), createReqVO)); + } + + @GetMapping("/get") + @Operation(summary = "获得指定流程实例", description = "在【流程详细】界面中,进行调用") + @Parameter(name = "id", description = "流程实例的编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + public CommonResult getProcessInstance(@RequestParam("id") String id) { + HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(id); + if (processInstance == null) { + return success(null); + } + + // 拼接返回 + ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( + processInstance.getProcessDefinitionId()); + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo( + processInstance.getProcessDefinitionId()); + AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())); + DeptRespDTO dept = null; + if (startUser != null && startUser.getDeptId() != null) { + dept = deptApi.getDept(startUser.getDeptId()); + } + return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance, + processDefinition, processDefinitionInfo, startUser, dept)); + } + + @DeleteMapping("/cancel-by-start-user") + @Operation(summary = "用户取消流程实例", description = "取消发起的流程") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel')") + public CommonResult cancelProcessInstanceByStartUser( + @Valid @RequestBody BpmProcessInstanceCancelReqVO cancelReqVO) { + processInstanceService.cancelProcessInstanceByStartUser(getLoginUserId(), cancelReqVO); + return success(true); + } + + @DeleteMapping("/cancel-by-admin") + @Operation(summary = "管理员取消流程实例", description = "管理员撤回流程") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel-by-admin')") + public CommonResult cancelProcessInstanceByManager( + @Valid @RequestBody BpmProcessInstanceCancelReqVO cancelReqVO) { + processInstanceService.cancelProcessInstanceByAdmin(getLoginUserId(), cancelReqVO); + return success(true); + } + + @GetMapping("/get-approval-detail") + @Operation(summary = "获得审批详情") + @Parameter(name = "id", description = "流程实例的编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + @SuppressWarnings("unchecked") + public CommonResult getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) { + if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) { + reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class)); + } + return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO)); + } + + @GetMapping("/get-next-approval-nodes") + @Operation(summary = "获取下一个执行的流程节点") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + @SuppressWarnings("unchecked") + public CommonResult> getNextApprovalNodes(@Valid BpmApprovalDetailReqVO reqVO) { + if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) { + reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class)); + } + return success(processInstanceService.getNextApprovalNodes(getLoginUserId(), reqVO)); + } + + @GetMapping("/get-bpmn-model-view") + @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用") + @Parameter(name = "id", description = "流程实例的编号", required = true) + public CommonResult getProcessInstanceBpmnModelView(@RequestParam(value = "id") String id) { + return success(processInstanceService.getProcessInstanceBpmnModelView(id)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java new file mode 100644 index 0000000..89bba8e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.cc.BpmProcessInstanceCopyRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.flowable.engine.history.HistoricProcessInstance; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 流程实例抄送") +@RestController +@RequestMapping("/bpm/process-instance/copy") +@Validated +public class BpmProcessInstanceCopyController { + + @Resource + private BpmProcessInstanceCopyService processInstanceCopyService; + @Resource + private BpmProcessInstanceService processInstanceService; + @Resource + private BpmProcessDefinitionService processDefinitionService; + + @Resource + private AdminUserApi adminUserApi; + + @GetMapping("/page") + @Operation(summary = "获得抄送流程分页列表") + @PreAuthorize("@ss.hasPermission('bpm:process-instance-cc:query')") + public CommonResult> getProcessInstanceCopyPage( + @Valid BpmProcessInstanceCopyPageReqVO pageReqVO) { + PageResult pageResult = processInstanceCopyService.getProcessInstanceCopyPage( + getLoginUserId(), pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); + } + + // 拼接返回 + Map processInstanceMap = processInstanceService.getHistoricProcessInstanceMap( + convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId)); + Map userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(), + copy -> Stream.of(copy.getStartUserId(), Long.parseLong(copy.getCreator())))); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessDefinitionId)); + return success(convertPage(pageResult, copy -> { + BpmProcessInstanceCopyRespVO copyVO = BeanUtils.toBean(copy, BpmProcessInstanceCopyRespVO.class); + MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()), + user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class))); + MapUtils.findAndThen(userMap, copy.getStartUserId(), + user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class))); + MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(), + processInstance -> { + copyVO.setSummary(FlowableUtils.getSummary( + processDefinitionInfoMap.get(processInstance.getProcessDefinitionId()), + processInstance.getProcessVariables())); + copyVO.setProcessInstanceStartTime(DateUtils.of(processInstance.getStartTime())); + }); + return copyVO; + })); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java new file mode 100644 index 0000000..b796c5c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java @@ -0,0 +1,239 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; +import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 流程任务实例") +@RestController +@RequestMapping("/bpm/task") +@Validated +public class BpmTaskController { + + @Resource + private BpmTaskService taskService; + @Resource + private BpmProcessInstanceService processInstanceService; + @Resource + private BpmFormService formService; + @Resource + private BpmProcessDefinitionService processDefinitionService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @GetMapping("todo-page") + @Operation(summary = "获取 Todo 待办任务分页") + @PreAuthorize("@ss.hasPermission('bpm:task:query')") + public CommonResult> getTaskTodoPage(@Valid BpmTaskPageReqVO pageVO) { + PageResult pageResult = taskService.getTaskTodoPage(getLoginUserId(), pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 拼接数据 + Map processInstanceMap = processInstanceService.getProcessInstanceMap( + convertSet(pageResult.getList(), Task::getProcessInstanceId)); + Map userMap = adminUserApi.getUserMap( + convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), Task::getProcessDefinitionId)); + return success(BpmTaskConvert.INSTANCE.buildTodoTaskPage(pageResult, processInstanceMap, userMap, processDefinitionInfoMap)); + } + + @GetMapping("done-page") + @Operation(summary = "获取 Done 已办任务分页") + @PreAuthorize("@ss.hasPermission('bpm:task:query')") + public CommonResult> getTaskDonePage(@Valid BpmTaskPageReqVO pageVO) { + PageResult pageResult = taskService.getTaskDonePage(getLoginUserId(), pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 拼接数据 + Map processInstanceMap = processInstanceService.getHistoricProcessInstanceMap( + convertSet(pageResult.getList(), HistoricTaskInstance::getProcessInstanceId)); + Map userMap = adminUserApi.getUserMap( + convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), HistoricTaskInstance::getProcessDefinitionId)); + return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, null, processDefinitionInfoMap)); + } + + @GetMapping("manager-page") + @Operation(summary = "获取全部任务的分页", description = "用于【流程任务】菜单") + @PreAuthorize("@ss.hasPermission('bpm:task:mananger-query')") + public CommonResult> getTaskManagerPage(@Valid BpmTaskPageReqVO pageVO) { + PageResult pageResult = taskService.getTaskPage(getLoginUserId(), pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 拼接数据 + Map processInstanceMap = processInstanceService.getHistoricProcessInstanceMap( + convertSet(pageResult.getList(), HistoricTaskInstance::getProcessInstanceId)); + // 获得 User 和 Dept Map + Set userIds = convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId())); + userIds.addAll(convertSet(pageResult.getList(), task -> NumberUtils.parseLong(task.getAssignee()))); + Map userMap = adminUserApi.getUserMap(userIds); + Map deptMap = deptApi.getDeptMap( + convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), HistoricTaskInstance::getProcessDefinitionId)); + return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, deptMap, processDefinitionInfoMap)); + } + + @GetMapping("/list-by-process-instance-id") + @Operation(summary = "获得指定流程实例的任务列表", description = "包括完成的、未完成的") + @Parameter(name = "processInstanceId", description = "流程实例的编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:task:query')") + public CommonResult> getTaskListByProcessInstanceId( + @RequestParam("processInstanceId") String processInstanceId) { + List taskList = taskService.getTaskListByProcessInstanceId(processInstanceId, true); + if (CollUtil.isEmpty(taskList)) { + return success(Collections.emptyList()); + } + + // 拼接数据 + Set userIds = convertSetByFlatMap(taskList, task -> + Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner()))); + Map userMap = adminUserApi.getUserMap(userIds); + Map deptMap = deptApi.getDeptMap( + convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + // 获得 Form Map + Map formMap = formService.getFormMap( + convertSet(taskList, task -> NumberUtils.parseLong(task.getFormKey()))); + return success(BpmTaskConvert.INSTANCE.buildTaskListByProcessInstanceId(taskList, + formMap, userMap, deptMap)); + } + + @PutMapping("/approve") + @Operation(summary = "通过任务") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult approveTask(@Valid @RequestBody BpmTaskApproveReqVO reqVO) { + taskService.approveTask(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/reject") + @Operation(summary = "不通过任务") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult rejectTask(@Valid @RequestBody BpmTaskRejectReqVO reqVO) { + taskService.rejectTask(getLoginUserId(), reqVO); + return success(true); + } + + @GetMapping("/list-by-return") + @Operation(summary = "获取所有可退回的节点", description = "用于【流程详情】的【退回】按钮") + @Parameter(name = "taskId", description = "当前任务ID", required = true) + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult> getTaskListByReturn(@RequestParam("id") String id) { + List userTaskList = taskService.getUserTaskListByReturn(id); + return success(convertList(userTaskList, userTask -> // 只返回 id 和 name + new BpmTaskRespVO().setName(userTask.getName()).setTaskDefinitionKey(userTask.getId()))); + } + + @PutMapping("/return") + @Operation(summary = "退回任务", description = "用于【流程详情】的【退回】按钮") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult returnTask(@Valid @RequestBody BpmTaskReturnReqVO reqVO) { + taskService.returnTask(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/delegate") + @Operation(summary = "委派任务", description = "用于【流程详情】的【委派】按钮") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult delegateTask(@Valid @RequestBody BpmTaskDelegateReqVO reqVO) { + taskService.delegateTask(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/transfer") + @Operation(summary = "转派任务", description = "用于【流程详情】的【转派】按钮") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult transferTask(@Valid @RequestBody BpmTaskTransferReqVO reqVO) { + taskService.transferTask(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/create-sign") + @Operation(summary = "加签", description = "before 前加签,after 后加签") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult createSignTask(@Valid @RequestBody BpmTaskSignCreateReqVO reqVO) { + taskService.createSignTask(getLoginUserId(), reqVO); + return success(true); + } + + @DeleteMapping("/delete-sign") + @Operation(summary = "减签") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult deleteSignTask(@Valid @RequestBody BpmTaskSignDeleteReqVO reqVO) { + taskService.deleteSignTask(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/copy") + @Operation(summary = "抄送任务") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult copyTask(@Valid @RequestBody BpmTaskCopyReqVO reqVO) { + taskService.copyTask(getLoginUserId(), reqVO); + return success(true); + } + + @GetMapping("/list-by-parent-task-id") + @Operation(summary = "获得指定父级任务的子任务列表") // 目前用于,减签的时候,获得子任务列表 + @Parameter(name = "parentTaskId", description = "父级任务编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:task:query')") + public CommonResult> getTaskListByParentTaskId(@RequestParam("parentTaskId") String parentTaskId) { + List taskList = taskService.getTaskListByParentTaskId(parentTaskId); + if (CollUtil.isEmpty(taskList)) { + return success(Collections.emptyList()); + } + // 拼接数据 + Map userMap = adminUserApi.getUserMap(convertSetByFlatMap(taskList, + user -> Stream.of(NumberUtils.parseLong(user.getAssignee()), NumberUtils.parseLong(user.getOwner())))); + Map deptMap = deptApi.getDeptMap( + convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + return success(BpmTaskConvert.INSTANCE.buildTaskListByParentTaskId(taskList, userMap, deptMap)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/activity/BpmActivityRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/activity/BpmActivityRespVO.java new file mode 100644 index 0000000..8f959e0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/activity/BpmActivityRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 流程活动的 Response VO") +@Data +public class BpmActivityRespVO { + + @Schema(description = "流程活动的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String key; + @Schema(description = "流程活动的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartEvent") + private String type; + + @Schema(description = "流程活动的开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime startTime; + @Schema(description = "流程活动的结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + @Schema(description = "关联的流程任务的编号", example = "2048") + private String taskId; // 关联的流程任务,只有 UserTask 等类型才有 + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java new file mode 100644 index 0000000..a7e194b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.cc; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 流程实例抄送的分页 Item Response VO") +@Data +public class BpmProcessInstanceCopyRespVO { + + @Schema(description = "抄送主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "发起人", requiredMode = Schema.RequiredMode.REQUIRED) + private UserSimpleBaseVO startUser; + + @Schema(description = "流程实例编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "A233") + private String processInstanceId; + @Schema(description = "流程实例的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试") + private String processInstanceName; + @Schema(description = "流程实例的发起时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime processInstanceStartTime; + + @Schema(description = "流程活动的编号", requiredMode = Schema.RequiredMode.REQUIRED) + private String activityId; + @Schema(description = "流程活动的名字", requiredMode = Schema.RequiredMode.REQUIRED) + private String activityName; + + @Schema(description = "流程活动的编号") + private String taskId; + + @Schema(description = "抄送人意见") + private String reason; + + @Schema(description = "创建人", requiredMode = Schema.RequiredMode.REQUIRED) + private UserSimpleBaseVO createUser; + + @Schema(description = "抄送时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "流程摘要", example = "[]") + private List> summary; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java new file mode 100644 index 0000000..069ec20 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import lombok.Data; + +import java.util.Map; + +@Schema(description = "管理后台 - 审批详情 Request VO") +@Data +public class BpmApprovalDetailReqVO { + + @Schema(description = "流程定义的编号", example = "1024") + private String processDefinitionId; // 使用场景:发起流程时,传流程定义 ID + + @Schema(description = "流程变量") + private Map processVariables; // 使用场景:同 processDefinitionId,用于流程预测 + + @Schema(description = "流程变量") + private String processVariablesStr; // 解决 GET 无法传递对象的问题,最终转换成 processVariables 变量 + + @Schema(description = "流程实例的编号", example = "1024") + private String processInstanceId; // 使用场景:流程已发起时候传流程实例 ID + + // TODO @芋艿:如果未来 BPMN 增加流程图,它没有发起人节点,会有问题。 + @Schema(description = "流程活动编号", example = "StartUserNode") + private String activityId; // 用于获取表单权限。1)发起流程时,传“发起人节点” activityId 可获取发起人的表单权限;2)从抄送列表界面进来时,传抄送的 activityId 可获取抄送人的表单权限; + + @Schema(description = "流程任务编号", example = "95f2f08b-621b-11ef-bf39-00ff4722db8b") + private String taskId; // 用于获取表单权限。1)从待审批/已审批界面进来时,传递 taskId 任务编号,可获取任务节点的变得权限 + + @AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空") + @JsonIgnore + public boolean isValidProcessParam() { + return StrUtil.isNotEmpty(processDefinitionId) || StrUtil.isNotEmpty(processInstanceId); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java new file mode 100644 index 0000000..38c2bc1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java @@ -0,0 +1,112 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + + +@Schema(description = "管理后台 - 审批详情 Response VO") +@Data +public class BpmApprovalDetailRespVO { + + @Schema(description = "流程实例的状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; // 参见 BpmProcessInstanceStatusEnum 枚举 + + @Schema(description = "活动节点列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List activityNodes; + + @Schema(description = "表单字段权限") + private Map formFieldsPermission; + + @Schema(description = "待办任务") + private BpmTaskRespVO todoTask; + + /** + * 所属流程定义信息 + */ + private BpmProcessDefinitionRespVO processDefinition; + + /** + * 所属流程实例信息 + */ + private BpmProcessInstanceRespVO processInstance; + + @Schema(description = "活动节点信息") + @Data + public static class ActivityNode { + + @Schema(description = "节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartUserNode") + private String id; + + @Schema(description = "节点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "发起人") + private String name; + + @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer nodeType; // 参见 BpmSimpleModelNodeType 枚举 + + @Schema(description = "节点状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; // 参见 BpmTaskStatusEnum 枚举 + + @Schema(description = "节点的开始时间") + private LocalDateTime startTime; + @Schema(description = "节点的结束时间") + private LocalDateTime endTime; + + @Schema(description = "审批节点的任务信息") + private List tasks; + + @Schema(description = "候选人策略", example = "35") + private Integer candidateStrategy; // 参见 BpmTaskCandidateStrategyEnum 枚举。主要用于发起时,审批节点、抄送节点自选 + + @Schema(description = "候选人用户 ID 列表", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1818") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 candidateUsers + private List candidateUserIds; + + @Schema(description = "候选人用户列表") + private List candidateUsers; // 只包含未生成 ApprovalTaskInfo 的用户列表 + + @Schema(description = "流程编号", example = "8761d8e0-0922-11f0-bd37-00ff1db677bf") + private String processInstanceId; // 当且仅当,该节点是子流程节点时,才会有值(CallActivity 的 processInstanceId 字段) + + } + + @Schema(description = "活动节点的任务信息") + @Data + public static class ActivityNodeTask { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String id; + + @Schema(description = "任务所属人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1818") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 ownerUser + private Long owner; + + @Schema(description = "任务所属人", example = "1024") + private UserSimpleBaseVO ownerUser; + + @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser + private Long assignee; + + @Schema(description = "任务分配人", example = "2048") + private UserSimpleBaseVO assigneeUser; + + @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; // 参见 BpmTaskStatusEnum 枚举 + + @Schema(description = "审批意见", example = "同意") + private String reason; + + @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png") + private String signPicUrl; + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceBpmnModelViewRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceBpmnModelViewRespVO.java new file mode 100644 index 0000000..5f9c0f3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceBpmnModelViewRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Set; + +@Schema(description = "管理后台 - 流程示例的 BPMN 视图 Response VO") +@Data +public class BpmProcessInstanceBpmnModelViewRespVO { + + // ========== 基本信息 ========== + + @Schema(description = "流程实例信息", requiredMode = Schema.RequiredMode.REQUIRED) + private BpmProcessInstanceRespVO processInstance; + + @Schema(description = "任务列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List tasks; + + @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED) + private String bpmnXml; + + @Schema(description = "SIMPLE 模型") + private BpmSimpleModelNodeVO simpleModel; + + // ========== 进度信息 ========== + + @Schema(description = "进行中的活动节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED) + private Set unfinishedTaskActivityIds; // 只包括 UserTask + + @Schema(description = "已经完成的活动节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED) + private Set finishedTaskActivityIds; // 包括 UserTask、Gateway 等,不包括 SequenceFlow + + @Schema(description = "已经完成的连线节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED) + private Set finishedSequenceFlowActivityIds; // 只包括 SequenceFlow + + @Schema(description = "已经拒绝的活动节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED) + private Set rejectedTaskActivityIds; // 只包括 UserTask + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCancelReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCancelReqVO.java new file mode 100644 index 0000000..36d4724 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCancelReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 流程实例的取消 Request VO") +@Data +public class BpmProcessInstanceCancelReqVO { + + @Schema(description = "流程实例的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "流程实例的编号不能为空") + private String id; + + @Schema(description = "取消原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "不请假了!") + @NotEmpty(message = "取消原因不能为空") + private String reason; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCopyPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCopyPageReqVO.java new file mode 100644 index 0000000..b702afd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCopyPageReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 流程实例抄送的分页 Request VO") +@Data +public class BpmProcessInstanceCopyPageReqVO extends PageParam { + + @Schema(description = "流程名称", example = "芋道") + private String processInstanceName; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java new file mode 100644 index 0000000..b13ff56 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - 流程实例的创建 Request VO") +@Data +public class BpmProcessInstanceCreateReqVO { + + @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "流程定义编号不能为空") + private String processDefinitionId; + + @Schema(description = "变量实例(动态表单)") + private Map variables; + + @Schema(description = "发起人自选审批人 Map", example = "{taskKey1: [1, 2]}") + private Map> startUserSelectAssignees; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstancePageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstancePageReqVO.java new file mode 100644 index 0000000..9d52294 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstancePageReqVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 流程实例分页 Request VO") +@Data +public class BpmProcessInstancePageReqVO extends PageParam { + + @Schema(description = "流程名称", example = "芋道") + private String name; + + @Schema(description = "流程定义的标识", example = "2048") + private String processDefinitionKey; // 精准匹配 + + @Schema(description = "流程实例的状态", example = "1") + @InEnum(BpmProcessInstanceStatusEnum.class) + private Integer status; + + @Schema(description = "流程分类", example = "1") + private String category; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "结束时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] endTime; + + @Schema(description = "发起用户编号", example = "1024") + private Long startUserId; // 注意,只有在【流程实例】菜单,才使用该参数 + + @Schema(description = "动态表单字段查询 JSON Str", example = "{}") + private String formFieldsParams; // SpringMVC 在 get 请求下,无法方便的定义 Map 类型的参数,所以通过 String 接收后,逻辑里面转换 + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java new file mode 100644 index 0000000..f73c490 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - 流程实例的 Response VO") +@Data +public class BpmProcessInstanceRespVO { + + @Schema(description = "流程实例的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "流程摘要") + private List> summary; // 只有流程表单,才有摘要! + + @Schema(description = "流程分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String category; + @Schema(description = "流程分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "请假") + private String categoryName; + + @Schema(description = "流程实例的状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; // 参见 BpmProcessInstanceStatusEnum 枚举 + + @Schema(description = "发起时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime startTime; + + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + @Schema(description = "持续时间", example = "1000") + private Long durationInMillis; + + @Schema(description = "提交的表单值", requiredMode = Schema.RequiredMode.REQUIRED) + private Map formVariables; + + @Schema(description = "业务的唯一标识-例如说,请假申请的编号", example = "1") + private String businessKey; + + /** + * 发起流程的用户 + */ + private UserSimpleBaseVO startUser; + + @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private String processDefinitionId; + /** + * 流程定义 + */ + private BpmProcessDefinitionRespVO processDefinition; + + /** + * 当前审批中的任务 + */ + private List tasks; // 仅在流程实例分页才返回 + + @Schema(description = "流程任务") + @Data + public static class Task { + + @Schema(description = "流程任务的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser + private Long assignee; + + @Schema(description = "任务分配人", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + private UserSimpleBaseVO assigneeUser; + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java new file mode 100644 index 0000000..0969fda --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - 通过流程任务的 Request VO") +@Data +public class BpmTaskApproveReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "审批意见", example = "不错不错!") + private String reason; + + @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png") + private String signPicUrl; + + @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED) + private Map variables; + + @Schema(description = "下一个节点审批人", example = "{nodeId:[1, 2]}") + private Map> nextAssignees; // 为什么是 Map,而不是 List 呢?因为下一个节点可能是多个,例如说并行网关的情况 + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskCopyReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskCopyReqVO.java new file mode 100644 index 0000000..0889875 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskCopyReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Collection; + +@Schema(description = "管理后台 - 抄送流程任务的 Request VO") +@Data +public class BpmTaskCopyReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "抄送的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2]") + @NotEmpty(message = "抄送用户不能为空") + private Collection copyUserIds; + + @Schema(description = "抄送意见", example = "帮忙看看!") + private String reason; +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskDelegateReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskDelegateReqVO.java new file mode 100644 index 0000000..cd1fce4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskDelegateReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 委派流程任务的 Request VO") +@Data +public class BpmTaskDelegateReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "被委派人 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "被委派人 ID 不能为空") + private Long delegateUserId; + + @Schema(description = "委派原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "做不了决定,需要你先帮忙瞅瞅") + @NotEmpty(message = "委派原因不能为空") + private String reason; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java new file mode 100644 index 0000000..f129e5a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 流程任务的的分页 Request VO") // 待办、已办,都使用该分页 +@Data +public class BpmTaskPageReqVO extends PageParam { + + @Schema(description = "流程任务名", example = "芋道") + private String name; + + @Schema(description = "流程分类", example = "1") + private String category; + + @Schema(description = "流程定义的标识", example = "2048") + private String processDefinitionKey; // 精准匹配 + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java new file mode 100644 index 0000000..d4dca29 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 不通过流程任务的 Request VO") +@Data +public class BpmTaskRejectReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!") + private String reason; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java new file mode 100644 index 0000000..fce72a4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java @@ -0,0 +1,128 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - 流程任务 Response VO") +@Data +public class BpmTaskRespVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "任务名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + @Schema(description = "持续时间", example = "1000") + private Long durationInMillis; + + @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer status; // 参见 BpmTaskStatusEnum 枚举 + + @Schema(description = "审批理由", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private String reason; + + @Schema(description = "任务负责人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 ownerUser + private Long owner; + /** + * 负责人的用户信息 + */ + private UserSimpleBaseVO ownerUser; + + @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser + private Long assignee; + /** + * 审核的用户信息 + */ + private UserSimpleBaseVO assigneeUser; + + @Schema(description = "任务定义的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "Activity_one") + private String taskDefinitionKey; + + @Schema(description = "所属流程实例编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8888") + private String processInstanceId; + /** + * 所属流程实例 + */ + private ProcessInstance processInstance; + + @Schema(description = "父任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String parentTaskId; + @Schema(description = "子任务列表(由加签生成)", requiredMode = Schema.RequiredMode.REQUIRED, example = "childrenTask") + private List children; // 由加签生成,包含多层子任务 + + @Schema(description = "表单编号", example = "1024") + private Long formId; + @Schema(description = "表单名字", example = "请假表单") + private String formName; + @Schema(description = "表单的配置,JSON 字符串") + private String formConf; + @Schema(description = "表单项的数组") + private List formFields; + @Schema(description = "提交的表单值", requiredMode = Schema.RequiredMode.REQUIRED) + private Map formVariables; + @Schema(description = "操作按钮设置值") + private Map buttonsSetting; + + @Schema(description = "是否需要签名", example = "false") + private Boolean signEnable; + + @Schema(description = "是否填写审批意见", example = "false") + private Boolean reasonRequire; + + @Schema(description = "节点类型", example = "10") + private Integer nodeType; // 参见 BpmSimpleModelNodeTypeEnum 枚举。 + + @Data + @Schema(description = "流程实例") + public static class ProcessInstance { + + @Schema(description = "流程实例编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "流程实例名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "提交时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private String processDefinitionId; + + @Schema(description = "流程摘要", example = "[]") + private List> summary; // 只有流程表单,才有摘要! + + /** + * 发起人的用户信息 + */ + private UserSimpleBaseVO startUser; + + } + + @Data + @Schema(description = "操作按钮设置") + public static class OperationButtonSetting { + + @Schema(description = "显示名称", example = "审批") + private String displayName; + + @Schema(description = "是否启用", example = "true") + private Boolean enable; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.java new file mode 100644 index 0000000..1f95750 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 退回流程任务的 Request VO") +@Data +public class BpmTaskReturnReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "退回到的任务 Key", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotEmpty(message = "退回到的任务 Key 不能为空") + private String targetTaskDefinitionKey; + + @Schema(description = "退回意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "我就是想驳回") + @NotEmpty(message = "退回意见不能为空") + private String reason; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskSignCreateReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskSignCreateReqVO.java new file mode 100644 index 0000000..71278b3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskSignCreateReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import java.util.Set; + +@Schema(description = "管理后台 - 加签任务的创建(加签) Request VO") +@Data +public class BpmTaskSignCreateReqVO { + + @Schema(description = "需要加签的任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "加签的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888") + @NotEmpty(message = "加签用户不能为空") + private Set userIds; + + @Schema(description = "加签类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "before") + @NotEmpty(message = "加签类型不能为空") + private String type; // 参见 BpmTaskSignTypeEnum 枚举 + + @Schema(description = "加签原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "需要加签") + @NotEmpty(message = "加签原因不能为空") + private String reason; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskSignDeleteReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskSignDeleteReqVO.java new file mode 100644 index 0000000..9ea8a10 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskSignDeleteReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 加签任务的删除(减签) Request VO") +@Data +public class BpmTaskSignDeleteReqVO { + + @Schema(description = "被减签的任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "加签原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "需要减签") + @NotEmpty(message = "加签原因不能为空") + private String reason; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskTransferReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskTransferReqVO.java new file mode 100644 index 0000000..b388705 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskTransferReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 流程任务的转办 Request VO") +@Data +public class BpmTaskTransferReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "新审批人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "新审批人的用户编号不能为空") + private Long assigneeUserId; + + @Schema(description = "转办原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "做不了决定,需要你先帮忙瞅瞅") + @NotEmpty(message = "转办原因不能为空") + private String reason; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/app/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/app/package-info.java new file mode 100644 index 0000000..e8d285e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/app/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.bpm.controller.app; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/package-info.java new file mode 100644 index 0000000..d1930bd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.iocoder.yudao.module.bpm.controller; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java new file mode 100644 index 0000000..2101b22 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java @@ -0,0 +1,132 @@ +package cn.iocoder.yudao.module.bpm.convert.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.base.dept.DeptSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.Model; +import org.flowable.engine.repository.ProcessDefinition; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 流程模型 Convert + * + * @author yunlongn + */ +@Mapper +public interface BpmModelConvert { + + BpmModelConvert INSTANCE = Mappers.getMapper(BpmModelConvert.class); + + default List buildModelList(List list, + Map formMap, + Map categoryMap, + Map deploymentMap, + Map processDefinitionMap, + Map userMap, + Map deptMap) { + List result = convertList(list, model -> { + BpmModelMetaInfoVO metaInfo = parseMetaInfo(model); + BpmFormDO form = metaInfo != null ? formMap.get(metaInfo.getFormId()) : null; + BpmCategoryDO category = categoryMap.get(model.getCategory()); + Deployment deployment = model.getDeploymentId() != null ? deploymentMap.get(model.getDeploymentId()) : null; + ProcessDefinition processDefinition = model.getDeploymentId() != null ? + processDefinitionMap.get(model.getDeploymentId()) : null; + List startUsers = metaInfo != null ? convertList(metaInfo.getStartUserIds(), userMap::get) : null; + List startDepts = metaInfo != null ? convertList(metaInfo.getStartDeptIds(), deptMap::get) : null; + return buildModel0(model, metaInfo, form, category, deployment, processDefinition, startUsers, startDepts); + }); + // 排序 + result.sort(Comparator.comparing(BpmModelMetaInfoVO::getSort)); + return result; + } + + default BpmModelRespVO buildModel(Model model, byte[] bpmnBytes, BpmSimpleModelNodeVO simpleModel) { + BpmModelMetaInfoVO metaInfo = parseMetaInfo(model); + BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null, null, null); + if (ArrayUtil.isNotEmpty(bpmnBytes)) { + modelVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnBytes)); + } + modelVO.setSimpleModel(simpleModel); + return modelVO; + } + + default BpmModelRespVO buildModel0(Model model, + BpmModelMetaInfoVO metaInfo, BpmFormDO form, BpmCategoryDO category, + Deployment deployment, ProcessDefinition processDefinition, + List startUsers, List startDepts) { + BpmModelRespVO modelRespVO = new BpmModelRespVO().setId(model.getId()).setName(model.getName()) + .setKey(model.getKey()).setCategory(model.getCategory()) + .setCreateTime(DateUtils.of(model.getCreateTime())); + // Form + BeanUtils.copyProperties(metaInfo, modelRespVO); + if (form != null) { + modelRespVO.setFormName(form.getName()); + } + // Category + if (category != null) { + modelRespVO.setCategoryName(category.getName()); + } + // ProcessDefinition + if (processDefinition != null) { + modelRespVO.setProcessDefinition(BeanUtils.toBean(processDefinition, BpmProcessDefinitionRespVO.class)); + modelRespVO.getProcessDefinition().setSuspensionState(processDefinition.isSuspended() ? + SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode()); + if (deployment != null) { + modelRespVO.getProcessDefinition().setDeploymentTime(DateUtils.of(deployment.getDeploymentTime())); + } + } + // User、Dept + modelRespVO.setStartUsers(BeanUtils.toBean(startUsers, UserSimpleBaseVO.class)) + .setStartDepts(BeanUtils.toBean(startDepts, DeptSimpleBaseVO.class)); + return modelRespVO; + } + + default void copyToModel(Model model, BpmModelSaveReqVO reqVO) { + model.setName(reqVO.getName()); + model.setKey(reqVO.getKey()); + model.setCategory(reqVO.getCategory()); + model.setMetaInfo(JsonUtils.toJsonString(BeanUtils.toBean(reqVO, BpmModelMetaInfoVO.class))); + } + + default BpmModelMetaInfoVO parseMetaInfo(Model model) { + BpmModelMetaInfoVO vo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoVO.class); + if (vo == null) { + return null; + } + if (vo.getManagerUserIds() == null) { + vo.setManagerUserIds(Collections.emptyList()); + } + if (vo.getStartUserIds() == null) { + vo.setStartUserIds(Collections.emptyList()); + } + // 如果为空,兜底处理,使用 createTime 创建时间 + if (vo.getSort() == null) { + vo.setSort(model.getCreateTime().getTime()); + } + return vo; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java new file mode 100644 index 0000000..1ef8b6f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.bpm.convert.definition; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.ProcessDefinition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Bpm 流程定义的 Convert + * + * @author yunlong.li + */ +@Mapper +public interface BpmProcessDefinitionConvert { + + BpmProcessDefinitionConvert INSTANCE = Mappers.getMapper(BpmProcessDefinitionConvert.class); + + default PageResult buildProcessDefinitionPage(PageResult page, + Map deploymentMap, + Map processDefinitionInfoMap, + Map formMap, + Map categoryMap) { + List list = buildProcessDefinitionList(page.getList(), deploymentMap, processDefinitionInfoMap, formMap, categoryMap); + return new PageResult<>(list, page.getTotal()); + } + + default List buildProcessDefinitionList(List list, + Map deploymentMap, + Map processDefinitionInfoMap, + Map formMap, + Map categoryMap) { + List result = CollectionUtils.convertList(list, definition -> { + Deployment deployment = MapUtil.get(deploymentMap, definition.getDeploymentId(), Deployment.class); + BpmProcessDefinitionInfoDO processDefinitionInfo = MapUtil.get(processDefinitionInfoMap, definition.getId(), BpmProcessDefinitionInfoDO.class); + BpmFormDO form = null; + if (processDefinitionInfo != null) { + form = MapUtil.get(formMap, processDefinitionInfo.getFormId(), BpmFormDO.class); + } + BpmCategoryDO category = MapUtil.get(categoryMap, definition.getCategory(), BpmCategoryDO.class); + return buildProcessDefinition(definition, deployment, processDefinitionInfo, form, category, null); + }); + // 排序 + result.sort(Comparator.comparing(BpmProcessDefinitionRespVO::getSort)); + return result; + } + + default BpmProcessDefinitionRespVO buildProcessDefinition(ProcessDefinition definition, + Deployment deployment, + BpmProcessDefinitionInfoDO processDefinitionInfo, + BpmFormDO form, + BpmCategoryDO category, + BpmnModel bpmnModel) { + BpmProcessDefinitionRespVO respVO = BeanUtils.toBean(definition, BpmProcessDefinitionRespVO.class); + respVO.setSuspensionState(definition.isSuspended() ? SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode()); + // Deployment + if (deployment != null) { + respVO.setDeploymentTime(LocalDateTimeUtil.of(deployment.getDeploymentTime())); + } + // BpmProcessDefinitionInfoDO + if (processDefinitionInfo != null) { + copyTo(processDefinitionInfo, respVO); + // Form + if (form != null) { + respVO.setFormName(form.getName()); + } + } + // Category + if (category != null) { + respVO.setCategoryName(category.getName()); + } + // BpmnModel + if (bpmnModel != null) { + respVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnModel)); + } + return respVO; + } + + @Mapping(source = "from.id", target = "to.id", ignore = true) + void copyTo(BpmProcessDefinitionInfoDO from, @MappingTarget BpmProcessDefinitionRespVO to); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/message/BpmMessageConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/message/BpmMessageConvert.java new file mode 100644 index 0000000..99b7beb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/message/BpmMessageConvert.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.bpm.convert.message; + +import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.Map; + +@Mapper +public interface BpmMessageConvert { + + BpmMessageConvert INSTANCE = Mappers.getMapper(BpmMessageConvert.class); + + @Mapping(target = "mobile", ignore = true) + @Mapping(source = "userId", target = "userId") + @Mapping(source = "templateCode", target = "templateCode") + @Mapping(source = "templateParams", target = "templateParams") + SmsSendSingleToUserReqDTO convert(Long userId, String templateCode, Map templateParams); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/package-info.java new file mode 100644 index 0000000..6db6ebc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package cn.iocoder.yudao.module.bpm.convert; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java new file mode 100644 index 0000000..c82414b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java @@ -0,0 +1,295 @@ +package cn.iocoder.yudao.module.bpm.convert.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * 流程实例 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface BpmProcessInstanceConvert { + + BpmProcessInstanceConvert INSTANCE = Mappers.getMapper(BpmProcessInstanceConvert.class); + + default PageResult buildProcessInstancePage(PageResult pageResult, + Map processDefinitionMap, + Map categoryMap, + Map> taskMap, + Map userMap, + Map deptMap, + Map processDefinitionInfoMap) { + PageResult vpPageResult = BeanUtils.toBean(pageResult, BpmProcessInstanceRespVO.class); + for (int i = 0; i < pageResult.getList().size(); i++) { + BpmProcessInstanceRespVO respVO = vpPageResult.getList().get(i); + respVO.setStatus(FlowableUtils.getProcessInstanceStatus(pageResult.getList().get(i))); + MapUtils.findAndThen(processDefinitionMap, respVO.getProcessDefinitionId(), + processDefinition -> respVO.setCategory(processDefinition.getCategory()) + .setProcessDefinition(BeanUtils.toBean(processDefinition, BpmProcessDefinitionRespVO.class))); + MapUtils.findAndThen(categoryMap, respVO.getCategory(), category -> respVO.setCategoryName(category.getName())); + respVO.setTasks(BeanUtils.toBean(taskMap.get(respVO.getId()), BpmProcessInstanceRespVO.Task.class)); + // user + if (userMap != null) { + AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(pageResult.getList().get(i).getStartUserId())); + if (startUser != null) { + respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); + MapUtils.findAndThen(deptMap, startUser.getDeptId(), dept -> respVO.getStartUser().setDeptName(dept.getName())); + } + if (CollUtil.isNotEmpty(respVO.getTasks())) { + respVO.getTasks().forEach(task -> { + AdminUserRespDTO assigneeUser = userMap.get(task.getAssignee()); + if (assigneeUser!= null) { + task.setAssigneeUser(BeanUtils.toBean(assigneeUser, UserSimpleBaseVO.class)); + MapUtils.findAndThen(deptMap, assigneeUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName())); + } + }); + } + } + // 摘要 + respVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(respVO.getProcessDefinitionId()), + pageResult.getList().get(i).getProcessVariables())); + // 表单 + respVO.setFormVariables(pageResult.getList().get(i).getProcessVariables()); + } + return vpPageResult; + } + + default BpmProcessInstanceRespVO buildProcessInstance(HistoricProcessInstance processInstance, + ProcessDefinition processDefinition, + BpmProcessDefinitionInfoDO processDefinitionInfo, + AdminUserRespDTO startUser, + DeptRespDTO dept) { + BpmProcessInstanceRespVO respVO = BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class); + respVO.setStatus(FlowableUtils.getProcessInstanceStatus(processInstance)) + .setFormVariables(FlowableUtils.getProcessInstanceFormVariable(processInstance)); + // definition + respVO.setProcessDefinition(BeanUtils.toBean(processDefinition, BpmProcessDefinitionRespVO.class)); + copyTo(processDefinitionInfo, respVO.getProcessDefinition()); + // user + if (startUser != null) { + respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); + if (dept != null) { + respVO.getStartUser().setDeptName(dept.getName()); + } + } + return respVO; + } + + @Mapping(source = "from.id", target = "to.id", ignore = true) + void copyTo(BpmProcessDefinitionInfoDO from, @MappingTarget BpmProcessDefinitionRespVO to); + + default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, ProcessInstance instance, Integer status) { + return new BpmProcessInstanceStatusEvent(source).setId(instance.getId()).setStatus(status) + .setProcessDefinitionKey(instance.getProcessDefinitionKey()).setBusinessKey(instance.getBusinessKey()); + } + + default BpmMessageSendWhenProcessInstanceApproveReqDTO buildProcessInstanceApproveMessage(ProcessInstance instance) { + return new BpmMessageSendWhenProcessInstanceApproveReqDTO() + .setStartUserId(NumberUtils.parseLong(instance.getStartUserId())) + .setProcessInstanceId(instance.getId()) + .setProcessInstanceName(instance.getName()); + } + + default BpmMessageSendWhenProcessInstanceRejectReqDTO buildProcessInstanceRejectMessage(ProcessInstance instance, String reason) { + return new BpmMessageSendWhenProcessInstanceRejectReqDTO() + .setProcessInstanceName(instance.getName()) + .setProcessInstanceId(instance.getId()) + .setReason(reason) + .setStartUserId(NumberUtils.parseLong(instance.getStartUserId())); + } + + default BpmProcessInstanceBpmnModelViewRespVO buildProcessInstanceBpmnModelView(HistoricProcessInstance processInstance, + List taskInstances, + BpmnModel bpmnModel, + BpmSimpleModelNodeVO simpleModel, + Set unfinishedTaskActivityIds, + Set finishedTaskActivityIds, + Set finishedSequenceFlowActivityIds, + Set rejectTaskActivityIds, + Map userMap, + Map deptMap) { + BpmProcessInstanceBpmnModelViewRespVO respVO = new BpmProcessInstanceBpmnModelViewRespVO(); + // 基本信息 + respVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class, o -> o + .setStatus(FlowableUtils.getProcessInstanceStatus(processInstance))) + .setStartUser(buildUser(processInstance.getStartUserId(), userMap, deptMap))); + respVO.setTasks(convertList(taskInstances, task -> BeanUtils.toBean(task, BpmTaskRespVO.class) + .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)) + .setAssigneeUser(buildUser(task.getAssignee(), userMap, deptMap)) + .setOwnerUser(buildUser(task.getOwner(), userMap, deptMap)))); + respVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnModel)); + respVO.setSimpleModel(simpleModel); + // 进度信息 + respVO.setUnfinishedTaskActivityIds(unfinishedTaskActivityIds) + .setFinishedTaskActivityIds(finishedTaskActivityIds) + .setFinishedSequenceFlowActivityIds(finishedSequenceFlowActivityIds) + .setRejectedTaskActivityIds(rejectTaskActivityIds); + return respVO; + } + + default UserSimpleBaseVO buildUser(String userIdStr, + Map userMap, + Map deptMap) { + if (StrUtil.isEmpty(userIdStr)) { + return null; + } + Long userId = NumberUtils.parseLong(userIdStr); + return buildUser(userId, userMap, deptMap); + } + + default UserSimpleBaseVO buildUser(Long userId, + Map userMap, + Map deptMap) { + if (userId == null) { + return null; + } + AdminUserRespDTO user = userMap.get(userId); + if (user == null) { + return null; + } + UserSimpleBaseVO userVO = BeanUtils.toBean(user, UserSimpleBaseVO.class); + DeptRespDTO dept = user.getDeptId() != null ? deptMap.get(user.getDeptId()) : null; + if (dept != null) { + userVO.setDeptName(dept.getName()); + } + return userVO; + } + + default BpmApprovalDetailRespVO.ActivityNodeTask buildApprovalTaskInfo(HistoricTaskInstance task) { + if (task == null) { + return null; + } + return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class) + .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)) + .setSignPicUrl(FlowableUtils.getTaskSignPicUrl(task)); + } + + default Set parseUserIds(HistoricProcessInstance processInstance, + List activityNodes, + BpmTaskRespVO todoTask) { + Set userIds = new HashSet<>(); + if (processInstance != null) { + userIds.add(NumberUtils.parseLong(processInstance.getStartUserId())); + } + for (BpmApprovalDetailRespVO.ActivityNode activityNode : activityNodes) { + CollUtil.addAll(userIds, convertSet(activityNode.getTasks(), BpmApprovalDetailRespVO.ActivityNodeTask::getAssignee)); + CollUtil.addAll(userIds, convertSet(activityNode.getTasks(), BpmApprovalDetailRespVO.ActivityNodeTask::getOwner)); + CollUtil.addAll(userIds, activityNode.getCandidateUserIds()); + } + if (todoTask != null) { + CollUtil.addIfAbsent(userIds, todoTask.getAssignee()); + CollUtil.addIfAbsent(userIds, todoTask.getOwner()); + if (CollUtil.isNotEmpty(todoTask.getChildren())) { + CollUtil.addAll(userIds, convertSet(todoTask.getChildren(), BpmTaskRespVO::getAssignee)); + CollUtil.addAll(userIds, convertSet(todoTask.getChildren(), BpmTaskRespVO::getOwner)); + } + } + return userIds; + } + + default Set parseUserIds02(HistoricProcessInstance processInstance, + List tasks) { + Set userIds = SetUtils.asSet(Long.valueOf(processInstance.getStartUserId())); + tasks.forEach(task -> { + CollUtil.addIfAbsent(userIds, NumberUtils.parseLong((task.getAssignee()))); + CollUtil.addIfAbsent(userIds, NumberUtils.parseLong((task.getOwner()))); + }); + return userIds; + } + + default BpmApprovalDetailRespVO buildApprovalDetail(BpmnModel bpmnModel, + ProcessDefinition processDefinition, + BpmProcessDefinitionInfoDO processDefinitionInfo, + HistoricProcessInstance processInstance, + Integer processInstanceStatus, + List activityNodes, + BpmTaskRespVO todoTask, + Map formFieldsPermission, + Map userMap, + Map deptMap) { + // 1.1 流程实例 + BpmProcessInstanceRespVO processInstanceResp = null; + if (processInstance != null) { + AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); + DeptRespDTO dept = startUser != null ? deptMap.get(startUser.getDeptId()) : null; + processInstanceResp = buildProcessInstance(processInstance, null, null, startUser, dept); + } + + // 1.2 流程定义 + BpmProcessDefinitionRespVO definitionResp = BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinition( + processDefinition, null, processDefinitionInfo, null, null, bpmnModel); + + // 1.3 流程节点 + activityNodes.forEach(approveNode -> { + if (approveNode.getTasks() != null) { + approveNode.getTasks().forEach(task -> { + task.setAssigneeUser(buildUser(task.getAssignee(), userMap, deptMap)); + task.setOwnerUser(buildUser(task.getOwner(), userMap, deptMap)); + }); + } + approveNode.setCandidateUsers(convertList(approveNode.getCandidateUserIds(), userId -> buildUser(userId, userMap, deptMap))); + }); + + // 1.4 待办任务 + if (todoTask != null) { + todoTask.setAssigneeUser(buildUser(todoTask.getAssignee(), userMap, deptMap)); + todoTask.setOwnerUser(buildUser(todoTask.getOwner(), userMap, deptMap)); + if (CollUtil.isNotEmpty(todoTask.getChildren())) { + todoTask.getChildren().forEach(childTask -> { + childTask.setAssigneeUser(buildUser(childTask.getAssignee(), userMap, deptMap)); + childTask.setOwnerUser(buildUser(childTask.getOwner(), userMap, deptMap)); + }); + } + } + + // 2. 拼接起来 + return new BpmApprovalDetailRespVO().setStatus(processInstanceStatus) + .setProcessDefinition(definitionResp) + .setProcessInstance(processInstanceResp) + .setFormFieldsPermission(formFieldsPermission) + .setTodoTask(todoTask) + .setActivityNodes(activityNodes); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java new file mode 100644 index 0000000..dfb76c8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java @@ -0,0 +1,221 @@ +package cn.iocoder.yudao.module.bpm.convert.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.flowable.task.service.impl.persistence.entity.TaskEntityImpl; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; + +/** + * Bpm 任务 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface BpmTaskConvert { + + BpmTaskConvert INSTANCE = Mappers.getMapper(BpmTaskConvert.class); + + default PageResult buildTodoTaskPage(PageResult pageResult, + Map processInstanceMap, + Map userMap, + Map processDefinitionInfoMap) { + return BeanUtils.toBean(pageResult, BpmTaskRespVO.class, taskVO -> { + ProcessInstance processInstance = processInstanceMap.get(taskVO.getProcessInstanceId()); + if (processInstance == null) { + return; + } + taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class)); + AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); + taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); + taskVO.getProcessInstance().setCreateTime(DateUtils.of(processInstance.getStartTime())); + // 摘要 + taskVO.getProcessInstance().setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(processInstance.getProcessDefinitionId()), + processInstance.getProcessVariables())); + }); + } + + default PageResult buildTaskPage(PageResult pageResult, + Map processInstanceMap, + Map userMap, + Map deptMap, + Map processDefinitionInfoMap) { + List taskVOList = CollectionUtils.convertList(pageResult.getList(), task -> { + BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class); + taskVO.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)); + // 用户信息 + AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(task.getAssignee())); + if (assignUser != null) { + taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, UserSimpleBaseVO.class)); + findAndThen(deptMap, assignUser.getDeptId(), dept -> taskVO.getAssigneeUser().setDeptName(dept.getName())); + } + // 流程实例 + HistoricProcessInstance processInstance = processInstanceMap.get(taskVO.getProcessInstanceId()); + if (processInstance != null) { + AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); + taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class)); + taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); + // 摘要 + taskVO.getProcessInstance().setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(processInstance.getProcessDefinitionId()), + processInstance.getProcessVariables())); + } + return taskVO; + }); + return new PageResult<>(taskVOList, pageResult.getTotal()); + } + + default List buildTaskListByProcessInstanceId(List taskList, + Map formMap, + Map userMap, + Map deptMap) { + return CollectionUtils.convertList(taskList, task -> { + // 特殊:已取消的任务,不返回 + BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class); + Integer taskStatus = FlowableUtils.getTaskStatus(task); + if (BpmTaskStatusEnum.isCancelStatus(taskStatus)) { + return null; + } + taskVO.setStatus(taskStatus).setReason(FlowableUtils.getTaskReason(task)); + // 表单信息 + BpmFormDO form = MapUtil.get(formMap, NumberUtils.parseLong(task.getFormKey()), BpmFormDO.class); + if (form != null) { + taskVO.setFormId(form.getId()).setFormName(form.getName()).setFormConf(form.getConf()) + .setFormFields(form.getFields()).setFormVariables(FlowableUtils.getTaskFormVariable(task)); + } + // 用户信息 + buildTaskAssignee(taskVO, task.getAssignee(), userMap, deptMap); + buildTaskOwner(taskVO, task.getOwner(), userMap, deptMap); + return taskVO; + }); + } + + default List buildTaskListByParentTaskId(List taskList, + Map userMap, + Map deptMap) { + return convertList(taskList, task -> BeanUtils.toBean(task, BpmTaskRespVO.class, taskVO -> { + AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(task.getAssignee())); + if (assignUser != null) { + taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, UserSimpleBaseVO.class)); + DeptRespDTO dept = deptMap.get(assignUser.getDeptId()); + if (dept != null) { + taskVO.getAssigneeUser().setDeptName(dept.getName()); + } + } + AdminUserRespDTO ownerUser = userMap.get(NumberUtils.parseLong(task.getOwner())); + if (ownerUser != null) { + taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, UserSimpleBaseVO.class)); + findAndThen(deptMap, ownerUser.getDeptId(), dept -> taskVO.getOwnerUser().setDeptName(dept.getName())); + } + })); + } + + default BpmTaskRespVO buildTodoTask(Task todoTask, List childrenTasks, + Map buttonsSetting, + BpmFormDO form) { + BpmTaskRespVO bpmTaskRespVO = BeanUtils.toBean(todoTask, BpmTaskRespVO.class) + .setStatus(FlowableUtils.getTaskStatus(todoTask)).setReason(FlowableUtils.getTaskReason(todoTask)) + .setButtonsSetting(buttonsSetting) + .setChildren(convertList(childrenTasks, childTask -> BeanUtils.toBean(childTask, BpmTaskRespVO.class) + .setStatus(FlowableUtils.getTaskStatus(childTask)))); + if (form != null) { + bpmTaskRespVO.setFormId(form.getId()).setFormName(form.getName()) + .setFormConf(form.getConf()).setFormFields(form.getFields()); + } + return bpmTaskRespVO; + } + + default BpmMessageSendWhenTaskCreatedReqDTO convert(ProcessInstance processInstance, AdminUserRespDTO startUser, + Task task) { + BpmMessageSendWhenTaskCreatedReqDTO reqDTO = new BpmMessageSendWhenTaskCreatedReqDTO(); + reqDTO.setProcessInstanceId(processInstance.getProcessInstanceId()) + .setProcessInstanceName(processInstance.getName()).setStartUserId(startUser.getId()) + .setStartUserNickname(startUser.getNickname()).setTaskId(task.getId()).setTaskName(task.getName()) + .setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())); + return reqDTO; + } + + default void buildTaskOwner(BpmTaskRespVO task, String taskOwner, + Map userMap, + Map deptMap) { + AdminUserRespDTO ownerUser = userMap.get(NumberUtils.parseLong(taskOwner)); + if (ownerUser != null) { + task.setOwnerUser(BeanUtils.toBean(ownerUser, UserSimpleBaseVO.class)); + findAndThen(deptMap, ownerUser.getDeptId(), dept -> task.getOwnerUser().setDeptName(dept.getName())); + } + } + + default void buildTaskChildren(BpmTaskRespVO task, Map> childrenTaskMap, + Map userMap, Map deptMap) { + List childTasks = childrenTaskMap.get(task.getId()); + if (CollUtil.isNotEmpty(childTasks)) { + task.setChildren( + convertList(childTasks, childTask -> { + BpmTaskRespVO childTaskVO = BeanUtils.toBean(childTask, BpmTaskRespVO.class); + childTaskVO.setStatus(FlowableUtils.getTaskStatus(childTask)); + buildTaskOwner(childTaskVO, childTask.getOwner(), userMap, deptMap); + buildTaskAssignee(childTaskVO, childTask.getAssignee(), userMap, deptMap); + return childTaskVO; + }) + ); + } + } + + default void buildTaskAssignee(BpmTaskRespVO task, String taskAssignee, + Map userMap, + Map deptMap) { + AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(taskAssignee)); + if (assignUser != null) { + task.setAssigneeUser(BeanUtils.toBean(assignUser, UserSimpleBaseVO.class)); + findAndThen(deptMap, assignUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName())); + } + } + + /** + * 将父任务的属性,拷贝到子任务(加签任务) + *

+ * 为什么不使用 mapstruct 映射?因为 TaskEntityImpl 还有很多其他属性,这里我们只设置我们需要的。 + * 使用 mapstruct 会将里面嵌套的各个属性值都设置进去,会出现意想不到的问题。 + * + * @param parentTask 父任务 + * @param childTask 加签任务 + */ + default void copyTo(TaskEntityImpl parentTask, TaskEntityImpl childTask) { + childTask.setName(parentTask.getName()); + childTask.setDescription(parentTask.getDescription()); + childTask.setCategory(parentTask.getCategory()); + childTask.setParentTaskId(parentTask.getId()); + childTask.setProcessDefinitionId(parentTask.getProcessDefinitionId()); + childTask.setProcessInstanceId(parentTask.getProcessInstanceId()); + childTask.setTaskDefinitionKey(parentTask.getTaskDefinitionKey()); + childTask.setTaskDefinitionId(parentTask.getTaskDefinitionId()); + childTask.setPriority(parentTask.getPriority()); + childTask.setCreateTime(new Date()); + childTask.setTenantId(parentTask.getTenantId()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..8153487 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmCategoryDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmCategoryDO.java new file mode 100644 index 0000000..01c71eb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmCategoryDO.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.bpm.dal.dataobject.definition; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * BPM 流程分类 DO + * + * @author 芋道源码 + */ +@TableName("bpm_category") +@KeySequence("bpm_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmCategoryDO extends BaseDO { + + /** + * 分类编号 + */ + @TableId + private Long id; + /** + * 分类名 + */ + private String name; + /** + * 分类标志 + */ + private String code; + /** + * 分类描述 + */ + private String description; + /** + * 分类状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 分类排序 + */ + private Integer sort; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmFormDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmFormDO.java new file mode 100644 index 0000000..21080cb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmFormDO.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.bpm.dal.dataobject.definition; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * BPM 工作流的表单定义 + * 用于工作流的申请表单,需要动态配置的场景 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_form", autoResultMap = true) +@KeySequence("bpm_form_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmFormDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 表单名 + */ + private String name; + /** + * 状态 + */ + private Integer status; + /** + * 表单的配置 + */ + private String conf; + /** + * 表单项的数组 + * + * 目前直接将 https://github.com/JakHuang/form-generator 生成的 JSON 串,直接保存 + * 定义:https://github.com/JakHuang/form-generator/issues/46 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List fields; + /** + * 备注 + */ + private String remark; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java new file mode 100644 index 0000000..c2799ef --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java @@ -0,0 +1,222 @@ +package cn.iocoder.yudao.module.bpm.dal.dataobject.definition; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.flowable.engine.repository.Model; +import org.flowable.engine.repository.ProcessDefinition; + +import java.util.List; + +/** + * BPM 流程定义的拓信息 + * 主要解决 Flowable {@link org.flowable.engine.repository.ProcessDefinition} 不支持拓展字段,所以新建该表 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_process_definition_info", autoResultMap = true) +@KeySequence("bpm_process_definition_info_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmProcessDefinitionInfoDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 流程定义的编号 + * + * 关联 {@link ProcessDefinition#getId()} 属性 + */ + private String processDefinitionId; + /** + * 流程模型的编号 + * + * 关联 {@link Model#getId()} 属性 + */ + private String modelId; + /** + * 流程模型的类型 + * + * 枚举 {@link BpmModelTypeEnum} + */ + private Integer modelType; + + /** + * 流程分类的编码 + * + * 关联 {@link BpmCategoryDO#getCode()} + * + * 为什么要存储?原因是,{@link ProcessDefinition#getCategory()} 无法设置 + */ + private String category; + /** + * 图标 + */ + private String icon; + /** + * 描述 + */ + private String description; + + /** + * 表单类型 + * + * 枚举 {@link BpmModelFormTypeEnum} + */ + private Integer formType; + /** + * 动态表单编号 + * + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + * + * 关联 {@link BpmFormDO#getId()} + */ + private Long formId; + /** + * 表单的配置 + * + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + * + * 冗余 {@link BpmFormDO#getConf()} + */ + private String formConf; + /** + * 表单项的数组 + * + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + * + * 冗余 {@link BpmFormDO#getFields()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List formFields; + /** + * 自定义表单的提交路径,使用 Vue 的路由地址 + * + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomCreatePath; + /** + * 自定义表单的查看路径,使用 Vue 的路由地址 + * + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomViewPath; + + /** + * SIMPLE 设计器模型数据 json 格式 + * + * 目的:当使用仿钉钉设计器时。流程模型发布的时候,需要保存流程模型设计器的快照数据。 + */ + private String simpleModel; + /** + * 是否可见 + * + * 目的:如果 false 不可见,则不展示在“发起流程”的列表里 + */ + private Boolean visible; + /** + * 排序值 + */ + private Long sort; + + /** + * 可发起用户编号数组 + * + * 关联 {@link AdminUserRespDTO#getId()} 字段的数组 + * + * 如果为空,则表示“全部可以发起”! + * + * 它和 {@link #visible} 的区别在于: + * 1. {@link #visible} 只是决定是否可见。即使不可见,还是可以发起 + * 2. startUserIds 决定某个用户是否可以发起。如果该用户不可发起,则他也是不可见的 + */ + @TableField(typeHandler = LongListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 + private List startUserIds; + + /** + * 可发起部门编号数组 + * + * 关联 {@link AdminUserRespDTO#getDeptId()} 字段的数组 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List startDeptIds; + + /** + * 可管理用户编号数组 + * + * 关联 {@link AdminUserRespDTO#getId()} 字段的数组 + */ + @TableField(typeHandler = LongListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 + private List managerUserIds; + + /** + * 是否允许撤销审批中的申请 + */ + private Boolean allowCancelRunningProcess; + + /** + * 流程 ID 规则 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.ProcessIdRule processIdRule; + + /** + * 自动去重类型 + * + * 枚举 {@link BpmAutoApproveTypeEnum} + */ + private Integer autoApprovalType; + + /** + * 标题设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.TitleSetting titleSetting; + /** + * 摘要设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.SummarySetting summarySetting; + + /** + * 流程前置通知设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.HttpRequestSetting processBeforeTriggerSetting; + /** + * 流程后置通知设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.HttpRequestSetting processAfterTriggerSetting; + + /** + * 任务前置通知设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.HttpRequestSetting taskBeforeTriggerSetting; + + /** + * 任务后置通知设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.HttpRequestSetting taskAfterTriggerSetting; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessExpressionDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessExpressionDO.java new file mode 100644 index 0000000..18494b6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessExpressionDO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.bpm.dal.dataobject.definition; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * BPM 流程表达式 DO + * + * @author 芋道源码 + */ +@TableName("bpm_process_expression") +@KeySequence("bpm_process_expression_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmProcessExpressionDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 表达式名字 + */ + private String name; + /** + * 表达式状态 + * + * 枚举 {@link TODO common_status 对应的类} + */ + private Integer status; + /** + * 表达式 + */ + private String expression; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessListenerDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessListenerDO.java new file mode 100644 index 0000000..4a04c46 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessListenerDO.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.bpm.dal.dataobject.definition; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * BPM 流程监听器 DO + * + * 目的:本质上它是流程监听器的模版,用于 BPMN 在设计时,直接选择这些模版 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_process_listener") +@KeySequence("bpm_process_listener_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmProcessListenerDO extends BaseDO { + + /** + * 主键 ID,自增 + */ + @TableId + private Long id; + /** + * 监听器名字 + */ + private String name; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 监听类型 + * + * 枚举 {@link BpmProcessListenerTypeEnum} + * + * 1. execution:ExecutionListener 执行监听器 + * 2. task:TaskListener 任务监听器 + */ + private String type; + /** + * 监听事件 + * + * execution 时:start、end + * task 时:create 创建、assignment 指派、complete 完成、delete 删除、update 更新、timeout 超时 + */ + private String event; + + /** + * 值类型 + * + * 1. class:Java 类,ExecutionListener 需要 {@link org.flowable.engine.delegate.JavaDelegate},TaskListener 需要 {@link org.flowable.engine.delegate.TaskListener} + * 2. delegateExpression:委托表达式,在 class 的基础上,需要注册到 Spring 容器里,后续表达式通过 Spring Bean 名称即可 + * 3. expression:表达式,一个普通类的普通方法,将这个普通类注册到 Spring 容器中,然后表达式中还可以执行这个类中的方法 + */ + private String valueType; + /** + * 值 + */ + private String value; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmUserGroupDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmUserGroupDO.java new file mode 100644 index 0000000..7b9f480 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmUserGroupDO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.bpm.dal.dataobject.definition; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * BPM 用户组 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_user_group", autoResultMap = true) +@KeySequence("bpm_user_group_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmUserGroupDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + /** + * 组名 + */ + private String name; + /** + * 描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 成员用户编号数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Set userIds; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java new file mode 100644 index 0000000..e75c974 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.bpm.dal.dataobject.oa; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * OA 请假申请 DO + * + * {@link #day} 请假天数,目前先简单做。一般是分成请假上午和下午,可以是 1 整天,可以是 0.5 半天 + * + * @author jason + * @author 芋道源码 + */ +@TableName("bpm_oa_leave") +@KeySequence("bpm_oa_leave_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmOALeaveDO extends BaseDO { + + /** + * 请假表单主键 + */ + @TableId + private Long id; + /** + * 申请人的用户编号 + * + * 关联 AdminUserDO 的 id 属性 + */ + private Long userId; + /** + * 请假类型 + */ + private String type; + /** + * 原因 + */ + private String reason; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 结束时间 + */ + private LocalDateTime endTime; + /** + * 请假天数 + */ + private Long day; + /** + * 审批结果 + * + * 枚举 {@link BpmTaskStatusEnum} + * 考虑到简单,所以直接复用了 BpmProcessInstanceStatusEnum 枚举,也可以自己定义一个枚举哈 + */ + private Integer status; + + /** + * 对应的流程编号 + * + * 关联 ProcessInstance 的 id 属性 + */ + private String processInstanceId; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java new file mode 100644 index 0000000..07f2df6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.bpm.dal.dataobject.task; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import org.flowable.bpmn.model.FlowNode; +import org.flowable.task.api.history.HistoricTaskInstance; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 流程抄送 DO + * + * @author kyle + * @since 2024-01-22 + */ +@TableName(value = "bpm_process_instance_copy", autoResultMap = true) +@KeySequence("bpm_process_instance_copy_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmProcessInstanceCopyDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 发起人 Id + * + * 冗余 ProcessInstance 的 startUserId 字段 + */ + private Long startUserId; + /** + * 流程名 + * + * 冗余 ProcessInstance 的 name 字段 + */ + private String processInstanceName; + /** + * 流程实例的编号 + * + * 关联 ProcessInstance 的 id 属性 + */ + private String processInstanceId; + /** + * 流程实例的流程定义编号 + * + * 关联 ProcessInstance 的 processDefinitionId 属性 + */ + private String processDefinitionId; + /** + * 流程分类 + * + * 冗余 ProcessInstance 的 category 字段 + */ + private String category; + /** + * 流程活动的编号 + *

+ * + * 冗余 {@link FlowNode#getId()},对应 BPMN XML 节点编号 + * 原因:用于查询抄送节点的表单字段权限。因为仿钉钉/飞书的抄送节点 (ServiceTask),没有 taskId,只有 activityId + */ + private String activityId; + /** + * 流程活动的名字 + * + * 冗余 {@link FlowNode#getName()} + */ + private String activityName; + /** + * 流程活动的编号 + * + * 关联 {@link HistoricTaskInstance#getId()} + */ + private String taskId; + + /** + * 用户编号(被抄送的用户编号) + * + * 关联 system_users 的 id 属性 + */ + private Long userId; + + /** + * 抄送意见 + */ + private String reason; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/category/BpmCategoryMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/category/BpmCategoryMapper.java new file mode 100644 index 0000000..5fc8236 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/category/BpmCategoryMapper.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.bpm.dal.mysql.category; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategoryPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * BPM 流程分类 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface BpmCategoryMapper extends BaseMapperX { + + default PageResult selectPage(BpmCategoryPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(BpmCategoryDO::getName, reqVO.getName()) + .likeIfPresent(BpmCategoryDO::getCode, reqVO.getCode()) + .eqIfPresent(BpmCategoryDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BpmCategoryDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(BpmCategoryDO::getSort)); + } + + default BpmCategoryDO selectByName(String name) { + return selectOne(BpmCategoryDO::getName, name); + } + + default BpmCategoryDO selectByCode(String code) { + return selectOne(BpmCategoryDO::getCode, code); + } + + default List selectListByCode(Collection codes) { + return selectList(BpmCategoryDO::getCode, codes); + } + + default List selectListByStatus(Integer status) { + return selectList(BpmCategoryDO::getStatus, status); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmFormMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmFormMapper.java new file mode 100644 index 0000000..53c01d9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmFormMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.bpm.dal.mysql.definition; + + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +/** + * 动态表单 Mapper + * + * @author 风里雾里 + */ +@Mapper +public interface BpmFormMapper extends BaseMapperX { + + default PageResult selectPage(BpmFormPageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .likeIfPresent("name", reqVO.getName()) + .orderByDesc("id")); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java new file mode 100644 index 0000000..6e2f2ea --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.bpm.dal.mysql.definition; + +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface BpmProcessDefinitionInfoMapper extends BaseMapperX { + + default List selectListByProcessDefinitionIds(Collection processDefinitionIds) { + return selectList(BpmProcessDefinitionInfoDO::getProcessDefinitionId, processDefinitionIds); + } + + default BpmProcessDefinitionInfoDO selectByProcessDefinitionId(String processDefinitionId) { + return selectOne(BpmProcessDefinitionInfoDO::getProcessDefinitionId, processDefinitionId); + } + + default void updateByModelId(String modelId, BpmProcessDefinitionInfoDO updateObj) { + update(updateObj, + new LambdaQueryWrapperX().eq(BpmProcessDefinitionInfoDO::getModelId, modelId)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessExpressionMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessExpressionMapper.java new file mode 100644 index 0000000..ab8c18c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessExpressionMapper.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.bpm.dal.mysql.definition; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessExpressionDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * BPM 流程表达式 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface BpmProcessExpressionMapper extends BaseMapperX { + + default PageResult selectPage(BpmProcessExpressionPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(BpmProcessExpressionDO::getName, reqVO.getName()) + .eqIfPresent(BpmProcessExpressionDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BpmProcessExpressionDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(BpmProcessExpressionDO::getId)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessListenerMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessListenerMapper.java new file mode 100644 index 0000000..10ccd2f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessListenerMapper.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.bpm.dal.mysql.definition; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessListenerDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * BPM 流程监听器 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface BpmProcessListenerMapper extends BaseMapperX { + + default PageResult selectPage(BpmProcessListenerPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(BpmProcessListenerDO::getName, reqVO.getName()) + .eqIfPresent(BpmProcessListenerDO::getType, reqVO.getType()) + .eqIfPresent(BpmProcessListenerDO::getEvent, reqVO.getEvent()) + .eqIfPresent(BpmProcessListenerDO::getStatus, reqVO.getStatus()) + .orderByDesc(BpmProcessListenerDO::getId)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmUserGroupMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmUserGroupMapper.java new file mode 100644 index 0000000..ed07627 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmUserGroupMapper.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.bpm.dal.mysql.definition; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 用户组 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface BpmUserGroupMapper extends BaseMapperX { + + default PageResult selectPage(BpmUserGroupPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(BpmUserGroupDO::getName, reqVO.getName()) + .eqIfPresent(BpmUserGroupDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BpmUserGroupDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(BpmUserGroupDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(BpmUserGroupDO::getStatus, status); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/oa/BpmOALeaveMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/oa/BpmOALeaveMapper.java new file mode 100644 index 0000000..2787874 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/oa/BpmOALeaveMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.bpm.dal.mysql.oa; + +import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeavePageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.BpmOALeaveDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +/** + * 请假申请 Mapper + * + * @author jason + * @author 芋道源码 + */ +@Mapper +public interface BpmOALeaveMapper extends BaseMapperX { + + default PageResult selectPage(Long userId, BpmOALeavePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(BpmOALeaveDO::getUserId, userId) + .eqIfPresent(BpmOALeaveDO::getStatus, reqVO.getStatus()) + .eqIfPresent(BpmOALeaveDO::getType, reqVO.getType()) + .likeIfPresent(BpmOALeaveDO::getReason, reqVO.getReason()) + .betweenIfPresent(BpmOALeaveDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(BpmOALeaveDO::getId)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java new file mode 100644 index 0000000..fbb6dde --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.bpm.dal.mysql.task; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface BpmProcessInstanceCopyMapper extends BaseMapperX { + + default PageResult selectPage(Long loginUserId, BpmProcessInstanceCopyPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(BpmProcessInstanceCopyDO::getUserId, loginUserId) + .likeIfPresent(BpmProcessInstanceCopyDO::getProcessInstanceName, reqVO.getProcessInstanceName()) + .betweenIfPresent(BpmProcessInstanceCopyDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(BpmProcessInstanceCopyDO::getId)); + } + + default void deleteByProcessInstanceId(String processInstanceId) { + delete(BpmProcessInstanceCopyDO::getProcessInstanceId, processInstanceId); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java new file mode 100644 index 0000000..1f9cd5f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.bpm.dal.redis; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.time.LocalDateTime; + +import static cn.hutool.core.date.DatePattern.*; + +/** + * BPM 流程 Id 编码的 Redis DAO + * + * @author Lesan + */ +@Repository +public class BpmProcessIdRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 生成序号,使用定义的 processIdRule 规则生成 + * + * @param processIdRule 规则 + * @return 序号 + */ + public String generate(BpmModelMetaInfoVO.ProcessIdRule processIdRule) { + // 生成日期前缀 + String infix = ""; + switch (processIdRule.getInfix()) { + case "DAY": + infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN); + break; + case "HOUR": + infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN + "HH"); + break; + case "MINUTE": + infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN + "HHmm"); + break; + case "SECOND": + infix = DateUtil.format(LocalDateTime.now(), PURE_DATETIME_PATTERN); + break; + } + + // 生成序号 + String noPrefix = processIdRule.getPrefix() + infix + processIdRule.getPostfix(); + String key = RedisKeyConstants.BPM_PROCESS_ID + noPrefix; + Long no = stringRedisTemplate.opsForValue().increment(key); + if (StrUtil.isNotEmpty(infix)) { + // 特殊:没有前缀,则不能过期,不能每次都是从 0 开始 + stringRedisTemplate.expire(key, Duration.ofDays(1L)); + } + return noPrefix + String.format("%0" + processIdRule.getLength() + "d", no); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/RedisKeyConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/RedisKeyConstants.java new file mode 100644 index 0000000..304cda3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/RedisKeyConstants.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.bpm.dal.redis; + +/** + * BPM Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 流程 ID 的缓存 + */ + String BPM_PROCESS_ID = "bpm:process_id:"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/DictTypeConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/DictTypeConstants.java new file mode 100644 index 0000000..c006386 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/DictTypeConstants.java @@ -0,0 +1,10 @@ +package cn.iocoder.yudao.module.bpm.enums; + +/** + * BPM 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..d5d6fa7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.bpm.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * Bpm 错误码枚举类 + *

+ * bpm 系统,使用 1-009-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 通用流程处理 模块 1-009-000-000 ========== + + // ========== OA 流程模块 1-009-001-000 ========== + ErrorCode OA_LEAVE_NOT_EXISTS = new ErrorCode(1_009_001_001, "请假申请不存在"); + + // ========== 流程模型 1-009-002-000 ========== + ErrorCode MODEL_KEY_EXISTS = new ErrorCode(1_009_002_000, "已经存在流程标识为【{}】的流程"); + ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_009_002_001, "流程模型不存在"); + ErrorCode MODEL_KEY_VALID = new ErrorCode(1_009_002_002, "流程标识格式不正确,需要以字母或下划线开头,后接任意字母、数字、中划线、下划线、句点!"); + ErrorCode MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG = new ErrorCode(1_009_002_003, "部署流程失败,原因:流程表单未配置,请点击【修改流程】按钮进行配置"); + ErrorCode MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG = new ErrorCode(1_009_002_004, "部署流程失败," + + "原因:用户任务({})未配置审批人,请点击【流程设计】按钮,选择该它的【任务(审批人)】进行配置"); + ErrorCode MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS = new ErrorCode(1_009_002_005, "部署流程失败,原因:BPMN 流程图中,没有开始事件"); + ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在"); + ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员"); + ErrorCode MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR = new ErrorCode(1_009_002_008, "部署流程失败,原因:首个任务({})的审批人不能是【审批人自选】"); + + // ========== 流程定义 1-009-003-000 ========== + ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图"); + ErrorCode PROCESS_DEFINITION_NAME_NOT_MATCH = new ErrorCode(1_009_003_001, "流程定义的名字期望是({}),当前是({}),请修改 BPMN 流程图"); + ErrorCode PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_009_003_002, "流程定义不存在"); + ErrorCode PROCESS_DEFINITION_IS_SUSPENDED = new ErrorCode(1_009_003_003, "流程定义处于挂起状态"); + + // ========== 流程实例 1-009-004-000 ========== + ErrorCode PROCESS_INSTANCE_NOT_EXISTS = new ErrorCode(1_009_004_000, "流程实例不存在"); + ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS = new ErrorCode(1_009_004_001, "流程取消失败,流程不处于运行中"); + ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF = new ErrorCode(1_009_004_002, "流程取消失败,该流程不是你发起的"); + ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "任务({})的候选人未配置"); + ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在"); + ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程"); + ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消"); + ErrorCode PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 触发器请求调用失败"); + ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置"); + ErrorCode PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_008, "子流程取消失败,子流程不允许取消"); + + // ========== 流程任务 1-009-005-000 ========== + ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你"); + ErrorCode TASK_NOT_EXISTS = new ErrorCode(1_009_005_002, "流程任务不存在"); + ErrorCode TASK_IS_PENDING = new ErrorCode(1_009_005_003, "当前任务处于挂起状态,不能操作"); + ErrorCode TASK_TARGET_NODE_NOT_EXISTS = new ErrorCode(1_009_005_004, " 目标节点不存在"); + ErrorCode TASK_RETURN_FAIL_SOURCE_TARGET_ERROR = new ErrorCode(1_009_005_006, "退回任务失败,目标节点是在并行网关上或非同一路线上,不可跳转"); + ErrorCode TASK_DELEGATE_FAIL_USER_REPEAT = new ErrorCode(1_009_005_007, "任务委派失败,委派人和当前审批人为同一人"); + ErrorCode TASK_DELEGATE_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_008, "任务委派失败,被委派人不存在"); + ErrorCode TASK_SIGN_CREATE_USER_NOT_EXIST = new ErrorCode(1_009_005_009, "任务加签:选择的用户不存在"); + ErrorCode TASK_SIGN_CREATE_TYPE_ERROR = new ErrorCode(1_009_005_010, "任务加签:当前任务已经{},不能{}"); + ErrorCode TASK_SIGN_CREATE_USER_REPEAT = new ErrorCode(1_009_005_011, "任务加签失败,加签人与现有审批人[{}]重复"); + ErrorCode TASK_SIGN_DELETE_NO_PARENT = new ErrorCode(1_009_005_012, "任务减签失败,被减签的任务必须是通过加签生成的任务"); + ErrorCode TASK_TRANSFER_FAIL_USER_REPEAT = new ErrorCode(1_009_005_013, "任务转办失败,转办人和当前审批人为同一人"); + ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在"); + ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!"); + ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); + ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!"); + + // ========== 动态表单模块 1-009-010-000 ========== + ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); + ErrorCode FORM_FIELD_REPEAT = new ErrorCode(1_009_010_001, "表单项({}) 和 ({}) 使用了相同的字段名({})"); + + // ========== 用户组模块 1-009-011-000 ========== + ErrorCode USER_GROUP_NOT_EXISTS = new ErrorCode(1_009_011_000, "用户分组不存在"); + ErrorCode USER_GROUP_IS_DISABLE = new ErrorCode(1_009_011_001, "名字为【{}】的用户分组已被禁用"); + + // ========== 用户组模块 1-009-012-000 ========== + ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(1_009_012_000, "流程分类不存在"); + ErrorCode CATEGORY_NAME_DUPLICATE = new ErrorCode(1_009_012_001, "流程分类名字【{}】重复"); + ErrorCode CATEGORY_CODE_DUPLICATE = new ErrorCode(1_009_012_002, "流程分类编码【{}】重复"); + + // ========== BPM 流程监听器 1-009-013-000 ========== + ErrorCode PROCESS_LISTENER_NOT_EXISTS = new ErrorCode(1_009_013_000, "流程监听器不存在"); + ErrorCode PROCESS_LISTENER_CLASS_NOT_FOUND = new ErrorCode(1_009_013_001, "流程监听器类({})不存在"); + ErrorCode PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR = new ErrorCode(1_009_013_002, "流程监听器类({})没有实现接口({})"); + ErrorCode PROCESS_LISTENER_EXPRESSION_INVALID = new ErrorCode(1_009_013_003, "流程监听器表达式({})不合法"); + + // ========== BPM 流程表达式 1-009-014-000 ========== + ErrorCode PROCESS_EXPRESSION_NOT_EXISTS = new ErrorCode(1_009_014_000, "流程表达式不存在"); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmAutoApproveTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmAutoApproveTypeEnum.java new file mode 100644 index 0000000..f2f58c0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmAutoApproveTypeEnum.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 自动去重的类型的枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmAutoApproveTypeEnum implements ArrayValuable { + + NONE(0, "不自动通过"), + APPROVE_ALL(1, "仅审批一次,后续重复的审批节点均自动通过"), + APPROVE_SEQUENT(2, "仅针对连续审批的节点自动通过"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmAutoApproveTypeEnum::getType).toArray(Integer[]::new); + + private final Integer type; + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java new file mode 100644 index 0000000..69c5512 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * BPM 边界事件 (boundary event) 自定义类型枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmBoundaryEventTypeEnum { + + USER_TASK_TIMEOUT(1, "用户任务超时"), + DELAY_TIMER_TIMEOUT(2, "延迟器超时"), + CHILD_PROCESS_TIMEOUT(3, "子流程超时"); + + private final Integer type; + private final String name; + + public static BpmBoundaryEventTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(eventType -> eventType.getType().equals(type), values()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java new file mode 100644 index 0000000..fab0ddf --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 子流程多实例来源类型枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmChildProcessMultiInstanceSourceTypeEnum implements ArrayValuable { + + FIXED_QUANTITY(1, "固定数量"), + NUMBER_FORM(2, "数字表单"), + MULTIPLE_FORM(3, "多选表单"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessMultiInstanceSourceTypeEnum::getType).toArray(Integer[]::new); + + public static BpmChildProcessMultiInstanceSourceTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java new file mode 100644 index 0000000..55e6e02 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 当子流程发起人为空时类型枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmChildProcessStartUserEmptyTypeEnum implements ArrayValuable { + + MAIN_PROCESS_START_USER(1, "同主流程发起人"), + CHILD_PROCESS_ADMIN(2, "子流程管理员"), + MAIN_PROCESS_ADMIN(3, "主流程管理员"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessStartUserEmptyTypeEnum::getType).toArray(Integer[]::new); + + public static BpmChildProcessStartUserEmptyTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java new file mode 100644 index 0000000..10d04ea --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 子流程发起人类型枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmChildProcessStartUserTypeEnum implements ArrayValuable { + + MAIN_PROCESS_START_USER(1, "同主流程发起人"), + FROM_FORM(2, "表单"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessStartUserTypeEnum::getType).toArray(Integer[]::new); + + public static BpmChildProcessStartUserTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmDelayTimerTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmDelayTimerTypeEnum.java new file mode 100644 index 0000000..69f3c19 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmDelayTimerTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 延迟器类型枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmDelayTimerTypeEnum implements ArrayValuable { + + FIXED_TIME_DURATION(1, "固定时长"), + FIXED_DATE_TIME(2, "固定日期"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmDelayTimerTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmFieldPermissionEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmFieldPermissionEnum.java new file mode 100644 index 0000000..5a9b4b2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmFieldPermissionEnum.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * BPM 表单权限的枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmFieldPermissionEnum { + + READ(1, "只读"), + WRITE(2, "可编辑"), + NONE(3, "隐藏"); + + /** + * 权限 + */ + private final Integer permission; + /** + * 名字 + */ + private final String name; + + public static BpmFieldPermissionEnum valueOf(Integer permission) { + return ArrayUtil.firstMatch(item -> item.getPermission().equals(permission), values()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmHttpRequestParamTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmHttpRequestParamTypeEnum.java new file mode 100644 index 0000000..e072ba5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmHttpRequestParamTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM HTTP 请求参数设置类型。用于 Simple 设计器任务监听器和触发器配置。 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmHttpRequestParamTypeEnum implements ArrayValuable { + + FIXED_VALUE(1, "固定值"), + FROM_FORM(2, "表单"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmHttpRequestParamTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelFormTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelFormTypeEnum.java new file mode 100644 index 0000000..40e96af --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelFormTypeEnum.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 模型的表单类型的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmModelFormTypeEnum implements ArrayValuable { + + NORMAL(10, "流程表单"), // 对应 BpmFormDO + CUSTOM(20, "业务表单") // 业务自己定义的表单,自己进行数据的存储 + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmModelFormTypeEnum::getType).toArray(Integer[]::new); + + private final Integer type; + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelTypeEnum.java new file mode 100644 index 0000000..6b2b857 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmModelTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 模型的类型的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmModelTypeEnum implements ArrayValuable { + + BPMN(10, "BPMN 设计器"), // https://bpmn.io/toolkit/bpmn-js/ + SIMPLE(20, "SIMPLE 设计器"); // 参考钉钉、飞书工作流的设计器 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmModelTypeEnum::getType).toArray(Integer[]::new); + + private final Integer type; + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerTypeEnum.java new file mode 100644 index 0000000..e4fed42 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerTypeEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * BPM 流程监听器的类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmProcessListenerTypeEnum { + + EXECUTION("execution", "执行监听器"), + TASK("task", "任务执行器"); + + private final String type; + private final String name; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueTypeEnum.java new file mode 100644 index 0000000..4e57f10 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueTypeEnum.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * BPM 流程监听器的值类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmProcessListenerValueTypeEnum { + + CLASS("class", "Java 类"), + DELEGATE_EXPRESSION("delegateExpression", "代理表达式"), + EXPRESSION("expression", "表达式"); + + private final String type; + private final String name; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionTypeEnum.java new file mode 100644 index 0000000..d886c37 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionTypeEnum.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 仿钉钉的流程器设计器条件节点的条件类型 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmSimpleModeConditionTypeEnum implements ArrayValuable { + + EXPRESSION(1, "条件表达式"), + RULE(2, "条件规则"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModeConditionTypeEnum::getType).toArray(Integer[]::new); + + private final Integer type; + + private final String name; + + public static BpmSimpleModeConditionTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java new file mode 100644 index 0000000..e5ffa12 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Objects; + +/** + * 仿钉钉的流程器设计器的模型节点类型 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable { + + // 0 ~ 1 开始和结束 + START_NODE(0, "开始", "startEvent"), + END_NODE(1, "结束", "endEvent"), + + // 10 ~ 49 各种节点 + START_USER_NODE(10, "发起人", "userTask"), // 发起人节点。前端的开始节点,Id 固定 + APPROVE_NODE(11, "审批人", "userTask"), + COPY_NODE(12, "抄送人", "serviceTask"), + TRANSACTOR_NODE(13, "办理人", "userTask"), + + DELAY_TIMER_NODE(14, "延迟器", "receiveTask"), + TRIGGER_NODE(15, "触发器", "serviceTask"), + + CHILD_PROCESS(20, "子流程", "callActivity"), + + // 50 ~ 条件分支 + CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 + CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), + PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"), + INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"), + ROUTER_BRANCH_NODE(54, "路由分支", "exclusiveGateway") + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModelNodeTypeEnum::getType).toArray(Integer[]::new); + + private final Integer type; + private final String name; + private final String bpmnType; + + /** + * 判断是否为分支节点 + * + * @param type 节点类型 + */ + public static boolean isBranchNode(Integer type) { + return Objects.equals(CONDITION_BRANCH_NODE.getType(), type) + || Objects.equals(PARALLEL_BRANCH_NODE.getType(), type) + || Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type) + || Objects.equals(ROUTER_BRANCH_NODE.getType(), type); + } + + public static BpmSimpleModelNodeTypeEnum valueOf(Integer type) { + return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java new file mode 100644 index 0000000..13f997c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM Simple 触发器类型枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmTriggerTypeEnum implements ArrayValuable { + + HTTP_REQUEST(1, "发起 HTTP 请求"), // BPM => 业务,流程继续执行,无需等待业务 + HTTP_CALLBACK(2, "接收 HTTP 回调"), // BPM => 业务 => BPM,流程卡主,等待业务回调 + + FORM_UPDATE(10, "更新流程表单数据"), + FORM_DELETE(11, "删除流程表单数据"), + ; + + /** + * 触发器执行动作类型 + */ + private final Integer type; + + /** + * 触发器执行动作描述 + */ + private final String desc; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmTriggerTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static BpmTriggerTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java new file mode 100644 index 0000000..3d4bb9d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 多人审批方式的枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmUserTaskApproveMethodEnum implements ArrayValuable { + + RANDOM(1, "随机挑选一人审批", null), + RATIO(2, "多人会签(按通过比例)", "${ nrOfCompletedInstances/nrOfInstances >= %s}"), // 会签(按通过比例) + ANY(3, "多人或签(一人通过或拒绝)", "${ nrOfCompletedInstances > 0 }"), // 或签(通过只需一人,拒绝只需一人) + SEQUENTIAL(4, "依次审批", "${ nrOfCompletedInstances >= nrOfInstances }"); // 依次审批 + + /** + * 审批方式 + */ + private final Integer method; + /** + * 名字 + */ + private final String name; + /** + * 完成表达式 + */ + private final String completionCondition; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskApproveMethodEnum::getMethod).toArray(Integer[]::new); + + public static BpmUserTaskApproveMethodEnum valueOf(Integer method) { + return ArrayUtil.firstMatch(item -> item.getMethod().equals(method), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveTypeEnum.java new file mode 100644 index 0000000..353124a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 用户任务的审批类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmUserTaskApproveTypeEnum implements ArrayValuable { + + USER(1), // 人工审批 + AUTO_APPROVE(2), // 自动通过 + AUTO_REJECT(3); // 自动拒绝 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskApproveTypeEnum::getType).toArray(Integer[]::new); + + private final Integer type; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignEmptyHandlerTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignEmptyHandlerTypeEnum.java new file mode 100644 index 0000000..f6cee3b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignEmptyHandlerTypeEnum.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * BPM 用户任务的审批人为空时,处理类型枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum BpmUserTaskAssignEmptyHandlerTypeEnum implements ArrayValuable { + + APPROVE(1), // 自动通过 + REJECT(2), // 自动拒绝 + ASSIGN_USER(3), // 指定人员审批 + ASSIGN_ADMIN(4), // 转交给流程管理员 + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskAssignEmptyHandlerTypeEnum::getType).toArray(Integer[]::new); + + private final Integer type; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignStartUserHandlerTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignStartUserHandlerTypeEnum.java new file mode 100644 index 0000000..6d0633a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskAssignStartUserHandlerTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * BPM 用户任务的审批人与发起人相同时,处理类型枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum BpmUserTaskAssignStartUserHandlerTypeEnum implements ArrayValuable { + + START_USER_AUDIT(1), // 由发起人对自己审批 + SKIP(2), // 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过 + TRANSFER_DEPT_LEADER(3); // 转交给部门负责人审批【参考飞书】:若部门负责人为空,则自动通过 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskAssignStartUserHandlerTypeEnum::getType).toArray(Integer[]::new); + + private final Integer type; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerTypeEnum.java new file mode 100644 index 0000000..6c94789 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerTypeEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 用户任务拒绝处理类型枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmUserTaskRejectHandlerTypeEnum implements ArrayValuable { + + FINISH_PROCESS_INSTANCE(1, "终止流程"), + RETURN_USER_TASK(2, "驳回到指定任务节点"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskRejectHandlerTypeEnum::getType).toArray(Integer[]::new); + + public static BpmUserTaskRejectHandlerTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskTimeoutHandlerTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskTimeoutHandlerTypeEnum.java new file mode 100644 index 0000000..ef9825c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskTimeoutHandlerTypeEnum.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 用户任务超时处理类型枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmUserTaskTimeoutHandlerTypeEnum implements ArrayValuable { + + REMINDER(1,"自动提醒"), + APPROVE(2, "自动同意"), + REJECT(3, "自动拒绝"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskTimeoutHandlerTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/message/BpmMessageEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/message/BpmMessageEnum.java new file mode 100644 index 0000000..abec702 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/message/BpmMessageEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.bpm.enums.message; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Bpm 消息的枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum BpmMessageEnum { + + PROCESS_INSTANCE_APPROVE("bpm_process_instance_approve"), // 流程任务被审批通过时,发送给申请人 + PROCESS_INSTANCE_REJECT("bpm_process_instance_reject"), // 流程任务被审批不通过时,发送给申请人 + TASK_ASSIGNED("bpm_task_assigned"), // 任务被分配时,发送给审批人 + TASK_TIMEOUT("bpm_task_timeout"); // 任务审批超时时,发送给审批人 + + /** + * 短信模板的标识 + * + * 关联 SmsTemplateDO 的 code 属性 + */ + private final String smsTemplateCode; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmCommentTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmCommentTypeEnum.java new file mode 100644 index 0000000..8b7b7ce --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmCommentTypeEnum.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.bpm.enums.task; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 流程任务的 Comment 评论类型枚举 + * + * @author kehaiyou + */ +@Getter +@AllArgsConstructor +public enum BpmCommentTypeEnum { + + APPROVE("1", "审批通过", "审批通过,原因是:{}"), + REJECT("2", "不通过", "审批不通过:原因是:{}"), + CANCEL("3", "已取消", "系统自动取消,原因是:{}"), + RETURN("4", "退回", "任务被退回,原因是:{}"), + DELEGATE_START("5", "委派发起", "[{}]将任务委派给[{}],委派理由为:{}"), + DELEGATE_END("6", "委派完成", "[{}]完成委派任务,任务重新回到[{}]手中,审批建议为:{}"), + TRANSFER("7", "转派", "[{}]将任务转派给[{}],转派理由为:{}"), + ADD_SIGN("8", "加签", "[{}]{}给了[{}],理由为:{}"), + SUB_SIGN("9", "减签", "[{}]操作了【减签】,审批人[{}]的任务被取消"), + ; + + /** + * 操作类型 + * + * 由于 BPM Comment 类型为 String,所以这里就不使用 Integer + */ + private final String type; + /** + * 操作名字 + */ + private final String name; + /** + * 操作描述 + */ + private final String comment; + + public String formatComment(Object... params) { + return StrUtil.format(comment, params); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java new file mode 100644 index 0000000..5b42df5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.bpm.enums.task; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 流程实例 ProcessInstance 的状态 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmProcessInstanceStatusEnum implements ArrayValuable { + + NOT_START(-1, "未开始"), + RUNNING(1, "审批中"), + APPROVE(2, "审批通过"), + REJECT(3, "审批不通过"), + CANCEL(4, "已取消"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmProcessInstanceStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer status; + /** + * 描述 + */ + private final String desc; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static boolean isRejectStatus(Integer status) { + return REJECT.getStatus().equals(status); + } + + public static boolean isProcessEndStatus(Integer status) { + return ObjectUtils.equalsAny(status, + APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java new file mode 100644 index 0000000..46d1482 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.bpm.enums.task; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 流程实例/任务的的处理原因枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmReasonEnum { + + // ========== 流程实例的独有原因 ========== + + REJECT_TASK("审批不通过任务,原因:{}"), // 场景:用户审批不通过任务。修改文案时,需要注意 isRejectReason 方法 + CANCEL_PROCESS_INSTANCE_BY_START_USER("用户主动取消流程,原因:{}"), // 场景:用户主动取消流程 + CANCEL_PROCESS_INSTANCE_BY_ADMIN("管理员【{}】取消流程,原因:{}"), // 场景:管理员取消流程 + CANCEL_CHILD_PROCESS_INSTANCE_BY_MAIN_PROCESS("子流程自动取消,原因:主流程已取消"), + + // ========== 流程任务的独有原因 ========== + + CANCEL_BY_SYSTEM("系统自动取消"), // 场景:非常多,比如说:1)多任务审批已经满足条件,无需审批该任务;2)流程实例被取消,无需审批该任务;等等 + TIMEOUT_APPROVE("审批超时,系统自动通过"), + TIMEOUT_REJECT("审批超时,系统自动不通过"), + ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"), + ASSIGN_START_USER_APPROVE_WHEN_SKIP("审批人与提交人为同一人时,自动通过"), + ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE("发起人节点首次自动通过"), // 目前仅“子流程”使用 + ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND("审批人与提交人为同一人时,找不到部门负责人,自动通过"), + ASSIGN_START_USER_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"), + ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"), + ASSIGN_EMPTY_REJECT("审批人为空,自动不通过"), + APPROVE_TYPE_AUTO_APPROVE("非人工审核,自动通过"), + APPROVE_TYPE_AUTO_REJECT("非人工审核,自动不通过"), + CANCEL_BY_PROCESS_CLEAN("进程清理自动取消"), + ; + + private final String reason; + + /** + * 格式化理由 + * + * @param args 参数 + * @return 理由 + */ + public String format(Object... args) { + return StrUtil.format(reason, args); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskSignTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskSignTypeEnum.java new file mode 100644 index 0000000..b01153d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskSignTypeEnum.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.bpm.enums.task; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 流程任务的加签类型枚举 + * + * @author kehaiyou + */ +@Getter +@AllArgsConstructor +public enum BpmTaskSignTypeEnum { + + /** + * 向前加签,需要前置任务审批完成,才回到原审批人 + */ + BEFORE("before", "向前加签"), + /** + * 向后加签,需要后置任务全部审批完,才会通过原审批人节点 + */ + AFTER("after", "向后加签"); + + /** + * 类型 + */ + private final String type; + /** + * 名字 + */ + private final String name; + + public static String nameOfType(String type) { + for (BpmTaskSignTypeEnum value : values()) { + if (value.type.equals(type)) { + return value.name; + } + } + return null; + } + + public static BpmTaskSignTypeEnum of(String type) { + return ArrayUtil.firstMatch(value -> value.getType().equals(type), values()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java new file mode 100644 index 0000000..a19f122 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.bpm.enums.task; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 流程任务 Task 的状态枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmTaskStatusEnum { + + NOT_START(-1, "未开始"), + RUNNING(1, "审批中"), + APPROVE(2, "审批通过"), + REJECT(3, "审批不通过"), + CANCEL(4, "已取消"), + + RETURN(5, "已退回"), + + /** + * 使用场景: + * 1. 任务被向后【加签】时,它在审批通过后,会变成 APPROVING 这个状态,然后等到【加签】出来的任务都被审批后,才会变成 APPROVE 审批通过 + */ + APPROVING(7, "审批通过中"), + /** + * 使用场景: + * 1. 任务被向前【加签】时,它会变成 WAIT 状态,需要等待【加签】出来的任务被审批后,它才能继续变为 RUNNING 继续审批 + * 2. 任务被向后【加签】时,【加签】出来的任务处于 WAIT 状态,它们需要等待该任务被审批后,它们才能继续变为 RUNNING 继续审批 + */ + WAIT(0, "待审批"); + + /** + * 状态 + *

+ * 如果新增时,注意 {@link #isEndStatus(Integer)} 是否需要变更 + */ + private final Integer status; + /** + * 名字 + */ + private final String name; + + public static boolean isRejectStatus(Integer status) { + return REJECT.getStatus().equals(status); + } + + /** + * 判断该状态是否已经处于 End 最终状态 + *

+ * 主要用于一些状态更新的逻辑,如果已经是最终状态,就不再进行更新 + * + * @param status 状态 + * @return 是否 + */ + public static boolean isEndStatus(Integer status) { + return ObjectUtils.equalsAny(status, + APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus(), + RETURN.getStatus(), APPROVING.getStatus()); + } + + public static boolean isCancelStatus(Integer status) { + return ObjUtil.equal(status, CANCEL.getStatus()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java new file mode 100644 index 0000000..e79437b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.config; + +import cn.hutool.core.collection.ListUtil; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmActivityBehaviorFactory; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import org.flowable.common.engine.api.delegate.FlowableFunctionDelegate; +import org.flowable.common.engine.api.delegate.event.FlowableEventListener; +import org.flowable.spring.SpringProcessEngineConfiguration; +import org.flowable.spring.boot.EngineConfigurationConfigurer; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.List; + +/** + * BPM 模块的 Flowable 配置类 + * + * @author jason + */ +@Configuration(proxyBeanMethods = false) +public class BpmFlowableConfiguration { + + /** + * 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean + * + * 如果不创建,会导致项目启动时,Flowable 报错的问题 + */ + @Bean(name = "applicationTaskExecutor") + @ConditionalOnMissingBean(name = "applicationTaskExecutor") + public AsyncListenableTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(8); + executor.setMaxPoolSize(8); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("flowable-task-Executor-"); + executor.setAwaitTerminationSeconds(30); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAllowCoreThreadTimeOut(true); + executor.initialize(); + return executor; + } + + /** + * BPM 模块的 ProcessEngineConfigurationConfigurer 实现类: + * + * 1. 设置各种监听器 + * 2. 设置自定义的 ActivityBehaviorFactory 实现 + */ + @Bean + public EngineConfigurationConfigurer bpmProcessEngineConfigurationConfigurer( + ObjectProvider listeners, + ObjectProvider customFlowableFunctionDelegates, + BpmActivityBehaviorFactory bpmActivityBehaviorFactory) { + return configuration -> { + // 注册监听器,例如说 BpmActivityEventListener + configuration.setEventListeners(ListUtil.toList(listeners.iterator())); + // 设置 ActivityBehaviorFactory 实现类,用于流程任务的审核人的自定义 + configuration.setActivityBehaviorFactory(bpmActivityBehaviorFactory); + // 设置自定义的函数 + configuration.setCustomFlowableFunctionDelegates(ListUtil.toList(customFlowableFunctionDelegates.stream().iterator())); + }; + } + + // =========== 审批人相关的 Bean ========== + + @Bean + public BpmActivityBehaviorFactory bpmActivityBehaviorFactory(BpmTaskCandidateInvoker bpmTaskCandidateInvoker) { + BpmActivityBehaviorFactory bpmActivityBehaviorFactory = new BpmActivityBehaviorFactory(); + bpmActivityBehaviorFactory.setTaskCandidateInvoker(bpmTaskCandidateInvoker); + return bpmActivityBehaviorFactory; + } + + @Bean + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") // adminUserApi 可以注入成功 + public BpmTaskCandidateInvoker bpmTaskCandidateInvoker(List strategyList, + AdminUserApi adminUserApi) { + return new BpmTaskCandidateInvoker(strategyList, adminUserApi); + } + + // =========== 自己拓展的 Bean ========== + + @Bean + public BpmProcessInstanceEventPublisher processInstanceEventPublisher(ApplicationEventPublisher publisher) { + return new BpmProcessInstanceEventPublisher(publisher); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmActivityBehaviorFactory.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmActivityBehaviorFactory.java new file mode 100644 index 0000000..b5278f5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmActivityBehaviorFactory.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; + +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import lombok.Setter; +import org.flowable.bpmn.model.Activity; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; +import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; +import org.flowable.engine.impl.bpmn.behavior.UserTaskActivityBehavior; +import org.flowable.engine.impl.bpmn.parser.factory.DefaultActivityBehaviorFactory; + +/** + * 自定义的 ActivityBehaviorFactory 实现类,目的如下: + * 1. 自定义 {@link #createUserTaskActivityBehavior(UserTask)}:实现自定义的流程任务的 assignee 负责人的分配 + * + * @author 芋道源码 + */ +@Setter +public class BpmActivityBehaviorFactory extends DefaultActivityBehaviorFactory { + + private BpmTaskCandidateInvoker taskCandidateInvoker; + + @Override + public UserTaskActivityBehavior createUserTaskActivityBehavior(UserTask userTask) { + return new BpmUserTaskActivityBehavior(userTask) + .setTaskCandidateInvoker(taskCandidateInvoker); + } + + @Override + public ParallelMultiInstanceBehavior createParallelMultiInstanceBehavior(Activity activity, + AbstractBpmnActivityBehavior behavior) { + return new BpmParallelMultiInstanceBehavior(activity, behavior) + .setTaskCandidateInvoker(taskCandidateInvoker); + } + + @Override + public SequentialMultiInstanceBehavior createSequentialMultiInstanceBehavior(Activity activity, + AbstractBpmnActivityBehavior behavior) { + return new BpmSequentialMultiInstanceBehavior(activity, behavior) + .setTaskCandidateInvoker(taskCandidateInvoker); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java new file mode 100644 index 0000000..57f4d39 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import lombok.Setter; +import org.flowable.bpmn.model.Activity; +import org.flowable.bpmn.model.CallActivity; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; + +import java.util.List; +import java.util.Set; + +/** + * 自定义的【并行】的【多个】流程任务的 assignee 负责人的分配 + * 第一步,基于分配规则,计算出分配任务的【多个】候选人们。 + * 第二步,将【多个】任务候选人们,设置到 DelegateExecution 的 collectionVariable 变量中,以便 BpmUserTaskActivityBehavior 使用它 + * + * @author kemengkai + * @since 2022-04-21 16:57 + */ +@Setter +public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehavior { + + private BpmTaskCandidateInvoker taskCandidateInvoker; + + public BpmParallelMultiInstanceBehavior(Activity activity, + AbstractBpmnActivityBehavior innerActivityBehavior) { + super(activity, innerActivityBehavior); + } + + /** + * 重写该方法,主要实现两个功能: + * 1. 忽略原有的 collectionVariable、collectionElementVariable 表达式,而是采用自己定义的 + * 2. 获得任务的处理人,并设置到 collectionVariable 中,用于 BpmUserTaskActivityBehavior 从中可以获取任务的处理人 + * + * 注意,多个任务实例,每个任务实例对应一个处理人,所以返回的数量就是任务处理人的数量 + * + * @param execution 执行任务 + * @return 数量 + */ + @Override + protected int resolveNrOfInstances(DelegateExecution execution) { + // 情况一:UserTask 节点 + if (execution.getCurrentFlowElement() instanceof UserTask) { + // 第一步,设置 collectionVariable 和 CollectionVariable + // 从 execution.getVariable() 读取所有任务处理人的 key + super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 + super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); + // 从 execution.getVariable() 读取当前所有任务处理的人的 key + super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); + + // 第二步,获取任务的所有处理人 + @SuppressWarnings("unchecked") + Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); + if (assigneeUserIds == null) { + assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); + if (CollUtil.isEmpty(assigneeUserIds)) { + // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! + // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 + // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 + assigneeUserIds = SetUtils.asSet((Long) null); + } + execution.setVariableLocal(super.collectionVariable, assigneeUserIds); + } + return assigneeUserIds.size(); + } + + // 情况二:CallActivity 节点 + if (execution.getCurrentFlowElement() instanceof CallActivity) { + FlowElement flowElement = execution.getCurrentFlowElement(); + Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement); + if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { + return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class); + } + if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { + return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size(); + } + } + + return super.resolveNrOfInstances(execution); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java new file mode 100644 index 0000000..21b4f9e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import lombok.Setter; +import org.flowable.bpmn.model.Activity; +import org.flowable.bpmn.model.CallActivity; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; +import org.flowable.engine.impl.persistence.entity.ExecutionEntity; + +import java.util.List; +import java.util.Set; + +/** + * 自定义的【串行】的【多个】流程任务的 assignee 负责人的分配 + * + * 本质上,实现和 {@link BpmParallelMultiInstanceBehavior} 一样,只是继承的类不一样 + * + * @author 芋道源码 + */ +@Setter +public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceBehavior { + + private BpmTaskCandidateInvoker taskCandidateInvoker; + + public BpmSequentialMultiInstanceBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) { + super(activity, innerActivityBehavior); + } + + /** + * 逻辑和 {@link BpmParallelMultiInstanceBehavior#resolveNrOfInstances(DelegateExecution)} 类似 + * + * 差异的点:是在【第二步】的时候,需要返回 LinkedHashSet 集合!因为它需要有序! + */ + @Override + protected int resolveNrOfInstances(DelegateExecution execution) { + // 情况一:UserTask 节点 + if (execution.getCurrentFlowElement() instanceof UserTask) { + // 第一步,设置 collectionVariable 和 CollectionVariable + // 从 execution.getVariable() 读取所有任务处理人的 key + super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 + super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); + // 从 execution.getVariable() 读取当前所有任务处理的人的 key + super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); + + // 第二步,获取任务的所有处理人 + // 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人 + @SuppressWarnings("unchecked") + Set assigneeUserIds = (Set) execution.getVariableLocal(super.collectionVariable, Set.class); + if (assigneeUserIds == null) { + assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); + if (CollUtil.isEmpty(assigneeUserIds)) { + // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! + // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 + // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 + assigneeUserIds = SetUtils.asSet((Long) null); + } + execution.setVariableLocal(super.collectionVariable, assigneeUserIds); + } + return assigneeUserIds.size(); + } + + // 情况二:CallActivity 节点 + if (execution.getCurrentFlowElement() instanceof CallActivity) { + FlowElement flowElement = execution.getCurrentFlowElement(); + Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement); + if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { + return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class); + } + if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { + return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size(); + } + } + + return super.resolveNrOfInstances(execution); + } + + @Override + protected void executeOriginalBehavior(DelegateExecution execution, ExecutionEntity multiInstanceRootExecution, int loopCounter) { + // 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/IC239F + super.collectionExpression = null; + super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); + super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); + super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java new file mode 100644 index 0000000..c4c8167 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.UserTask; +import org.flowable.common.engine.impl.el.ExpressionManager; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.impl.bpmn.behavior.UserTaskActivityBehavior; +import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; +import org.flowable.engine.impl.util.CommandContextUtil; +import org.flowable.engine.impl.util.TaskHelper; +import org.flowable.engine.interceptor.CreateUserTaskBeforeContext; +import org.flowable.task.service.TaskService; +import org.flowable.task.service.impl.persistence.entity.TaskEntity; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Set; + +/** + * 自定义的【单个】流程任务的 assignee 负责人的分配 + * 第一步,基于分配规则,计算出分配任务的【单个】候选人。如果找不到,则直接报业务异常,不继续执行后续的流程; + * 第二步,随机选择一个候选人,则选择作为 assignee 负责人。 + * + * @author 芋道源码 + */ +@Slf4j +public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior { + + @Setter + private BpmTaskCandidateInvoker taskCandidateInvoker; + + public BpmUserTaskActivityBehavior(UserTask userTask) { + super(userTask); + } + + @Override + @Transactional(rollbackFor = Exception.class) + protected void handleAssignments(TaskService taskService, String assignee, String owner, + List candidateUsers, List candidateGroups, TaskEntity task, ExpressionManager expressionManager, + DelegateExecution execution, ProcessEngineConfigurationImpl processEngineConfiguration) { + // 第一步,获得任务的候选用户 + Long assigneeUserId = calculateTaskCandidateUsers(execution); + // 第二步,设置作为负责人 + if (assigneeUserId != null) { + TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId)); + } + } + + private Long calculateTaskCandidateUsers(DelegateExecution execution) { + // 情况一,如果是多实例的任务,例如说会签、或签等情况,则从 Variable 中获取。 + // 顺序审批可见 BpmSequentialMultiInstanceBehavior,并发审批可见 BpmSequentialMultiInstanceBehavior + if (super.multiInstanceActivityBehavior != null) { + return execution.getVariable(super.multiInstanceActivityBehavior.getCollectionElementVariable(), Long.class); + } + + // 情况二,如果非多实例的任务,则计算任务处理人 + // 第一步,先计算可处理该任务的处理人们 + Set candidateUserIds = taskCandidateInvoker.calculateUsersByTask(execution); + if (CollUtil.isEmpty(candidateUserIds)) { + return null; + } + // 第二步,后随机选择一个任务的处理人 + // 疑问:为什么一定要选择一个任务处理人? + // 解答:项目对 bpm 的任务是责任到人,所以每个任务有且仅有一个处理人。 + // 如果希望一个任务可以同时被多个人处理,可以考虑使用 BpmParallelMultiInstanceBehavior 实现的会签 or 或签。 + int index = RandomUtil.randomInt(candidateUserIds.size()); + return CollUtil.get(candidateUserIds, index); + } + + @Override + protected void handleCategory(CreateUserTaskBeforeContext beforeContext, ExpressionManager expressionManager, + TaskEntity task, DelegateExecution execution) { + ProcessDefinitionEntity processDefinitionEntity = CommandContextUtil.getProcessDefinitionEntityManager().findById(execution.getProcessDefinitionId()); + if (processDefinitionEntity == null) { + log.warn("[handleCategory][任务编号({}) 找不到流程定义({})]", task.getId(), execution.getProcessDefinitionId()); + return; + } + task.setCategory(processDefinitionEntity.getCategory()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java new file mode 100644 index 0000000..7f66b29 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java @@ -0,0 +1,207 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.CallActivity; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG; + +/** + * {@link BpmTaskCandidateStrategy} 的调用者,用于调用对应的策略,实现任务的候选人的计算 + * + * @author 芋道源码 + */ +@Slf4j +public class BpmTaskCandidateInvoker { + + private final Map strategyMap = new HashMap<>(); + + private final AdminUserApi adminUserApi; + + public BpmTaskCandidateInvoker(List strategyList, + AdminUserApi adminUserApi) { + strategyList.forEach(strategy -> { + BpmTaskCandidateStrategy oldStrategy = strategyMap.put(strategy.getStrategy(), strategy); + Assert.isNull(oldStrategy, "策略(%s) 重复", strategy.getStrategy()); + }); + this.adminUserApi = adminUserApi; + } + + /** + * 校验流程模型的任务分配规则全部都配置了 + * 目的:如果有规则未配置,会导致流程任务找不到负责人,进而流程无法进行下去! + * + * @param bpmnBytes BPMN XML + */ + public void validateBpmnConfig(byte[] bpmnBytes) { + BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes); + assert bpmnModel != null; + List userTaskList = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class); + // 遍历所有的 UserTask,校验审批人配置 + userTaskList.forEach(userTask -> { + // 1.1 非人工审批,无需校验审批人配置 + Integer approveType = BpmnModelUtils.parseApproveType(userTask); + if (ObjectUtils.equalsAny(approveType, + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(), + BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) { + return; + } + // 1.2 非空校验 + Integer strategy = BpmnModelUtils.parseCandidateStrategy(userTask); + String param = BpmnModelUtils.parseCandidateParam(userTask); + if (strategy == null) { + throw exception(MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG, userTask.getName()); + } + BpmTaskCandidateStrategy candidateStrategy = getCandidateStrategy(strategy); + if (candidateStrategy.isParamRequired() && StrUtil.isBlank(param)) { + throw exception(MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG, userTask.getName()); + } + // 2. 具体策略校验 + getCandidateStrategy(strategy).validateParam(param); + }); + } + + /** + * 计算任务的候选人 + * + * @param execution 执行任务 + * @return 用户编号集合 + */ + @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人 + public Set calculateUsersByTask(DelegateExecution execution) { + // 注意:解决极端情况下,Flowable 异步调用,导致租户 id 丢失的情况 + // 例如说,SIMPLE 延迟器在 trigger 的时候!!! + return FlowableUtils.execute(execution.getTenantId(), () -> { + // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 + FlowElement flowElement = execution.getCurrentFlowElement(); + Integer approveType = BpmnModelUtils.parseApproveType(flowElement); + if (ObjectUtils.equalsAny(approveType, + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(), + BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) { + return new HashSet<>(); + } + + // 1.1 计算任务的候选人 + Integer strategy = BpmnModelUtils.parseCandidateStrategy(flowElement); + String param = BpmnModelUtils.parseCandidateParam(flowElement); + Set userIds = getCandidateStrategy(strategy).calculateUsersByTask(execution, param); + // 1.2 移除被禁用的用户 + removeDisableUsers(userIds); + + // 2. 候选人为空时,根据“审批人为空”的配置补充 + if (CollUtil.isEmpty(userIds)) { + userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy()) + .calculateUsersByTask(execution, param); + // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!! + } + + // 3. 移除发起人的用户 + ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class) + .getProcessInstance(execution.getProcessInstanceId()); + Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId()); + removeStartUserIfSkip(userIds, flowElement, Long.valueOf(processInstance.getStartUserId())); + return userIds; + }); + } + + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, + Long startUserId, String processDefinitionId, Map processVariables) { + // 如果是 CallActivity 子流程,不进行计算候选人 + FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); + if (flowElement instanceof CallActivity) { + return new HashSet<>(); + } + // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 + Integer approveType = BpmnModelUtils.parseApproveType(flowElement); + if (ObjectUtils.equalsAny(approveType, + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(), + BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) { + return new HashSet<>(); + } + + // 1.1 计算任务的候选人 + Integer strategy = BpmnModelUtils.parseCandidateStrategy(flowElement); + String param = BpmnModelUtils.parseCandidateParam(flowElement); + Set userIds = getCandidateStrategy(strategy).calculateUsersByActivity(bpmnModel, activityId, param, + startUserId, processDefinitionId, processVariables); + // 1.2 移除被禁用的用户 + removeDisableUsers(userIds); + + // 2. 候选人为空时,根据“审批人为空”的配置补充 + if (CollUtil.isEmpty(userIds)) { + userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy()) + .calculateUsersByActivity(bpmnModel, activityId, param, startUserId, processDefinitionId, processVariables); + // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!! + } + + // 3. 移除发起人的用户 + removeStartUserIfSkip(userIds, flowElement, startUserId); + return userIds; + } + + @VisibleForTesting + void removeDisableUsers(Set assigneeUserIds) { + if (CollUtil.isEmpty(assigneeUserIds)) { + return; + } + Map userMap = adminUserApi.getUserMap(assigneeUserIds); + assigneeUserIds.removeIf(id -> { + AdminUserRespDTO user = userMap.get(id); + return user == null || CommonStatusEnum.isDisable(user.getStatus()); + }); + } + + /** + * 如果“审批人与发起人相同时”,配置了 SKIP 跳过,则移除发起人 + * + * 注意:如果只有一个候选人,则不处理,避免无法审批 + * + * @param assigneeUserIds 当前分配的候选人 + * @param flowElement 当前节点 + * @param startUserId 发起人 + */ + @VisibleForTesting + void removeStartUserIfSkip(Set assigneeUserIds, FlowElement flowElement, Long startUserId) { + if (CollUtil.size(assigneeUserIds) <= 1) { + return; + } + Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(flowElement); + if (ObjectUtil.notEqual(assignStartUserHandlerType, BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) { + return; + } + assigneeUserIds.remove(startUserId); + } + + private BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) { + BpmTaskCandidateStrategyEnum strategyEnum = BpmTaskCandidateStrategyEnum.valueOf(strategy); + Assert.notNull(strategyEnum, "策略(%s) 不存在", strategy); + BpmTaskCandidateStrategy strategyObj = strategyMap.get(strategyEnum); + Assert.notNull(strategyObj, "策略(%s) 不存在", strategy); + return strategyObj; + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java new file mode 100644 index 0000000..90eb37c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate; + +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; + +import java.util.Map; +import java.util.Set; + +/** + * BPM 任务的候选人的策略接口 + *

+ * 例如说:分配审批人 + * + * @author 芋道源码 + */ +public interface BpmTaskCandidateStrategy { + + /** + * 对应策略 + * + * @return 策略 + */ + BpmTaskCandidateStrategyEnum getStrategy(); + + /** + * 校验参数 + * + * @param param 参数 + */ + void validateParam(String param); + + /** + * 是否一定要输入参数 + * + * @return 是否 + */ + default boolean isParamRequired() { + return true; + } + + /** + * 基于候选人参数,获得任务的候选用户们 + * + * 注意:实现 calculateUsers 系列方法时,有两种选择: + * 1. 只重写 calculateUsers 默认方法 + * 2. 都重写 calculateUsersByTask 和 calculateUsersByActivity 两个方法 + * + * @param param 执行任务 + * @return 用户编号集合 + */ + default Set calculateUsers(String param) { + throw new UnsupportedOperationException("该分配方法未实现,请检查!"); + } + + /** + * 基于【执行任务】,获得任务的候选用户们 + * + * @param execution 执行任务 + * @return 用户编号集合 + */ + default Set calculateUsersByTask(DelegateExecution execution, String param) { + return calculateUsers(param); + } + + /** + * 基于【流程活动】,获得任务的候选用户们 + *

+ * 目的:用于获取未执行节点的候选用户们 + * + * @param bpmnModel 流程图 + * @param activityId 活动 ID (对应 Bpmn XML id) + * @param param 节点的参数 + * @param startUserId 流程发起人编号 + * @param processDefinitionId 流程定义编号 + * @param processVariables 流程变量 + * @return 用户编号集合 + */ + @SuppressWarnings("unused") + default Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + return calculateUsers(param); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java new file mode 100644 index 0000000..7c1950f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.expression; + +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import jakarta.annotation.Resource; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static java.util.Collections.emptySet; + +/** + * 分配给发起人的 Leader 审批的 Expression 流程表达式 + * 目前 Leader 的定义是,发起人所在部门的 Leader + * + * @author 芋道源码 + */ +@Component +public class BpmTaskAssignLeaderExpression { + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @Resource + private BpmProcessInstanceService processInstanceService; + + /** + * 计算审批的候选人 + * + * @param execution 流程执行实体 + * @param level 指定级别 + * @return 指定级别的领导 + */ + public Set calculateUsers(DelegateExecution execution, int level) { + Assert.isTrue(level > 0, "level 必须大于 0"); + // 获得发起人 + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId()); + // 获得对应 leve 的部门 + DeptRespDTO dept = null; + for (int i = 0; i < level; i++) { + // 获得 level 对应的部门 + if (dept == null) { + dept = getStartUserDept(startUserId); + if (dept == null) { // 找不到发起人的部门,所以无法使用该规则 + return emptySet(); + } + } else { + DeptRespDTO parentDept = deptApi.getDept(dept.getParentId()); + if (parentDept == null) { // 找不到父级部门,所以只好结束寻找。原因是:例如说,级别比较高的人,所在部门层级比较少 + break; + } + dept = parentDept; + } + } + return dept.getLeaderUserId() != null ? asSet(dept.getLeaderUserId()) : emptySet(); + } + + private DeptRespDTO getStartUserDept(Long startUserId) { + AdminUserRespDTO startUser = adminUserApi.getUser(startUserId); + if (startUser.getDeptId() == null) { // 找不到部门,所以无法使用该规则 + return null; + } + return deptApi.getDept(startUser.getDeptId()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java new file mode 100644 index 0000000..ac243c0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.expression; + +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.stereotype.Component; + +import java.util.Set; + +/** + * 分配给发起人审批的 Expression 流程表达式 + * + * @author 芋道源码 + */ +@Component +public class BpmTaskAssignStartUserExpression { + + @Resource + private BpmProcessInstanceService processInstanceService; + + /** + * 计算审批的候选人 + * + * @param execution 流程执行实体 + * @return 发起人 + */ + public Set calculateUsers(ExecutionEntityImpl execution) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId()); + return SetUtils.asSet(startUserId); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java new file mode 100644 index 0000000..cac3498 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; + +import java.util.*; + +/** + * 部门的负责人 {@link BpmTaskCandidateStrategy} 抽象类 + * + * @author jason + */ +public abstract class AbstractBpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrategy { + + @Resource + protected DeptApi deptApi; + @Resource + protected AdminUserApi adminUserApi; + + /** + * 获得指定层级的部门负责人,只有第 level 的负责人 + * + * @param dept 指定部门 + * @param level 第几级 + * @return 部门负责人的编号 + */ + protected Long getAssignLevelDeptLeaderId(DeptRespDTO dept, Integer level) { + Assert.isTrue(level > 0, "level 必须大于 0"); + if (dept == null) { + return null; + } + DeptRespDTO currentDept = dept; + for (int i = 1; i < level; i++) { + DeptRespDTO parentDept = deptApi.getDept(currentDept.getParentId()); + if (parentDept == null) { // 找不到父级部门,到了最高级。返回最高级的部门负责人 + break; + } + currentDept = parentDept; + } + return currentDept.getLeaderUserId(); + } + + /** + * 获得连续层级的部门负责人,包含 [1, level] 的负责人 + * + * @param deptIds 指定部门编号数组 + * @param level 最大层级 + * @return 连续部门负责人 Id + */ + protected Set getMultiLevelDeptLeaderIds(List deptIds, Integer level) { + Assert.isTrue(level > 0, "level 必须大于 0"); + if (CollUtil.isEmpty(deptIds)) { + return new HashSet<>(); + } + Set deptLeaderIds = new LinkedHashSet<>(); // 保证有序 + for (Long deptId : deptIds) { + DeptRespDTO dept = deptApi.getDept(deptId); + for (int i = 0; i < level; i++) { + if (dept.getLeaderUserId() != null) { + deptLeaderIds.add(dept.getLeaderUserId()); + } + DeptRespDTO parentDept = deptApi.getDept(dept.getParentId()); + if (parentDept == null) { // 找不到父级部门. 已经到了最高层级了 + break; + } + dept = parentDept; + } + } + return deptLeaderIds; + } + + /** + * 获取发起人的部门 + * + * @param startUserId 发起人 Id + */ + protected DeptRespDTO getStartUserDept(Long startUserId) { + AdminUserRespDTO startUser = adminUserApi.getUser(startUserId); + if (startUser.getDeptId() == null) { // 找不到部门 + return null; + } + return deptApi.getDept(startUser.getDeptId()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java new file mode 100644 index 0000000..a316925 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import com.google.common.collect.Sets; +import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +/** + * 审批人自选 {@link BpmTaskCandidateUserStrategy} 实现类 + * 审批人在审批时选择下一个节点的审批人 + * + * @author smallNorthLee + */ +@Component +public class BpmTaskCandidateApproveUserSelectStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT; + } + + @Override + public void validateParam(String param) {} + + @Override + public boolean isParamRequired() { + return false; + } + + @Override + public LinkedHashSet calculateUsersByTask(DelegateExecution execution, String param) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId()); + Map> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processInstance); + Assert.notNull(approveUserSelectAssignees, "流程实例({}) 的下一个执行节点审批人不能为空", + execution.getProcessInstanceId()); + if (approveUserSelectAssignees == null) { + return Sets.newLinkedHashSet(); + } + // 获得审批人 + List assignees = approveUserSelectAssignees.get(execution.getCurrentActivityId()); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); + } + + @Override + public LinkedHashSet calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + if (processVariables == null) { + return Sets.newLinkedHashSet(); + } + // 流程预测时会使用,允许审批人为空,如果为空前端会弹出提示选择下一个节点审批人,避免流程无法进行,审批时会真正校验节点是否配置审批人 + Map> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processVariables); + if (approveUserSelectAssignees == null) { + return Sets.newLinkedHashSet(); + } + // 获得审批人 + List assignees = approveUserSelectAssignees.get(activityId); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategy.java new file mode 100644 index 0000000..ce8b2ac --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategy.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +/** + * 连续多级部门的负责人 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author jason + */ +@Component +public class BpmTaskCandidateDeptLeaderMultiStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.MULTI_DEPT_LEADER_MULTI; + } + + @Override + public void validateParam(String param) { + // 参数格式: | 分隔:1)左边为部门(多个部门用 , 分隔)。2)右边为部门层级 + String[] params = param.split("\\|"); + Assert.isTrue(params.length == 2, "参数格式不匹配"); + List deptIds = StrUtils.splitToLong(params[0], ","); + int level = Integer.parseInt(params[1]); + // 校验部门存在 + deptApi.validateDeptList(deptIds); + Assert.isTrue(level > 0, "部门层级必须大于 0"); + } + + @Override + public Set calculateUsers(String param) { + String[] params = param.split("\\|"); + List deptIds = StrUtils.splitToLong(params[0], ","); + int level = Integer.parseInt(params[1]); + return super.getMultiLevelDeptLeaderIds(deptIds, level); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategy.java new file mode 100644 index 0000000..336f97b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategy.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * 部门的负责人 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author kyle + */ +@Component +public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrategy { + + @Resource + private DeptApi deptApi; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.DEPT_LEADER; + } + + @Override + public void validateParam(String param) { + Set deptIds = StrUtils.splitToLongSet(param); + deptApi.validateDeptList(deptIds); + } + + @Override + public Set calculateUsers(String param) { + Set deptIds = StrUtils.splitToLongSet(param); + List depts = deptApi.getDeptList(deptIds); + return convertSet(depts, DeptRespDTO::getLeaderUserId); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategy.java new file mode 100644 index 0000000..703ec7d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategy.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * 部门的成员 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author kyle + */ +@Component +public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrategy { + + @Resource + private DeptApi deptApi; + @Resource + private AdminUserApi adminUserApi; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.DEPT_MEMBER; + } + + @Override + public void validateParam(String param) { + Set deptIds = StrUtils.splitToLongSet(param); + deptApi.validateDeptList(deptIds); + } + + @Override + public Set calculateUsers(String param) { + Set deptIds = StrUtils.splitToLongSet(param); + List users = adminUserApi.getUserListByDeptIds(deptIds); + return convertSet(users, AdminUserRespDTO::getId); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java new file mode 100644 index 0000000..00da98b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static cn.hutool.core.collection.ListUtil.toList; + +/** + * 发起人连续多级部门的负责人 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author jason + */ +@Component +public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { + + @Resource + @Lazy + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER_MULTI; + } + + @Override + public void validateParam(String param) { + int level = Integer.parseInt(param); // 参数是部门的层级 + Assert.isTrue(level > 0, "部门的层级必须大于 0"); + } + + @Override + public Set calculateUsersByTask(DelegateExecution execution, String param) { + int level = Integer.parseInt(param); // 参数是部门的层级 + // 获得流程发起人 + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId()); + // 获取发起人的 multi 部门负责人 + DeptRespDTO dept = super.getStartUserDept(startUserId); + if (dept == null) { + return new HashSet<>(); + } + return super.getMultiLevelDeptLeaderIds(toList(dept.getId()), level); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + int level = Integer.parseInt(param); // 参数是部门的层级 + DeptRespDTO dept = super.getStartUserDept(startUserId); + if (dept == null) { + return new HashSet<>(); + } + return super.getMultiLevelDeptLeaderIds(toList(dept.getId()), level); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java new file mode 100644 index 0000000..115bb3c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; + +/** + * 发起人的部门负责人, 可以是上级部门负责人 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author jason + */ +@Component +public class BpmTaskCandidateStartUserDeptLeaderStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { + + @Resource + @Lazy // 避免循环依赖 + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER; + } + + @Override + public void validateParam(String param) { + // 参数是部门的层级 + Assert.isTrue(Integer.parseInt(param) > 0, "部门的层级必须大于 0"); + } + + @Override + public Set calculateUsersByTask(DelegateExecution execution, String param) { + // 获得流程发起人 + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId()); + // 获取发起人的部门负责人 + return getStartUserDeptLeader(startUserId, param); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + // 获取发起人的部门负责人 + return getStartUserDeptLeader(startUserId, param); + } + + private Set getStartUserDeptLeader(Long startUserId, String param) { + int level = Integer.parseInt(param); // 参数是部门的层级 + DeptRespDTO dept = super.getStartUserDept(startUserId); + if (dept == null) { + return new HashSet<>(); + } + Long deptLeaderId = super.getAssignLevelDeptLeaderId(dept, level); + return deptLeaderId != null ? asSet(deptLeaderId) : new HashSet<>(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java new file mode 100644 index 0000000..9304d28 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import com.google.common.collect.Sets; +import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +/** + * 发起人自选 {@link BpmTaskCandidateUserStrategy} 实现类 + * + * @author 芋道源码 + */ +@Component +public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.START_USER_SELECT; + } + + @Override + public void validateParam(String param) {} + + @Override + public boolean isParamRequired() { + return false; + } + + @Override + public LinkedHashSet calculateUsersByTask(DelegateExecution execution, String param) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId()); + Map> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance); + Assert.notNull(startUserSelectAssignees, "流程实例({}) 的发起人自选审批人不能为空", + execution.getProcessInstanceId()); + // 获得审批人 + List assignees = startUserSelectAssignees.get(execution.getCurrentActivityId()); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); + } + + @Override + public LinkedHashSet calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + if (processVariables == null) { + return Sets.newLinkedHashSet(); + } + Map> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processVariables); + if (startUserSelectAssignees == null) { + return Sets.newLinkedHashSet(); + } + // 获得审批人 + List assignees = startUserSelectAssignees.get(activityId); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormDeptLeaderStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormDeptLeaderStrategy.java new file mode 100644 index 0000000..8ecebbd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormDeptLeaderStrategy.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept.AbstractBpmTaskCandidateDeptLeaderStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Set; + +/** + * 表单内部门负责人 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author jason + */ +@Component +public class BpmTaskCandidateFormDeptLeaderStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.FORM_DEPT_LEADER; + } + + @Override + public void validateParam(String param) { + // 参数格式: | 分隔:1)左边为表单内部门字段。2)右边为部门层级 + String[] params = param.split("\\|"); + Assert.isTrue(params.length == 2, "参数格式不匹配"); + Assert.notEmpty(param, "表单内部门字段不能为空"); + int level = Integer.parseInt(params[1]); + Assert.isTrue(level > 0, "部门层级必须大于 0"); + } + + @Override + public Set calculateUsersByTask(DelegateExecution execution, String param) { + String[] params = param.split("\\|"); + Object result = execution.getVariable(params[0]); + int level = Integer.parseInt(params[1]); + return super.getMultiLevelDeptLeaderIds(Convert.toList(Long.class, result), level); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, + String param, Long startUserId, String processDefinitionId, + Map processVariables) { + String[] params = param.split("\\|"); + Object result = processVariables == null ? null : processVariables.get(params[0]); + int level = Integer.parseInt(params[1]); + return super.getMultiLevelDeptLeaderIds(Convert.toList(Long.class, result), level); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java new file mode 100644 index 0000000..6298c32 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Set; + +/** + * 表单内用户字段 {@link BpmTaskCandidateUserStrategy} 实现类 + * + * @author jason + */ +@Component +public class BpmTaskCandidateFormUserStrategy implements BpmTaskCandidateStrategy { + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.FORM_USER; + } + + @Override + public void validateParam(String param) { + Assert.notEmpty(param, "表单内用户字段不能为空"); + } + + @Override + public Set calculateUsersByTask(DelegateExecution execution, String param) { + Object result = execution.getVariable(param); + return CollectionUtils.toLinkedHashSet(Long.class, result); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, + String param, Long startUserId, String processDefinitionId, + Map processVariables) { + Object result = processVariables == null ? null : processVariables.get(param); + return CollectionUtils.toLinkedHashSet(Long.class, result); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategy.java new file mode 100644 index 0000000..de19997 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategy.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * 审批人为空 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author kyle + */ +@Component +public class BpmTaskCandidateAssignEmptyStrategy implements BpmTaskCandidateStrategy { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmProcessDefinitionService processDefinitionService; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY; + } + + @Override + public void validateParam(String param) { + } + + @Override + public Set calculateUsersByTask(DelegateExecution execution, String param) { + return getCandidateUsers(execution.getProcessDefinitionId(), execution.getCurrentFlowElement()); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); + return getCandidateUsers(processDefinitionId, flowElement); + } + + private Set getCandidateUsers(String processDefinitionId, FlowElement flowElement) { + // 情况一:指定人员审批 + Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(flowElement); + if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_USER.getType())) { + return new HashSet<>(BpmnModelUtils.parseAssignEmptyHandlerUserIds(flowElement)); + } + + // 情况二:流程管理员 + if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType())) { + BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(processDefinitionId); + Assert.notNull(processDefinition, "流程定义({})不存在", processDefinitionId); + return new HashSet<>(processDefinition.getManagerUserIds()); + } + + // 都不满足,还是返回空 + return new HashSet<>(); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java new file mode 100644 index 0000000..64ca9e8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other; + +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import com.google.common.collect.Sets; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.common.engine.api.FlowableException; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * 流程表达式 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrategy { + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.EXPRESSION; + } + + @Override + public void validateParam(String param) { + // do nothing 因为它基本做不了校验 + } + + @Override + public Set calculateUsersByTask(DelegateExecution execution, String param) { + Object result = FlowableUtils.getExpressionValue(execution, param); + return CollectionUtils.toLinkedHashSet(Long.class, result); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + Map variables = processVariables == null ? new HashMap<>() : processVariables; + try { + Object result = FlowableUtils.getExpressionValue(variables, param); + return CollectionUtils.toLinkedHashSet(Long.class, result); + } catch (FlowableException ex) { + // 预测未运行的节点时候,表达式如果包含 execution 或者不存在的流程变量会抛异常, + log.warn("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex); + // 不能预测候选人,返回空列表, 避免流程无法进行 + return Sets.newHashSet(); + } + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategy.java new file mode 100644 index 0000000..1d63e15 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategy.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +/** + * 用户组 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author kyle + */ +@Component +public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy { + + @Resource + private BpmUserGroupService userGroupService; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.USER_GROUP; + } + + @Override + public void validateParam(String param) { + Set groupIds = StrUtils.splitToLongSet(param); + userGroupService.validUserGroups(groupIds); + } + + @Override + public Set calculateUsers(String param) { + Set groupIds = StrUtils.splitToLongSet(param); + List groups = userGroupService.getUserGroupList(groupIds); + return convertSetByFlatMap(groups, BpmUserGroupDO::getUserIds, Collection::stream); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategy.java new file mode 100644 index 0000000..873c4ba --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategy.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.system.api.dept.PostApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * 岗位 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author kyle + */ +@Component +public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy { + + @Resource + private PostApi postApi; + @Resource + private AdminUserApi adminUserApi; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.POST; + } + + @Override + public void validateParam(String param) { + Set postIds = StrUtils.splitToLongSet(param); + postApi.validPostList(postIds); + } + + @Override + public Set calculateUsers(String param) { + Set postIds = StrUtils.splitToLongSet(param); + List users = adminUserApi.getUserListByPostIds(postIds); + return convertSet(users, AdminUserRespDTO::getId); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategy.java new file mode 100644 index 0000000..4b0b898 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategy.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.permission.RoleApi; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.Set; + +/** + * 角色 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author kyle + */ +@Component +public class BpmTaskCandidateRoleStrategy implements BpmTaskCandidateStrategy { + + @Resource + private RoleApi roleApi; + @Resource + private PermissionApi permissionApi; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.ROLE; + } + + @Override + public void validateParam(String param) { + Set roleIds = StrUtils.splitToLongSet(param); + roleApi.validRoleList(roleIds); + } + + @Override + public Set calculateUsers(String param) { + Set roleIds = StrUtils.splitToLongSet(param); + return permissionApi.getUserRoleIdListByRoleIds(roleIds); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategy.java new file mode 100644 index 0000000..2273167 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategy.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Set; + +/** + * 发起人自己 {@link BpmTaskCandidateUserStrategy} 实现类 + *

+ * 适合场景:用于需要发起人信息复核等场景 + * + * @author jason + */ +@Component +public class BpmTaskCandidateStartUserStrategy implements BpmTaskCandidateStrategy { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.START_USER; + } + + @Override + public void validateParam(String param) { + } + + @Override + public boolean isParamRequired() { + return false; + } + + @Override + public Set calculateUsersByTask(DelegateExecution execution, String param) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); + return SetUtils.asSet(Long.valueOf(processInstance.getStartUserId())); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + return SetUtils.asSet(startUserId); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategy.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategy.java new file mode 100644 index 0000000..15002d9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategy.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.hutool.core.text.StrPool; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashSet; + +/** + * 用户 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author kyle + */ +@Component +public class BpmTaskCandidateUserStrategy implements BpmTaskCandidateStrategy { + + @Resource + private AdminUserApi adminUserApi; + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.USER; + } + + @Override + public void validateParam(String param) { + adminUserApi.validateUserList(StrUtils.splitToLongSet(param)); + } + + @Override + public LinkedHashSet calculateUsers(String param) { + return new LinkedHashSet<>(StrUtils.splitToLong(param, StrPool.COMMA)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java new file mode 100644 index 0000000..0e18176 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.el; + +import org.flowable.common.engine.api.variable.VariableContainer; +import org.flowable.common.engine.impl.el.function.AbstractFlowableVariableExpressionFunction; +import org.springframework.stereotype.Component; + +/** + * 根据流程变量 variable 的类型,转换参数的值 + * + * 目前用于 ConditionNodeConvert 的 buildConditionExpression 方法中 + * + * @author jason + */ +@Component +public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction { + + public VariableConvertByTypeExpressionFunction() { + super("convertByType"); + } + + public static Object convertByType(VariableContainer variableContainer, String variableName, Object parmaValue) { + Object variable = variableContainer.getVariable(variableName); + if (variable != null && parmaValue != null) { + // 如果值不是字符串类型,流程变量的类型是字符串,把值转成字符串 + if (!(parmaValue instanceof String) && variable instanceof String ) { + return parmaValue.toString(); + } + } + return parmaValue; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java new file mode 100644 index 0000000..2a45e3a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 任务的候选人策略枚举 + * + * 例如说:分配给指定人审批 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmTaskCandidateStrategyEnum implements ArrayValuable { + + ROLE(10, "角色"), + DEPT_MEMBER(20, "部门的成员"), // 包括负责人 + DEPT_LEADER(21, "部门的负责人"), + MULTI_DEPT_LEADER_MULTI(23, "连续多级部门的负责人"), + POST(22, "岗位"), + USER(30, "用户"), + APPROVE_USER_SELECT(34, "审批人自身"), // 当前审批人,可在审批时,选择下一个节点的审批人 + START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时,选择此节点的审批人 + START_USER(36, "发起人自己"), // 申请人自己, 一般紧挨开始节点,常用于发起人信息审核场景 + START_USER_DEPT_LEADER(37, "发起人部门负责人"), + START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"), + USER_GROUP(40, "用户组"), + FORM_USER(50, "表单内用户字段"), + FORM_DEPT_LEADER(51, "表单内部门负责人"), + EXPRESSION(60, "流程表达式"), // 表达式 ExpressionManager + ASSIGN_EMPTY(1, "审批人为空"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmTaskCandidateStrategyEnum::getStrategy).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer strategy; + /** + * 描述 + */ + private final String description; + + public static BpmTaskCandidateStrategyEnum valueOf(Integer strategy) { + return ArrayUtil.firstMatch(o -> o.getStrategy().equals(strategy), values()); + } + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java new file mode 100644 index 0000000..e416428 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java @@ -0,0 +1,146 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums; + +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; + +/** + * BPMN XML 常量信息 + * + * @author 芋道源码 + */ +public interface BpmnModelConstants { + + String BPMN_FILE_SUFFIX = ".bpmn"; + + /** + * BPMN 中的命名空间 + */ + String NAMESPACE = "http://flowable.org/bpmn"; + + /** + * BPMN UserTask 的扩展属性,用于标记候选人策略 + */ + String USER_TASK_CANDIDATE_STRATEGY = "candidateStrategy"; + /** + * BPMN UserTask 的扩展属性,用于标记候选人参数 + */ + String USER_TASK_CANDIDATE_PARAM = "candidateParam"; + + /** + * BPMN ExtensionElement 的扩展属性,用于标记边界事件类型 + */ + String BOUNDARY_EVENT_TYPE = "boundaryEventType"; + + /** + * BPMN ExtensionElement 的扩展属性,用于标记用户任务超时执行动作 + */ + String USER_TASK_TIMEOUT_HANDLER_TYPE = "timeoutHandlerType"; + + /** + * BPMN ExtensionElement 的扩展属性,用于标记用户任务的审批人与发起人相同时,对应的处理类型 + */ + String USER_TASK_ASSIGN_START_USER_HANDLER_TYPE = "assignStartUserHandlerType"; + + /** + * BPMN ExtensionElement 的扩展属性,用于标记用户任务的空处理类型 + */ + String USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE = "assignEmptyHandlerType"; + /** + * BPMN ExtensionElement 的扩展属性,用于标记用户任务的空处理的指定用户编号数组 + */ + String USER_TASK_ASSIGN_USER_IDS = "assignEmptyUserIds"; + + /** + * BPMN ExtensionElement 的扩展属性,用于标记用户任务拒绝处理类型 + */ + String USER_TASK_REJECT_HANDLER_TYPE = "rejectHandlerType"; + /** + * BPMN ExtensionElement 的扩展属性,用于标记用户任务拒绝后的退回的任务 Id + */ + String USER_TASK_REJECT_RETURN_TASK_ID = "rejectReturnTaskId"; + + /** + * BPMN UserTask 的扩展属性,用于标记用户任务的审批类型 + */ + String USER_TASK_APPROVE_TYPE = "approveType"; + + /** + * BPMN UserTask 的扩展属性,用于标记用户任务的审批方式 + */ + String USER_TASK_APPROVE_METHOD = "approveMethod"; + + /** + * BPMN Child Process 的扩展属性,用于标记多实例来源类型 + */ + String CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = "childProcessMultiInstanceSourceType"; + + /** + * BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限 + */ + String FORM_FIELD_PERMISSION_ELEMENT = "fieldsPermission"; + + /** + * BPMN ExtensionElement Attribute, 用于标记表单字段 + */ + String FORM_FIELD_PERMISSION_ELEMENT_FIELD_ATTRIBUTE = "field"; + /** + * BPMN ExtensionElement Attribute, 用于标记表单权限 + */ + String FORM_FIELD_PERMISSION_ELEMENT_PERMISSION_ATTRIBUTE = "permission"; + + /** + * BPMN ExtensionElement 操作按钮设置元素, 用于审批节点操作按钮设置 + */ + String BUTTON_SETTING_ELEMENT = "buttonsSetting"; + + /** + * BPMN ExtensionElement Attribute, 用于标记按钮编号 + */ + String BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE = "id"; + + /** + * BPMN ExtensionElement Attribute, 用于标记按钮显示名称 + */ + String BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE = "displayName"; + + /** + * BPMN ExtensionElement Attribute, 用于标记按钮是否启用 + */ + String BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE = "enable"; + + /** + * BPMN ExtensionElement 的扩展属性,用于标记触发器的类型 + */ + String TRIGGER_TYPE = "triggerType"; + /** + * BPMN ExtensionElement 的扩展属性,用于标记触发器参数 + */ + String TRIGGER_PARAM = "triggerParam"; + + /** + * BPMN Start Event Node Id + */ + String START_EVENT_NODE_ID = "StartEvent"; + + /** + * 发起人节点 ID + */ + String START_USER_NODE_ID = "StartUserNode"; + + /** + * 是否需要签名 + */ + String SIGN_ENABLE = "signEnable"; + + /** + * 审批意见是否必填 + */ + String REASON_REQUIRE = "reasonRequire"; + + /** + * 节点类型 + * + * 目前只有 {@link BpmModelTypeEnum#SIMPLE} 的 UserTask 节点会设置该属性,用于区分是审批节点、还是办理节点 + */ + String NODE_TYPE = "nodeType"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java new file mode 100644 index 0000000..893c4d0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums; + +import org.flowable.engine.runtime.ProcessInstance; + +/** + * BPM Variable 通用常量 + * + * @author 芋道源码 + */ +public class BpmnVariableConstants { + + /** + * 流程实例的变量 - 状态 + * + * @see ProcessInstance#getProcessVariables() + */ + public static final String PROCESS_INSTANCE_VARIABLE_STATUS = "PROCESS_STATUS"; + /** + * 流程实例的变量 - 理由 + * + * 例如说:审批不通过的理由(目前审核通过暂时不会记录) + * + * @see ProcessInstance#getProcessVariables() + */ + public static final String PROCESS_INSTANCE_VARIABLE_REASON = "PROCESS_REASON"; + /** + * 流程实例的变量 - 发起用户选择的审批人 Map + * + * @see ProcessInstance#getProcessVariables() + * @see BpmTaskCandidateStrategyEnum#START_USER_SELECT + */ + public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES"; + /** + * 流程实例的变量 - 审批人选择的审批人 Map + * + * @see ProcessInstance#getProcessVariables() + * @see BpmTaskCandidateStrategyEnum#APPROVE_USER_SELECT + */ + public static final String PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES = "PROCESS_APPROVE_USER_SELECT_ASSIGNEES"; + /** + * 流程实例的变量 - 发起用户 ID + * + * @see ProcessInstance#getProcessVariables() + */ + public static final String PROCESS_INSTANCE_VARIABLE_START_USER_ID = "PROCESS_START_USER_ID"; + /** + * 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id} + * + * 目的是:驳回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过 + * + * @see ProcessInstance#getProcessVariables() + */ + public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s"; + /** + * 流程实例的变量 - 是否跳过表达式 + * + * @see ProcessInstance#getProcessVariables() + * @see Flowable/Activiti之SkipExpression 完成自动审批 + */ + public static final String PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED = "_FLOWABLE_SKIP_EXPRESSION_ENABLED"; + + /** + * 流程实例的变量 - 用于判断流程是否需要跳过发起人节点 + * + * @see ProcessInstance#getProcessVariables() + */ + public static final String PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE = "PROCESS_SKIP_START_USER_NODE"; + + /** + * 流程实例的变量 - 流程开始时间 + * + * 【非存储变量】用于部分需要 format 的场景,例如说:流程实例的自定义标题 + */ + public static final String PROCESS_START_TIME = "PROCESS_START_TIME"; + /** + * 流程实例的变量 - 流程定义名称 + */ + public static final String PROCESS_DEFINITION_NAME = "PROCESS_DEFINITION_NAME"; + + /** + * 任务的变量 - 状态 + * + * @see org.flowable.task.api.Task#getTaskLocalVariables() + */ + public static final String TASK_VARIABLE_STATUS = "TASK_STATUS"; + /** + * 任务的变量 - 理由 + * + * 例如说:审批通过、不通过的理由 + * + * @see org.flowable.task.api.Task#getTaskLocalVariables() + */ + public static final String TASK_VARIABLE_REASON = "TASK_REASON"; + /** + * 任务变量 - 签名图片 URL + */ + public static final String TASK_SIGN_PIC_URL = "TASK_SIGN_PIC_URL"; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/event/BpmProcessInstanceEventPublisher.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/event/BpmProcessInstanceEventPublisher.java new file mode 100644 index 0000000..885e05e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/event/BpmProcessInstanceEventPublisher.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.event; + +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent; +import lombok.AllArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.Valid; + +/** + * {@link BpmProcessInstanceStatusEvent} 的生产者 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Validated +public class BpmProcessInstanceEventPublisher { + + private final ApplicationEventPublisher publisher; + + public void sendProcessInstanceResultEvent(@Valid BpmProcessInstanceStatusEvent event) { + publisher.publishEvent(event); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java new file mode 100644 index 0000000..e58d345 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService; +import jakarta.annotation.Resource; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.JavaDelegate; +import org.springframework.stereotype.Component; + +import java.util.Set; + +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate.BEAN_NAME; + +/** + * 处理抄送用户的 {@link JavaDelegate} 的实现类 + *

+ * 目前只有仿钉钉/飞书模式的【抄送节点】使用 + * + * @author jason + */ +@Component(BEAN_NAME) +public class BpmCopyTaskDelegate implements JavaDelegate { + + public static final String BEAN_NAME = "bpmCopyTaskDelegate"; + + @Resource + private BpmTaskCandidateInvoker taskCandidateInvoker; + + @Resource + private BpmProcessInstanceCopyService processInstanceCopyService; + + @Override + public void execute(DelegateExecution execution) { + // 1. 获得抄送人 + Set userIds = taskCandidateInvoker.calculateUsersByTask(execution); + if (CollUtil.isEmpty(userIds)) { + return; + } + // 2. 执行抄送 + FlowElement currentFlowElement = execution.getCurrentFlowElement(); + processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(), + currentFlowElement.getId(), currentFlowElement.getName(), null); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java new file mode 100644 index 0000000..ba2aaa6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; + +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import com.google.common.collect.ImmutableSet; +import jakarta.annotation.Resource; +import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent; +import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; +import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener; +import org.flowable.engine.delegate.event.FlowableCancelledEvent; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.Set; + +/** + * 监听 {@link ProcessInstance} 的状态变更,更新其对应的 status 状态 + * + * @author jason + */ +@Component +public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener { + + public static final Set PROCESS_INSTANCE_EVENTS = ImmutableSet.builder() + .add(FlowableEngineEventType.PROCESS_CREATED) + .add(FlowableEngineEventType.PROCESS_COMPLETED) + .add(FlowableEngineEventType.PROCESS_CANCELLED) + .build(); + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmProcessInstanceService processInstanceService; + + public BpmProcessInstanceEventListener(){ + super(PROCESS_INSTANCE_EVENTS); + } + + @Override + protected void processCreated(FlowableEngineEntityEvent event) { + processInstanceService.processProcessInstanceCreated((ProcessInstance)event.getEntity()); + } + + @Override + protected void processCompleted(FlowableEngineEntityEvent event) { + processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity()); + } + + @Override // 特殊情况:当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法 + protected void processCancelled(FlowableCancelledEvent event) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getProcessInstanceId()); + processInstanceService.processProcessInstanceCompleted(processInstance); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java new file mode 100644 index 0000000..e50df0b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java @@ -0,0 +1,125 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; +import com.google.common.collect.ImmutableSet; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.BoundaryEvent; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent; +import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; +import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener; +import org.flowable.engine.delegate.event.FlowableActivityCancelledEvent; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.job.api.Job; +import org.flowable.task.api.Task; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +/** + * 监听 {@link Task} 的开始与完成 + * + * @author jason + */ +@Component +@Slf4j +public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmModelService modelService; + @Resource + @Lazy // 解决循环依赖 + private BpmTaskService taskService; + + public static final Set TASK_EVENTS = ImmutableSet.builder() + .add(FlowableEngineEventType.TASK_CREATED) + .add(FlowableEngineEventType.TASK_ASSIGNED) + .add(FlowableEngineEventType.TASK_COMPLETED) // 由于审批通过时,已经记录了 task 的 status 为通过,这里仅处理任务后置通知。 + .add(FlowableEngineEventType.ACTIVITY_CANCELLED) + .add(FlowableEngineEventType.TIMER_FIRED) // 监听审批超时 + .build(); + + public BpmTaskEventListener() { + super(TASK_EVENTS); + } + + @Override + protected void taskCreated(FlowableEngineEntityEvent event) { + taskService.processTaskCreated((Task) event.getEntity()); + } + + @Override + protected void taskAssigned(FlowableEngineEntityEvent event) { + taskService.processTaskAssigned((Task) event.getEntity()); + } + + @Override + protected void taskCompleted(FlowableEngineEntityEvent event) { + taskService.processTaskCompleted((Task) event.getEntity()); + } + + @Override + protected void activityCancelled(FlowableActivityCancelledEvent event) { + List activityList = taskService.getHistoricActivityListByExecutionId(event.getExecutionId()); + if (CollUtil.isEmpty(activityList)) { + log.error("[activityCancelled][使用 executionId({}) 查找不到对应的活动实例]", event.getExecutionId()); + return; + } + // 遍历处理 + activityList.forEach(activity -> { + if (StrUtil.isEmpty(activity.getTaskId())) { + return; + } + taskService.processTaskCanceled(activity.getTaskId()); + }); + } + + @Override + @SuppressWarnings("PatternVariableCanBeUsed") + protected void timerFired(FlowableEngineEntityEvent event) { + // 1.1 只处理 BoundaryEvent 边界计时时间 + String processDefinitionId = event.getProcessDefinitionId(); + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId); + Job entity = (Job) event.getEntity(); + FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId()); + if (!(element instanceof BoundaryEvent)) { + return; + } + // 1.2 判断是否为超时处理 + BoundaryEvent boundaryEvent = (BoundaryEvent) element; + String boundaryEventType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, + BpmnModelConstants.BOUNDARY_EVENT_TYPE); + BpmBoundaryEventTypeEnum bpmTimerBoundaryEventType = BpmBoundaryEventTypeEnum.typeOf(NumberUtils.parseInt(boundaryEventType)); + + // 2. 处理超时 + if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT)) { + // 2.1 用户任务超时处理 + String timeoutHandlerType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, + BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE); + String taskKey = boundaryEvent.getAttachedToRefId(); + taskService.processTaskTimeout(event.getProcessInstanceId(), taskKey, NumberUtils.parseInt(timeoutHandlerType)); + // 2.2 延迟器超时处理 + } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT)) { + String taskKey = boundaryEvent.getAttachedToRefId(); + taskService.triggerTask(event.getProcessInstanceId(), taskKey); + // 2.3 子流程超时处理 + } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT)) { + String taskKey = boundaryEvent.getAttachedToRefId(); + taskService.processChildProcessTimeout(event.getProcessInstanceId(), taskKey); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTriggerTaskDelegate.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTriggerTaskDelegate.java new file mode 100644 index 0000000..1ee347d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTriggerTaskDelegate.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; + +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.JavaDelegate; +import org.springframework.stereotype.Component; + +import java.util.EnumMap; +import java.util.List; + +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate.BEAN_NAME; + + +/** + * 处理触发器任务 {@link JavaDelegate} 的实现类 + *

+ * 目前只有 Simple 设计器【触发器节点】使用 + * + * @author jason + */ +@Component(BEAN_NAME) +@Slf4j +public class BpmTriggerTaskDelegate implements JavaDelegate { + + public static final String BEAN_NAME = "bpmTriggerTaskDelegate"; + + @Resource + private List triggers; + + private final EnumMap triggerMap = new EnumMap<>(BpmTriggerTypeEnum.class); + + @PostConstruct + private void init() { + triggers.forEach(trigger -> triggerMap.put(trigger.getType(), trigger)); + } + + @Override + public void execute(DelegateExecution execution) { + FlowElement flowElement = execution.getCurrentFlowElement(); + BpmTriggerTypeEnum bpmTriggerType = BpmnModelUtils.parserTriggerType(flowElement); + BpmTrigger bpmTrigger = triggerMap.get(bpmTriggerType); + if (bpmTrigger == null) { + log.error("[execute][FlowElement({}), {} 找不到匹配的触发器]", execution.getCurrentActivityId(), flowElement); + return; + } + bpmTrigger.execute(execution.getProcessInstanceId(), BpmnModelUtils.parserTriggerParam(flowElement)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/exection/DemoDelegateClassExecutionListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/exection/DemoDelegateClassExecutionListener.java new file mode 100644 index 0000000..0ce920e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/exection/DemoDelegateClassExecutionListener.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.demo.exection; + +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.JavaDelegate; + +/** + * 类型为 class 的 ExecutionListener 监听器示例 + * + * @author 芋道源码 + */ +@Slf4j +public class DemoDelegateClassExecutionListener implements JavaDelegate { + + @Override + public void execute(DelegateExecution execution) { + log.info("[execute][execution({}) 被调用!变量有:{}]", execution.getId(), + execution.getCurrentFlowableListener().getFieldExtensions()); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/exection/DemoDelegateExpressionExecutionListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/exection/DemoDelegateExpressionExecutionListener.java new file mode 100644 index 0000000..00f3504 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/exection/DemoDelegateExpressionExecutionListener.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.demo.exection; + +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.JavaDelegate; +import org.springframework.stereotype.Component; + +/** + * 类型为 delegateExpression 的 ExecutionListener 监听器示例 + * + * 和 {@link DemoDelegateClassExecutionListener} 的差异是,需要注册到 Spring 中 + */ +@Component +@Slf4j +public class DemoDelegateExpressionExecutionListener implements JavaDelegate { + + @Override + public void execute(DelegateExecution execution) { + log.info("[execute][execution({}) 被调用!变量有:{}]", execution.getId(), + execution.getCurrentFlowableListener().getFieldExtensions()); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/exection/DemoSpringExpressionExecutionListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/exection/DemoSpringExpressionExecutionListener.java new file mode 100644 index 0000000..949140f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/exection/DemoSpringExpressionExecutionListener.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.demo.exection; + +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +/** + * 类型为 expression 的 ExecutionListener 监听器示例 + * + * 和 {@link DemoDelegateClassExecutionListener} 的差异是,需要注册到 Spring 中,但不用实现 {@link org.flowable.engine.delegate.JavaDelegate} 接口 + */ +@Component +@Slf4j +public class DemoSpringExpressionExecutionListener { + + public void execute(DelegateExecution execution) { + log.info("[execute][execution({}) 被调用!变量有:{}]", execution.getId(), + execution.getCurrentFlowableListener().getFieldExtensions()); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/task/DemoDelegateClassTaskListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/task/DemoDelegateClassTaskListener.java new file mode 100644 index 0000000..dee2b95 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/task/DemoDelegateClassTaskListener.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.demo.task; + +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; + +/** + * 类型为 class 的 TaskListener 监听器示例 + * + * @author 芋道源码 + */ +@Slf4j +public class DemoDelegateClassTaskListener implements TaskListener { + + @Override + public void notify(DelegateTask delegateTask) { + log.info("[execute][task({}) 被调用]", delegateTask.getId()); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/task/DemoDelegateExpressionTaskListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/task/DemoDelegateExpressionTaskListener.java new file mode 100644 index 0000000..e554ff8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/task/DemoDelegateExpressionTaskListener.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.demo.task; + +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; +import org.springframework.stereotype.Component; + +/** + * 类型为 delegateExpression 的 TaskListener 监听器示例 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class DemoDelegateExpressionTaskListener implements TaskListener { + + @Override + public void notify(DelegateTask delegateTask) { + log.info("[execute][task({}) 被调用]", delegateTask.getId()); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/task/DemoSpringExpressionTaskListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/task/DemoSpringExpressionTaskListener.java new file mode 100644 index 0000000..0917abd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/demo/task/DemoSpringExpressionTaskListener.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.demo.task; + +import lombok.extern.slf4j.Slf4j; +import org.flowable.task.service.delegate.DelegateTask; +import org.springframework.stereotype.Component; + +/** + * 类型为 expression 的 TaskListener 监听器示例 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class DemoSpringExpressionTaskListener { + + public void notify(DelegateTask delegateTask) { + log.info("[execute][task({}) 被调用]", delegateTask.getId()); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java new file mode 100644 index 0000000..2503c0f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java @@ -0,0 +1,158 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR; + +/** + * 工作流发起 HTTP 请求工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class BpmHttpRequestUtils { + + public static void executeBpmHttpRequest(ProcessInstance processInstance, + String url, + List headerParams, + List bodyParams, + Boolean handleResponse, + List> response) { + RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class); + BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class); + + // 1.1 设置请求头 + MultiValueMap headers = buildHttpHeaders(processInstance, headerParams); + // 1.2 设置请求体 + MultiValueMap body = buildHttpBody(processInstance, bodyParams); + + // 2. 发起请求 + ResponseEntity responseEntity = sendHttpRequest(url, headers, body, restTemplate); + + // 3. 处理返回 + if (Boolean.FALSE.equals(handleResponse)) { + return; + } + // 3.1 判断是否需要解析返回值 + if (responseEntity == null + || StrUtil.isEmpty(responseEntity.getBody()) + || !responseEntity.getStatusCode().is2xxSuccessful() + || CollUtil.isEmpty(response)) { + return; + } + // 3.2 解析返回值, 返回值必须符合 CommonResult 规范。 + CommonResult> respResult = JsonUtils.parseObjectQuietly(responseEntity.getBody(), + new TypeReference<>() {}); + if (respResult == null || !respResult.isSuccess()) { + return; + } + // 3.3 获取需要更新的流程变量 + Map updateVariables = getNeedUpdatedVariablesFromResponse(respResult.getData(), response); + // 3.4 更新流程变量 + if (CollUtil.isNotEmpty(updateVariables)) { + processInstanceService.updateProcessInstanceVariables(processInstance.getId(), updateVariables); + } + } + + public static ResponseEntity sendHttpRequest(String url, + MultiValueMap headers, + MultiValueMap body, + RestTemplate restTemplate) { + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + ResponseEntity responseEntity; + try { + responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + log.info("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity); + } catch (RestClientException e) { + log.error("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage()); + throw exception(PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR); + } + return responseEntity; + } + + public static MultiValueMap buildHttpHeaders(ProcessInstance processInstance, + List headerSettings) { + Map processVariables = processInstance.getProcessVariables(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); + addHttpRequestParam(headers, headerSettings, processVariables); + return headers; + } + + public static MultiValueMap buildHttpBody(ProcessInstance processInstance, + List bodySettings) { + Map processVariables = processInstance.getProcessVariables(); + MultiValueMap body = new LinkedMultiValueMap<>(); + addHttpRequestParam(body, bodySettings, processVariables); + body.add("processInstanceId", processInstance.getId()); + return body; + } + + /** + * 从请求返回值获取需要更新的流程变量 + * + * @param result 请求返回结果 + * @param responseSettings 返回设置 + * @return 需要更新的流程变量 + */ + public static Map getNeedUpdatedVariablesFromResponse(Map result, + List> responseSettings) { + Map updateVariables = new HashMap<>(); + if (CollUtil.isEmpty(result)) { + return updateVariables; + } + responseSettings.forEach(responseSetting -> { + if (StrUtil.isNotEmpty(responseSetting.getKey()) && result.containsKey(responseSetting.getValue())) { + updateVariables.put(responseSetting.getKey(), result.get(responseSetting.getValue())); + } + }); + return updateVariables; + } + + /** + * 添加 HTTP 请求参数。请求头或者请求体 + * + * @param params HTTP 请求参数 + * @param paramSettings HTTP 请求参数设置 + * @param processVariables 流程变量 + */ + public static void addHttpRequestParam(MultiValueMap params, + List paramSettings, + Map processVariables) { + if (CollUtil.isEmpty(paramSettings)) { + return; + } + paramSettings.forEach(item -> { + if (item.getType().equals(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType())) { + params.add(item.getKey(), item.getValue()); + } else if (item.getType().equals(BpmHttpRequestParamTypeEnum.FROM_FORM.getType())) { + params.add(item.getKey(), processVariables.get(item.getValue()).toString()); + } + }); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java new file mode 100644 index 0000000..1cccf18 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -0,0 +1,1025 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import cn.iocoder.yudao.module.bpm.enums.definition.*; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.Process; +import org.flowable.bpmn.model.*; +import org.flowable.common.engine.api.FlowableException; +import org.flowable.common.engine.impl.util.io.BytesStreamSource; +import org.flowable.engine.impl.el.FixedValue; + +import java.util.*; + +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*; +import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_NAMESPACE; +import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_PREFIX; + +/** + * BPMN Model 操作工具类。目前分成三部分: + * + * 1. BPMN 修改 + 解析元素相关的方法 + * 2. BPMN 简单查找相关的方法 + * 3. BPMN 复杂遍历相关的方法 + * 4. BPMN 流程预测相关的方法 + * + * @author 芋道源码 + */ +@Slf4j +public class BpmnModelUtils { + + // ========== BPMN 修改 + 解析元素相关的方法 ========== + + public static void addExtensionElement(FlowElement element, String name, String value) { + if (value == null) { + return; + } + ExtensionElement extensionElement = new ExtensionElement(); + extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); + extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX); + extensionElement.setElementText(value); + extensionElement.setName(name); + element.addExtensionElement(extensionElement); + } + + public static void addExtensionElement(FlowElement element, String name, Integer value) { + if (value == null) { + return; + } + addExtensionElement(element, name, String.valueOf(value)); + } + + public static void addExtensionElementJson(FlowElement element, String name, Object value) { + if (value == null) { + return; + } + addExtensionElement(element, name, JsonUtils.toJsonString(value)); + } + + public static void addExtensionElement(FlowElement element, String name, Map attributes) { + if (attributes == null) { + return; + } + ExtensionElement extensionElement = new ExtensionElement(); + extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); + extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX); + extensionElement.setName(name); + attributes.forEach((key, value) -> { + ExtensionAttribute extensionAttribute = new ExtensionAttribute(key, value); + extensionElement.addAttribute(extensionAttribute); + }); + element.addExtensionElement(extensionElement); + } + + /** + * 解析扩展元素 + * + * @param flowElement 节点 + * @param elementName 元素名称 + * @return 扩展元素 + */ + public static String parseExtensionElement(FlowElement flowElement, String elementName) { + if (flowElement == null) { + return null; + } + ExtensionElement element = CollUtil.getFirst(flowElement.getExtensionElements().get(elementName)); + return element != null ? element.getElementText() : null; + } + + /** + * 给节点添加候选人元素 + * + * @param candidateStrategy 候选人策略 + * @param candidateParam 候选人参数,允许空 + * @param flowElement 节点 + */ + public static void addCandidateElements(Integer candidateStrategy, String candidateParam, FlowElement flowElement) { + addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY, + candidateStrategy == null ? null : candidateStrategy.toString()); + addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM, candidateParam); + } + + /** + * 解析候选人策略 + * + * @param userTask 任务节点 + * @return 候选人策略 + */ + public static Integer parseCandidateStrategy(FlowElement userTask) { + Integer candidateStrategy = NumberUtils.parseInt(userTask.getAttributeValue( + BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY)); + // TODO @芋艿 尝试从 ExtensionElement 取. 后续相关扩展是否都可以 存 extensionElement。 如表单权限。 按钮权限 + if (candidateStrategy == null) { + ExtensionElement element = CollUtil.getFirst(userTask.getExtensionElements().get(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY)); + candidateStrategy = element != null ? NumberUtils.parseInt(element.getElementText()) : null; + } + return candidateStrategy; + } + + /** + * 解析候选人参数 + * + * @param userTask 任务节点 + * @return 候选人参数 + */ + public static String parseCandidateParam(FlowElement userTask) { + String candidateParam = userTask.getAttributeValue( + BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM); + if (candidateParam == null) { + ExtensionElement element = CollUtil.getFirst(userTask.getExtensionElements().get(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM)); + candidateParam = element != null ? element.getElementText() : null; + } + return candidateParam; + } + + /** + * 解析审批类型 + * + * @see BpmUserTaskApproveTypeEnum + * @param userTask 任务节点 + * @return 审批类型 + */ + public static Integer parseApproveType(FlowElement userTask) { + return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE)); + } + + /** + * 解析子流程多实例来源类型 + * + * @see BpmChildProcessMultiInstanceSourceTypeEnum + * @param element 任务节点 + * @return 多实例来源类型 + */ + public static Integer parseMultiInstanceSourceType(FlowElement element) { + return NumberUtils.parseInt(parseExtensionElement(element, BpmnModelConstants.CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE)); + } + + /** + * 添加任务拒绝处理元素 + * + * @param rejectHandler 任务拒绝处理 + * @param userTask 任务节点 + */ + public static void addTaskRejectElements(BpmSimpleModelNodeVO.RejectHandler rejectHandler, UserTask userTask) { + if (rejectHandler == null) { + return; + } + addExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE, StrUtil.toStringOrNull(rejectHandler.getType())); + addExtensionElement(userTask, USER_TASK_REJECT_RETURN_TASK_ID, rejectHandler.getReturnNodeId()); + } + + /** + * 解析任务拒绝处理类型 + * + * @param userTask 任务节点 + * @return 任务拒绝处理类型 + */ + public static BpmUserTaskRejectHandlerTypeEnum parseRejectHandlerType(FlowElement userTask) { + Integer rejectHandlerType = NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE)); + return BpmUserTaskRejectHandlerTypeEnum.typeOf(rejectHandlerType); + } + + /** + * 解析任务拒绝返回任务节点 ID + * + * @param flowElement 任务节点 + * @return 任务拒绝返回任务节点 ID + */ + public static String parseReturnTaskId(FlowElement flowElement) { + return parseExtensionElement(flowElement, USER_TASK_REJECT_RETURN_TASK_ID); + } + + /** + * 给节点添加用户任务的审批人与发起人相同时,处理类型枚举 + * + * @see BpmUserTaskAssignStartUserHandlerTypeEnum + * @param assignStartUserHandlerType 发起人处理类型 + * @param userTask 任务节点 + */ + public static void addAssignStartUserHandlerType(Integer assignStartUserHandlerType, UserTask userTask) { + if (assignStartUserHandlerType == null) { + return; + } + addExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, assignStartUserHandlerType.toString()); + } + + /** + * 给节点添加用户任务的审批人为空时,处理类型枚举 + * + * @see BpmUserTaskAssignEmptyHandlerTypeEnum + * @param emptyHandler 空处理 + * @param userTask 任务节点 + */ + public static void addAssignEmptyHandlerType(BpmSimpleModelNodeVO.AssignEmptyHandler emptyHandler, UserTask userTask) { + if (emptyHandler == null) { + return; + } + addExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE, StrUtil.toStringOrNull(emptyHandler.getType())); + addExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS, StrUtil.join(",", emptyHandler.getUserIds())); + } + + /** + * 解析用户任务的审批人与发起人相同时,处理类型枚举 + * + * @param userTask 任务节点 + * @return 处理类型枚举 + */ + public static Integer parseAssignStartUserHandlerType(FlowElement userTask) { + return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE)); + } + + /** + * 解析用户任务的审批人为空时,处理类型枚举 + * + * @param userTask 任务节点 + * @return 处理类型枚举 + */ + public static Integer parseAssignEmptyHandlerType(FlowElement userTask) { + return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE)); + } + + /** + * 解析用户任务的审批人为空时,处理用户 ID 数组 + * + * @param userTask 任务节点 + * @return 处理用户 ID 数组 + */ + public static List parseAssignEmptyHandlerUserIds(FlowElement userTask) { + return StrUtils.splitToLong(parseExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS), ","); + } + + /** + * 给节点添加表单字段权限元素 + * + * @param fieldsPermissions 表单字段权限 + * @param flowElement 节点 + */ + public static void addFormFieldsPermission(List> fieldsPermissions, FlowElement flowElement) { + if (CollUtil.isNotEmpty(fieldsPermissions)) { + fieldsPermissions.forEach(item -> addExtensionElement(flowElement, FORM_FIELD_PERMISSION_ELEMENT, item)); + } + } + + /** + * 解析表单字段权限 + * + * @param bpmnModel bpmnModel 对象 + * @param flowElementId 元素 ID + * @return 表单字段权限 + */ + public static Map parseFormFieldsPermission(BpmnModel bpmnModel, String flowElementId) { + if (bpmnModel == null || StrUtil.isEmpty(flowElementId)) { + return null; + } + FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId); + if (flowElement == null) { + return null; + } + List extensionElements = flowElement.getExtensionElements().get(FORM_FIELD_PERMISSION_ELEMENT); + if (CollUtil.isEmpty(extensionElements)) { + return null; + } + Map fieldsPermission = MapUtil.newHashMap(); + extensionElements.forEach(element -> { + String field = element.getAttributeValue(null, FORM_FIELD_PERMISSION_ELEMENT_FIELD_ATTRIBUTE); + String permission = element.getAttributeValue(null, FORM_FIELD_PERMISSION_ELEMENT_PERMISSION_ATTRIBUTE); + if (StrUtil.isNotEmpty(field) && StrUtil.isNotEmpty(permission)) { + fieldsPermission.put(field, permission); + } + }); + return fieldsPermission; + } + + /** + * 给节点添加操作按钮设置元素 + */ + public static void addButtonsSetting(List buttonsSetting, UserTask userTask) { + if (CollUtil.isNotEmpty(buttonsSetting)) { + List> list = CollectionUtils.convertList(buttonsSetting, item -> { + Map settingMap = Maps.newHashMapWithExpectedSize(3); + settingMap.put(BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE, String.valueOf(item.getId())); + settingMap.put(BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE, item.getDisplayName()); + settingMap.put(BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE, String.valueOf(item.getEnable())); + return settingMap; + }); + list.forEach(item -> addExtensionElement(userTask, BUTTON_SETTING_ELEMENT, item)); + } + } + + /** + * 解析操作按钮设置 + * + * @param bpmnModel bpmnModel 对象 + * @param flowElementId 元素 ID + * @return 操作按钮设置 + */ + public static Map parseButtonsSetting(BpmnModel bpmnModel, String flowElementId) { + FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId); + if (flowElement == null) { + return null; + } + List extensionElements = flowElement.getExtensionElements().get(BUTTON_SETTING_ELEMENT); + if (CollUtil.isEmpty(extensionElements)) { + return null; + } + Map buttonSettings = Maps.newHashMapWithExpectedSize(extensionElements.size()); + extensionElements.forEach(element -> { + String id = element.getAttributeValue(null, BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE); + String displayName = element.getAttributeValue(null, BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE); + String enable = element.getAttributeValue(null, BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE); + if (StrUtil.isNotEmpty(id)) { + BpmTaskRespVO.OperationButtonSetting setting = new BpmTaskRespVO.OperationButtonSetting(); + buttonSettings.put(Integer.valueOf(id), setting.setDisplayName(displayName).setEnable(Boolean.parseBoolean(enable))); + } + }); + return buttonSettings; + } + + /** + * 解析边界事件扩展元素 + * + * @param boundaryEvent 边界事件 + * @param customElement 元素 + * @return 扩展元素 + */ + public static String parseBoundaryEventExtensionElement(BoundaryEvent boundaryEvent, String customElement) { + if (boundaryEvent == null) { + return null; + } + ExtensionElement extensionElement = CollUtil.getFirst(boundaryEvent.getExtensionElements().get(customElement)); + return Optional.ofNullable(extensionElement).map(ExtensionElement::getElementText).orElse(null); + } + + public static void addSignEnable(Boolean signEnable, FlowElement userTask) { + addExtensionElement(userTask, SIGN_ENABLE, + ObjUtil.isNotNull(signEnable) ? signEnable.toString() : Boolean.FALSE.toString()); + } + + public static Boolean parseSignEnable(BpmnModel bpmnModel, String flowElementId) { + FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId); + if (flowElement == null) { + return false; + } + List extensionElements = flowElement.getExtensionElements().get(SIGN_ENABLE); + if (CollUtil.isEmpty(extensionElements)) { + return false; + } + return Convert.toBool(extensionElements.get(0).getElementText(), false); + } + + public static void addReasonRequire(Boolean reasonRequire, FlowElement userTask) { + addExtensionElement(userTask, REASON_REQUIRE, + ObjUtil.isNotNull(reasonRequire) ? reasonRequire.toString() : Boolean.FALSE.toString()); + } + + public static Boolean parseReasonRequire(BpmnModel bpmnModel, String flowElementId) { + FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId); + if (flowElement == null) { + return false; + } + List extensionElements = flowElement.getExtensionElements().get(REASON_REQUIRE); + if (CollUtil.isEmpty(extensionElements)) { + return false; + } + return Convert.toBool(extensionElements.get(0).getElementText(), false); + } + + public static void addListenerConfig(FlowableListener flowableListener, BpmSimpleModelNodeVO.ListenerHandler handler) { + FieldExtension fieldExtension = new FieldExtension(); + fieldExtension.setFieldName("listenerConfig"); + fieldExtension.setStringValue(JsonUtils.toJsonString(handler)); + flowableListener.getFieldExtensions().add(fieldExtension); + } + + public static BpmSimpleModelNodeVO.ListenerHandler parseListenerConfig(FixedValue fixedValue) { + String expressionText = fixedValue.getExpressionText(); + Assert.notNull(expressionText, "监听器扩展字段({})不能为空", expressionText); + return JsonUtils.parseObject(expressionText, BpmSimpleModelNodeVO.ListenerHandler.class); + } + + public static BpmTriggerTypeEnum parserTriggerType(FlowElement flowElement) { + Integer triggerType = NumberUtils.parseInt(parseExtensionElement(flowElement, TRIGGER_TYPE)); + return BpmTriggerTypeEnum.typeOf(triggerType); + } + + public static String parserTriggerParam(FlowElement flowElement) { + return parseExtensionElement(flowElement, TRIGGER_PARAM); + } + + /** + * 给节点添加节点类型 + * + * @param nodeType 节点类型 + * @param flowElement 节点 + */ + public static void addNodeType(Integer nodeType, FlowElement flowElement) { + addExtensionElement(flowElement, BpmnModelConstants.NODE_TYPE, nodeType); + } + + /** + * 解析节点类型 + * + * @param flowElement 节点 + * @return 节点类型 + */ + public static Integer parseNodeType(FlowElement flowElement) { + return NumberUtils.parseInt(parseExtensionElement(flowElement, BpmnModelConstants.NODE_TYPE)); + } + + // ========== BPM 简单查找相关的方法 ========== + + /** + * 根据节点,获取入口连线 + * + * @param source 起始节点 + * @return 入口连线列表 + */ + public static List getElementIncomingFlows(FlowElement source) { + if (source instanceof FlowNode) { + return ((FlowNode) source).getIncomingFlows(); + } + return new ArrayList<>(); + } + + /** + * 根据节点,获取出口连线 + * + * @param source 起始节点 + * @return 出口连线列表 + */ + public static List getElementOutgoingFlows(FlowElement source) { + if (source instanceof FlowNode) { + return ((FlowNode) source).getOutgoingFlows(); + } + return new ArrayList<>(); + } + + /** + * 获取流程元素信息 + * + * @param model bpmnModel 对象 + * @param flowElementId 元素 ID + * @return 元素信息 + */ + public static FlowElement getFlowElementById(BpmnModel model, String flowElementId) { + Process process = model.getMainProcess(); + return process.getFlowElement(flowElementId); + } + + /** + * 获得 BPMN 流程中,指定的元素们 + * + * @param model 模型 + * @param clazz 指定元素。例如说,{@link UserTask}、{@link Gateway} 等等 + * @return 元素们 + */ + @SuppressWarnings("unchecked") + public static List getBpmnModelElements(BpmnModel model, Class clazz) { + List result = new ArrayList<>(); + model.getProcesses().forEach(process -> process.getFlowElements().forEach(flowElement -> { + if (flowElement.getClass().isAssignableFrom(clazz)) { + result.add((T) flowElement); + } + })); + return result; + } + + public static StartEvent getStartEvent(BpmnModel model) { + Process process = model.getMainProcess(); + // 从 initialFlowElement 找 + FlowElement startElement = process.getInitialFlowElement(); + if (startElement instanceof StartEvent) { + return (StartEvent) startElement; + } + // 从 flowElementList 找 + return (StartEvent) CollUtil.findOne(process.getFlowElements(), flowElement -> flowElement instanceof StartEvent); + } + + public static EndEvent getEndEvent(BpmnModel model) { + Process process = model.getMainProcess(); + // 从 flowElementList 找 endEvent + return (EndEvent) CollUtil.findOne(process.getFlowElements(), flowElement -> flowElement instanceof EndEvent); + } + + public static BpmnModel getBpmnModel(byte[] bpmnBytes) { + if (ArrayUtil.isEmpty(bpmnBytes)) { + return null; + } + BpmnXMLConverter converter = new BpmnXMLConverter(); + // 补充说明:由于在 Flowable 中自定义了属性,所以 validateSchema 传递 false + return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), false, false); + } + + public static String getBpmnXml(BpmnModel model) { + if (model == null) { + return null; + } + BpmnXMLConverter converter = new BpmnXMLConverter(); + return StrUtil.utf8Str(converter.convertToXML(model)); + } + + public static String getBpmnXml(byte[] bpmnBytes) { + if (ArrayUtil.isEmpty(bpmnBytes)) { + return null; + } + return StrUtil.utf8Str(bpmnBytes); + } + + // ========== BPMN 复杂遍历相关的方法 ========== + + /** + * 找到 source 节点之前的所有用户任务节点 + * + * @param source 起始节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 已找到的用户任务节点 + * @return 用户任务节点 数组 + */ + public static List getPreviousUserTaskList(FlowElement source, Set hasSequenceFlow, List userTaskList) { + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + userTaskList = getPreviousUserTaskList(source.getSubProcess(), hasSequenceFlow, userTaskList); + } + + // 根据类型,获取入口连线 + List sequenceFlows = getElementIncomingFlows(source); + if (sequenceFlows == null) { + return userTaskList; + } + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 类型为用户节点,则新增父级节点 + if (sequenceFlow.getSourceFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getSourceFlowElement()); + } + // 类型为子流程,则添加子流程开始节点出口处相连的节点 + if (sequenceFlow.getSourceFlowElement() instanceof SubProcess) { + // 获取子流程用户任务节点 + List childUserTaskList = findChildProcessUserTaskList((StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (CollUtil.isNotEmpty(childUserTaskList)) { + userTaskList.addAll(childUserTaskList); + } + } + // 继续迭代 + userTaskList = getPreviousUserTaskList(sequenceFlow.getSourceFlowElement(), hasSequenceFlow, userTaskList); + } + return userTaskList; + } + + /** + * 迭代获取子流程用户任务节点 + * + * @param source 起始节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 需要撤回的用户任务列表 + * @return 用户任务节点 + */ + public static List findChildProcessUserTaskList(FlowElement source, Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows == null) { + return userTaskList; + } + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 + if (sequenceFlow.getTargetFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = findChildProcessUserTaskList((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (CollUtil.isNotEmpty(childUserTaskList)) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + userTaskList = findChildProcessUserTaskList(sequenceFlow.getTargetFlowElement(), hasSequenceFlow, userTaskList); + } + return userTaskList; + } + + /** + * 迭代从后向前扫描,判断目标节点相对于当前节点是否是串行 + * 不存在直接退回到子流程中的情况,但存在从子流程出去到父流程情况 + * + * @param source 起始节点 + * @param target 目标节点 + * @param visitedElements 已经经过的连线的 ID,用于判断线路是否重复 + * @return 结果 + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean isSequentialReachable(FlowElement source, FlowElement target, Set visitedElements) { + visitedElements = visitedElements == null ? new HashSet<>() : visitedElements; + // 不能是开始事件和子流程 + if (source instanceof StartEvent && isInEventSubprocess(source)) { + return false; + } + + // 根据类型,获取入口连线 + List sequenceFlows = getElementIncomingFlows(source); + if (CollUtil.isEmpty(sequenceFlows)) { + return true; + } + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (visitedElements.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + visitedElements.add(sequenceFlow.getId()); + // 这条线路存在目标节点,这条线路完成,进入下个线路 + FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement(); + if (target.getId().equals(sourceFlowElement.getId())) { + continue; + } + // 如果目标节点为并行网关,则不继续 + if (sourceFlowElement instanceof ParallelGateway) { + return false; + } + // 否则就继续迭代 + if (!isSequentialReachable(sourceFlowElement, target, visitedElements)) { + return false; + } + } + return true; + } + + /** + * 判断当前节点是否属于不同的子流程 + * + * @param flowElement 被判断的节点 + * @return true 表示属于子流程 + */ + private static boolean isInEventSubprocess(FlowElement flowElement) { + FlowElementsContainer flowElementsContainer = flowElement.getParentContainer(); + while (flowElementsContainer != null) { + if (flowElementsContainer instanceof EventSubProcess) { + return true; + } + + if (flowElementsContainer instanceof FlowElement) { + flowElementsContainer = ((FlowElement) flowElementsContainer).getParentContainer(); + } else { + flowElementsContainer = null; + } + } + return false; + } + + /** + * 根据正在运行的任务节点,迭代获取子级任务节点列表,向后找 + * + * @param source 起始节点 + * @param runTaskKeyList 正在运行的任务 Key,用于校验任务节点是否是正在运行的节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 需要撤回的用户任务列表 + * @return 子级任务节点列表 + */ + public static List iteratorFindChildUserTasks(FlowElement source, List runTaskKeyList, + Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + userTaskList = iteratorFindChildUserTasks(source.getSubProcess(), runTaskKeyList, hasSequenceFlow, userTaskList); + } + + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows == null) { + return userTaskList; + } + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 + if (sequenceFlow.getTargetFlowElement() instanceof UserTask && runTaskKeyList.contains((sequenceFlow.getTargetFlowElement()).getId())) { + userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = iteratorFindChildUserTasks((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), runTaskKeyList, hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (CollUtil.isNotEmpty(childUserTaskList)) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + userTaskList = iteratorFindChildUserTasks(sequenceFlow.getTargetFlowElement(), runTaskKeyList, hasSequenceFlow, userTaskList); + } + return userTaskList; + } + + // ========== BPMN 流程预测相关的方法 ========== + + /** + * 流程预测,返回 StartEvent、UserTask、ServiceTask、EndEvent 节点元素,最终是 List 串行结果 + * + * @param bpmnModel BPMN 图 + * @param variables 变量 + * @return 节点元素数组 + */ + public static List simulateProcess(BpmnModel bpmnModel, Map variables) { + List resultElements = new ArrayList<>(); + Set visitElements = new HashSet<>(); + + // 从 StartEvent 开始遍历 + StartEvent startEvent = getStartEvent(bpmnModel); + simulateNextFlowElements(startEvent, variables, resultElements, visitElements); + + // 将 EndEvent 放在末尾。原因是,DFS 遍历,可能 EndEvent 在 resultElements 中 + List endEvents = CollUtil.removeWithAddIf(resultElements, + flowElement -> flowElement instanceof EndEvent); + resultElements.addAll(endEvents); + return resultElements; + } + + @SuppressWarnings("PatternVariableCanBeUsed") + private static void simulateNextFlowElements(FlowElement currentElement, Map variables, + List resultElements, Set visitElements) { + // 如果为空,或者已经遍历过,则直接结束 + if (currentElement == null) { + return; + } + if (visitElements.contains(currentElement)) { + return; + } + visitElements.add(currentElement); + + // 情况:StartEvent/EndEvent/UserTask/ServiceTask + if (currentElement instanceof StartEvent + || currentElement instanceof EndEvent + || currentElement instanceof UserTask + || currentElement instanceof ServiceTask) { + // 添加元素 + FlowNode flowNode = (FlowNode) currentElement; + resultElements.add(flowNode); + // 遍历子节点 + flowNode.getOutgoingFlows().forEach( + nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements)); + return; + } + + // 情况:ExclusiveGateway 排它,只有一个满足条件的。如果没有,就走默认的 + if (currentElement instanceof ExclusiveGateway) { + // 查找满足条件的 SequenceFlow 路径 + SequenceFlow matchSequenceFlow = findMatchSequenceFlowByExclusiveGateway((Gateway) currentElement, variables); + // 遍历满足条件的 SequenceFlow 路径 + if (matchSequenceFlow != null) { + simulateNextFlowElements(matchSequenceFlow.getTargetFlowElement(), variables, resultElements, visitElements); + } + } + // 情况:InclusiveGateway 包容,多个满足条件的。如果没有,就走默认的 + else if (currentElement instanceof InclusiveGateway) { + // 查找满足条件的 SequenceFlow 路径 + Collection matchSequenceFlows = findMatchSequenceFlowsByInclusiveGateway((Gateway) currentElement, variables); + // 遍历满足条件的 SequenceFlow 路径 + matchSequenceFlows.forEach( + flow -> simulateNextFlowElements(flow.getTargetFlowElement(), variables, resultElements, visitElements)); + } + // 情况:ParallelGateway 并行,都满足,都走 + else if (currentElement instanceof ParallelGateway) { + Gateway gateway = (Gateway) currentElement; + // 遍历子节点 + gateway.getOutgoingFlows().forEach( + nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements)); + } + } + + /** + * 根据当前节点,获取下一个节点 + * + * @param currentElement 当前节点 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + */ + @SuppressWarnings("PatternVariableCanBeUsed") + public static List getNextFlowNodes(FlowElement currentElement, BpmnModel bpmnModel, + Map variables){ + List nextFlowNodes = new ArrayList<>(); // 下一个执行的流程节点集合 + FlowNode currentNode = (FlowNode) currentElement; // 当前执行节点的基本属性 + List outgoingFlows = currentNode.getOutgoingFlows(); // 当前节点的关联节点 + if (CollUtil.isEmpty(outgoingFlows)) { + log.warn("[getNextFlowNodes][当前节点({}) 的 outgoingFlows 为空]", currentNode.getId()); + return nextFlowNodes; + } + + // 遍历每个出口流 + for (SequenceFlow outgoingFlow : outgoingFlows) { + // 获取目标节点的基本属性 + FlowElement targetElement = bpmnModel.getFlowElement(outgoingFlow.getTargetRef()); + if (targetElement == null) { + continue; + } + // 如果是结束节点,直接返回 + if (targetElement instanceof EndEvent) { + break; + } + // 情况一:处理不同类型的网关 + if (targetElement instanceof Gateway) { + Gateway gateway = (Gateway) targetElement; + if (gateway instanceof ExclusiveGateway) { + handleExclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes); + } else if (gateway instanceof InclusiveGateway) { + handleInclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes); + } else if (gateway instanceof ParallelGateway) { + handleParallelGateway(gateway, bpmnModel, variables, nextFlowNodes); + } + } else { + // 情况二:如果不是网关,直接添加到下一个节点列表 + nextFlowNodes.add((FlowNode) targetElement); + } + } + return nextFlowNodes; + } + + /** + * 处理排它网关 + * + * @param gateway 排他网关 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + * @param nextFlowNodes 下一个执行的流程节点集合 + */ + private static void handleExclusiveGateway(Gateway gateway, BpmnModel bpmnModel, + Map variables, List nextFlowNodes) { + // 查找满足条件的 SequenceFlow 路径 + SequenceFlow matchSequenceFlow = findMatchSequenceFlowByExclusiveGateway(gateway, variables); + // 遍历满足条件的 SequenceFlow 路径 + if (matchSequenceFlow != null) { + FlowElement targetElement = bpmnModel.getFlowElement(matchSequenceFlow.getTargetRef()); + if (targetElement instanceof FlowNode) { + nextFlowNodes.add((FlowNode) targetElement); + } + } + } + + /** + * 处理排它网关(Exclusive Gateway),选择符合条件的路径 + * + * @param gateway 排他网关 + * @param variables 流程变量 + * @return 符合条件的路径 + */ + private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map variables) { + SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) + && (evalConditionExpress(variables, flow.getConditionExpression()))); + if (matchSequenceFlow == null) { + matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), + flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 + if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) { + matchSequenceFlow = gateway.getOutgoingFlows().get(0); + } + } + return matchSequenceFlow; + } + + /** + * 处理包容网关 + * + * @param gateway 排他网关 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + * @param nextFlowNodes 下一个执行的流程节点集合 + */ + private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel, + Map variables, List nextFlowNodes) { + // 查找满足条件的 SequenceFlow 路径集合 + Collection matchSequenceFlows = findMatchSequenceFlowsByInclusiveGateway(gateway, variables); + // 遍历满足条件的 SequenceFlow 路径,获取目标节点 + matchSequenceFlows.forEach(flow -> { + FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef()); + if (targetElement instanceof FlowNode) { + nextFlowNodes.add((FlowNode) targetElement); + } + }); + } + + /** + * 处理排它网关(Inclusive Gateway),选择符合条件的路径 + * + * @param gateway 排他网关 + * @param variables 流程变量 + * @return 符合条件的路径 + */ + private static Collection findMatchSequenceFlowsByInclusiveGateway(Gateway gateway, Map variables) { + // 查找满足条件的 SequenceFlow 路径 + Collection matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) + && evalConditionExpress(variables, flow.getConditionExpression())); + if (CollUtil.isEmpty(matchSequenceFlows)) { + matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), + flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 + if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) { + matchSequenceFlows = gateway.getOutgoingFlows(); + } + } + return matchSequenceFlows; + } + + + /** + * 处理并行网关 + * + * @param gateway 排他网关 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + * @param nextFlowNodes 下一个执行的流程节点集合 + */ + private static void handleParallelGateway(Gateway gateway, BpmnModel bpmnModel, + Map variables, List nextFlowNodes) { + // 并行网关,遍历所有出口路径,获取目标节点 + gateway.getOutgoingFlows().forEach(flow -> { + FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef()); + if (targetElement instanceof FlowNode) { + nextFlowNodes.add((FlowNode) targetElement); + } + }); + } + + /** + * 计算条件表达式是否为 true 满足条件 + * + * @param variables 流程实例 + * @param expression 条件表达式 + * @return 是否满足条件 + */ + public static boolean evalConditionExpress(Map variables, String expression) { + if (expression == null) { + return Boolean.FALSE; + } + // 如果 variables 为空,则创建一个的原因?可能 expression 的计算,不依赖于 variables + if (variables == null) { + variables = new HashMap<>(); + } + + // 执行计算 + try { + Object result = FlowableUtils.getExpressionValue(variables, expression); + return Boolean.TRUE.equals(result); + } catch (FlowableException ex) { + // 为什么使用 info 日志?原因是,expression 如果从 variables 取不到值,会报错。实际这种情况下,可以忽略 + log.info("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", expression, variables, ex); + return Boolean.FALSE; + } + } + + @SuppressWarnings("PatternVariableCanBeUsed") + public static boolean isSequentialUserTask(FlowElement flowElement) { + if (!(flowElement instanceof UserTask)) { + return false; + } + UserTask userTask = (UserTask) flowElement; + MultiInstanceLoopCharacteristics loopCharacteristics = userTask.getLoopCharacteristics(); + return loopCharacteristics != null && loopCharacteristics.isSequential(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java new file mode 100644 index 0000000..67c24bb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java @@ -0,0 +1,362 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormFieldVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; +import lombok.SneakyThrows; +import org.flowable.common.engine.api.delegate.Expression; +import org.flowable.common.engine.api.variable.VariableContainer; +import org.flowable.common.engine.impl.el.ExpressionManager; +import org.flowable.common.engine.impl.identity.Authentication; +import org.flowable.common.engine.impl.variable.MapDelegateVariableContainer; +import org.flowable.engine.ManagementService; +import org.flowable.engine.ProcessEngineConfiguration; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.util.CommandContextUtil; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.TaskInfo; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * Flowable 相关的工具方法 + * + * @author 芋道源码 + */ +public class FlowableUtils { + + // ========== User 相关的工具方法 ========== + + public static void setAuthenticatedUserId(Long userId) { + Authentication.setAuthenticatedUserId(String.valueOf(userId)); + } + + public static void clearAuthenticatedUserId() { + Authentication.setAuthenticatedUserId(null); + } + + public static V executeAuthenticatedUserId(Long userId, Callable callable) { + setAuthenticatedUserId(userId); + try { + return callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + clearAuthenticatedUserId(); + } + } + + public static String getTenantId() { + Long tenantId = TenantContextHolder.getTenantId(); + return tenantId != null ? String.valueOf(tenantId) : ProcessEngineConfiguration.NO_TENANT_ID; + } + + public static void execute(String tenantIdStr, Runnable runnable) { + if (ObjectUtil.isEmpty(tenantIdStr) + || Objects.equals(tenantIdStr, ProcessEngineConfiguration.NO_TENANT_ID)) { + runnable.run(); + } else { + Long tenantId = Long.valueOf(tenantIdStr); + TenantUtils.execute(tenantId, runnable); + } + } + + @SneakyThrows + public static V execute(String tenantIdStr, Callable callable) { + if (ObjectUtil.isEmpty(tenantIdStr) + || Objects.equals(tenantIdStr, ProcessEngineConfiguration.NO_TENANT_ID)) { + return callable.call(); + } else { + Long tenantId = Long.valueOf(tenantIdStr); + return TenantUtils.execute(tenantId, callable); + } + } + + // ========== Execution 相关的工具方法 ========== + + /** + * 格式化多实例(并签、或签)的 collectionVariable 变量(多实例对应的多审批人列表) + * + * @param activityId 活动编号 + * @return collectionVariable 变量 + */ + public static String formatExecutionCollectionVariable(String activityId) { + return activityId + "_assignees"; + } + + /** + * 格式化多实例(并签、或签)的 collectionElementVariable 变量(当前实例对应的一个审批人) + * + * @param activityId 活动编号 + * @return collectionElementVariable 变量 + */ + public static String formatExecutionCollectionElementVariable(String activityId) { + return activityId + "_assignee"; + } + + // ========== ProcessInstance 相关的工具方法 ========== + + public static Integer getProcessInstanceStatus(ProcessInstance processInstance) { + return getProcessInstanceStatus(processInstance.getProcessVariables()); + } + + public static Integer getProcessInstanceStatus(HistoricProcessInstance processInstance) { + return getProcessInstanceStatus(processInstance.getProcessVariables()); + } + + /** + * 获得流程实例的状态 + * + * @param processVariables 流程实例的 variables + * @return 状态 + */ + private static Integer getProcessInstanceStatus(Map processVariables) { + return (Integer) processVariables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); + } + + /** + * 获得流程实例的审批原因 + * + * @param processInstance 流程实例 + * @return 审批原因 + */ + public static String getProcessInstanceReason(HistoricProcessInstance processInstance) { + return (String) processInstance.getProcessVariables().get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); + } + + /** + * 获得流程实例的表单 + * + * @param processInstance 流程实例 + * @return 表单 + */ + public static Map getProcessInstanceFormVariable(ProcessInstance processInstance) { + Map processVariables = new HashMap<>(processInstance.getProcessVariables()); + return filterProcessInstanceFormVariable(processVariables); + } + + /** + * 获得流程实例的表单 + * + * @param processInstance 流程实例 + * @return 表单 + */ + public static Map getProcessInstanceFormVariable(HistoricProcessInstance processInstance) { + Map processVariables = new HashMap<>(processInstance.getProcessVariables()); + return filterProcessInstanceFormVariable(processVariables); + } + + /** + * 过滤流程实例的表单 + * + * 为什么要过滤?目前使用 processVariables 存储所有流程实例的拓展字段,需要过滤掉一部分的系统字段,从而实现表单的展示 + * + * @param processVariables 流程实例的 variables + * @return 过滤后的表单 + */ + public static Map filterProcessInstanceFormVariable(Map processVariables) { + processVariables.remove(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); + return processVariables; + } + + /** + * 获得流程实例的发起用户选择的审批人 Map + * + * @param processInstance 流程实例 + * @return 发起用户选择的审批人 Map + */ + public static Map> getStartUserSelectAssignees(ProcessInstance processInstance) { + return processInstance != null ? getStartUserSelectAssignees(processInstance.getProcessVariables()) : null; + } + + /** + * 获得流程实例的发起用户选择的审批人 Map + * + * @param processVariables 流程变量 + * @return 发起用户选择的审批人 Map + */ + @SuppressWarnings("unchecked") + public static Map> getStartUserSelectAssignees(Map processVariables) { + if (processVariables == null) { + return new HashMap<>(); + } + return (Map>) processVariables.get( + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES); + } + + /** + * 获得流程实例的审批用户选择的下一个节点的审批人 Map + * + * @param processInstance 流程实例 + * @return 审批用户选择的下一个节点的审批人Map + */ + public static Map> getApproveUserSelectAssignees(ProcessInstance processInstance) { + return processInstance != null ? getApproveUserSelectAssignees(processInstance.getProcessVariables()) : null; + } + + /** + * 获得流程实例的审批用户选择的下一个节点的审批人 Map + * + * @param processVariables 流程变量 + * @return 审批用户选择的下一个节点的审批人Map Map + */ + @SuppressWarnings("unchecked") + public static Map> getApproveUserSelectAssignees(Map processVariables) { + if (processVariables == null) { + return new HashMap<>(); + } + return (Map>) processVariables.get( + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES); + } + + /** + * 获得流程实例的摘要 + * + * 仅有 {@link BpmModelFormTypeEnum#getType()} 表单,才有摘要。 + * 原因是,只有它才有表单项的配置,从而可以根据配置,展示摘要。 + * + * @param processDefinitionInfo 流程定义 + * @param processVariables 流程实例的 variables + * @return 摘要 + */ + public static List> getSummary(BpmProcessDefinitionInfoDO processDefinitionInfo, + Map processVariables) { + // 只有流程表单才会显示摘要! + if (ObjectUtil.isNull(processDefinitionInfo) + || !BpmModelFormTypeEnum.NORMAL.getType().equals(processDefinitionInfo.getFormType())) { + return null; + } + + // 解析表单配置 + Map formFieldsMap = new HashMap<>(); + processDefinitionInfo.getFormFields().forEach(formFieldStr -> { + BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class); + if (formField != null) { + formFieldsMap.put(formField.getField(), formField); + } + }); + + // 情况一:当自定义了摘要 + if (ObjectUtil.isNotNull(processDefinitionInfo.getSummarySetting()) + && Boolean.TRUE.equals(processDefinitionInfo.getSummarySetting().getEnable())) { + return convertList(processDefinitionInfo.getSummarySetting().getSummary(), item -> { + BpmFormFieldVO formField = formFieldsMap.get(item); + if (formField != null) { + return new KeyValue(formField.getTitle(), + processVariables.getOrDefault(item, "").toString()); + } + return null; + }); + } + + // 情况二:默认摘要展示前三个表单字段 + return formFieldsMap.entrySet().stream() + .limit(3) + .map(entry -> new KeyValue<>(entry.getValue().getTitle(), + MapUtil.getStr(processVariables, entry.getValue().getField(), ""))) + .collect(Collectors.toList()); + } + + // ========== Task 相关的工具方法 ========== + + /** + * 获得任务的状态 + * + * @param task 任务 + * @return 状态 + */ + public static Integer getTaskStatus(TaskInfo task) { + return (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS); + } + + /** + * 获得任务的审批原因 + * + * @param task 任务 + * @return 审批原因 + */ + public static String getTaskReason(TaskInfo task) { + return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON); + } + + /** + * 获得任务的签名图片 URL + * + * @param task 任务 + * @return 签名图片 URL + */ + public static String getTaskSignPicUrl(TaskInfo task) { + return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_SIGN_PIC_URL); + } + + /** + * 获得任务的表单 + * + * @param task 任务 + * @return 表单 + */ + public static Map getTaskFormVariable(TaskInfo task) { + Map formVariables = new HashMap<>(task.getTaskLocalVariables()); + filterTaskFormVariable(formVariables); + return formVariables; + } + + /** + * 过滤任务的表单 + * + * 为什么要过滤?目前使用 taskLocalVariables 存储所有任务的拓展字段,需要过滤掉一部分的系统字段,从而实现表单的展示 + * + * @param taskLocalVariables 任务的 taskLocalVariables + * @return 过滤后的表单 + */ + public static Map filterTaskFormVariable(Map taskLocalVariables) { + taskLocalVariables.remove(BpmnVariableConstants.TASK_VARIABLE_STATUS); + taskLocalVariables.remove(BpmnVariableConstants.TASK_VARIABLE_REASON); + return taskLocalVariables; + } + + // ========== Expression 相关的工具方法 ========== + + private static Object getExpressionValue(VariableContainer variableContainer, String expressionString, + ProcessEngineConfigurationImpl processEngineConfiguration) { + assert processEngineConfiguration != null; + ExpressionManager expressionManager = processEngineConfiguration.getExpressionManager(); + assert expressionManager != null; + Expression expression = expressionManager.createExpression(expressionString); + return expression.getValue(variableContainer); + } + + public static Object getExpressionValue(VariableContainer variableContainer, String expressionString) { + ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(); + if (processEngineConfiguration != null) { + return getExpressionValue(variableContainer, expressionString, processEngineConfiguration); + } + // 如果 ProcessEngineConfigurationImpl 获取不到,则需要通过 ManagementService 来获取 + ManagementService managementService = SpringUtil.getBean(ManagementService.class); + assert managementService != null; + return managementService.executeCommand(context -> + getExpressionValue(variableContainer, expressionString, CommandContextUtil.getProcessEngineConfiguration())); + } + + public static Object getExpressionValue(Map variable, String expressionString) { + VariableContainer variableContainer = new MapDelegateVariableContainer(variable, VariableContainer.empty()); + return getExpressionValue(variableContainer, expressionString); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java new file mode 100644 index 0000000..bd427e3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -0,0 +1,1007 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.*; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.ConditionGroups; +import cn.iocoder.yudao.module.bpm.enums.definition.*; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate; +import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener; +import cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener; +import org.flowable.bpmn.BpmnAutoLayout; +import org.flowable.bpmn.constants.BpmnXMLConstants; +import org.flowable.bpmn.model.Process; +import org.flowable.bpmn.model.*; +import org.flowable.engine.delegate.ExecutionListener; +import org.flowable.engine.delegate.TaskListener; + +import java.util.*; + +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; +import static java.util.Arrays.asList; + +/** + * 仿钉钉/飞书的模型相关的工具方法 + *

+ * 1. 核心的逻辑实现,可见 {@link #buildBpmnModel(String, String, BpmSimpleModelNodeVO)} 方法 + * 2. 所有的 BpmSimpleModelNodeVO 转换成 BPMN FlowNode 元素,可见 {@link NodeConvert} 实现类 + * + * @author jason + */ +public class SimpleModelUtils { + + private static final Map NODE_CONVERTS = MapUtil.newHashMap(); + + static { + List converts = asList(new StartNodeConvert(), new EndNodeConvert(), + new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(), new TransactorNodeConvert(), + new DelayTimerNodeConvert(), new TriggerNodeConvert(), + new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert(), new RouteBranchNodeConvert(), + new ChildProcessConvert()); + converts.forEach(convert -> NODE_CONVERTS.put(convert.getType(), convert)); + } + + /** + * 仿钉钉流程设计模型数据结构(json)转换成 Bpmn Model + *

+ * 整体逻辑如下: + * 1. 创建:BpmnModel、Process 对象 + * 2. 转换:将 BpmSimpleModelNodeVO 转换成 BPMN FlowNode 元素 + * 3. 连接:构建并添加节点之间的连线 Sequence Flow + * + * @param processId 流程标识 + * @param processName 流程名称 + * @param simpleModelNode 仿钉钉流程设计模型数据结构 + * @return Bpmn Model + */ + public static BpmnModel buildBpmnModel(String processId, String processName, BpmSimpleModelNodeVO simpleModelNode) { + // 1. 创建 BpmnModel + BpmnModel bpmnModel = new BpmnModel(); + bpmnModel.setTargetNamespace(BpmnXMLConstants.BPMN2_NAMESPACE); // 设置命名空间。不加这个,解析 Message 会报 NPE 异常 + // 创建 Process 对象 + Process process = new Process(); + process.setId(processId); + process.setName(processName); + process.setExecutable(Boolean.TRUE); + bpmnModel.addProcess(process); + + // 2.1 创建 StartNode 节点 + // 原因是:目前前端的第一个节点是“发起人节点”,所以这里构建一个 StartNode,用于创建 Bpmn 的 StartEvent 节点 + BpmSimpleModelNodeVO startNode = buildStartNode(); + startNode.setChildNode(simpleModelNode); + // 2.2 将前端传递的 simpleModelNode 数据结构(json),转换成从 BPMN FlowNode 元素,并添加到 Main Process 中 + traverseNodeToBuildFlowNode(startNode, process); + + // 3. 构建并添加节点之间的连线 Sequence Flow + EndEvent endEvent = getEndEvent(bpmnModel); + traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId()); + + // 4. 自动布局 + new BpmnAutoLayout(bpmnModel).execute(); + return bpmnModel; + } + + private static BpmSimpleModelNodeVO buildStartNode() { + return new BpmSimpleModelNodeVO().setId(START_EVENT_NODE_ID) + .setName(BpmSimpleModelNodeTypeEnum.START_NODE.getName()) + .setType(BpmSimpleModelNodeTypeEnum.START_NODE.getType()); + } + + /** + * 遍历节点,构建 FlowNode 元素 + * + * @param node SIMPLE 节点 + * @param process BPMN 流程 + */ + private static void traverseNodeToBuildFlowNode(BpmSimpleModelNodeVO node, Process process) { + // 1. 判断是否有效节点 + if (!isValidNode(node)) { + return; + } + BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(node.getType()); + Assert.notNull(nodeType, "模型节点类型({})不支持", node.getType()); + + // 2. 处理当前节点 + NodeConvert nodeConvert = NODE_CONVERTS.get(nodeType); + Assert.notNull(nodeConvert, "模型节点类型的转换器({})不存在", node.getType()); + List flowElements = nodeConvert.convertList(node); + flowElements.forEach(process::addFlowElement); + + // 3.1 情况一:如果当前是分支节点,并且存在条件节点,则处理每个条件的子节点 + if (BpmSimpleModelNodeTypeEnum.isBranchNode(node.getType()) + && CollUtil.isNotEmpty(node.getConditionNodes())) { + // 注意:这里的 item.getChildNode() 处理的是每个条件的子节点,不是处理条件 + node.getConditionNodes().forEach(item -> traverseNodeToBuildFlowNode(item.getChildNode(), process)); + } + + // 3.2 情况二:如果有“子”节点,则递归处理子节点 + traverseNodeToBuildFlowNode(node.getChildNode(), process); + } + + /** + * 遍历节点,构建 SequenceFlow 元素 + * + * @param process Bpmn 流程 + * @param node 当前节点 + * @param targetNodeId 目标节点 ID + */ + private static void traverseNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) { + // 1.1 无效节点返回 + if (!isValidNode(node)) { + return; + } + // 1.2 END_NODE 直接返回 + BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(node.getType()); + Assert.notNull(nodeType, "模型节点类型不支持"); + if (nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { + return; + } + + // 2.1 情况一:普通节点 + if (!BpmSimpleModelNodeTypeEnum.isBranchNode(node.getType())) { + traverseNormalNodeToBuildSequenceFlow(process, node, targetNodeId); + } else { + // 2.2 情况二:分支节点 + traverseBranchNodeToBuildSequenceFlow(process, node, targetNodeId); + } + } + + /** + * 遍历普通(非条件)节点,构建 SequenceFlow 元素 + * + * @param process Bpmn 流程 + * @param node 当前节点 + * @param targetNodeId 目标节点 ID + */ + private static void traverseNormalNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) { + BpmSimpleModelNodeVO childNode = node.getChildNode(); + boolean isChildNodeValid = isValidNode(childNode); + // 情况一:有“子”节点,则建立连线 + // 情况二:没有“子节点”,则直接跟 targetNodeId 建立连线。例如说,结束节点、条件分支(分支节点的孩子节点或聚合节点)的最后一个节点 + String finalTargetNodeId = isChildNodeValid ? childNode.getId() : targetNodeId; + + // 如果没有附加节点:则直接建立连线 + if (StrUtil.isEmpty(node.getAttachNodeId())) { + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId); + process.addFlowElement(sequenceFlow); + } else { + // 如果有附加节点:需要先建立和附加节点的连线,再建立附加节点和目标节点的连线。例如说,触发器节点(HTTP 回调) + List sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), finalTargetNodeId); + sequenceFlows.forEach(process::addFlowElement); + } + + // 因为有子节点,递归调用后续子节点 + if (isChildNodeValid) { + traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId); + } + } + + /** + * 构建有附加节点的连线 + * + * @param nodeId 当前节点 ID + * @param attachNodeId 附属节点 ID + * @param targetNodeId 目标节点 ID + */ + private static List buildAttachNodeSequenceFlow(String nodeId, String attachNodeId, String targetNodeId) { + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(nodeId, attachNodeId, null, null, null); + SequenceFlow attachSequenceFlow = buildBpmnSequenceFlow(attachNodeId, targetNodeId, null, null, null); + return CollUtil.newArrayList(sequenceFlow, attachSequenceFlow); + } + + /** + * 遍历条件节点,构建 SequenceFlow 元素 + * + * @param process Bpmn 流程 + * @param node 当前节点 + * @param targetNodeId 目标节点 ID + */ + private static void traverseBranchNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) { + BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(node.getType()); + BpmSimpleModelNodeVO childNode = node.getChildNode(); + List conditionNodes = node.getConditionNodes(); + // TODO @芋艿 路由分支没有conditionNodes 这里注释会影响吗?@jason:一起帮忙瞅瞅! +// Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空"); + // 分支终点节点 ID + String branchEndNodeId = null; + if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE) { // 条件分支或路由分支 + // 分两种情况 1. 分支节点有孩子节点为孩子节点 Id 2. 分支节点孩子为无效节点时 (分支嵌套且为分支最后一个节点) 为分支终点节点 ID + branchEndNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId; + } else if (nodeType == BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) { // 并行分支或包容分支 + // 分支节点:分支终点节点 Id 为程序创建的网关集合节点。目前不会从前端传入。 + branchEndNodeId = buildGatewayJoinId(node.getId()); + } + Assert.notEmpty(branchEndNodeId, "分支终点节点 Id 不能为空"); + + // 3. 遍历分支节点 + if (nodeType == BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE) { + // 路由分支遍历 + for (BpmSimpleModelNodeVO.RouterSetting router : node.getRouterGroups()) { + SequenceFlow sequenceFlow = RouteBranchNodeConvert.buildSequenceFlow(node.getId(), router); + process.addFlowElement(sequenceFlow); + } + } else { + // 下面的注释,以如下情况举例子。分支 1:A->B->C->D->E,分支 2:A->D->E。其中,A 为分支节点, D 为 A 孩子节点 + for (BpmSimpleModelNodeVO item : conditionNodes) { + Assert.isTrue(Objects.equals(item.getType(), BpmSimpleModelNodeTypeEnum.CONDITION_NODE.getType()), + "条件节点类型({})不符合", item.getType()); + BpmSimpleModelNodeVO conditionChildNode = item.getChildNode(); + // 3.1 分支有后续节点。即分支 1: A->B->C->D 的情况 + if (isValidNode(conditionChildNode)) { + // 3.1.1 建立与后续的节点的连线。例如说,建立 A->B 的连线 + SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), conditionChildNode.getId(), item); + process.addFlowElement(sequenceFlow); + // 3.1.2 递归调用后续节点连线。例如说,建立 B->C->D 的连线 + traverseNodeToBuildSequenceFlow(process, conditionChildNode, branchEndNodeId); + } else { + // 3.2 分支没有后续节点。例如说,建立 A->D 的连线 + SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, item); + process.addFlowElement(sequenceFlow); + } + } + } + + // 4.1 如果是并行分支、包容分支,由于是程序创建的聚合网关,需要手工创建聚合网关和下一个节点的连线 + if (nodeType == BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) { + String nextNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId; + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(branchEndNodeId, nextNodeId); + process.addFlowElement(sequenceFlow); + // 4.2 如果是路由分支,需要连接后续节点为默认路由 + } else if (nodeType == BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE) { + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), branchEndNodeId, node.getRouterDefaultFlowId(), + null, null); + process.addFlowElement(sequenceFlow); + } + + // 5. 递归调用后续节点 继续递归。例如说,建立 D->E 的连线 + traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId); + } + + private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId) { + return buildBpmnSequenceFlow(sourceId, targetId, null, null, null); + } + + private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId, + String sequenceFlowId, String sequenceFlowName, + String conditionExpression) { + Assert.notEmpty(sourceId, "sourceId 不能为空"); + Assert.notEmpty(targetId, "targetId 不能为空"); + // TODO @jason:如果 sequenceFlowId 不存在的时候,是不是要生成一个默认的 sequenceFlowId? @芋艿: 貌似不需要,Flowable 会默认生成;TODO @jason:建议还是搞一个,主要是后续好排查问题。 + // TODO @jason:如果 name 不存在的时候,是不是要生成一个默认的 name? @芋艿: 不需要生成默认的吧? 这个会在流程图展示的, 一般用户填写的。不好生成默认的吧;TODO @jason:建议还是搞一个,主要是后续好排查问题。 + SequenceFlow sequenceFlow = new SequenceFlow(sourceId, targetId); + if (StrUtil.isNotEmpty(sequenceFlowId)) { + sequenceFlow.setId(sequenceFlowId); + } + if (StrUtil.isNotEmpty(sequenceFlowName)) { + sequenceFlow.setName(sequenceFlowName); + } + if (StrUtil.isNotEmpty(conditionExpression)) { + sequenceFlow.setConditionExpression(conditionExpression); + } + return sequenceFlow; + } + + public static boolean isValidNode(BpmSimpleModelNodeVO node) { + return node != null && node.getId() != null; + } + + public static boolean isSequentialApproveNode(BpmSimpleModelNodeVO node) { + return BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType().equals(node.getType()) + && BpmUserTaskApproveMethodEnum.SEQUENTIAL.getMethod().equals(node.getApproveMethod()); + } + + // ========== 各种 convert 节点的方法: BpmSimpleModelNodeVO => BPMN FlowElement ========== + + private interface NodeConvert { + + default List convertList(BpmSimpleModelNodeVO node) { + return Collections.singletonList(convert(node)); + } + + default FlowElement convert(BpmSimpleModelNodeVO node) { + throw new UnsupportedOperationException("请实现该方法"); + } + + BpmSimpleModelNodeTypeEnum getType(); + + } + + private static class StartNodeConvert implements NodeConvert { + + @Override + public StartEvent convert(BpmSimpleModelNodeVO node) { + StartEvent startEvent = new StartEvent(); + startEvent.setId(node.getId()); + startEvent.setName(node.getName()); + return startEvent; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.START_NODE; + } + + } + + private static class EndNodeConvert implements NodeConvert { + + @Override + public EndEvent convert(BpmSimpleModelNodeVO node) { + EndEvent endEvent = new EndEvent(); + endEvent.setId(node.getId()); + endEvent.setName(node.getName()); + // TODO @芋艿 + jason:要不要加一个终止定义? + return endEvent; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.END_NODE; + } + + } + + private static class StartUserNodeConvert implements NodeConvert { + + @Override + public UserTask convert(BpmSimpleModelNodeVO node) { + UserTask userTask = new UserTask(); + userTask.setId(node.getId()); + userTask.setName(node.getName()); + + // 人工审批 + addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, BpmUserTaskApproveTypeEnum.USER.getType()); + // 候选人策略为发起人自己 + addCandidateElements(BpmTaskCandidateStrategyEnum.START_USER.getStrategy(), null, userTask); + // 添加表单字段权限属性元素 + addFormFieldsPermission(node.getFieldsPermission(), userTask); + // 添加操作按钮配置属性元素 + addButtonsSetting(node.getButtonsSetting(), userTask); + // 使用自动通过策略 + // TODO @芋艿 复用了SKIP, 是否需要新加一个策略;TODO @芋艿:【回复】是不是应该类似飞书,搞个草稿状态。待定;还有一种策略,不标记自动通过,而是首次发起后,第一个节点,自动通过; + addAssignStartUserHandlerType(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType(), userTask); + return userTask; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.START_USER_NODE; + } + + } + + private static class ApproveNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + List flowElements = new ArrayList<>(2); + // 1. 构建用户任务 + UserTask userTask = buildBpmnUserTask(node); + flowElements.add(userTask); + + // 2. 添加用户任务的 Timer Boundary Event, 用于任务的审批超时处理 + if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) { + BoundaryEvent boundaryEvent = buildUserTaskTimeoutBoundaryEvent(userTask, node.getTimeoutHandler()); + flowElements.add(boundaryEvent); + } + return flowElements; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.APPROVE_NODE; + } + + /** + * 添加 UserTask 用户的审批超时 BoundaryEvent 事件 + * + * @param userTask 审批任务 + * @param timeoutHandler 超时处理器 + * @return BoundaryEvent 超时事件 + */ + private BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask, + BpmSimpleModelNodeVO.TimeoutHandler timeoutHandler) { + // 1. 创建 Timeout Boundary Event + String timeCycle = null; + if (Objects.equals(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType(), timeoutHandler.getType()) && + timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) { + timeCycle = String.format("R%d/%s", + timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration()); + } + BoundaryEvent boundaryEvent = buildTimeoutBoundaryEvent(userTask, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType(), + timeoutHandler.getTimeDuration(), timeCycle, null); + + // 2 添加超时执行动作元素 + addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType()); + return boundaryEvent; + } + + private UserTask buildBpmnUserTask(BpmSimpleModelNodeVO node) { + UserTask userTask = new UserTask(); + userTask.setId(node.getId()); + userTask.setName(node.getName()); + + // 如果不是审批人节点,则直接返回 + addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, node.getApproveType()); + if (ObjectUtil.notEqual(node.getApproveType(), BpmUserTaskApproveTypeEnum.USER.getType())) { + return userTask; + } + + // 添加候选人元素 + addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), userTask); + // 添加表单字段权限属性元素 + addFormFieldsPermission(node.getFieldsPermission(), userTask); + // 添加操作按钮配置属性元素 + addButtonsSetting(node.getButtonsSetting(), userTask); + // 处理多实例(审批方式) + processMultiInstanceLoopCharacteristics(node.getApproveMethod(), node.getApproveRatio(), userTask); + // 添加任务被拒绝的处理元素 + addTaskRejectElements(node.getRejectHandler(), userTask); + // 添加用户任务的审批人与发起人相同时的处理元素 + addAssignStartUserHandlerType(node.getAssignStartUserHandlerType(), userTask); + // 添加用户任务的空处理元素 + addAssignEmptyHandlerType(node.getAssignEmptyHandler(), userTask); + // 设置审批任务的截止时间 + if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) { + userTask.setDueDate(node.getTimeoutHandler().getTimeDuration()); + } + // 设置监听器 + addUserTaskListener(node, userTask); + // 添加是否需要签名 + addSignEnable(node.getSignEnable(), userTask); + // 审批意见 + addReasonRequire(node.getReasonRequire(), userTask); + // 节点类型 + addNodeType(node.getType(), userTask); + return userTask; + } + + private void addUserTaskListener(BpmSimpleModelNodeVO node, UserTask userTask) { + List flowableListeners = new ArrayList<>(3); + if (node.getTaskCreateListener() != null + && Boolean.TRUE.equals(node.getTaskCreateListener().getEnable())) { + FlowableListener flowableListener = new FlowableListener(); + flowableListener.setEvent(TaskListener.EVENTNAME_CREATE); + flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION); + addListenerConfig(flowableListener, node.getTaskCreateListener()); + flowableListeners.add(flowableListener); + } + if (node.getTaskAssignListener() != null + && Boolean.TRUE.equals(node.getTaskAssignListener().getEnable())) { + FlowableListener flowableListener = new FlowableListener(); + flowableListener.setEvent(TaskListener.EVENTNAME_ASSIGNMENT); + flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION); + addListenerConfig(flowableListener, node.getTaskAssignListener()); + flowableListeners.add(flowableListener); + } + if (node.getTaskCompleteListener() != null + && Boolean.TRUE.equals(node.getTaskCompleteListener().getEnable())) { + FlowableListener flowableListener = new FlowableListener(); + flowableListener.setEvent(TaskListener.EVENTNAME_COMPLETE); + flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION); + addListenerConfig(flowableListener, node.getTaskCompleteListener()); + flowableListeners.add(flowableListener); + } + if (CollUtil.isNotEmpty(flowableListeners)) { + userTask.setTaskListeners(flowableListeners); + } + } + + private void processMultiInstanceLoopCharacteristics(Integer approveMethod, Integer approveRatio, UserTask userTask) { + BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod); + Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum); + // 添加审批方式的扩展属性 + addExtensionElement(userTask, USER_TASK_APPROVE_METHOD, approveMethod); + if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RANDOM) { + // 随机审批,不需要设置多实例属性 + return; + } + + // 处理多实例审批方式 + MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics(); + // 设置 collectionVariable。本系统用不到,仅仅为了 Flowable 校验不报错 + multiInstanceCharacteristics.setInputDataItem("${coll_userList}"); + if (approveMethodEnum == BpmUserTaskApproveMethodEnum.ANY) { + multiInstanceCharacteristics.setCompletionCondition(approveMethodEnum.getCompletionCondition()); + multiInstanceCharacteristics.setSequential(false); + } else if (approveMethodEnum == BpmUserTaskApproveMethodEnum.SEQUENTIAL) { + multiInstanceCharacteristics.setCompletionCondition(approveMethodEnum.getCompletionCondition()); + multiInstanceCharacteristics.setSequential(true); + multiInstanceCharacteristics.setLoopCardinality("1"); + } else if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RATIO) { + Assert.notNull(approveRatio, "通过比例不能为空"); + multiInstanceCharacteristics.setCompletionCondition( + String.format(approveMethodEnum.getCompletionCondition(), String.format("%.2f", approveRatio / 100D))); + multiInstanceCharacteristics.setSequential(false); + } + userTask.setLoopCharacteristics(multiInstanceCharacteristics); + } + + } + + private static class TransactorNodeConvert extends ApproveNodeConvert { + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE; + } + + } + + private static class CopyNodeConvert implements NodeConvert { + + @Override + public ServiceTask convert(BpmSimpleModelNodeVO node) { + ServiceTask serviceTask = new ServiceTask(); + serviceTask.setId(node.getId()); + serviceTask.setName(node.getName()); + serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + serviceTask.setImplementation("${" + BpmCopyTaskDelegate.BEAN_NAME + "}"); + + // 添加抄送候选人元素 + addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), serviceTask); + // 添加表单字段权限属性元素 + addFormFieldsPermission(node.getFieldsPermission(), serviceTask); + return serviceTask; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.COPY_NODE; + } + + } + + private static class ConditionBranchNodeConvert implements NodeConvert { + + @Override + public ExclusiveGateway convert(BpmSimpleModelNodeVO node) { + ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); + exclusiveGateway.setId(node.getId()); + // TODO @jason:setName + + // 设置默认的序列流(条件) + BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(), + item -> BooleanUtil.isTrue(item.getConditionSetting().getDefaultFlow())); + Assert.notNull(defaultSeqFlow, "条件分支节点({})的默认序列流不能为空", node.getId()); + exclusiveGateway.setDefaultFlow(defaultSeqFlow.getId()); + return exclusiveGateway; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE; + } + + } + + private static class ParallelBranchNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + ParallelGateway parallelGateway = new ParallelGateway(); + parallelGateway.setId(node.getId()); + // TODO @jason:setName + + // 并行聚合网关由程序创建,前端不需要传入 + ParallelGateway joinParallelGateway = new ParallelGateway(); + joinParallelGateway.setId(buildGatewayJoinId(node.getId())); + // TODO @jason:setName + return CollUtil.newArrayList(parallelGateway, joinParallelGateway); + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE; + } + + } + + private static class InclusiveBranchNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + InclusiveGateway inclusiveGateway = new InclusiveGateway(); + inclusiveGateway.setId(node.getId()); + // 设置默认的序列流(条件) + BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(), + item -> BooleanUtil.isTrue(item.getConditionSetting().getDefaultFlow())); + Assert.notNull(defaultSeqFlow, "包容分支节点({})的默认序列流不能为空", node.getId()); + inclusiveGateway.setDefaultFlow(defaultSeqFlow.getId()); + // TODO @jason:setName + + // 并行聚合网关由程序创建,前端不需要传入 + InclusiveGateway joinInclusiveGateway = new InclusiveGateway(); + joinInclusiveGateway.setId(buildGatewayJoinId(node.getId())); + // TODO @jason:setName + return CollUtil.newArrayList(inclusiveGateway, joinInclusiveGateway); + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE; + } + + } + + public static class ConditionNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + // 原因是:正常情况下,它不会被调用到 + throw new UnsupportedOperationException("条件节点不支持转换"); + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.CONDITION_NODE; + } + + public static SequenceFlow buildSequenceFlow(String sourceId, String targetId, + BpmSimpleModelNodeVO node) { + String conditionExpression = buildConditionExpression(node.getConditionSetting()); + return buildBpmnSequenceFlow(sourceId, targetId, node.getId(), node.getName(), conditionExpression); + } + } + + /** + * 构造条件表达式 + */ + public static String buildConditionExpression(BpmSimpleModelNodeVO.ConditionSetting conditionSetting) { + // 并行网关不需要设置条件 + if (conditionSetting == null) { + return null; + } + return buildConditionExpression(conditionSetting.getConditionType(), conditionSetting.getConditionExpression(), + conditionSetting.getConditionGroups()); + } + + public static String buildConditionExpression(BpmSimpleModelNodeVO.RouterSetting routerSetting) { + return buildConditionExpression(routerSetting.getConditionType(), routerSetting.getConditionExpression(), + routerSetting.getConditionGroups()); + } + + public static String buildConditionExpression(Integer conditionType, String conditionExpression, ConditionGroups conditionGroups) { + BpmSimpleModeConditionTypeEnum conditionTypeEnum = BpmSimpleModeConditionTypeEnum.valueOf(conditionType); + if (conditionTypeEnum == BpmSimpleModeConditionTypeEnum.EXPRESSION) { + return conditionExpression; + } + if (conditionTypeEnum == BpmSimpleModeConditionTypeEnum.RULE) { + if (conditionGroups == null || CollUtil.isEmpty(conditionGroups.getConditions())) { + return null; + } + List strConditionGroups = CollectionUtils.convertList(conditionGroups.getConditions(), item -> { + if (CollUtil.isEmpty(item.getRules())) { + return ""; + } + // 构造规则表达式 + List list = CollectionUtils.convertList(item.getRules(), (rule) -> { + String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide() + : "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号 + return String.format(" %s %s var:convertByType(%s,%s)", rule.getLeftSide(), rule.getOpCode(), rule.getLeftSide(), rightSide); + }); + // 构造条件组的表达式 + Boolean and = item.getAnd(); + return "(" + CollUtil.join(list, and ? " && " : " || ") + ")"; + }); + return String.format("${%s}", CollUtil.join(strConditionGroups, conditionGroups.getAnd() ? " && " : " || ")); + } + return null; + } + + public static class DelayTimerNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + List flowElements = new ArrayList<>(2); + // 1. 构建接收任务,通过接收任务可卡住节点 + ReceiveTask receiveTask = new ReceiveTask(); + receiveTask.setId(node.getId()); + receiveTask.setName(node.getName()); + flowElements.add(receiveTask); + + // 2. 添加接收任务的 Timer Boundary Event + if (node.getDelaySetting() != null) { + BoundaryEvent boundaryEvent = null; + if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) { + boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), + node.getDelaySetting().getDelayTime(), null, null); + } else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { + boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), + null, null, node.getDelaySetting().getDelayTime()); + } else { + throw new UnsupportedOperationException("不支持的延迟类型:" + node.getDelaySetting()); + } + flowElements.add(boundaryEvent); + } + return flowElements; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.DELAY_TIMER_NODE; + } + } + + public static class TriggerNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + Assert.notNull(node.getTriggerSetting(), "触发器节点设置不能为空"); + List flowElements = new ArrayList<>(2); + // HTTP 回调请求。需要附加一个 ReceiveTask、发起请求后、等待回调执行 + if (BpmTriggerTypeEnum.HTTP_CALLBACK.getType().equals(node.getTriggerSetting().getType())) { + Assert.notNull(node.getTriggerSetting().getHttpRequestSetting(), "触发器 HTTP 回调请求设置不能为空"); + ReceiveTask receiveTask = new ReceiveTask(); + receiveTask.setId("Activity_" + IdUtil.fastUUID()); + receiveTask.setName("HTTP 回调"); + node.setAttachNodeId(receiveTask.getId()); + flowElements.add(receiveTask); + // 重要:设置 callbackTaskDefineKey,用于 HTTP 回调 + node.getTriggerSetting().getHttpRequestSetting().setCallbackTaskDefineKey(receiveTask.getId()); + } + + // 触发器使用 ServiceTask 来实现 + ServiceTask serviceTask = new ServiceTask(); + serviceTask.setId(node.getId()); + serviceTask.setName(node.getName()); + serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + serviceTask.setImplementation("${" + BpmTriggerTaskDelegate.BEAN_NAME + "}"); + addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType()); + if (node.getTriggerSetting().getHttpRequestSetting() != null) { + addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getHttpRequestSetting()); + } + if (node.getTriggerSetting().getFormSettings() != null) { + addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getFormSettings()); + } + flowElements.add(serviceTask); + return flowElements; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.TRIGGER_NODE; + } + } + + public static class RouteBranchNodeConvert implements NodeConvert { + + @Override + public ExclusiveGateway convert(BpmSimpleModelNodeVO node) { + ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); + exclusiveGateway.setId(node.getId()); + + // 设置默认的序列流(条件) + node.setRouterDefaultFlowId("Flow_" + IdUtil.fastUUID()); + exclusiveGateway.setDefaultFlow(node.getRouterDefaultFlowId()); + return exclusiveGateway; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE; + } + + public static SequenceFlow buildSequenceFlow(String nodeId, BpmSimpleModelNodeVO.RouterSetting router) { + String conditionExpression = SimpleModelUtils.buildConditionExpression(router); + return buildBpmnSequenceFlow(nodeId, router.getNodeId(), null, null, conditionExpression); + } + + } + + private static class ChildProcessConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + List flowElements = new ArrayList<>(2); + BpmSimpleModelNodeVO.ChildProcessSetting childProcessSetting = node.getChildProcessSetting(); + List inVariables = childProcessSetting.getInVariables() == null ? + new ArrayList<>() : new ArrayList<>(childProcessSetting.getInVariables()); + CallActivity callActivity = new CallActivity(); + callActivity.setId(node.getId()); + callActivity.setName(node.getName()); + callActivity.setCalledElementType("key"); + // 1. 是否异步 + if (node.getChildProcessSetting().getAsync()) { + callActivity.setAsynchronous(true); + } + + // 2. 调用的子流程 + callActivity.setCalledElement(childProcessSetting.getCalledProcessDefinitionKey()); + callActivity.setProcessInstanceName(childProcessSetting.getCalledProcessDefinitionName()); + + // 3. 是否自动跳过子流程发起节点 + IOParameter ioParameter = new IOParameter(); + ioParameter.setSourceExpression(childProcessSetting.getSkipStartUserNode().toString()); + ioParameter.setTarget(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE); + inVariables.add(ioParameter); + + // 4. 【默认需要传递的一些变量】流程状态 + ioParameter = new IOParameter(); + ioParameter.setSource(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); + ioParameter.setTarget(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); + inVariables.add(ioParameter); + + // 5. 主→子变量传递、子->主变量传递 + callActivity.setInParameters(inVariables); + if (ArrayUtil.isNotEmpty(childProcessSetting.getOutVariables()) && ObjUtil.notEqual(childProcessSetting.getAsync(), Boolean.TRUE)) { + callActivity.setOutParameters(childProcessSetting.getOutVariables()); + } + + // 6. 子流程发起人配置 + List executionListeners = new ArrayList<>(); + FlowableListener flowableListener = new FlowableListener(); + flowableListener.setEvent(ExecutionListener.EVENTNAME_START); + flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + flowableListener.setImplementation(BpmCallActivityListener.DELEGATE_EXPRESSION); + FieldExtension fieldExtension = new FieldExtension(); + fieldExtension.setFieldName("listenerConfig"); + fieldExtension.setStringValue(JsonUtils.toJsonString(childProcessSetting.getStartUserSetting())); + flowableListener.getFieldExtensions().add(fieldExtension); + executionListeners.add(flowableListener); + callActivity.setExecutionListeners(executionListeners); + + // 7. 超时设置 + if (childProcessSetting.getTimeoutSetting() != null && Boolean.TRUE.equals(childProcessSetting.getTimeoutSetting().getEnable())) { + BoundaryEvent boundaryEvent = null; + if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) { + boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), + childProcessSetting.getTimeoutSetting().getTimeExpression(), null, null); + } else if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { + boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType(), + null, null, childProcessSetting.getTimeoutSetting().getTimeExpression()); + } + flowElements.add(boundaryEvent); + } + + // 8. 多实例 + if (childProcessSetting.getMultiInstanceSetting() != null && Boolean.TRUE.equals(childProcessSetting.getMultiInstanceSetting().getEnable())) { + MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics(); + multiInstanceCharacteristics.setSequential(childProcessSetting.getMultiInstanceSetting().getSequential()); + if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY.getType())) { + multiInstanceCharacteristics.setLoopCardinality(childProcessSetting.getMultiInstanceSetting().getSource()); + } + if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType()) || + childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { + multiInstanceCharacteristics.setInputDataItem(childProcessSetting.getMultiInstanceSetting().getSource()); + } + multiInstanceCharacteristics.setCompletionCondition(String.format(BpmUserTaskApproveMethodEnum.RATIO.getCompletionCondition(), + String.format("%.2f", childProcessSetting.getMultiInstanceSetting().getApproveRatio() / 100D))); + callActivity.setLoopCharacteristics(multiInstanceCharacteristics); + addExtensionElement(callActivity, CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE, childProcessSetting.getMultiInstanceSetting().getSourceType()); + } + + // 添加节点类型 + addNodeType(node.getType(), callActivity); + flowElements.add(callActivity); + return flowElements; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.CHILD_PROCESS; + } + + } + + private static String buildGatewayJoinId(String id) { + return id + "_join"; + } + + private static BoundaryEvent buildTimeoutBoundaryEvent(Activity attachedToRef, Integer type, + String timeDuration, String timeCycle, String timeDate) { + // 1.1 定时器边界事件 + BoundaryEvent boundaryEvent = new BoundaryEvent(); + boundaryEvent.setId("Event-" + IdUtil.fastUUID()); + boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断 + boundaryEvent.setAttachedToRef(attachedToRef); + // 1.2 定义超时时间表达式 + TimerEventDefinition eventDefinition = new TimerEventDefinition(); + if (ObjUtil.isNotNull(timeDuration)) { + eventDefinition.setTimeDuration(timeDuration); + } + if (ObjUtil.isNotNull(timeDuration)) { + eventDefinition.setTimeCycle(timeCycle); + } + if (ObjUtil.isNotNull(timeDate)) { + eventDefinition.setTimeDate(timeDate); + } + boundaryEvent.addEventDefinition(eventDefinition); + + // 2. 添加定时器边界事件类型 + addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, type); + return boundaryEvent; + } + + // ========== SIMPLE 流程预测相关的方法 ========== + + public static List simulateProcess(BpmSimpleModelNodeVO rootNode, Map variables) { + List resultNodes = new ArrayList<>(); + + // 从头开始遍历 + simulateNextNode(rootNode, variables, resultNodes); + return resultNodes; + } + + private static void simulateNextNode(BpmSimpleModelNodeVO currentNode, Map variables, + List resultNodes) { + // 如果不合法(包括为空),则直接结束 + if (!isValidNode(currentNode)) { + return; + } + BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(currentNode.getType()); + Assert.notNull(nodeType, "模型节点类型不支持"); + + // 情况:START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE/TRANSACTOR_NODE + if (nodeType == BpmSimpleModelNodeTypeEnum.START_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.START_USER_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.APPROVE_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.CHILD_PROCESS + || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { + // 添加元素 + resultNodes.add(currentNode); + } + + // 情况:CONDITION_BRANCH_NODE 排它,只有一个满足条件的。如果没有,就走默认的 + if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE) { + // 查找满足条件的 BpmSimpleModelNodeVO 节点 + BpmSimpleModelNodeVO matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), + conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) + && evalConditionExpress(variables, conditionNode.getConditionSetting())); + if (matchConditionNode == null) { + matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), + conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())); + } + Assert.notNull(matchConditionNode, "找不到条件节点({})", currentNode); + // 遍历满足条件的 BpmSimpleModelNodeVO 节点 + simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes); + } + + // 情况:INCLUSIVE_BRANCH_NODE 包容,多个满足条件的。如果没有,就走默认的 + if (nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) { + // 查找满足条件的 BpmSimpleModelNodeVO 节点 + Collection matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), + conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) + && evalConditionExpress(variables, conditionNode.getConditionSetting())); + if (CollUtil.isEmpty(matchConditionNodes)) { + matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), + conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())); + } + Assert.isTrue(!matchConditionNodes.isEmpty(), "找不到条件节点({})", currentNode); + // 遍历满足条件的 BpmSimpleModelNodeVO 节点 + matchConditionNodes.forEach(matchConditionNode -> + simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes)); + } + + // 情况:PARALLEL_BRANCH_NODE 并行,都满足,都走 + if (nodeType == BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE) { + // 遍历所有 BpmSimpleModelNodeVO 节点 + currentNode.getConditionNodes().forEach(matchConditionNode -> + simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes)); + } + + // 遍历子节点 + simulateNextNode(currentNode.getChildNode(), variables, resultNodes); + } + + public static boolean evalConditionExpress(Map variables, BpmSimpleModelNodeVO.ConditionSetting conditionSetting) { + return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/package-info.java new file mode 100644 index 0000000..52fdb7f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 bpm 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.bpm.framework; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/config/BpmWebConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/config/BpmWebConfiguration.java new file mode 100644 index 0000000..d4af61c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/config/BpmWebConfiguration.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.bpm.framework.web.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration; +import cn.iocoder.yudao.module.bpm.framework.web.core.FlowableWebFilter; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * bpm 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class BpmWebConfiguration { + + /** + * bpm 模块的 API 分组 + */ + @Bean + public GroupedOpenApi bpmGroupedOpenApi() { + return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("bpm"); + } + + /** + * 配置 Flowable Web 过滤器 + */ + @Bean + public FilterRegistrationBean flowableWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new FlowableWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.FLOWABLE_FILTER); + return registrationBean; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/core/FlowableWebFilter.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/core/FlowableWebFilter.java new file mode 100644 index 0000000..29fc7c3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/core/FlowableWebFilter.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.bpm.framework.web.core; + +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * Flowable Web 过滤器,将 userId 设置到 {@link org.flowable.common.engine.impl.identity.Authentication} 中 + * + * @author jason + */ +public class FlowableWebFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + // 设置工作流的用户 + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (userId != null) { + FlowableUtils.setAuthenticatedUserId(userId); + } + // 过滤 + chain.doFilter(request, response); + } finally { + // 清理 + FlowableUtils.clearAuthenticatedUserId(); + } + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/package-info.java new file mode 100644 index 0000000..c014170 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * bpm 模块的 web 配置 + */ +package cn.iocoder.yudao.module.bpm.framework.web; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/package-info.java new file mode 100644 index 0000000..9d02824 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/package-info.java @@ -0,0 +1,12 @@ +/** + * bpm 包下,业务流程管理(Business Process Management),我们放工作流的功能,基于 Flowable 6 版本实现。 + * 例如说:流程定义、表单配置、审核中心(我的申请、我的待办、我的已办)等等 + * + * bpm 解释:https://baike.baidu.com/item/BPM/1933 + * + * 1. Controller URL:以 /bpm/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 bpm_ 开头,方便在数据库中区分 + * + * 注意,由于 Bpm 模块下,容易和其它模块重名,所以类名都加载 Bpm 的前缀~ + */ +package cn.iocoder.yudao.module.bpm; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryService.java new file mode 100644 index 0000000..104f269 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryService.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategoryPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategorySaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * BPM 流程分类 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmCategoryService { + + /** + * 创建流程分类 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createCategory(@Valid BpmCategorySaveReqVO createReqVO); + + /** + * 更新流程分类 + * + * @param updateReqVO 更新信息 + */ + void updateCategory(@Valid BpmCategorySaveReqVO updateReqVO); + + /** + * 删除流程分类 + * + * @param id 编号 + */ + void deleteCategory(Long id); + + /** + * 获得流程分类 + * + * @param id 编号 + * @return BPM 流程分类 + */ + BpmCategoryDO getCategory(Long id); + + /** + * 获得流程分类分页 + * + * @param pageReqVO 分页查询 + * @return 流程分类分页 + */ + PageResult getCategoryPage(BpmCategoryPageReqVO pageReqVO); + + /** + * 获得流程分类 Map,基于指定编码 + * + * @param codes 编号数组 + * @return 流程分类 Map + */ + default Map getCategoryMap(Collection codes) { + return convertMap(getCategoryListByCode(codes), BpmCategoryDO::getCode); + } + + /** + * 获得流程分类列表,基于指定编码 + * + * @return 流程分类列表 + */ + List getCategoryListByCode(Collection codes); + + /** + * 获得流程分类列表,基于指定状态 + * + * @param status 状态 + * @return 流程分类列表 + */ + List getCategoryListByStatus(Integer status); + + /** + * 批量更新流程分类的排序:每个分类的 sort 值,从 0 开始递增 + * + * @param ids 分类编号列表 + */ + void updateCategorySortBatch(List ids); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java new file mode 100644 index 0000000..8a48da1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java @@ -0,0 +1,130 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategoryPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategorySaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.category.BpmCategoryMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; + +/** + * BPM 流程分类 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class BpmCategoryServiceImpl implements BpmCategoryService { + + @Resource + private BpmCategoryMapper bpmCategoryMapper; + + @Override + public Long createCategory(BpmCategorySaveReqVO createReqVO) { + // 校验唯一 + validateCategoryNameUnique(createReqVO); + validateCategoryCodeUnique(createReqVO); + // 插入 + BpmCategoryDO category = BeanUtils.toBean(createReqVO, BpmCategoryDO.class); + bpmCategoryMapper.insert(category); + return category.getId(); + } + + @Override + public void updateCategory(BpmCategorySaveReqVO updateReqVO) { + // 校验存在 + validateCategoryExists(updateReqVO.getId()); + validateCategoryNameUnique(updateReqVO); + validateCategoryCodeUnique(updateReqVO); + // 更新 + BpmCategoryDO updateObj = BeanUtils.toBean(updateReqVO, BpmCategoryDO.class); + bpmCategoryMapper.updateById(updateObj); + } + + private void validateCategoryNameUnique(BpmCategorySaveReqVO updateReqVO) { + BpmCategoryDO category = bpmCategoryMapper.selectByName(updateReqVO.getName()); + if (category == null + || ObjUtil.equal(category.getId(), updateReqVO.getId())) { + return; + } + throw exception(CATEGORY_NAME_DUPLICATE, updateReqVO.getName()); + } + + private void validateCategoryCodeUnique(BpmCategorySaveReqVO updateReqVO) { + BpmCategoryDO category = bpmCategoryMapper.selectByCode(updateReqVO.getCode()); + if (category == null + || ObjUtil.equal(category.getId(), updateReqVO.getId())) { + return; + } + throw exception(CATEGORY_CODE_DUPLICATE, updateReqVO.getCode()); + } + + @Override + public void deleteCategory(Long id) { + // 校验存在 + validateCategoryExists(id); + // 删除 + bpmCategoryMapper.deleteById(id); + } + + private void validateCategoryExists(Long id) { + if (bpmCategoryMapper.selectById(id) == null) { + throw exception(CATEGORY_NOT_EXISTS); + } + } + + @Override + public BpmCategoryDO getCategory(Long id) { + return bpmCategoryMapper.selectById(id); + } + + @Override + public PageResult getCategoryPage(BpmCategoryPageReqVO pageReqVO) { + return bpmCategoryMapper.selectPage(pageReqVO); + } + + @Override + public List getCategoryListByCode(Collection codes) { + if (CollUtil.isEmpty(codes)) { + return Collections.emptyList(); + } + return bpmCategoryMapper.selectListByCode(codes); + } + + @Override + public List getCategoryListByStatus(Integer status) { + return bpmCategoryMapper.selectListByStatus(status); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCategorySortBatch(List ids) { + // 校验分类都存在 + List categories = bpmCategoryMapper.selectByIds(ids); + if (categories.size() != ids.size()) { + throw exception(CATEGORY_NOT_EXISTS); + } + + // 批量更新排序 + List updateList = IntStream.range(0, ids.size()) + .mapToObj(index -> new BpmCategoryDO().setId(ids.get(index)).setSort(index)) + .collect(Collectors.toList()); + bpmCategoryMapper.updateBatch(updateList); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormService.java new file mode 100644 index 0000000..3c84250 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormService.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormSaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + + +/** + * 动态表单 Service 接口 + * + * @author @风里雾里 + */ +public interface BpmFormService { + + /** + * 创建动态表单 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createForm(@Valid BpmFormSaveReqVO createReqVO); + + /** + * 更新动态表单 + * + * @param updateReqVO 更新信息 + */ + void updateForm(@Valid BpmFormSaveReqVO updateReqVO); + + /** + * 删除动态表单 + * + * @param id 编号 + */ + void deleteForm(Long id); + + /** + * 获得动态表单 + * + * @param id 编号 + * @return 动态表单 + */ + BpmFormDO getForm(Long id); + + /** + * 获得动态表单列表 + * + * @return 动态表单列表 + */ + List getFormList(); + + /** + * 获得动态表单列表 + * + * @param ids 编号 + * @return 动态表单列表 + */ + List getFormList(Collection ids); + + /** + * 获得动态表单 Map + * + * @param ids 编号 + * @return 动态表单 Map + */ + default Map getFormMap(Collection ids) { + return CollectionUtils.convertMap(this.getFormList(ids), BpmFormDO::getId); + } + + /** + * 获得动态表单分页 + * + * @param pageReqVO 分页查询 + * @return 动态表单分页 + */ + PageResult getFormPage(BpmFormPageReqVO pageReqVO); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceImpl.java new file mode 100644 index 0000000..2a4b9f1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceImpl.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormSaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmFormMapper; +import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmFormFieldRespDTO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * 动态表单 Service 实现类 + * + * @author 风里雾里 + */ +@Service +@Validated +public class BpmFormServiceImpl implements BpmFormService { + + @Resource + private BpmFormMapper formMapper; + + @Override + public Long createForm(BpmFormSaveReqVO createReqVO) { + this.validateFields(createReqVO.getFields()); + // 插入 + BpmFormDO form = BeanUtils.toBean(createReqVO, BpmFormDO.class); + formMapper.insert(form); + // 返回 + return form.getId(); + } + + @Override + public void updateForm(BpmFormSaveReqVO updateReqVO) { + validateFields(updateReqVO.getFields()); + // 校验存在 + validateFormExists(updateReqVO.getId()); + // 更新 + BpmFormDO updateObj = BeanUtils.toBean(updateReqVO, BpmFormDO.class); + formMapper.updateById(updateObj); + } + + @Override + public void deleteForm(Long id) { + // 校验存在 + this.validateFormExists(id); + // 删除 + formMapper.deleteById(id); + } + + private void validateFormExists(Long id) { + if (formMapper.selectById(id) == null) { + throw exception(ErrorCodeConstants.FORM_NOT_EXISTS); + } + } + + @Override + public BpmFormDO getForm(Long id) { + return formMapper.selectById(id); + } + + @Override + public List getFormList() { + return formMapper.selectList(); + } + + @Override + public List getFormList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return formMapper.selectByIds(ids); + } + + @Override + public PageResult getFormPage(BpmFormPageReqVO pageReqVO) { + return formMapper.selectPage(pageReqVO); + } + + /** + * 校验 Field,避免 field 重复 + * + * @param fields field 数组 + */ + private void validateFields(List fields) { + if (true) { // TODO 芋艿:兼容 Vue3 工作流:因为采用了新的表单设计器,所以暂时不校验 + return; + } + Map fieldMap = new HashMap<>(); // key 是 vModel,value 是 label + for (String field : fields) { + BpmFormFieldRespDTO fieldDTO = JsonUtils.parseObject(field, BpmFormFieldRespDTO.class); + Assert.notNull(fieldDTO); + String oldLabel = fieldMap.put(fieldDTO.getVModel(), fieldDTO.getLabel()); + // 如果不存在,则直接返回 + if (oldLabel == null) { + continue; + } + // 如果存在,则报错 + throw exception(ErrorCodeConstants.FORM_FIELD_REPEAT, oldLabel, fieldDTO.getLabel(), fieldDTO.getVModel()); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java new file mode 100644 index 0000000..5601bcd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO; +import jakarta.validation.Valid; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.repository.Model; + +import java.util.List; + +/** + * 流程模型接口 + * + * @author yunlongn + */ +public interface BpmModelService { + + /** + * 获得流程模型列表 + * + * @param name 模型名称 + * @return 流程模型列表 + */ + List getModelList(String name); + + /** + * 创建流程模型 + * + * @param modelVO 创建信息 + * @return 创建的流程模型的编号 + */ + String createModel(@Valid BpmModelSaveReqVO modelVO); + + /** + * 获得流程模块 + * + * @param id 编号 + * @return 流程模型 + */ + Model getModel(String id); + + /** + * 获得流程模型的 BPMN XML + * + * @param id 编号 + * @return BPMN XML + */ + byte[] getModelBpmnXML(String id); + + /** + * 修改流程模型的 BPMN XML + * + * @param id 编号 + * @param bpmnXml BPMN XML + */ + void updateModelBpmnXml(String id, String bpmnXml); + + /** + * 修改流程模型 + * + * @param userId 用户编号 + * @param updateReqVO 更新信息 + */ + void updateModel(Long userId, @Valid BpmModelSaveReqVO updateReqVO); + + /** + * 批量更新模型排序 + * + * @param userId 用户编号 + * @param ids 编号列表 + */ + void updateModelSortBatch(Long userId, List ids); + + /** + * 将流程模型,部署成一个流程定义 + * + * @param userId 用户编号 + * @param id 编号 + */ + void deployModel(Long userId, String id); + + /** + * 删除模型 + * + * @param userId 用户编号 + * @param id 编号 + */ + void deleteModel(Long userId, String id); + + /** + * 清理模型,包括流程实例 + * + * @param userId 用户编号 + * @param id 编号 + */ + void cleanModel(Long userId, String id); + + /** + * 修改模型的状态,实际更新的部署的流程定义的状态 + * + * @param userId 用户编号 + * @param id 编号 + * @param state 状态 + */ + void updateModelState(Long userId, String id, Integer state); + + /** + * 获得流程定义编号对应的 BPMN Model + * + * @param processDefinitionId 流程定义编号 + * @return BPMN Model + */ + BpmnModel getBpmnModelByDefinitionId(String processDefinitionId); + + // ========== 仿钉钉/飞书的精简模型 ========= + + /** + * 获取仿钉钉流程设计模型结构 + * + * @param modelId 流程模型编号 + * @return 仿钉钉流程设计模型结构 + */ + BpmSimpleModelNodeVO getSimpleModel(String modelId); + + /** + * 更新仿钉钉流程设计模型 + * + * @param userId 用户编号 + * @param reqVO 请求信息 + */ + void updateSimpleModel(Long userId, @Valid BpmSimpleModelUpdateReqVO reqVO); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java new file mode 100644 index 0000000..e8e9000 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -0,0 +1,432 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO; +import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.*; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.repository.Model; +import org.flowable.engine.repository.ModelQuery; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseCandidateStrategy; + +/** + * 流程模型实现:主要进行 Flowable {@link Model} 的维护 + * + * @author yunlongn + * @author 芋道源码 + * @author jason + */ +@Service +@Validated +@Slf4j +public class BpmModelServiceImpl implements BpmModelService { + + @Resource + private RepositoryService repositoryService; + @Resource + private BpmProcessDefinitionService processDefinitionService; + @Resource + private BpmFormService bpmFormService; + + @Resource + private BpmTaskCandidateInvoker taskCandidateInvoker; + + @Resource + private HistoryService historyService; + @Resource + private RuntimeService runtimeService; + @Resource + private TaskService taskService; + @Resource + private BpmProcessInstanceCopyService processInstanceCopyService; + + @Override + public List getModelList(String name) { + ModelQuery modelQuery = repositoryService.createModelQuery(); + if (StrUtil.isNotEmpty(name)) { + modelQuery.modelNameLike("%" + name + "%"); + } + modelQuery.modelTenantId(FlowableUtils.getTenantId()); + return modelQuery.list(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String createModel(@Valid BpmModelSaveReqVO createReqVO) { + if (!ValidationUtils.isXmlNCName(createReqVO.getKey())) { + throw exception(MODEL_KEY_VALID); + } + // 1. 校验流程标识已经存在 + Model keyModel = getModelByKey(createReqVO.getKey()); + if (keyModel != null) { + throw exception(MODEL_KEY_EXISTS, createReqVO.getKey()); + } + + // 2. 创建 Model 对象 + createReqVO.setSort(System.currentTimeMillis()); // 使用当前时间,作为排序 + Model model = repositoryService.newModel(); + BpmModelConvert.INSTANCE.copyToModel(model, createReqVO); + model.setTenantId(FlowableUtils.getTenantId()); + + // 3. 保存模型 + saveModel(model, createReqVO); + return model.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务 + public void updateModel(Long userId, BpmModelSaveReqVO updateReqVO) { + // 1. 校验流程模型存在 + Model model = validateModelManager(updateReqVO.getId(), userId); + + // 2. 填充 Model 信息 + BpmModelConvert.INSTANCE.copyToModel(model, updateReqVO); + + // 3. 保存模型 + saveModel(model, updateReqVO); + } + + /** + * 保存模型的基本信息、流程图 + * + * @param model 模型 + * @param saveReqVO 保存信息 + */ + private void saveModel(Model model, BpmModelSaveReqVO saveReqVO) { + // 1. 保存模型的基础信息 + repositoryService.saveModel(model); + + // 2. 保存流程图 + if (ObjUtil.equals(BpmModelTypeEnum.BPMN.getType(), saveReqVO.getType()) + && StrUtil.isNotEmpty(saveReqVO.getBpmnXml())) { + updateModelBpmnXml(model.getId(), saveReqVO.getBpmnXml()); + } else if (ObjUtil.equals(BpmModelTypeEnum.SIMPLE.getType(), saveReqVO.getType()) + && saveReqVO.getSimpleModel() != null) { + // JSON 转换成 bpmnModel + BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel(model.getKey(), model.getName(), + saveReqVO.getSimpleModel()); + // 保存 Bpmn XML + updateModelBpmnXml(model.getId(), BpmnModelUtils.getBpmnXml(bpmnModel)); + // 保存 JSON 数据 + updateModelSimpleJson(model.getId(), saveReqVO.getSimpleModel()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateModelSortBatch(Long userId, List ids) { + // 1.1 校验流程模型存在 + List models = repositoryService.createModelQuery() + .modelTenantId(FlowableUtils.getTenantId()).list(); + models.removeIf(model -> !ids.contains(model.getId())); + if (ids.size() != models.size()) { + throw exception(MODEL_NOT_EXISTS); + } + Map modelMap = convertMap(models, Model::getId); + // 1.2 校验是否为管理员 + ids.forEach(id -> validateModelManager(id, userId)); + + // 保存排序 + long sort = System.currentTimeMillis(); // 使用时间戳 - i 作为排序 + for (int i = ids.size() - 1; i > 0; i--) { + Model model = modelMap.get(ids.get(i)); + // 更新模型 + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model).setSort(sort); + model.setMetaInfo(JsonUtils.toJsonString(metaInfo)); + repositoryService.saveModel(model); + // 更新排序 + processDefinitionService.updateProcessDefinitionSortByModelId(model.getId(), sort); + sort--; + } + } + + private Model validateModelExists(String id) { + Model model = repositoryService.getModel(id); + if (model == null) { + throw exception(MODEL_NOT_EXISTS); + } + return model; + } + + /** + * 校验是否有流程模型的管理权限 + * + * @param id 流程模型编号 + * @param userId 用户编号 + * @return 流程模型 + */ + private Model validateModelManager(String id, Long userId) { + Model model = validateModelExists(id); + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); + if (metaInfo == null || !CollUtil.contains(metaInfo.getManagerUserIds(), userId)) { + throw exception(MODEL_UPDATE_FAIL_NOT_MANAGER, model.getName()); + } + return model; + } + + @Override + @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务 + public void deployModel(Long userId, String id) { + // 1.1 校验流程模型存在 + Model model = validateModelManager(id, userId); + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); + // 1.2 校验流程图 + byte[] bpmnBytes = getModelBpmnXML(model.getId()); + validateBpmnXml(bpmnBytes, metaInfo.getType()); + // 1.3 校验表单已配 + BpmFormDO form = validateFormConfig(metaInfo); + // 1.4 校验任务分配规则已配置 + taskCandidateInvoker.validateBpmnConfig(bpmnBytes); + // 1.5 获取仿钉钉流程设计器模型数据 + String simpleJson = getModelSimpleJson(model.getId()); + + // 2.1 创建流程定义 + String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, simpleJson, + form); + + // 2.2 将老的流程定义进行挂起。也就是说,只有最新部署的流程定义,才可以发起任务。 + updateProcessDefinitionSuspended(model.getDeploymentId()); + + // 2.3 更新 model 的 deploymentId,进行关联 + ProcessDefinition definition = processDefinitionService.getProcessDefinition(definitionId); + model.setDeploymentId(definition.getDeploymentId()); + repositoryService.saveModel(model); + } + + private void validateBpmnXml(byte[] bpmnBytes, Integer type) { + BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes); + if (bpmnModel == null) { + throw exception(MODEL_NOT_EXISTS); + } + // 1. 没有 StartEvent + StartEvent startEvent = BpmnModelUtils.getStartEvent(bpmnModel); + if (startEvent == null) { + throw exception(MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS); + } + // 2. 校验 UserTask 的 name 都配置了 + List userTasks = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class); + userTasks.forEach(userTask -> { + if (StrUtil.isEmpty(userTask.getName())) { + throw exception(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS, userTask.getId()); + } + }); + // 3. 校验第一个用户任务节点的规则类型是否为“审批人自选”,BPMN 设计器,校验第一个用户任务节点,SIMPLE 设计器,第一个节点固定为发起人所以校验第二个用户任务节点 + UserTask firUserTask = CollUtil.get(userTasks, BpmModelTypeEnum.BPMN.getType().equals(type) ? 0 : 1); + if (firUserTask == null) { + return; + } + Integer candidateStrategy = parseCandidateStrategy(firUserTask); + if (Objects.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) { + throw exception(MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR, firUserTask.getName()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteModel(Long userId, String id) { + // 校验流程模型存在 + Model model = validateModelManager(id, userId); + + // 执行删除 + repositoryService.deleteModel(id); + // 禁用流程定义 + updateProcessDefinitionSuspended(model.getDeploymentId()); + } + + @Override + public void cleanModel(Long userId, String id) { + // 1. 校验流程模型存在 + Model model = validateModelManager(id, userId); + + // 2. 清理所有流程数据 + // 2.1 先取消所有正在运行的流程 + List processInstances = runtimeService.createProcessInstanceQuery() + .processDefinitionKey(model.getKey()).list(); + processInstances.forEach(processInstance -> { + runtimeService.deleteProcessInstance(processInstance.getId(), + BpmReasonEnum.CANCEL_BY_SYSTEM.getReason()); + historyService.deleteHistoricProcessInstance(processInstance.getId()); + processInstanceCopyService.deleteProcessInstanceCopy(processInstance.getId()); + }); + // 2.2 再从历史中删除所有相关的流程数据 + List historicProcessInstances = historyService.createHistoricProcessInstanceQuery() + .processDefinitionKey(model.getKey()).list(); + historicProcessInstances.forEach(historicProcessInstance -> { + historyService.deleteHistoricProcessInstance(historicProcessInstance.getId()); + processInstanceCopyService.deleteProcessInstanceCopy(historicProcessInstance.getId()); + }); + // 2.3 清理所有 Task + List tasks = taskService.createTaskQuery() + .processDefinitionKey(model.getKey()).list(); + tasks.forEach(task -> taskService.deleteTask(task.getId(),BpmReasonEnum.CANCEL_BY_PROCESS_CLEAN.getReason())); + } + + @Override + public void updateModelState(Long userId, String id, Integer state) { + // 1.1 校验流程模型存在 + Model model = validateModelManager(id, userId); + // 1.2 校验流程定义存在 + ProcessDefinition definition = processDefinitionService + .getProcessDefinitionByDeploymentId(model.getDeploymentId()); + if (definition == null) { + throw exception(PROCESS_DEFINITION_NOT_EXISTS); + } + + // 2. 更新状态 + processDefinitionService.updateProcessDefinitionState(definition.getId(), state); + } + + @Override + public BpmnModel getBpmnModelByDefinitionId(String processDefinitionId) { + return repositoryService.getBpmnModel(processDefinitionId); + } + + @Override + public BpmSimpleModelNodeVO getSimpleModel(String modelId) { + Model model = validateModelExists(modelId); + // 通过 ACT_RE_MODEL 表 EDITOR_SOURCE_EXTRA_VALUE_ID_ ,获取仿钉钉快搭模型的 JSON 数据 + String json = getModelSimpleJson(model.getId()); + return JsonUtils.parseObject(json, BpmSimpleModelNodeVO.class); + } + + @Override + public void updateSimpleModel(Long userId, BpmSimpleModelUpdateReqVO reqVO) { + // 1. 校验流程模型存在 + Model model = validateModelManager(reqVO.getId(), userId); + + // 2.1 JSON 转换成 bpmnModel + BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel(model.getKey(), model.getName(), reqVO.getSimpleModel()); + // 2.2 保存 Bpmn XML + updateModelBpmnXml(model.getId(), BpmnModelUtils.getBpmnXml(bpmnModel)); + // 2.3 保存 JSON 数据 + updateModelSimpleJson(model.getId(), reqVO.getSimpleModel()); + } + + /** + * 校验流程表单已配置 + * + * @param metaInfo 流程模型元数据 + * @return 表单配置 + */ + private BpmFormDO validateFormConfig(BpmModelMetaInfoVO metaInfo) { + if (metaInfo == null || metaInfo.getFormType() == null) { + throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG); + } + // 校验表单存在 + if (Objects.equals(metaInfo.getFormType(), BpmModelFormTypeEnum.NORMAL.getType())) { + if (metaInfo.getFormId() == null) { + throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG); + } + BpmFormDO form = bpmFormService.getForm(metaInfo.getFormId()); + if (form == null) { + throw exception(FORM_NOT_EXISTS); + } + return form; + } else { + if (StrUtil.isEmpty(metaInfo.getFormCustomCreatePath()) + || StrUtil.isEmpty(metaInfo.getFormCustomViewPath())) { + throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG); + } + return null; + } + } + + @Override + public void updateModelBpmnXml(String id, String bpmnXml) { + if (StrUtil.isEmpty(bpmnXml)) { + return; + } + repositoryService.addModelEditorSource(id, StrUtil.utf8Bytes(bpmnXml)); + } + + @SuppressWarnings("JavaExistingMethodCanBeUsed") + private String getModelSimpleJson(String id) { + byte[] bytes = repositoryService.getModelEditorSourceExtra(id); + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + return StrUtil.utf8Str(bytes); + } + + private void updateModelSimpleJson(String id, BpmSimpleModelNodeVO node) { + if (node == null) { + return; + } + byte[] bytes = JsonUtils.toJsonByte(node); + repositoryService.addModelEditorSourceExtra(id, bytes); + } + + /** + * 挂起 deploymentId 对应的流程定义 + *

+ * 注意:这里一个 deploymentId 只关联一个流程定义 + * + * @param deploymentId 流程发布Id + */ + private void updateProcessDefinitionSuspended(String deploymentId) { + if (StrUtil.isEmpty(deploymentId)) { + return; + } + ProcessDefinition oldDefinition = processDefinitionService.getProcessDefinitionByDeploymentId(deploymentId); + if (oldDefinition == null) { + return; + } + processDefinitionService.updateProcessDefinitionState(oldDefinition.getId(), + SuspensionState.SUSPENDED.getStateCode()); + } + + private Model getModelByKey(String key) { + return repositoryService.createModelQuery() + .modelTenantId(FlowableUtils.getTenantId()) + .modelKey(key).singleResult(); + } + + @Override + public Model getModel(String id) { + return repositoryService.getModel(id); + } + + @Override + public byte[] getModelBpmnXML(String id) { + return repositoryService.getModelEditorSource(id); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java new file mode 100644 index 0000000..f42e69e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java @@ -0,0 +1,181 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.Model; +import org.flowable.engine.repository.ProcessDefinition; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 流程定义接口 + * + * @author yunlong.li + * @author ZJQ + * @author 芋道源码 + */ +public interface BpmProcessDefinitionService { + + /** + * 获得流程定义分页 + * + * @param pageReqVO 分页入参 + * @return 流程定义 Page + */ + PageResult getProcessDefinitionPage(BpmProcessDefinitionPageReqVO pageReqVO); + + /** + * 获得流程定义列表 + * + * @param suspensionState 中断状态 + * @return 流程定义列表 + */ + List getProcessDefinitionListBySuspensionState(Integer suspensionState); + + /** + * 基于流程模型,创建流程定义 + * + * @param model 流程模型 + * @param modelMetaInfo 流程模型元信息 + * @param bpmnBytes BPMN XML 字节数组 + * @param simpleJson SIMPLE Model JSON + * @param form 表单 + * @return 流程编号 + */ + String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo, + byte[] bpmnBytes, String simpleJson, BpmFormDO form); + + /** + * 更新流程定义状态 + * + * @param id 流程定义的编号 + * @param state 状态 + */ + void updateProcessDefinitionState(String id, Integer state); + + /** + * 更新模型编号 + * + * @param modelId 流程定义编号 + * @param sort 排序 + */ + void updateProcessDefinitionSortByModelId(String modelId, Long sort); + + /** + * 获得流程定义对应的 BPMN + * + * @param id 流程定义编号 + * @return BPMN + */ + BpmnModel getProcessDefinitionBpmnModel(String id); + + /** + * 获得流程定义的信息 + * + * @param id 流程定义编号 + * @return 流程定义信息 + */ + BpmProcessDefinitionInfoDO getProcessDefinitionInfo(String id); + + /** + * 获得流程定义的信息 List + * + * @param ids 流程定义编号数组 + * @return 流程额定义信息数组 + */ + List getProcessDefinitionInfoList(Collection ids); + + default Map getProcessDefinitionInfoMap(Set ids) { + return convertMap(getProcessDefinitionInfoList(ids), BpmProcessDefinitionInfoDO::getProcessDefinitionId); + } + + /** + * 获得流程定义编号对应的 ProcessDefinition + * + * @param id 流程定义编号 + * @return 流程定义 + */ + ProcessDefinition getProcessDefinition(String id); + + /** + * 获得 ids 对应的 ProcessDefinition 数组 + * + * @param ids 编号的数组 + * @return 流程定义的数组 + */ + List getProcessDefinitionList(Set ids); + + default Map getProcessDefinitionMap(Set ids) { + return convertMap(getProcessDefinitionList(ids), ProcessDefinition::getId); + } + + /** + * 获得 deploymentId 对应的 ProcessDefinition + * + * @param deploymentId 部署编号 + * @return 流程定义 + */ + ProcessDefinition getProcessDefinitionByDeploymentId(String deploymentId); + + /** + * 获得 deploymentIds 对应的 ProcessDefinition 数组 + * + * @param deploymentIds 部署编号的数组 + * @return 流程定义的数组 + */ + List getProcessDefinitionListByDeploymentIds(Set deploymentIds); + + /** + * 获得流程定义标识对应的激活的流程定义 + * + * @param key 流程定义的标识 + * @return 流程定义 + */ + ProcessDefinition getActiveProcessDefinition(String key); + + /** + * 判断用户是否可以使用该流程定义,进行流程的发起 + * + * @param processDefinition 流程定义 + * @param userId 用户编号 + * @return 是否可以发起流程 + */ + boolean canUserStartProcessDefinition(BpmProcessDefinitionInfoDO processDefinition, Long userId); + + /** + * 获得 ids 对应的 Deployment Map + * + * @param ids 部署编号的数组 + * @return 流程部署 Map + */ + default Map getDeploymentMap(Set ids) { + return convertMap(getDeploymentList(ids), Deployment::getId); + } + + /** + * 获得 ids 对应的 Deployment 数组 + * + * @param ids 部署编号的数组 + * @return 流程部署的数组 + */ + List getDeploymentList(Set ids); + + /** + * 获得 id 对应的 Deployment + * + * @param id 部署编号 + * @return 流程部署 + */ + Deployment getDeployment(String id); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java new file mode 100644 index 0000000..836b518 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java @@ -0,0 +1,247 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.object.PageUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessDefinitionInfoMapper; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.Model; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.repository.ProcessDefinitionQuery; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.addIfNotNull; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; +import static java.util.Collections.emptyList; + +/** + * 流程定义实现 + * 主要进行 Flowable {@link ProcessDefinition} 和 {@link Deployment} 的维护 + * + * @author yunlongn + * @author ZJQ + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionService { + + @Resource + private RepositoryService repositoryService; + + @Resource + private BpmProcessDefinitionInfoMapper processDefinitionMapper; + + @Resource + private AdminUserApi adminUserApi; + + @Override + public ProcessDefinition getProcessDefinition(String id) { + return repositoryService.getProcessDefinition(id); + } + + @Override + public List getProcessDefinitionList(Set ids) { + return repositoryService.createProcessDefinitionQuery().processDefinitionIds(ids).list(); + } + + @Override + public ProcessDefinition getProcessDefinitionByDeploymentId(String deploymentId) { + if (StrUtil.isEmpty(deploymentId)) { + return null; + } + return repositoryService.createProcessDefinitionQuery().deploymentId(deploymentId).singleResult(); + } + + @Override + public List getProcessDefinitionListByDeploymentIds(Set deploymentIds) { + if (CollUtil.isEmpty(deploymentIds)) { + return emptyList(); + } + return repositoryService.createProcessDefinitionQuery().deploymentIds(deploymentIds).list(); + } + + @Override + public ProcessDefinition getActiveProcessDefinition(String key) { + return repositoryService.createProcessDefinitionQuery() + .processDefinitionTenantId(FlowableUtils.getTenantId()) + .processDefinitionKey(key).active().singleResult(); + } + + @Override + public boolean canUserStartProcessDefinition(BpmProcessDefinitionInfoDO processDefinition, Long userId) { + if (processDefinition == null) { + return false; + } + + // 校验用户是否在允许发起的用户列表中 + if (CollUtil.isNotEmpty(processDefinition.getStartUserIds())) { + return processDefinition.getStartUserIds().contains(userId); + } + + // 校验用户是否在允许发起的部门列表中 + if (CollUtil.isNotEmpty(processDefinition.getStartDeptIds())) { + AdminUserRespDTO user = adminUserApi.getUser(userId); + return user != null + && user.getDeptId() != null + && processDefinition.getStartDeptIds().contains(user.getDeptId()); + } + + // 都为空,则所有人都可以发起 + return true; + } + + @Override + public List getDeploymentList(Set ids) { + if (CollUtil.isEmpty(ids)) { + return emptyList(); + } + List list = new ArrayList<>(ids.size()); + for (String id : ids) { + addIfNotNull(list, getDeployment(id)); + } + return list; + } + + @Override + public Deployment getDeployment(String id) { + if (StrUtil.isEmpty(id)) { + return null; + } + return repositoryService.createDeploymentQuery().deploymentId(id).singleResult(); + } + + @Override + public String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo, + byte[] bpmnBytes, String simpleJson, BpmFormDO form) { + // 创建 Deployment 部署 + Deployment deploy = repositoryService.createDeployment() + .key(model.getKey()).name(model.getName()).category(model.getCategory()) + .addBytes(model.getKey() + BpmnModelConstants.BPMN_FILE_SUFFIX, bpmnBytes) + .tenantId(FlowableUtils.getTenantId()) + .disableSchemaValidation() // 禁用 XML Schema 验证,因为有自定义的属性 + .deploy(); + + // 设置 ProcessDefinition 的 category 分类 + ProcessDefinition definition = repositoryService.createProcessDefinitionQuery() + .deploymentId(deploy.getId()).singleResult(); + repositoryService.setProcessDefinitionCategory(definition.getId(), model.getCategory()); + // 注意 1,ProcessDefinition 的 key 和 name 是通过 BPMN 中的 的 id 和 name 决定 + // 注意 2,目前该项目的设计上,需要保证 Model、Deployment、ProcessDefinition 使用相同的 key,保证关联性。 + // 否则,会导致 ProcessDefinition 的分页无法查询到。 + if (!Objects.equals(definition.getKey(), model.getKey())) { + throw exception(PROCESS_DEFINITION_KEY_NOT_MATCH, model.getKey(), definition.getKey()); + } + if (!Objects.equals(definition.getName(), model.getName())) { + throw exception(PROCESS_DEFINITION_NAME_NOT_MATCH, model.getName(), definition.getName()); + } + + // 插入拓展表 + BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class) + .setModelId(model.getId()).setCategory(model.getCategory()).setProcessDefinitionId(definition.getId()) + .setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson); + if (form != null) { + definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf()); + } + processDefinitionMapper.insert(definitionDO); + return definition.getId(); + } + + @Override + public void updateProcessDefinitionState(String id, Integer state) { + ProcessDefinition processDefinition = repositoryService.getProcessDefinition(id); + if (processDefinition == null) { + throw exception(PROCESS_DEFINITION_NOT_EXISTS); + } + + // 激活 + if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), state)) { + if (processDefinition.isSuspended()) { + repositoryService.activateProcessDefinitionById(id, false, null); + } + return; + } + // 挂起 + if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), state)) { + // suspendProcessInstances = false,进行中的任务,不进行挂起。 + // 原因:只要新的流程不允许发起即可,老流程继续可以执行。 + if (!processDefinition.isSuspended()) { + repositoryService.suspendProcessDefinitionById(id, false, null); + } + return; + } + log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", id, state); + } + + @Override + public void updateProcessDefinitionSortByModelId(String modelId, Long sort) { + processDefinitionMapper.updateByModelId(modelId, new BpmProcessDefinitionInfoDO().setSort(sort)); + } + + @Override + public BpmnModel getProcessDefinitionBpmnModel(String id) { + return repositoryService.getBpmnModel(id); + } + + @Override + public BpmProcessDefinitionInfoDO getProcessDefinitionInfo(String id) { + return processDefinitionMapper.selectByProcessDefinitionId(id); + } + + @Override + public List getProcessDefinitionInfoList(Collection ids) { + return processDefinitionMapper.selectListByProcessDefinitionIds(ids); + } + + @Override + public PageResult getProcessDefinitionPage(BpmProcessDefinitionPageReqVO pageVO) { + ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery(); + query.processDefinitionTenantId(FlowableUtils.getTenantId()); + if (StrUtil.isNotBlank(pageVO.getKey())) { + query.processDefinitionKey(pageVO.getKey()); + } + // 执行查询 + long count = query.count(); + if (count == 0) { + return PageResult.empty(count); + } + List list = query.orderByProcessDefinitionVersion().desc() + .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + return new PageResult<>(list, count); + } + + @Override + public List getProcessDefinitionListBySuspensionState(Integer suspensionState) { + // 拼接查询条件 + ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery(); + if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), suspensionState)) { + query.suspended(); + } else if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), suspensionState)) { + query.active(); + } + // 执行查询 + query.processDefinitionTenantId(FlowableUtils.getTenantId()); + return query.list(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionService.java new file mode 100644 index 0000000..caca96e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionService.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionSaveReqVO; +import jakarta.validation.*; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessExpressionDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +/** + * BPM 流程表达式 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmProcessExpressionService { + + /** + * 创建流程表达式 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createProcessExpression(@Valid BpmProcessExpressionSaveReqVO createReqVO); + + /** + * 更新流程表达式 + * + * @param updateReqVO 更新信息 + */ + void updateProcessExpression(@Valid BpmProcessExpressionSaveReqVO updateReqVO); + + /** + * 删除流程表达式 + * + * @param id 编号 + */ + void deleteProcessExpression(Long id); + + /** + * 获得流程表达式 + * + * @param id 编号 + * @return 流程表达式 + */ + BpmProcessExpressionDO getProcessExpression(Long id); + + /** + * 获得流程表达式分页 + * + * @param pageReqVO 分页查询 + * @return 流程表达式分页 + */ + PageResult getProcessExpressionPage(BpmProcessExpressionPageReqVO pageReqVO); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionServiceImpl.java new file mode 100644 index 0000000..122f392 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionServiceImpl.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionSaveReqVO; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; + +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessExpressionDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; + +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessExpressionMapper; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; + +/** + * BPM 流程表达式 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class BpmProcessExpressionServiceImpl implements BpmProcessExpressionService { + + @Resource + private BpmProcessExpressionMapper processExpressionMapper; + + @Override + public Long createProcessExpression(BpmProcessExpressionSaveReqVO createReqVO) { + // 插入 + BpmProcessExpressionDO processExpression = BeanUtils.toBean(createReqVO, BpmProcessExpressionDO.class); + processExpressionMapper.insert(processExpression); + // 返回 + return processExpression.getId(); + } + + @Override + public void updateProcessExpression(BpmProcessExpressionSaveReqVO updateReqVO) { + // 校验存在 + validateProcessExpressionExists(updateReqVO.getId()); + // 更新 + BpmProcessExpressionDO updateObj = BeanUtils.toBean(updateReqVO, BpmProcessExpressionDO.class); + processExpressionMapper.updateById(updateObj); + } + + @Override + public void deleteProcessExpression(Long id) { + // 校验存在 + validateProcessExpressionExists(id); + // 删除 + processExpressionMapper.deleteById(id); + } + + private void validateProcessExpressionExists(Long id) { + if (processExpressionMapper.selectById(id) == null) { + throw exception(PROCESS_EXPRESSION_NOT_EXISTS); + } + } + + @Override + public BpmProcessExpressionDO getProcessExpression(Long id) { + return processExpressionMapper.selectById(id); + } + + @Override + public PageResult getProcessExpressionPage(BpmProcessExpressionPageReqVO pageReqVO) { + return processExpressionMapper.selectPage(pageReqVO); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerService.java new file mode 100644 index 0000000..6529005 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerService.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerSaveReqVO; +import jakarta.validation.*; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessListenerDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; + +/** + * BPM 流程监听器 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmProcessListenerService { + + /** + * 创建流程监听器 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createProcessListener(@Valid BpmProcessListenerSaveReqVO createReqVO); + + /** + * 更新流程监听器 + * + * @param updateReqVO 更新信息 + */ + void updateProcessListener(@Valid BpmProcessListenerSaveReqVO updateReqVO); + + /** + * 删除流程监听器 + * + * @param id 编号 + */ + void deleteProcessListener(Long id); + + /** + * 获得流程监听器 + * + * @param id 编号 + * @return 流程监听器 + */ + BpmProcessListenerDO getProcessListener(Long id); + + /** + * 获得流程监听器分页 + * + * @param pageReqVO 分页查询 + * @return 流程监听器分页 + */ + PageResult getProcessListenerPage(BpmProcessListenerPageReqVO pageReqVO); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImpl.java new file mode 100644 index 0000000..1eb3f82 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImpl.java @@ -0,0 +1,102 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerSaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessListenerDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessListenerMapper; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerValueTypeEnum; +import jakarta.annotation.Resource; +import org.flowable.engine.delegate.JavaDelegate; +import org.flowable.engine.delegate.TaskListener; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; + +/** + * BPM 流程监听器 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class BpmProcessListenerServiceImpl implements BpmProcessListenerService { + + @Resource + private BpmProcessListenerMapper processListenerMapper; + + @Override + public Long createProcessListener(BpmProcessListenerSaveReqVO createReqVO) { + // 校验 + validateCreateProcessListenerValue(createReqVO); + // 插入 + BpmProcessListenerDO processListener = BeanUtils.toBean(createReqVO, BpmProcessListenerDO.class); + processListenerMapper.insert(processListener); + return processListener.getId(); + } + + @Override + public void updateProcessListener(BpmProcessListenerSaveReqVO updateReqVO) { + // 校验存在 + validateProcessListenerExists(updateReqVO.getId()); + validateCreateProcessListenerValue(updateReqVO); + // 更新 + BpmProcessListenerDO updateObj = BeanUtils.toBean(updateReqVO, BpmProcessListenerDO.class); + processListenerMapper.updateById(updateObj); + } + + private void validateCreateProcessListenerValue(BpmProcessListenerSaveReqVO createReqVO) { + // class 类型 + if (createReqVO.getValueType().equals(BpmProcessListenerValueTypeEnum.CLASS.getType())) { + try { + Class clazz = Class.forName(createReqVO.getValue()); + if (createReqVO.getType().equals(BpmProcessListenerTypeEnum.EXECUTION.getType()) + && !JavaDelegate.class.isAssignableFrom(clazz)) { + throw exception(PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR, createReqVO.getValue(), + JavaDelegate.class.getName()); + } else if (createReqVO.getType().equals(BpmProcessListenerTypeEnum.TASK.getType()) + && !TaskListener.class.isAssignableFrom(clazz)) { + throw exception(PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR, createReqVO.getValue(), + TaskListener.class.getName()); + } + } catch (ClassNotFoundException e) { + throw exception(PROCESS_LISTENER_CLASS_NOT_FOUND, createReqVO.getValue()); + } + return; + } + // 表达式 + if (!StrUtil.startWith(createReqVO.getValue(), "${") || !StrUtil.endWith(createReqVO.getValue(), "}")) { + throw exception(PROCESS_LISTENER_EXPRESSION_INVALID, createReqVO.getValue()); + } + } + + @Override + public void deleteProcessListener(Long id) { + // 校验存在 + validateProcessListenerExists(id); + // 删除 + processListenerMapper.deleteById(id); + } + + private void validateProcessListenerExists(Long id) { + if (processListenerMapper.selectById(id) == null) { + throw exception(PROCESS_LISTENER_NOT_EXISTS); + } + } + + @Override + public BpmProcessListenerDO getProcessListener(Long id) { + return processListenerMapper.selectById(id); + } + + @Override + public PageResult getProcessListenerPage(BpmProcessListenerPageReqVO pageReqVO) { + return processListenerMapper.selectPage(pageReqVO); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupService.java new file mode 100644 index 0000000..a95dc24 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupService.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupSaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; + +/** + * 用户组 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmUserGroupService { + + /** + * 创建用户组 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createUserGroup(@Valid BpmUserGroupSaveReqVO createReqVO); + + /** + * 更新用户组 + * + * @param updateReqVO 更新信息 + */ + void updateUserGroup(@Valid BpmUserGroupSaveReqVO updateReqVO); + + /** + * 删除用户组 + * + * @param id 编号 + */ + void deleteUserGroup(Long id); + + /** + * 获得用户组 + * + * @param id 编号 + * @return 用户组 + */ + BpmUserGroupDO getUserGroup(Long id); + + /** + * 获得用户组列表 + * + * @param ids 编号 + * @return 用户组列表 + */ + List getUserGroupList(Collection ids); + + /** + * 获得指定状态的用户组列表 + * + * @param status 状态 + * @return 用户组列表 + */ + List getUserGroupListByStatus(Integer status); + + /** + * 获得用户组分页 + * + * @param pageReqVO 分页查询 + * @return 用户组分页 + */ + PageResult getUserGroupPage(BpmUserGroupPageReqVO pageReqVO); + + /** + * 校验用户组们是否有效。如下情况,视为无效: + * 1. 用户组编号不存在 + * 2. 用户组被禁用 + * + * @param ids 用户组编号数组 + */ + void validUserGroups(Collection ids); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceImpl.java new file mode 100644 index 0000000..c638669 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceImpl.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupSaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmUserGroupMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.USER_GROUP_IS_DISABLE; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.USER_GROUP_NOT_EXISTS; + +/** + * 用户组 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class BpmUserGroupServiceImpl implements BpmUserGroupService { + + @Resource + private BpmUserGroupMapper userGroupMapper; + + @Override + public Long createUserGroup(BpmUserGroupSaveReqVO createReqVO) { + BpmUserGroupDO userGroup = BeanUtils.toBean(createReqVO, BpmUserGroupDO.class); + userGroupMapper.insert(userGroup); + return userGroup.getId(); + } + + @Override + public void updateUserGroup(BpmUserGroupSaveReqVO updateReqVO) { + // 校验存在 + validateUserGroupExists(updateReqVO.getId()); + // 更新 + BpmUserGroupDO updateObj = BeanUtils.toBean(updateReqVO, BpmUserGroupDO.class); + userGroupMapper.updateById(updateObj); + } + + @Override + public void deleteUserGroup(Long id) { + // 校验存在 + this.validateUserGroupExists(id); + // 删除 + userGroupMapper.deleteById(id); + } + + private void validateUserGroupExists(Long id) { + if (userGroupMapper.selectById(id) == null) { + throw exception(USER_GROUP_NOT_EXISTS); + } + } + + @Override + public BpmUserGroupDO getUserGroup(Long id) { + return userGroupMapper.selectById(id); + } + + @Override + public List getUserGroupList(Collection ids) { + return userGroupMapper.selectByIds(ids); + } + + + @Override + public List getUserGroupListByStatus(Integer status) { + return userGroupMapper.selectListByStatus(status); + } + + @Override + public PageResult getUserGroupPage(BpmUserGroupPageReqVO pageReqVO) { + return userGroupMapper.selectPage(pageReqVO); + } + + @Override + public void validUserGroups(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得用户组信息 + List userGroups = userGroupMapper.selectByIds(ids); + Map userGroupMap = convertMap(userGroups, BpmUserGroupDO::getId); + // 校验 + ids.forEach(id -> { + BpmUserGroupDO userGroup = userGroupMap.get(id); + if (userGroup == null) { + throw exception(USER_GROUP_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(userGroup.getStatus())) { + throw exception(USER_GROUP_IS_DISABLE, userGroup.getName()); + } + }); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/dto/BpmFormFieldRespDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/dto/BpmFormFieldRespDTO.java new file mode 100644 index 0000000..1606192 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/dto/BpmFormFieldRespDTO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.bpm.service.definition.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * Bpm 表单的 Field 表单项 Response DTO + * 字段的定义,可见 https://github.com/JakHuang/form-generator/issues/46 文档 + * + * @author 芋道源码 + */ +@Data +public class BpmFormFieldRespDTO { + + /** + * 表单标题 + */ + private String label; + /** + * 表单字段的属性名,可自定义 + */ + @JsonProperty(value = "vModel") + private String vModel; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/dto/BpmProcessDefinitionCreateReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/dto/BpmProcessDefinitionCreateReqDTO.java new file mode 100644 index 0000000..4008f8f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/dto/BpmProcessDefinitionCreateReqDTO.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.bpm.service.definition.dto; + +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * 流程定义创建 Request DTO + */ +@Data +public class BpmProcessDefinitionCreateReqDTO { + + // ========== 模型相关 ========== + + /** + * 流程模型的编号 + */ + @NotEmpty(message = "流程模型编号不能为空") + private String modelId; + /** + * 流程标识 + */ + @NotEmpty(message = "流程标识不能为空") + private String key; + /** + * 流程名称 + */ + @NotEmpty(message = "流程名称不能为空") + private String name; + /** + * 流程描述 + */ + private String description; + /** + * 流程分类 + */ + @NotEmpty(message = "流程分类不能为空") + private String category; + /** + * BPMN XML + */ + @NotEmpty(message = "BPMN XML 不能为空") + private byte[] bpmnBytes; + + // ========== 表单相关 ========== + + /** + * 表单类型 + */ + @NotNull(message = "表单类型不能为空") + private Integer formType; + /** + * 动态表单编号 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + */ + private Long formId; + /** + * 表单的配置 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + */ + private String formConf; + /** + * 表单项的数组 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + */ + private List formFields; + /** + * 自定义表单的提交路径,使用 Vue 的路由地址 + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomCreatePath; + /** + * 自定义表单的查看路径,使用 Vue 的路由地址 + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomViewPath; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageService.java new file mode 100644 index 0000000..268be72 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageService.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.bpm.service.message; + +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO; +import jakarta.validation.Valid; + +/** + * BPM 消息 Service 接口 + * + * TODO 芋艿:未来支持消息的可配置;不同的流程,在什么场景下,需要发送什么消息,消息的内容是什么; + * + * @author 芋道源码 + */ +public interface BpmMessageService { + + /** + * 发送流程实例被通过的消息 + * + * @param reqDTO 发送信息 + */ + void sendMessageWhenProcessInstanceApprove(@Valid BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO); + + /** + * 发送流程实例被不通过的消息 + * + * @param reqDTO 发送信息 + */ + void sendMessageWhenProcessInstanceReject(@Valid BpmMessageSendWhenProcessInstanceRejectReqDTO reqDTO); + + /** + * 发送任务被分配的消息 + * + * @param reqDTO 发送信息 + */ + void sendMessageWhenTaskAssigned(@Valid BpmMessageSendWhenTaskCreatedReqDTO reqDTO); + + /** + * 发送任务审批超时的消息 + * + * @param reqDTO 发送信息 + */ + void sendMessageWhenTaskTimeout(@Valid BpmMessageSendWhenTaskTimeoutReqDTO reqDTO); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java new file mode 100644 index 0000000..c9889ad --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.bpm.service.message; + +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.module.bpm.convert.message.BpmMessageConvert; +import cn.iocoder.yudao.module.bpm.enums.message.BpmMessageEnum; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO; +import cn.iocoder.yudao.module.system.api.sms.SmsSendApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +/** + * BPM 消息 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class BpmMessageServiceImpl implements BpmMessageService { + + @Resource + private SmsSendApi smsSendApi; + + @Resource + private WebProperties webProperties; + + @Override + public void sendMessageWhenProcessInstanceApprove(BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO) { + Map templateParams = new HashMap<>(); + templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); + templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); + smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(), + BpmMessageEnum.PROCESS_INSTANCE_APPROVE.getSmsTemplateCode(), templateParams)); + } + + @Override + public void sendMessageWhenProcessInstanceReject(BpmMessageSendWhenProcessInstanceRejectReqDTO reqDTO) { + Map templateParams = new HashMap<>(); + templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); + templateParams.put("reason", reqDTO.getReason()); + templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); + smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(), + BpmMessageEnum.PROCESS_INSTANCE_REJECT.getSmsTemplateCode(), templateParams)); + } + + @Override + public void sendMessageWhenTaskAssigned(BpmMessageSendWhenTaskCreatedReqDTO reqDTO) { + Map templateParams = new HashMap<>(); + templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); + templateParams.put("taskName", reqDTO.getTaskName()); + templateParams.put("startUserNickname", reqDTO.getStartUserNickname()); + templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); + smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(), + BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams)); + } + + @Override + public void sendMessageWhenTaskTimeout(BpmMessageSendWhenTaskTimeoutReqDTO reqDTO) { + Map templateParams = new HashMap<>(); + templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); + templateParams.put("taskName", reqDTO.getTaskName()); + templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); + smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(), + BpmMessageEnum.TASK_TIMEOUT.getSmsTemplateCode(), templateParams)); + } + + private String getProcessInstanceDetailUrl(String taskId) { + return webProperties.getAdminUi().getUrl() + "/bpm/process-instance/detail?id=" + taskId; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceApproveReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceApproveReqDTO.java new file mode 100644 index 0000000..9065ac2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceApproveReqDTO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.bpm.service.message.dto; + +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * BPM 发送流程实例被通过 Request DTO + */ +@Data +public class BpmMessageSendWhenProcessInstanceApproveReqDTO { + + /** + * 流程实例的编号 + */ + @NotEmpty(message = "流程实例的编号不能为空") + private String processInstanceId; + /** + * 流程实例的名字 + */ + @NotEmpty(message = "流程实例的名字不能为空") + private String processInstanceName; + @NotNull(message = "发起人的用户编号") + private Long startUserId; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceRejectReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceRejectReqDTO.java new file mode 100644 index 0000000..69b0dbe --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceRejectReqDTO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.bpm.service.message.dto; + +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * BPM 发送流程实例被不通过 Request DTO + */ +@Data +public class BpmMessageSendWhenProcessInstanceRejectReqDTO { + + /** + * 流程实例的编号 + */ + @NotEmpty(message = "流程实例的编号不能为空") + private String processInstanceId; + /** + * 流程实例的名字 + */ + @NotEmpty(message = "流程实例的名字不能为空") + private String processInstanceName; + @NotNull(message = "发起人的用户编号") + private Long startUserId; + + /** + * 不通过理由 + */ + @NotEmpty(message = "不通过理由不能为空") + private String reason; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenTaskCreatedReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenTaskCreatedReqDTO.java new file mode 100644 index 0000000..0e09fc5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenTaskCreatedReqDTO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.bpm.service.message.dto; + +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * BPM 发送任务被分配 Request DTO + */ +@Data +public class BpmMessageSendWhenTaskCreatedReqDTO { + + /** + * 流程实例的编号 + */ + @NotEmpty(message = "流程实例的编号不能为空") + private String processInstanceId; + /** + * 流程实例的名字 + */ + @NotEmpty(message = "流程实例的名字不能为空") + private String processInstanceName; + @NotNull(message = "发起人的用户编号") + private Long startUserId; + @NotEmpty(message = "发起人的昵称") + private String startUserNickname; + + /** + * 流程任务的编号 + */ + @NotEmpty(message = "流程任务的编号不能为空") + private String taskId; + /** + * 流程任务的名字 + */ + @NotEmpty(message = "流程任务的名字不能为空") + private String taskName; + + /** + * 审批人的用户编号 + */ + @NotNull(message = "审批人的用户编号不能为空") + private Long assigneeUserId; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenTaskTimeoutReqDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenTaskTimeoutReqDTO.java new file mode 100644 index 0000000..22d86ed --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenTaskTimeoutReqDTO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.bpm.service.message.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * BPM 发送任务审批超时 Request DTO + */ +@Data +public class BpmMessageSendWhenTaskTimeoutReqDTO { + + /** + * 流程实例的编号 + */ + @NotEmpty(message = "流程实例的编号不能为空") + private String processInstanceId; + /** + * 流程实例的名字 + */ + @NotEmpty(message = "流程实例的名字不能为空") + private String processInstanceName; + + /** + * 流程任务的编号 + */ + @NotEmpty(message = "流程任务的编号不能为空") + private String taskId; + /** + * 流程任务的名字 + */ + @NotEmpty(message = "流程任务的名字不能为空") + private String taskName; + + /** + * 审批人的用户编号 + */ + @NotNull(message = "审批人的用户编号不能为空") + private Long assigneeUserId; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveService.java new file mode 100644 index 0000000..24ca6d3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveService.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.bpm.service.oa; + + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeaveCreateReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeavePageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.BpmOALeaveDO; + +import jakarta.validation.Valid; + +/** + * 请假申请 Service 接口 + * + * @author jason + * @author 芋道源码 + */ +public interface BpmOALeaveService { + + /** + * 创建请假申请 + * + * @param userId 用户编号 + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createLeave(Long userId, @Valid BpmOALeaveCreateReqVO createReqVO); + + /** + * 更新请假申请的状态 + * + * @param id 编号 + * @param status 结果 + */ + void updateLeaveStatus(Long id, Integer status); + + /** + * 获得请假申请 + * + * @param id 编号 + * @return 请假申请 + */ + BpmOALeaveDO getLeave(Long id); + + /** + * 获得请假申请分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页查询 + * @return 请假申请分页 + */ + PageResult getLeavePage(Long userId, BpmOALeavePageReqVO pageReqVO); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveServiceImpl.java new file mode 100644 index 0000000..53e2759 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveServiceImpl.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.bpm.service.oa; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeaveCreateReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeavePageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.BpmOALeaveDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.oa.BpmOALeaveMapper; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.OA_LEAVE_NOT_EXISTS; + +/** + * OA 请假申请 Service 实现类 + * + * @author jason + * @author 芋道源码 + */ +@Service +@Validated +public class BpmOALeaveServiceImpl implements BpmOALeaveService { + + /** + * OA 请假对应的流程定义 KEY + */ + public static final String PROCESS_KEY = "oa_leave"; + + @Resource + private BpmOALeaveMapper leaveMapper; + + @Resource + private BpmProcessInstanceApi processInstanceApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createLeave(Long userId, BpmOALeaveCreateReqVO createReqVO) { + // 插入 OA 请假单 + long day = LocalDateTimeUtil.between(createReqVO.getStartTime(), createReqVO.getEndTime()).toDays(); + BpmOALeaveDO leave = BeanUtils.toBean(createReqVO, BpmOALeaveDO.class) + .setUserId(userId).setDay(day).setStatus(BpmTaskStatusEnum.RUNNING.getStatus()); + leaveMapper.insert(leave); + + // 发起 BPM 流程 + Map processInstanceVariables = new HashMap<>(); + processInstanceVariables.put("day", day); + String processInstanceId = processInstanceApi.createProcessInstance(userId, + new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(PROCESS_KEY) + .setVariables(processInstanceVariables).setBusinessKey(String.valueOf(leave.getId())) + .setStartUserSelectAssignees(createReqVO.getStartUserSelectAssignees())); + + // 将工作流的编号,更新到 OA 请假单中 + leaveMapper.updateById(new BpmOALeaveDO().setId(leave.getId()).setProcessInstanceId(processInstanceId)); + return leave.getId(); + } + + @Override + public void updateLeaveStatus(Long id, Integer status) { + validateLeaveExists(id); + leaveMapper.updateById(new BpmOALeaveDO().setId(id).setStatus(status)); + } + + private void validateLeaveExists(Long id) { + if (leaveMapper.selectById(id) == null) { + throw exception(OA_LEAVE_NOT_EXISTS); + } + } + + @Override + public BpmOALeaveDO getLeave(Long id) { + return leaveMapper.selectById(id); + } + + @Override + public PageResult getLeavePage(Long userId, BpmOALeavePageReqVO pageReqVO) { + return leaveMapper.selectPage(userId, pageReqVO); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveStatusListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveStatusListener.java new file mode 100644 index 0000000..ae5da93 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveStatusListener.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.bpm.service.oa.listener; + +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent; +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener; +import cn.iocoder.yudao.module.bpm.service.oa.BpmOALeaveService; +import cn.iocoder.yudao.module.bpm.service.oa.BpmOALeaveServiceImpl; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * OA 请假单的结果的监听器实现类 + * + * @author 芋道源码 + */ +@Component +public class BpmOALeaveStatusListener extends BpmProcessInstanceStatusEventListener { + + @Resource + private BpmOALeaveService leaveService; + + @Override + protected String getProcessDefinitionKey() { + return BpmOALeaveServiceImpl.PROCESS_KEY; + } + + @Override + protected void onEvent(BpmProcessInstanceStatusEvent event) { + leaveService.updateLeaveStatus(Long.parseLong(event.getBusinessKey()), event.getStatus()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java new file mode 100644 index 0000000..6c784e4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.bpm.service.task; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO; +import jakarta.validation.constraints.NotEmpty; +import org.flowable.bpmn.model.FlowNode; + +import java.util.Collection; + +/** + * 流程抄送 Service 接口 + * + * 现在是在审批的时候进行流程抄送 + */ +public interface BpmProcessInstanceCopyService { + + /** + * 【管理员】流程实例的抄送 + * + * @param userIds 抄送的用户编号 + * @param reason 抄送意见 + * @param taskId 流程任务编号 + */ + void createProcessInstanceCopy(Collection userIds, String reason, String taskId); + + /** + * 【自动抄送】流程实例的抄送 + * + * @param userIds 抄送的用户编号 + * @param reason 抄送意见 + * @param processInstanceId 流程编号 + * @param activityId 流程活动编号(对应 {@link FlowNode#getId()}) + * @param activityName 任务编号(对应 {@link FlowNode#getName()}) + * @param taskId 任务编号,允许空 + */ + void createProcessInstanceCopy(Collection userIds, String reason, + @NotEmpty(message = "流程实例编号不能为空") String processInstanceId, + @NotEmpty(message = "流程活动编号不能为空") String activityId, + @NotEmpty(message = "流程活动名字不能为空") String activityName, + String taskId); + + /** + * 获得抄送的流程的分页 + * + * @param userId 当前登录用户 + * @param pageReqVO 分页请求 + * @return 抄送的分页结果 + */ + PageResult getProcessInstanceCopyPage(Long userId, + BpmProcessInstanceCopyPageReqVO pageReqVO); + + /** + * 删除抄送流程 + * + * @param processInstanceId 流程实例 ID + */ + void deleteProcessInstanceCopy(String processInstanceId); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java new file mode 100644 index 0000000..ed89cfe --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.bpm.service.task; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceCopyMapper; +import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 流程抄送 Service 实现类 + * + * @author kyle + */ +@Service +@Validated +@Slf4j +public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopyService { + + @Resource + private BpmProcessInstanceCopyMapper processInstanceCopyMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmTaskService taskService; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmProcessInstanceService processInstanceService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private BpmProcessDefinitionService processDefinitionService; + + @Override + public void createProcessInstanceCopy(Collection userIds, String reason, String taskId) { + Task task = taskService.getTask(taskId); + if (ObjectUtil.isNull(task)) { + throw exception(ErrorCodeConstants.TASK_NOT_EXISTS); + } + // 执行抄送 + createProcessInstanceCopy(userIds, reason, + task.getProcessInstanceId(), task.getTaskDefinitionKey(), task.getId(), task.getName()); + } + + @Override + public void createProcessInstanceCopy(Collection userIds, String reason, String processInstanceId, + String activityId, String activityName, String taskId) { + // 1.1 校验流程实例存在 + ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); + if (processInstance == null) { + throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS); + } + // 1.2 校验流程定义存在 + ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( + processInstance.getProcessDefinitionId()); + if (processDefinition == null) { + throw exception(ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS); + } + + // 2. 创建抄送流程 + List copyList = convertList(userIds, userId -> new BpmProcessInstanceCopyDO() + .setUserId(userId).setReason(reason).setStartUserId(Long.valueOf(processInstance.getStartUserId())) + .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName()) + .setCategory(processDefinition.getCategory()).setTaskId(taskId) + .setActivityId(activityId).setActivityName(activityName) + .setProcessDefinitionId(processInstance.getProcessDefinitionId())); + processInstanceCopyMapper.insertBatch(copyList); + } + + @Override + public PageResult getProcessInstanceCopyPage(Long userId, + BpmProcessInstanceCopyPageReqVO pageReqVO) { + return processInstanceCopyMapper.selectPage(userId, pageReqVO); + } + + @Override + public void deleteProcessInstanceCopy(String processInstanceId) { + processInstanceCopyMapper.deleteByProcessInstanceId(processInstanceId); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java new file mode 100644 index 0000000..abba224 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java @@ -0,0 +1,191 @@ +package cn.iocoder.yudao.module.bpm.service.task; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; +import jakarta.validation.Valid; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 流程实例 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmProcessInstanceService { + + // ========== Query 查询相关方法 ========== + + /** + * 获得流程实例 + * + * @param id 流程实例的编号 + * @return 流程实例 + */ + ProcessInstance getProcessInstance(String id); + + /** + * 获得流程实例列表 + * + * @param ids 流程实例的编号集合 + * @return 流程实例列表 + */ + List getProcessInstances(Set ids); + + /** + * 获得流程实例 Map + * + * @param ids 流程实例的编号集合 + * @return 流程实例列表 Map + */ + default Map getProcessInstanceMap(Set ids) { + return convertMap(getProcessInstances(ids), ProcessInstance::getProcessInstanceId); + } + + /** + * 获得历史的流程实例 + * + * @param id 流程实例的编号 + * @return 历史的流程实例 + */ + HistoricProcessInstance getHistoricProcessInstance(String id); + + /** + * 获得历史的流程实例列表 + * + * @param ids 流程实例的编号集合 + * @return 历史的流程实例列表 + */ + List getHistoricProcessInstances(Set ids); + + /** + * 获得历史的流程实例 Map + * + * @param ids 流程实例的编号集合 + * @return 历史的流程实例列表 Map + */ + default Map getHistoricProcessInstanceMap(Set ids) { + return convertMap(getHistoricProcessInstances(ids), HistoricProcessInstance::getId); + } + + /** + * 获得流程实例的分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页请求 + * @return 流程实例的分页 + */ + PageResult getProcessInstancePage(Long userId, + @Valid BpmProcessInstancePageReqVO pageReqVO); + + /** + * 获取审批详情。 + *

+ * 可以是准备发起的流程、进行中的流程、已经结束的流程 + * + * @param loginUserId 登录人的用户编号 + * @param reqVO 请求信息 + * @return 流程实例的进度 + */ + BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO); + + /** + * 获取下一个执行节点信息 + * + * @param loginUserId 登录人的用户编号 + * @param reqVO 请求信息 + * @return 下一个执行节点信息 + */ + List getNextApprovalNodes(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO); + + /** + * 获取流程实例的 BPMN 模型视图 + * + * @param id 流程实例的编号 + * @return BPMN 模型视图 + */ + BpmProcessInstanceBpmnModelViewRespVO getProcessInstanceBpmnModelView(String id); + + // ========== Update 写入相关方法 ========== + + /** + * 创建流程实例(提供给前端) + * + * @param userId 用户编号 + * @param createReqVO 创建信息 + * @return 实例的编号 + */ + String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO); + + /** + * 创建流程实例(提供给内部) + * + * @param userId 用户编号 + * @param createReqDTO 创建信息 + * @return 实例的编号 + */ + String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO); + + /** + * 发起人取消流程实例 + * + * @param userId 用户编号 + * @param cancelReqVO 取消信息 + */ + void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO); + + /** + * 管理员取消流程实例 + * + * @param userId 用户编号 + * @param cancelReqVO 取消信息 + */ + void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO); + + /** + * 更新 ProcessInstance 为不通过 + * + * @param processInstance 流程实例 + * @param reason 理由。例如说,审批不通过时,需要传递该值 + */ + void updateProcessInstanceReject(ProcessInstance processInstance, String reason); + + /** + * 更新 ProcessInstance 的变量 + * + * @param id 流程编号 + * @param variables 流程变量 + */ + void updateProcessInstanceVariables(String id, Map variables); + + /** + * 删除 ProcessInstance 的变量 + * + * @param id 流程编号 + * @param variableNames 流程变量名 + */ + void removeProcessInstanceVariables(String id, Collection variableNames); + + // ========== Event 事件相关方法 ========== + + /** + * 处理 ProcessInstance 完成事件,例如说:审批通过、不通过、取消 + * + * @param instance 流程任务 + */ + void processProcessInstanceCompleted(ProcessInstance instance); + + /** + * 处理 ProcessInstance 开始事件,例如说:流程前置通知 + * + * @param instance 流程任务 + */ + void processProcessInstanceCreated(ProcessInstance instance); +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java new file mode 100644 index 0000000..a42a3ec --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -0,0 +1,963 @@ +package cn.iocoder.yudao.module.bpm.service.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.object.PageUtils; +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNodeTask; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO; +import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.constants.BpmnXMLConstants; +import org.flowable.bpmn.model.*; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.history.HistoricProcessInstanceQuery; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.runtime.ProcessInstanceBuilder; +import org.flowable.task.api.Task; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.flowable.bpmn.constants.BpmnXMLConstants.*; + +/** + * 流程实例 Service 实现类 + *

+ * ProcessDefinition & ProcessInstance & Execution & Task 的关系: + * 1. + *

+ * HistoricProcessInstance & ProcessInstance 的关系: + * 1. + *

+ * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService { + + @Resource + private RuntimeService runtimeService; + @Resource + private HistoryService historyService; + + @Resource + private BpmProcessDefinitionService processDefinitionService; + @Resource + @Lazy // 避免循环依赖 + private BpmTaskService taskService; + @Resource + private BpmMessageService messageService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + @Resource + private BpmProcessInstanceEventPublisher processInstanceEventPublisher; + + @Resource + private BpmTaskCandidateInvoker taskCandidateInvoker; + + @Resource + private BpmProcessIdRedisDAO processIdRedisDAO; + + // ========== Query 查询相关方法 ========== + + @Override + public ProcessInstance getProcessInstance(String id) { + return runtimeService.createProcessInstanceQuery() + .includeProcessVariables() + .processInstanceId(id) + .singleResult(); + } + + @Override + public List getProcessInstances(Set ids) { + return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).includeProcessVariables().list(); + } + + @Override + public HistoricProcessInstance getHistoricProcessInstance(String id) { + return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).includeProcessVariables() + .singleResult(); + } + + @Override + public List getHistoricProcessInstances(Set ids) { + return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).includeProcessVariables() + .list(); + } + + private Map getFormFieldsPermission(BpmnModel bpmnModel, + String activityId, String taskId) { + // 1. 获取流程活动编号。流程活动 Id 为空事,从流程任务中获取流程活动 Id + if (StrUtil.isEmpty(activityId) && StrUtil.isNotEmpty(taskId)) { + activityId = Optional.ofNullable(taskService.getHistoricTask(taskId)) + .map(HistoricTaskInstance::getTaskDefinitionKey).orElse(null); + } + if (StrUtil.isEmpty(activityId)) { + return null; + } + + // 2. 从 BpmnModel 中解析表单字段权限 + return BpmnModelUtils.parseFormFieldsPermission(bpmnModel, activityId); + } + + @Override + public BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, BpmApprovalDetailReqVO reqVO) { + // 1.1 从 reqVO 中,读取公共变量 + Long startUserId = loginUserId; // 流程发起人 + HistoricProcessInstance historicProcessInstance = null; // 流程实例 + Integer processInstanceStatus = BpmProcessInstanceStatusEnum.NOT_START.getStatus(); // 流程状态 + Map processVariables = new HashMap<>(); // 流程变量 + // 1.2 如果是流程已发起的场景,则使用流程实例的数据 + if (reqVO.getProcessInstanceId() != null) { + historicProcessInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId()); + if (historicProcessInstance == null) { + throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS); + } + startUserId = Long.valueOf(historicProcessInstance.getStartUserId()); + processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance); + // 合并 DB 和前端传递的流量变量,以前端的为主 + if (CollUtil.isNotEmpty(historicProcessInstance.getProcessVariables())) { + processVariables.putAll(historicProcessInstance.getProcessVariables()); + } + } + if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) { + processVariables.putAll(reqVO.getProcessVariables()); + } + // 1.3 读取其它相关数据 + ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( + historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId() + : reqVO.getProcessDefinitionId()); + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService + .getProcessDefinitionInfo(processDefinition.getId()); + BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId()); + + // 2.1 已结束 + 进行中的活动节点 + List endActivityNodes = null; // 已结束的审批信息 + List runActivityNodes = null; // 进行中的审批信息 + List activities = null; // 流程实例列表 + if (reqVO.getProcessInstanceId() != null) { + activities = taskService.getActivityListByProcessInstanceId(reqVO.getProcessInstanceId()); + List tasks = taskService.getTaskListByProcessInstanceId(reqVO.getProcessInstanceId(), + true); + endActivityNodes = getEndActivityNodeList(startUserId, bpmnModel, processDefinitionInfo, + historicProcessInstance, processInstanceStatus, activities, tasks); + runActivityNodes = getRunApproveNodeList(startUserId, bpmnModel, processDefinition, processVariables, + activities, tasks); + } + + // 2.2 流程已经结束,直接 return,无需预测 + if (BpmProcessInstanceStatusEnum.isProcessEndStatus(processInstanceStatus)) { + return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, + historicProcessInstance, + processInstanceStatus, endActivityNodes, runActivityNodes, null, null); + } + + // 3.1 计算当前登录用户的待办任务 + BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId()); + // 3.2 预测未运行节点的审批信息 + List simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel, + processDefinitionInfo, + processVariables, activities); + // 3.3 如果是发起动作,activityId 为开始节点,不校验审批人自选节点 + if (ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID)) { + simulateActivityNodes.removeIf(node -> + BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy().equals(node.getCandidateStrategy())); + } + + // 4. 拼接最终数据 + return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, + processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask); + } + + @Override + public List getNextApprovalNodes(Long loginUserId, BpmApprovalDetailReqVO reqVO) { + // 1.1 校验任务存在,且是当前用户的 + Task task = taskService.validateTask(loginUserId, reqVO.getTaskId()); + // 1.2 校验流程实例存在 + ProcessInstance instance = getProcessInstance(task.getProcessInstanceId()); + if (instance == null) { + throw exception(PROCESS_INSTANCE_NOT_EXISTS); + } + HistoricProcessInstance historicProcessInstance = getHistoricProcessInstance(task.getProcessInstanceId()); + if (historicProcessInstance == null) { + throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS); + } + // 1.3 校验BpmnModel + BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(task.getProcessDefinitionId()); + if (bpmnModel == null) { + return null; + } + + // 2. 设置流程变量 + Map processVariables = new HashMap<>(); + // 2.1 获取历史中流程变量 + if (CollUtil.isNotEmpty(historicProcessInstance.getProcessVariables())) { + processVariables.putAll(historicProcessInstance.getProcessVariables()); + } + // 2.2 合并前端传递的流程变量,以前端为准 + if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) { + processVariables.putAll(reqVO.getProcessVariables()); + } + + // 3. 获取下一个将要执行的节点集合 + FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey()); + List nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables); + List nextActivityNodes = convertList(nextFlowNodes, node -> new ActivityNode().setId(node.getId()) + .setName(node.getName()).setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) + .setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) + .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node)) + .setCandidateUserIds(getTaskCandidateUserList(bpmnModel, node.getId(), + loginUserId, historicProcessInstance.getProcessDefinitionId(), processVariables))); + if (CollUtil.isEmpty(nextActivityNodes)) { + return nextActivityNodes; + } + + // 4. 拼接基础信息 + Map userMap = adminUserApi.getUserMap( + convertSetByFlatMap(nextActivityNodes, ActivityNode::getCandidateUserIds, Collection::stream)); + Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + nextActivityNodes.forEach(node -> node.setCandidateUsers(convertList(node.getCandidateUserIds(), userId -> { + AdminUserRespDTO user = userMap.get(userId); + if (user != null) { + return BpmProcessInstanceConvert.INSTANCE.buildUser(userId, userMap, deptMap); + } + return null; + }))); + return nextActivityNodes; + } + + @Override + @SuppressWarnings("unchecked") + public PageResult getProcessInstancePage(Long userId, + BpmProcessInstancePageReqVO pageReqVO) { + // 1. 构建查询条件 + HistoricProcessInstanceQuery processInstanceQuery = historyService.createHistoricProcessInstanceQuery() + .includeProcessVariables() + .processInstanceTenantId(FlowableUtils.getTenantId()) + .orderByProcessInstanceStartTime().desc(); + if (userId != null) { // 【我的流程】菜单时,需要传递该字段 + processInstanceQuery.startedBy(String.valueOf(userId)); + } else if (pageReqVO.getStartUserId() != null) { // 【管理流程】菜单时,才会传递该字段 + processInstanceQuery.startedBy(String.valueOf(pageReqVO.getStartUserId())); + } + if (StrUtil.isNotEmpty(pageReqVO.getName())) { + processInstanceQuery.processInstanceNameLike("%" + pageReqVO.getName() + "%"); + } + if (StrUtil.isNotEmpty(pageReqVO.getProcessDefinitionKey())) { + processInstanceQuery.processDefinitionKey(pageReqVO.getProcessDefinitionKey()); + } + if (StrUtil.isNotEmpty(pageReqVO.getCategory())) { + processInstanceQuery.processDefinitionCategory(pageReqVO.getCategory()); + } + if (pageReqVO.getStatus() != null) { + processInstanceQuery.variableValueEquals(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, + pageReqVO.getStatus()); + } + if (ArrayUtil.isNotEmpty(pageReqVO.getCreateTime())) { + processInstanceQuery.startedAfter(DateUtils.of(pageReqVO.getCreateTime()[0])); + processInstanceQuery.startedBefore(DateUtils.of(pageReqVO.getCreateTime()[1])); + } + if (ArrayUtil.isNotEmpty(pageReqVO.getEndTime())) { + processInstanceQuery.finishedAfter(DateUtils.of(pageReqVO.getEndTime()[0])); + processInstanceQuery.finishedBefore(DateUtils.of(pageReqVO.getEndTime()[1])); + } + // 表单字段查询 + Map formFieldsParams = JsonUtils.parseObject(pageReqVO.getFormFieldsParams(), Map.class); + if (CollUtil.isNotEmpty(formFieldsParams)) { + formFieldsParams.forEach((key, value) -> { + if (StrUtil.isEmpty(String.valueOf(value))) { + return; + } + // TODO @lesan:应支持多种类型的查询方式,目前只有字符串全等 + processInstanceQuery.variableValueEquals(key, value); + }); + } + + // 2.1 查询数量 + long processInstanceCount = processInstanceQuery.count(); + if (processInstanceCount == 0) { + return PageResult.empty(processInstanceCount); + } + // 2.2 查询列表 + List processInstanceList = processInstanceQuery.listPage(PageUtils.getStart(pageReqVO), + pageReqVO.getPageSize()); + return new PageResult<>(processInstanceList, processInstanceCount); + } + + /** + * 拼接审批详情的最终数据 + *

+ * 主要是,拼接审批人的用户信息、部门信息 + */ + private BpmApprovalDetailRespVO buildApprovalDetail(BpmApprovalDetailReqVO reqVO, + BpmnModel bpmnModel, + ProcessDefinition processDefinition, + BpmProcessDefinitionInfoDO processDefinitionInfo, + HistoricProcessInstance processInstance, + Integer processInstanceStatus, + List endApprovalNodeInfos, + List runningApprovalNodeInfos, + List simulateApprovalNodeInfos, + BpmTaskRespVO todoTask) { + // 1. 获取所有需要读取用户信息的 userIds + List approveNodes = newArrayList( + asList(endApprovalNodeInfos, runningApprovalNodeInfos, simulateApprovalNodeInfos)); + Set userIds = BpmProcessInstanceConvert.INSTANCE.parseUserIds(processInstance, approveNodes, todoTask); + Map userMap = adminUserApi.getUserMap(userIds); + Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + + // 2. 表单权限 + String taskId = reqVO.getTaskId() == null && todoTask != null ? todoTask.getId() : reqVO.getTaskId(); + Map formFieldsPermission = getFormFieldsPermission(bpmnModel, reqVO.getActivityId(), taskId); + + // 3. 拼接数据 + return BpmProcessInstanceConvert.INSTANCE.buildApprovalDetail(bpmnModel, processDefinition, + processDefinitionInfo, processInstance, + processInstanceStatus, approveNodes, todoTask, formFieldsPermission, userMap, deptMap); + } + + /** + * 获得【已结束】的活动节点们 + */ + private List getEndActivityNodeList(Long startUserId, BpmnModel bpmnModel, + BpmProcessDefinitionInfoDO processDefinitionInfo, + HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus, + List activities, List tasks) { + // 遍历 tasks 列表,只处理已结束的 UserTask + // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点 + List endTasks = filterList(tasks, task -> task.getEndTime() != null); + List approvalNodes = convertList(endTasks, task -> { + FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); + ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName()) + .setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey()) + ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() + : ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”的识别 + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) + .setStatus(FlowableUtils.getTaskStatus(task)) + .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) + .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime())) + .setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task))); + // 如果是取消状态,则跳过 + if (BpmTaskStatusEnum.isCancelStatus(activityNode.getStatus())) { + return null; + } + return activityNode; + }); + + // 遍历 activities,只处理已结束的 StartEvent、EndEvent + List endActivities = filterList(activities, activity -> activity.getEndTime() != null + && (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_EVENT_START, ELEMENT_CALL_ACTIVITY, ELEMENT_EVENT_END))); + endActivities.forEach(activity -> { + // StartEvent:只处理 BPMN 的场景。因为,SIMPLE 情况下,已经有 START_USER_NODE 节点 + if (ELEMENT_EVENT_START.equals(activity.getActivityType()) + && BpmModelTypeEnum.BPMN.getType().equals(processDefinitionInfo.getModelType())) { + ActivityNodeTask startTask = new ActivityNodeTask().setId(BpmnModelConstants.START_USER_NODE_ID) + .setAssignee(startUserId).setStatus(BpmTaskStatusEnum.APPROVE.getStatus()); + ActivityNode startNode = new ActivityNode().setId(startTask.getId()) + .setName(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getName()) + .setNodeType(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()) + .setStatus(startTask.getStatus()).setTasks(ListUtil.of(startTask)) + .setStartTime(DateUtils.of(activity.getStartTime())) + .setEndTime(DateUtils.of(activity.getEndTime())); + approvalNodes.add(0, startNode); + return; + } + // EndEvent + if (ELEMENT_EVENT_END.equals(activity.getActivityType())) { + if (BpmProcessInstanceStatusEnum.isRejectStatus(processInstanceStatus)) { + // 拒绝情况下,不需要展示 EndEvent 结束节点。原因是:前端已经展示 x 效果,无需重复展示 + return; + } + ActivityNode endNode = new ActivityNode().setId(activity.getId()) + .setName(BpmSimpleModelNodeTypeEnum.END_NODE.getName()) + .setNodeType(BpmSimpleModelNodeTypeEnum.END_NODE.getType()).setStatus(processInstanceStatus) + .setStartTime(DateUtils.of(activity.getStartTime())) + .setEndTime(DateUtils.of(activity.getEndTime())); + String reason = FlowableUtils.getProcessInstanceReason(historicProcessInstance); + if (StrUtil.isNotEmpty(reason)) { + endNode.setTasks(singletonList(new ActivityNodeTask().setId(endNode.getId()) + .setStatus(endNode.getStatus()).setReason(reason))); + } + approvalNodes.add(endNode); + } + // CallActivity + if (ELEMENT_CALL_ACTIVITY.equals(activity.getActivityType())) { + ActivityNode callActivity = new ActivityNode().setId(activity.getId()) + .setName(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getName()) + .setNodeType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType()).setStatus(processInstanceStatus) + .setStartTime(DateUtils.of(activity.getStartTime())) + .setEndTime(DateUtils.of(activity.getEndTime())) + .setProcessInstanceId(activity.getProcessInstanceId()); + approvalNodes.add(callActivity); + } + }); + + // 按照时间排序 + approvalNodes.sort(Comparator.comparing(ActivityNode::getStartTime)); + return approvalNodes; + } + + /** + * 获得【进行中】的活动节点们 + */ + private List getRunApproveNodeList(Long startUserId, + BpmnModel bpmnModel, + ProcessDefinition processDefinition, + Map processVariables, + List activities, + List tasks) { + // 构建运行中的任务、子流程,基于 activityId 分组 + List runActivities = filterList(activities, activity -> activity.getEndTime() == null + && (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_TASK_USER, ELEMENT_CALL_ACTIVITY))); + Map> runningTaskMap = convertMultiMap(runActivities, + HistoricActivityInstance::getActivityId); + + // 按照 activityId 分组,构建 ApprovalNodeInfo 节点 + Map taskMap = convertMap(tasks, HistoricTaskInstance::getId); + return convertList(runningTaskMap.entrySet(), entry -> { + String activityId = entry.getKey(); + List taskActivities = entry.getValue(); + // 构建活动节点 + FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); + HistoricActivityInstance firstActivity = CollUtil.getFirst(taskActivities); // 取第一个任务,会签/或签的任务,开始时间相同 + ActivityNode activityNode = new ActivityNode().setId(firstActivity.getActivityId()) + .setName(firstActivity.getActivityName()) + .setNodeType(ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”和"子流程"的识别 + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) + .setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) + .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) + .setStartTime(DateUtils.of(CollUtil.getFirst(taskActivities).getStartTime())) + .setTasks(new ArrayList<>()); + // 处理每个任务的 tasks 属性 + for (HistoricActivityInstance activity : taskActivities) { + HistoricTaskInstance task = taskMap.get(activity.getTaskId()); + // 特殊情况:子流程节点 ChildProcess 仅存在于 activity 中,并且没有自身的 task,需要跳过执行 + // TODO @芋艿:后续看看怎么优化! + if (task == null) { + continue; + } + activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task)); + // 加签子任务,需要过滤掉已经完成的加签子任务 + List childrenTasks = filterList( + taskService.getAllChildrenTaskListByParentTaskId(activity.getTaskId(), tasks), + childTask -> childTask.getEndTime() == null); + if (CollUtil.isNotEmpty(childrenTasks)) { + activityNode.getTasks().addAll( + convertList(childrenTasks, BpmProcessInstanceConvert.INSTANCE::buildApprovalTaskInfo)); + } + } + // 处理每个任务的 candidateUsers 属性:如果是依次审批,需要预测它的后续审批人。因为 Task 是审批完一个,创建一个新的 Task + if (BpmnModelUtils.isSequentialUserTask(flowNode)) { + List candidateUserIds = getTaskCandidateUserList(bpmnModel, flowNode.getId(), + startUserId, processDefinition.getId(), processVariables); + // 截取当前审批人位置后面的候选人,不包含当前审批人 + ActivityNodeTask approvalTaskInfo = CollUtil.getFirst(activityNode.getTasks()); + Assert.notNull(approvalTaskInfo, "任务不能为空"); + int index = CollUtil.indexOf(candidateUserIds, + userId -> ObjectUtils.equalsAny(userId, approvalTaskInfo.getOwner(), + approvalTaskInfo.getAssignee())); // 委派或者向前加签情况,需要先比较 owner + activityNode.setCandidateUserIds(CollUtil.sub(candidateUserIds, index + 1, candidateUserIds.size())); + } + if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(activityNode.getNodeType())) { + activityNode.setProcessInstanceId(firstActivity.getProcessInstanceId()); + } + return activityNode; + }); + } + + /** + * 获得【预测(未来)】的活动节点们 + */ + private List getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel, + BpmProcessDefinitionInfoDO processDefinitionInfo, + Map processVariables, + List activities) { + // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance + // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 + Set runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId); + // 情况一:BPMN 设计器 + if (Objects.equals(BpmModelTypeEnum.BPMN.getType(), processDefinitionInfo.getModelType())) { + List flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables); + return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn(startUserId, bpmnModel, + processDefinitionInfo, processVariables, flowElement, runActivityIds)); + } + // 情况二:SIMPLE 设计器 + if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) { + BpmSimpleModelNodeVO simpleModel = JsonUtils.parseObject(processDefinitionInfo.getSimpleModel(), + BpmSimpleModelNodeVO.class); + List simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables); + return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple(startUserId, bpmnModel, + processDefinitionInfo, processVariables, simpleNode, runActivityIds)); + } + throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType()); + } + + private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel, + BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, + BpmSimpleModelNodeVO node, Set runActivityIds) { + // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance + // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 + if (runActivityIds.contains(node.getId())) { + return null; + } + + ActivityNode activityNode = new ActivityNode().setId(node.getId()).setName(node.getName()) + .setNodeType(node.getType()).setCandidateStrategy(node.getCandidateStrategy()) + .setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); + + // 1. 开始节点/审批节点 + if (ObjectUtils.equalsAny(node.getType(), + BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(), + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType(), + BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE.getType())) { + List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), + startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); + activityNode.setCandidateUserIds(candidateUserIds); + return activityNode; + } + + // 2. 结束节点 + if (BpmSimpleModelNodeTypeEnum.END_NODE.getType().equals(node.getType())) { + return activityNode; + } + + // 3. 抄送节点 + if (CollUtil.isEmpty(runActivityIds) && // 流程发起时:需要展示抄送节点,用于选择抄送人 + BpmSimpleModelNodeTypeEnum.COPY_NODE.getType().equals(node.getType())) { + List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), + startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); + activityNode.setCandidateUserIds(candidateUserIds); + return activityNode; + } + + // 4. 子流程节点 + if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(node.getType())) { + return activityNode; + } + return null; + } + + private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, + BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, + FlowElement node, Set runActivityIds) { + if (runActivityIds.contains(node.getId())) { + return null; + } + ActivityNode activityNode = new ActivityNode().setId(node.getId()) + .setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); + + // 1. 开始节点 + if (node instanceof StartEvent) { + return activityNode.setName(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getName()) + .setNodeType(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()); + } + + // 2. 审批节点 + if (node instanceof UserTask) { + List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), + startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); + return activityNode.setName(node.getName()).setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) + .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node)) + .setCandidateUserIds(candidateUserIds); + } + + // 3. 结束节点 + if (node instanceof EndEvent) { + return activityNode.setName(BpmSimpleModelNodeTypeEnum.END_NODE.getName()) + .setNodeType(BpmSimpleModelNodeTypeEnum.END_NODE.getType()); + } + return null; + } + + private List getTaskCandidateUserList(BpmnModel bpmnModel, String activityId, + Long startUserId, String processDefinitionId, Map processVariables) { + Set userIds = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId, + startUserId, processDefinitionId, processVariables); + return new ArrayList<>(userIds); + } + + @Override + public BpmProcessInstanceBpmnModelViewRespVO getProcessInstanceBpmnModelView(String id) { + // 1.1 获得流程实例 + HistoricProcessInstance processInstance = getHistoricProcessInstance(id); + if (processInstance == null) { + return null; + } + // 1.2 获得流程定义 + BpmnModel bpmnModel = processDefinitionService + .getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId()); + if (bpmnModel == null) { + return null; + } + BpmSimpleModelNodeVO simpleModel = null; + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo( + processInstance.getProcessDefinitionId()); + if (processDefinitionInfo != null + && BpmModelTypeEnum.SIMPLE.getType().equals(processDefinitionInfo.getModelType())) { + simpleModel = JsonUtils.parseObject(processDefinitionInfo.getSimpleModel(), BpmSimpleModelNodeVO.class); + } + // 1.3 获得流程实例对应的活动实例列表 + 任务列表 + List activities = taskService.getActivityListByProcessInstanceId(id); + List tasks = taskService.getTaskListByProcessInstanceId(id, true); + + // 2.1 拼接进度信息 + Set unfinishedTaskActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, + activityInstance -> activityInstance.getEndTime() == null); + Set finishedTaskActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, + activityInstance -> activityInstance.getEndTime() != null + && ObjectUtil.notEqual(activityInstance.getActivityType(), + BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); + Set finishedSequenceFlowActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, + activityInstance -> activityInstance.getEndTime() != null + && ObjectUtil.equals(activityInstance.getActivityType(), + BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); + // 特殊:会签情况下,会有部分已完成(审批)、部分未完成(待审批),此时需要 finishedTaskActivityIds 移除掉 + finishedTaskActivityIds.removeAll(unfinishedTaskActivityIds); + // 特殊:如果流程实例被拒绝,则需要计算是哪个活动节点。 + // 注意,只取最后一个。因为会存在多次拒绝的情况,拒绝驳回到指定节点 + Set rejectTaskActivityIds = CollUtil.newHashSet(); + if (BpmProcessInstanceStatusEnum.isRejectStatus(FlowableUtils.getProcessInstanceStatus(processInstance))) { + tasks.stream() + .filter(task -> BpmTaskStatusEnum.isRejectStatus(FlowableUtils.getTaskStatus(task))) + .max(Comparator.comparing(HistoricTaskInstance::getEndTime)) + .ifPresent(reject -> rejectTaskActivityIds.add(reject.getTaskDefinitionKey())); + finishedTaskActivityIds.removeAll(rejectTaskActivityIds); + } + + // 2.2 拼接基础信息 + Set userIds = BpmProcessInstanceConvert.INSTANCE.parseUserIds02(processInstance, tasks); + Map userMap = adminUserApi.getUserMap(userIds); + Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + return BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceBpmnModelView(processInstance, tasks, bpmnModel, + simpleModel, + unfinishedTaskActivityIds, finishedTaskActivityIds, finishedSequenceFlowActivityIds, + rejectTaskActivityIds, + userMap, deptMap); + } + + // ========== Update 写入相关方法 ========== + + @Override + @Transactional(rollbackFor = Exception.class) + public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { + // 获得流程定义 + ProcessDefinition definition = processDefinitionService + .getProcessDefinition(createReqVO.getProcessDefinitionId()); + // 发起流程 + return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, + createReqVO.getStartUserSelectAssignees()); + } + + @Override + public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { + return FlowableUtils.executeAuthenticatedUserId(userId, () -> { + // 获得流程定义 + ProcessDefinition definition = processDefinitionService + .getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); + // 发起流程 + return createProcessInstance0(userId, definition, createReqDTO.getVariables(), + createReqDTO.getBusinessKey(), + createReqDTO.getStartUserSelectAssignees()); + }); + } + + private String createProcessInstance0(Long userId, ProcessDefinition definition, + Map variables, String businessKey, + Map> startUserSelectAssignees) { + // 1.1 校验流程定义 + if (definition == null) { + throw exception(PROCESS_DEFINITION_NOT_EXISTS); + } + if (definition.isSuspended()) { + throw exception(PROCESS_DEFINITION_IS_SUSPENDED); + } + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService + .getProcessDefinitionInfo(definition.getId()); + if (processDefinitionInfo == null) { + throw exception(PROCESS_DEFINITION_NOT_EXISTS); + } + // 1.2 校验是否能够发起 + if (!processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, userId)) { + throw exception(PROCESS_INSTANCE_START_USER_CAN_START); + } + // 1.3 校验发起人自选审批人 + validateStartUserSelectAssignees(userId, definition, startUserSelectAssignees, variables); + + // 2. 创建流程实例 + if (variables == null) { + variables = new HashMap<>(); + } + FlowableUtils.filterProcessInstanceFormVariable(variables); // 过滤一下,避免 ProcessInstance 系统级的变量被占用 + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, userId); // 设置流程变量,发起人 ID + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中 + BpmProcessInstanceStatusEnum.RUNNING.getStatus()); + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 true,不影响没配置 skipExpression 的节点 + if (CollUtil.isNotEmpty(startUserSelectAssignees)) { + // 设置流程变量,发起人自选审批人 + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, + startUserSelectAssignees); + } + + // 3. 创建流程 + ProcessInstanceBuilder processInstanceBuilder = runtimeService.createProcessInstanceBuilder() + .processDefinitionId(definition.getId()) + .businessKey(businessKey) + .variables(variables); + // 3.1 创建流程 ID + BpmModelMetaInfoVO.ProcessIdRule processIdRule = processDefinitionInfo.getProcessIdRule(); + if (processIdRule != null && Boolean.TRUE.equals(processIdRule.getEnable())) { + processInstanceBuilder.predefineProcessInstanceId(processIdRedisDAO.generate(processIdRule)); + } + // 3.2 流程名称 + BpmModelMetaInfoVO.TitleSetting titleSetting = processDefinitionInfo.getTitleSetting(); + if (titleSetting != null && Boolean.TRUE.equals(titleSetting.getEnable())) { + AdminUserRespDTO user = adminUserApi.getUser(userId); + Map cloneVariables = new HashMap<>(variables); + cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname()); + cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now()); + cloneVariables.put(BpmnVariableConstants.PROCESS_DEFINITION_NAME, definition.getName().trim()); + processInstanceBuilder.name(StrUtil.format(titleSetting.getTitle(), cloneVariables)); + } else { + processInstanceBuilder.name(definition.getName().trim()); + } + // 3.3 发起流程实例 + ProcessInstance instance = processInstanceBuilder.start(); + return instance.getId(); + } + + private void validateStartUserSelectAssignees(Long userId, ProcessDefinition definition, + Map> startUserSelectAssignees, + Map variables) { + // 1. 获取预测的节点信息 + BpmApprovalDetailRespVO detailRespVO = getApprovalDetail(userId, new BpmApprovalDetailReqVO() + .setProcessDefinitionId(definition.getId()) + .setProcessVariables(variables)); + List activityNodes = detailRespVO.getActivityNodes(); + if (CollUtil.isEmpty(activityNodes)) { + return; + } + + // 2.1 移除掉不是发起人自选审批人节点 + activityNodes.removeIf(task -> + ObjectUtil.notEqual(BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy(), task.getCandidateStrategy())); + // 2.2 流程发起时要先获取当前流程的预测走向节点,发起时只校验预测的节点发起人自选审批人的审批人和抄送人是否都配置了 + activityNodes.forEach(task -> { + List assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(task.getId()) : null; + if (CollUtil.isEmpty(assignees)) { + throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, task.getName()); + } + Map userMap = adminUserApi.getUserMap(assignees); + assignees.forEach(assignee -> { + if (userMap.get(assignee) == null) { + throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS, task.getName(), assignee); + } + }); + }); + } + + @Override + public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { + // 1.1 校验流程实例存在 + ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); + if (instance == null) { + throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); + } + // 1.2 只能取消自己的 + if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { + throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); + } + // 1.3 校验允许撤销审批中的申请 + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService + .getProcessDefinitionInfo(instance.getProcessDefinitionId()); + Assert.notNull(processDefinitionInfo, "流程定义({})不存在", processDefinitionInfo); + if (processDefinitionInfo.getAllowCancelRunningProcess() != null // 防止未配置 AllowCancelRunningProcess , 默认为可取消 + && Boolean.FALSE.equals(processDefinitionInfo.getAllowCancelRunningProcess())) { + throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW); + } + // 1.4 子流程不允许取消 + if (StrUtil.isNotBlank(instance.getSuperExecutionId())) { + throw exception(PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW); + } + + // 2. 取消流程 + updateProcessInstanceCancel(cancelReqVO.getId(), + BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format(cancelReqVO.getReason())); + } + + @Override + public void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO) { + // 1.1 校验流程实例存在 + ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); + if (instance == null) { + throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); + } + + // 2. 取消流程 + AdminUserRespDTO user = adminUserApi.getUser(userId); + updateProcessInstanceCancel(cancelReqVO.getId(), + BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format(user.getNickname(), cancelReqVO.getReason())); + } + + private void updateProcessInstanceCancel(String id, String reason) { + // 1. 更新流程实例 status + runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, + BpmProcessInstanceStatusEnum.CANCEL.getStatus()); + runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason); + + // 2. 取消所有子流程 + List childProcessInstances = runtimeService.createProcessInstanceQuery() + .superProcessInstanceId(id).list(); + childProcessInstances.forEach(processInstance -> updateProcessInstanceCancel( + processInstance.getProcessInstanceId(), BpmReasonEnum.CANCEL_CHILD_PROCESS_INSTANCE_BY_MAIN_PROCESS.getReason())); + + // 3. 结束流程 + taskService.moveTaskToEnd(id, reason); + } + + @Override + public void updateProcessInstanceReject(ProcessInstance processInstance, String reason) { + runtimeService.setVariable(processInstance.getProcessInstanceId(), + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, + BpmProcessInstanceStatusEnum.REJECT.getStatus()); + runtimeService.setVariable(processInstance.getProcessInstanceId(), + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, + BpmReasonEnum.REJECT_TASK.format(reason)); + } + + @Override + public void updateProcessInstanceVariables(String id, Map variables) { + runtimeService.setVariables(id, variables); + } + + @Override + public void removeProcessInstanceVariables(String id, Collection variableNames) { + runtimeService.removeVariables(id, variableNames); + } + + // ========== Event 事件相关方法 ========== + + @Override + public void processProcessInstanceCompleted(ProcessInstance instance) { + // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 + FlowableUtils.execute(instance.getTenantId(), () -> { + // 1.1 获取当前状态 + Integer status = (Integer) instance.getProcessVariables() + .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); + String reason = (String) instance.getProcessVariables() + .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); + // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 + // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 + if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { + status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); + runtimeService.setVariable(instance.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, + status); + } + + // 2. 发送对应的消息通知 + if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) { + messageService.sendMessageWhenProcessInstanceApprove( + BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceApproveMessage(instance)); + } else if (Objects.equals(status, BpmProcessInstanceStatusEnum.REJECT.getStatus())) { + messageService.sendMessageWhenProcessInstanceReject( + BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(instance, reason)); + } + + // 3. 发送流程实例的状态事件 + processInstanceEventPublisher.sendProcessInstanceResultEvent( + BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status)); + + // 4. 流程后置通知 + if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) { + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. + getProcessDefinitionInfo(instance.getProcessDefinitionId()); + if (ObjUtil.isNotNull(processDefinitionInfo) && + ObjUtil.isNotNull(processDefinitionInfo.getProcessAfterTriggerSetting())) { + BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessAfterTriggerSetting(); + + BpmHttpRequestUtils.executeBpmHttpRequest(instance, + setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); + } + } + }); + } + + @Override + public void processProcessInstanceCreated(ProcessInstance instance) { + // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 + FlowableUtils.execute(instance.getTenantId(), () -> { + // 流程前置通知 + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. + getProcessDefinitionInfo(instance.getProcessDefinitionId()); + if (ObjUtil.isNull(processDefinitionInfo) || + ObjUtil.isNull(processDefinitionInfo.getProcessBeforeTriggerSetting())) { + return; + } + BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessBeforeTriggerSetting(); + BpmHttpRequestUtils.executeBpmHttpRequest(instance, + setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); + }); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java new file mode 100644 index 0000000..0a5c866 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java @@ -0,0 +1,316 @@ +package cn.iocoder.yudao.module.bpm.service.task; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum; +import jakarta.validation.Valid; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.history.HistoricTaskInstance; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 流程任务实例 Service 接口 + * + * @author jason + * @author 芋道源码 + */ +public interface BpmTaskService { + + // ========== Query 查询相关方法 ========== + + /** + * 获得待办的流程任务分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页请求 + * @return 流程任务分页 + */ + PageResult getTaskTodoPage(Long userId, BpmTaskPageReqVO pageReqVO); + + /** + * 获得用户(待办)的任务: + * 1. 根据 taskId 查询待办任务 + * 2. 如果任务不存在(或者已审核),获取指定流程下,首个需要处理任务 + * + * @param userId 用户编号 + * @param taskId 任务编号 + * @param processInstanceId 流程实例编号 + * @return 待办任务 + */ + BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId); + + /** + * 获得已办的流程任务分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页请求 + * @return 流程任务分页 + */ + PageResult getTaskDonePage(Long userId, BpmTaskPageReqVO pageReqVO); + + /** + * 获得全部的流程任务分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页请求 + * @return 流程任务分页 + */ + PageResult getTaskPage(Long userId, BpmTaskPageReqVO pageReqVO); + + /** + * 获得流程任务 Map + * + * @param processInstanceIds 流程实例的编号数组 + * @return 流程任务 Map + */ + default Map> getTaskMapByProcessInstanceIds(List processInstanceIds) { + return CollectionUtils.convertMultiMap(getTasksByProcessInstanceIds(processInstanceIds), + Task::getProcessInstanceId); + } + + /** + * 获得流程任务列表 + * + * @param processInstanceIds 流程实例的编号数组 + * @return 流程任务列表 + */ + List getTasksByProcessInstanceIds(List processInstanceIds); + + /** + * 获得指定流程实例的流程任务列表,包括所有状态的 + * + * @param processInstanceId 流程实例的编号 + * @param asc 是否升序 + * @return 流程任务列表 + */ + List getTaskListByProcessInstanceId(String processInstanceId, Boolean asc); + + /** + * 校验任务是否存在,并且是否是分配给自己的任务 + * + * @param userId 用户 id + * @param taskId task id + */ + Task validateTask(Long userId, String taskId); + + /** + * 获取任务 + * + * @param id 任务编号 + * @return 任务 + */ + Task getTask(String id); + + /** + * 获取历史任务 + * + * @param id 任务编号 + * @return 历史任务 + */ + HistoricTaskInstance getHistoricTask(String id); + + /** + * 获取历史任务列表 + * + * @param taskIds 任务编号集合 + * @return 历史任务列表 + */ + List getHistoricTasks(Collection taskIds); + + /** + * 根据条件查询正在进行中的任务 + * + * @param processInstanceId 流程实例编号,不允许为空 + * @param assigned 是否分配了审批人,允许空 + * @param taskDefineKey 任务定义 Key,允许空 + */ + List getRunningTaskListByProcessInstanceId(String processInstanceId, + Boolean assigned, + String taskDefineKey); + + /** + * 获取当前任务的可退回的 UserTask 集合 + * + * @param id 当前的任务 ID + * @return 可以退回的节点列表 + */ + List getUserTaskListByReturn(String id); + + /** + * 获取指定任务的子任务列表(多层) + * + * @param parentTaskId 父任务 ID + * @param tasks 任务列表 + * @return 子任务列表 + */ + List getAllChildrenTaskListByParentTaskId(String parentTaskId, List tasks); + + /** + * 获取指定任务的子任务列表 + * + * @param parentTaskId 父任务ID + * @return 子任务列表 + */ + List getTaskListByParentTaskId(String parentTaskId); + + /** + * 获得指定流程实例的活动实例列表 + * + * @param processInstanceId 流程实例的编号 + * @return 活动实例列表 + */ + List getActivityListByProcessInstanceId(String processInstanceId); + + /** + * 获得执行编号对应的活动实例 + * + * @param executionId 执行编号 + * @return 活动实例 + */ + List getHistoricActivityListByExecutionId(String executionId); + + // ========== Update 写入相关方法 ========== + + /** + * 通过任务 + * + * @param userId 用户编号 + * @param reqVO 通过请求 + */ + void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO); + + /** + * 不通过任务 + * + * @param userId 用户编号 + * @param reqVO 不通过请求 + */ + void rejectTask(Long userId, @Valid BpmTaskRejectReqVO reqVO); + + /** + * 将流程任务分配给指定用户 + * + * @param userId 用户编号 + * @param reqVO 分配请求 + */ + void transferTask(Long userId, BpmTaskTransferReqVO reqVO); + + /** + * 将指定流程实例的、进行中的流程任务,移动到结束节点 + * + * @param processInstanceId 流程编号 + * @param reason 原因 + */ + void moveTaskToEnd(String processInstanceId, String reason); + + /** + * 将任务退回到指定的 targetDefinitionKey 位置 + * + * @param userId 用户编号 + * @param reqVO 退回的任务key和当前所在的任务ID + */ + void returnTask(Long userId, BpmTaskReturnReqVO reqVO); + + /** + * 将指定任务委派给其他人处理,等接收人处理后再回到原审批人手中审批 + * + * @param userId 用户编号 + * @param reqVO 被委派人和被委派的任务编号理由参数 + */ + void delegateTask(Long userId, BpmTaskDelegateReqVO reqVO); + + /** + * 任务加签 + * + * @param userId 被加签的用户和任务 ID,加签类型 + * @param reqVO 当前用户 ID + */ + void createSignTask(Long userId, BpmTaskSignCreateReqVO reqVO); + + /** + * 任务减签 + * + * @param userId 当前用户ID + * @param reqVO 被减签的任务 ID,理由 + */ + void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO); + + /** + * 抄送任务 + * + * @param userId 用户编号 + * @param reqVO 通过请求 + */ + void copyTask(Long userId, @Valid BpmTaskCopyReqVO reqVO); + + // ========== Event 事件相关方法 ========== + + /** + * 处理 Task 创建事件,目前是 + *

+ * 1. 更新它的状态为审批中 + * 2. 处理自动通过的情况,例如说:1)无审批人时,是否自动通过、不通过;2)非【人工审核】时,是否自动通过、不通过 + *

+ * 注意:它的触发时机,晚于 {@link #processTaskAssigned(Task)} 之后 + * + * @param task 任务实体 + */ + void processTaskCreated(Task task); + + /** + * 处理 Task 取消事件,目前是更新它的状态为已取消 + * + * @param taskId 任务的编号 + */ + void processTaskCanceled(String taskId); + + /** + * 处理 Task 设置审批人事件,目前是发送审批消息 + * + * @param task 任务实体 + */ + void processTaskAssigned(Task task); + + /** + * 处理 Task 完成事件,目前是发送任务后置通知 + * + * @param task 任务实体 + */ + void processTaskCompleted(Task task); + + /** + * 处理 Task 审批超时事件,可能会处理多个当前审批中的任务 + * + * @param processInstanceId 流程示例编号 + * @param taskDefineKey 任务 Key + * @param handlerType 处理类型,参见 {@link BpmUserTaskTimeoutHandlerTypeEnum} + */ + void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType); + + /** + * 处理 ChildProcess 子流程的审批超时事件 + * + * @param processInstanceId 流程示例编号 + * @param taskDefineKey 任务 Key + */ + void processChildProcessTimeout(String processInstanceId, String taskDefineKey); + + /** + * 触发流程任务 (ReceiveTask) 的执行 + *

+ * 1. Simple 模型 HTTP 回调请求触发器节点的回调,触发流程继续执行 + * 2. Simple 模型延迟器节点,到时触发流程继续执行 + * + * @param processInstanceId 流程示例编号 + * @param taskDefineKey 任务 Key + */ + void triggerTask(String processInstanceId, String taskDefineKey); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java new file mode 100644 index 0000000..1164f4d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -0,0 +1,1520 @@ +package cn.iocoder.yudao.module.bpm.service.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.*; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.object.PageUtils; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; +import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.*; +import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.*; +import org.flowable.engine.HistoryService; +import org.flowable.engine.ManagementService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.runtime.ActivityInstance; +import org.flowable.engine.runtime.Execution; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.DelegationState; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; +import org.flowable.task.api.TaskQuery; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.flowable.task.api.history.HistoricTaskInstanceQuery; +import org.flowable.task.service.impl.persistence.entity.TaskEntity; +import org.flowable.task.service.impl.persistence.entity.TaskEntityImpl; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.util.*; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; + +/** + * 流程任务实例 Service 实现类 + * + * @author 芋道源码 + * @author jason + */ +@Slf4j +@Service +public class BpmTaskServiceImpl implements BpmTaskService { + + @Resource + private TaskService taskService; + @Resource + private HistoryService historyService; + @Resource + private RuntimeService runtimeService; + @Resource + private ManagementService managementService; + + @Resource + private BpmProcessInstanceService processInstanceService; + @Resource + private BpmProcessDefinitionService bpmProcessDefinitionService; + @Resource + private BpmProcessInstanceCopyService processInstanceCopyService; + @Resource + private BpmModelService modelService; + @Resource + private BpmMessageService messageService; + @Resource + private BpmFormService formService; + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + + // ========== Query 查询相关方法 ========== + + @Override + public PageResult getTaskTodoPage(Long userId, BpmTaskPageReqVO pageVO) { + TaskQuery taskQuery = taskService.createTaskQuery() + .taskAssignee(String.valueOf(userId)) // 分配给自己 + .active() + .includeProcessVariables() + .orderByTaskCreateTime().desc(); // 创建时间倒序 + if (StrUtil.isNotBlank(pageVO.getName())) { + taskQuery.taskNameLike("%" + pageVO.getName() + "%"); + } + if (StrUtil.isNotEmpty(pageVO.getCategory())) { + taskQuery.taskCategory(pageVO.getCategory()); + } + if (StrUtil.isNotEmpty(pageVO.getProcessDefinitionKey())) { + taskQuery.processDefinitionKey(pageVO.getProcessDefinitionKey()); + } + if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { + taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); + } + long count = taskQuery.count(); + if (count == 0) { + return PageResult.empty(); + } + List tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + return new PageResult<>(tasks, count); + } + + @Override + public BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId) { + // 1.1 获取指定的用户待办任务 + Task todoTask = getMyTodoTask(userId, taskId); + // 1.2 获取不到,则获取该流程实例下,第一个用户的待办任务 + if (todoTask == null) { + todoTask = getMyFirstTodoTask(userId, processInstanceId); + } + if (todoTask == null) { + return null; + } + + // 2. 查询该任务的子任务 + List childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), CollUtil.newArrayList(todoTask)); + + // 3. 转换返回 + BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId()); + Map buttonsSetting = BpmnModelUtils.parseButtonsSetting( + bpmnModel, todoTask.getTaskDefinitionKey()); + Boolean signEnable = parseSignEnable(bpmnModel, todoTask.getTaskDefinitionKey()); + Boolean reasonRequire = parseReasonRequire(bpmnModel, todoTask.getTaskDefinitionKey()); + Integer nodeType = parseNodeType(BpmnModelUtils.getFlowElementById(bpmnModel, todoTask.getTaskDefinitionKey())); + + // 4. 任务表单 + BpmFormDO taskForm = null; + if (StrUtil.isNotBlank(todoTask.getFormKey())) { + taskForm = formService.getForm(NumberUtils.parseLong(todoTask.getFormKey())); + } + + return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm) + .setNodeType(nodeType).setSignEnable(signEnable).setReasonRequire(reasonRequire); + } + + /** + * 获得用户指定 taskId 任务编号的“待办”(未审批、且可审核)的任务 + * + * @param userId 用户编号 + * @param taskId 任务编号 + * @return 任务 + */ + private Task getMyTodoTask(Long userId, String taskId) { + if (StrUtil.isEmpty(taskId)) { + return null; + } + Task task = getTask(taskId); + if (task == null) { + return null; + } + if (!isAssignUserTask(userId, task) && !isAddSignUserTask(userId, task)) { + return null; + } + return task; + } + + /** + * 获得用户指定 processInstanceId 流程编号下的首个“待办”(未审批、且可审核)的任务 + * + * @param userId 用户编号 + * @param processInstanceId 流程编号 + * @return 任务 + */ + private Task getMyFirstTodoTask(Long userId, String processInstanceId) { + if (processInstanceId == null) { + return null; + } + // 1. 查询所有任务 + List tasks = taskService.createTaskQuery() + .active() + .processInstanceId(processInstanceId) + .includeTaskLocalVariables() + .includeProcessVariables() + .orderByTaskCreateTime().asc() // 按创建时间升序 + .list(); + + // 2. 查询我的首个任务 + return CollUtil.findOne(tasks, task -> { + return isAssignUserTask(userId, task) // 当前用户为审批人 + || isAddSignUserTask(userId, task); // 当前用户为加签人(为了减签) + }); + } + + @Override + public PageResult getTaskDonePage(Long userId, BpmTaskPageReqVO pageVO) { + HistoricTaskInstanceQuery taskQuery = historyService.createHistoricTaskInstanceQuery() + .finished() // 已完成 + .taskAssignee(String.valueOf(userId)) // 分配给自己 + .includeTaskLocalVariables() + .orderByHistoricTaskInstanceEndTime().desc(); // 审批时间倒序 + if (StrUtil.isNotBlank(pageVO.getName())) { + taskQuery.taskNameLike("%" + pageVO.getName() + "%"); + } + if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { + taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); + } + // 执行查询 + long count = taskQuery.count(); + if (count == 0) { + return PageResult.empty(); + } + List tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + + // 特殊:强制移除自动完成的“发起人”节点 + // 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤 + tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID)); + return new PageResult<>(tasks, count); + } + + @Override + public PageResult getTaskPage(Long userId, BpmTaskPageReqVO pageVO) { + HistoricTaskInstanceQuery taskQuery = historyService.createHistoricTaskInstanceQuery() + .includeTaskLocalVariables() + .taskTenantId(FlowableUtils.getTenantId()) + .orderByHistoricTaskInstanceEndTime().desc(); // 审批时间倒序 + if (StrUtil.isNotBlank(pageVO.getName())) { + taskQuery.taskNameLike("%" + pageVO.getName() + "%"); + } + if (StrUtil.isNotEmpty(pageVO.getCategory())) { + taskQuery.taskCategory(pageVO.getCategory()); + } + if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { + taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); + } + // 执行查询 + long count = taskQuery.count(); + if (count == 0) { + return PageResult.empty(); + } + List tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + return new PageResult<>(tasks, count); + } + + @Override + public List getTasksByProcessInstanceIds(List processInstanceIds) { + if (CollUtil.isEmpty(processInstanceIds)) { + return Collections.emptyList(); + } + return taskService.createTaskQuery().processInstanceIdIn(processInstanceIds).list(); + } + + @Override + public List getTaskListByProcessInstanceId(String processInstanceId, Boolean asc) { + HistoricTaskInstanceQuery query = historyService.createHistoricTaskInstanceQuery() + .includeTaskLocalVariables() + .processInstanceId(processInstanceId); + if (Boolean.TRUE.equals(asc)) { + query.orderByHistoricTaskInstanceStartTime().asc(); + } else { + query.orderByHistoricTaskInstanceStartTime().desc(); + } + return query.list(); + } + + @Override + public Task validateTask(Long userId, String taskId) { + Task task = validateTaskExist(taskId); + // 为什么判断 assignee 非空的情况下? + // 例如说:在审批人为空时,我们会有“自动审批通过”的策略,此时 userId 为 null,允许通过 + if (StrUtil.isNotBlank(task.getAssignee()) + && ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) { + throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + } + return task; + } + + private Task validateTaskExist(String id) { + Task task = getTask(id); + if (task == null) { + throw exception(TASK_NOT_EXISTS); + } + return task; + } + + @Override + public Task getTask(String id) { + return taskService.createTaskQuery().taskId(id).includeTaskLocalVariables().singleResult(); + } + + @Override + public HistoricTaskInstance getHistoricTask(String id) { + return historyService.createHistoricTaskInstanceQuery().taskId(id).includeTaskLocalVariables().singleResult(); + } + + @Override + public List getHistoricTasks(Collection taskIds) { + return historyService.createHistoricTaskInstanceQuery().taskIds(taskIds).includeTaskLocalVariables().list(); + } + + @Override + public List getRunningTaskListByProcessInstanceId(String processInstanceId, Boolean assigned, String defineKey) { + Assert.notNull(processInstanceId, "processInstanceId 不能为空"); + TaskQuery taskQuery = taskService.createTaskQuery().processInstanceId(processInstanceId).active() + .includeTaskLocalVariables(); + if (BooleanUtil.isTrue(assigned)) { + taskQuery.taskAssigned(); + } else if (BooleanUtil.isFalse(assigned)) { + taskQuery.taskUnassigned(); + } + if (StrUtil.isNotEmpty(defineKey)) { + taskQuery.taskDefinitionKey(defineKey); + } + return taskQuery.list(); + } + + @Override + public List getUserTaskListByReturn(String id) { + // 1.1 校验当前任务 task 存在 + Task task = validateTaskExist(id); + // 1.2 根据流程定义获取流程模型信息 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId()); + FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); + if (source == null) { + throw exception(TASK_NOT_EXISTS); + } + + // 2.1 查询该任务的前置任务节点的 key 集合 + List previousUserList = BpmnModelUtils.getPreviousUserTaskList(source, null, null); + if (CollUtil.isEmpty(previousUserList)) { + return Collections.emptyList(); + } + // 2.2 过滤:只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回 + previousUserList.removeIf(userTask -> !BpmnModelUtils.isSequentialReachable(source, userTask, null)); + return previousUserList; + } + + @Override + public List getAllChildrenTaskListByParentTaskId(String parentTaskId, List tasks) { + if (CollUtil.isEmpty(tasks)) { + return Collections.emptyList(); + } + Map> parentTaskMap = convertMultiMap( + filterList(tasks, task -> StrUtil.isNotEmpty(task.getParentTaskId())), TaskInfo::getParentTaskId); + if (CollUtil.isEmpty(parentTaskMap)) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + // 1. 递归获取子级 + Stack stack = new Stack<>(); + stack.push(parentTaskId); + // 2. 递归遍历 + for (int i = 0; i < Short.MAX_VALUE; i++) { + if (stack.isEmpty()) { + break; + } + // 2.1 获取子任务们 + String taskId = stack.pop(); + List childTaskList = filterList(tasks, task -> StrUtil.equals(task.getParentTaskId(), taskId)); + // 2.2 如果非空,则添加到 stack 进一步递归 + if (CollUtil.isNotEmpty(childTaskList)) { + stack.addAll(convertList(childTaskList, TaskInfo::getId)); + result.addAll(childTaskList); + } + } + return result; + } + + /** + * 获得所有子任务列表 + * + * @param parentTask 父任务 + * @return 所有子任务列表 + */ + private List getAllChildTaskList(Task parentTask) { + List result = new ArrayList<>(); + // 1. 递归获取子级 + Stack stack = new Stack<>(); + stack.push(parentTask); + // 2. 递归遍历 + for (int i = 0; i < Short.MAX_VALUE; i++) { + if (stack.isEmpty()) { + break; + } + // 2.1 获取子任务们 + Task task = stack.pop(); + List childTaskList = getTaskListByParentTaskId(task.getId()); + // 2.2 如果非空,则添加到 stack 进一步递归 + if (CollUtil.isNotEmpty(childTaskList)) { + stack.addAll(childTaskList); + result.addAll(childTaskList); + } + } + return result; + } + + @Override + public List getTaskListByParentTaskId(String parentTaskId) { + String tableName = managementService.getTableName(TaskEntity.class); + // taskService.createTaskQuery() 没有 parentId 参数,所以写 sql 查询 + String sql = "select ID_,NAME_,OWNER_,ASSIGNEE_ from " + tableName + " where PARENT_TASK_ID_=#{parentTaskId}"; + return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).list(); + } + + /** + * 获取子任务个数 + * + * @param parentTaskId 父任务 ID + * @return 剩余子任务个数 + */ + private Long getTaskCountByParentTaskId(String parentTaskId) { + String tableName = managementService.getTableName(TaskEntity.class); + String sql = "SELECT COUNT(1) from " + tableName + " WHERE PARENT_TASK_ID_=#{parentTaskId}"; + return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).count(); + } + + /** + * 获得任务根任务的父任务编号 + * + * @param task 任务 + * @return 根任务的父任务编号 + */ + private String getTaskRootParentId(Task task) { + if (task == null || task.getParentTaskId() == null) { + return null; + } + for (int i = 0; i < Short.MAX_VALUE; i++) { + Task parentTask = getTask(task.getParentTaskId()); + if (parentTask == null) { + return null; + } + if (parentTask.getParentTaskId() == null) { + return parentTask.getId(); + } + task = parentTask; + } + throw new IllegalArgumentException(String.format("Task(%s) 层级过深,无法获取父节点编号", task.getId())); + } + + @Override + public List getActivityListByProcessInstanceId(String processInstanceId) { + return historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId) + .orderByHistoricActivityInstanceStartTime().asc().list(); + } + + @Override + public List getHistoricActivityListByExecutionId(String executionId) { + return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list(); + } + + /** + * 判断指定用户,是否是当前任务的审批人 + * + * @param userId 用户编号 + * @param task 任务 + * @return 是否 + */ + private boolean isAssignUserTask(Long userId, Task task) { + Long assignee = NumberUtil.parseLong(task.getAssignee(), null); + return ObjectUtil.equals(userId, assignee); + } + + /** + * 判断指定用户,是否是当前任务的拥有人 + * + * @param userId 用户编号 + * @param task 任务 + * @return 是否 + */ + private boolean isOwnerUserTask(Long userId, Task task) { + Long assignee = NumberUtil.parseLong(task.getOwner(), null); + return ObjectUtil.equal(userId, assignee); + } + + /** + * 判断指定用户,是否是当前任务的加签人 + * + * @param userId 用户 Id + * @param task 任务 + * @return 是否 + */ + private boolean isAddSignUserTask(Long userId, Task task) { + return (isAssignUserTask(userId, task) || isOwnerUserTask(userId, task)) + && BpmTaskSignTypeEnum.of(task.getScopeType()) != null; + } + + // ========== Update 写入相关方法 ========== + + @Override + @Transactional(rollbackFor = Exception.class) + public void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO) { + // 1.1 校验任务存在 + Task task = validateTask(userId, reqVO.getId()); + // 1.2 校验流程实例存在 + ProcessInstance instance = processInstanceService.getProcessInstance(task.getProcessInstanceId()); + if (instance == null) { + throw exception(PROCESS_INSTANCE_NOT_EXISTS); + } + // 1.3 校验签名 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId()); + Boolean signEnable = parseSignEnable(bpmnModel, task.getTaskDefinitionKey()); + if (signEnable && StrUtil.isEmpty(reqVO.getSignPicUrl())) { + throw exception(TASK_SIGNATURE_NOT_EXISTS); + } + // 1.4 校验审批意见 + Boolean reasonRequire = parseReasonRequire(bpmnModel, task.getTaskDefinitionKey()); + if (reasonRequire && StrUtil.isEmpty(reqVO.getReason())) { + throw exception(TASK_REASON_REQUIRE); + } + + // 情况一:被委派的任务,不调用 complete 去完成任务 + if (DelegationState.PENDING.equals(task.getDelegationState())) { + approveDelegateTask(reqVO, task); + return; + } + + // 情况二:审批有【后】加签的任务 + if (BpmTaskSignTypeEnum.AFTER.getType().equals(task.getScopeType())) { + approveAfterSignTask(task, reqVO); + return; + } + + // 情况三:审批普通的任务。大多数情况下,都是这样 + // 2.1 更新 task 状态、原因、签字 + updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), reqVO.getReason()); + if (signEnable) { + taskService.setVariableLocal(task.getId(), BpmnVariableConstants.TASK_SIGN_PIC_URL, reqVO.getSignPicUrl()); + } + // 2.2 添加评论 + taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(), + BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason())); + + // 3. 设置流程变量。如果流程变量前端传空,需要从历史实例中获取,原因:前端表单如果在当前节点无可编辑的字段时 variables 一定会为空 + // 场景一:A 节点发起,B 节点表单无可编辑字段,审批通过时,C 节点需要流程变量获取下一个执行节点,但因为 B 节点无可编辑的字段,variables 为空,流程可能出现问题。 + // 场景二:A 节点发起,B 节点只有某一个字段可编辑(比如 day),但 C 节点需要多个节点。 + // (比如 work + day 变量,在发起时填写,因为 B 节点只有 day 的编辑权限,在审批后,variables 会缺少 work 的值) + Map processVariables = new HashMap<>(); + if (CollUtil.isNotEmpty(instance.getProcessVariables())) { // 获取历史中流程变量 + processVariables.putAll(instance.getProcessVariables()); + } + if (CollUtil.isNotEmpty(reqVO.getVariables())) { // 合并前端传递的流程变量,以前端为准 + processVariables.putAll(reqVO.getVariables()); + } + + // 4. 校验并处理 APPROVE_USER_SELECT 当前审批人,选择下一节点审批人的逻辑 + Map variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), processVariables, + bpmnModel, reqVO.getNextAssignees(), instance); + runtimeService.setVariables(task.getProcessInstanceId(), variables); + + // 5. 调用 BPM complete 去完成任务 + taskService.complete(task.getId(), variables, true); + + // 【加签专属】处理加签任务 + handleParentTaskIfSign(task.getParentTaskId()); + } + + /** + * 校验选择的下一个节点的审批人,是否合法 + * + * 1. 是否有漏选:没有选择审批人 + * 2. 是否有多选:非下一个节点 + * + * @param taskDefinitionKey 当前任务节点标识 + * @param variables 流程变量 + * @param bpmnModel 流程模型 + * @param nextAssignees 下一个节点审批人集合(参数) + * @param processInstance 流程实例 + */ + @SuppressWarnings("unchecked") + private Map validateAndSetNextAssignees(String taskDefinitionKey, Map variables, BpmnModel bpmnModel, + Map> nextAssignees, ProcessInstance processInstance) { + // simple 设计器第一个节点默认为发起人节点,不校验是否存在审批人 + if (Objects.equals(taskDefinitionKey, START_USER_NODE_ID)) { + return variables; + } + // 1. 获取下一个将要执行的节点集合 + FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey); + List nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables); + + // 2. 校验选择的下一个节点的审批人,是否合法 + for (FlowNode nextFlowNode : nextFlowNodes) { + Integer candidateStrategy = parseCandidateStrategy(nextFlowNode); + // 2.1 情况一:如果节点中的审批人策略为 发起人自选 + if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())) { + // 特殊:如果当前节点已经存在审批人,则不允许覆盖 + Map> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables()); + if (startUserSelectAssignees != null && CollUtil.isNotEmpty(startUserSelectAssignees.get(nextFlowNode.getId()))) { + continue; + } + // 如果节点存在,但未配置审批人 + List assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null; + if (CollUtil.isEmpty(assignees)) { + throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName()); + } + + // 设置 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES + if (startUserSelectAssignees == null) { + startUserSelectAssignees = new HashMap<>(); + } + startUserSelectAssignees.put(nextFlowNode.getId(), assignees); + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees); + continue; + } + + // 2.2 情况二:如果节点中的审批人策略为 审批人,在审批时选择下一个节点的审批人,并且该节点的审批人为空 + if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) { + // 如果节点存在,但未配置审批人 + Map> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processInstance.getProcessVariables()); + List assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null; + if (CollUtil.isEmpty(assignees)) { + throw exception(PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName()); + } + + // 设置 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES + if (approveUserSelectAssignees == null) { + approveUserSelectAssignees = new HashMap<>(); + } + approveUserSelectAssignees.put(nextFlowNode.getId(), assignees); + Map> existingApproveUserSelectAssignees = (Map>) variables.get( + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES); + if (CollUtil.isNotEmpty(existingApproveUserSelectAssignees)) { + approveUserSelectAssignees.putAll(existingApproveUserSelectAssignees); + } + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES, approveUserSelectAssignees); + } + } + return variables; + } + + /** + * 审批通过存在“后加签”的任务。 + *

+ * 注意:该任务不能马上完成,需要一个中间状态(APPROVING),并激活剩余所有子任务(PROCESS)为可审批处理 + * 如果马上完成,则会触发下一个任务,甚至如果没有下一个任务则流程实例就直接结束了! + * + * @param task 当前任务 + * @param reqVO 前端请求参数 + */ + private void approveAfterSignTask(Task task, BpmTaskApproveReqVO reqVO) { + // 更新父 task 状态 + 原因 + updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.APPROVING.getStatus(), reqVO.getReason()); + + // 2. 激活子任务 + List childrenTaskList = getTaskListByParentTaskId(task.getId()); + for (Task childrenTask : childrenTaskList) { + taskService.resolveTask(childrenTask.getId()); + // 更新子 task 状态 + updateTaskStatus(childrenTask.getId(), BpmTaskStatusEnum.RUNNING.getStatus()); + } + } + + /** + * 如果父任务是有前后【加签】的任务,如果它【加签】出来的子任务都被处理,需要处理父任务: + *

+ * 1. 如果是【向前】加签,则需要重新激活父任务,让它可以被审批 + * 2. 如果是【向后】加签,则需要完成父任务,让它完成审批 + * + * @param parentTaskId 父任务编号 + */ + private void handleParentTaskIfSign(String parentTaskId) { + if (StrUtil.isBlank(parentTaskId)) { + return; + } + // 1.1 判断是否还有子任务。如果没有,就不处理 + Long childrenTaskCount = getTaskCountByParentTaskId(parentTaskId); + if (childrenTaskCount > 0) { + return; + } + // 1.2 只处理加签的父任务 + Task parentTask = validateTaskExist(parentTaskId); + String scopeType = parentTask.getScopeType(); + if (BpmTaskSignTypeEnum.of(scopeType) == null) { + return; + } + + // 2. 子任务已处理完成,清空 scopeType 字段,修改 parentTask 信息,方便后续可以继续向前后向后加签 + TaskEntityImpl parentTaskImpl = (TaskEntityImpl) parentTask; + parentTaskImpl.setScopeType(null); + taskService.saveTask(parentTaskImpl); + + // 3.1 情况一:处理向【向前】加签 + if (BpmTaskSignTypeEnum.BEFORE.getType().equals(scopeType)) { + // 3.1.1 owner 重新赋值给父任务的 assignee,这样它就可以被审批 + taskService.resolveTask(parentTaskId); + // 3.1.2 更新流程任务 status + updateTaskStatus(parentTaskId, BpmTaskStatusEnum.RUNNING.getStatus()); + // 3.2 情况二:处理向【向后】加签 + } else if (BpmTaskSignTypeEnum.AFTER.getType().equals(scopeType)) { + // 只有 parentTask 处于 APPROVING 的情况下,才可以继续 complete 完成 + // 否则,一个未审批的 parentTask 任务,在加签出来的任务都被减签的情况下,就直接完成审批,这样会存在问题 + Integer status = (Integer) parentTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS); + if (ObjectUtil.notEqual(status, BpmTaskStatusEnum.APPROVING.getStatus())) { + return; + } + // 3.2.2 完成自己(因为它已经没有子任务,所以也可以完成) + updateTaskStatus(parentTaskId, BpmTaskStatusEnum.APPROVE.getStatus()); + taskService.complete(parentTaskId); + } + + // 4. 递归处理父任务 + handleParentTaskIfSign(parentTask.getParentTaskId()); + } + + /** + * 审批被委派的任务 + * + * @param reqVO 前端请求参数,包含当前任务ID,审批意见等 + * @param task 当前被审批的任务 + */ + private void approveDelegateTask(BpmTaskApproveReqVO reqVO, Task task) { + // 1. 添加审批意见 + AdminUserRespDTO currentUser = adminUserApi.getUser(WebFrameworkUtils.getLoginUserId()); + AdminUserRespDTO ownerUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner())); // 发起委托的用户 + Assert.notNull(ownerUser, "委派任务找不到原审批人,需要检查数据"); + taskService.addComment(reqVO.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_END.getType(), + BpmCommentTypeEnum.DELEGATE_END.formatComment(currentUser.getNickname(), ownerUser.getNickname(), reqVO.getReason())); + + // 2.1 调用 resolveTask 完成任务。 + // 底层调用 TaskHelper.changeTaskAssignee(task, task.getOwner()):将 owner 设置为 assignee + taskService.resolveTask(task.getId()); + // 2.2 更新 task 状态 + 原因 + updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.RUNNING.getStatus(), reqVO.getReason()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void rejectTask(Long userId, @Valid BpmTaskRejectReqVO reqVO) { + // 1.1 校验任务存在 + Task task = validateTask(userId, reqVO.getId()); + // 1.2 校验流程实例存在 + ProcessInstance instance = processInstanceService.getProcessInstance(task.getProcessInstanceId()); + if (instance == null) { + throw exception(PROCESS_INSTANCE_NOT_EXISTS); + } + + // 2.1 更新流程任务为不通过 + updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.REJECT.getStatus(), reqVO.getReason()); + // 2.2 添加流程评论 + taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(), + BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason())); + // 2.3 如果当前任务时被加签的,则加它的根任务也标记成未通过 + // 疑问:为什么要标记未通过呢? + // 回答:例如说 A 任务被向前加签除 B 任务时,B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示“已取消”的任务,导致展示不出审批不通过的细节。 + if (task.getParentTaskId() != null) { + String rootParentId = getTaskRootParentId(task); + updateTaskStatusAndReason(rootParentId, BpmTaskStatusEnum.REJECT.getStatus(), + BpmCommentTypeEnum.REJECT.formatComment("加签任务不通过")); + taskService.addComment(rootParentId, task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(), + BpmCommentTypeEnum.REJECT.formatComment("加签任务不通过")); + } + + // 3. 根据不同的 RejectHandler 处理策略 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId()); + FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); + // 3.1 情况一:驳回到指定的任务节点 + BpmUserTaskRejectHandlerTypeEnum userTaskRejectHandlerType = BpmnModelUtils.parseRejectHandlerType(userTaskElement); + if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerTypeEnum.RETURN_USER_TASK) { + String returnTaskId = BpmnModelUtils.parseReturnTaskId(userTaskElement); + Assert.notNull(returnTaskId, "退回的节点不能为空"); + returnTask(userId, new BpmTaskReturnReqVO().setId(task.getId()) + .setTargetTaskDefinitionKey(returnTaskId).setReason(reqVO.getReason())); + return; + } + // 3.2 情况二:直接结束,审批不通过 + processInstanceService.updateProcessInstanceReject(instance, reqVO.getReason()); // 标记不通过 + moveTaskToEnd(task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason())); // 结束流程 + } + + /** + * 更新流程任务的 status 状态 + * + * @param id 任务编号 + * @param status 状态 + */ + private void updateTaskStatus(String id, Integer status) { + taskService.setVariableLocal(id, BpmnVariableConstants.TASK_VARIABLE_STATUS, status); + } + + /** + * 更新流程任务的 status 状态、reason 理由 + * + * @param id 任务编号 + * @param status 状态 + * @param reason 理由(审批通过、审批不通过的理由) + */ + private void updateTaskStatusAndReason(String id, Integer status, String reason) { + updateTaskStatus(id, status); + taskService.setVariableLocal(id, BpmnVariableConstants.TASK_VARIABLE_REASON, reason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void returnTask(Long userId, BpmTaskReturnReqVO reqVO) { + // 1.1 当前任务 task + Task task = validateTask(userId, reqVO.getId()); + if (task.isSuspended()) { + throw exception(TASK_IS_PENDING); + } + // 1.2 校验源头和目标节点的关系,并返回目标元素 + FlowElement targetElement = validateTargetTaskCanReturn(task.getTaskDefinitionKey(), + reqVO.getTargetTaskDefinitionKey(), task.getProcessDefinitionId()); + + // 2. 调用 Flowable 框架的退回逻辑 + returnTask(userId, task, targetElement, reqVO); + } + + /** + * 退回流程节点时,校验目标任务节点是否可退回 + * + * @param sourceKey 当前任务节点 Key + * @param targetKey 目标任务节点 key + * @param processDefinitionId 当前流程定义 ID + * @return 目标任务节点元素 + */ + private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) { + // 1.1 获取流程模型信息 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId); + // 1.3 获取当前任务节点元素 + FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey); + // 1.3 获取跳转的节点元素 + FlowElement target = BpmnModelUtils.getFlowElementById(bpmnModel, targetKey); + if (target == null) { + throw exception(TASK_TARGET_NODE_NOT_EXISTS); + } + + // 2.2 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回 + if (!BpmnModelUtils.isSequentialReachable(source, target, null)) { + throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR); + } + return target; + } + + /** + * 执行退回逻辑 + * + * @param userId 用户编号 + * @param currentTask 当前退回的任务 + * @param targetElement 需要退回到的目标任务 + * @param reqVO 前端参数封装 + */ + public void returnTask(Long userId, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) { + // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveActivityIdsToSingleActivityId 回撤 + // 1.1 获取所有正常进行的任务节点 Key + List taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list(); + List runTaskKeyList = convertList(taskList, Task::getTaskDefinitionKey); + // 1.2 通过 targetElement 的出口连线,计算在 runTaskKeyList 有哪些 key 需要被撤回 + // 为什么不直接使用 runTaskKeyList 呢?因为可能存在多个审批分支,例如说:A -> B -> C 和 D -> F,而只要 C 撤回到 A,需要排除掉 F + List returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null); + List returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId); + + List runExecutionIds = new ArrayList<>(); + // 2. 给当前要被退回的 task 数组,设置退回意见 + taskList.forEach(task -> { + // 需要排除掉,不需要设置退回意见的任务 + if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) { + return; + } + runExecutionIds.add(task.getExecutionId()); + + // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务 + if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记 + // 2.1.1 添加评论 + taskService.addComment(task.getId(), currentTask.getProcessInstanceId(), BpmCommentTypeEnum.RETURN.getType(), + BpmCommentTypeEnum.RETURN.formatComment(reqVO.getReason())); + // 2.1.2 更新 task 状态 + 原因 + updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.RETURN.getStatus(), reqVO.getReason()); + } else { // 情况二:别人的任务,进行 CANCEL 标记 + processTaskCanceled(task.getId()); + } + }); + + // 3. 设置流程变量节点驳回标记:用于驳回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略。导致自动通过 + runtimeService.setVariable(currentTask.getProcessInstanceId(), + String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE); + // 4. 执行驳回 + // 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId 原因: + // 当多实例任务回退的时候有问题。相关 issue: https://github.com/flowable/flowable-engine/issues/3944 + runtimeService.createChangeActivityStateBuilder() + .processInstanceId(currentTask.getProcessInstanceId()) + .moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey()) + .changeState(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delegateTask(Long userId, BpmTaskDelegateReqVO reqVO) { + String taskId = reqVO.getId(); + // 1.1 校验任务 + Task task = validateTask(userId, reqVO.getId()); + if (task.getAssignee().equals(reqVO.getDelegateUserId().toString())) { // 校验当前审批人和被委派人不是同一人 + throw exception(TASK_DELEGATE_FAIL_USER_REPEAT); + } + // 1.2 校验目标用户存在 + AdminUserRespDTO delegateUser = adminUserApi.getUser(reqVO.getDelegateUserId()); + if (delegateUser == null) { + throw exception(TASK_DELEGATE_FAIL_USER_NOT_EXISTS); + } + + // 2. 添加委托意见 + AdminUserRespDTO currentUser = adminUserApi.getUser(userId); + taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_START.getType(), + BpmCommentTypeEnum.DELEGATE_START.formatComment(currentUser.getNickname(), delegateUser.getNickname(), reqVO.getReason())); + + // 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee) + taskService.setOwner(taskId, task.getAssignee()); + // 3.2 执行委派,将任务委派给 delegateUser + taskService.delegateTask(taskId, reqVO.getDelegateUserId().toString()); + // 补充说明:委托不单独设置状态。如果需要,可通过 Task 的 DelegationState 字段,判断是否为 DelegationState.PENDING 委托中 + } + + @Override + public void transferTask(Long userId, BpmTaskTransferReqVO reqVO) { + String taskId = reqVO.getId(); + // 1.1 校验任务 + Task task = validateTask(userId, reqVO.getId()); + if (task.getAssignee().equals(reqVO.getAssigneeUserId().toString())) { // 校验当前审批人和被转派人不是同一人 + throw exception(TASK_TRANSFER_FAIL_USER_REPEAT); + } + // 1.2 校验目标用户存在 + AdminUserRespDTO assigneeUser = adminUserApi.getUser(reqVO.getAssigneeUserId()); + if (assigneeUser == null) { + throw exception(TASK_TRANSFER_FAIL_USER_NOT_EXISTS); + } + + // 2. 添加委托意见 + AdminUserRespDTO currentUser = adminUserApi.getUser(userId); + taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.TRANSFER.getType(), + BpmCommentTypeEnum.TRANSFER.formatComment(currentUser.getNickname(), assigneeUser.getNickname(), reqVO.getReason())); + + // 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee) + taskService.setOwner(taskId, task.getAssignee()); + // 3.2 执行转派(审批人),将任务转派给 assigneeUser + // 委托( delegate)和转派(transfer)的差别,就在这块的调用!!!! + taskService.setAssignee(taskId, reqVO.getAssigneeUserId().toString()); + } + + @Override + public void moveTaskToEnd(String processInstanceId, String reason) { + List taskList = getRunningTaskListByProcessInstanceId(processInstanceId, null, null); + if (CollUtil.isEmpty(taskList)) { + return; + } + + // 1. 其它未结束的任务,直接取消 + // 疑问:为什么不通过 updateTaskStatusWhenCanceled 监听取消,而是直接提前调用呢? + // 回答:详细见 updateTaskStatusWhenCanceled 的方法,加签的场景 + taskList.forEach(task -> { + Integer otherTaskStatus = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS); + if (BpmTaskStatusEnum.isEndStatus(otherTaskStatus)) { + return; + } + processTaskCanceled(task.getId()); + }); + + // 2. 终止流程 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskList.get(0).getProcessDefinitionId()); + List activityIds = CollUtil.newArrayList(convertSet(taskList, Task::getTaskDefinitionKey)); + EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel); + Assert.notNull(endEvent, "结束节点不能为空"); + runtimeService.createChangeActivityStateBuilder() + .processInstanceId(processInstanceId) + .moveActivityIdsToSingleActivityId(activityIds, endEvent.getId()) + .changeState(); + + // 3. 特殊:如果跳转到 EndEvent 流程还未结束, 执行 deleteProcessInstance 方法 + // TODO 芋艿:目前发现并行分支情况下,会存在这个情况,后续看看有没更好的方案; + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstanceId).list(); + if (CollUtil.isNotEmpty(executions)) { + log.warn("[moveTaskToEnd][执行跳转到 EndEvent 后, 流程实例未结束,强制执行 deleteProcessInstance 方法]"); + runtimeService.deleteProcessInstance(processInstanceId, reason); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void createSignTask(Long userId, BpmTaskSignCreateReqVO reqVO) { + // 1. 获取和校验任务 + TaskEntityImpl taskEntity = validateTaskCanCreateSign(userId, reqVO); + List userList = adminUserApi.getUserList(reqVO.getUserIds()); + if (CollUtil.isEmpty(userList)) { + throw exception(TASK_SIGN_CREATE_USER_NOT_EXIST); + } + + // 2. 处理当前任务 + // 2.1 开启计数功能,主要用于为了让表 ACT_RU_TASK 中的 SUB_TASK_COUNT_ 字段记录下总共有多少子任务,后续可能有用 + taskEntity.setCountEnabled(true); + // 2.2 向前加签,设置 owner,置空 assign。等子任务都完成后,再调用 resolveTask 重新将 owner 设置为 assign + // 原因是:不能和向前加签的子任务一起审批,需要等前面的子任务都完成才能审批 + if (reqVO.getType().equals(BpmTaskSignTypeEnum.BEFORE.getType())) { + taskEntity.setOwner(taskEntity.getAssignee()); + taskEntity.setAssignee(null); + } + // 2.4 记录加签方式,完成任务时需要用到判断 + taskEntity.setScopeType(reqVO.getType()); + // 2.5 保存当前任务修改后的值 + taskService.saveTask(taskEntity); + // 2.6 更新 task 状态为 WAIT,只有在向前加签的时候 + if (reqVO.getType().equals(BpmTaskSignTypeEnum.BEFORE.getType())) { + updateTaskStatus(taskEntity.getId(), BpmTaskStatusEnum.WAIT.getStatus()); + } + + // 3. 创建加签任务 + createSignTaskList(convertList(reqVO.getUserIds(), String::valueOf), taskEntity); + + // 4. 记录加签的评论到 task 任务 + AdminUserRespDTO currentUser = adminUserApi.getUser(userId); + String comment = StrUtil.format(BpmCommentTypeEnum.ADD_SIGN.getComment(), + currentUser.getNickname(), BpmTaskSignTypeEnum.nameOfType(reqVO.getType()), + String.join(",", convertList(userList, AdminUserRespDTO::getNickname)), reqVO.getReason()); + taskService.addComment(reqVO.getId(), taskEntity.getProcessInstanceId(), BpmCommentTypeEnum.ADD_SIGN.getType(), comment); + } + + /** + * 校验任务是否可以加签,主要校验加签类型是否一致: + *

+ * 1. 如果存在“向前加签”的任务,则不能“向后加签” + * 2. 如果存在“向后加签”的任务,则不能“向前加签” + * + * @param userId 当前用户 ID + * @param reqVO 请求参数,包含任务 ID 和加签类型 + * @return 当前任务 + */ + private TaskEntityImpl validateTaskCanCreateSign(Long userId, BpmTaskSignCreateReqVO reqVO) { + TaskEntityImpl taskEntity = (TaskEntityImpl) validateTask(userId, reqVO.getId()); + // 向前加签和向后加签不能同时存在 + if (taskEntity.getScopeType() != null + && ObjectUtil.notEqual(taskEntity.getScopeType(), reqVO.getType())) { + throw exception(TASK_SIGN_CREATE_TYPE_ERROR, + BpmTaskSignTypeEnum.nameOfType(taskEntity.getScopeType()), BpmTaskSignTypeEnum.nameOfType(reqVO.getType())); + } + + // 同一个 key 的任务,审批人不重复 + List taskList = taskService.createTaskQuery().processInstanceId(taskEntity.getProcessInstanceId()) + .taskDefinitionKey(taskEntity.getTaskDefinitionKey()).list(); + List currentAssigneeList = convertListByFlatMap(taskList, task -> // 需要考虑 owner 的情况,因为向后加签时,它暂时没 assignee 而是 owner + Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner()))); + if (CollUtil.containsAny(currentAssigneeList, reqVO.getUserIds())) { + List userList = adminUserApi.getUserList(CollUtil.intersection(currentAssigneeList, reqVO.getUserIds())); + throw exception(TASK_SIGN_CREATE_USER_REPEAT, String.join(",", convertList(userList, AdminUserRespDTO::getNickname))); + } + return taskEntity; + } + + /** + * 创建加签子任务 + * + * @param userIds 被加签的用户 ID + * @param taskEntity 被加签的任务 + */ + private void createSignTaskList(List userIds, TaskEntityImpl taskEntity) { + if (CollUtil.isEmpty(userIds)) { + return; + } + // 创建加签人的新任务,全部基于 taskEntity 为父任务来创建 + for (String addSignId : userIds) { + if (StrUtil.isBlank(addSignId)) { + continue; + } + createSignTask(taskEntity, addSignId); + } + } + + /** + * 创建加签子任务 + * + * @param parentTask 父任务 + * @param assignee 子任务的执行人 + */ + private void createSignTask(TaskEntityImpl parentTask, String assignee) { + // 1. 生成子任务 + TaskEntityImpl task = (TaskEntityImpl) taskService.newTask(IdUtil.fastSimpleUUID()); + BpmTaskConvert.INSTANCE.copyTo(parentTask, task); + + // 2.1 向前加签,设置审批人 + if (BpmTaskSignTypeEnum.BEFORE.getType().equals(parentTask.getScopeType())) { + task.setAssignee(assignee); + // 2.2 向后加签,设置 owner 不设置 assignee 是因为不能同时审批,需要等父任务完成 + } else { + task.setOwner(assignee); + } + // 2.3 保存子任务 + taskService.saveTask(task); + + // 3. 向后前签,设置子任务的状态为 WAIT,因为需要等父任务审批完 + if (BpmTaskSignTypeEnum.AFTER.getType().equals(parentTask.getScopeType())) { + updateTaskStatus(task.getId(), BpmTaskStatusEnum.WAIT.getStatus()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + @SuppressWarnings("DataFlowIssue") + public void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO) { + // 1.1 校验 task 可以被减签 + Task task = validateTaskCanSignDelete(reqVO.getId()); + // 1.2 校验取消人存在 + AdminUserRespDTO cancelUser = null; + if (StrUtil.isNotBlank(task.getAssignee())) { + cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getAssignee())); + } + if (cancelUser == null && StrUtil.isNotBlank(task.getOwner())) { + cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner())); + } + Assert.notNull(cancelUser, "任务中没有所有者和审批人,数据错误"); + + // 2.1 获得子任务列表,包括子任务的子任务 + List childTaskList = getAllChildTaskList(task); + childTaskList.add(task); + // 2.2 更新子任务为已取消 + String cancelReason = StrUtil.format("任务被取消,原因:由于[{}]操作[减签],", cancelUser.getNickname()); + childTaskList.forEach(childTask -> updateTaskStatusAndReason(childTask.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), cancelReason)); + // 2.3 删除任务和所有子任务 + taskService.deleteTasks(convertList(childTaskList, Task::getId)); + + // 3. 记录日志到父任务中。先记录日志是因为,通过 handleParentTask 方法之后,任务可能被完成了,并且不存在了,会报异常,所以先记录 + AdminUserRespDTO user = adminUserApi.getUser(userId); + taskService.addComment(task.getParentTaskId(), task.getProcessInstanceId(), BpmCommentTypeEnum.SUB_SIGN.getType(), + StrUtil.format(BpmCommentTypeEnum.SUB_SIGN.getComment(), user.getNickname(), cancelUser.getNickname())); + + // 4. 处理当前任务的父任务 + handleParentTaskIfSign(task.getParentTaskId()); + } + + @Override + public void copyTask(Long userId, BpmTaskCopyReqVO reqVO) { + processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getReason(), reqVO.getId()); + } + + /** + * 校验任务是否能被减签 + * + * @param id 任务编号 + * @return 任务信息 + */ + private Task validateTaskCanSignDelete(String id) { + Task task = validateTaskExist(id); + if (task.getParentTaskId() == null) { + throw exception(TASK_SIGN_DELETE_NO_PARENT); + } + Task parentTask = getTask(task.getParentTaskId()); + if (parentTask == null) { + throw exception(TASK_SIGN_DELETE_NO_PARENT); + } + if (BpmTaskSignTypeEnum.of(parentTask.getScopeType()) == null) { + throw exception(TASK_SIGN_DELETE_NO_PARENT); + } + return task; + } + + // ========== Event 事件相关方法 ========== + + @Override + public void processTaskCreated(Task task) { + // 1. 设置为待办中 + Integer status = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS); + if (status != null) { + log.error("[updateTaskStatusWhenCreated][taskId({}) 已经有状态({})]", task.getId(), status); + return; + } + updateTaskStatus(task.getId(), BpmTaskStatusEnum.RUNNING.getStatus()); + + ProcessInstance processInstance = processInstanceService.getProcessInstance(task.getProcessInstanceId()); + if (processInstance == null) { + log.error("[processTaskCreated][taskId({}) 没有找到流程实例]", task.getId()); + return; + } + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService. + getProcessDefinitionInfo(processInstance.getProcessDefinitionId()); + if (processDefinitionInfo == null) { + log.error("[processTaskCreated][processDefinitionId({}) 没有找到流程定义]", processInstance.getProcessDefinitionId()); + return; + } + + // 2. 任务前置通知 + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskBeforeTriggerSetting())){ + BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskBeforeTriggerSetting(); + BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); + } + + // 3. 处理自动通过的情况,例如说:1)无审批人时,是否自动通过、不通过;2)非【人工审核】时,是否自动通过、不通过 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); + FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); + Integer approveType = BpmnModelUtils.parseApproveType(userTaskElement); + Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(userTaskElement); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + /** + * 特殊情况:部分情况下,TransactionSynchronizationManager 注册 afterCommit 监听时,不会被调用,但是 afterCompletion 可以 + * 例如说:第一个 task 就是配置【自动通过】或者【自动拒绝】时 + * 参见 issue 反馈 + */ + @Override + public void afterCompletion(int transactionStatus) { + // 回滚情况,直接返回 + if (ObjectUtil.equal(transactionStatus, TransactionSynchronization.STATUS_ROLLED_BACK)) { + return; + } + // 特殊情况:第一个 task 【自动通过】时,第二个任务设置审批人时 transactionStatus 会为 STATUS_UNKNOWN,不知道啥原因 + if (ObjectUtil.equal(transactionStatus, TransactionSynchronization.STATUS_UNKNOWN) + && getTask(task.getId()) == null) { + return; + } + // 特殊情况一:【人工审核】审批人为空,根据配置是否要自动通过、自动拒绝 + if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.USER.getType())) { + // 如果有审批人、或者拥有人,则说明不满足情况一,不自动通过、不自动拒绝 + if (!ObjectUtil.isAllEmpty(task.getAssignee(), task.getOwner())) { + return; + } + if (ObjectUtil.equal(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.APPROVE.getType())) { + getSelf().approveTask(null, new BpmTaskApproveReqVO() + .setId(task.getId()).setReason(BpmReasonEnum.ASSIGN_EMPTY_APPROVE.getReason())); + } else if (ObjectUtil.equal(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.REJECT.getType())) { + getSelf().rejectTask(null, new BpmTaskRejectReqVO() + .setId(task.getId()).setReason(BpmReasonEnum.ASSIGN_EMPTY_REJECT.getReason())); + } + // 特殊情况二:【自动审核】审批类型为自动通过、不通过 + } else { + if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType())) { + getSelf().approveTask(null, new BpmTaskApproveReqVO() + .setId(task.getId()).setReason(BpmReasonEnum.APPROVE_TYPE_AUTO_APPROVE.getReason())); + } else if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) { + getSelf().rejectTask(null, new BpmTaskRejectReqVO() + .setId(task.getId()).setReason(BpmReasonEnum.APPROVE_TYPE_AUTO_REJECT.getReason())); + } + } + } + + }); + } + + /** + * 重要补充说明:该方法目前主要有两个情况会调用到: + *

+ * 1. 或签场景 + 审批通过:一个或签有多个审批时,如果 A 审批通过,其它或签 B、C 等任务会被 Flowable 自动删除,此时需要通过该方法更新状态为已取消 + * 2. 审批不通过:在 {@link #rejectTask(Long, BpmTaskRejectReqVO)} 不通过时,对于加签的任务,不会被 Flowable 删除,此时需要通过该方法更新状态为已取消 + */ + @Override + public void processTaskCanceled(String taskId) { + Task task = getTask(taskId); + // 1. 可能只是活动,不是任务,所以查询不到 + if (task == null) { + log.error("[updateTaskStatusWhenCanceled][taskId({}) 任务不存在]", taskId); + return; + } + + // 2. 更新 task 状态 + 原因 + Integer status = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS); + if (BpmTaskStatusEnum.isEndStatus(status)) { + log.error("[updateTaskStatusWhenCanceled][taskId({}) 处于结果({}),无需进行更新]", taskId, status); + return; + } + updateTaskStatusAndReason(taskId, BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_SYSTEM.getReason()); + // 补充说明:由于 Task 被删除成 HistoricTask 后,无法通过 taskService.addComment 添加理由,所以无法存储具体的取消理由 + } + + @Override + @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人 + public void processTaskAssigned(Task task) { + // 发送通知。在事务提交时,批量执行操作,所以直接查询会无法查询到 ProcessInstance,所以这里是通过监听事务的提交来实现。 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + /** + * 特殊情况:部分情况下,TransactionSynchronizationManager 注册 afterCommit 监听时,不会被调用,但是 afterCompletion 可以 + * 例如说:第一个 task 就是配置【自动通过】或者【自动拒绝】时 + * 参见 issue 反馈 + */ + @Override + public void afterCompletion(int transactionStatus) { + // 回滚情况,直接返回 + if (ObjectUtil.equal(transactionStatus, TransactionSynchronization.STATUS_ROLLED_BACK)) { + return; + } + // 特殊情况:第一个 task 【自动通过】时,第二个任务设置审批人时 transactionStatus 会为 STATUS_UNKNOWN,不知道啥原因 + if (ObjectUtil.equal(transactionStatus, TransactionSynchronization.STATUS_UNKNOWN) + && getTask(task.getId()) == null) { + return; + } + if (StrUtil.isEmpty(task.getAssignee())) { + log.error("[processTaskAssigned][taskId({}) 没有分配到负责人]", task.getId()); + return; + } + ProcessInstance processInstance = processInstanceService.getProcessInstance(task.getProcessInstanceId()); + if (processInstance == null) { + log.error("[processTaskAssigned][taskId({}) 没有找到流程实例]", task.getId()); + return; + } + + // 自动去重,通过自动审批的方式 TODO @芋艿 驳回的情况得考虑一下;@lesan:驳回后,又自动审批么? + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId()); + if (processDefinitionInfo == null) { + log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId()); + return; + } + if (processDefinitionInfo.getAutoApprovalType() != null) { + HistoricTaskInstanceQuery sameAssigneeQuery = historyService.createHistoricTaskInstanceQuery() + .processInstanceId(task.getProcessInstanceId()) + .taskAssignee(task.getAssignee()) // 相同审批人 + .taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus()) + .finished(); + if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType()) + && sameAssigneeQuery.count() > 0) { + getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) + .setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName())); + return; + } + if (BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getType().equals(processDefinitionInfo.getAutoApprovalType())) { + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); + if (bpmnModel == null) { + log.error("[processTaskAssigned][taskId({}) 没有找到流程模型({})]", task.getId(), task.getProcessDefinitionId()); + return; + } + List sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点 + BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), + SequenceFlow::getSourceRef); + if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) { + getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) + .setReason(BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName())); + return; + } + } + } + + // 获取发起人节点 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); + if (bpmnModel == null) { + log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId()); + return; + } + FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); + // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略 + // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识 + Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(), + String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class); + Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(), + PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); + if (userTaskElement.getId().equals(START_USER_NODE_ID) + && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 + || Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 + && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { + getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) + .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); + return; + } + // 当不为发起人节点时,审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理 + if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID) + && StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) { + if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { + Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement); + + // 情况一:自动跳过 + if (ObjectUtils.equalsAny(assignStartUserHandlerType, + BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) { + getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) + .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason())); + return; + } + // 情况二:转交给部门负责人审批 + if (ObjectUtils.equalsAny(assignStartUserHandlerType, + BpmUserTaskAssignStartUserHandlerTypeEnum.TRANSFER_DEPT_LEADER.getType())) { + AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())); + Assert.notNull(startUser, "提交人({})信息为空", processInstance.getStartUserId()); + DeptRespDTO dept = startUser.getDeptId() != null ? deptApi.getDept(startUser.getDeptId()) : null; + Assert.notNull(dept, "提交人({})部门({})信息为空", processInstance.getStartUserId(), startUser.getDeptId()); + // 找不到部门负责人的情况下,自动审批通过 + // noinspection DataFlowIssue + if (dept.getLeaderUserId() == null) { + getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) + .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason())); + return; + } + // 找得到部门负责人的情况下,修改负责人 + if (ObjectUtil.notEqual(dept.getLeaderUserId(), startUser.getId())) { + getSelf().transferTask(Long.valueOf(task.getAssignee()), new BpmTaskTransferReqVO() + .setId(task.getId()).setAssigneeUserId(dept.getLeaderUserId()) + .setReason(BpmReasonEnum.ASSIGN_START_USER_TRANSFER_DEPT_LEADER.getReason())); + return; + } + // 如果部门负责人是自己,还是自己审批吧~ + } + } + } + // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 + FlowableUtils.execute(processInstance.getTenantId(), () -> { + AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())); + messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task)); + }); + } + + }); + } + + @Override + public void processTaskCompleted(Task task) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(task.getProcessInstanceId()); + if (processInstance == null) { + log.error("[processTaskCompleted][taskId({}) 没有找到流程实例]", task.getId()); + return; + } + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService. + getProcessDefinitionInfo(processInstance.getProcessDefinitionId()); + if (processDefinitionInfo == null) { + log.error("[processTaskCompleted][processDefinitionId({}) 没有找到流程定义]", processInstance.getProcessDefinitionId()); + return; + } + + // 任务后置通知 + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskAfterTriggerSetting())){ + BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskAfterTriggerSetting(); + BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); + if (processInstance == null) { + log.error("[processTaskTimeout][processInstanceId({}) 没有找到流程实例]", processInstanceId); + return; + } + List taskList = getRunningTaskListByProcessInstanceId(processInstanceId, true, taskDefineKey); + // TODO 优化:未来需要考虑加签的情况 + if (CollUtil.isEmpty(taskList)) { + log.error("[processTaskTimeout][processInstanceId({}) 定义Key({}) 没有找到任务]", processInstanceId, taskDefineKey); + return; + } + + taskList.forEach(task -> FlowableUtils.execute(task.getTenantId(), () -> { + // 情况一:自动提醒 + if (Objects.equals(handlerType, BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType())) { + messageService.sendMessageWhenTaskTimeout(new BpmMessageSendWhenTaskTimeoutReqDTO() + .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName()) + .setTaskId(task.getId()).setTaskName(task.getName()).setAssigneeUserId(Long.parseLong(task.getAssignee()))); + return; + } + + // 情况二:自动同意 + if (Objects.equals(handlerType, BpmUserTaskTimeoutHandlerTypeEnum.APPROVE.getType())) { + approveTask(Long.parseLong(task.getAssignee()), + new BpmTaskApproveReqVO().setId(task.getId()).setReason(BpmReasonEnum.TIMEOUT_APPROVE.getReason())); + return; + } + + // 情况三:自动拒绝 + if (Objects.equals(handlerType, BpmUserTaskTimeoutHandlerTypeEnum.REJECT.getType())) { + rejectTask(Long.parseLong(task.getAssignee()), + new BpmTaskRejectReqVO().setId(task.getId()).setReason(BpmReasonEnum.REJECT_TASK.getReason())); + } + })); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void processChildProcessTimeout(String processInstanceId, String taskDefineKey) { + List activityInstances = runtimeService.createActivityInstanceQuery() + .processInstanceId(processInstanceId) + .activityId(taskDefineKey).list(); + activityInstances.forEach(activityInstance -> FlowableUtils.execute(activityInstance.getTenantId(), + () -> moveTaskToEnd(activityInstance.getCalledProcessInstanceId(), BpmReasonEnum.TIMEOUT_APPROVE.getReason()))); + } + + @Override + public void triggerTask(String processInstanceId, String taskDefineKey) { + Execution execution = runtimeService.createExecutionQuery() + .processInstanceId(processInstanceId) + .activityId(taskDefineKey) + .singleResult(); + if (execution == null) { + log.error("[triggerTask][processInstanceId({}) activityId({}) 没有找到执行活动]", processInstanceId, taskDefineKey); + return; + } + + // 若存在直接触发接收任务,执行后续节点 + FlowableUtils.execute(execution.getTenantId(), + () -> runtimeService.trigger(execution.getId())); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private BpmTaskServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java new file mode 100644 index 0000000..40313a9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.bpm.service.task.listener; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserEmptyTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.ExecutionListener; +import org.flowable.engine.impl.el.FixedValue; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * BPM 子流程监听器:设置流程的发起人 + * + * @author Lesan + */ +@Component +@Slf4j +public class BpmCallActivityListener implements ExecutionListener { + + public static final String DELEGATE_EXPRESSION = "${bpmCallActivityListener}"; + + @Setter + private FixedValue listenerConfig; + + @Resource + private BpmProcessDefinitionService processDefinitionService; + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public void notify(DelegateExecution execution) { + String expressionText = listenerConfig.getExpressionText(); + Assert.notNull(expressionText, "监听器扩展字段({})不能为空", expressionText); + BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting startUserSetting = JsonUtils.parseObject( + expressionText, BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting.class); + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getRootProcessInstanceId()); + + // 1. 当发起人来源为主流程发起人时,并兜底 startUserSetting 为空时 + if (startUserSetting == null + || startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())) { + FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + return; + } + + // 2. 当发起人来源为表单时 + if (startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.FROM_FORM.getType())) { + String formFieldValue = MapUtil.getStr(processInstance.getProcessVariables(), startUserSetting.getFormField()); + // 2.1 当表单值为空时 + if (StrUtil.isEmpty(formFieldValue)) { + // 2.1.1 来自主流程发起人 + if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType())) { + FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + return; + } + // 2.1.2 来自子流程管理员 + if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.CHILD_PROCESS_ADMIN.getType())) { + BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(execution.getProcessDefinitionId()); + List managerUserIds = processDefinition.getManagerUserIds(); + FlowableUtils.setAuthenticatedUserId(managerUserIds.get(0)); + return; + } + // 2.1.3 来自主流程管理员 + if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_ADMIN.getType())) { + BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(processInstance.getProcessDefinitionId()); + List managerUserIds = processDefinition.getManagerUserIds(); + FlowableUtils.setAuthenticatedUserId(managerUserIds.get(0)); + return; + } + } + // 2.2 使用表单值,并兜底字符串转 Long 失败时使用主流程发起人 + try { + FlowableUtils.setAuthenticatedUserId(Long.parseLong(formFieldValue)); + } catch (Exception e) { + log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]", + DELEGATE_EXPRESSION, formFieldValue); + FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + } + } + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java new file mode 100644 index 0000000..0364edd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.bpm.service.task.listener; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.engine.impl.el.FixedValue; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.service.delegate.DelegateTask; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig; + +// TODO @芋艿:可能会想换个包地址 +/** + * BPM 用户任务通用监听器 + * + * @author Lesan + */ +@Component +@Slf4j +@Scope("prototype") +public class BpmUserTaskListener implements TaskListener { + + public static final String DELEGATE_EXPRESSION = "${bpmUserTaskListener}"; + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Setter + private FixedValue listenerConfig; + + @Override + public void notify(DelegateTask delegateTask) { + // 1. 获取所需基础信息 + ProcessInstance processInstance = processInstanceService.getProcessInstance(delegateTask.getProcessInstanceId()); + BpmSimpleModelNodeVO.ListenerHandler listenerHandler = parseListenerConfig(listenerConfig); + + // 2. 发起请求 + // TODO @芋艿:哪些默认参数,后续再调研下;感觉可以搞个 task 字段,把整个 delegateTask 放进去; + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("processInstanceId") + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getProcessInstanceId())); + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("assignee") + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getAssignee())); + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskDefinitionKey") + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getTaskDefinitionKey())); + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskId") + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getId())); + BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + listenerHandler.getPath(), listenerHandler.getHeader(), listenerHandler.getBody(), false, null); + + // 3. 是否需要后续操作?TODO 芋艿:待定! + } +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java new file mode 100644 index 0000000..dfbaa63 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger; + +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; + +// TODO @芋艿:可能会想换个包地址 +/** + * BPM 触发器接口 + *

+ * 处理不同的动作 + * + * @author jason + */ +public interface BpmTrigger { + + /** + * 对应触发器类型 + * + * @return 触发器类型 + */ + BpmTriggerTypeEnum getType(); + + /** + * 触发器执行 + * + * @param processInstanceId 流程实例编号 + * @param param 触发器参数 + */ + void execute(String processInstanceId, String param); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java new file mode 100644 index 0000000..c441c5f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.form; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * BPM 删除流程表单数据触发器 + * + * @author jason + */ +@Component +@Slf4j +public class BpmFormDeleteTrigger implements BpmTrigger { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTriggerTypeEnum getType() { + return BpmTriggerTypeEnum.FORM_DELETE; + } + + @Override + public void execute(String processInstanceId, String param) { + // 1. 解析删除流程表单数据配置 + List settings = JsonUtils.parseObject(param, new TypeReference<>() {}); + if (CollUtil.isEmpty(settings)) { + log.error("[execute][流程({}) 删除流程表单数据触发器配置为空]", processInstanceId); + return; + } + + // 2. 获取流程变量 + Map processVariables = processInstanceService.getProcessInstance(processInstanceId).getProcessVariables(); + + // 3.1 获取需要删除的表单字段 + Set deleteFields = new HashSet<>(); + settings.forEach(setting -> { + if (CollUtil.isEmpty(setting.getDeleteFields())) { + return; + } + // 配置了条件,判断条件是否满足 + boolean isFieldDeletedNeeded = true; + if (setting.getConditionType() != null) { + String conditionExpression = SimpleModelUtils.buildConditionExpression( + setting.getConditionType(), setting.getConditionExpression(), setting.getConditionGroups()); + isFieldDeletedNeeded = BpmnModelUtils.evalConditionExpress(processVariables, conditionExpression); + } + if (isFieldDeletedNeeded) { + deleteFields.addAll(setting.getDeleteFields()); + } + }); + + // 3.2 删除流程变量 + if (CollUtil.isNotEmpty(deleteFields)) { + processInstanceService.removeProcessInstanceVariables(processInstanceId, deleteFields); + } + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java new file mode 100644 index 0000000..4940d69 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.form; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.FormTriggerSetting; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * BPM 更新流程表单触发器 + * + * @author jason + */ +@Component +@Slf4j +public class BpmFormUpdateTrigger implements BpmTrigger { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTriggerTypeEnum getType() { + return BpmTriggerTypeEnum.FORM_UPDATE; + } + + @Override + public void execute(String processInstanceId, String param) { + // 1. 解析更新流程表单配置 + List settings = JsonUtils.parseObject(param, new TypeReference<>() {}); + if (CollUtil.isEmpty(settings)) { + log.error("[execute][流程({}) 更新流程表单触发器配置为空]", processInstanceId); + return; + } + + // 2. 获取流程变量 + Map processVariables = processInstanceService.getProcessInstance(processInstanceId).getProcessVariables(); + + // 3. 更新流程变量 + for (FormTriggerSetting setting : settings) { + if (CollUtil.isEmpty(setting.getUpdateFormFields())) { + continue; + } + // 配置了条件,判断条件是否满足 + boolean isFormUpdateNeeded = true; + if (setting.getConditionType() != null) { + String conditionExpression = SimpleModelUtils.buildConditionExpression( + setting.getConditionType(), setting.getConditionExpression(), setting.getConditionGroups()); + isFormUpdateNeeded = BpmnModelUtils.evalConditionExpress(processVariables, conditionExpression); + } + // 更新流程表单 + if (isFormUpdateNeeded) { + processInstanceService.updateProcessInstanceVariables(processInstanceId, setting.getUpdateFormFields()); + } + } + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java new file mode 100644 index 0000000..b1d81bc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.http; + +import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger; +import lombok.extern.slf4j.Slf4j; + +/** + * BPM 发送 HTTP 请求触发器抽象类 + * + * @author jason + */ +@Slf4j +public abstract class BpmAbstractHttpRequestTrigger implements BpmTrigger { + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java new file mode 100644 index 0000000..d2cc03e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.http; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * BPM HTTP 回调触发器 + * + * @author jason + */ +@Component +@Slf4j +public class BpmHttpCallbackTrigger extends BpmAbstractHttpRequestTrigger { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTriggerTypeEnum getType() { + return BpmTriggerTypeEnum.HTTP_CALLBACK; + } + + @Override + public void execute(String processInstanceId, String param) { + // 1. 解析 http 请求配置 + BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, + BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting.class); + if (setting == null) { + log.error("[execute][流程({}) HTTP 回调触发器配置为空]", processInstanceId); + return; + } + + // 2. 发起请求 + ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); + setting.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam() + .setKey("taskDefineKey") // 重要:回调请求 taskDefineKey 需要传给被调用方,用于回调执行 + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(setting.getCallbackTaskDefineKey())); + BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + setting.getUrl(), setting.getHeader(), setting.getBody(), false, null); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java new file mode 100644 index 0000000..cecbe6a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.http; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * BPM 发送同步 HTTP 请求触发器 + * + * @author jason + */ +@Component +@Slf4j +public class BpmSyncHttpRequestTrigger extends BpmAbstractHttpRequestTrigger { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public BpmTriggerTypeEnum getType() { + return BpmTriggerTypeEnum.HTTP_REQUEST; + } + + @Override + public void execute(String processInstanceId, String param) { + // 1. 解析 http 请求配置 + HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, HttpRequestTriggerSetting.class); + if (setting == null) { + log.error("[execute][流程({}) HTTP 触发器请求配置为空]", processInstanceId); + return; + } + + // 2. 发起请求 + ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); + BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java new file mode 100644 index 0000000..308d866 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java @@ -0,0 +1,274 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other.BpmTaskCandidateAssignEmptyStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.ExtensionElement; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Spy; +import org.mockito.internal.util.collections.Sets; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_NAMESPACE; +import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_PREFIX; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * {@link BpmTaskCandidateInvoker} 的单元测试 + * + * @author 芋道源码 + */ +public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest { + + private BpmTaskCandidateInvoker taskCandidateInvoker; + + @Mock + private AdminUserApi adminUserApi; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Spy + private BpmTaskCandidateStrategy userStrategy; + @Mock + private BpmTaskCandidateAssignEmptyStrategy emptyStrategy; + + @Spy + private List strategyList; + + @BeforeEach + public void setUp() { + userStrategy = new BpmTaskCandidateUserStrategy(); // 创建 strategy 实例 + when(emptyStrategy.getStrategy()).thenReturn(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY); + strategyList = List.of(userStrategy, emptyStrategy); // 创建 strategyList + taskCandidateInvoker = new BpmTaskCandidateInvoker(strategyList, adminUserApi); + } + + /** + * 场景:成功计算到候选人,但是移除了发起人的用户 + */ + @Test + public void testCalculateUsersByTask_some() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + // 准备参数 + String param = "1,2"; + DelegateExecution execution = mock(DelegateExecution.class); + // mock 方法(DelegateExecution) + UserTask userTask = mock(UserTask.class); + String processInstanceId = randomString(); + when(execution.getProcessInstanceId()).thenReturn(processInstanceId); + when(execution.getCurrentFlowElement()).thenReturn(userTask); + when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY))) + .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString()); + when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM))) + .thenReturn(param); + // mock 方法(adminUserApi) + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); + // mock 移除发起人的用户 + springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class)) + .thenReturn(processInstanceService); + ProcessInstance processInstance = mock(ProcessInstance.class); + when(processInstanceService.getProcessInstance(eq(processInstanceId))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn("1"); + mockFlowElementExtensionElement(userTask, BpmnModelConstants.USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, + String.valueOf(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())); + + // 调用 + Set results = taskCandidateInvoker.calculateUsersByTask(execution); + // 断言 + assertEquals(asSet(2L), results); + } + } + + /** + * 场景:没有计算到候选人,但是被禁用移除,最终通过 empty 进行分配 + */ + @Test + public void testCalculateUsersByTask_none() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + // 准备参数 + String param = "1,2"; + DelegateExecution execution = mock(DelegateExecution.class); + // mock 方法(DelegateExecution) + UserTask userTask = mock(UserTask.class); + String processInstanceId = randomString(); + when(execution.getProcessInstanceId()).thenReturn(processInstanceId); + when(execution.getCurrentFlowElement()).thenReturn(userTask); + when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY))) + .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString()); + when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM))) + .thenReturn(param); + // mock 方法(adminUserApi) + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); + // mock 方法(empty) + when(emptyStrategy.calculateUsersByTask(same(execution), same(param))) + .thenReturn(Sets.newSet(2L)); + // mock 移除发起人的用户 + springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class)) + .thenReturn(processInstanceService); + ProcessInstance processInstance = mock(ProcessInstance.class); + when(processInstanceService.getProcessInstance(eq(processInstanceId))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn("1"); + + // 调用 + Set results = taskCandidateInvoker.calculateUsersByTask(execution); + // 断言 + assertEquals(asSet(2L), results); + } + } + + /** + * 场景:没有计算到候选人,但是被禁用移除,最终通过 empty 进行分配 + */ + @Test + public void testCalculateUsersByActivity_some() { + try (MockedStatic bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) { + // 准备参数 + String param = "1,2"; + BpmnModel bpmnModel = mock(BpmnModel.class); + String activityId = randomString(); + Long startUserId = 1L; + String processDefinitionId = randomString(); + Map processVariables = new HashMap<>(); + // mock 方法(DelegateExecution) + UserTask userTask = mock(UserTask.class); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateStrategy(same(userTask))) + .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy()); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateParam(same(userTask))) + .thenReturn(param); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))).thenReturn(userTask); + // mock 方法(adminUserApi) + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); + // mock 移除发起人的用户 + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignStartUserHandlerType(same(userTask))) + .thenReturn(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType()); + + // 调用 + Set results = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId, + startUserId, processDefinitionId, processVariables); + // 断言 + assertEquals(asSet(2L), results); + } + } + + /** + * 场景:成功计算到候选人,但是移除了发起人的用户 + */ + @Test + public void testCalculateUsersByActivity_none() { + try (MockedStatic bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) { + // 准备参数 + String param = "1,2"; + BpmnModel bpmnModel = mock(BpmnModel.class); + String activityId = randomString(); + Long startUserId = 1L; + String processDefinitionId = randomString(); + Map processVariables = new HashMap<>(); + // mock 方法(DelegateExecution) + UserTask userTask = mock(UserTask.class); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateStrategy(same(userTask))) + .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy()); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateParam(same(userTask))) + .thenReturn(param); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))).thenReturn(userTask); + // mock 方法(adminUserApi) + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); + // mock 方法(empty) + when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId), + eq(param), same(startUserId), same(processDefinitionId), same(processVariables))) + .thenReturn(Sets.newSet(2L)); + + // 调用 + Set results = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId, + startUserId, processDefinitionId, processVariables); + // 断言 + assertEquals(asSet(2L), results); + } + } + + private static void mockFlowElementExtensionElement(FlowElement element, String name, String value) { + if (value == null) { + return; + } + ExtensionElement extensionElement = new ExtensionElement(); + extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); + extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX); + extensionElement.setElementText(value); + extensionElement.setName(name); + // mock + Map> extensionElements = element.getExtensionElements(); + if (extensionElements == null) { + extensionElements = new LinkedHashMap<>(); + } + extensionElements.put(name, Collections.singletonList(extensionElement)); + when(element.getExtensionElements()).thenReturn(extensionElements); + } + + @Test + public void testRemoveDisableUsers() { + // 准备参数. 1L 可以找到;2L 是禁用的;3L 找不到 + Set assigneeUserIds = asSet(1L, 2L, 3L); + // mock 方法 + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(assigneeUserIds))).thenReturn(userMap); + + // 调用 + taskCandidateInvoker.removeDisableUsers(assigneeUserIds); + // 断言 + assertEquals(asSet(1L), assigneeUserIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpressionTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpressionTest.java new file mode 100644 index 0000000..f7eac7c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpressionTest.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.expression; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskAssignLeaderExpression expression; + + @Mock + private AdminUserApi adminUserApi; + @Mock + private DeptApi deptApi; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Test + public void testCalculateUsers_noDept() { + // 准备参数 + DelegateExecution execution = mockDelegateExecution(1L); + // mock 方法(startUser) + AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); + when(adminUserApi.getUser(eq(1L))).thenReturn(startUser); + // mock 方法(getStartUserDept)没有部门 + when(deptApi.getDept(eq(10L))).thenReturn(null); + + // 调用 + Set result = expression.calculateUsers(execution, 1); + // 断言 + assertEquals(0, result.size()); + } + + @Test + public void testCalculateUsers_noParentDept() { + // 准备参数 + DelegateExecution execution = mockDelegateExecution(1L); + // mock 方法(startUser) + AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); + when(adminUserApi.getUser(eq(1L))).thenReturn(startUser); + DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L) + .setLeaderUserId(20L)); + // mock 方法(getDept) + when(deptApi.getDept(eq(10L))).thenReturn(startUserDept); + when(deptApi.getDept(eq(100L))).thenReturn(null); + + // 调用 + Set result = expression.calculateUsers(execution, 2); + // 断言 + assertEquals(asSet(20L), result); + } + + @Test + public void testCalculateUsers_existParentDept() { + // 准备参数 + DelegateExecution execution = mockDelegateExecution(1L); + // mock 方法(startUser) + AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); + when(adminUserApi.getUser(eq(1L))).thenReturn(startUser); + DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L) + .setLeaderUserId(20L)); + when(deptApi.getDept(eq(10L))).thenReturn(startUserDept); + // mock 方法(父 dept) + DeptRespDTO parentDept = randomPojo(DeptRespDTO.class, o -> o.setId(100L).setParentId(1000L) + .setLeaderUserId(200L)); + when(deptApi.getDept(eq(100L))).thenReturn(parentDept); + + // 调用 + Set result = expression.calculateUsers(execution, 2); + // 断言 + assertEquals(asSet(200L), result); + } + + @SuppressWarnings("SameParameterValue") + private DelegateExecution mockDelegateExecution(Long startUserId) { + ExecutionEntityImpl execution = new ExecutionEntityImpl(); + execution.setProcessInstanceId(randomString()); + // mock 返回 startUserId + ExecutionEntityImpl processInstance = new ExecutionEntityImpl(); + processInstance.setStartUserId(String.valueOf(startUserId)); + when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))) + .thenReturn(processInstance); + return execution; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategyTest.java new file mode 100644 index 0000000..3d671cf --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategyTest.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import org.assertj.core.util.Sets; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateDeptLeaderMultiStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateDeptLeaderMultiStrategy strategy; + + @Mock + private DeptApi deptApi; + + @Test + public void testCalculateUsers() { + // 准备参数 + String param = "10,20|2"; + // mock 方法 + when(deptApi.getDept(any())).thenAnswer((Answer) invocationOnMock -> { + Long deptId = invocationOnMock.getArgument(0); + return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)); + }); + + // 调用 + Set userIds = strategy.calculateUsers(param); + // 断言结果 + assertEquals(Sets.newLinkedHashSet(11L, 1001L, 21L, 2001L), userIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategyTest.java new file mode 100644 index 0000000..fc27bcd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategyTest.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import org.assertj.core.util.Sets; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateDeptLeaderStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateDeptLeaderStrategy strategy; + + @Mock + private DeptApi deptApi; + + @Test + public void testCalculateUsers() { + // 准备参数 + String param = "10,20"; + // mock 方法 + when(deptApi.getDeptList(eq(SetUtils.asSet(10L, 20L)))).thenReturn(asList( + randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(10L).setLeaderUserId(11L)), + randomPojo(DeptRespDTO.class, o -> o.setId(20L).setParentId(20L).setLeaderUserId(21L)))); + + // 调用 + Set userIds = strategy.calculateUsers(param); + // 断言结果 + assertEquals(Sets.newLinkedHashSet(11L, 21L), userIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategyTest.java new file mode 100644 index 0000000..57ab228 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategyTest.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.assertj.core.util.Sets; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateDeptMemberStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateDeptMemberStrategy strategy; + + @Mock + private DeptApi deptApi; + @Mock + private AdminUserApi adminUserApi; + + @Test + public void testCalculateUsers() { + // 准备参数 + String param = "10,20"; + // mock 方法 + when(adminUserApi.getUserListByDeptIds(eq(SetUtils.asSet(10L, 20L)))).thenReturn(asList( + randomPojo(AdminUserRespDTO.class, o -> o.setId(11L)), + randomPojo(AdminUserRespDTO.class, o -> o.setId(21L)))); + + // 调用 + Set userIds = strategy.calculateUsers(param); + // 断言结果 + assertEquals(Sets.newLinkedHashSet(11L, 21L), userIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java new file mode 100644 index 0000000..2f66a83 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.assertj.core.util.Sets; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateStartUserDeptLeaderMultiStrategy strategy; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Mock + private AdminUserApi adminUserApi; + @Mock + private DeptApi deptApi; + + @Test + public void testCalculateUsersByTask() { + // 准备参数 + String param = "2"; + // mock 方法(获得流程发起人) + Long startUserId = 1L; + ProcessInstance processInstance = mock(ProcessInstance.class); + DelegateExecution execution = mock(DelegateExecution.class); + when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn(startUserId.toString()); + // mock 方法(获取发起人的 multi 部门负责人) + mockGetStartUserDept(startUserId); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(Sets.newLinkedHashSet(11L, 1001L), userIds); + } + + @Test + public void testCalculateUsersByActivity() { + // 准备参数 + String param = "2"; + // mock 方法 + Long startUserId = 1L; + mockGetStartUserDept(startUserId); + + // 调用 + Set userIds = strategy.calculateUsersByActivity(null, null, param, + startUserId, null, null); + // 断言 + assertEquals(Sets.newLinkedHashSet(11L, 1001L), userIds); + } + + private void mockGetStartUserDept(Long startUserId) { + when(adminUserApi.getUser(eq(startUserId))).thenReturn( + randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L))); + when(deptApi.getDept(any())).thenAnswer((Answer) invocationOnMock -> { + Long deptId = invocationOnMock.getArgument(0); + return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)); + }); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java new file mode 100644 index 0000000..a5f863f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.assertj.core.util.Sets; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateStartUserDeptLeaderStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateStartUserDeptLeaderStrategy strategy; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Mock + private AdminUserApi adminUserApi; + @Mock + private DeptApi deptApi; + + @Test + public void testCalculateUsersByTask() { + // 准备参数 + String param = "2"; + // mock 方法(获得流程发起人) + Long startUserId = 1L; + ProcessInstance processInstance = mock(ProcessInstance.class); + DelegateExecution execution = mock(DelegateExecution.class); + when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn(startUserId.toString()); + // mock 方法(获取发起人的部门负责人) + mockGetStartUserDeptLeader(startUserId); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(Sets.newLinkedHashSet(1001L), userIds); + } + + @Test + public void testGetStartUserDeptLeader() { + // 准备参数 + String param = "2"; + // mock 方法 + Long startUserId = 1L; + mockGetStartUserDeptLeader(startUserId); + + // 调用 + Set userIds = strategy.calculateUsersByActivity(null, null, param, + startUserId, null, null); + // 断言 + assertEquals(Sets.newLinkedHashSet(1001L), userIds); + } + + private void mockGetStartUserDeptLeader(Long startUserId) { + when(adminUserApi.getUser(eq(startUserId))).thenReturn( + randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L))); + when(deptApi.getDept(any())).thenAnswer((Answer) invocationOnMock -> { + Long deptId = invocationOnMock.getArgument(0); + return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)); + }); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java new file mode 100644 index 0000000..f63ccc3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import org.assertj.core.util.Sets; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateStartUserSelectStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateStartUserSelectStrategy strategy; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Test + public void testCalculateUsersByTask() { + // 准备参数 + String param = "2"; + // mock 方法(获得流程发起人) + ProcessInstance processInstance = mock(ProcessInstance.class); + DelegateExecution execution = mock(DelegateExecution.class); + when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance); + when(execution.getCurrentActivityId()).thenReturn("activity_001"); + // mock 方法(FlowableUtils) + Map processVariables = new HashMap<>(); + processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, + MapUtil.of("activity_001", List.of(1L, 2L))); + when(processInstance.getProcessVariables()).thenReturn(processVariables); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(Sets.newLinkedHashSet(1L, 2L), userIds); + } + + @Test + public void testCalculateUsersByActivity() { + // 准备参数 + String activityId = "activity_001"; + Map processVariables = new HashMap<>(); + processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, + MapUtil.of("activity_001", List.of(1L, 2L))); + + // 调用 + Set userIds = strategy.calculateUsersByActivity(null, activityId, null, + null, null, processVariables); + // 断言 + assertEquals(Sets.newLinkedHashSet(1L, 2L), userIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategyTest.java new file mode 100644 index 0000000..d0add24 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategyTest.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other; + +import cn.hutool.core.collection.ListUtil; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.engine.delegate.DelegateExecution; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +public class BpmTaskCandidateAssignEmptyStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateAssignEmptyStrategy strategy; + + @Mock + private BpmProcessDefinitionService processDefinitionService; + + @Test + public void testCalculateUsersByTask() { + try (MockedStatic flowableUtilMockedStatic = mockStatic(FlowableUtils.class); + MockedStatic bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) { + // 准备参数 + DelegateExecution execution = mock(DelegateExecution.class); + String param = randomString(); + // mock 方法(execution) + String processDefinitionId = randomString(); + when(execution.getProcessDefinitionId()).thenReturn(processDefinitionId); + FlowElement flowElement = mock(FlowElement.class); + when(execution.getCurrentFlowElement()).thenReturn(flowElement); + // mock 方法(parseAssignEmptyHandlerType) + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignEmptyHandlerType(same(flowElement))) + .thenReturn(BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_USER.getType()); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignEmptyHandlerUserIds(same(flowElement))) + .thenReturn(ListUtil.of(1L, 2L)); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(SetUtils.asSet(1L, 2L), userIds); + } + + } + + @Test + public void testCalculateUsersByActivity() { + try (MockedStatic bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) { + // 准备参数 + String processDefinitionId = randomString(); + String activityId = randomString(); + String param = randomString(); + // mock 方法(getFlowElementById) + FlowElement flowElement = mock(FlowElement.class); + BpmnModel bpmnModel = mock(BpmnModel.class); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))) + .thenReturn(flowElement); + // mock 方法(parseAssignEmptyHandlerType) + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignEmptyHandlerType(same(flowElement))) + .thenReturn(BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType()); + // mock 方法(getProcessDefinitionInfo) + BpmProcessDefinitionInfoDO processDefinition = randomPojo(BpmProcessDefinitionInfoDO.class, + o -> o.setManagerUserIds(ListUtil.of(1L, 2L))); + when(processDefinitionService.getProcessDefinitionInfo(eq(processDefinitionId))).thenReturn(processDefinition); + + // 调用 + Set userIds = strategy.calculateUsersByActivity(bpmnModel, activityId, param, + null, processDefinitionId, null); + // 断言 + assertEquals(SetUtils.asSet(1L, 2L), userIds); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java new file mode 100644 index 0000000..1da8a2f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import org.flowable.engine.delegate.DelegateExecution; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.MockedStatic; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Disabled // TODO 芋艿:临时注释 +public class BpmTaskCandidateExpressionStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateExpressionStrategy strategy; + + @Test + public void testCalculateUsersByTask() { + try (MockedStatic flowableUtilMockedStatic = mockStatic(FlowableUtils.class)) { + // 准备参数 + String param = "1,2"; + DelegateExecution execution = mock(DelegateExecution.class); + // mock 方法 + flowableUtilMockedStatic.when(() -> FlowableUtils.getExpressionValue(same(execution), eq(param))) + .thenReturn(asSet(1L, 2L)); + + // 调用 + Set results = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(asSet(1L, 2L), results); + } + } + + @Test + public void testCalculateUsersByActivity() { + try (MockedStatic flowableUtilMockedStatic = mockStatic(FlowableUtils.class)) { + // 准备参数 + String param = "1,2"; + Map processVariables = new HashMap<>(); + // mock 方法 + flowableUtilMockedStatic.when(() -> FlowableUtils.getExpressionValue(same(processVariables), eq(param))) + .thenReturn(asSet(1L, 2L)); + + // 调用 + Set results = strategy.calculateUsersByActivity(null, null, param, + null, null, processVariables); + // 断言 + assertEquals(asSet(1L, 2L), results); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java new file mode 100644 index 0000000..44327ec --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Arrays; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Disabled // TODO 芋艿:临时注释 +public class BpmTaskCandidateGroupStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateGroupStrategy strategy; + + @Mock + private BpmUserGroupService userGroupService; + + @Test + public void testCalculateUsers() { + // 准备参数 + String param = "1,2"; + // mock 方法 + BpmUserGroupDO userGroup1 = randomPojo(BpmUserGroupDO.class, o -> o.setUserIds(asSet(11L, 12L))); + BpmUserGroupDO userGroup2 = randomPojo(BpmUserGroupDO.class, o -> o.setUserIds(asSet(21L, 22L))); + when(userGroupService.getUserGroupList(eq(asSet(1L, 2L)))).thenReturn(Arrays.asList(userGroup1, userGroup2)); + + // 调用 + Set userIds = strategy.calculateUsersByTask(null, param); + // 断言 + assertEquals(asSet(11L, 12L, 21L, 22L), userIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java new file mode 100644 index 0000000..a444224 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.api.dept.PostApi; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Disabled // TODO 芋艿:临时注释 +public class BpmTaskCandidatePostStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidatePostStrategy strategy; + + @Mock + private PostApi postApi; + @Mock + private AdminUserApi adminUserApi; + + @Test + public void testCalculateUsers() { + // 准备参数 + String param = "1,2"; + // mock 方法 + List users = convertList(asSet(11L, 22L), + id -> new AdminUserRespDTO().setId(id)); + when(adminUserApi.getUserListByPostIds(eq(asSet(1L, 2L)))).thenReturn(users); + + // 调用 + Set userIds = strategy.calculateUsersByTask(null, param); + // 断言 + assertEquals(asSet(11L, 22L), userIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java new file mode 100644 index 0000000..4db4cb1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; +import cn.iocoder.yudao.module.system.api.permission.RoleApi; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Disabled // TODO 芋艿:临时注释 +public class BpmTaskCandidateRoleStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateRoleStrategy strategy; + + @Mock + private RoleApi roleApi; + @Mock + private PermissionApi permissionApi; + + @Test + public void testCalculateUsers() { + // 准备参数 + String param = "1,2"; + // mock 方法 + when(permissionApi.getUserRoleIdListByRoleIds(eq(asSet(1L, 2L)))) + .thenReturn(asSet(11L, 22L)); + + // 调用 + Set userIds = strategy.calculateUsersByTask(null, param); + // 断言 + assertEquals(asSet(11L, 22L), userIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategyTest.java new file mode 100644 index 0000000..1b8eba1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategyTest.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import org.assertj.core.util.Sets; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateStartUserStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateStartUserStrategy strategy; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Test + public void testCalculateUsersByTask() { + // 准备参数 + String param = "2"; + // mock 方法(获得流程发起人) + Long startUserId = 1L; + ProcessInstance processInstance = mock(ProcessInstance.class); + DelegateExecution execution = mock(DelegateExecution.class); + when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn(startUserId.toString()); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(Sets.newLinkedHashSet(startUserId), userIds); + } + + @Test + public void testCalculateUsersByActivity() { + // 准备参数 + Long startUserId = 1L; + + // 调用 + Set userIds = strategy.calculateUsersByActivity(null, null, null, + startUserId, null, null); + // 断言 + assertEquals(Sets.newLinkedHashSet(startUserId), userIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java new file mode 100644 index 0000000..fca4aa1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Disabled // TODO 芋艿:临时注释 +public class BpmTaskCandidateUserStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateUserStrategy strategy; + + @Test + public void test() { + // 准备参数 + String param = "1,2"; + + // 调用 + Set userIds = strategy.calculateUsersByTask(null, param); + // 断言 + assertEquals(asSet(1L, 2L), userIds); + } + + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java new file mode 100644 index 0000000..f71b8aa --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java @@ -0,0 +1,136 @@ +package cn.iocoder.yudao.module.bpm.service.category; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategoryPageReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategorySaveReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.category.BpmCategoryMapper; +import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryServiceImpl; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.CATEGORY_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link BpmCategoryServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(BpmCategoryServiceImpl.class) +public class BpmCategoryServiceImplTest extends BaseDbUnitTest { + + @Resource + private BpmCategoryServiceImpl categoryService; + + @Resource + private BpmCategoryMapper categoryMapper; + + @Test + public void testCreateCategory_success() { + // 准备参数 + BpmCategorySaveReqVO createReqVO = randomPojo(BpmCategorySaveReqVO.class).setId(null) + .setStatus(randomCommonStatus()); + + // 调用 + Long categoryId = categoryService.createCategory(createReqVO); + // 断言 + assertNotNull(categoryId); + // 校验记录的属性是否正确 + BpmCategoryDO category = categoryMapper.selectById(categoryId); + assertPojoEquals(createReqVO, category, "id"); + } + + @Test + public void testUpdateCategory_success() { + // mock 数据 + BpmCategoryDO dbCategory = randomPojo(BpmCategoryDO.class); + categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据 + // 准备参数 + BpmCategorySaveReqVO updateReqVO = randomPojo(BpmCategorySaveReqVO.class, o -> { + o.setId(dbCategory.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + }); + + // 调用 + categoryService.updateCategory(updateReqVO); + // 校验是否更新正确 + BpmCategoryDO category = categoryMapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, category); + } + + @Test + public void testUpdateCategory_notExists() { + // 准备参数 + BpmCategorySaveReqVO updateReqVO = randomPojo(BpmCategorySaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> categoryService.updateCategory(updateReqVO), CATEGORY_NOT_EXISTS); + } + + @Test + public void testDeleteCategory_success() { + // mock 数据 + BpmCategoryDO dbCategory = randomPojo(BpmCategoryDO.class); + categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbCategory.getId(); + + // 调用 + categoryService.deleteCategory(id); + // 校验数据不存在了 + assertNull(categoryMapper.selectById(id)); + } + + @Test + public void testDeleteCategory_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> categoryService.deleteCategory(id), CATEGORY_NOT_EXISTS); + } + + @Test + public void testGetCategoryPage() { + // mock 数据 + BpmCategoryDO dbCategory = randomPojo(BpmCategoryDO.class, o -> { // 等会查询到 + o.setName("芋头"); + o.setCode("xiaodun"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2023, 2, 2)); + }); + categoryMapper.insert(dbCategory); + // 测试 name 不匹配 + categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName("小盾"))); + // 测试 code 不匹配 + categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setCode("tudou"))); + // 测试 status 不匹配 + categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setCreateTime(buildTime(2024, 2, 2)))); + // 准备参数 + BpmCategoryPageReqVO reqVO = new BpmCategoryPageReqVO(); + reqVO.setName("芋"); + reqVO.setCode("xiao"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = categoryService.getCategoryPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbCategory, pageResult.getList().get(0)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceTest.java new file mode 100644 index 0000000..93281a4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceTest.java @@ -0,0 +1,144 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormSaveReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmFormMapper; +import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmFormFieldRespDTO; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import jakarta.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.FORM_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link BpmFormServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(BpmFormServiceImpl.class) +public class BpmFormServiceTest extends BaseDbUnitTest { + + @Resource + private BpmFormServiceImpl formService; + + @Resource + private BpmFormMapper formMapper; + + @Test + public void testCreateForm_success() { + // 准备参数 + BpmFormSaveReqVO reqVO = randomPojo(BpmFormSaveReqVO.class, o -> { + o.setConf("{}"); + o.setFields(randomFields()); + }); + + // 调用 + Long formId = formService.createForm(reqVO); + // 断言 + assertNotNull(formId); + // 校验记录的属性是否正确 + BpmFormDO form = formMapper.selectById(formId); + assertPojoEquals(reqVO, form); + } + + @Test + public void testUpdateForm_success() { + // mock 数据 + BpmFormDO dbForm = randomPojo(BpmFormDO.class, o -> { + o.setConf("{}"); + o.setFields(randomFields()); + }); + formMapper.insert(dbForm);// @Sql: 先插入出一条存在的数据 + // 准备参数 + BpmFormSaveReqVO reqVO = randomPojo(BpmFormSaveReqVO.class, o -> { + o.setId(dbForm.getId()); // 设置更新的 ID + o.setConf("{'yudao': 'yuanma'}"); + o.setFields(randomFields()); + }); + + // 调用 + formService.updateForm(reqVO); + // 校验是否更新正确 + BpmFormDO form = formMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, form); + } + + @Test + public void testUpdateForm_notExists() { + // 准备参数 + BpmFormSaveReqVO reqVO = randomPojo(BpmFormSaveReqVO.class, o -> { + o.setConf("{'yudao': 'yuanma'}"); + o.setFields(randomFields()); + }); + + // 调用, 并断言异常 + assertServiceException(() -> formService.updateForm(reqVO), FORM_NOT_EXISTS); + } + + @Test + public void testDeleteForm_success() { + // mock 数据 + BpmFormDO dbForm = randomPojo(BpmFormDO.class); + formMapper.insert(dbForm);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbForm.getId(); + + // 调用 + formService.deleteForm(id); + // 校验数据不存在了 + assertNull(formMapper.selectById(id)); + } + + @Test + public void testDeleteForm_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> formService.deleteForm(id), FORM_NOT_EXISTS); + } + + @Test + public void testGetFormPage() { + // mock 数据 + BpmFormDO dbForm = randomPojo(BpmFormDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + }); + formMapper.insert(dbForm); + // 测试 name 不匹配 + formMapper.insert(cloneIgnoreId(dbForm, o -> o.setName("源码"))); + // 准备参数 + BpmFormPageReqVO reqVO = new BpmFormPageReqVO(); + reqVO.setName("芋道"); + + // 调用 + PageResult pageResult = formService.getFormPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbForm, pageResult.getList().get(0)); + } + + private List randomFields() { + int size = RandomUtil.randomInt(1, 3); + return Stream.iterate(0, i -> i).limit(size) + .map(i -> JsonUtils.toJsonString(randomPojo(BpmFormFieldRespDTO.class))) + .collect(Collectors.toList()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceTest.java b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceTest.java new file mode 100644 index 0000000..cf119f2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceTest.java @@ -0,0 +1,130 @@ +package cn.iocoder.yudao.module.bpm.service.definition; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.framework.test.core.util.AssertUtils; +import cn.iocoder.yudao.framework.test.core.util.RandomUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupSaveReqVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmUserGroupMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import jakarta.annotation.Resource; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.USER_GROUP_NOT_EXISTS; + +/** + * {@link BpmUserGroupServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(BpmUserGroupServiceImpl.class) +public class BpmUserGroupServiceTest extends BaseDbUnitTest { + + @Resource + private BpmUserGroupServiceImpl userGroupService; + + @Resource + private BpmUserGroupMapper userGroupMapper; + + @Test + public void testCreateUserGroup_success() { + // 准备参数 + BpmUserGroupSaveReqVO reqVO = RandomUtils.randomPojo(BpmUserGroupSaveReqVO.class); + + // 调用 + Long userGroupId = userGroupService.createUserGroup(reqVO); + // 断言 + Assertions.assertNotNull(userGroupId); + // 校验记录的属性是否正确 + BpmUserGroupDO userGroup = userGroupMapper.selectById(userGroupId); + AssertUtils.assertPojoEquals(reqVO, userGroup); + } + + @Test + public void testUpdateUserGroup_success() { + // mock 数据 + BpmUserGroupDO dbUserGroup = RandomUtils.randomPojo(BpmUserGroupDO.class); + userGroupMapper.insert(dbUserGroup);// @Sql: 先插入出一条存在的数据 + // 准备参数 + BpmUserGroupSaveReqVO reqVO = RandomUtils.randomPojo(BpmUserGroupSaveReqVO.class, o -> { + o.setId(dbUserGroup.getId()); // 设置更新的 ID + }); + + // 调用 + userGroupService.updateUserGroup(reqVO); + // 校验是否更新正确 + BpmUserGroupDO userGroup = userGroupMapper.selectById(reqVO.getId()); // 获取最新的 + AssertUtils.assertPojoEquals(reqVO, userGroup); + } + + @Test + public void testUpdateUserGroup_notExists() { + // 准备参数 + BpmUserGroupSaveReqVO reqVO = RandomUtils.randomPojo(BpmUserGroupSaveReqVO.class); + + // 调用, 并断言异常 + AssertUtils.assertServiceException(() -> userGroupService.updateUserGroup(reqVO), USER_GROUP_NOT_EXISTS); + } + + @Test + public void testDeleteUserGroup_success() { + // mock 数据 + BpmUserGroupDO dbUserGroup = RandomUtils.randomPojo(BpmUserGroupDO.class); + userGroupMapper.insert(dbUserGroup);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbUserGroup.getId(); + + // 调用 + userGroupService.deleteUserGroup(id); + // 校验数据不存在了 + Assertions.assertNull(userGroupMapper.selectById(id)); + } + + @Test + public void testDeleteUserGroup_notExists() { + // 准备参数 + Long id = RandomUtils.randomLongId(); + + // 调用, 并断言异常 + AssertUtils.assertServiceException(() -> userGroupService.deleteUserGroup(id), USER_GROUP_NOT_EXISTS); + } + + @Test + public void testGetUserGroupPage() { + // mock 数据 + BpmUserGroupDO dbUserGroup = RandomUtils.randomPojo(BpmUserGroupDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + userGroupMapper.insert(dbUserGroup); + // 测试 name 不匹配 + userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setName("芋道"))); + // 测试 status 不匹配 + userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + BpmUserGroupPageReqVO reqVO = new BpmUserGroupPageReqVO(); + reqVO.setName("源码"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime((new LocalDateTime[]{buildTime(2021, 11, 10),buildTime(2021, 11, 12)})); + + // 调用 + PageResult pageResult = userGroupService.getUserGroupPage(reqVO); + // 断言 + Assertions.assertEquals(1, pageResult.getTotal()); + Assertions.assertEquals(1, pageResult.getList().size()); + AssertUtils.assertPojoEquals(dbUserGroup, pageResult.getList().get(0)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/application-unit-test.yaml b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..dd9b95b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/application-unit-test.yaml @@ -0,0 +1,42 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + +mybatis-plus: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + info: + base-package: cn.iocoder.yudao.module diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/logback.xml b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/sql/clean.sql b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..d4f93bb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/sql/clean.sql @@ -0,0 +1,3 @@ +DELETE FROM "bpm_form"; +DELETE FROM "bpm_user_group"; +DELETE FROM "bpm_category"; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/sql/create_tables.sql b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..1034962 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-bpm/src/test/resources/sql/create_tables.sql @@ -0,0 +1,43 @@ +CREATE TABLE IF NOT EXISTS "bpm_user_group" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "description" varchar(255) NOT NULL, + "status" tinyint NOT NULL, + "user_ids" varchar(255) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '用户组'; + +CREATE TABLE IF NOT EXISTS "bpm_category" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "code" varchar(63) NOT NULL, + "description" varchar(255) NOT NULL, + "status" tinyint NOT NULL, + "sort" int NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '分类'; + +CREATE TABLE IF NOT EXISTS "bpm_form" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "status" tinyint NOT NULL, + "fields" varchar(255) NOT NULL, + "conf" varchar(255) NOT NULL, + "remark" varchar(255), + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '动态表单'; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/pom.xml b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/pom.xml new file mode 100644 index 0000000..8b983cf --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/pom.xml @@ -0,0 +1,119 @@ + + + + cn.iocoder.boot + yudao + ${revision} + + 4.0.0 + yudao-module-infra + jar + + ${project.artifactId} + + infra 模块,主要提供两块能力: + 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + + + + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + + + + cn.iocoder.boot + yudao-spring-boot-starter-websocket + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + + + com.baomidou + mybatis-plus-generator + + + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + + + + + + cn.iocoder.boot + yudao-spring-boot-starter-job + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mq + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + + cn.iocoder.boot + yudao-spring-boot-starter-excel + + + + org.apache.velocity + velocity-engine-core + + + + + cn.iocoder.boot + yudao-spring-boot-starter-monitor + + + + de.codecentric + spring-boot-admin-starter-server + + + + + commons-net + commons-net + + + com.jcraft + jsch + + + + software.amazon.awssdk + s3 + + + + org.apache.tika + tika-core + + + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/config/ConfigApi.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/config/ConfigApi.java new file mode 100644 index 0000000..85de49e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/config/ConfigApi.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.infra.api.config; + +/** + * 参数配置 API 接口 + * + * @author 芋道源码 + */ +public interface ConfigApi { + + /** + * 根据参数键查询参数值 + * + * @param key 参数键 + * @return 参数值 + */ + String getConfigValueByKey(String key); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/config/ConfigApiImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/config/ConfigApiImpl.java new file mode 100644 index 0000000..d613fe4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/config/ConfigApiImpl.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.api.config; + +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import cn.iocoder.yudao.module.infra.service.config.ConfigService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * 参数配置 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ConfigApiImpl implements ConfigApi { + + @Resource + private ConfigService configService; + + @Override + public String getConfigValueByKey(String key) { + ConfigDO config = configService.getConfigByKey(key); + return config != null ? config.getValue() : null; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java new file mode 100644 index 0000000..e97d11b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.infra.api.file; + +import jakarta.validation.constraints.NotEmpty; + +/** + * 文件 API 接口 + * + * @author 芋道源码 + */ +public interface FileApi { + + /** + * 保存文件,并返回文件的访问路径 + * + * @param content 文件内容 + * @return 文件路径 + */ + default String createFile(byte[] content) { + return createFile(content, null, null, null); + } + + /** + * 保存文件,并返回文件的访问路径 + * + * @param content 文件内容 + * @param name 文件名称,允许空 + * @return 文件路径 + */ + default String createFile(byte[] content, String name) { + return createFile(content, name, null, null); + } + + /** + * 保存文件,并返回文件的访问路径 + * + * @param content 文件内容 + * @param name 文件名称,允许空 + * @param directory 目录,允许空 + * @param type 文件的 MIME 类型,允许空 + * @return 文件路径 + */ + String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content, + String name, String directory, String type); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java new file mode 100644 index 0000000..72c3511 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.infra.api.file; + +import cn.iocoder.yudao.module.infra.service.file.FileService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * 文件 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FileApiImpl implements FileApi { + + @Resource + private FileService fileService; + + @Override + public String createFile(byte[] content, String name, String directory, String type) { + return fileService.createFile(content, name, directory, type); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApiImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApiImpl.java new file mode 100644 index 0000000..69f8062 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiAccessLogApiImpl.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.api.logger; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiAccessLogCommonApi; +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO; +import cn.iocoder.yudao.module.infra.service.logger.ApiAccessLogService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * API 访问日志的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiAccessLogApiImpl implements ApiAccessLogCommonApi { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @Override + public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { + apiAccessLogService.createApiAccessLog(createDTO); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApiImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApiImpl.java new file mode 100644 index 0000000..f782768 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/logger/ApiErrorLogApiImpl.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.infra.api.logger; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO; +import cn.iocoder.yudao.module.infra.service.logger.ApiErrorLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; + +/** + * API 访问日志的 API 接口 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiErrorLogApiImpl implements ApiErrorLogCommonApi { + + @Resource + private ApiErrorLogService apiErrorLogService; + + @Override + public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { + apiErrorLogService.createApiErrorLog(createDTO); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/package-info.java new file mode 100644 index 0000000..9831fe7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/package-info.java @@ -0,0 +1,4 @@ +/** + * infra API 包,定义并实现提供给其它模块的 API + */ +package cn.iocoder.yudao.module.infra.api; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApi.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApi.java new file mode 100644 index 0000000..38582c2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApi.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.infra.api.websocket; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; + +/** + * WebSocket 发送器的 API 接口 + * + * 对 WebSocketMessageSender 进行封装,提供给其它模块使用 + * + * @author 芋道源码 + */ +public interface WebSocketSenderApi { + + /** + * 发送消息给指定用户 + * + * @param userType 用户类型 + * @param userId 用户编号 + * @param messageType 消息类型 + * @param messageContent 消息内容,JSON 格式 + */ + void send(Integer userType, Long userId, String messageType, String messageContent); + + /** + * 发送消息给指定用户类型 + * + * @param userType 用户类型 + * @param messageType 消息类型 + * @param messageContent 消息内容,JSON 格式 + */ + void send(Integer userType, String messageType, String messageContent); + + /** + * 发送消息给指定 Session + * + * @param sessionId Session 编号 + * @param messageType 消息类型 + * @param messageContent 消息内容,JSON 格式 + */ + void send(String sessionId, String messageType, String messageContent); + + default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) { + send(userType, userId, messageType, JsonUtils.toJsonString(messageContent)); + } + + default void sendObject(Integer userType, String messageType, Object messageContent) { + send(userType, messageType, JsonUtils.toJsonString(messageContent)); + } + + default void sendObject(String sessionId, String messageType, Object messageContent) { + send(sessionId, messageType, JsonUtils.toJsonString(messageContent)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java new file mode 100644 index 0000000..cc060aa --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.api.websocket; + +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * WebSocket 发送器的 API 实现类 + * + * @author 芋道源码 + */ +@Component +public class WebSocketSenderApiImpl implements WebSocketSenderApi { + + @Resource + private WebSocketMessageSender webSocketMessageSender; + + @Override + public void send(Integer userType, Long userId, String messageType, String messageContent) { + webSocketMessageSender.send(userType, userId, messageType, messageContent); + } + + @Override + public void send(Integer userType, String messageType, String messageContent) { + webSocketMessageSender.send(userType, messageType, messageContent); + } + + @Override + public void send(String sessionId, String messageType, String messageContent) { + webSocketMessageSender.send(sessionId, messageType, messageContent); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java new file mode 100644 index 0000000..26dda89 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ZipUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import cn.iocoder.yudao.module.infra.convert.codegen.CodegenConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import cn.iocoder.yudao.module.infra.service.codegen.CodegenService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserNickname; +import static cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment; + +@Tag(name = "管理后台 - 代码生成器") +@RestController +@RequestMapping("/infra/codegen") +@Validated +public class CodegenController { + + @Resource + private CodegenService codegenService; + + @GetMapping("/db/table/list") + @Operation(summary = "获得数据库自带的表定义列表", description = "会过滤掉已经导入 Codegen 的表") + @Parameters({ + @Parameter(name = "dataSourceConfigId", description = "数据源配置的编号", required = true, example = "1"), + @Parameter(name = "name", description = "表名,模糊匹配", example = "yudao"), + @Parameter(name = "comment", description = "描述,模糊匹配", example = "芋道") + }) + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult> getDatabaseTableList( + @RequestParam(value = "dataSourceConfigId") Long dataSourceConfigId, + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "comment", required = false) String comment) { + return success(codegenService.getDatabaseTableList(dataSourceConfigId, name, comment)); + } + + @GetMapping("/table/list") + @Operation(summary = "获得表定义列表") + @Parameter(name = "dataSourceConfigId", description = "数据源配置的编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult> getCodegenTableList(@RequestParam(value = "dataSourceConfigId") Long dataSourceConfigId) { + List list = codegenService.getCodegenTableList(dataSourceConfigId); + return success(BeanUtils.toBean(list, CodegenTableRespVO.class)); + } + + @GetMapping("/table/page") + @Operation(summary = "获得表定义分页") + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult> getCodegenTablePage(@Valid CodegenTablePageReqVO pageReqVO) { + PageResult pageResult = codegenService.getCodegenTablePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, CodegenTableRespVO.class)); + } + + @GetMapping("/detail") + @Operation(summary = "获得表和字段的明细") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult getCodegenDetail(@RequestParam("tableId") Long tableId) { + CodegenTableDO table = codegenService.getCodegenTable(tableId); + List columns = codegenService.getCodegenColumnListByTableId(tableId); + // 拼装返回 + return success(CodegenConvert.INSTANCE.convert(table, columns)); + } + + @Operation(summary = "基于数据库的表结构,创建代码生成器的表和字段定义") + @PostMapping("/create-list") + @PreAuthorize("@ss.hasPermission('infra:codegen:create')") + public CommonResult> createCodegenList(@Valid @RequestBody CodegenCreateListReqVO reqVO) { + return success(codegenService.createCodegenList(getLoginUserNickname(), reqVO)); + } + + @Operation(summary = "更新数据库的表和字段定义") + @PutMapping("/update") + @PreAuthorize("@ss.hasPermission('infra:codegen:update')") + public CommonResult updateCodegen(@Valid @RequestBody CodegenUpdateReqVO updateReqVO) { + codegenService.updateCodegen(updateReqVO); + return success(true); + } + + @Operation(summary = "基于数据库的表结构,同步数据库的表和字段定义") + @PutMapping("/sync-from-db") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:update')") + public CommonResult syncCodegenFromDB(@RequestParam("tableId") Long tableId) { + codegenService.syncCodegenFromDB(tableId); + return success(true); + } + + @Operation(summary = "删除数据库的表和字段定义") + @DeleteMapping("/delete") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:delete')") + public CommonResult deleteCodegen(@RequestParam("tableId") Long tableId) { + codegenService.deleteCodegen(tableId); + return success(true); + } + + @Operation(summary = "批量删除数据库的表和字段定义") + @DeleteMapping("/delete-list") + @Parameter(name = "tableIds", description = "表编号列表", required = true) + @PreAuthorize("@ss.hasPermission('infra:codegen:delete')") + public CommonResult deleteCodegenList(@RequestParam("tableIds") List tableIds) { + codegenService.deleteCodegenList(tableIds); + return success(true); + } + + @Operation(summary = "预览生成代码") + @GetMapping("/preview") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:preview')") + public CommonResult> previewCodegen(@RequestParam("tableId") Long tableId) { + Map codes = codegenService.generationCodes(tableId); + return success(CodegenConvert.INSTANCE.convert(codes)); + } + + @Operation(summary = "下载生成代码") + @GetMapping("/download") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:download')") + public void downloadCodegen(@RequestParam("tableId") Long tableId, + HttpServletResponse response) throws IOException { + // 生成代码 + Map codes = codegenService.generationCodes(tableId); + // 构建 zip 包 + String[] paths = codes.keySet().toArray(new String[0]); + ByteArrayInputStream[] ins = codes.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipUtil.zip(outputStream, paths, ins); + // 输出 + writeAttachment(response, "codegen.zip", outputStream.toByteArray()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java new file mode 100644 index 0000000..cd00e8a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 基于数据库的表结构,创建代码生成器的表和字段定义 Request VO") +@Data +public class CodegenCreateListReqVO { + + @Schema(description = "数据源配置的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数据源配置的编号不能为空") + private Long dataSourceConfigId; + + @Schema(description = "表名数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3]") + @NotNull(message = "表名数组不能为空") + private List tableNames; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java new file mode 100644 index 0000000..f7d8a62 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 代码生成表和字段的明细 Response VO") +@Data +public class CodegenDetailRespVO { + + @Schema(description = "表定义") + private CodegenTableRespVO table; + + @Schema(description = "字段定义") + private List columns; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java new file mode 100644 index 0000000..ffd924a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 代码生成预览 Response VO,注意,每个文件都是一个该对象") +@Data +public class CodegenPreviewRespVO { + + @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "java/cn/iocoder/yudao/adminserver/modules/system/controller/test/SysTestDemoController.java") + private String filePath; + + @Schema(description = "代码", requiredMode = Schema.RequiredMode.REQUIRED, example = "Hello World") + private String code; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java new file mode 100644 index 0000000..d381ec6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column.CodegenColumnSaveReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableSaveReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 代码生成表和字段的修改 Request VO") +@Data +public class CodegenUpdateReqVO { + + @Valid // 校验内嵌的字段 + @NotNull(message = "表定义不能为空") + private CodegenTableSaveReqVO table; + + @Valid // 校验内嵌的字段 + @NotNull(message = "字段定义不能为空") + private List columns; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java new file mode 100644 index 0000000..29a8c74 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 代码生成字段定义 Response VO") +@Data +public class CodegenColumnRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long tableId; + + @Schema(description = "字段名", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_age") + private String columnName; + + @Schema(description = "字段类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "int(11)") + private String dataType; + + @Schema(description = "字段描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "年龄") + private String columnComment; + + @Schema(description = "是否允许为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean nullable; + + @Schema(description = "是否主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean primaryKey; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer ordinalPosition; + + @Schema(description = "Java 属性类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "userAge") + private String javaType; + + @Schema(description = "Java 属性名", requiredMode = Schema.RequiredMode.REQUIRED, example = "Integer") + private String javaField; + + @Schema(description = "字典类型", example = "sys_gender") + private String dictType; + + @Schema(description = "数据示例", example = "1024") + private String example; + + @Schema(description = "是否为 Create 创建操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean createOperation; + + @Schema(description = "是否为 Update 更新操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean updateOperation; + + @Schema(description = "是否为 List 查询操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean listOperation; + + @Schema(description = "List 查询操作的条件类型,参见 CodegenColumnListConditionEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "LIKE") + private String listOperationCondition; + + @Schema(description = "是否为 List 查询操作的返回字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean listOperationResult; + + @Schema(description = "显示类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "input") + private String htmlType; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnSaveReqVO.java new file mode 100644 index 0000000..9579d79 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/column/CodegenColumnSaveReqVO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 代码生成字段定义创建/修改 Request VO") +@Data +public class CodegenColumnSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "表编号不能为空") + private Long tableId; + + @Schema(description = "字段名", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_age") + @NotNull(message = "字段名不能为空") + private String columnName; + + @Schema(description = "字段类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "int(11)") + @NotNull(message = "字段类型不能为空") + private String dataType; + + @Schema(description = "字段描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "年龄") + @NotNull(message = "字段描述不能为空") + private String columnComment; + + @Schema(description = "是否允许为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否允许为空不能为空") + private Boolean nullable; + + @Schema(description = "是否主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否主键不能为空") + private Boolean primaryKey; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "排序不能为空") + private Integer ordinalPosition; + + @Schema(description = "Java 属性类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "userAge") + @NotNull(message = "Java 属性类型不能为空") + private String javaType; + + @Schema(description = "Java 属性名", requiredMode = Schema.RequiredMode.REQUIRED, example = "Integer") + @NotNull(message = "Java 属性名不能为空") + private String javaField; + + @Schema(description = "字典类型", example = "sys_gender") + private String dictType; + + @Schema(description = "数据示例", example = "1024") + private String example; + + @Schema(description = "是否为 Create 创建操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否为 Create 创建操作的字段不能为空") + private Boolean createOperation; + + @Schema(description = "是否为 Update 更新操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否为 Update 更新操作的字段不能为空") + private Boolean updateOperation; + + @Schema(description = "是否为 List 查询操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否为 List 查询操作的字段不能为空") + private Boolean listOperation; + + @Schema(description = "List 查询操作的条件类型,参见 CodegenColumnListConditionEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "LIKE") + @NotNull(message = "List 查询操作的条件类型不能为空") + private String listOperationCondition; + + @Schema(description = "是否为 List 查询操作的返回字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否为 List 查询操作的返回字段不能为空") + private Boolean listOperationResult; + + @Schema(description = "显示类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "input") + @NotNull(message = "显示类型不能为空") + private String htmlType; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java new file mode 100644 index 0000000..7bd26d6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 表定义分页 Request VO") +@Data +public class CodegenTablePageReqVO extends PageParam { + + @Schema(description = "表名称,模糊匹配", example = "yudao") + private String tableName; + + @Schema(description = "表描述,模糊匹配", example = "芋道") + private String tableComment; + + @Schema(description = "实体,模糊匹配", example = "Yudao") + private String className; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java new file mode 100644 index 0000000..8b438b2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 代码生成表定义 Response VO") +@Data +public class CodegenTableRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "生成场景,参见 CodegenSceneEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer scene; + + @Schema(description = "表名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + private String tableName; + + @Schema(description = "表描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String tableComment; + + @Schema(description = "备注", example = "我是备注") + private String remark; + + @Schema(description = "模块名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system") + private String moduleName; + + @Schema(description = "业务名", requiredMode = Schema.RequiredMode.REQUIRED, example = "codegen") + private String businessName; + + @Schema(description = "类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "CodegenTable") + private String className; + + @Schema(description = "类描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "代码生成器的表定义") + private String classComment; + + @Schema(description = "作者", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String author; + + @Schema(description = "模板类型,参见 CodegenTemplateTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer templateType; + + @Schema(description = "前端类型,参见 CodegenFrontTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer frontType; + + @Schema(description = "父菜单编号", example = "1024") + private Long parentMenuId; + + @Schema(description = "主表的编号", example = "2048") + private Long masterTableId; + @Schema(description = "子表关联主表的字段编号", example = "4096") + private Long subJoinColumnId; + @Schema(description = "主表与子表是否一对多", example = "4096") + private Boolean subJoinMany; + + @Schema(description = "树表的父字段编号", example = "8192") + private Long treeParentColumnId; + @Schema(description = "树表的名字字段编号", example = "16384") + private Long treeNameColumnId; + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer dataSourceConfigId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableSaveReqVO.java new file mode 100644 index 0000000..5f12e3d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableSaveReqVO.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 代码生成表定义创建/修改 Response VO") +@Data +public class CodegenTableSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "生成场景,参见 CodegenSceneEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "导入类型不能为空") + private Integer scene; + + @Schema(description = "表名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + @NotNull(message = "表名称不能为空") + private String tableName; + + @Schema(description = "表描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "表描述不能为空") + private String tableComment; + + @Schema(description = "备注", example = "我是备注") + private String remark; + + @Schema(description = "模块名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system") + @NotNull(message = "模块名不能为空") + private String moduleName; + + @Schema(description = "业务名", requiredMode = Schema.RequiredMode.REQUIRED, example = "codegen") + @NotNull(message = "业务名不能为空") + private String businessName; + + @Schema(description = "类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "CodegenTable") + @NotNull(message = "类名称不能为空") + private String className; + + @Schema(description = "类描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "代码生成器的表定义") + @NotNull(message = "类描述不能为空") + private String classComment; + + @Schema(description = "作者", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + @NotNull(message = "作者不能为空") + private String author; + + @Schema(description = "模板类型,参见 CodegenTemplateTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模板类型不能为空") + private Integer templateType; + + @Schema(description = "前端类型,参见 CodegenFrontTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + @NotNull(message = "前端类型不能为空") + private Integer frontType; + + @Schema(description = "父菜单编号", example = "1024") + private Long parentMenuId; + + @Schema(description = "主表的编号", example = "2048") + private Long masterTableId; + @Schema(description = "子表关联主表的字段编号", example = "4096") + private Long subJoinColumnId; + @Schema(description = "主表与子表是否一对多", example = "4096") + private Boolean subJoinMany; + + @Schema(description = "树表的父字段编号", example = "8192") + private Long treeParentColumnId; + @Schema(description = "树表的名字字段编号", example = "16384") + private Long treeNameColumnId; + + @AssertTrue(message = "上级菜单不能为空,请前往 [修改生成配置 -> 生成信息] 界面,设置“上级菜单”字段") + @JsonIgnore + public boolean isParentMenuIdValid() { + // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的 + return ObjectUtil.notEqual(getScene(), CodegenSceneEnum.ADMIN.getScene()) + || getParentMenuId() != null; + } + + @AssertTrue(message = "关联的父表信息不全") + @JsonIgnore + public boolean isSubValid() { + return ObjectUtil.notEqual(getTemplateType(), CodegenTemplateTypeEnum.SUB) + || (ObjectUtil.isAllNotEmpty(masterTableId, subJoinColumnId, subJoinMany)); + } + + @AssertTrue(message = "关联的树表信息不全") + @JsonIgnore + public boolean isTreeValid() { + return ObjectUtil.notEqual(templateType, CodegenTemplateTypeEnum.TREE) + || (ObjectUtil.isAllNotEmpty(treeParentColumnId, treeNameColumnId)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java new file mode 100644 index 0000000..a1f683c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 数据库的表定义 Response VO") +@Data +public class DatabaseTableRespVO { + + @Schema(description = "表名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yuanma") + private String name; + + @Schema(description = "表描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String comment; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java new file mode 100644 index 0000000..af3973f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.convert.config.ConfigConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.infra.service.config.ConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 参数配置") +@RestController +@RequestMapping("/infra/config") +@Validated +public class ConfigController { + + @Resource + private ConfigService configService; + + @PostMapping("/create") + @Operation(summary = "创建参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:create')") + public CommonResult createConfig(@Valid @RequestBody ConfigSaveReqVO createReqVO) { + return success(configService.createConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:update')") + public CommonResult updateConfig(@Valid @RequestBody ConfigSaveReqVO updateReqVO) { + configService.updateConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除参数配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:config:delete')") + public CommonResult deleteConfig(@RequestParam("id") Long id) { + configService.deleteConfig(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除参数配置") + @Parameter(name = "ids", description = "编号列表", required = true) + @PreAuthorize("@ss.hasPermission('infra:config:delete')") + public CommonResult deleteConfigList(@RequestParam("ids") List ids) { + configService.deleteConfigList(ids); + return success(true); + } + + @GetMapping(value = "/get") + @Operation(summary = "获得参数配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:config:query')") + public CommonResult getConfig(@RequestParam("id") Long id) { + return success(ConfigConvert.INSTANCE.convert(configService.getConfig(id))); + } + + @GetMapping(value = "/get-value-by-key") + @Operation(summary = "根据参数键名查询参数值", description = "不可见的配置,不允许返回给前端") + @Parameter(name = "key", description = "参数键", required = true, example = "yunai.biz.username") + public CommonResult getConfigKey(@RequestParam("key") String key) { + ConfigDO config = configService.getConfigByKey(key); + if (config == null) { + return success(null); + } + if (!config.getVisible()) { + throw exception(ErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_VISIBLE); + } + return success(config.getValue()); + } + + @GetMapping("/page") + @Operation(summary = "获取参数配置分页") + @PreAuthorize("@ss.hasPermission('infra:config:query')") + public CommonResult> getConfigPage(@Valid ConfigPageReqVO pageReqVO) { + PageResult page = configService.getConfigPage(pageReqVO); + return success(ConfigConvert.INSTANCE.convertPage(page)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportConfig(ConfigPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = configService.getConfigPage(exportReqVO).getList(); + // 输出 + ExcelUtils.write(response, "参数配置.xls", "数据", ConfigRespVO.class, + ConfigConvert.INSTANCE.convertList(list)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigPageReqVO.java new file mode 100644 index 0000000..1a8a139 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigPageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 参数配置分页 Request VO") +@Data +public class ConfigPageReqVO extends PageParam { + + @Schema(description = "数据源名称,模糊匹配", example = "名称") + private String name; + + @Schema(description = "参数键名,模糊匹配", example = "yunai.db.username") + private String key; + + @Schema(description = "参数类型,参见 SysConfigTypeEnum 枚举", example = "1") + private Integer type; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java new file mode 100644 index 0000000..b4f642f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 参数配置信息 Response VO") +@Data +@ExcelIgnoreUnannotated +public class ConfigRespVO { + + @Schema(description = "参数配置序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("参数配置序号") + private Long id; + + @Schema(description = "参数分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "biz") + @ExcelProperty("参数分类") + private String category; + + @Schema(description = "参数名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "数据库名") + @ExcelProperty("参数名称") + private String name; + + @Schema(description = "参数键名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunai.db.username") + @ExcelProperty("参数键名") + private String key; + + @Schema(description = "参数键值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("参数键值") + private String value; + + @Schema(description = "参数类型,参见 SysConfigTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "参数类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CONFIG_TYPE) + private Integer type; + + @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @ExcelProperty(value = "是否可见", converter = DictConvert.class) + @DictFormat(DictTypeConstants.BOOLEAN_STRING) + private Boolean visible; + + @Schema(description = "备注", example = "备注一下很帅气!") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigSaveReqVO.java new file mode 100644 index 0000000..bbde578 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigSaveReqVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.infra.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - 参数配置创建/修改 Request VO") +@Data +public class ConfigSaveReqVO { + + @Schema(description = "参数配置序号", example = "1024") + private Long id; + + @Schema(description = "参数分组", requiredMode = Schema.RequiredMode.REQUIRED, example = "biz") + @NotEmpty(message = "参数分组不能为空") + @Size(max = 50, message = "参数名称不能超过 50 个字符") + private String category; + + @Schema(description = "参数名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "数据库名") + @NotBlank(message = "参数名称不能为空") + @Size(max = 100, message = "参数名称不能超过 100 个字符") + private String name; + + @Schema(description = "参数键名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunai.db.username") + @NotBlank(message = "参数键名长度不能为空") + @Size(max = 100, message = "参数键名长度不能超过 100 个字符") + private String key; + + @Schema(description = "参数键值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotBlank(message = "参数键值不能为空") + @Size(max = 500, message = "参数键值长度不能超过 500 个字符") + private String value; + + @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否可见不能为空") + private Boolean visible; + + @Schema(description = "备注", example = "备注一下很帅气!") + private String remark; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/DataSourceConfigController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/DataSourceConfigController.java new file mode 100644 index 0000000..a5e9737 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/DataSourceConfigController.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.infra.controller.admin.db; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import cn.iocoder.yudao.module.infra.service.db.DataSourceConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 数据源配置") +@RestController +@RequestMapping("/infra/data-source-config") +@Validated +public class DataSourceConfigController { + + @Resource + private DataSourceConfigService dataSourceConfigService; + + @PostMapping("/create") + @Operation(summary = "创建数据源配置") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:create')") + public CommonResult createDataSourceConfig(@Valid @RequestBody DataSourceConfigSaveReqVO createReqVO) { + return success(dataSourceConfigService.createDataSourceConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新数据源配置") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:update')") + public CommonResult updateDataSourceConfig(@Valid @RequestBody DataSourceConfigSaveReqVO updateReqVO) { + dataSourceConfigService.updateDataSourceConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除数据源配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:data-source-config:delete')") + public CommonResult deleteDataSourceConfig(@RequestParam("id") Long id) { + dataSourceConfigService.deleteDataSourceConfig(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除数据源配置") + @Parameter(name = "ids", description = "编号列表", required = true) + @PreAuthorize("@ss.hasPermission('infra:data-source-config:delete')") + public CommonResult deleteDataSourceConfigList(@RequestParam("ids") List ids) { + dataSourceConfigService.deleteDataSourceConfigList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得数据源配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:query')") + public CommonResult getDataSourceConfig(@RequestParam("id") Long id) { + DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(id); + return success(BeanUtils.toBean(config, DataSourceConfigRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得数据源配置列表") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:query')") + public CommonResult> getDataSourceConfigList() { + List list = dataSourceConfigService.getDataSourceConfigList(); + return success(BeanUtils.toBean(list, DataSourceConfigRespVO.class)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java new file mode 100644 index 0000000..def8ce0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.controller.admin.db.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 数据源配置 Response VO") +@Data +public class DataSourceConfigRespVO { + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "数据源名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "test") + private String name; + + @Schema(description = "数据源连接", requiredMode = Schema.RequiredMode.REQUIRED, example = "jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro") + private String url; + + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "root") + private String username; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigSaveReqVO.java new file mode 100644 index 0000000..def5eef --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/db/vo/DataSourceConfigSaveReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.controller.admin.db.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 数据源配置创建/修改 Request VO") +@Data +public class DataSourceConfigSaveReqVO { + + @Schema(description = "主键编号", example = "1024") + private Long id; + + @Schema(description = "数据源名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "test") + @NotNull(message = "数据源名称不能为空") + private String name; + + @Schema(description = "数据源连接", requiredMode = Schema.RequiredMode.REQUIRED, example = "jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro") + @NotNull(message = "数据源连接不能为空") + private String url; + + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "root") + @NotNull(message = "用户名不能为空") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotNull(message = "密码不能为空") + private String password; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/Demo01ContactController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/Demo01ContactController.java new file mode 100644 index 0000000..2cfde80 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/Demo01ContactController.java @@ -0,0 +1,102 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; +import cn.iocoder.yudao.module.infra.service.demo.demo01.Demo01ContactService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 示例联系人") +@RestController +@RequestMapping("/infra/demo01-contact") +@Validated +public class Demo01ContactController { + + @Resource + private Demo01ContactService demo01ContactService; + + @PostMapping("/create") + @Operation(summary = "创建示例联系人") + @PreAuthorize("@ss.hasPermission('infra:demo01-contact:create')") + public CommonResult createDemo01Contact(@Valid @RequestBody Demo01ContactSaveReqVO createReqVO) { + return success(demo01ContactService.createDemo01Contact(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新示例联系人") + @PreAuthorize("@ss.hasPermission('infra:demo01-contact:update')") + public CommonResult updateDemo01Contact(@Valid @RequestBody Demo01ContactSaveReqVO updateReqVO) { + demo01ContactService.updateDemo01Contact(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除示例联系人") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:demo01-contact:delete')") + public CommonResult deleteDemo01Contact(@RequestParam("id") Long id) { + demo01ContactService.deleteDemo01Contact(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除示例联系人") + @PreAuthorize("@ss.hasPermission('infra:demo01-contact:delete')") + public CommonResult deleteDemo0iContactList(@RequestParam("ids") List ids) { + demo01ContactService.deleteDemo0iContactList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得示例联系人") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:demo01-contact:query')") + public CommonResult getDemo01Contact(@RequestParam("id") Long id) { + Demo01ContactDO demo01Contact = demo01ContactService.getDemo01Contact(id); + return success(BeanUtils.toBean(demo01Contact, Demo01ContactRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得示例联系人分页") + @PreAuthorize("@ss.hasPermission('infra:demo01-contact:query')") + public CommonResult> getDemo01ContactPage(@Valid Demo01ContactPageReqVO pageReqVO) { + PageResult pageResult = demo01ContactService.getDemo01ContactPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, Demo01ContactRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出示例联系人 Excel") + @PreAuthorize("@ss.hasPermission('infra:demo01-contact:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDemo01ContactExcel(@Valid Demo01ContactPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = demo01ContactService.getDemo01ContactPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "示例联系人.xls", "数据", Demo01ContactRespVO.class, + BeanUtils.toBean(list, Demo01ContactRespVO.class)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactPageReqVO.java new file mode 100644 index 0000000..07c4ba1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactPageReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 示例联系人分页 Request VO") +@Data +public class Demo01ContactPageReqVO extends PageParam { + + @Schema(description = "名字", example = "张三") + private String name; + + @Schema(description = "性别", example = "1") + private Integer sex; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java new file mode 100644 index 0000000..17ee9fe --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 示例联系人 Response VO") +@Data +@ExcelIgnoreUnannotated +public class Demo01ContactRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21555") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @ExcelProperty("名字") + private String name; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Integer sex; + + @Schema(description = "出生年", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("出生年") + private LocalDateTime birthday; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对") + @ExcelProperty("简介") + private String description; + + @Schema(description = "头像") + @ExcelProperty("头像") + private String avatar; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactSaveReqVO.java new file mode 100644 index 0000000..15d2727 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactSaveReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 示例联系人新增/修改 Request VO") +@Data +public class Demo01ContactSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21555") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "性别不能为空") + private Integer sex; + + @Schema(description = "出生年", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出生年不能为空") + private LocalDateTime birthday; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对") + @NotEmpty(message = "简介不能为空") + private String description; + + @Schema(description = "头像") + private String avatar; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/Demo02CategoryController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/Demo02CategoryController.java new file mode 100644 index 0000000..95ed772 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/Demo02CategoryController.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; +import cn.iocoder.yudao.module.infra.service.demo.demo02.Demo02CategoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 示例分类") +@RestController +@RequestMapping("/infra/demo02-category") +@Validated +public class Demo02CategoryController { + + @Resource + private Demo02CategoryService demo02CategoryService; + + @PostMapping("/create") + @Operation(summary = "创建示例分类") + @PreAuthorize("@ss.hasPermission('infra:demo02-category:create')") + public CommonResult createDemo02Category(@Valid @RequestBody Demo02CategorySaveReqVO createReqVO) { + return success(demo02CategoryService.createDemo02Category(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新示例分类") + @PreAuthorize("@ss.hasPermission('infra:demo02-category:update')") + public CommonResult updateDemo02Category(@Valid @RequestBody Demo02CategorySaveReqVO updateReqVO) { + demo02CategoryService.updateDemo02Category(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除示例分类") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:demo02-category:delete')") + public CommonResult deleteDemo02Category(@RequestParam("id") Long id) { + demo02CategoryService.deleteDemo02Category(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得示例分类") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:demo02-category:query')") + public CommonResult getDemo02Category(@RequestParam("id") Long id) { + Demo02CategoryDO demo02Category = demo02CategoryService.getDemo02Category(id); + return success(BeanUtils.toBean(demo02Category, Demo02CategoryRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得示例分类列表") + @PreAuthorize("@ss.hasPermission('infra:demo02-category:query')") + public CommonResult> getDemo02CategoryList(@Valid Demo02CategoryListReqVO listReqVO) { + List list = demo02CategoryService.getDemo02CategoryList(listReqVO); + return success(BeanUtils.toBean(list, Demo02CategoryRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出示例分类 Excel") + @PreAuthorize("@ss.hasPermission('infra:demo02-category:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDemo02CategoryExcel(@Valid Demo02CategoryListReqVO listReqVO, + HttpServletResponse response) throws IOException { + List list = demo02CategoryService.getDemo02CategoryList(listReqVO); + // 导出 Excel + ExcelUtils.write(response, "示例分类.xls", "数据", Demo02CategoryRespVO.class, + BeanUtils.toBean(list, Demo02CategoryRespVO.class)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryListReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryListReqVO.java new file mode 100644 index 0000000..69fcfd9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryListReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 示例分类列表 Request VO") +@Data +public class Demo02CategoryListReqVO { + + @Schema(description = "名字", example = "芋艿") + private String name; + + @Schema(description = "父级编号", example = "6080") + private Long parentId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java new file mode 100644 index 0000000..1f2efd4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 示例分类 Response VO") +@Data +@ExcelIgnoreUnannotated +public class Demo02CategoryRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10304") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @ExcelProperty("名字") + private String name; + + @Schema(description = "父级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6080") + @ExcelProperty("父级编号") + private Long parentId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategorySaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategorySaveReqVO.java new file mode 100644 index 0000000..0bc1a78 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategorySaveReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 示例分类新增/修改 Request VO") +@Data +public class Demo02CategorySaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10304") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "父级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6080") + @NotNull(message = "父级编号不能为空") + private Long parentId; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/Demo03StudentErpController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/Demo03StudentErpController.java new file mode 100644 index 0000000..5115409 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/Demo03StudentErpController.java @@ -0,0 +1,208 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo.Demo03StudentErpPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo.Demo03StudentErpRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo.Demo03StudentErpSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import cn.iocoder.yudao.module.infra.service.demo.demo03.erp.Demo03StudentErpService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 学生") +@RestController +@RequestMapping("/infra/demo03-student-erp") +@Validated +public class Demo03StudentErpController { + + @Resource + private Demo03StudentErpService demo03StudentErpService; + + @PostMapping("/create") + @Operation(summary = "创建学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')") + public CommonResult createDemo03Student(@Valid @RequestBody Demo03StudentErpSaveReqVO createReqVO) { + return success(demo03StudentErpService.createDemo03Student(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") + public CommonResult updateDemo03Student(@Valid @RequestBody Demo03StudentErpSaveReqVO updateReqVO) { + demo03StudentErpService.updateDemo03Student(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除学生") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03Student(@RequestParam("id") Long id) { + demo03StudentErpService.deleteDemo03Student(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03StudentList(@RequestParam("ids") List ids) { + demo03StudentErpService.deleteDemo03StudentList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得学生") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult getDemo03Student(@RequestParam("id") Long id) { + Demo03StudentDO demo03Student = demo03StudentErpService.getDemo03Student(id); + return success(BeanUtils.toBean(demo03Student, Demo03StudentErpRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得学生分页") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult> getDemo03StudentPage(@Valid Demo03StudentErpPageReqVO pageReqVO) { + PageResult pageResult = demo03StudentErpService.getDemo03StudentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, Demo03StudentErpRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出学生 Excel") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDemo03StudentExcel(@Valid Demo03StudentErpPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = demo03StudentErpService.getDemo03StudentPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "学生.xls", "数据", Demo03StudentErpRespVO.class, + BeanUtils.toBean(list, Demo03StudentErpRespVO.class)); + } + + // ==================== 子表(学生课程) ==================== + + @GetMapping("/demo03-course/page") + @Operation(summary = "获得学生课程分页") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult> getDemo03CoursePage(PageParam pageReqVO, + @RequestParam("studentId") Long studentId) { + return success(demo03StudentErpService.getDemo03CoursePage(pageReqVO, studentId)); + } + + @PostMapping("/demo03-course/create") + @Operation(summary = "创建学生课程") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')") + public CommonResult createDemo03Course(@Valid @RequestBody Demo03CourseDO demo03Course) { + return success(demo03StudentErpService.createDemo03Course(demo03Course)); + } + + @PutMapping("/demo03-course/update") + @Operation(summary = "更新学生课程") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") + public CommonResult updateDemo03Course(@Valid @RequestBody Demo03CourseDO demo03Course) { + demo03StudentErpService.updateDemo03Course(demo03Course); + return success(true); + } + + @DeleteMapping("/demo03-course/delete") + @Parameter(name = "id", description = "编号", required = true) + @Operation(summary = "删除学生课程") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03Course(@RequestParam("id") Long id) { + demo03StudentErpService.deleteDemo03Course(id); + return success(true); + } + + @DeleteMapping("/demo03-course/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生课程") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03CourseList(@RequestParam("ids") List ids) { + demo03StudentErpService.deleteDemo03CourseList(ids); + return success(true); + } + + @GetMapping("/demo03-course/get") + @Operation(summary = "获得学生课程") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult getDemo03Course(@RequestParam("id") Long id) { + return success(demo03StudentErpService.getDemo03Course(id)); + } + + // ==================== 子表(学生班级) ==================== + + @GetMapping("/demo03-grade/page") + @Operation(summary = "获得学生班级分页") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult> getDemo03GradePage(PageParam pageReqVO, + @RequestParam("studentId") Long studentId) { + return success(demo03StudentErpService.getDemo03GradePage(pageReqVO, studentId)); + } + + @PostMapping("/demo03-grade/create") + @Operation(summary = "创建学生班级") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')") + public CommonResult createDemo03Grade(@Valid @RequestBody Demo03GradeDO demo03Grade) { + return success(demo03StudentErpService.createDemo03Grade(demo03Grade)); + } + + @PutMapping("/demo03-grade/update") + @Operation(summary = "更新学生班级") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") + public CommonResult updateDemo03Grade(@Valid @RequestBody Demo03GradeDO demo03Grade) { + demo03StudentErpService.updateDemo03Grade(demo03Grade); + return success(true); + } + + @DeleteMapping("/demo03-grade/delete") + @Parameter(name = "id", description = "编号", required = true) + @Operation(summary = "删除学生班级") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03Grade(@RequestParam("id") Long id) { + demo03StudentErpService.deleteDemo03Grade(id); + return success(true); + } + + @DeleteMapping("/demo03-grade/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生班级") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03GradeList(@RequestParam("ids") List ids) { + demo03StudentErpService.deleteDemo03GradeList(ids); + return success(true); + } + + @GetMapping("/demo03-grade/get") + @Operation(summary = "获得学生班级") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult getDemo03Grade(@RequestParam("id") Long id) { + return success(demo03StudentErpService.getDemo03Grade(id)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpPageReqVO.java new file mode 100644 index 0000000..5fcb656 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpPageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 学生分页 Request VO") +@Data +public class Demo03StudentErpPageReqVO extends PageParam { + + @Schema(description = "名字", example = "芋艿") + private String name; + + @Schema(description = "性别") + private Integer sex; + + @Schema(description = "简介", example = "随便") + private String description; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java new file mode 100644 index 0000000..84dfe6e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 学生 Response VO") +@Data +@ExcelIgnoreUnannotated +public class Demo03StudentErpRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @ExcelProperty("名字") + private String name; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Integer sex; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("出生日期") + private LocalDateTime birthday; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便") + @ExcelProperty("简介") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpSaveReqVO.java new file mode 100644 index 0000000..d68e9b5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpSaveReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 学生新增/修改 Request VO") +@Data +public class Demo03StudentErpSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "性别不能为空") + private Integer sex; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出生日期不能为空") + private LocalDateTime birthday; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便") + @NotEmpty(message = "简介不能为空") + private String description; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/Demo03StudentInnerController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/Demo03StudentInnerController.java new file mode 100644 index 0000000..1a0fb42 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/Demo03StudentInnerController.java @@ -0,0 +1,124 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo.Demo03StudentInnerPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo.Demo03StudentInnerRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo.Demo03StudentInnerSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import cn.iocoder.yudao.module.infra.service.demo.demo03.inner.Demo03StudentInnerService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 学生") +@RestController +@RequestMapping("/infra/demo03-student-inner") +@Validated +public class Demo03StudentInnerController { + + @Resource + private Demo03StudentInnerService demo03StudentInnerService; + + @PostMapping("/create") + @Operation(summary = "创建学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')") + public CommonResult createDemo03Student(@Valid @RequestBody Demo03StudentInnerSaveReqVO createReqVO) { + return success(demo03StudentInnerService.createDemo03Student(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") + public CommonResult updateDemo03Student(@Valid @RequestBody Demo03StudentInnerSaveReqVO updateReqVO) { + demo03StudentInnerService.updateDemo03Student(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除学生") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03Student(@RequestParam("id") Long id) { + demo03StudentInnerService.deleteDemo03Student(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03StudentList(@RequestParam("ids") List ids) { + demo03StudentInnerService.deleteDemo03StudentList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得学生") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult getDemo03Student(@RequestParam("id") Long id) { + Demo03StudentDO demo03Student = demo03StudentInnerService.getDemo03Student(id); + return success(BeanUtils.toBean(demo03Student, Demo03StudentInnerRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得学生分页") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult> getDemo03StudentPage(@Valid Demo03StudentInnerPageReqVO pageReqVO) { + PageResult pageResult = demo03StudentInnerService.getDemo03StudentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, Demo03StudentInnerRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出学生 Excel") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDemo03StudentExcel(@Valid Demo03StudentInnerPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = demo03StudentInnerService.getDemo03StudentPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "学生.xls", "数据", Demo03StudentInnerRespVO.class, + BeanUtils.toBean(list, Demo03StudentInnerRespVO.class)); + } + + // ==================== 子表(学生课程) ==================== + + @GetMapping("/demo03-course/list-by-student-id") + @Operation(summary = "获得学生课程列表") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult> getDemo03CourseListByStudentId(@RequestParam("studentId") Long studentId) { + return success(demo03StudentInnerService.getDemo03CourseListByStudentId(studentId)); + } + + // ==================== 子表(学生班级) ==================== + + @GetMapping("/demo03-grade/get-by-student-id") + @Operation(summary = "获得学生班级") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult getDemo03GradeByStudentId(@RequestParam("studentId") Long studentId) { + return success(demo03StudentInnerService.getDemo03GradeByStudentId(studentId)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerPageReqVO.java new file mode 100644 index 0000000..82c7265 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerPageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 学生分页 Request VO") +@Data +public class Demo03StudentInnerPageReqVO extends PageParam { + + @Schema(description = "名字", example = "芋艿") + private String name; + + @Schema(description = "性别") + private Integer sex; + + @Schema(description = "简介", example = "随便") + private String description; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java new file mode 100644 index 0000000..48d5e48 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 学生 Response VO") +@Data +@ExcelIgnoreUnannotated +public class Demo03StudentInnerRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @ExcelProperty("名字") + private String name; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Integer sex; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("出生日期") + private LocalDateTime birthday; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便") + @ExcelProperty("简介") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerSaveReqVO.java new file mode 100644 index 0000000..e181eea --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerSaveReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo; + +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 学生新增/修改 Request VO") +@Data +public class Demo03StudentInnerSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "性别不能为空") + private Integer sex; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出生日期不能为空") + private LocalDateTime birthday; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便") + @NotEmpty(message = "简介不能为空") + private String description; + + @Schema(description = "学生课程列表") + private List demo03Courses; + + @Schema(description = "学生班级") + private Demo03GradeDO demo03Grade; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/Demo03StudentNormalController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/Demo03StudentNormalController.java new file mode 100644 index 0000000..8b39359 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/Demo03StudentNormalController.java @@ -0,0 +1,124 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo.Demo03StudentNormalPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo.Demo03StudentNormalRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo.Demo03StudentNormalSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import cn.iocoder.yudao.module.infra.service.demo.demo03.normal.Demo03StudentNormalService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 学生") +@RestController +@RequestMapping("/infra/demo03-student-normal") +@Validated +public class Demo03StudentNormalController { + + @Resource + private Demo03StudentNormalService demo03StudentNormalService; + + @PostMapping("/create") + @Operation(summary = "创建学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')") + public CommonResult createDemo03Student(@Valid @RequestBody Demo03StudentNormalSaveReqVO createReqVO) { + return success(demo03StudentNormalService.createDemo03Student(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") + public CommonResult updateDemo03Student(@Valid @RequestBody Demo03StudentNormalSaveReqVO updateReqVO) { + demo03StudentNormalService.updateDemo03Student(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除学生") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03Student(@RequestParam("id") Long id) { + demo03StudentNormalService.deleteDemo03Student(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')") + public CommonResult deleteDemo03StudentList(@RequestParam("ids") List ids) { + demo03StudentNormalService.deleteDemo03StudentList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得学生") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult getDemo03Student(@RequestParam("id") Long id) { + Demo03StudentDO demo03Student = demo03StudentNormalService.getDemo03Student(id); + return success(BeanUtils.toBean(demo03Student, Demo03StudentNormalRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得学生分页") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult> getDemo03StudentPage(@Valid Demo03StudentNormalPageReqVO pageReqVO) { + PageResult pageResult = demo03StudentNormalService.getDemo03StudentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, Demo03StudentNormalRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出学生 Excel") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDemo03StudentExcel(@Valid Demo03StudentNormalPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = demo03StudentNormalService.getDemo03StudentPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "学生.xls", "数据", Demo03StudentNormalRespVO.class, + BeanUtils.toBean(list, Demo03StudentNormalRespVO.class)); + } + + // ==================== 子表(学生课程) ==================== + + @GetMapping("/demo03-course/list-by-student-id") + @Operation(summary = "获得学生课程列表") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult> getDemo03CourseListByStudentId(@RequestParam("studentId") Long studentId) { + return success(demo03StudentNormalService.getDemo03CourseListByStudentId(studentId)); + } + + // ==================== 子表(学生班级) ==================== + + @GetMapping("/demo03-grade/get-by-student-id") + @Operation(summary = "获得学生班级") + @Parameter(name = "studentId", description = "学生编号") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult getDemo03GradeByStudentId(@RequestParam("studentId") Long studentId) { + return success(demo03StudentNormalService.getDemo03GradeByStudentId(studentId)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalPageReqVO.java new file mode 100644 index 0000000..8d6c76c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalPageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 学生分页 Request VO") +@Data +public class Demo03StudentNormalPageReqVO extends PageParam { + + @Schema(description = "名字", example = "芋艿") + private String name; + + @Schema(description = "性别") + private Integer sex; + + @Schema(description = "简介", example = "随便") + private String description; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java new file mode 100644 index 0000000..e36a796 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 学生 Response VO") +@Data +@ExcelIgnoreUnannotated +public class Demo03StudentNormalRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @ExcelProperty("名字") + private String name; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "性别", converter = DictConvert.class) + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 + private Integer sex; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("出生日期") + private LocalDateTime birthday; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便") + @ExcelProperty("简介") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalSaveReqVO.java new file mode 100644 index 0000000..1b3edd9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalSaveReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo; + +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 学生新增/修改 Request VO") +@Data +public class Demo03StudentNormalSaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525") + private Long id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotEmpty(message = "名字不能为空") + private String name; + + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "性别不能为空") + private Integer sex; + + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "出生日期不能为空") + private LocalDateTime birthday; + + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便") + @NotEmpty(message = "简介不能为空") + private String description; + + @Schema(description = "学生课程列表") + private List demo03Courses; + + @Schema(description = "学生班级") + private Demo03GradeDO demo03Grade; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/package-info.java new file mode 100644 index 0000000..c448646 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/package-info.java @@ -0,0 +1,8 @@ +/** + * 代码生成示例 + * + * 1. demo01:单表(增删改查) + * 2. demo02:单表(树形结构) + * 3. demo03:主子表(标准模式)+ 主子表(ERP 模式)+ 主子表(内嵌模式) + */ +package cn.iocoder.yudao.module.infra.controller.admin.demo; \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http new file mode 100644 index 0000000..14b6228 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http @@ -0,0 +1,45 @@ +### 请求 /infra/file-config/create 接口 => 成功 +POST {{baseUrl}}/infra/file-config/create +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} + +{ + "name": "S3 - 七牛云", + "remark": "", + "storage": 20, + "config": { + "accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8", + "accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP", + "bucket": "ruoyi-vue-pro", + "endpoint": "s3-cn-south-1.qiniucs.com", + "domain": "http://test.yudao.iocoder.cn", + "region": "oss-cn-beijing" + } +} + +### 请求 /infra/file-config/update 接口 => 成功 +PUT {{baseUrl}}/infra/file-config/update +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} + +{ + "id": 2, + "name": "S3 - 七牛云", + "remark": "", + "config": { + "accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8", + "accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP", + "bucket": "ruoyi-vue-pro", + "endpoint": "s3-cn-south-1.qiniucs.com", + "domain": "http://test.yudao.iocoder.cn", + "region": "oss-cn-beijing" + } +} + +### 请求 /infra/file-config/test 接口 => 成功 +GET {{baseUrl}}/infra/file-config/test?id=2 +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.java new file mode 100644 index 0000000..c593f4b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import cn.iocoder.yudao.module.infra.service.file.FileConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 文件配置") +@RestController +@RequestMapping("/infra/file-config") +@Validated +public class FileConfigController { + + @Resource + private FileConfigService fileConfigService; + + @PostMapping("/create") + @Operation(summary = "创建文件配置") + @PreAuthorize("@ss.hasPermission('infra:file-config:create')") + public CommonResult createFileConfig(@Valid @RequestBody FileConfigSaveReqVO createReqVO) { + return success(fileConfigService.createFileConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新文件配置") + @PreAuthorize("@ss.hasPermission('infra:file-config:update')") + public CommonResult updateFileConfig(@Valid @RequestBody FileConfigSaveReqVO updateReqVO) { + fileConfigService.updateFileConfig(updateReqVO); + return success(true); + } + + @PutMapping("/update-master") + @Operation(summary = "更新文件配置为 Master") + @PreAuthorize("@ss.hasPermission('infra:file-config:update')") + public CommonResult updateFileConfigMaster(@RequestParam("id") Long id) { + fileConfigService.updateFileConfigMaster(id); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除文件配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:file-config:delete')") + public CommonResult deleteFileConfig(@RequestParam("id") Long id) { + fileConfigService.deleteFileConfig(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除文件配置") + @Parameter(name = "ids", description = "编号列表", required = true) + @PreAuthorize("@ss.hasPermission('infra:file-config:delete')") + public CommonResult deleteFileConfigList(@RequestParam("ids") List ids) { + fileConfigService.deleteFileConfigList(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得文件配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:file-config:query')") + public CommonResult getFileConfig(@RequestParam("id") Long id) { + FileConfigDO config = fileConfigService.getFileConfig(id); + return success(BeanUtils.toBean(config, FileConfigRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得文件配置分页") + @PreAuthorize("@ss.hasPermission('infra:file-config:query')") + public CommonResult> getFileConfigPage(@Valid FileConfigPageReqVO pageVO) { + PageResult pageResult = fileConfigService.getFileConfigPage(pageVO); + return success(BeanUtils.toBean(pageResult, FileConfigRespVO.class)); + } + + @GetMapping("/test") + @Operation(summary = "测试文件配置是否正确") + @PreAuthorize("@ss.hasPermission('infra:file-config:query')") + public CommonResult testFileConfig(@RequestParam("id") Long id) throws Exception { + String url = fileConfigService.testFileConfig(id); + return success(url); + } +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java new file mode 100644 index 0000000..d5611b7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -0,0 +1,123 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.*; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import cn.iocoder.yudao.module.infra.service.file.FileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment; + +@Tag(name = "管理后台 - 文件存储") +@RestController +@RequestMapping("/infra/file") +@Validated +@Slf4j +public class FileController { + + @Resource + private FileService fileService; + + @PostMapping("/upload") + @Operation(summary = "上传文件", description = "模式一:后端上传文件") + public CommonResult uploadFile(FileUploadReqVO uploadReqVO) throws Exception { + MultipartFile file = uploadReqVO.getFile(); + byte[] content = IoUtil.readBytes(file.getInputStream()); + return success(fileService.createFile(content, file.getOriginalFilename(), + uploadReqVO.getDirectory(), file.getContentType())); + } + + @GetMapping("/presigned-url") + @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") + @Parameters({ + @Parameter(name = "name", description = "文件名称", required = true), + @Parameter(name = "directory", description = "文件目录") + }) + public CommonResult getFilePresignedUrl( + @RequestParam("name") String name, + @RequestParam(value = "directory", required = false) String directory) { + return success(fileService.getFilePresignedUrl(name, directory)); + } + + @PostMapping("/create") + @Operation(summary = "创建文件", description = "模式二:前端上传文件:配合 presigned-url 接口,记录上传了上传的文件") + public CommonResult createFile(@Valid @RequestBody FileCreateReqVO createReqVO) { + return success(fileService.createFile(createReqVO)); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除文件") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:file:delete')") + public CommonResult deleteFile(@RequestParam("id") Long id) throws Exception { + fileService.deleteFile(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除文件") + @Parameter(name = "ids", description = "编号列表", required = true) + @PreAuthorize("@ss.hasPermission('infra:file:delete')") + public CommonResult deleteFileList(@RequestParam("ids") List ids) throws Exception { + fileService.deleteFileList(ids); + return success(true); + } + + @GetMapping("/{configId}/get/**") + @PermitAll + @TenantIgnore + @Operation(summary = "下载文件") + @Parameter(name = "configId", description = "配置编号", required = true) + public void getFileContent(HttpServletRequest request, + HttpServletResponse response, + @PathVariable("configId") Long configId) throws Exception { + // 获取请求的路径 + String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false); + if (StrUtil.isEmpty(path)) { + throw new IllegalArgumentException("结尾的 path 路径必须传递"); + } + // 解码,解决中文路径的问题 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/ + path = URLUtil.decode(path); + + // 读取内容 + byte[] content = fileService.getFileContent(configId, path); + if (content == null) { + log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path); + response.setStatus(HttpStatus.NOT_FOUND.value()); + return; + } + writeAttachment(response, path, content); + } + + @GetMapping("/page") + @Operation(summary = "获得文件分页") + @PreAuthorize("@ss.hasPermission('infra:file:query')") + public CommonResult> getFilePage(@Valid FilePageReqVO pageVO) { + PageResult pageResult = fileService.getFilePage(pageVO); + return success(BeanUtils.toBean(pageResult, FileRespVO.class)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java new file mode 100644 index 0000000..23b3077 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 文件配置分页 Request VO") +@Data +public class FileConfigPageReqVO extends PageParam { + + @Schema(description = "配置名", example = "S3 - 阿里云") + private String name; + + @Schema(description = "存储器", example = "1") + private Integer storage; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java new file mode 100644 index 0000000..e344f82 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config; + +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 文件配置 Response VO") +@Data +public class FileConfigRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "配置名", requiredMode = Schema.RequiredMode.REQUIRED, example = "S3 - 阿里云") + private String name; + + @Schema(description = "存储器,参见 FileStorageEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer storage; + + @Schema(description = "是否为主配置", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean master; + + @Schema(description = "存储配置", requiredMode = Schema.RequiredMode.REQUIRED) + private FileClientConfig config; + + @Schema(description = "备注", example = "我是备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigSaveReqVO.java new file mode 100644 index 0000000..211bee5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/config/FileConfigSaveReqVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Map; + +@Schema(description = "管理后台 - 文件配置创建/修改 Request VO") +@Data +public class FileConfigSaveReqVO { + + @Schema(description = "编号", example = "1") + private Long id; + + @Schema(description = "配置名", requiredMode = Schema.RequiredMode.REQUIRED, example = "S3 - 阿里云") + @NotNull(message = "配置名不能为空") + private String name; + + @Schema(description = "存储器,参见 FileStorageEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "存储器不能为空") + private Integer storage; + + @Schema(description = "存储配置,配置是动态参数,所以使用 Map 接收", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "存储配置不能为空") + private Map config; + + @Schema(description = "备注", example = "我是备注") + private String remark; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileCreateReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileCreateReqVO.java new file mode 100644 index 0000000..5daa397 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileCreateReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 文件创建 Request VO") +@Data +public class FileCreateReqVO { + + @NotNull(message = "文件配置编号不能为空") + @Schema(description = "文件配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + private Long configId; + + @NotNull(message = "文件路径不能为空") + @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao.jpg") + private String path; + + @NotNull(message = "原文件名不能为空") + @Schema(description = "原文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao.jpg") + private String name; + + @NotNull(message = "文件 URL不能为空") + @Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg") + private String url; + + @Schema(description = "文件 MIME 类型", example = "application/octet-stream") + private String type; + + @Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer size; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePageReqVO.java new file mode 100644 index 0000000..48d4c43 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePageReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 文件分页 Request VO") +@Data +public class FilePageReqVO extends PageParam { + + @Schema(description = "文件路径,模糊匹配", example = "yudao") + private String path; + + @Schema(description = "文件类型,模糊匹配", example = "jpg") + private String type; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java new file mode 100644 index 0000000..72be6ae --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "管理后台 - 文件预签名地址 Response VO") +@Data +public class FilePresignedUrlRespVO { + + @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + private Long configId; + + @Schema(description = "文件上传 URL", requiredMode = Schema.RequiredMode.REQUIRED, + example = "https://s3.cn-south-1.qiniucs.com/ruoyi-vue-pro/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS%2F20240217%2Fcn-south-1%2Fs3%2Faws4_request&X-Amz-Date=20240217T123222Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=a29f33770ab79bf523ccd4034d0752ac545f3c2a3b17baa1eb4e280cfdccfda5") + private String uploadUrl; + + /** + * 为什么要返回 url 字段? + * + * 前端上传完文件后,需要使用该 URL 进行访问 + */ + @Schema(description = "文件访问 URL", requiredMode = Schema.RequiredMode.REQUIRED, + example = "https://test.yudao.iocoder.cn/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png") + private String url; + + /** + * 为什么要返回 path 字段? + * + * 前端上传完文件后,需要调用 createFile 记录下 path 路径 + */ + @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx.png") + private String path; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java new file mode 100644 index 0000000..a0357da --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 文件 Response VO,不返回 content 字段,太大") +@Data +public class FileRespVO { + + @Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + private Long configId; + + @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao.jpg") + private String path; + + @Schema(description = "原文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao.jpg") + private String name; + + @Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg") + private String url; + + @Schema(description = "文件MIME类型", example = "application/octet-stream") + private String type; + + @Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer size; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java new file mode 100644 index 0000000..44e8b65 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +@Schema(description = "管理后台 - 上传文件 Request VO") +@Data +public class FileUploadReqVO { + + @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件附件不能为空") + private MultipartFile file; + + @Schema(description = "文件目录", example = "XXX/YYY") + private String directory; + + @AssertTrue(message = "文件目录不正确") + @JsonIgnore + public boolean isDirectoryValid() { + return !StrUtil.containsAny(directory, "..", "/", "\\"); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.http b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.http new file mode 100644 index 0000000..52e7394 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.http @@ -0,0 +1,5 @@ +### 请求 /infra/job/sync 接口 => 成功 +POST {{baseUrl}}/infra/job/sync +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.java new file mode 100644 index 0000000..1ae53e3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.java @@ -0,0 +1,158 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.quartz.core.util.CronUtils; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import cn.iocoder.yudao.module.infra.service.job.JobService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.quartz.SchedulerException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 定时任务") +@RestController +@RequestMapping("/infra/job") +@Validated +public class JobController { + + @Resource + private JobService jobService; + + @PostMapping("/create") + @Operation(summary = "创建定时任务") + @PreAuthorize("@ss.hasPermission('infra:job:create')") + public CommonResult createJob(@Valid @RequestBody JobSaveReqVO createReqVO) + throws SchedulerException { + return success(jobService.createJob(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新定时任务") + @PreAuthorize("@ss.hasPermission('infra:job:update')") + public CommonResult updateJob(@Valid @RequestBody JobSaveReqVO updateReqVO) + throws SchedulerException { + jobService.updateJob(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新定时任务的状态") + @Parameters({ + @Parameter(name = "id", description = "编号", required = true, example = "1024"), + @Parameter(name = "status", description = "状态", required = true, example = "1"), + }) + @PreAuthorize("@ss.hasPermission('infra:job:update')") + public CommonResult updateJobStatus(@RequestParam(value = "id") Long id, @RequestParam("status") Integer status) + throws SchedulerException { + jobService.updateJobStatus(id, status); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除定时任务") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:job:delete')") + public CommonResult deleteJob(@RequestParam("id") Long id) + throws SchedulerException { + jobService.deleteJob(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除定时任务") + @Parameter(name = "ids", description = "编号列表", required = true) + @PreAuthorize("@ss.hasPermission('infra:job:delete')") + public CommonResult deleteJobList(@RequestParam("ids") List ids) + throws SchedulerException { + jobService.deleteJobList(ids); + return success(true); + } + + @PutMapping("/trigger") + @Operation(summary = "触发定时任务") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:job:trigger')") + public CommonResult triggerJob(@RequestParam("id") Long id) throws SchedulerException { + jobService.triggerJob(id); + return success(true); + } + + @PostMapping("/sync") + @Operation(summary = "同步定时任务") + @PreAuthorize("@ss.hasPermission('infra:job:create')") + public CommonResult syncJob() throws SchedulerException { + jobService.syncJob(); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得定时任务") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult getJob(@RequestParam("id") Long id) { + JobDO job = jobService.getJob(id); + return success(BeanUtils.toBean(job, JobRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得定时任务分页") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobPage(@Valid JobPageReqVO pageVO) { + PageResult pageResult = jobService.getJobPage(pageVO); + return success(BeanUtils.toBean(pageResult, JobRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出定时任务 Excel") + @PreAuthorize("@ss.hasPermission('infra:job:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportJobExcel(@Valid JobPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = jobService.getJobPage(exportReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "定时任务.xls", "数据", JobRespVO.class, + BeanUtils.toBean(list, JobRespVO.class)); + } + + @GetMapping("/get_next_times") + @Operation(summary = "获得定时任务的下 n 次执行时间") + @Parameters({ + @Parameter(name = "id", description = "编号", required = true, example = "1024"), + @Parameter(name = "count", description = "数量", example = "5") + }) + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobNextTimes( + @RequestParam("id") Long id, + @RequestParam(value = "count", required = false, defaultValue = "5") Integer count) { + JobDO job = jobService.getJob(id); + if (job == null) { + return success(Collections.emptyList()); + } + return success(CronUtils.getNextTimes(job.getCronExpression(), count)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobLogController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobLogController.java new file mode 100644 index 0000000..71b098b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobLogController.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; +import cn.iocoder.yudao.module.infra.service.job.JobLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 定时任务日志") +@RestController +@RequestMapping("/infra/job-log") +@Validated +public class JobLogController { + + @Resource + private JobLogService jobLogService; + + @GetMapping("/get") + @Operation(summary = "获得定时任务日志") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult getJobLog(@RequestParam("id") Long id) { + JobLogDO jobLog = jobLogService.getJobLog(id); + return success(BeanUtils.toBean(jobLog, JobLogRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得定时任务日志分页") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobLogPage(@Valid JobLogPageReqVO pageVO) { + PageResult pageResult = jobLogService.getJobLogPage(pageVO); + return success(BeanUtils.toBean(pageResult, JobLogRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出定时任务日志 Excel") + @PreAuthorize("@ss.hasPermission('infra:job:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportJobLogExcel(@Valid JobLogPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = jobLogService.getJobLogPage(exportReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "任务日志.xls", "数据", JobLogRespVO.class, + BeanUtils.toBean(list, JobLogRespVO.class)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobPageReqVO.java new file mode 100644 index 0000000..67a6aae --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobPageReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 定时任务分页 Request VO") +@Data +public class JobPageReqVO extends PageParam { + + @Schema(description = "任务名称,模糊匹配", example = "测试任务") + private String name; + + @Schema(description = "任务状态,参见 JobStatusEnum 枚举", example = "1") + private Integer status; + + @Schema(description = "处理器的名字,模糊匹配", example = "sysUserSessionTimeoutJob") + private String handlerName; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java new file mode 100644 index 0000000..aee0d9b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 定时任务 Response VO") +@Data +@ExcelIgnoreUnannotated +public class JobRespVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("任务编号") + private Long id; + + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试任务") + @ExcelProperty("任务名称") + private String name; + + @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "任务状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.JOB_STATUS) + private Integer status; + + @Schema(description = "处理器的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "sysUserSessionTimeoutJob") + @ExcelProperty("处理器的名字") + private String handlerName; + + @Schema(description = "处理器的参数", example = "yudao") + @ExcelProperty("处理器的参数") + private String handlerParam; + + @Schema(description = "CRON 表达式", requiredMode = Schema.RequiredMode.REQUIRED, example = "0/10 * * * * ? *") + @ExcelProperty("CRON 表达式") + private String cronExpression; + + @Schema(description = "重试次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "重试次数不能为空") + private Integer retryCount; + + @Schema(description = "重试间隔", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer retryInterval; + + @Schema(description = "监控超时时间", example = "1000") + @ExcelProperty("监控超时时间") + private Integer monitorTimeout; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobSaveReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobSaveReqVO.java new file mode 100644 index 0000000..a8e20fc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobSaveReqVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 定时任务创建/修改 Request VO") +@Data +public class JobSaveReqVO { + + @Schema(description = "任务编号", example = "1024") + private Long id; + + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试任务") + @NotEmpty(message = "任务名称不能为空") + private String name; + + @Schema(description = "处理器的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "sysUserSessionTimeoutJob") + @NotEmpty(message = "处理器的名字不能为空") + private String handlerName; + + @Schema(description = "处理器的参数", example = "yudao") + private String handlerParam; + + @Schema(description = "CRON 表达式", requiredMode = Schema.RequiredMode.REQUIRED, example = "0/10 * * * * ? *") + @NotEmpty(message = "CRON 表达式不能为空") + private String cronExpression; + + @Schema(description = "重试次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "重试次数不能为空") + private Integer retryCount; + + @Schema(description = "重试间隔", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + @NotNull(message = "重试间隔不能为空") + private Integer retryInterval; + + @Schema(description = "监控超时时间", example = "1000") + private Integer monitorTimeout; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java new file mode 100644 index 0000000..2d8c30e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 定时任务日志分页 Request VO") +@Data +public class JobLogPageReqVO extends PageParam { + + @Schema(description = "任务编号", example = "10") + private Long jobId; + + @Schema(description = "处理器的名字,模糊匹配") + private String handlerName; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "开始执行时间") + private LocalDateTime beginTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "结束执行时间") + private LocalDateTime endTime; + + @Schema(description = "任务状态,参见 JobLogStatusEnum 枚举") + private Integer status; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java new file mode 100644 index 0000000..543339d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 定时任务日志 Response VO") +@Data +@ExcelIgnoreUnannotated +public class JobLogRespVO { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("日志编号") + private Long id; + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("任务编号") + private Long jobId; + + @Schema(description = "处理器的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "sysUserSessionTimeoutJob") + @ExcelProperty("处理器的名字") + private String handlerName; + + @Schema(description = "处理器的参数", example = "yudao") + @ExcelProperty("处理器的参数") + private String handlerParam; + + @Schema(description = "第几次执行", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("第几次执行") + private Integer executeIndex; + + @Schema(description = "开始执行时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("开始执行时间") + private LocalDateTime beginTime; + + @Schema(description = "结束执行时间") + @ExcelProperty("结束执行时间") + private LocalDateTime endTime; + + @Schema(description = "执行时长", example = "123") + @ExcelProperty("执行时长") + private Integer duration; + + @Schema(description = "任务状态,参见 JobLogStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "任务状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.JOB_STATUS) + private Integer status; + + @Schema(description = "结果数据", example = "执行成功") + @ExcelProperty("结果数据") + private String result; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiAccessLogController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiAccessLogController.java new file mode 100644 index 0000000..513b3ef --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiAccessLogController.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import cn.iocoder.yudao.module.infra.service.logger.ApiAccessLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - API 访问日志") +@RestController +@RequestMapping("/infra/api-access-log") +@Validated +public class ApiAccessLogController { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @GetMapping("/page") + @Operation(summary = "获得API 访问日志分页") + @PreAuthorize("@ss.hasPermission('infra:api-access-log:query')") + public CommonResult> getApiAccessLogPage(@Valid ApiAccessLogPageReqVO pageReqVO) { + PageResult pageResult = apiAccessLogService.getApiAccessLogPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, ApiAccessLogRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出API 访问日志 Excel") + @PreAuthorize("@ss.hasPermission('infra:api-access-log:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportApiAccessLogExcel(@Valid ApiAccessLogPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = apiAccessLogService.getApiAccessLogPage(exportReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "API 访问日志.xls", "数据", ApiAccessLogRespVO.class, + BeanUtils.toBean(list, ApiAccessLogRespVO.class)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiErrorLogController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiErrorLogController.java new file mode 100644 index 0000000..525bb9e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/ApiErrorLogController.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import cn.iocoder.yudao.module.infra.service.logger.ApiErrorLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - API 错误日志") +@RestController +@RequestMapping("/infra/api-error-log") +@Validated +public class ApiErrorLogController { + + @Resource + private ApiErrorLogService apiErrorLogService; + + @PutMapping("/update-status") + @Operation(summary = "更新 API 错误日志的状态") + @Parameters({ + @Parameter(name = "id", description = "编号", required = true, example = "1024"), + @Parameter(name = "processStatus", description = "处理状态", required = true, example = "1") + }) + @PreAuthorize("@ss.hasPermission('infra:api-error-log:update-status')") + public CommonResult updateApiErrorLogProcess(@RequestParam("id") Long id, + @RequestParam("processStatus") Integer processStatus) { + apiErrorLogService.updateApiErrorLogProcess(id, processStatus, getLoginUserId()); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得 API 错误日志分页") + @PreAuthorize("@ss.hasPermission('infra:api-error-log:query')") + public CommonResult> getApiErrorLogPage(@Valid ApiErrorLogPageReqVO pageReqVO) { + PageResult pageResult = apiErrorLogService.getApiErrorLogPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, ApiErrorLogRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出 API 错误日志 Excel") + @PreAuthorize("@ss.hasPermission('infra:api-error-log:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportApiErrorLogExcel(@Valid ApiErrorLogPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = apiErrorLogService.getApiErrorLogPage(exportReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "API 错误日志.xls", "数据", ApiErrorLogRespVO.class, + BeanUtils.toBean(list, ApiErrorLogRespVO.class)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java new file mode 100644 index 0000000..1da136b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - API 访问日志分页 Request VO") +@Data +public class ApiAccessLogPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "666") + private Long userId; + + @Schema(description = "用户类型", example = "2") + private Integer userType; + + @Schema(description = "应用名", example = "dashboard") + private String applicationName; + + @Schema(description = "请求地址,模糊匹配", example = "/xxx/yyy") + private String requestUrl; + + @Schema(description = "开始时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] beginTime; + + @Schema(description = "执行时长,大于等于,单位:毫秒", example = "100") + private Integer duration; + + @Schema(description = "结果码", example = "0") + private Integer resultCode; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java new file mode 100644 index 0000000..d9e65c4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - API 访问日志 Response VO") +@Data +@ExcelIgnoreUnannotated +public class ApiAccessLogRespVO { + + @Schema(description = "日志主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("日志主键") + private Long id; + + @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "66600cb6-7852-11eb-9439-0242ac130002") + @ExcelProperty("链路追踪编号") + private String traceId; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + @ExcelProperty("用户编号") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_TYPE) + private Integer userType; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard") + @ExcelProperty("应用名") + private String applicationName; + + @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET") + @ExcelProperty("请求方法名") + private String requestMethod; + + @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xxx/yyy") + @ExcelProperty("请求地址") + private String requestUrl; + + @Schema(description = "请求参数") + @ExcelProperty("请求参数") + private String requestParams; + + @Schema(description = "响应结果") + @ExcelProperty("响应结果") + private String responseBody; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @ExcelProperty("用户 IP") + private String userIp; + + @Schema(description = "浏览器 UA", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0") + @ExcelProperty("浏览器 UA") + private String userAgent; + + @Schema(description = "操作模块", requiredMode = Schema.RequiredMode.REQUIRED, example = "商品模块") + @ExcelProperty("操作模块") + private String operateModule; + + @Schema(description = "操作名", requiredMode = Schema.RequiredMode.REQUIRED, example = "创建商品") + @ExcelProperty("操作名") + private String operateName; + + @Schema(description = "操作分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "操作分类", converter = DictConvert.class) + @DictFormat(cn.iocoder.yudao.module.infra.enums.DictTypeConstants.OPERATE_TYPE) + private Integer operateType; + + @Schema(description = "开始请求时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("开始请求时间") + private LocalDateTime beginTime; + + @Schema(description = "结束请求时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("结束请求时间") + private LocalDateTime endTime; + + @Schema(description = "执行时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @ExcelProperty("执行时长") + private Integer duration; + + @Schema(description = "结果码", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @ExcelProperty("结果码") + private Integer resultCode; + + @Schema(description = "结果提示", example = "芋道源码,牛逼!") + @ExcelProperty("结果提示") + private String resultMsg; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java new file mode 100644 index 0000000..9611f69 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - API 错误日志分页 Request VO") +@Data +public class ApiErrorLogPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "666") + private Long userId; + + @Schema(description = "用户类型", example = "1") + private Integer userType; + + @Schema(description = "应用名", example = "dashboard") + private String applicationName; + + @Schema(description = "请求地址", example = "/xx/yy") + private String requestUrl; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "异常发生时间") + private LocalDateTime[] exceptionTime; + + @Schema(description = "处理状态", example = "0") + private Integer processStatus; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java new file mode 100644 index 0000000..a3f6f0e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java @@ -0,0 +1,112 @@ +package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - API 错误日志 Response VO") +@Data +@ExcelIgnoreUnannotated +public class ApiErrorLogRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "66600cb6-7852-11eb-9439-0242ac130002") + @ExcelProperty("链路追踪编号") + private String traceId; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + @ExcelProperty("用户编号") + private Long userId; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_TYPE) + private Integer userType; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard") + @ExcelProperty("应用名") + private String applicationName; + + @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET") + @ExcelProperty("请求方法名") + private String requestMethod; + + @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xx/yy") + @ExcelProperty("请求地址") + private String requestUrl; + + @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("请求参数") + private String requestParams; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @ExcelProperty("用户 IP") + private String userIp; + + @Schema(description = "浏览器 UA", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0") + @ExcelProperty("浏览器 UA") + private String userAgent; + + @Schema(description = "异常发生时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("异常发生时间") + private LocalDateTime exceptionTime; + + @Schema(description = "异常名", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("异常名") + private String exceptionName; + + @Schema(description = "异常导致的消息", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("异常导致的消息") + private String exceptionMessage; + + @Schema(description = "异常导致的根消息", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("异常导致的根消息") + private String exceptionRootCauseMessage; + + @Schema(description = "异常的栈轨迹", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("异常的栈轨迹") + private String exceptionStackTrace; + + @Schema(description = "异常发生的类全名", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("异常发生的类全名") + private String exceptionClassName; + + @Schema(description = "异常发生的类文件", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("异常发生的类文件") + private String exceptionFileName; + + @Schema(description = "异常发生的方法名", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("异常发生的方法名") + private String exceptionMethodName; + + @Schema(description = "异常发生的方法所在行", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("异常发生的方法所在行") + private Integer exceptionLineNumber; + + @Schema(description = "处理状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @ExcelProperty(value = "处理状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.API_ERROR_LOG_PROCESS_STATUS) + private Integer processStatus; + + @Schema(description = "处理时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("处理时间") + private LocalDateTime processTime; + + @Schema(description = "处理用户编号", example = "233") + @ExcelProperty("处理用户编号") + private Integer processUserId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http new file mode 100644 index 0000000..68b5487 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http @@ -0,0 +1,4 @@ +### 请求 /infra/redis/get-monitor-info 接口 => 成功 +GET {{baseUrl}}/infra/redis/get-monitor-info +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.java new file mode 100644 index 0000000..7ddfc0b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.infra.controller.admin.redis; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisMonitorRespVO; +import cn.iocoder.yudao.module.infra.convert.redis.RedisConvert; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Properties; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - Redis 监控") +@RestController +@RequestMapping("/infra/redis") +public class RedisController { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @GetMapping("/get-monitor-info") + @Operation(summary = "获得 Redis 监控信息") + @PreAuthorize("@ss.hasPermission('infra:redis:get-monitor-info')") + public CommonResult getRedisMonitorInfo() { + // 获得 Redis 统计信息 + Properties info = stringRedisTemplate.execute((RedisCallback) RedisServerCommands::info); + Long dbSize = stringRedisTemplate.execute(RedisServerCommands::dbSize); + Properties commandStats = stringRedisTemplate.execute(( + RedisCallback) connection -> connection.serverCommands().info("commandstats")); + assert commandStats != null; // 断言,避免警告 + // 拼接结果返回 + return success(RedisConvert.INSTANCE.build(info, dbSize, commandStats)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java new file mode 100644 index 0000000..cc0aa27 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.infra.controller.admin.redis.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Properties; + +@Schema(description = "管理后台 - Redis 监控信息 Response VO") +@Data +@Builder +@AllArgsConstructor +public class RedisMonitorRespVO { + + @Schema(description = "Redis info 指令结果,具体字段,查看 Redis 文档", requiredMode = Schema.RequiredMode.REQUIRED) + private Properties info; + + @Schema(description = "Redis key 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long dbSize; + + @Schema(description = "CommandStat 数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List commandStats; + + @Schema(description = "Redis 命令统计结果") + @Data + @Builder + @AllArgsConstructor + public static class CommandStat { + + @Schema(description = "Redis 命令", requiredMode = Schema.RequiredMode.REQUIRED, example = "get") + private String command; + + @Schema(description = "调用次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long calls; + + @Schema(description = "消耗 CPU 秒数", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private Long usec; + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java new file mode 100644 index 0000000..7f85e99 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.infra.controller.app.file; + +import cn.hutool.core.io.IoUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; +import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO; +import cn.iocoder.yudao.module.infra.service.file.FileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 文件存储") +@RestController +@RequestMapping("/infra/file") +@Validated +@Slf4j +public class AppFileController { + + @Resource + private FileService fileService; + + @PostMapping("/upload") + @Operation(summary = "上传文件") + @PermitAll + public CommonResult uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception { + MultipartFile file = uploadReqVO.getFile(); + byte[] content = IoUtil.readBytes(file.getInputStream()); + return success(fileService.createFile(content, file.getOriginalFilename(), + uploadReqVO.getDirectory(), file.getContentType())); + } + + @GetMapping("/presigned-url") + @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") + @Parameters({ + @Parameter(name = "name", description = "文件名称", required = true), + @Parameter(name = "directory", description = "文件目录") + }) + public CommonResult getFilePresignedUrl( + @RequestParam("name") String name, + @RequestParam(value = "directory", required = false) String directory) { + return success(fileService.getFilePresignedUrl(name, directory)); + } + + @PostMapping("/create") + @Operation(summary = "创建文件", description = "模式二:前端上传文件:配合 presigned-url 接口,记录上传了上传的文件") + @PermitAll + public CommonResult createFile(@Valid @RequestBody FileCreateReqVO createReqVO) { + return success(fileService.createFile(createReqVO)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java new file mode 100644 index 0000000..d10a21c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.infra.controller.app.file.vo; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +@Schema(description = "用户 App - 上传文件 Request VO") +@Data +public class AppFileUploadReqVO { + + @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件附件不能为空") + private MultipartFile file; + + @Schema(description = "文件目录", example = "XXX/YYY") + private String directory; + + @AssertTrue(message = "文件目录不正确") + @JsonIgnore + public boolean isDirectoryValid() { + return !StrUtil.containsAny(directory, "..", "/", "\\"); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/package-info.java new file mode 100644 index 0000000..d0f921f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.infra.controller.app; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/package-info.java new file mode 100644 index 0000000..04fde2f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.iocoder.yudao.module.infra.controller; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java new file mode 100644 index 0000000..6b97613 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.infra.convert.codegen; + +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import org.apache.ibatis.type.JdbcType; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface CodegenConvert { + + CodegenConvert INSTANCE = Mappers.getMapper(CodegenConvert.class); + + // ========== TableInfo 相关 ========== + + @Mappings({ + @Mapping(source = "name", target = "tableName"), + @Mapping(source = "comment", target = "tableComment"), + }) + CodegenTableDO convert(TableInfo bean); + + List convertList(List list); + + @Mappings({ + @Mapping(source = "name", target = "columnName"), + @Mapping(source = "metaInfo.jdbcType", target = "dataType", qualifiedByName = "getDataType"), + @Mapping(source = "comment", target = "columnComment"), + @Mapping(source = "metaInfo.nullable", target = "nullable"), + @Mapping(source = "keyFlag", target = "primaryKey"), + @Mapping(source = "columnType.type", target = "javaType"), + @Mapping(source = "propertyName", target = "javaField"), + }) + CodegenColumnDO convert(TableField bean); + + @Named("getDataType") + default String getDataType(JdbcType jdbcType) { + return jdbcType.name(); + } + + // ========== 其它 ========== + + default CodegenDetailRespVO convert(CodegenTableDO table, List columns) { + CodegenDetailRespVO respVO = new CodegenDetailRespVO(); + respVO.setTable(BeanUtils.toBean(table, CodegenTableRespVO.class)); + respVO.setColumns(BeanUtils.toBean(columns, CodegenColumnRespVO.class)); + return respVO; + } + + default List convert(Map codes) { + return CollectionUtils.convertList(codes.entrySet(), + entry -> new CodegenPreviewRespVO().setFilePath(entry.getKey()).setCode(entry.getValue())); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/config/ConfigConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/config/ConfigConvert.java new file mode 100644 index 0000000..92094b0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/config/ConfigConvert.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.infra.convert.config; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface ConfigConvert { + + ConfigConvert INSTANCE = Mappers.getMapper(ConfigConvert.class); + + PageResult convertPage(PageResult page); + + List convertList(List list); + + @Mapping(source = "configKey", target = "key") + ConfigRespVO convert(ConfigDO bean); + + @Mapping(source = "key", target = "configKey") + ConfigDO convert(ConfigSaveReqVO bean); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConfigConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConfigConvert.java new file mode 100644 index 0000000..7df2b57 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/file/FileConfigConvert.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.infra.convert.file; + +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * 文件配置 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface FileConfigConvert { + + FileConfigConvert INSTANCE = Mappers.getMapper(FileConfigConvert.class); + + @Mapping(target = "config", ignore = true) + FileConfigDO convert(FileConfigSaveReqVO bean); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/package-info.java new file mode 100644 index 0000000..8e27593 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package cn.iocoder.yudao.module.infra.convert; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/redis/RedisConvert.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/redis/RedisConvert.java new file mode 100644 index 0000000..0c8ca98 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/redis/RedisConvert.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.convert.redis; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.infra.controller.admin.redis.vo.RedisMonitorRespVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.Properties; + +@Mapper +public interface RedisConvert { + + RedisConvert INSTANCE = Mappers.getMapper(RedisConvert.class); + + default RedisMonitorRespVO build(Properties info, Long dbSize, Properties commandStats) { + RedisMonitorRespVO respVO = RedisMonitorRespVO.builder().info(info).dbSize(dbSize) + .commandStats(new ArrayList<>(commandStats.size())).build(); + commandStats.forEach((key, value) -> { + respVO.getCommandStats().add(RedisMonitorRespVO.CommandStat.builder() + .command(StrUtil.subAfter((String) key, "cmdstat_", false)) + .calls(Long.valueOf(StrUtil.subBetween((String) value, "calls=", ","))) + .usec(Long.valueOf(StrUtil.subBetween((String) value, "usec=", ","))) + .build()); + }); + return respVO; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..8153487 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java new file mode 100644 index 0000000..44063ac --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java @@ -0,0 +1,138 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.codegen; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 代码生成 column 字段定义 + * + * @author 芋道源码 + */ +@TableName(value = "infra_codegen_column", autoResultMap = true) +@KeySequence("infra_codegen_column_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@TenantIgnore +public class CodegenColumnDO extends BaseDO { + + /** + * ID 编号 + */ + @TableId + private Long id; + /** + * 表编号 + *

+ * 关联 {@link CodegenTableDO#getId()} + */ + private Long tableId; + + // ========== 表相关字段 ========== + + /** + * 字段名 + * + * 关联 {@link TableField#getName()} + */ + private String columnName; + /** + * 数据库字段类型 + * + * 关联 {@link TableField.MetaInfo#getJdbcType()} + */ + private String dataType; + /** + * 字段描述 + * + * 关联 {@link TableField#getComment()} + */ + private String columnComment; + /** + * 是否允许为空 + * + * 关联 {@link TableField.MetaInfo#isNullable()} + */ + private Boolean nullable; + /** + * 是否主键 + * + * 关联 {@link TableField#isKeyFlag()} + */ + private Boolean primaryKey; + /** + * 排序 + */ + private Integer ordinalPosition; + + // ========== Java 相关字段 ========== + + /** + * Java 属性类型 + * + * 例如说 String、Boolean 等等 + * + * 关联 {@link TableField#getColumnType()} + */ + private String javaType; + /** + * Java 属性名 + * + * 关联 {@link TableField#getPropertyName()} + */ + private String javaField; + /** + * 字典类型 + *

+ * 关联 DictTypeDO 的 type 属性 + */ + private String dictType; + /** + * 数据示例,主要用于生成 Swagger 注解的 example 字段 + */ + private String example; + + // ========== CRUD 相关字段 ========== + + /** + * 是否为 Create 创建操作的字段 + */ + private Boolean createOperation; + /** + * 是否为 Update 更新操作的字段 + */ + private Boolean updateOperation; + /** + * 是否为 List 查询操作的字段 + */ + private Boolean listOperation; + /** + * List 查询操作的条件类型 + *

+ * 枚举 {@link CodegenColumnListConditionEnum} + */ + private String listOperationCondition; + /** + * 是否为 List 查询操作的返回字段 + */ + private Boolean listOperationResult; + + // ========== UI 相关字段 ========== + + /** + * 显示类型 + *

+ * 枚举 {@link CodegenColumnHtmlTypeEnum} + */ + private String htmlType; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenTableDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenTableDO.java new file mode 100644 index 0000000..b46cbe4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenTableDO.java @@ -0,0 +1,160 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.codegen; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 代码生成 table 表定义 + * + * @author 芋道源码 + */ +@TableName(value = "infra_codegen_table", autoResultMap = true) +@KeySequence("infra_codegen_table_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@TenantIgnore +public class CodegenTableDO extends BaseDO { + + /** + * ID 编号 + */ + @TableId + private Long id; + + /** + * 数据源编号 + * + * 关联 {@link DataSourceConfigDO#getId()} + */ + private Long dataSourceConfigId; + /** + * 生成场景 + * + * 枚举 {@link CodegenSceneEnum} + */ + private Integer scene; + + // ========== 表相关字段 ========== + + /** + * 表名称 + * + * 关联 {@link TableInfo#getName()} + */ + private String tableName; + /** + * 表描述 + * + * 关联 {@link TableInfo#getComment()} + */ + private String tableComment; + /** + * 备注 + */ + private String remark; + + // ========== 类相关字段 ========== + + /** + * 模块名,即一级目录 + * + * 例如说,system、infra、tool 等等 + */ + private String moduleName; + /** + * 业务名,即二级目录 + * + * 例如说,user、permission、dict 等等 + */ + private String businessName; + /** + * 类名称(首字母大写) + * + * 例如说,SysUser、SysMenu、SysDictData 等等 + */ + private String className; + /** + * 类描述 + */ + private String classComment; + /** + * 作者 + */ + private String author; + + // ========== 生成相关字段 ========== + + /** + * 模板类型 + * + * 枚举 {@link CodegenTemplateTypeEnum} + */ + private Integer templateType; + /** + * 代码生成的前端类型 + * + * 枚举 {@link CodegenFrontTypeEnum} + */ + private Integer frontType; + + // ========== 菜单相关字段 ========== + + /** + * 父菜单编号 + * + * 关联 MenuDO 的 id 属性 + */ + private Long parentMenuId; + + // ========== 主子表相关字段 ========== + + /** + * 主表的编号 + * + * 关联 {@link CodegenTableDO#getId()} + */ + private Long masterTableId; + /** + * 【自己】子表关联主表的字段编号 + * + * 关联 {@link CodegenColumnDO#getId()} + */ + private Long subJoinColumnId; + /** + * 主表与子表是否一对多 + * + * true:一对多 + * false:一对一 + */ + private Boolean subJoinMany; + + // ========== 树表相关字段 ========== + + /** + * 树表的父字段编号 + * + * 关联 {@link CodegenColumnDO#getId()} + */ + private Long treeParentColumnId; + /** + * 树表的名字字段编号 + * + * 名字的用途:新增或修改时,select 框展示的字段 + * + * 关联 {@link CodegenColumnDO#getId()} + */ + private Long treeNameColumnId; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/config/ConfigDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/config/ConfigDO.java new file mode 100644 index 0000000..b0b37c3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/config/ConfigDO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.enums.config.ConfigTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 参数配置表 + * + * @author 芋道源码 + */ +@TableName("infra_config") +@KeySequence("infra_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@TenantIgnore +public class ConfigDO extends BaseDO { + + /** + * 参数主键 + */ + @TableId + private Long id; + /** + * 参数分类 + */ + private String category; + /** + * 参数名称 + */ + private String name; + /** + * 参数键名 + * + * 支持多 DB 类型时,无法直接使用 key + @TableField("config_key") 来实现转换,原因是 "config_key" AS key 而存在报错 + */ + private String configKey; + /** + * 参数键值 + */ + private String value; + /** + * 参数类型 + * + * 枚举 {@link ConfigTypeEnum} + */ + private Integer type; + /** + * 是否可见 + * + * 不可见的参数,一般是敏感参数,前端不可获取 + */ + private Boolean visible; + /** + * 备注 + */ + private String remark; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/db/DataSourceConfigDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/db/DataSourceConfigDO.java new file mode 100644 index 0000000..c906937 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/db/DataSourceConfigDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.db; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.EncryptTypeHandler; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 数据源配置 + * + * @author 芋道源码 + */ +@TableName(value = "infra_data_source_config", autoResultMap = true) +@KeySequence("infra_data_source_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@TenantIgnore +public class DataSourceConfigDO extends BaseDO { + + /** + * 主键编号 - Master 数据源 + */ + public static final Long ID_MASTER = 0L; + + /** + * 主键编号 + */ + private Long id; + /** + * 连接名 + */ + private String name; + + /** + * 数据源连接 + */ + private String url; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + @TableField(typeHandler = EncryptTypeHandler.class) + private String password; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo01/Demo01ContactDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo01/Demo01ContactDO.java new file mode 100644 index 0000000..de9c6d4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo01/Demo01ContactDO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 示例联系人 DO + * + * @author 芋道源码 + */ +@TableName("yudao_demo01_contact") +@KeySequence("yudao_demo01_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Demo01ContactDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 出生年 + */ + private LocalDateTime birthday; + /** + * 简介 + */ + private String description; + /** + * 头像 + */ + private String avatar; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo02/Demo02CategoryDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo02/Demo02CategoryDO.java new file mode 100644 index 0000000..55005dc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo02/Demo02CategoryDO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 示例分类 DO + * + * @author 芋道源码 + */ +@TableName("yudao_demo02_category") +@KeySequence("yudao_demo02_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Demo02CategoryDO extends BaseDO { + + public static final Long PARENT_ID_ROOT = 0L; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 父级编号 + */ + private Long parentId; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java new file mode 100644 index 0000000..6f7f3f4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 学生课程 DO + * + * @author 芋道源码 + */ +@TableName("yudao_demo03_course") +@KeySequence("yudao_demo03_course_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Demo03CourseDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 分数 + */ + private Integer score; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java new file mode 100644 index 0000000..afb7090 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 学生班级 DO + * + * @author 芋道源码 + */ +@TableName("yudao_demo03_grade") +@KeySequence("yudao_demo03_grade_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Demo03GradeDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 学生编号 + */ + private Long studentId; + /** + * 名字 + */ + private String name; + /** + * 班主任 + */ + private String teacher; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03StudentDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03StudentDO.java new file mode 100644 index 0000000..99995c2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03StudentDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 学生 DO + * + * @author 芋道源码 + */ +@TableName("yudao_demo03_student") +@KeySequence("yudao_demo03_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Demo03StudentDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 性别 + * + * 枚举 {@link TODO system_user_sex 对应的类} + */ + private Integer sex; + /** + * 出生日期 + */ + private LocalDateTime birthday; + /** + * 简介 + */ + private String description; + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileConfigDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileConfigDO.java new file mode 100644 index 0000000..4387983 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileConfigDO.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.file; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.db.DBFileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.enums.FileStorageEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.*; + +import java.lang.reflect.Field; + +/** + * 文件配置表 + * + * @author 芋道源码 + */ +@TableName(value = "infra_file_config", autoResultMap = true) +@KeySequence("infra_file_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TenantIgnore +public class FileConfigDO extends BaseDO { + + /** + * 配置编号,数据库自增 + */ + private Long id; + /** + * 配置名 + */ + private String name; + /** + * 存储器 + * + * 枚举 {@link FileStorageEnum} + */ + private Integer storage; + /** + * 备注 + */ + private String remark; + /** + * 是否为主配置 + * + * 由于我们可以配置多个文件配置,默认情况下,使用主配置进行文件的上传 + */ + private Boolean master; + + /** + * 支付渠道配置 + */ + @TableField(typeHandler = FileClientConfigTypeHandler.class) + private FileClientConfig config; + + public static class FileClientConfigTypeHandler extends AbstractJsonTypeHandler { + + public FileClientConfigTypeHandler(Class type) { + super(type); + } + + public FileClientConfigTypeHandler(Class type, Field field) { + super(type, field); + } + + @Override + public Object parse(String json) { + FileClientConfig config = JsonUtils.parseObjectQuietly(json, new TypeReference<>() { + }); + if (config != null) { + return config; + } + + // 兼容老版本的包路径 + String className = JsonUtils.parseObject(json, "@class", String.class); + className = StrUtil.subAfter(className, ".", true); + switch (className) { + case "DBFileClientConfig": + return JsonUtils.parseObject2(json, DBFileClientConfig.class); + case "FtpFileClientConfig": + return JsonUtils.parseObject2(json, FtpFileClientConfig.class); + case "LocalFileClientConfig": + return JsonUtils.parseObject2(json, LocalFileClientConfig.class); + case "SftpFileClientConfig": + return JsonUtils.parseObject2(json, SftpFileClientConfig.class); + case "S3FileClientConfig": + return JsonUtils.parseObject2(json, S3FileClientConfig.class); + default: + throw new IllegalArgumentException("未知的 FileClientConfig 类型:" + json); + } + } + + @Override + public String toJson(Object obj) { + return JsonUtils.toJsonString(obj); + } + + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileContentDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileContentDO.java new file mode 100644 index 0000000..8ae7a59 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileContentDO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.file; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.framework.file.core.client.db.DBFileClient; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 文件内容表 + * + * 专门用于存储 {@link DBFileClient} 的文件内容 + * + * @author 芋道源码 + */ +@TableName("infra_file_content") +@KeySequence("infra_file_content_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TenantIgnore +public class FileContentDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + @TableId + private Long id; + /** + * 配置编号 + * + * 关联 {@link FileConfigDO#getId()} + */ + private Long configId; + /** + * 路径,即文件名 + */ + private String path; + /** + * 文件内容 + */ + private byte[] content; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java new file mode 100644 index 0000000..5cb666e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.file; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 文件表 + * 每次文件上传,都会记录一条记录到该表中 + * + * @author 芋道源码 + */ +@TableName("infra_file") +@KeySequence("infra_file_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TenantIgnore +public class FileDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + private Long id; + /** + * 配置编号 + * + * 关联 {@link FileConfigDO#getId()} + */ + private Long configId; + /** + * 原文件名 + */ + private String name; + /** + * 路径,即文件名 + */ + private String path; + /** + * 访问地址 + */ + private String url; + /** + * 文件的 MIME 类型,例如 "application/octet-stream" + */ + private String type; + /** + * 文件大小 + */ + private Integer size; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobDO.java new file mode 100644 index 0000000..8080036 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobDO.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.job; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 定时任务 DO + * + * @author 芋道源码 + */ +@TableName("infra_job") +@KeySequence("infra_job_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TenantIgnore +public class JobDO extends BaseDO { + + /** + * 任务编号 + */ + @TableId + private Long id; + /** + * 任务名称 + */ + private String name; + /** + * 任务状态 + * + * 枚举 {@link JobStatusEnum} + */ + private Integer status; + /** + * 处理器的名字 + */ + private String handlerName; + /** + * 处理器的参数 + */ + private String handlerParam; + /** + * CRON 表达式 + */ + private String cronExpression; + + // ========== 重试相关字段 ========== + /** + * 重试次数 + * 如果不重试,则设置为 0 + */ + private Integer retryCount; + /** + * 重试间隔,单位:毫秒 + * 如果没有间隔,则设置为 0 + */ + private Integer retryInterval; + + // ========== 监控相关字段 ========== + /** + * 监控超时时间,单位:毫秒 + * 为空时,表示不监控 + * + * 注意,这里的超时的目的,不是进行任务的取消,而是告警任务的执行时间过长 + */ + private Integer monitorTimeout; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobLogDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobLogDO.java new file mode 100644 index 0000000..ac8eabb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/job/JobLogDO.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.job; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.enums.job.JobLogStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 定时任务的执行日志 + * + * @author 芋道源码 + */ +@TableName("infra_job_log") +@KeySequence("infra_job_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TenantIgnore +public class JobLogDO extends BaseDO { + + /** + * 日志编号 + */ + private Long id; + /** + * 任务编号 + * + * 关联 {@link JobDO#getId()} + */ + private Long jobId; + /** + * 处理器的名字 + * + * 冗余字段 {@link JobDO#getHandlerName()} + */ + private String handlerName; + /** + * 处理器的参数 + * + * 冗余字段 {@link JobDO#getHandlerParam()} + */ + private String handlerParam; + /** + * 第几次执行 + * + * 用于区分是不是重试执行。如果是重试执行,则 index 大于 1 + */ + private Integer executeIndex; + + /** + * 开始执行时间 + */ + private LocalDateTime beginTime; + /** + * 结束执行时间 + */ + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 状态 + * + * 枚举 {@link JobLogStatusEnum} + */ + private Integer status; + /** + * 结果数据 + * + * 成功时,使用 {@link JobHandler#execute(String)} 的结果 + * 失败时,使用 {@link JobHandler#execute(String)} 的异常堆栈 + */ + private String result; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiAccessLogDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiAccessLogDO.java new file mode 100644 index 0000000..4f671c7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiAccessLogDO.java @@ -0,0 +1,140 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.logger; + +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * API 访问日志 + * + * @author 芋道源码 + */ +@TableName("infra_api_access_log") +@KeySequence(value = "infra_api_access_log_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiAccessLogDO extends BaseDO { + + /** + * {@link #requestParams} 的最大长度 + */ + public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000; + + /** + * {@link #resultMsg} 的最大长度 + */ + public static final Integer RESULT_MSG_MAX_LENGTH = 512; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 应用名 + * + * 目前读取 `spring.application.name` 配置项 + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 响应结果 + */ + private String responseBody; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 执行相关字段 ========== + + /** + * 操作模块 + */ + private String operateModule; + /** + * 操作名 + */ + private String operateName; + /** + * 操作分类 + * + * 枚举 {@link OperateTypeEnum} + */ + private Integer operateType; + + /** + * 开始请求时间 + */ + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiErrorLogDO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiErrorLogDO.java new file mode 100644 index 0000000..87d6974 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiErrorLogDO.java @@ -0,0 +1,161 @@ +package cn.iocoder.yudao.module.infra.dal.dataobject.logger; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * API 异常数据 + * + * @author 芋道源码 + */ +@TableName("infra_api_error_log") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@KeySequence(value = "infra_api_error_log_seq") +public class ApiErrorLogDO extends BaseDO { + + /** + * {@link #requestParams} 的最大长度 + */ + public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 应用名 + * + * 目前读取 spring.application.name + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 异常相关字段 ========== + + /** + * 异常发生时间 + */ + private LocalDateTime exceptionTime; + /** + * 异常名 + * + * {@link Throwable#getClass()} 的类全名 + */ + private String exceptionName; + /** + * 异常导致的消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getMessage(Throwable)} + */ + private String exceptionMessage; + /** + * 异常导致的根消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getRootCauseMessage(Throwable)} + */ + private String exceptionRootCauseMessage; + /** + * 异常的栈轨迹 + * + * {@link org.apache.commons.lang3.exception.ExceptionUtils#getStackTrace(Throwable)} + */ + private String exceptionStackTrace; + /** + * 异常发生的类全名 + * + * {@link StackTraceElement#getClassName()} + */ + private String exceptionClassName; + /** + * 异常发生的类文件 + * + * {@link StackTraceElement#getFileName()} + */ + private String exceptionFileName; + /** + * 异常发生的方法名 + * + * {@link StackTraceElement#getMethodName()} + */ + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + * + * {@link StackTraceElement#getLineNumber()} + */ + private Integer exceptionLineNumber; + + // ========== 处理相关字段 ========== + + /** + * 处理状态 + * + * 枚举 {@link ApiErrorLogProcessStatusEnum} + */ + private Integer processStatus; + /** + * 处理时间 + */ + private LocalDateTime processTime; + /** + * 处理用户编号 + * + * 关联 cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.user.SysUserDO.SysUserDO#getId() + */ + private Long processUserId; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenColumnMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenColumnMapper.java new file mode 100644 index 0000000..28a705f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenColumnMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.codegen; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface CodegenColumnMapper extends BaseMapperX { + + default List selectListByTableId(Long tableId) { + return selectList(new LambdaQueryWrapperX() + .eq(CodegenColumnDO::getTableId, tableId) + .orderByAsc(CodegenColumnDO::getOrdinalPosition)); + } + + default void deleteListByTableId(Long tableId) { + delete(CodegenColumnDO::getTableId, tableId); + } + + default void deleteListByTableId(Collection tableIds) { + delete(new LambdaQueryWrapperX() + .in(CodegenColumnDO::getTableId, tableIds)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java new file mode 100644 index 0000000..0afe32a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.codegen; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface CodegenTableMapper extends BaseMapperX { + + default CodegenTableDO selectByTableNameAndDataSourceConfigId(String tableName, Long dataSourceConfigId) { + return selectOne(CodegenTableDO::getTableName, tableName, + CodegenTableDO::getDataSourceConfigId, dataSourceConfigId); + } + + default PageResult selectPage(CodegenTablePageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(CodegenTableDO::getTableName, pageReqVO.getTableName()) + .likeIfPresent(CodegenTableDO::getTableComment, pageReqVO.getTableComment()) + .likeIfPresent(CodegenTableDO::getClassName, pageReqVO.getClassName()) + .betweenIfPresent(CodegenTableDO::getCreateTime, pageReqVO.getCreateTime()) + .orderByDesc(CodegenTableDO::getUpdateTime)); + } + + default List selectListByDataSourceConfigId(Long dataSourceConfigId) { + return selectList(CodegenTableDO::getDataSourceConfigId, dataSourceConfigId); + } + + default List selectListByTemplateTypeAndMasterTableId(Integer templateType, Long masterTableId) { + return selectList(CodegenTableDO::getTemplateType, templateType, + CodegenTableDO::getMasterTableId, masterTableId); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigMapper.java new file mode 100644 index 0000000..7d36610 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/config/ConfigMapper.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.config; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ConfigMapper extends BaseMapperX { + + default ConfigDO selectByKey(String key) { + return selectOne(ConfigDO::getConfigKey, key); + } + + default PageResult selectPage(ConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(ConfigDO::getName, reqVO.getName()) + .likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey()) + .eqIfPresent(ConfigDO::getType, reqVO.getType()) + .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(ConfigDO::getId)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/db/DataSourceConfigMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/db/DataSourceConfigMapper.java new file mode 100644 index 0000000..5ad8be4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/db/DataSourceConfigMapper.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.db; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 数据源配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface DataSourceConfigMapper extends BaseMapperX { +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo01/Demo01ContactMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo01/Demo01ContactMapper.java new file mode 100644 index 0000000..916dcfc --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo01/Demo01ContactMapper.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo01; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 示例联系人 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo01ContactMapper extends BaseMapperX { + + default PageResult selectPage(Demo01ContactPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(Demo01ContactDO::getName, reqVO.getName()) + .eqIfPresent(Demo01ContactDO::getSex, reqVO.getSex()) + .betweenIfPresent(Demo01ContactDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(Demo01ContactDO::getId)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo02/Demo02CategoryMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo02/Demo02CategoryMapper.java new file mode 100644 index 0000000..72dea77 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo02/Demo02CategoryMapper.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo02; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 示例分类 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo02CategoryMapper extends BaseMapperX { + + default List selectList(Demo02CategoryListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(Demo02CategoryDO::getName, reqVO.getName()) + .eqIfPresent(Demo02CategoryDO::getParentId, reqVO.getParentId()) + .betweenIfPresent(Demo02CategoryDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(Demo02CategoryDO::getId)); + } + + default Demo02CategoryDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(Demo02CategoryDO::getParentId, parentId, Demo02CategoryDO::getName, name); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(Demo02CategoryDO::getParentId, parentId); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/erp/Demo03CourseErpMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/erp/Demo03CourseErpMapper.java new file mode 100644 index 0000000..cbd8707 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/erp/Demo03CourseErpMapper.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.erp; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 学生课程 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03CourseErpMapper extends BaseMapperX { + + default PageResult selectPage(PageParam reqVO, Long studentId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(Demo03CourseDO::getStudentId, studentId) + .orderByDesc(Demo03CourseDO::getId)); + } + + default int deleteByStudentId(Long studentId) { + return delete(Demo03CourseDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(Demo03CourseDO::getStudentId, studentIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/erp/Demo03GradeErpMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/erp/Demo03GradeErpMapper.java new file mode 100644 index 0000000..cf61430 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/erp/Demo03GradeErpMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.erp; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 学生班级 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03GradeErpMapper extends BaseMapperX { + + default PageResult selectPage(PageParam reqVO, Long studentId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(Demo03GradeDO::getStudentId, studentId) + .orderByDesc(Demo03GradeDO::getId)); + } + + default Demo03GradeDO selectByStudentId(Long studentId) { + return selectOne(Demo03GradeDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(Demo03GradeDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(Demo03GradeDO::getStudentId, studentIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/erp/Demo03StudentErpMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/erp/Demo03StudentErpMapper.java new file mode 100644 index 0000000..4387ef5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/erp/Demo03StudentErpMapper.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.erp; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo.Demo03StudentErpPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03StudentErpMapper extends BaseMapperX { + + default PageResult selectPage(Demo03StudentErpPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(Demo03StudentDO::getName, reqVO.getName()) + .eqIfPresent(Demo03StudentDO::getSex, reqVO.getSex()) + .eqIfPresent(Demo03StudentDO::getDescription, reqVO.getDescription()) + .betweenIfPresent(Demo03StudentDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(Demo03StudentDO::getId)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/inner/Demo03CourseInnerMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/inner/Demo03CourseInnerMapper.java new file mode 100644 index 0000000..74e45c4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/inner/Demo03CourseInnerMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.inner; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 学生课程 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03CourseInnerMapper extends BaseMapperX { + + default List selectListByStudentId(Long studentId) { + return selectList(Demo03CourseDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(Demo03CourseDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(Demo03CourseDO::getStudentId, studentIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/inner/Demo03GradeInnerMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/inner/Demo03GradeInnerMapper.java new file mode 100644 index 0000000..bb91d24 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/inner/Demo03GradeInnerMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.inner; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 学生班级 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03GradeInnerMapper extends BaseMapperX { + + default Demo03GradeDO selectByStudentId(Long studentId) { + return selectOne(Demo03GradeDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(Demo03GradeDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(Demo03GradeDO::getStudentId, studentIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/inner/Demo03StudentInnerMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/inner/Demo03StudentInnerMapper.java new file mode 100644 index 0000000..6a329fd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/inner/Demo03StudentInnerMapper.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.inner; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo.Demo03StudentInnerPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03StudentInnerMapper extends BaseMapperX { + + default PageResult selectPage(Demo03StudentInnerPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(Demo03StudentDO::getName, reqVO.getName()) + .eqIfPresent(Demo03StudentDO::getSex, reqVO.getSex()) + .eqIfPresent(Demo03StudentDO::getDescription, reqVO.getDescription()) + .betweenIfPresent(Demo03StudentDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(Demo03StudentDO::getId)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/normal/Demo03CourseNormalMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/normal/Demo03CourseNormalMapper.java new file mode 100644 index 0000000..91ea892 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/normal/Demo03CourseNormalMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.normal; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 学生课程 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03CourseNormalMapper extends BaseMapperX { + + default List selectListByStudentId(Long studentId) { + return selectList(Demo03CourseDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(Demo03CourseDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(Demo03CourseDO::getStudentId, studentIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/normal/Demo03GradeNormalMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/normal/Demo03GradeNormalMapper.java new file mode 100644 index 0000000..c883f90 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/normal/Demo03GradeNormalMapper.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.normal; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 学生班级 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03GradeNormalMapper extends BaseMapperX { + + default Demo03GradeDO selectByStudentId(Long studentId) { + return selectOne(Demo03GradeDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(Demo03GradeDO::getStudentId, studentId); + } + + default int deleteByStudentIds(List studentIds) { + return deleteBatch(Demo03GradeDO::getStudentId, studentIds); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/normal/Demo03StudentNormalMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/normal/Demo03StudentNormalMapper.java new file mode 100644 index 0000000..268b1de --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/normal/Demo03StudentNormalMapper.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.normal; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo.Demo03StudentNormalPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03StudentNormalMapper extends BaseMapperX { + + default PageResult selectPage(Demo03StudentNormalPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(Demo03StudentDO::getName, reqVO.getName()) + .eqIfPresent(Demo03StudentDO::getSex, reqVO.getSex()) + .eqIfPresent(Demo03StudentDO::getDescription, reqVO.getDescription()) + .betweenIfPresent(Demo03StudentDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(Demo03StudentDO::getId)); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileConfigMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileConfigMapper.java new file mode 100644 index 0000000..c0e2af3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileConfigMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.file; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FileConfigMapper extends BaseMapperX { + + default PageResult selectPage(FileConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(FileConfigDO::getName, reqVO.getName()) + .eqIfPresent(FileConfigDO::getStorage, reqVO.getStorage()) + .betweenIfPresent(FileConfigDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(FileConfigDO::getId)); + } + + default FileConfigDO selectByMaster() { + return selectOne(FileConfigDO::getMaster, true); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentMapper.java new file mode 100644 index 0000000..3d6ec51 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.file; + +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface FileContentMapper extends BaseMapper { + + default void deleteByConfigIdAndPath(Long configId, String path) { + this.delete(new LambdaQueryWrapper() + .eq(FileContentDO::getConfigId, configId) + .eq(FileContentDO::getPath, path)); + } + + default List selectListByConfigIdAndPath(Long configId, String path) { + return selectList(new LambdaQueryWrapper() + .eq(FileContentDO::getConfigId, configId) + .eq(FileContentDO::getPath, path)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java new file mode 100644 index 0000000..11233a4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.file; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 文件操作 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FileMapper extends BaseMapperX { + + default PageResult selectPage(FilePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(FileDO::getPath, reqVO.getPath()) + .likeIfPresent(FileDO::getType, reqVO.getType()) + .betweenIfPresent(FileDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(FileDO::getId)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobLogMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobLogMapper.java new file mode 100644 index 0000000..3902339 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobLogMapper.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; + +/** + * 任务日志 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface JobLogMapper extends BaseMapperX { + + default PageResult selectPage(JobLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(JobLogDO::getJobId, reqVO.getJobId()) + .likeIfPresent(JobLogDO::getHandlerName, reqVO.getHandlerName()) + .geIfPresent(JobLogDO::getBeginTime, reqVO.getBeginTime()) + .leIfPresent(JobLogDO::getEndTime, reqVO.getEndTime()) + .eqIfPresent(JobLogDO::getStatus, reqVO.getStatus()) + .orderByDesc(JobLogDO::getId) // ID 倒序 + ); + } + + /** + * 物理删除指定时间之前的日志 + * + * @param createTime 最大时间 + * @param limit 删除条数,防止一次删除太多 + * @return 删除条数 + */ + @Delete("DELETE FROM infra_job_log WHERE create_time < #{createTime} LIMIT #{limit}") + Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobMapper.java new file mode 100644 index 0000000..481b4e7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobMapper.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 定时任务 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface JobMapper extends BaseMapperX { + + default JobDO selectByHandlerName(String handlerName) { + return selectOne(JobDO::getHandlerName, handlerName); + } + + default PageResult selectPage(JobPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(JobDO::getName, reqVO.getName()) + .eqIfPresent(JobDO::getStatus, reqVO.getStatus()) + .likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName()) + .orderByDesc(JobDO::getId)); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiAccessLogMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiAccessLogMapper.java new file mode 100644 index 0000000..820c4b1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiAccessLogMapper.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.logger; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; + +/** + * API 访问日志 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface ApiAccessLogMapper extends BaseMapperX { + + default PageResult selectPage(ApiAccessLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(ApiAccessLogDO::getUserId, reqVO.getUserId()) + .eqIfPresent(ApiAccessLogDO::getUserType, reqVO.getUserType()) + .eqIfPresent(ApiAccessLogDO::getApplicationName, reqVO.getApplicationName()) + .likeIfPresent(ApiAccessLogDO::getRequestUrl, reqVO.getRequestUrl()) + .betweenIfPresent(ApiAccessLogDO::getBeginTime, reqVO.getBeginTime()) + .geIfPresent(ApiAccessLogDO::getDuration, reqVO.getDuration()) + .eqIfPresent(ApiAccessLogDO::getResultCode, reqVO.getResultCode()) + .orderByDesc(ApiAccessLogDO::getId) + ); + } + + /** + * 物理删除指定时间之前的日志 + * + * @param createTime 最大时间 + * @param limit 删除条数,防止一次删除太多 + * @return 删除条数 + */ + @Delete("DELETE FROM infra_api_access_log WHERE create_time < #{createTime} LIMIT #{limit}") + Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiErrorLogMapper.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiErrorLogMapper.java new file mode 100644 index 0000000..f220696 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiErrorLogMapper.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.infra.dal.mysql.logger; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; + +/** + * API 错误日志 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface ApiErrorLogMapper extends BaseMapperX { + + default PageResult selectPage(ApiErrorLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(ApiErrorLogDO::getUserId, reqVO.getUserId()) + .eqIfPresent(ApiErrorLogDO::getUserType, reqVO.getUserType()) + .eqIfPresent(ApiErrorLogDO::getApplicationName, reqVO.getApplicationName()) + .likeIfPresent(ApiErrorLogDO::getRequestUrl, reqVO.getRequestUrl()) + .betweenIfPresent(ApiErrorLogDO::getExceptionTime, reqVO.getExceptionTime()) + .eqIfPresent(ApiErrorLogDO::getProcessStatus, reqVO.getProcessStatus()) + .orderByDesc(ApiErrorLogDO::getId) + ); + } + + /** + * 物理删除指定时间之前的日志 + * + * @param createTime 最大时间 + * @param limit 删除条数,防止一次删除太多 + * @return 删除条数 + */ + @Delete("DELETE FROM infra_api_error_log WHERE create_time < #{createTime} LIMIT #{limit}") + Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java new file mode 100644 index 0000000..fcab9e6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/DictTypeConstants.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.infra.enums; + +/** + * Infra 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String USER_TYPE = "user_type"; // 用户类型 + + String JOB_STATUS = "infra_job_status"; // 定时任务状态的枚举 + String JOB_LOG_STATUS = "infra_job_log_status"; // 定时任务日志状态的枚举 + + String API_ERROR_LOG_PROCESS_STATUS = "infra_api_error_log_process_status"; // API 错误日志的处理状态的枚举 + + String CONFIG_TYPE = "infra_config_type"; // 参数配置类型 + String BOOLEAN_STRING = "infra_boolean_string"; // Boolean 是否类型 + + String OPERATE_TYPE = "infra_operate_type"; // 操作类型 + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..2233f35 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.infra.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * Infra 错误码枚举类 + * + * infra 系统,使用 1-001-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 参数配置 1-001-000-000 ========== + ErrorCode CONFIG_NOT_EXISTS = new ErrorCode(1_001_000_001, "参数配置不存在"); + ErrorCode CONFIG_KEY_DUPLICATE = new ErrorCode(1_001_000_002, "参数配置 key 重复"); + ErrorCode CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE = new ErrorCode(1_001_000_003, "不能删除类型为系统内置的参数配置"); + ErrorCode CONFIG_GET_VALUE_ERROR_IF_VISIBLE = new ErrorCode(1_001_000_004, "获取参数配置失败,原因:不允许获取不可见配置"); + + // ========== 定时任务 1-001-001-000 ========== + ErrorCode JOB_NOT_EXISTS = new ErrorCode(1_001_001_000, "定时任务不存在"); + ErrorCode JOB_HANDLER_EXISTS = new ErrorCode(1_001_001_001, "定时任务的处理器已经存在"); + ErrorCode JOB_CHANGE_STATUS_INVALID = new ErrorCode(1_001_001_002, "只允许修改为开启或者关闭状态"); + ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改"); + ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改"); + ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确"); + ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在,注意 Bean 默认首字母小写"); + ErrorCode JOB_HANDLER_BEAN_TYPE_ERROR = new ErrorCode(1_001_001_007, "定时任务的处理器 Bean 类型不正确,未实现 JobHandler 接口"); + + // ========== API 错误日志 1-001-002-000 ========== + ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1_001_002_000, "API 错误日志不存在"); + ErrorCode API_ERROR_LOG_PROCESSED = new ErrorCode(1_001_002_001, "API 错误日志已处理"); + + // ========= 文件相关 1-001-003-000 ================= + ErrorCode FILE_PATH_EXISTS = new ErrorCode(1_001_003_000, "文件路径已存在"); + ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在"); + ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空"); + + // ========== 代码生成器 1-001-004-000 ========== + ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1_001_004_002, "表定义已经存在"); + ErrorCode CODEGEN_IMPORT_TABLE_NULL = new ErrorCode(1_001_004_001, "导入的表不存在"); + ErrorCode CODEGEN_IMPORT_COLUMNS_NULL = new ErrorCode(1_001_004_002, "导入的字段不存在"); + ErrorCode CODEGEN_TABLE_NOT_EXISTS = new ErrorCode(1_001_004_004, "表定义不存在"); + ErrorCode CODEGEN_COLUMN_NOT_EXISTS = new ErrorCode(1_001_004_005, "字段义不存在"); + ErrorCode CODEGEN_SYNC_COLUMNS_NULL = new ErrorCode(1_001_004_006, "同步的字段不存在"); + ErrorCode CODEGEN_SYNC_NONE_CHANGE = new ErrorCode(1_001_004_007, "同步失败,不存在改变"); + ErrorCode CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL = new ErrorCode(1_001_004_008, "数据库的表注释未填写"); + ErrorCode CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL = new ErrorCode(1_001_004_009, "数据库的表字段({})注释未填写"); + ErrorCode CODEGEN_MASTER_TABLE_NOT_EXISTS = new ErrorCode(1_001_004_010, "主表(id={})定义不存在,请检查"); + ErrorCode CODEGEN_SUB_COLUMN_NOT_EXISTS = new ErrorCode(1_001_004_011, "子表的字段(id={})不存在,请检查"); + ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE = new ErrorCode(1_001_004_012, "主表生成代码失败,原因:它没有子表"); + + // ========== 文件配置 1-001-006-000 ========== + ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_006_000, "文件配置不存在"); + ErrorCode FILE_CONFIG_DELETE_FAIL_MASTER = new ErrorCode(1_001_006_001, "该文件配置不允许删除,原因:它是主配置,删除会导致无法上传文件"); + + // ========== 数据源配置 1-001-007-000 ========== + ErrorCode DATA_SOURCE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_007_000, "数据源配置不存在"); + ErrorCode DATA_SOURCE_CONFIG_NOT_OK = new ErrorCode(1_001_007_001, "数据源配置不正确,无法进行连接"); + + // ========== 学生 1-001-201-000 ========== + ErrorCode DEMO01_CONTACT_NOT_EXISTS = new ErrorCode(1_001_201_000, "示例联系人不存在"); + ErrorCode DEMO02_CATEGORY_NOT_EXISTS = new ErrorCode(1_001_201_001, "示例分类不存在"); + ErrorCode DEMO02_CATEGORY_EXITS_CHILDREN = new ErrorCode(1_001_201_002, "存在存在子示例分类,无法删除"); + ErrorCode DEMO02_CATEGORY_PARENT_NOT_EXITS = new ErrorCode(1_001_201_003,"父级示例分类不存在"); + ErrorCode DEMO02_CATEGORY_PARENT_ERROR = new ErrorCode(1_001_201_004, "不能设置自己为父示例分类"); + ErrorCode DEMO02_CATEGORY_NAME_DUPLICATE = new ErrorCode(1_001_201_005, "已经存在该名字的示例分类"); + ErrorCode DEMO02_CATEGORY_PARENT_IS_CHILD = new ErrorCode(1_001_201_006, "不能设置自己的子示例分类为父示例分类"); + ErrorCode DEMO03_STUDENT_NOT_EXISTS = new ErrorCode(1_001_201_007, "学生不存在"); + ErrorCode DEMO03_COURSE_NOT_EXISTS = new ErrorCode(1_001_201_008, "学生课程不存在"); + ErrorCode DEMO03_GRADE_NOT_EXISTS = new ErrorCode(1_001_201_009, "学生班级不存在"); + ErrorCode DEMO03_GRADE_EXISTS = new ErrorCode(1_001_201_010, "学生班级已存在"); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java new file mode 100644 index 0000000..2c0cdbb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成器的字段 HTML 展示枚举 + */ +@AllArgsConstructor +@Getter +public enum CodegenColumnHtmlTypeEnum { + + INPUT("input"), // 文本框 + TEXTAREA("textarea"), // 文本域 + SELECT("select"), // 下拉框 + RADIO("radio"), // 单选框 + CHECKBOX("checkbox"), // 复选框 + DATETIME("datetime"), // 日期控件 + IMAGE_UPLOAD("imageUpload"), // 上传图片 + FILE_UPLOAD("fileUpload"), // 上传文件 + EDITOR("editor"), // 富文本控件 + ; + + /** + * 条件 + */ + private final String type; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnListConditionEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnListConditionEnum.java new file mode 100644 index 0000000..7098700 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnListConditionEnum.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成器的字段过滤条件枚举 + */ +@AllArgsConstructor +@Getter +public enum CodegenColumnListConditionEnum { + + EQ("="), + NE("!="), + GT(">"), + GTE(">="), + LT("<"), + LTE("<="), + LIKE("LIKE"), + BETWEEN("BETWEEN"); + + /** + * 条件 + */ + private final String condition; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java new file mode 100644 index 0000000..0ff9e56 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成的前端类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenFrontTypeEnum { + + VUE2_ELEMENT_UI(10), // Vue2 Element UI 标准模版 + + VUE3_ELEMENT_PLUS(20), // Vue3 Element Plus 标准模版 + + VUE3_VBEN2_ANTD_SCHEMA(30), // Vue3 VBEN2 + ANTD + Schema 模版 + + VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版 + VUE3_VBEN5_ANTD_GENERAL(41), // Vue3 VBEN5 + ANTD 标准模版 + + // TODO @puhui999::50、51 会好点; + VUE3_VBEN5_EP_SCHEMA(42), // Vue3 VBEN5 + EP + schema 模版 + VUE3_VBEN5_EP_GENERAL(43), // Vue3 VBEN5 + EP 标准模版 + ; + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenSceneEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenSceneEnum.java new file mode 100644 index 0000000..f1fdea0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenSceneEnum.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import static cn.hutool.core.util.ArrayUtil.*; + +/** + * 代码生成的场景枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenSceneEnum { + + ADMIN(1, "管理后台", "admin", ""), + APP(2, "用户 APP", "app", "App"); + + /** + * 场景 + */ + private final Integer scene; + /** + * 场景名 + */ + private final String name; + /** + * 基础包名 + */ + private final String basePackage; + /** + * Controller 和 VO 类的前缀 + */ + private final String prefixClass; + + public static CodegenSceneEnum valueOf(Integer scene) { + return firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene), values()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenTemplateTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenTemplateTypeEnum.java new file mode 100644 index 0000000..9a21833 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenTemplateTypeEnum.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 代码生成模板类型 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenTemplateTypeEnum { + + ONE(1), // 单表(增删改查) + TREE(2), // 树表(增删改查) + + MASTER_NORMAL(10), // 主子表 - 主表 - 普通模式 + MASTER_ERP(11), // 主子表 - 主表 - ERP 模式 + MASTER_INNER(12), // 主子表 - 主表 - 内嵌模式 + SUB(15), // 主子表 - 子表 + ; + + /** + * 类型 + */ + private final Integer type; + + /** + * 是否为主表 + * + * @param type 类型 + * @return 是否主表 + */ + public static boolean isMaster(Integer type) { + return ObjectUtils.equalsAny(type, + MASTER_NORMAL.type, MASTER_ERP.type, MASTER_INNER.type); + } + + /** + * 是否为树表 + * + * @param type 类型 + * @return 是否树表 + */ + public static boolean isTree(Integer type) { + return Objects.equals(type, TREE.type); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenVOTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenVOTypeEnum.java new file mode 100644 index 0000000..60b4790 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenVOTypeEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成的 VO 类型枚举 + * + * 目前的作用:Controller 新增、修改、响应时,使用 VO 还是 DO + * 注意:不包括 Controller 的分页参数! + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenVOTypeEnum { + + VO(10, "VO"), + DO(20, "DO"); + + /** + * 场景 + */ + private final Integer type; + /** + * 场景名 + */ + private final String name; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/config/ConfigTypeEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/config/ConfigTypeEnum.java new file mode 100644 index 0000000..15c2015 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/config/ConfigTypeEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.infra.enums.config; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ConfigTypeEnum { + + /** + * 系统配置 + */ + SYSTEM(1), + /** + * 自定义配置 + */ + CUSTOM(2); + + private final Integer type; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobLogStatusEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobLogStatusEnum.java new file mode 100644 index 0000000..9567053 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobLogStatusEnum.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.enums.job; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 任务日志的状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum JobLogStatusEnum { + + RUNNING(0), // 运行中 + SUCCESS(1), // 成功 + FAILURE(2); // 失败 + + /** + * 状态 + */ + private final Integer status; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobStatusEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobStatusEnum.java new file mode 100644 index 0000000..3d9ec3f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/job/JobStatusEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.infra.enums.job; + +import com.google.common.collect.Sets; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.quartz.impl.jdbcjobstore.Constants; + +import java.util.Collections; +import java.util.Set; + +/** + * 任务状态的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum JobStatusEnum { + + /** + * 初始化中 + */ + INIT(0, Collections.emptySet()), + /** + * 开启 + */ + NORMAL(1, Sets.newHashSet(Constants.STATE_WAITING, Constants.STATE_ACQUIRED, Constants.STATE_BLOCKED)), + /** + * 暂停 + */ + STOP(2, Sets.newHashSet(Constants.STATE_PAUSED, Constants.STATE_PAUSED_BLOCKED)); + + /** + * 状态 + */ + private final Integer status; + /** + * 对应的 Quartz 触发器的状态集合 + */ + private final Set quartzStates; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java new file mode 100644 index 0000000..b309753 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.infra.enums.logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * API 异常数据的处理状态 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum ApiErrorLogProcessStatusEnum { + + INIT(0, "未处理"), + DONE(1, "已处理"), + IGNORE(2, "已忽略"); + + /** + * 状态 + */ + private final Integer status; + /** + * 资源类型名 + */ + private final String name; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/package-info.java new file mode 100644 index 0000000..0749e8e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.infra.enums; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenConfiguration.java new file mode 100644 index 0000000..d49723d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenConfiguration.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.module.infra.framework.codegen.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(CodegenProperties.class) +public class CodegenConfiguration { +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenProperties.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenProperties.java new file mode 100644 index 0000000..b764f5a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/config/CodegenProperties.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.infra.framework.codegen.config; + +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.Collection; + +@ConfigurationProperties(prefix = "yudao.codegen") +@Validated +@Data +public class CodegenProperties { + + /** + * 生成的 Java 代码的基础包 + */ + @NotNull(message = "Java 代码的基础包不能为空") + private String basePackage; + + /** + * 数据库名数组 + */ + @NotEmpty(message = "数据库不能为空") + private Collection dbSchemas; + + /** + * 代码生成的前端类型(默认) + * + * 枚举 {@link CodegenFrontTypeEnum#getType()} + */ + @NotNull(message = "代码生成的前端类型不能为空") + private Integer frontType; + + /** + * 代码生成的 VO 类型 + * + * 枚举 {@link CodegenVOTypeEnum#getType()} + */ + @NotNull(message = "代码生成的 VO 类型不能为空") + private Integer voType; + + /** + * 是否生成批量删除接口 + */ + @NotNull(message = "是否生成批量删除接口不能为空") + private Boolean deleteBatchEnable; + + /** + * 是否生成单元测试 + */ + @NotNull(message = "是否生成单元测试不能为空") + private Boolean unitTestEnable; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/package-info.java new file mode 100644 index 0000000..cf1d22c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/codegen/package-info.java @@ -0,0 +1,4 @@ +/** + * 代码生成器 + */ +package cn.iocoder.yudao.module.infra.framework.codegen; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/config/YudaoFileAutoConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/config/YudaoFileAutoConfiguration.java new file mode 100644 index 0000000..e0d3f8e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/config/YudaoFileAutoConfiguration.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.infra.framework.file.config; + +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientFactory; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientFactoryImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 文件配置类 + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class YudaoFileAutoConfiguration { + + @Bean + public FileClientFactory fileClientFactory() { + return new FileClientFactoryImpl(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/AbstractFileClient.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/AbstractFileClient.java new file mode 100644 index 0000000..3c7883b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/AbstractFileClient.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * 文件客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractFileClient implements FileClient { + + /** + * 配置编号 + */ + private final Long id; + /** + * 文件配置 + */ + protected Config config; + + public AbstractFileClient(Long id, Config config) { + this.id = id; + this.config = config; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.debug("[init][配置({}) 初始化完成]", config); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(Config config) { + // 判断是否更新 + if (config.equals(this.config)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", config); + this.config = config; + // 初始化 + this.init(); + } + + @Override + public Long getId() { + return id; + } + + /** + * 格式化文件的 URL 访问地址 + * 使用场景:local、ftp、db,通过 FileController 的 getFile 来获取文件内容 + * + * @param domain 自定义域名 + * @param path 文件路径 + * @return URL 访问地址 + */ + protected String formatFileUrl(String domain, String path) { + return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), path); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java new file mode 100644 index 0000000..053b3c5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client; + +import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; + +/** + * 文件客户端 + * + * @author 芋道源码 + */ +public interface FileClient { + + /** + * 获得客户端编号 + * + * @return 客户端编号 + */ + Long getId(); + + /** + * 上传文件 + * + * @param content 文件流 + * @param path 相对路径 + * @return 完整路径,即 HTTP 访问地址 + * @throws Exception 上传文件时,抛出 Exception 异常 + */ + String upload(byte[] content, String path, String type) throws Exception; + + /** + * 删除文件 + * + * @param path 相对路径 + * @throws Exception 删除文件时,抛出 Exception 异常 + */ + void delete(String path) throws Exception; + + /** + * 获得文件的内容 + * + * @param path 相对路径 + * @return 文件的内容 + */ + byte[] getContent(String path) throws Exception; + + /** + * 获得文件预签名地址 + * + * @param path 相对路径 + * @return 文件预签名地址 + */ + default FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception { + throw new UnsupportedOperationException("不支持的操作"); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClientConfig.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClientConfig.java new file mode 100644 index 0000000..ed09275 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClientConfig.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * 文件客户端的配置 + * 不同实现的客户端,需要不同的配置,通过子类来定义 + * + * @author 芋道源码 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +// @JsonTypeInfo 注解的作用,Jackson 多态 +// 1. 序列化到时数据库时,增加 @class 属性。 +// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 +public interface FileClientConfig { +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClientFactory.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClientFactory.java new file mode 100644 index 0000000..e4c84ed --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClientFactory.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client; + +import cn.iocoder.yudao.module.infra.framework.file.core.enums.FileStorageEnum; + +public interface FileClientFactory { + + /** + * 获得文件客户端 + * + * @param configId 配置编号 + * @return 文件客户端 + */ + FileClient getFileClient(Long configId); + + /** + * 创建文件客户端 + * + * @param configId 配置编号 + * @param storage 存储器的枚举 {@link FileStorageEnum} + * @param config 文件配置 + */ + void createOrUpdateFileClient(Long configId, Integer storage, Config config); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClientFactoryImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClientFactoryImpl.java new file mode 100644 index 0000000..2bd5464 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClientFactoryImpl.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.module.infra.framework.file.core.enums.FileStorageEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 文件客户端的工厂实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class FileClientFactoryImpl implements FileClientFactory { + + /** + * 文件客户端 Map + * key:配置编号 + */ + private final ConcurrentMap> clients = new ConcurrentHashMap<>(); + + @Override + public FileClient getFileClient(Long configId) { + AbstractFileClient client = clients.get(configId); + if (client == null) { + log.error("[getFileClient][配置编号({}) 找不到客户端]", configId); + } + return client; + } + + @Override + @SuppressWarnings("unchecked") + public void createOrUpdateFileClient(Long configId, Integer storage, Config config) { + AbstractFileClient client = (AbstractFileClient) clients.get(configId); + if (client == null) { + client = this.createFileClient(configId, storage, config); + client.init(); + clients.put(client.getId(), client); + } else { + client.refresh(config); + } + } + + @SuppressWarnings("unchecked") + private AbstractFileClient createFileClient( + Long configId, Integer storage, Config config) { + FileStorageEnum storageEnum = FileStorageEnum.getByStorage(storage); + Assert.notNull(storageEnum, String.format("文件配置(%s) 为空", storageEnum)); + // 创建客户端 + return (AbstractFileClient) ReflectUtil.newInstance(storageEnum.getClientClass(), configId, config); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/db/DBFileClient.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/db/DBFileClient.java new file mode 100644 index 0000000..0a050d3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/db/DBFileClient.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.db; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO; +import cn.iocoder.yudao.module.infra.dal.mysql.file.FileContentMapper; +import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; + +import java.util.Comparator; +import java.util.List; + +/** + * 基于 DB 存储的文件客户端的配置类 + * + * @author 芋道源码 + */ +public class DBFileClient extends AbstractFileClient { + + private FileContentMapper fileContentMapper; + + public DBFileClient(Long id, DBFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + fileContentMapper = SpringUtil.getBean(FileContentMapper.class); + } + + @Override + public String upload(byte[] content, String path, String type) { + FileContentDO contentDO = new FileContentDO().setConfigId(getId()) + .setPath(path).setContent(content); + fileContentMapper.insert(contentDO); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + fileContentMapper.deleteByConfigIdAndPath(getId(), path); + } + + @Override + public byte[] getContent(String path) { + List list = fileContentMapper.selectListByConfigIdAndPath(getId(), path); + if (CollUtil.isEmpty(list)) { + return null; + } + // 排序后,拿 id 最大的,即最后上传的 + list.sort(Comparator.comparing(FileContentDO::getId)); + return CollUtil.getLast(list).getContent(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/db/DBFileClientConfig.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/db/DBFileClientConfig.java new file mode 100644 index 0000000..1f619df --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/db/DBFileClientConfig.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.db; + +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.constraints.NotEmpty; + +/** + * 基于 DB 存储的文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class DBFileClientConfig implements FileClientConfig { + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClient.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClient.java new file mode 100644 index 0000000..4207eb7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClient.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.ftp; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.ftp.Ftp; +import cn.hutool.extra.ftp.FtpException; +import cn.hutool.extra.ftp.FtpMode; +import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * Ftp 文件客户端 + * + * @author 芋道源码 + */ +public class FtpFileClient extends AbstractFileClient { + + private Ftp ftp; + + public FtpFileClient(Long id, FtpFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 初始化 Ftp 对象 + this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(), + CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode())); + } + + @Override + public String upload(byte[] content, String path, String type) { + // 执行写入 + String filePath = getFilePath(path); + String fileName = FileUtil.getName(filePath); + String dir = StrUtil.removeSuffix(filePath, fileName); + reconnectIfTimeout(); + boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); // 不需要主动创建目录,ftp 内部已经处理(见源码) + if (!success) { + throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); + } + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + reconnectIfTimeout(); + ftp.delFile(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + String fileName = FileUtil.getName(filePath); + String dir = StrUtil.removeSuffix(filePath, fileName); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + reconnectIfTimeout(); + ftp.download(dir, fileName, out); + return out.toByteArray(); + } + + private String getFilePath(String path) { + return config.getBasePath() + StrUtil.SLASH + path; + } + + private synchronized void reconnectIfTimeout() { + ftp.reconnectIfTimeout(); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClientConfig.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClientConfig.java new file mode 100644 index 0000000..a1b92ff --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClientConfig.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.ftp; + +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * Ftp 文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class FtpFileClientConfig implements FileClientConfig { + + /** + * 基础路径 + */ + @NotEmpty(message = "基础路径不能为空") + private String basePath; + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + + /** + * 主机地址 + */ + @NotEmpty(message = "host 不能为空") + private String host; + /** + * 主机端口 + */ + @NotNull(message = "port 不能为空") + private Integer port; + /** + * 用户名 + */ + @NotEmpty(message = "用户名不能为空") + private String username; + /** + * 密码 + */ + @NotEmpty(message = "密码不能为空") + private String password; + /** + * 连接模式 + * + * 使用 {@link cn.hutool.extra.ftp.FtpMode} 对应的字符串 + */ + @NotEmpty(message = "连接模式不能为空") + private String mode; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java new file mode 100644 index 0000000..7fa2a7e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.local; + +import cn.hutool.core.io.FileUtil; +import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; + +import java.io.File; + +/** + * 本地文件客户端 + * + * @author 芋道源码 + */ +public class LocalFileClient extends AbstractFileClient { + + public LocalFileClient(Long id, LocalFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + } + + @Override + public String upload(byte[] content, String path, String type) { + // 执行写入 + String filePath = getFilePath(path); + FileUtil.writeBytes(content, filePath); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + FileUtil.del(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + return FileUtil.readBytes(filePath); + } + + private String getFilePath(String path) { + return config.getBasePath() + File.separator + path; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClientConfig.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClientConfig.java new file mode 100644 index 0000000..d56d4ef --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClientConfig.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.local; + +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.constraints.NotEmpty; + +/** + * 本地文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class LocalFileClientConfig implements FileClientConfig { + + /** + * 基础路径 + */ + @NotEmpty(message = "基础路径不能为空") + private String basePath; + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java new file mode 100644 index 0000000..6a1258e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.s3; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 文件预签名地址 Response DTO + * + * @author owen + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class FilePresignedUrlRespDTO { + + /** + * 文件上传 URL(用于上传) + * + * 例如说: + */ + private String uploadUrl; + + /** + * 文件 URL(用于读取、下载等) + */ + private String url; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java new file mode 100644 index 0000000..a33f0d7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java @@ -0,0 +1,146 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.s3; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; + +import java.net.URI; +import java.time.Duration; + +/** + * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务 + * + * @author 芋道源码 + */ +public class S3FileClient extends AbstractFileClient { + + private S3Client client; + private S3Presigner presigner; + + public S3FileClient(Long id, S3FileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 补全 domain + if (StrUtil.isEmpty(config.getDomain())) { + config.setDomain(buildDomain()); + } + // 初始化 S3 客户端 + Region region = Region.of("us-east-1"); // 必须填,但填什么都行,常见的值有 "us-east-1",不填会报错 + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret())); + URI endpoint = URI.create(buildEndpoint()); + S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问 + .pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess())) + .chunkedEncodingEnabled(false) // 禁用分块编码,参见 https://t.zsxq.com/kBy57 + .build(); + client = S3Client.builder() + .credentialsProvider(credentialsProvider) + .region(region) + .endpointOverride(endpoint) + .serviceConfiguration(serviceConfiguration) + .build(); + presigner = S3Presigner.builder() + .credentialsProvider(credentialsProvider) + .region(region) + .endpointOverride(endpoint) + .serviceConfiguration(serviceConfiguration) + .build(); + } + + @Override + public String upload(byte[] content, String path, String type) { + // 构造 PutObjectRequest + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(config.getBucket()) + .key(path) + .contentType(type) + .contentLength((long) content.length) + .build(); + // 上传文件 + client.putObject(putRequest, RequestBody.fromBytes(content)); + // 拼接返回路径 + return config.getDomain() + "/" + path; + } + + @Override + public void delete(String path) { + DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder() + .bucket(config.getBucket()) + .key(path) + .build(); + client.deleteObject(deleteRequest); + } + + @Override + public byte[] getContent(String path) { + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(config.getBucket()) + .key(path) + .build(); + return IoUtil.readBytes(client.getObject(getRequest)); + } + + @Override + public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) { + Duration expiration = Duration.ofHours(24); + return new FilePresignedUrlRespDTO(getPresignedUrl(path, expiration), config.getDomain() + "/" + path); + } + + /** + * 生成动态的预签名上传 URL + * + * @param path 相对路径 + * @param expiration 过期时间 + * @return 生成的上传 URL + */ + private String getPresignedUrl(String path, Duration expiration) { + return presigner.presignPutObject(PutObjectPresignRequest.builder() + .signatureDuration(expiration) + .putObjectRequest(b -> b.bucket(config.getBucket()).key(path)) + .build()).url().toString(); + } + + /** + * 基于 bucket + endpoint 构建访问的 Domain 地址 + * + * @return Domain 地址 + */ + private String buildDomain() { + // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO + if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) { + return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket()); + } + // 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名 + return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint()); + } + + /** + * 节点地址补全协议头 + * + * @return 节点地址 + */ + private String buildEndpoint() { + // 如果已经是 http 或者 https,则不进行拼接 + if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) { + return config.getEndpoint(); + } + return StrUtil.format("https://{}", config.getEndpoint()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java new file mode 100644 index 0000000..fb19317 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.s3; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; + +/** + * S3 文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class S3FileClientConfig implements FileClientConfig { + + public static final String ENDPOINT_QINIU = "qiniucs.com"; + public static final String ENDPOINT_ALIYUN = "aliyuncs.com"; + public static final String ENDPOINT_TENCENT = "myqcloud.com"; + public static final String ENDPOINT_VOLCES = "volces.com"; // 火山云(字节) + + /** + * 节点地址 + * 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO 。例如说,http://127.0.0.1:9000 + * 2. 阿里云:https://help.aliyun.com/document_detail/31837.html + * 3. 腾讯云:https://cloud.tencent.com/document/product/436/6224 + * 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname + * 5. 华为云:https://console.huaweicloud.com/apiexplorer/#/endpoint/OBS + * 6. 火山云:https://www.volcengine.com/docs/6349/107356 + */ + @NotNull(message = "endpoint 不能为空") + private String endpoint; + /** + * 自定义域名 + * 1. MinIO:通过 Nginx 配置 + * 2. 阿里云:https://help.aliyun.com/document_detail/31836.html + * 3. 腾讯云:https://cloud.tencent.com/document/product/436/11142 + * 4. 七牛云:https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name + * 5. 华为云:https://support.huaweicloud.com/usermanual-obs/obs_03_0032.html + * 6. 火山云:https://www.volcengine.com/docs/6349/128983 + */ + @URL(message = "domain 必须是 URL 格式") + private String domain; + /** + * 存储 Bucket + */ + @NotNull(message = "bucket 不能为空") + private String bucket; + + /** + * 访问 Key + * 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO + * 2. 阿里云:https://ram.console.aliyun.com/manage/ak + * 3. 腾讯云:https://console.cloud.tencent.com/cam/capi + * 4. 七牛云:https://portal.qiniu.com/user/key + * 5. 华为云:https://support.huaweicloud.com/qs-obs/obs_qs_0005.html + * 6. 火山云:https://console.volcengine.com/iam/keymanage/ + */ + @NotNull(message = "accessKey 不能为空") + private String accessKey; + /** + * 访问 Secret + */ + @NotNull(message = "accessSecret 不能为空") + private String accessSecret; + + /** + * 是否启用 PathStyle 访问 + */ + @NotNull(message = "enablePathStyleAccess 不能为空") + private Boolean enablePathStyleAccess; + + @SuppressWarnings("RedundantIfStatement") + @AssertTrue(message = "domain 不能为空") + @JsonIgnore + public boolean isDomainValid() { + // 如果是七牛,必须带有 domain + if (StrUtil.contains(endpoint, ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) { + return false; + } + return true; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClient.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClient.java new file mode 100644 index 0000000..000cbd1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClient.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.sftp; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.extra.ssh.Sftp; +import cn.iocoder.yudao.framework.common.util.io.FileUtils; +import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; + +import java.io.File; + +/** + * Sftp 文件客户端 + * + * @author 芋道源码 + */ +public class SftpFileClient extends AbstractFileClient { + + private Sftp sftp; + + public SftpFileClient(Long id, SftpFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 初始化 Ftp 对象 + this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); + } + + @Override + public String upload(byte[] content, String path, String type) { + // 执行写入 + String filePath = getFilePath(path); + File file = FileUtils.createTempFile(content); + reconnectIfTimeout(); + sftp.mkDirs(FileUtil.getParent(filePath, 1)); // 需要创建父目录,不然会报错 + sftp.upload(filePath, file); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + reconnectIfTimeout(); + sftp.delFile(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + File destFile = FileUtils.createTempFile(); + reconnectIfTimeout(); + sftp.download(filePath, destFile); + return FileUtil.readBytes(destFile); + } + + private String getFilePath(String path) { + return config.getBasePath() + File.separator + path; + } + + private synchronized void reconnectIfTimeout() { + sftp.reconnectIfTimeout(); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClientConfig.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClientConfig.java new file mode 100644 index 0000000..c56d045 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClientConfig.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.client.sftp; + +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * Sftp 文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class SftpFileClientConfig implements FileClientConfig { + + /** + * 基础路径 + */ + @NotEmpty(message = "基础路径不能为空") + private String basePath; + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + + /** + * 主机地址 + */ + @NotEmpty(message = "host 不能为空") + private String host; + /** + * 主机端口 + */ + @NotNull(message = "port 不能为空") + private Integer port; + /** + * 用户名 + */ + @NotEmpty(message = "用户名不能为空") + private String username; + /** + * 密码 + */ + @NotEmpty(message = "密码不能为空") + private String password; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/enums/FileStorageEnum.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/enums/FileStorageEnum.java new file mode 100644 index 0000000..866c43b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/enums/FileStorageEnum.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.db.DBFileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.client.db.DBFileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileClientConfig; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 文件存储器枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum FileStorageEnum { + + DB(1, DBFileClientConfig.class, DBFileClient.class), + + LOCAL(10, LocalFileClientConfig.class, LocalFileClient.class), + FTP(11, FtpFileClientConfig.class, FtpFileClient.class), + SFTP(12, SftpFileClientConfig.class, SftpFileClient.class), + + S3(20, S3FileClientConfig.class, S3FileClient.class), + ; + + /** + * 存储器 + */ + private final Integer storage; + + /** + * 配置类 + */ + private final Class configClass; + /** + * 客户端类 + */ + private final Class clientClass; + + public static FileStorageEnum getByStorage(Integer storage) { + return ArrayUtil.firstMatch(o -> o.getStorage().equals(storage), values()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java new file mode 100644 index 0000000..9cc4175 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.infra.framework.file.core.utils; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import jakarta.servlet.http.HttpServletResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.tika.Tika; +import org.apache.tika.mime.MimeTypeException; +import org.apache.tika.mime.MimeTypes; + +import java.io.IOException; + +/** + * 文件类型 Utils + * + * @author 芋道源码 + */ +@Slf4j +public class FileTypeUtils { + + private static final Tika TIKA = new Tika(); + + /** + * 获得文件的 mineType,对于 doc,jar 等文件会有误差 + * + * @param data 文件内容 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + @SneakyThrows + public static String getMineType(byte[] data) { + return TIKA.detect(data); + } + + /** + * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用 jar 文件时,通过名字更为准确 + * + * @param name 文件名 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + public static String getMineType(String name) { + return TIKA.detect(name); + } + + /** + * 在拥有文件和数据的情况下,最好使用此方法,最为准确 + * + * @param data 文件内容 + * @param name 文件名 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + public static String getMineType(byte[] data, String name) { + return TIKA.detect(data, name); + } + + /** + * 根据 mineType 获得文件后缀 + * + * 注意:如果获取不到,或者发生异常,都返回 null + * + * @param mineType 类型 + * @return 后缀,例如说 .pdf + */ + public static String getExtension(String mineType) { + try { + return MimeTypes.getDefaultMimeTypes().forName(mineType).getExtension(); + } catch (MimeTypeException e) { + log.warn("[getExtension][获取文件后缀({}) 失败]", mineType, e); + return null; + } + } + + /** + * 返回附件 + * + * @param response 响应 + * @param filename 文件名 + * @param content 附件内容 + */ + public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { + // 设置 header 和 contentType + response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); + String contentType = getMineType(content, filename); + response.setContentType(contentType); + // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 + if (StrUtil.containsIgnoreCase(contentType, "video")) { + response.setHeader("Content-Length", String.valueOf(content.length)); + response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length); + response.setHeader("Accept-Ranges", "bytes"); + } + // 输出附件 + IoUtil.write(response.getOutputStream(), false, content); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/package-info.java new file mode 100644 index 0000000..04b6867 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/package-info.java @@ -0,0 +1,12 @@ +/** + * 文件客户端,支持多种存储器 + * + * 1. local:本地磁盘 + * 2. ftp:FTP 服务器 + * 3. sftp:SFTP 服务器 + * 4. db:数据库 + * 5. s3:支持 S3 协议的云存储服务,例如说 MinIO、阿里云、华为云、腾讯云、七牛云等等 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.infra.framework.file; \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/config/AdminServerConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/config/AdminServerConfiguration.java new file mode 100644 index 0000000..0c29865 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/config/AdminServerConfiguration.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.module.infra.framework.monitor.config; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableAdminServer +public class AdminServerConfiguration { +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/package-info.java new file mode 100644 index 0000000..f52e72c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Spring Boot Admin 实现简单的监控平台 + */ +package cn.iocoder.yudao.module.infra.framework.monitor; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md new file mode 100644 index 0000000..a1e3676 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md @@ -0,0 +1 @@ + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/package-info.java new file mode 100644 index 0000000..77a646a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 infra 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.infra.framework; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java new file mode 100644 index 0000000..8d30408 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.infra.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * Infra 模块的 Security 配置 + */ +@Configuration(proxyBeanMethods = false, value = "infraSecurityConfiguration") +public class SecurityConfiguration { + + @Value("${spring.boot.admin.context-path:''}") + private String adminSeverContextPath; + + @Bean("infraAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // Swagger 接口文档 + registry.requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers("/webjars/**").permitAll() + .requestMatchers("/swagger-ui.html").permitAll() + .requestMatchers("/swagger-ui/**").permitAll(); + // Spring Boot Actuator 的安全配置 + registry.requestMatchers("/actuator").permitAll() + .requestMatchers("/actuator/**").permitAll(); + // Druid 监控 + registry.requestMatchers("/druid/**").permitAll(); + // Spring Boot Admin Server 的安全配置 + registry.requestMatchers(adminSeverContextPath).permitAll() + .requestMatchers(adminSeverContextPath + "/**").permitAll(); + // 文件读取 + registry.requestMatchers(buildAdminApi("/infra/file/*/get/**")).permitAll(); + } + + }; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/security/core/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/security/core/package-info.java new file mode 100644 index 0000000..7762626 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.infra.framework.security.core; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/web/config/InfraWebConfiguration.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/web/config/InfraWebConfiguration.java new file mode 100644 index 0000000..7ab1e9e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/web/config/InfraWebConfiguration.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.framework.web.config; + +import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * infra 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class InfraWebConfiguration { + + /** + * infra 模块的 API 分组 + */ + @Bean + public GroupedOpenApi infraGroupedOpenApi() { + return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("infra"); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/web/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/web/package-info.java new file mode 100644 index 0000000..6fb49bf --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * infra 模块的 web 配置 + */ +package cn.iocoder.yudao.module.infra.framework.web; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/job/job/JobLogCleanJob.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/job/job/JobLogCleanJob.java new file mode 100644 index 0000000..f2e6f42 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/job/job/JobLogCleanJob.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.infra.job.job; + +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.service.job.JobLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import jakarta.annotation.Resource; + +/** + * 物理删除 N 天前的任务日志的 Job + * + * @author j-sentinel + */ +@Slf4j +@Component +public class JobLogCleanJob implements JobHandler { + + @Resource + private JobLogService jobLogService; + + /** + * 清理超过(14)天的日志 + */ + private static final Integer JOB_CLEAN_RETAIN_DAY = 14; + + /** + * 每次删除间隔的条数,如果值太高可能会造成数据库的压力过大 + */ + private static final Integer DELETE_LIMIT = 100; + + @Override + @TenantIgnore + public String execute(String param) { + Integer count = jobLogService.cleanJobLog(JOB_CLEAN_RETAIN_DAY, DELETE_LIMIT); + log.info("[execute][定时执行清理定时任务日志数量 ({}) 个]", count); + return String.format("定时执行清理定时任务日志数量 %s 个", count); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/job/logger/AccessLogCleanJob.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/job/logger/AccessLogCleanJob.java new file mode 100644 index 0000000..23a277b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/job/logger/AccessLogCleanJob.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.infra.job.logger; + +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.service.logger.ApiAccessLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * 物理删除 N 天前的访问日志的 Job + * + * @author j-sentinel + */ +@Component +@Slf4j +public class AccessLogCleanJob implements JobHandler { + + @Resource + private ApiAccessLogService apiAccessLogService; + + /** + * 清理超过(14)天的日志 + */ + private static final Integer JOB_CLEAN_RETAIN_DAY = 14; + + /** + * 每次删除间隔的条数,如果值太高可能会造成数据库的压力过大 + */ + private static final Integer DELETE_LIMIT = 100; + + @Override + @TenantIgnore + public String execute(String param) { + Integer count = apiAccessLogService.cleanAccessLog(JOB_CLEAN_RETAIN_DAY, DELETE_LIMIT); + log.info("[execute][定时执行清理访问日志数量 ({}) 个]", count); + return String.format("定时执行清理访问日志数量 %s 个", count); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/job/logger/ErrorLogCleanJob.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/job/logger/ErrorLogCleanJob.java new file mode 100644 index 0000000..8b3b566 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/job/logger/ErrorLogCleanJob.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.infra.job.logger; + +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.infra.service.logger.ApiErrorLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * 物理删除 N 天前的错误日志的 Job + * + * @author j-sentinel + */ +@Slf4j +@Component +public class ErrorLogCleanJob implements JobHandler { + + @Resource + private ApiErrorLogService apiErrorLogService; + + /** + * 清理超过(14)天的日志 + */ + private static final Integer JOB_CLEAN_RETAIN_DAY = 14; + + /** + * 每次删除间隔的条数,如果值太高可能会造成数据库的压力过大 + */ + private static final Integer DELETE_LIMIT = 100; + + @Override + @TenantIgnore + public String execute(String param) { + Integer count = apiErrorLogService.cleanErrorLog(JOB_CLEAN_RETAIN_DAY,DELETE_LIMIT); + log.info("[execute][定时执行清理错误日志数量 ({}) 个]", count); + return String.format("定时执行清理错误日志数量 %s 个", count); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/mq/consumer/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/mq/consumer/package-info.java new file mode 100644 index 0000000..5ba5264 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/mq/consumer/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的消费者 + */ +package cn.iocoder.yudao.module.infra.mq.consumer; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/mq/message/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/mq/message/package-info.java new file mode 100644 index 0000000..19edb02 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/mq/message/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的消息 + */ +package cn.iocoder.yudao.module.infra.mq.message; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/mq/producer/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/mq/producer/package-info.java new file mode 100644 index 0000000..343179c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/mq/producer/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的生产者 + */ +package cn.iocoder.yudao.module.infra.mq.producer; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/package-info.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/package-info.java new file mode 100644 index 0000000..b45e7a2 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/package-info.java @@ -0,0 +1,9 @@ +/** + * infra 模块,主要提供两块能力: + * 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + * 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + * + * 1. Controller URL:以 /infra/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 infra_ 开头,方便在数据库中区分 + */ +package cn.iocoder.yudao.module.infra; diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java new file mode 100644 index 0000000..ca22021 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.module.infra.service.codegen; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; + +import java.util.List; +import java.util.Map; + +/** + * 代码生成 Service 接口 + * + * @author 芋道源码 + */ +public interface CodegenService { + + /** + * 基于数据库的表结构,创建代码生成器的表定义 + * + * @param author 作者 + * @param reqVO 表信息 + * @return 创建的表定义的编号数组 + */ + List createCodegenList(String author, CodegenCreateListReqVO reqVO); + + /** + * 更新数据库的表和字段定义 + * + * @param updateReqVO 更新信息 + */ + void updateCodegen(CodegenUpdateReqVO updateReqVO); + + /** + * 基于数据库的表结构,同步数据库的表和字段定义 + * + * @param tableId 表编号 + */ + void syncCodegenFromDB(Long tableId); + + /** + * 删除数据库的表和字段定义 + * + * @param tableId 数据编号 + */ + void deleteCodegen(Long tableId); + + /** + * 批量删除数据库的表和字段定义 + * + * @param tableIds 数据编号列表 + */ + void deleteCodegenList(List tableIds); + + /** + * 获得表定义列表 + * + * @param dataSourceConfigId 数据源配置的编号 + * @return 表定义列表 + */ + List getCodegenTableList(Long dataSourceConfigId); + + /** + * 获得表定义分页 + * + * @param pageReqVO 分页条件 + * @return 表定义分页 + */ + PageResult getCodegenTablePage(CodegenTablePageReqVO pageReqVO); + + /** + * 获得表定义 + * + * @param id 表编号 + * @return 表定义 + */ + CodegenTableDO getCodegenTable(Long id); + + /** + * 获得指定表的字段定义数组 + * + * @param tableId 表编号 + * @return 字段定义数组 + */ + List getCodegenColumnListByTableId(Long tableId); + + /** + * 执行指定表的代码生成 + * + * @param tableId 表编号 + * @return 生成结果。key 为文件路径,value 为对应的代码内容 + */ + Map generationCodes(Long tableId); + + /** + * 获得数据库自带的表定义列表 + * + * @param dataSourceConfigId 数据源的配置编号 + * @param name 表名称 + * @param comment 表描述 + * @return 表定义列表 + */ + List getDatabaseTableList(Long dataSourceConfigId, String name, String comment); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java new file mode 100644 index 0000000..01efb65 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java @@ -0,0 +1,301 @@ +package cn.iocoder.yudao.module.infra.service.codegen; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties; +import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder; +import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine; +import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.function.BiPredicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 代码生成 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class CodegenServiceImpl implements CodegenService { + + @Resource + private DatabaseTableService databaseTableService; + + @Resource + private CodegenTableMapper codegenTableMapper; + @Resource + private CodegenColumnMapper codegenColumnMapper; + + @Resource + private CodegenBuilder codegenBuilder; + @Resource + private CodegenEngine codegenEngine; + + @Resource + private CodegenProperties codegenProperties; + + @Override + @Transactional(rollbackFor = Exception.class) + public List createCodegenList(String author, CodegenCreateListReqVO reqVO) { + List ids = new ArrayList<>(reqVO.getTableNames().size()); + // 遍历添加。虽然效率会低一点,但是没必要做成完全批量,因为不会这么大量 + reqVO.getTableNames().forEach(tableName -> ids.add(createCodegen(author, reqVO.getDataSourceConfigId(), tableName))); + return ids; + } + + private Long createCodegen(String author, Long dataSourceConfigId, String tableName) { + // 从数据库中,获得数据库表结构 + TableInfo tableInfo = databaseTableService.getTable(dataSourceConfigId, tableName); + // 导入 + return createCodegen0(author, dataSourceConfigId, tableInfo); + } + + private Long createCodegen0(String author, Long dataSourceConfigId, TableInfo tableInfo) { + // 校验导入的表和字段非空 + validateTableInfo(tableInfo); + // 校验是否已经存在 + if (codegenTableMapper.selectByTableNameAndDataSourceConfigId(tableInfo.getName(), + dataSourceConfigId) != null) { + throw exception(CODEGEN_TABLE_EXISTS); + } + + // 构建 CodegenTableDO 对象,插入到 DB 中 + CodegenTableDO table = codegenBuilder.buildTable(tableInfo); + table.setDataSourceConfigId(dataSourceConfigId); + table.setScene(CodegenSceneEnum.ADMIN.getScene()); // 默认配置下,使用管理后台的模板 + table.setFrontType(codegenProperties.getFrontType()); + table.setAuthor(author); + codegenTableMapper.insert(table); + + // 构建 CodegenColumnDO 数组,插入到 DB 中 + List columns = codegenBuilder.buildColumns(table.getId(), tableInfo.getFields()); + // 如果没有主键,则使用第一个字段作为主键 + if (!tableInfo.isHavePrimaryKey()) { + columns.get(0).setPrimaryKey(true); + } + codegenColumnMapper.insertBatch(columns); + return table.getId(); + } + + @VisibleForTesting + void validateTableInfo(TableInfo tableInfo) { + if (tableInfo == null) { + throw exception(CODEGEN_IMPORT_TABLE_NULL); + } + if (StrUtil.isEmpty(tableInfo.getComment())) { + throw exception(CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL); + } + if (CollUtil.isEmpty(tableInfo.getFields())) { + throw exception(CODEGEN_IMPORT_COLUMNS_NULL); + } + tableInfo.getFields().forEach(field -> { + if (StrUtil.isEmpty(field.getComment())) { + throw exception(CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL, field.getName()); + } + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCodegen(CodegenUpdateReqVO updateReqVO) { + // 校验是否已经存在 + if (codegenTableMapper.selectById(updateReqVO.getTable().getId()) == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + // 校验主表字段存在 + if (Objects.equals(updateReqVO.getTable().getTemplateType(), CodegenTemplateTypeEnum.SUB.getType())) { + if (codegenTableMapper.selectById(updateReqVO.getTable().getMasterTableId()) == null) { + throw exception(CODEGEN_MASTER_TABLE_NOT_EXISTS, updateReqVO.getTable().getMasterTableId()); + } + if (CollUtil.findOne(updateReqVO.getColumns(), // 关联主表的字段不存在 + column -> column.getId().equals(updateReqVO.getTable().getSubJoinColumnId())) == null) { + throw exception(CODEGEN_SUB_COLUMN_NOT_EXISTS, updateReqVO.getTable().getSubJoinColumnId()); + } + } + + // 更新 table 表定义 + CodegenTableDO updateTableObj = BeanUtils.toBean(updateReqVO.getTable(), CodegenTableDO.class); + codegenTableMapper.updateById(updateTableObj); + // 更新 column 字段定义 + List updateColumnObjs = BeanUtils.toBean(updateReqVO.getColumns(), CodegenColumnDO.class); + updateColumnObjs.forEach(updateColumnObj -> codegenColumnMapper.updateById(updateColumnObj)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncCodegenFromDB(Long tableId) { + // 校验是否已经存在 + CodegenTableDO table = codegenTableMapper.selectById(tableId); + if (table == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + // 从数据库中,获得数据库表结构 + TableInfo tableInfo = databaseTableService.getTable(table.getDataSourceConfigId(), table.getTableName()); + // 执行同步 + syncCodegen0(tableId, tableInfo); + } + + private void syncCodegen0(Long tableId, TableInfo tableInfo) { + // 1. 校验导入的表和字段非空 + validateTableInfo(tableInfo); + List tableFields = tableInfo.getFields(); + + // 2. 构建 CodegenColumnDO 数组,只同步新增的字段 + List codegenColumns = codegenColumnMapper.selectListByTableId(tableId); + Set codegenColumnNames = convertSet(codegenColumns, CodegenColumnDO::getColumnName); + + // 3.1 计算需要【修改】的字段,插入时重新插入,删除时将原来的删除 + Map codegenColumnDOMap = convertMap(codegenColumns, CodegenColumnDO::getColumnName); + BiPredicate primaryKeyPredicate = + (tableField, codegenColumn) -> tableField.getMetaInfo().getJdbcType().name().equals(codegenColumn.getDataType()) + && tableField.getMetaInfo().isNullable() == codegenColumn.getNullable() + && tableField.isKeyFlag() == codegenColumn.getPrimaryKey() + && tableField.getComment().equals(codegenColumn.getColumnComment()); + Set modifyFieldNames = IntStream.range(0, tableFields.size()).mapToObj(index -> { + TableField tableField = tableFields.get(index); + String columnName = tableField.getColumnName(); + CodegenColumnDO codegenColumn = codegenColumnDOMap.get(columnName); + if (codegenColumn == null) { + return null; + } + if (!primaryKeyPredicate.test(tableField, codegenColumn) || codegenColumn.getOrdinalPosition() != index) { + return columnName; + } + return null; + }).filter(Objects::nonNull).collect(Collectors.toSet()); + // 3.2 计算需要【删除】的字段 + Set tableFieldNames = convertSet(tableFields, TableField::getName); + Set deleteColumnIds = codegenColumns.stream() + .filter(column -> (!tableFieldNames.contains(column.getColumnName())) || modifyFieldNames.contains(column.getColumnName())) + .map(CodegenColumnDO::getId).collect(Collectors.toSet()); + // 移除已经存在的字段 + tableFields.removeIf(column -> codegenColumnNames.contains(column.getColumnName()) && (!modifyFieldNames.contains(column.getColumnName()))); + if (CollUtil.isEmpty(tableFields) && CollUtil.isEmpty(deleteColumnIds)) { + throw exception(CODEGEN_SYNC_NONE_CHANGE); + } + + // 4.1 插入新增的字段 + List columns = codegenBuilder.buildColumns(tableId, tableFields); + codegenColumnMapper.insertBatch(columns); + // 4.2 删除不存在的字段 + if (CollUtil.isNotEmpty(deleteColumnIds)) { + codegenColumnMapper.deleteByIds(deleteColumnIds); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCodegen(Long tableId) { + // 校验是否已经存在 + if (codegenTableMapper.selectById(tableId) == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + + // 删除 table 表定义 + codegenTableMapper.deleteById(tableId); + // 删除 column 字段定义 + codegenColumnMapper.deleteListByTableId(tableId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCodegenList(List tableIds) { + // 批量删除 table 表定义 + codegenTableMapper.deleteByIds(tableIds); + // 批量删除 column 字段定义 + codegenColumnMapper.deleteListByTableId(tableIds); + } + + @Override + public List getCodegenTableList(Long dataSourceConfigId) { + return codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId); + } + + @Override + public PageResult getCodegenTablePage(CodegenTablePageReqVO pageReqVO) { + return codegenTableMapper.selectPage(pageReqVO); + } + + @Override + public CodegenTableDO getCodegenTable(Long id) { + return codegenTableMapper.selectById(id); + } + + @Override + public List getCodegenColumnListByTableId(Long tableId) { + return codegenColumnMapper.selectListByTableId(tableId); + } + + @Override + public Map generationCodes(Long tableId) { + // 校验是否已经存在 + CodegenTableDO table = codegenTableMapper.selectById(tableId); + if (table == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + List columns = codegenColumnMapper.selectListByTableId(tableId); + if (CollUtil.isEmpty(columns)) { + throw exception(CODEGEN_COLUMN_NOT_EXISTS); + } + + // 如果是主子表,则加载对应的子表信息 + List subTables = null; + List> subColumnsList = null; + if (CodegenTemplateTypeEnum.isMaster(table.getTemplateType())) { + // 校验子表存在 + subTables = codegenTableMapper.selectListByTemplateTypeAndMasterTableId( + CodegenTemplateTypeEnum.SUB.getType(), tableId); + if (CollUtil.isEmpty(subTables)) { + throw exception(CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE); + } + // 校验子表的关联字段存在 + subColumnsList = new ArrayList<>(); + for (CodegenTableDO subTable : subTables) { + List subColumns = codegenColumnMapper.selectListByTableId(subTable.getId()); + if (CollUtil.findOne(subColumns, column -> column.getId().equals(subTable.getSubJoinColumnId())) == null) { + throw exception(CODEGEN_SUB_COLUMN_NOT_EXISTS, subTable.getId()); + } + subColumnsList.add(subColumns); + } + } + + // 执行生成 + return codegenEngine.execute(table, columns, subTables, subColumnsList); + } + + @Override + public List getDatabaseTableList(Long dataSourceConfigId, String name, String comment) { + List tables = databaseTableService.getTableList(dataSourceConfigId, name, comment); + // 移除在 Codegen 中,已经存在的 + Set existsTables = convertSet( + codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId), CodegenTableDO::getTableName); + tables.removeIf(table -> existsTables.contains(table.getName())); + return BeanUtils.toBean(tables, DatabaseTableRespVO.class); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java new file mode 100644 index 0000000..06f0478 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java @@ -0,0 +1,220 @@ +package cn.iocoder.yudao.module.infra.service.codegen.inner; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.infra.convert.codegen.CodegenConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.google.common.collect.Sets; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.hutool.core.text.CharSequenceUtil.*; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.hutool.core.util.RandomUtil.randomInt; + +/** + * 代码生成器的 Builder,负责: + * 1. 将数据库的表 {@link TableInfo} 定义,构建成 {@link CodegenTableDO} + * 2. 将数据库的列 {@link TableField} 构定义,建成 {@link CodegenColumnDO} + */ +@Component +public class CodegenBuilder { + + /** + * 字段名与 {@link CodegenColumnListConditionEnum} 的默认映射 + * 注意,字段的匹配以后缀的方式 + */ + private static final Map COLUMN_LIST_OPERATION_CONDITION_MAPPINGS = + MapUtil.builder() + .put("name", CodegenColumnListConditionEnum.LIKE) + .put("time", CodegenColumnListConditionEnum.BETWEEN) + .put("date", CodegenColumnListConditionEnum.BETWEEN) + .build(); + + /** + * 字段名与 {@link CodegenColumnHtmlTypeEnum} 的默认映射 + * 注意,字段的匹配以后缀的方式 + */ + private static final Map COLUMN_HTML_TYPE_MAPPINGS = + MapUtil.builder() + .put("status", CodegenColumnHtmlTypeEnum.RADIO) + .put("sex", CodegenColumnHtmlTypeEnum.RADIO) + .put("type", CodegenColumnHtmlTypeEnum.SELECT) + .put("image", CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD) + .put("file", CodegenColumnHtmlTypeEnum.FILE_UPLOAD) + .put("content", CodegenColumnHtmlTypeEnum.EDITOR) + .put("description", CodegenColumnHtmlTypeEnum.EDITOR) + .put("demo", CodegenColumnHtmlTypeEnum.EDITOR) + .put("time", CodegenColumnHtmlTypeEnum.DATETIME) + .put("date", CodegenColumnHtmlTypeEnum.DATETIME) + .build(); + + /** + * 多租户编号的字段名 + */ + public static final String TENANT_ID_FIELD = "tenantId"; + /** + * {@link cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO} 的字段 + */ + public static final Set BASE_DO_FIELDS = new HashSet<>(); + /** + * 新增操作,不需要传递的字段 + */ + private static final Set CREATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); + /** + * 修改操作,不需要传递的字段 + */ + private static final Set UPDATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet(); + /** + * 列表操作的条件,不需要传递的字段 + */ + private static final Set LIST_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); + /** + * 列表操作的结果,不需要返回的字段 + */ + private static final Set LIST_OPERATION_RESULT_EXCLUDE_COLUMN = Sets.newHashSet(); + + static { + Arrays.stream(ReflectUtil.getFields(BaseDO.class)).forEach(field -> BASE_DO_FIELDS.add(field.getName())); + BASE_DO_FIELDS.add(TENANT_ID_FIELD); + // 处理 OPERATION 相关的字段 + CREATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + UPDATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + LIST_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + LIST_OPERATION_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是可能需要传递的 + LIST_OPERATION_RESULT_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + LIST_OPERATION_RESULT_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是需要返回的 + } + + public CodegenTableDO buildTable(TableInfo tableInfo) { + CodegenTableDO table = CodegenConvert.INSTANCE.convert(tableInfo); + initTableDefault(table); + return table; + } + + /** + * 初始化 Table 表的默认字段 + * + * @param table 表定义 + */ + private void initTableDefault(CodegenTableDO table) { + // 以 system_dept 举例子。moduleName 为 system、businessName 为 dept、className 为 Dept + // 如果希望以 System 前缀,则可以手动在【代码生成 - 修改生成配置 - 基本信息】,将实体类名称改为 SystemDept 即可 + String tableName = table.getTableName().toLowerCase(); + // 第一步,_ 前缀的前面,作为 module 名字;第二步,moduleName 必须小写; + table.setModuleName(subBefore(tableName, '_', false).toLowerCase()); + // 第一步,第一个 _ 前缀的后面,作为 module 名字; 第二步,可能存在多个 _ 的情况,转换成驼峰; 第三步,businessName 必须小写; + table.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase()); + // 驼峰 + 首字母大写;第一步,第一个 _ 前缀的后面,作为 class 名字;第二步,驼峰命名 + table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false)))); + // 去除结尾的表,作为类描述 + table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表")); + table.setTemplateType(CodegenTemplateTypeEnum.ONE.getType()); + } + + public List buildColumns(Long tableId, List tableFields) { + List columns = CodegenConvert.INSTANCE.convertList(tableFields); + int index = 1; + for (CodegenColumnDO column : columns) { + column.setTableId(tableId); + column.setOrdinalPosition(index++); + // 特殊处理:Byte => Integer + if (Byte.class.getSimpleName().equals(column.getJavaType())) { + column.setJavaType(Integer.class.getSimpleName()); + } + // 初始化 Column 列的默认字段 + processColumnOperation(column); // 处理 CRUD 相关的字段的默认值 + processColumnUI(column); // 处理 UI 相关的字段的默认值 + processColumnExample(column); // 处理字段的 swagger example 示例 + } + return columns; + } + + private void processColumnOperation(CodegenColumnDO column) { + // 处理 createOperation 字段 + column.setCreateOperation(!CREATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) + && !column.getPrimaryKey()); // 对于主键,创建时无需传递 + // 处理 updateOperation 字段 + column.setUpdateOperation(!UPDATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) + || column.getPrimaryKey()); // 对于主键,更新时需要传递 + // 处理 listOperation 字段 + column.setListOperation(!LIST_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) + && !column.getPrimaryKey()); // 对于主键,列表过滤不需要传递 + // 处理 listOperationCondition 字段 + COLUMN_LIST_OPERATION_CONDITION_MAPPINGS.entrySet().stream() + .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) + .findFirst().ifPresent(entry -> column.setListOperationCondition(entry.getValue().getCondition())); + if (column.getListOperationCondition() == null) { + column.setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition()); + } + // 处理 listOperationResult 字段 + column.setListOperationResult(!LIST_OPERATION_RESULT_EXCLUDE_COLUMN.contains(column.getJavaField())); + } + + private void processColumnUI(CodegenColumnDO column) { + // 基于后缀进行匹配 + COLUMN_HTML_TYPE_MAPPINGS.entrySet().stream() + .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) + .findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType())); + // 如果是 Boolean 类型时,设置为 radio 类型. + if (Boolean.class.getSimpleName().equals(column.getJavaType())) { + column.setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType()); + } + // 如果是 LocalDateTime 类型,则设置为 datetime 类型 + if (LocalDateTime.class.getSimpleName().equals(column.getJavaType())) { + column.setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType()); + } + // 兜底,设置默认为 input 类型 + if (column.getHtmlType() == null) { + column.setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType()); + } + } + + /** + * 处理字段的 swagger example 示例 + * + * @param column 字段 + */ + private void processColumnExample(CodegenColumnDO column) { + // id、price、count 等可能是整数的后缀 + if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "id", "price", "count")) { + column.setExample(String.valueOf(randomInt(1, Short.MAX_VALUE))); + return; + } + // name + if (StrUtil.endWithIgnoreCase(column.getJavaField(), "name")) { + column.setExample(randomEle(new String[]{"张三", "李四", "王五", "赵六", "芋艿"})); + return; + } + // status + if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "status", "type")) { + column.setExample(randomEle(new String[]{"1", "2"})); + return; + } + // url + if (StrUtil.endWithIgnoreCase(column.getColumnName(), "url")) { + column.setExample("https://www.iocoder.cn"); + return; + } + // reason + if (StrUtil.endWithIgnoreCase(column.getColumnName(), "reason")) { + column.setExample(randomEle(new String[]{"不喜欢", "不对", "不好", "不香"})); + return; + } + // description、memo、remark + if (StrUtil.endWithAnyIgnoreCase(column.getColumnName(), "description", "memo", "remark")) { + column.setExample(randomEle(new String[]{"你猜", "随便", "你说的对"})); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java new file mode 100644 index 0000000..b69f0c6 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -0,0 +1,649 @@ +package cn.iocoder.yudao.module.infra.service.codegen.inner; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.engine.velocity.VelocityEngine; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum; +import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableTable; +import com.google.common.collect.Maps; +import com.google.common.collect.Table; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.Setter; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; + +import java.util.*; + +import static cn.hutool.core.map.MapUtil.getStr; +import static cn.hutool.core.text.CharSequenceUtil.*; + +/** + * 代码生成的引擎,用于具体生成代码 + * 目前基于 {@link org.apache.velocity.app.Velocity} 模板引擎实现 + * + * 考虑到 Java 模板引擎的框架非常多,Freemarker、Velocity、Thymeleaf 等等,所以我们采用 hutool 封装的 {@link cn.hutool.extra.template.Template} 抽象 + * + * @author 芋道源码 + */ +@Component +public class CodegenEngine { + + /** + * 后端的模板配置 + * + * key:模板在 resources 的地址 + * value:生成的路径 + */ + private static final Map SERVER_TEMPLATES = MapUtil.builder(new LinkedHashMap<>()) // 有序 + // Java module-biz(server) Main + .put(javaTemplatePath("controller/vo/pageReqVO"), javaModuleImplVOFilePath("PageReqVO")) + .put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO")) + .put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO")) + .put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO")) + .put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath()) + .put(javaTemplatePath("dal/do"), + javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO")) + .put(javaTemplatePath("dal/do_sub"), // 特殊:主子表专属逻辑 + javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${subTable.className}DO")) + .put(javaTemplatePath("dal/mapper"), + javaModuleImplMainFilePath("dal/mysql/${table.businessName}/${table.className}Mapper")) + .put(javaTemplatePath("dal/mapper_sub"), // 特殊:主子表专属逻辑 + javaModuleImplMainFilePath("dal/mysql/${table.businessName}/${subTable.className}Mapper")) + .put(javaTemplatePath("dal/mapper.xml"), mapperXmlFilePath()) + .put(javaTemplatePath("service/serviceImpl"), + javaModuleImplMainFilePath("service/${table.businessName}/${table.className}ServiceImpl")) + .put(javaTemplatePath("service/service"), + javaModuleImplMainFilePath("service/${table.businessName}/${table.className}Service")) + // Java module-biz(server) Test + .put(javaTemplatePath("test/serviceTest"), + javaModuleImplTestFilePath("service/${table.businessName}/${table.className}ServiceImplTest")) + // Java module-api Main + .put(javaTemplatePath("enums/errorcode"), javaModuleApiMainFilePath("enums/ErrorCodeConstants_手动操作")) + // SQL + .put("codegen/sql/sql.vm", "sql/sql.sql") + .put("codegen/sql/h2.vm", "sql/h2.sql") + .build(); + + /** + * 后端的配置模版 + * + * key1:UI 模版的类型 {@link CodegenFrontTypeEnum#getType()} + * key2:模板在 resources 的地址 + * value:生成的路径 + */ + private static final Table FRONT_TEMPLATES = ImmutableTable.builder() + // VUE2_ELEMENT_UI + .put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/index.vue"), + vueFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("api/api.js"), + vueFilePath("api/${table.moduleName}/${table.businessName}/index.js")) + .put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/form.vue"), + vueFilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) + .put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) + // VUE3_ELEMENT_PLUS + .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) + .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) + .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + // VUE3_VBEN2_ANTD_SCHEMA + .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) + .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + // VUE3_VBEN5_ANTD_SCHEMA + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/data.ts"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/data.ts")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + // VUE3_VBEN5_ANTD + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + // VUE3_VBEN5_EP_SCHEMA + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/data.ts"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/data.ts")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + // VUE3_VBEN5_EP + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + .build(); + + @Resource + private CodegenProperties codegenProperties; + + /** + * 是否使用 jakarta 包,用于解决 Spring Boot 2.X 和 3.X 的兼容性问题 + * + * true - 使用 jakarta.validation.constraints.* + * false - 使用 javax.validation.constraints.* + */ + @Setter // 允许设置的原因,是因为单测需要手动改变 + private Boolean jakartaEnable; + + /** + * 是否为 yudao-cloud 项目,用于解决 Boot 和 Cloud 的 api 模块兼容性问题 + * + * true - 需要有 yudao-module-xxx-api 模块 + * false - 不需要有,使用 api、enum 包即可 + */ + @Setter + private Boolean cloudEnable; + + /** + * 模板引擎,由 hutool 实现 + */ + private final TemplateEngine templateEngine; + /** + * 全局通用变量映射 + */ + private final Map globalBindingMap = new HashMap<>(); + + public CodegenEngine() { + // 初始化 TemplateEngine 属性 + TemplateConfig config = new TemplateConfig(); + config.setResourceMode(TemplateConfig.ResourceMode.CLASSPATH); + this.templateEngine = new VelocityEngine(config); + // 设置 javaxEnable,按照是否使用 JDK17 来判断 + this.jakartaEnable = SystemUtil.getJavaInfo().isJavaVersionAtLeast(1700) // 17.00 * 100 + && ClassUtils.isPresent("jakarta.annotation.Resource", ClassUtils.getDefaultClassLoader()); + // 设置 cloudEnable,按照是否使用 Spring Cloud 来判断 + this.cloudEnable = ClassUtils.isPresent("cn.iocoder.yudao.module.infra.framework.rpc.config.RpcConfiguration", + ClassUtils.getDefaultClassLoader()); + } + + @PostConstruct + @VisibleForTesting + void initGlobalBindingMap() { + // 全局配置 + globalBindingMap.put("basePackage", codegenProperties.getBasePackage()); + globalBindingMap.put("baseFrameworkPackage", codegenProperties.getBasePackage() + + '.' + "framework"); // 用于后续获取测试类的 package 地址 + globalBindingMap.put("jakartaPackage", jakartaEnable ? "jakarta" : "javax"); + globalBindingMap.put("voType", codegenProperties.getVoType()); + globalBindingMap.put("deleteBatchEnable", codegenProperties.getDeleteBatchEnable()); + // 全局 Java Bean + globalBindingMap.put("CommonResultClassName", CommonResult.class.getName()); + globalBindingMap.put("PageResultClassName", PageResult.class.getName()); + // VO 类,独有字段 + globalBindingMap.put("PageParamClassName", PageParam.class.getName()); + globalBindingMap.put("DictFormatClassName", DictFormat.class.getName()); + // DO 类,独有字段 + globalBindingMap.put("BaseDOClassName", BaseDO.class.getName()); + globalBindingMap.put("baseDOFields", CodegenBuilder.BASE_DO_FIELDS); + globalBindingMap.put("QueryWrapperClassName", LambdaQueryWrapperX.class.getName()); + globalBindingMap.put("BaseMapperClassName", BaseMapperX.class.getName()); + // Util 工具类 + globalBindingMap.put("ServiceExceptionUtilClassName", ServiceExceptionUtil.class.getName()); + globalBindingMap.put("DateUtilsClassName", DateUtils.class.getName()); + globalBindingMap.put("ExcelUtilsClassName", ExcelUtils.class.getName()); + globalBindingMap.put("LocalDateTimeUtilsClassName", LocalDateTimeUtils.class.getName()); + globalBindingMap.put("ObjectUtilsClassName", ObjectUtils.class.getName()); + globalBindingMap.put("DictConvertClassName", DictConvert.class.getName()); + globalBindingMap.put("ApiAccessLogClassName", ApiAccessLog.class.getName()); + globalBindingMap.put("OperateTypeEnumClassName", OperateTypeEnum.class.getName()); + globalBindingMap.put("BeanUtils", BeanUtils.class.getName()); + globalBindingMap.put("CollectionUtilsClassName", CollectionUtils.class.getName()); + } + + /** + * 生成代码 + * + * @param table 表定义 + * @param columns table 的字段定义数组 + * @param subTables 子表数组,当且仅当主子表时使用 + * @param subColumnsList subTables 的字段定义数组 + * @return 生成的代码,key 是路径,value 是对应代码 + */ + public Map execute(CodegenTableDO table, List columns, + List subTables, List> subColumnsList) { + // 1.1 初始化 bindMap 上下文 + Map bindingMap = initBindingMap(table, columns, subTables, subColumnsList); + // 1.2 获得模版 + Map templates = getTemplates(table.getFrontType()); + + // 2. 执行生成 + Map result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序 + templates.forEach((vmPath, filePath) -> { + // 2.1 特殊:主子表专属逻辑 + if (isSubTemplate(vmPath)) { + generateSubCode(table, subTables, result, vmPath, filePath, bindingMap); + return; + // 2.2 特殊:树表专属逻辑 + } else if (isPageReqVOTemplate(vmPath)) { + // 减少多余的类生成,例如说 PageVO.java 类 + if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) { + return; + } + } else if (isListReqVOTemplate(vmPath)) { + // 减少多余的类生成,例如说 ListVO.java 类 + if (!CodegenTemplateTypeEnum.isTree(table.getTemplateType())) { + return; + } + } + // 2.3 默认生成 + generateCode(result, vmPath, filePath, bindingMap); + }); + return result; + } + + private void generateCode(Map result, String vmPath, + String filePath, Map bindingMap) { + filePath = formatFilePath(filePath, bindingMap); + String content = templateEngine.getTemplate(vmPath).render(bindingMap); + // 格式化代码 + content = prettyCode(content); + result.put(filePath, content); + } + + private void generateSubCode(CodegenTableDO table, List subTables, + Map result, String vmPath, + String filePath, Map bindingMap) { + // 没有子表,所以不生成 + if (CollUtil.isEmpty(subTables)) { + return; + } + // 主子表的模式匹配。目的:过滤掉个性化的模版 + if (vmPath.contains("_normal") + && ObjectUtil.notEqual(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_NORMAL.getType())) { + return; + } + if (vmPath.contains("_erp") + && ObjectUtil.notEqual(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_ERP.getType())) { + return; + } + if (vmPath.contains("_inner") + && ObjectUtil.notEqual(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_INNER.getType())) { + return; + } + + // 逐个生成 + for (int i = 0; i < subTables.size(); i++) { + bindingMap.put("subIndex", i); + generateCode(result, vmPath, filePath, bindingMap); + } + bindingMap.remove("subIndex"); + } + + /** + * 格式化生成后的代码 + * + * 因为尽量让 vm 模版简单,所以统一的处理都在这个方法。 + * 如果不处理,Vue 的 Pretty 格式校验可能会报错 + * + * @param content 格式化前的代码 + * @return 格式化后的代码 + */ + private String prettyCode(String content) { + // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错 + content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); + // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到 + if (StrUtil.count(content, "dateFormatter") == 1) { + content = StrUtils.removeLineContains(content, "dateFormatter"); + } + // Vue2 界面:修正 $refs + if (StrUtil.count(content, "this.refs") >= 1) { + content = content.replace("this.refs", "this.$refs"); + } + // Vue 界面:去除多的 dict 相关,只有一个的情况下,说明没使用到 + if (StrUtil.count(content, "getIntDictOptions") == 1) { + content = content.replace("getIntDictOptions, ", ""); + } + if (StrUtil.count(content, "getStrDictOptions") == 1) { + content = content.replace("getStrDictOptions, ", ""); + } + if (StrUtil.count(content, "getBoolDictOptions") == 1) { + content = content.replace("getBoolDictOptions, ", ""); + } + if (StrUtil.count(content, "DICT_TYPE.") == 0) { + content = StrUtils.removeLineContains(content, "DICT_TYPE"); + } + return content; + } + + private Map initBindingMap(CodegenTableDO table, List columns, + List subTables, List> subColumnsList) { + // 创建 bindingMap + Map bindingMap = new HashMap<>(globalBindingMap); + bindingMap.put("table", table); + bindingMap.put("columns", columns); + bindingMap.put("primaryColumn", CollectionUtils.findFirst(columns, CodegenColumnDO::getPrimaryKey)); // 主键字段 + bindingMap.put("sceneEnum", CodegenSceneEnum.valueOf(table.getScene())); + // className 相关 + // 去掉指定前缀,将 TestDictType 转换成 DictType. 因为在 create 等方法后,不需要带上 Test 前缀 + String className = table.getClassName(); + String simpleClassName = equalsAnyIgnoreCase(table.getClassName(), table.getModuleName()) ? table.getClassName() + : removePrefix(table.getClassName(), upperFirst(table.getModuleName())); + String classNameVar = lowerFirst(simpleClassName); + bindingMap.put("simpleClassName", simpleClassName); + bindingMap.put("simpleClassName_underlineCase", toUnderlineCase(simpleClassName)); // 将 DictType 转换成 dict_type + bindingMap.put("classNameVar", classNameVar); // 将 DictType 转换成 dictType,用于变量 + // 将 DictType 转换成 dict-type + String simpleClassNameStrikeCase = toSymbolCase(simpleClassName, '-'); + bindingMap.put("simpleClassName_strikeCase", simpleClassNameStrikeCase); + // permission 前缀 + bindingMap.put("permissionPrefix", table.getModuleName() + ":" + simpleClassNameStrikeCase); + + // 特殊:树表专属逻辑 + if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) { + CodegenColumnDO treeParentColumn = CollUtil.findOne(columns, + column -> Objects.equals(column.getId(), table.getTreeParentColumnId())); + bindingMap.put("treeParentColumn", treeParentColumn); + bindingMap.put("treeParentColumn_javaField_underlineCase", toUnderlineCase(treeParentColumn.getJavaField())); + CodegenColumnDO treeNameColumn = CollUtil.findOne(columns, + column -> Objects.equals(column.getId(), table.getTreeNameColumnId())); + bindingMap.put("treeNameColumn", treeNameColumn); + bindingMap.put("treeNameColumn_javaField_underlineCase", toUnderlineCase(treeNameColumn.getJavaField())); + } + + // 特殊:主子表专属逻辑 + if (CollUtil.isNotEmpty(subTables)) { + // 创建 bindingMap + bindingMap.put("subTables", subTables); + bindingMap.put("subColumnsList", subColumnsList); + List subPrimaryColumns = new ArrayList<>(); + List subJoinColumns = new ArrayList<>(); + List subJoinColumnStrikeCases = new ArrayList<>(); + List subSimpleClassNames = new ArrayList<>(); + List subClassNameVars = new ArrayList<>(); + List simpleClassNameUnderlineCases = new ArrayList<>(); + List subSimpleClassNameStrikeCases = new ArrayList<>(); + for (int i = 0; i < subTables.size(); i++) { + CodegenTableDO subTable = subTables.get(i); + List subColumns = subColumnsList.get(i); + subPrimaryColumns.add(CollectionUtils.findFirst(subColumns, CodegenColumnDO::getPrimaryKey)); // + CodegenColumnDO subColumn = CollectionUtils.findFirst(subColumns, // 关联的字段 + column -> Objects.equals(column.getId(), subTable.getSubJoinColumnId())); + subJoinColumns.add(subColumn); + subJoinColumnStrikeCases.add(toSymbolCase(subColumn.getJavaField(), '-')); // 将 DictType 转换成 dict-type + // className 相关 + String subSimpleClassName = removePrefix(subTable.getClassName(), upperFirst(subTable.getModuleName())); + subSimpleClassNames.add(subSimpleClassName); + simpleClassNameUnderlineCases.add(toUnderlineCase(subSimpleClassName)); // 将 DictType 转换成 dict_type + subClassNameVars.add(lowerFirst(subSimpleClassName)); // 将 DictType 转换成 dictType,用于变量 + subSimpleClassNameStrikeCases.add(toSymbolCase(subSimpleClassName, '-')); // 将 DictType 转换成 dict-type + } + bindingMap.put("subPrimaryColumns", subPrimaryColumns); + bindingMap.put("subJoinColumns", subJoinColumns); + bindingMap.put("subJoinColumn_strikeCases", subJoinColumnStrikeCases); + bindingMap.put("subSimpleClassNames", subSimpleClassNames); + bindingMap.put("simpleClassNameUnderlineCases", simpleClassNameUnderlineCases); + bindingMap.put("subClassNameVars", subClassNameVars); + bindingMap.put("subSimpleClassName_strikeCases", subSimpleClassNameStrikeCases); + } + + // 多个 vm 公用的 VO 变量 + if (ObjectUtil.equal(codegenProperties.getVoType(), CodegenVOTypeEnum.VO.getType())) { + String prefixClass = CodegenSceneEnum.valueOf(table.getScene()).getPrefixClass(); + bindingMap.put("saveReqVOClass", prefixClass + className + "SaveReqVO"); + bindingMap.put("updateReqVOClass", prefixClass + className + "SaveReqVO"); + bindingMap.put("respVOClass", prefixClass + className + "RespVO"); + bindingMap.put("saveReqVOVar", "createReqVO"); + bindingMap.put("updateReqVOVar", "updateReqVO"); + } else if (ObjectUtil.equal(codegenProperties.getVoType(), CodegenVOTypeEnum.DO.getType())) { + bindingMap.put("saveReqVOClass", className + "DO"); + bindingMap.put("updateReqVOClass", className + "DO"); + bindingMap.put("respVOClass", className + "DO"); + bindingMap.put("saveReqVOVar", classNameVar); + bindingMap.put("updateReqVOVar", classNameVar); + } + return bindingMap; + } + + private Map getTemplates(Integer frontType) { + Map templates = new LinkedHashMap<>(); + templates.putAll(SERVER_TEMPLATES); + templates.putAll(FRONT_TEMPLATES.row(frontType)); + // 如果是 Boot 项目,则不使用 api/server 模块 + if (Boolean.FALSE.equals(cloudEnable)) { + SERVER_TEMPLATES.forEach((templatePath, filePath) -> { + filePath = StrUtil.replace(filePath, "/yudao-module-${table.moduleName}-api", ""); + filePath = StrUtil.replace(filePath, "/yudao-module-${table.moduleName}-server", ""); + templates.put(templatePath, filePath); + }); + } + // 如果禁用单元测试,则移除对应的模版 + if (Boolean.FALSE.equals(codegenProperties.getUnitTestEnable())) { + templates.remove(javaTemplatePath("test/serviceTest")); + templates.remove("codegen/sql/h2.vm"); + } + // 如果禁用 VO 类型,则移除对应的模版 + if (ObjectUtil.notEqual(codegenProperties.getVoType(), CodegenVOTypeEnum.VO.getType())) { + templates.remove(javaTemplatePath("controller/vo/respVO")); + templates.remove(javaTemplatePath("controller/vo/saveReqVO")); + } + return templates; + } + + @SuppressWarnings("unchecked") + private String formatFilePath(String filePath, Map bindingMap) { + filePath = StrUtil.replace(filePath, "${basePackage}", + getStr(bindingMap, "basePackage").replaceAll("\\.", "/")); + filePath = StrUtil.replace(filePath, "${classNameVar}", + getStr(bindingMap, "classNameVar")); + filePath = StrUtil.replace(filePath, "${simpleClassName}", + getStr(bindingMap, "simpleClassName")); + // sceneEnum 包含的字段 + CodegenSceneEnum sceneEnum = (CodegenSceneEnum) bindingMap.get("sceneEnum"); + filePath = StrUtil.replace(filePath, "${sceneEnum.prefixClass}", sceneEnum.getPrefixClass()); + filePath = StrUtil.replace(filePath, "${sceneEnum.basePackage}", sceneEnum.getBasePackage()); + // table 包含的字段 + CodegenTableDO table = (CodegenTableDO) bindingMap.get("table"); + filePath = StrUtil.replace(filePath, "${table.moduleName}", table.getModuleName()); + filePath = StrUtil.replace(filePath, "${table.businessName}", table.getBusinessName()); + filePath = StrUtil.replace(filePath, "${table.className}", table.getClassName()); + // 特殊:主子表专属逻辑 + Integer subIndex = (Integer) bindingMap.get("subIndex"); + if (subIndex != null) { + CodegenTableDO subTable = ((List) bindingMap.get("subTables")).get(subIndex); + filePath = StrUtil.replace(filePath, "${subTable.moduleName}", subTable.getModuleName()); + filePath = StrUtil.replace(filePath, "${subTable.businessName}", subTable.getBusinessName()); + filePath = StrUtil.replace(filePath, "${subTable.className}", subTable.getClassName()); + filePath = StrUtil.replace(filePath, "${subSimpleClassName}", + ((List) bindingMap.get("subSimpleClassNames")).get(subIndex)); + filePath = StrUtil.replace(filePath, "${subSimpleClassName_strikeCase}", + ((List) bindingMap.get("subSimpleClassName_strikeCases")).get(subIndex)); + } + return filePath; + } + + private static String javaTemplatePath(String path) { + return "codegen/java/" + path + ".vm"; + } + + private static String javaModuleImplVOFilePath(String path) { + return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + + "vo/${sceneEnum.prefixClass}${table.className}" + path, "server", "main"); + } + + private static String javaModuleImplControllerFilePath() { + return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + + "${sceneEnum.prefixClass}${table.className}Controller", "server", "main"); + } + + private static String javaModuleImplMainFilePath(String path) { + return javaModuleFilePath(path, "server", "main"); + } + + private static String javaModuleApiMainFilePath(String path) { + return javaModuleFilePath(path, "api", "main"); + } + + private static String javaModuleImplTestFilePath(String path) { + return javaModuleFilePath(path, "server", "test"); + } + + private static String javaModuleFilePath(String path, String module, String src) { + return "yudao-module-${table.moduleName}/" + // 顶级模块 + "yudao-module-${table.moduleName}-" + module + "/" + // 子模块 + "src/" + src + "/java/${basePackage}/module/${table.moduleName}/" + path + ".java"; + } + + private static String mapperXmlFilePath() { + return "yudao-module-${table.moduleName}/" + // 顶级模块 + "yudao-module-${table.moduleName}-server/" + // 子模块 + "src/main/resources/mapper/${table.businessName}/${table.className}Mapper.xml"; + } + + private static String vueTemplatePath(String path) { + return "codegen/vue/" + path + ".vm"; + } + + private static String vueFilePath(String path) { + return "yudao-ui-${sceneEnum.basePackage}-vue2/" + // 顶级目录 + "src/" + path; + } + + private static String vue3TemplatePath(String path) { + return "codegen/vue3/" + path + ".vm"; + } + + private static String vue3FilePath(String path) { + return "yudao-ui-${sceneEnum.basePackage}-vue3/" + // 顶级目录 + "src/" + path; + } + + private static String vue3VbenTemplatePath(String path) { + return "codegen/vue3_vben/" + path + ".vm"; + } + + private static String vue3Vben5AntdSchemaTemplatePath(String path) { + return "codegen/vue3_vben5_antd/schema/" + path + ".vm"; + } + + private static String vue3Vben5AntdGeneralTemplatePath(String path) { + return "codegen/vue3_vben5_antd/general/" + path + ".vm"; + } + + private static String vue3Vben5EpSchemaTemplatePath(String path) { + return "codegen/vue3_vben5_ele/schema/" + path + ".vm"; + } + + private static String vue3Vben5EpGeneralTemplatePath(String path) { + return "codegen/vue3_vben5_ele/general/" + path + ".vm"; + } + + private static boolean isSubTemplate(String path) { + return path.contains("_sub"); + } + + private static boolean isPageReqVOTemplate(String path) { + return path.contains("pageReqVO"); + } + + private static boolean isListReqVOTemplate(String path) { + return path.contains("listReqVO"); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigService.java new file mode 100644 index 0000000..854d13a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigService.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.infra.service.config; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 参数配置 Service 接口 + * + * @author 芋道源码 + */ +public interface ConfigService { + + /** + * 创建参数配置 + * + * @param createReqVO 创建信息 + * @return 配置编号 + */ + Long createConfig(@Valid ConfigSaveReqVO createReqVO); + + /** + * 更新参数配置 + * + * @param updateReqVO 更新信息 + */ + void updateConfig(@Valid ConfigSaveReqVO updateReqVO); + + /** + * 删除参数配置 + * + * @param id 配置编号 + */ + void deleteConfig(Long id); + + /** + * 批量删除参数配置 + * + * @param ids 配置编号列表 + */ + void deleteConfigList(List ids); + + /** + * 获得参数配置 + * + * @param id 配置编号 + * @return 参数配置 + */ + ConfigDO getConfig(Long id); + + /** + * 根据参数键,获得参数配置 + * + * @param key 配置键 + * @return 参数配置 + */ + ConfigDO getConfigByKey(String key); + + /** + * 获得参数配置分页列表 + * + * @param reqVO 分页条件 + * @return 分页列表 + */ + PageResult getConfigPage(ConfigPageReqVO reqVO); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigServiceImpl.java new file mode 100644 index 0000000..d900580 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/config/ConfigServiceImpl.java @@ -0,0 +1,124 @@ +package cn.iocoder.yudao.module.infra.service.config; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.convert.config.ConfigConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; +import cn.iocoder.yudao.module.infra.dal.mysql.config.ConfigMapper; +import cn.iocoder.yudao.module.infra.enums.config.ConfigTypeEnum; +import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 参数配置 Service 实现类 + */ +@Service +@Slf4j +@Validated +public class ConfigServiceImpl implements ConfigService { + + @Resource + private ConfigMapper configMapper; + + @Override + public Long createConfig(ConfigSaveReqVO createReqVO) { + // 校验参数配置 key 的唯一性 + validateConfigKeyUnique(null, createReqVO.getKey()); + + // 插入参数配置 + ConfigDO config = ConfigConvert.INSTANCE.convert(createReqVO); + config.setType(ConfigTypeEnum.CUSTOM.getType()); + configMapper.insert(config); + return config.getId(); + } + + @Override + public void updateConfig(ConfigSaveReqVO updateReqVO) { + // 校验自己存在 + validateConfigExists(updateReqVO.getId()); + // 校验参数配置 key 的唯一性 + validateConfigKeyUnique(updateReqVO.getId(), updateReqVO.getKey()); + + // 更新参数配置 + ConfigDO updateObj = ConfigConvert.INSTANCE.convert(updateReqVO); + configMapper.updateById(updateObj); + } + + @Override + public void deleteConfig(Long id) { + // 校验配置存在 + ConfigDO config = validateConfigExists(id); + // 内置配置,不允许删除 + if (ConfigTypeEnum.SYSTEM.getType().equals(config.getType())) { + throw exception(CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE); + } + // 删除 + configMapper.deleteById(id); + } + + @Override + public void deleteConfigList(List ids) { + // 校验是否有内置配置 + List configs = configMapper.selectByIds(ids); + configs.forEach(config -> { + if (ConfigTypeEnum.SYSTEM.getType().equals(config.getType())) { + throw exception(CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE); + } + }); + + // 批量删除 + configMapper.deleteByIds(ids); + } + + @Override + public ConfigDO getConfig(Long id) { + return configMapper.selectById(id); + } + + @Override + public ConfigDO getConfigByKey(String key) { + return configMapper.selectByKey(key); + } + + @Override + public PageResult getConfigPage(ConfigPageReqVO pageReqVO) { + return configMapper.selectPage(pageReqVO); + } + + @VisibleForTesting + public ConfigDO validateConfigExists(Long id) { + if (id == null) { + return null; + } + ConfigDO config = configMapper.selectById(id); + if (config == null) { + throw exception(CONFIG_NOT_EXISTS); + } + return config; + } + + @VisibleForTesting + public void validateConfigKeyUnique(Long id, String key) { + ConfigDO config = configMapper.selectByKey(key); + if (config == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的参数配置 + if (id == null) { + throw exception(CONFIG_KEY_DUPLICATE); + } + if (!config.getId().equals(id)) { + throw exception(CONFIG_KEY_DUPLICATE); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigService.java new file mode 100644 index 0000000..8382b48 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigService.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.infra.service.db; + +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 数据源配置 Service 接口 + * + * @author 芋道源码 + */ +public interface DataSourceConfigService { + + /** + * 创建数据源配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDataSourceConfig(@Valid DataSourceConfigSaveReqVO createReqVO); + + /** + * 更新数据源配置 + * + * @param updateReqVO 更新信息 + */ + void updateDataSourceConfig(@Valid DataSourceConfigSaveReqVO updateReqVO); + + /** + * 删除数据源配置 + * + * @param id 编号 + */ + void deleteDataSourceConfig(Long id); + + /** + * 批量删除数据源配置 + * + * @param ids 编号列表 + */ + void deleteDataSourceConfigList(List ids); + + /** + * 获得数据源配置 + * + * @param id 编号 + * @return 数据源配置 + */ + DataSourceConfigDO getDataSourceConfig(Long id); + + /** + * 获得数据源配置列表 + * + * @return 数据源配置列表 + */ + List getDataSourceConfigList(); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigServiceImpl.java new file mode 100644 index 0000000..b39e21c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DataSourceConfigServiceImpl.java @@ -0,0 +1,111 @@ +package cn.iocoder.yudao.module.infra.service.db; + +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; +import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import cn.iocoder.yudao.module.infra.dal.mysql.db.DataSourceConfigMapper; +import com.baomidou.dynamic.datasource.creator.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_EXISTS; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_OK; + +/** + * 数据源配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class DataSourceConfigServiceImpl implements DataSourceConfigService { + + @Resource + private DataSourceConfigMapper dataSourceConfigMapper; + + @Resource + private DynamicDataSourceProperties dynamicDataSourceProperties; + + @Override + public Long createDataSourceConfig(DataSourceConfigSaveReqVO createReqVO) { + DataSourceConfigDO config = BeanUtils.toBean(createReqVO, DataSourceConfigDO.class); + validateConnectionOK(config); + + // 插入 + dataSourceConfigMapper.insert(config); + // 返回 + return config.getId(); + } + + @Override + public void updateDataSourceConfig(DataSourceConfigSaveReqVO updateReqVO) { + // 校验存在 + validateDataSourceConfigExists(updateReqVO.getId()); + DataSourceConfigDO updateObj = BeanUtils.toBean(updateReqVO, DataSourceConfigDO.class); + validateConnectionOK(updateObj); + + // 更新 + dataSourceConfigMapper.updateById(updateObj); + } + + @Override + public void deleteDataSourceConfig(Long id) { + // 校验存在 + validateDataSourceConfigExists(id); + // 删除 + dataSourceConfigMapper.deleteById(id); + } + + @Override + public void deleteDataSourceConfigList(List ids) { + dataSourceConfigMapper.deleteByIds(ids); + } + + private void validateDataSourceConfigExists(Long id) { + if (dataSourceConfigMapper.selectById(id) == null) { + throw exception(DATA_SOURCE_CONFIG_NOT_EXISTS); + } + } + + @Override + public DataSourceConfigDO getDataSourceConfig(Long id) { + // 如果 id 为 0,默认为 master 的数据源 + if (Objects.equals(id, DataSourceConfigDO.ID_MASTER)) { + return buildMasterDataSourceConfig(); + } + // 从 DB 中读取 + return dataSourceConfigMapper.selectById(id); + } + + @Override + public List getDataSourceConfigList() { + List result = dataSourceConfigMapper.selectList(); + // 补充 master 数据源 + result.add(0, buildMasterDataSourceConfig()); + return result; + } + + private void validateConnectionOK(DataSourceConfigDO config) { + boolean success = JdbcUtils.isConnectionOK(config.getUrl(), config.getUsername(), config.getPassword()); + if (!success) { + throw exception(DATA_SOURCE_CONFIG_NOT_OK); + } + } + + private DataSourceConfigDO buildMasterDataSourceConfig() { + String primary = dynamicDataSourceProperties.getPrimary(); + DataSourceProperty dataSourceProperty = dynamicDataSourceProperties.getDatasource().get(primary); + return new DataSourceConfigDO().setId(DataSourceConfigDO.ID_MASTER).setName(primary) + .setUrl(dataSourceProperty.getUrl()) + .setUsername(dataSourceProperty.getUsername()) + .setPassword(dataSourceProperty.getPassword()); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableService.java new file mode 100644 index 0000000..c58b125 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableService.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.infra.service.db; + +import com.baomidou.mybatisplus.generator.config.po.TableInfo; + +import java.util.List; + +/** + * 数据库表 Service + * + * @author 芋道源码 + */ +public interface DatabaseTableService { + + /** + * 获得表列表,基于表名称 + 表描述进行模糊匹配 + * + * @param dataSourceConfigId 数据源配置的编号 + * @param nameLike 表名称,模糊匹配 + * @param commentLike 表描述,模糊匹配 + * @return 表列表 + */ + List getTableList(Long dataSourceConfigId, String nameLike, String commentLike); + + /** + * 获得指定表名 + * + * @param dataSourceConfigId 数据源配置的编号 + * @param tableName 表名称 + * @return 表 + */ + TableInfo getTable(Long dataSourceConfigId, String tableName); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java new file mode 100644 index 0000000..289cae1 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/db/DatabaseTableServiceImpl.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.infra.service.db; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; +import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.baomidou.mybatisplus.generator.config.DataSourceConfig; +import com.baomidou.mybatisplus.generator.config.GlobalConfig; +import com.baomidou.mybatisplus.generator.config.StrategyConfig; +import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.baomidou.mybatisplus.generator.config.rules.DateType; +import com.baomidou.mybatisplus.generator.query.SQLQuery; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 数据库表 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class DatabaseTableServiceImpl implements DatabaseTableService { + + @Resource + private DataSourceConfigService dataSourceConfigService; + + @Override + public List getTableList(Long dataSourceConfigId, String nameLike, String commentLike) { + List tables = getTableList0(dataSourceConfigId, null); + return tables.stream().filter(tableInfo -> (StrUtil.isEmpty(nameLike) || tableInfo.getName().contains(nameLike)) + && (StrUtil.isEmpty(commentLike) || tableInfo.getComment().contains(commentLike))) + .collect(Collectors.toList()); + } + + @Override + public TableInfo getTable(Long dataSourceConfigId, String name) { + return CollUtil.getFirst(getTableList0(dataSourceConfigId, name)); + } + + private List getTableList0(Long dataSourceConfigId, String name) { + // 获得数据源配置 + DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId); + Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId); + + // 使用 MyBatis Plus Generator 解析表结构 + DataSourceConfig.Builder dataSourceConfigBuilder = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(), + config.getPassword()); + if (JdbcUtils.isSQLServer(config.getUrl())) { // 特殊:SQLServer jdbc 非标准,参见 https://github.com/baomidou/mybatis-plus/issues/5419 + dataSourceConfigBuilder.databaseQueryClass(SQLQuery.class); + } + StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder().enableSkipView(); // 忽略视图,业务上一般用不到 + if (StrUtil.isNotEmpty(name)) { + strategyConfig.addInclude(name); + } else { + // 移除工作流和定时任务前缀的表名 + strategyConfig.addExclude("ACT_[\\S\\s]+|QRTZ_[\\S\\s]+|FLW_[\\S\\s]+"); + // 移除 ORACLE 相关的系统表 + strategyConfig.addExclude("IMPDP_[\\S\\s]+|ALL_[\\S\\s]+|HS_[\\S\\\\s]+"); + strategyConfig.addExclude("[\\S\\s]+\\$[\\S\\s]+|[\\S\\s]+\\$"); // 表里不能有 $,一般有都是系统的表 + } + + GlobalConfig globalConfig = new GlobalConfig.Builder().dateType(DateType.TIME_PACK).build(); // 只使用 LocalDateTime 类型,不使用 LocalDate + ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfigBuilder.build(), strategyConfig.build(), + null, globalConfig, null); + // 按照名字排序 + List tables = builder.getTableInfoList(); + tables.sort(Comparator.comparing(TableInfo::getName)); + return tables; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactService.java new file mode 100644 index 0000000..8a868c3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactService.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo01; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 示例联系人 Service 接口 + * + * @author 芋道源码 + */ +public interface Demo01ContactService { + + /** + * 创建示例联系人 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemo01Contact(@Valid Demo01ContactSaveReqVO createReqVO); + + /** + * 更新示例联系人 + * + * @param updateReqVO 更新信息 + */ + void updateDemo01Contact(@Valid Demo01ContactSaveReqVO updateReqVO); + + /** + * 删除示例联系人 + * + * @param id 编号 + */ + void deleteDemo01Contact(Long id); + + /** + * 批量删除示例联系人 + * + * @param ids 编号 + */ + void deleteDemo0iContactList(List ids); + + /** + * 获得示例联系人 + * + * @param id 编号 + * @return 示例联系人 + */ + Demo01ContactDO getDemo01Contact(Long id); + + /** + * 获得示例联系人分页 + * + * @param pageReqVO 分页查询 + * @return 示例联系人分页 + */ + PageResult getDemo01ContactPage(Demo01ContactPageReqVO pageReqVO); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactServiceImpl.java new file mode 100644 index 0000000..43f828b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactServiceImpl.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo01; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo01.Demo01ContactMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DEMO01_CONTACT_NOT_EXISTS; + +/** + * 示例联系人 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class Demo01ContactServiceImpl implements Demo01ContactService { + + @Resource + private Demo01ContactMapper demo01ContactMapper; + + @Override + public Long createDemo01Contact(Demo01ContactSaveReqVO createReqVO) { + // 插入 + Demo01ContactDO demo01Contact = BeanUtils.toBean(createReqVO, Demo01ContactDO.class); + demo01ContactMapper.insert(demo01Contact); + // 返回 + return demo01Contact.getId(); + } + + @Override + public void updateDemo01Contact(Demo01ContactSaveReqVO updateReqVO) { + // 校验存在 + validateDemo01ContactExists(updateReqVO.getId()); + // 更新 + Demo01ContactDO updateObj = BeanUtils.toBean(updateReqVO, Demo01ContactDO.class); + demo01ContactMapper.updateById(updateObj); + } + + @Override + public void deleteDemo01Contact(Long id) { + // 校验存在 + validateDemo01ContactExists(id); + // 删除 + demo01ContactMapper.deleteById(id); + } + + @Override + public void deleteDemo0iContactList(List ids) { + // 校验存在 + validateDemo01ContactExists(ids); + // 删除 + demo01ContactMapper.deleteByIds(ids); + } + + private void validateDemo01ContactExists(List ids) { + List list = demo01ContactMapper.selectByIds(ids); + if (CollUtil.isEmpty(list) || list.size() != ids.size()) { + throw exception(DEMO01_CONTACT_NOT_EXISTS); + } + } + + private void validateDemo01ContactExists(Long id) { + if (demo01ContactMapper.selectById(id) == null) { + throw exception(DEMO01_CONTACT_NOT_EXISTS); + } + } + + @Override + public Demo01ContactDO getDemo01Contact(Long id) { + return demo01ContactMapper.selectById(id); + } + + @Override + public PageResult getDemo01ContactPage(Demo01ContactPageReqVO pageReqVO) { + return demo01ContactMapper.selectPage(pageReqVO); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryService.java new file mode 100644 index 0000000..e280f6f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryService.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo02; + +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 示例分类 Service 接口 + * + * @author 芋道源码 + */ +public interface Demo02CategoryService { + + /** + * 创建示例分类 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemo02Category(@Valid Demo02CategorySaveReqVO createReqVO); + + /** + * 更新示例分类 + * + * @param updateReqVO 更新信息 + */ + void updateDemo02Category(@Valid Demo02CategorySaveReqVO updateReqVO); + + /** + * 删除示例分类 + * + * @param id 编号 + */ + void deleteDemo02Category(Long id); + + /** + * 获得示例分类 + * + * @param id 编号 + * @return 示例分类 + */ + Demo02CategoryDO getDemo02Category(Long id); + + /** + * 获得示例分类列表 + * + * @param listReqVO 查询条件 + * @return 示例分类列表 + */ + List getDemo02CategoryList(Demo02CategoryListReqVO listReqVO); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryServiceImpl.java new file mode 100644 index 0000000..d9a9edd --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryServiceImpl.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo02; + +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo02.Demo02CategoryMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 示例分类 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class Demo02CategoryServiceImpl implements Demo02CategoryService { + + @Resource + private Demo02CategoryMapper demo02CategoryMapper; + + @Override + public Long createDemo02Category(Demo02CategorySaveReqVO createReqVO) { + // 校验父级编号的有效性 + validateParentDemo02Category(null, createReqVO.getParentId()); + // 校验名字的唯一性 + validateDemo02CategoryNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); + + // 插入 + Demo02CategoryDO demo02Category = BeanUtils.toBean(createReqVO, Demo02CategoryDO.class); + demo02CategoryMapper.insert(demo02Category); + // 返回 + return demo02Category.getId(); + } + + @Override + public void updateDemo02Category(Demo02CategorySaveReqVO updateReqVO) { + // 校验存在 + validateDemo02CategoryExists(updateReqVO.getId()); + // 校验父级编号的有效性 + validateParentDemo02Category(updateReqVO.getId(), updateReqVO.getParentId()); + // 校验名字的唯一性 + validateDemo02CategoryNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName()); + + // 更新 + Demo02CategoryDO updateObj = BeanUtils.toBean(updateReqVO, Demo02CategoryDO.class); + demo02CategoryMapper.updateById(updateObj); + } + + @Override + public void deleteDemo02Category(Long id) { + // 校验存在 + validateDemo02CategoryExists(id); + // 校验是否有子示例分类 + if (demo02CategoryMapper.selectCountByParentId(id) > 0) { + throw exception(DEMO02_CATEGORY_EXITS_CHILDREN); + } + // 删除 + demo02CategoryMapper.deleteById(id); + } + + private void validateDemo02CategoryExists(Long id) { + if (demo02CategoryMapper.selectById(id) == null) { + throw exception(DEMO02_CATEGORY_NOT_EXISTS); + } + } + + private void validateParentDemo02Category(Long id, Long parentId) { + if (parentId == null || Demo02CategoryDO.PARENT_ID_ROOT.equals(parentId)) { + return; + } + // 1. 不能设置自己为父示例分类 + if (Objects.equals(id, parentId)) { + throw exception(DEMO02_CATEGORY_PARENT_ERROR); + } + // 2. 父示例分类不存在 + Demo02CategoryDO parentDemo02Category = demo02CategoryMapper.selectById(parentId); + if (parentDemo02Category == null) { + throw exception(DEMO02_CATEGORY_PARENT_NOT_EXITS); + } + // 3. 递归校验父示例分类,如果父示例分类是自己的子示例分类,则报错,避免形成环路 + if (id == null) { // id 为空,说明新增,不需要考虑环路 + return; + } + for (int i = 0; i < Short.MAX_VALUE; i++) { + // 3.1 校验环路 + parentId = parentDemo02Category.getParentId(); + if (Objects.equals(id, parentId)) { + throw exception(DEMO02_CATEGORY_PARENT_IS_CHILD); + } + // 3.2 继续递归下一级父示例分类 + if (parentId == null || Demo02CategoryDO.PARENT_ID_ROOT.equals(parentId)) { + break; + } + parentDemo02Category = demo02CategoryMapper.selectById(parentId); + if (parentDemo02Category == null) { + break; + } + } + } + + private void validateDemo02CategoryNameUnique(Long id, Long parentId, String name) { + Demo02CategoryDO demo02Category = demo02CategoryMapper.selectByParentIdAndName(parentId, name); + if (demo02Category == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的示例分类 + if (id == null) { + throw exception(DEMO02_CATEGORY_NAME_DUPLICATE); + } + if (!Objects.equals(demo02Category.getId(), id)) { + throw exception(DEMO02_CATEGORY_NAME_DUPLICATE); + } + } + + @Override + public Demo02CategoryDO getDemo02Category(Long id) { + return demo02CategoryMapper.selectById(id); + } + + @Override + public List getDemo02CategoryList(Demo02CategoryListReqVO listReqVO) { + return demo02CategoryMapper.selectList(listReqVO); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/erp/Demo03StudentErpService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/erp/Demo03StudentErpService.java new file mode 100644 index 0000000..26c67d9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/erp/Demo03StudentErpService.java @@ -0,0 +1,162 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo03.erp; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo.Demo03StudentErpPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo.Demo03StudentErpSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 学生 Service 接口 + * + * @author 芋道源码 + */ +public interface Demo03StudentErpService { + + /** + * 创建学生 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemo03Student(@Valid Demo03StudentErpSaveReqVO createReqVO); + + /** + * 更新学生 + * + * @param updateReqVO 更新信息 + */ + void updateDemo03Student(@Valid Demo03StudentErpSaveReqVO updateReqVO); + + /** + * 删除学生 + * + * @param id 编号 + */ + void deleteDemo03Student(Long id); + + /** + * 批量删除学生 + * + * @param ids 编号 + */ + void deleteDemo03StudentList(List ids); + + /** + * 获得学生 + * + * @param id 编号 + * @return 学生 + */ + Demo03StudentDO getDemo03Student(Long id); + + /** + * 获得学生分页 + * + * @param pageReqVO 分页查询 + * @return 学生分页 + */ + PageResult getDemo03StudentPage(Demo03StudentErpPageReqVO pageReqVO); + + // ==================== 子表(学生课程) ==================== + + /** + * 获得学生课程分页 + * + * @param pageReqVO 分页查询 + * @param studentId 学生编号 + * @return 学生课程分页 + */ + PageResult getDemo03CoursePage(PageParam pageReqVO, Long studentId); + + /** + * 创建学生课程 + * + * @param demo03Course 创建信息 + * @return 编号 + */ + Long createDemo03Course(@Valid Demo03CourseDO demo03Course); + + /** + * 更新学生课程 + * + * @param demo03Course 更新信息 + */ + void updateDemo03Course(@Valid Demo03CourseDO demo03Course); + + /** + * 删除学生课程 + * + * @param id 编号 + */ + void deleteDemo03Course(Long id); + + /** + * 批量删除学生课程 + * + * @param ids 编号 + */ + void deleteDemo03CourseList(List ids); + + /** + * 获得学生课程 + * + * @param id 编号 + * @return 学生课程 + */ + Demo03CourseDO getDemo03Course(Long id); + + // ==================== 子表(学生班级) ==================== + + /** + * 获得学生班级分页 + * + * @param pageReqVO 分页查询 + * @param studentId 学生编号 + * @return 学生班级分页 + */ + PageResult getDemo03GradePage(PageParam pageReqVO, Long studentId); + + /** + * 创建学生班级 + * + * @param demo03Grade 创建信息 + * @return 编号 + */ + Long createDemo03Grade(@Valid Demo03GradeDO demo03Grade); + + /** + * 更新学生班级 + * + * @param demo03Grade 更新信息 + */ + void updateDemo03Grade(@Valid Demo03GradeDO demo03Grade); + + /** + * 删除学生班级 + * + * @param id 编号 + */ + void deleteDemo03Grade(Long id); + + /** + * 批量删除学生班级 + * + * @param ids 编号 + */ + void deleteDemo03GradeList(List ids); + + /** + * 获得学生班级 + * + * @param id 编号 + * @return 学生班级 + */ + Demo03GradeDO getDemo03Grade(Long id); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/erp/Demo03StudentErpServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/erp/Demo03StudentErpServiceImpl.java new file mode 100644 index 0000000..122cb69 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/erp/Demo03StudentErpServiceImpl.java @@ -0,0 +1,219 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo03.erp; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo.Demo03StudentErpPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo.Demo03StudentErpSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.erp.Demo03CourseErpMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.erp.Demo03GradeErpMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.erp.Demo03StudentErpMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 学生 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class Demo03StudentErpServiceImpl implements Demo03StudentErpService { + + @Resource + private Demo03StudentErpMapper demo03StudentErpMapper; + @Resource + private Demo03CourseErpMapper demo03CourseErpMapper; + @Resource + private Demo03GradeErpMapper demo03GradeErpMapper; + + @Override + public Long createDemo03Student(Demo03StudentErpSaveReqVO createReqVO) { + // 插入 + Demo03StudentDO demo03Student = BeanUtils.toBean(createReqVO, Demo03StudentDO.class); + demo03StudentErpMapper.insert(demo03Student); + // 返回 + return demo03Student.getId(); + } + + @Override + public void updateDemo03Student(Demo03StudentErpSaveReqVO updateReqVO) { + // 校验存在 + validateDemo03StudentExists(updateReqVO.getId()); + // 更新 + Demo03StudentDO updateObj = BeanUtils.toBean(updateReqVO, Demo03StudentDO.class); + demo03StudentErpMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDemo03Student(Long id) { + // 校验存在 + validateDemo03StudentExists(id); + // 删除 + demo03StudentErpMapper.deleteById(id); + + // 删除子表 + deleteDemo03CourseByStudentId(id); + deleteDemo03GradeByStudentId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDemo03StudentList(List ids) { + // 校验存在 + validateDemo03StudentExists(ids); + // 删除 + demo03StudentErpMapper.deleteByIds(ids); + + // 删除子表 + deleteDemo03CourseByStudentIds(ids); + deleteDemo03GradeByStudentIds(ids); + } + + private void validateDemo03StudentExists(List ids) { + List list = demo03StudentErpMapper.selectByIds(ids); + if (CollUtil.isEmpty(list) || list.size() != ids.size()) { + throw exception(DEMO03_STUDENT_NOT_EXISTS); + } + } + + private void validateDemo03StudentExists(Long id) { + if (demo03StudentErpMapper.selectById(id) == null) { + throw exception(DEMO03_STUDENT_NOT_EXISTS); + } + } + + @Override + public Demo03StudentDO getDemo03Student(Long id) { + return demo03StudentErpMapper.selectById(id); + } + + @Override + public PageResult getDemo03StudentPage(Demo03StudentErpPageReqVO pageReqVO) { + return demo03StudentErpMapper.selectPage(pageReqVO); + } + + // ==================== 子表(学生课程) ==================== + + @Override + public PageResult getDemo03CoursePage(PageParam pageReqVO, Long studentId) { + return demo03CourseErpMapper.selectPage(pageReqVO, studentId); + } + + @Override + public Long createDemo03Course(Demo03CourseDO demo03Course) { + demo03CourseErpMapper.insert(demo03Course); + return demo03Course.getId(); + } + + @Override + public void updateDemo03Course(Demo03CourseDO demo03Course) { + // 校验存在 + validateDemo03CourseExists(demo03Course.getId()); + // 更新 + demo03Course.clean(); + demo03CourseErpMapper.updateById(demo03Course); + } + + @Override + public void deleteDemo03Course(Long id) { + // 删除 + demo03CourseErpMapper.deleteById(id); + } + + @Override + public void deleteDemo03CourseList(List ids) { + // 删除 + demo03CourseErpMapper.deleteByIds(ids); + } + + @Override + public Demo03CourseDO getDemo03Course(Long id) { + return demo03CourseErpMapper.selectById(id); + } + + private void validateDemo03CourseExists(Long id) { + if (demo03CourseErpMapper.selectById(id) == null) { + throw exception(DEMO03_COURSE_NOT_EXISTS); + } + } + + private void deleteDemo03CourseByStudentId(Long studentId) { + demo03CourseErpMapper.deleteByStudentId(studentId); + } + + private void deleteDemo03CourseByStudentIds(List studentIds) { + demo03CourseErpMapper.deleteByStudentIds(studentIds); + } + + // ==================== 子表(学生班级) ==================== + + @Override + public PageResult getDemo03GradePage(PageParam pageReqVO, Long studentId) { + return demo03GradeErpMapper.selectPage(pageReqVO, studentId); + } + + @Override + public Long createDemo03Grade(Demo03GradeDO demo03Grade) { + // 校验是否已经存在 + if (demo03GradeErpMapper.selectByStudentId(demo03Grade.getStudentId()) != null) { + throw exception(DEMO03_GRADE_EXISTS); + } + // 插入 + demo03GradeErpMapper.insert(demo03Grade); + return demo03Grade.getId(); + } + + @Override + public void updateDemo03Grade(Demo03GradeDO demo03Grade) { + // 校验存在 + validateDemo03GradeExists(demo03Grade.getId()); + // 更新 + demo03Grade.clean(); + demo03GradeErpMapper.updateById(demo03Grade); + } + + @Override + public void deleteDemo03Grade(Long id) { + // 删除 + demo03GradeErpMapper.deleteById(id); + } + + @Override + public void deleteDemo03GradeList(List ids) { + // 删除 + demo03GradeErpMapper.deleteByIds(ids); + } + + @Override + public Demo03GradeDO getDemo03Grade(Long id) { + return demo03GradeErpMapper.selectById(id); + } + + private void validateDemo03GradeExists(Long id) { + if (demo03GradeErpMapper.selectById(id) == null) { + throw exception(DEMO03_GRADE_NOT_EXISTS); + } + } + + private void deleteDemo03GradeByStudentId(Long studentId) { + demo03GradeErpMapper.deleteByStudentId(studentId); + } + + private void deleteDemo03GradeByStudentIds(List studentIds) { + demo03GradeErpMapper.deleteByStudentIds(studentIds); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/inner/Demo03StudentInnerService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/inner/Demo03StudentInnerService.java new file mode 100644 index 0000000..a2aa684 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/inner/Demo03StudentInnerService.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo03.inner; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo.Demo03StudentInnerPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo.Demo03StudentInnerSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 学生 Service 接口 + * + * @author 芋道源码 + */ +public interface Demo03StudentInnerService { + + /** + * 创建学生 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemo03Student(@Valid Demo03StudentInnerSaveReqVO createReqVO); + + /** + * 更新学生 + * + * @param updateReqVO 更新信息 + */ + void updateDemo03Student(@Valid Demo03StudentInnerSaveReqVO updateReqVO); + + /** + * 删除学生 + * + * @param id 编号 + */ + void deleteDemo03Student(Long id); + + /** + * 批量删除学生 + * + * @param ids 编号 + */ + void deleteDemo03StudentList(List ids); + + /** + * 获得学生 + * + * @param id 编号 + * @return 学生 + */ + Demo03StudentDO getDemo03Student(Long id); + + /** + * 获得学生分页 + * + * @param pageReqVO 分页查询 + * @return 学生分页 + */ + PageResult getDemo03StudentPage(Demo03StudentInnerPageReqVO pageReqVO); + + // ==================== 子表(学生课程) ==================== + + /** + * 获得学生课程列表 + * + * @param studentId 学生编号 + * @return 学生课程列表 + */ + List getDemo03CourseListByStudentId(Long studentId); + + // ==================== 子表(学生班级) ==================== + + /** + * 获得学生班级 + * + * @param studentId 学生编号 + * @return 学生班级 + */ + Demo03GradeDO getDemo03GradeByStudentId(Long studentId); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/inner/Demo03StudentInnerServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/inner/Demo03StudentInnerServiceImpl.java new file mode 100644 index 0000000..a5008c4 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/inner/Demo03StudentInnerServiceImpl.java @@ -0,0 +1,194 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo03.inner; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo.Demo03StudentInnerPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo.Demo03StudentInnerSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.inner.Demo03CourseInnerMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.inner.Demo03GradeInnerMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.inner.Demo03StudentInnerMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DEMO03_STUDENT_NOT_EXISTS; + +/** + * 学生 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class Demo03StudentInnerServiceImpl implements Demo03StudentInnerService { + + @Resource + private Demo03StudentInnerMapper demo03StudentInnerMapper; + @Resource + private Demo03CourseInnerMapper demo03CourseInnerMapper; + @Resource + private Demo03GradeInnerMapper demo03GradeInnerMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createDemo03Student(Demo03StudentInnerSaveReqVO createReqVO) { + // 插入 + Demo03StudentDO demo03Student = BeanUtils.toBean(createReqVO, Demo03StudentDO.class); + demo03StudentInnerMapper.insert(demo03Student); + + // 插入子表 + createDemo03CourseList(demo03Student.getId(), createReqVO.getDemo03Courses()); + createDemo03Grade(demo03Student.getId(), createReqVO.getDemo03Grade()); + // 返回 + return demo03Student.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDemo03Student(Demo03StudentInnerSaveReqVO updateReqVO) { + // 校验存在 + validateDemo03StudentExists(updateReqVO.getId()); + // 更新 + Demo03StudentDO updateObj = BeanUtils.toBean(updateReqVO, Demo03StudentDO.class); + demo03StudentInnerMapper.updateById(updateObj); + + // 更新子表 + updateDemo03CourseList(updateReqVO.getId(), updateReqVO.getDemo03Courses()); + updateDemo03Grade(updateReqVO.getId(), updateReqVO.getDemo03Grade()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDemo03Student(Long id) { + // 校验存在 + validateDemo03StudentExists(id); + // 删除 + demo03StudentInnerMapper.deleteById(id); + + // 删除子表 + deleteDemo03CourseByStudentId(id); + deleteDemo03GradeByStudentId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDemo03StudentList(List ids) { + // 校验存在 + validateDemo03StudentExists(ids); + // 删除 + demo03StudentInnerMapper.deleteByIds(ids); + + // 删除子表 + deleteDemo03CourseByStudentIds(ids); + deleteDemo03GradeByStudentIds(ids); + } + + private void validateDemo03StudentExists(List ids) { + List list = demo03StudentInnerMapper.selectByIds(ids); + if (CollUtil.isEmpty(list) || list.size() != ids.size()) { + throw exception(DEMO03_STUDENT_NOT_EXISTS); + } + } + + private void validateDemo03StudentExists(Long id) { + if (demo03StudentInnerMapper.selectById(id) == null) { + throw exception(DEMO03_STUDENT_NOT_EXISTS); + } + } + + @Override + public Demo03StudentDO getDemo03Student(Long id) { + return demo03StudentInnerMapper.selectById(id); + } + + @Override + public PageResult getDemo03StudentPage(Demo03StudentInnerPageReqVO pageReqVO) { + return demo03StudentInnerMapper.selectPage(pageReqVO); + } + + // ==================== 子表(学生课程) ==================== + + @Override + public List getDemo03CourseListByStudentId(Long studentId) { + return demo03CourseInnerMapper.selectListByStudentId(studentId); + } + + private void createDemo03CourseList(Long studentId, List list) { + list.forEach(o -> o.setStudentId(studentId).clean()); + demo03CourseInnerMapper.insertBatch(list); + } + + private void updateDemo03CourseList(Long studentId, List list) { + list.forEach(o -> o.setStudentId(studentId).clean()); + List oldList = demo03CourseInnerMapper.selectListByStudentId(studentId); + List> diffList = diffList(oldList, list, (oldVal, newVal) -> { + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId()); + if (same) { + newVal.setId(oldVal.getId()); + } + return same; + }); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + demo03CourseInnerMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + demo03CourseInnerMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + demo03CourseInnerMapper.deleteByIds(convertList(diffList.get(2), Demo03CourseDO::getId)); + } + } + + private void deleteDemo03CourseByStudentId(Long studentId) { + demo03CourseInnerMapper.deleteByStudentId(studentId); + } + + private void deleteDemo03CourseByStudentIds(List studentIds) { + demo03CourseInnerMapper.deleteByStudentIds(studentIds); + } + + // ==================== 子表(学生班级) ==================== + + @Override + public Demo03GradeDO getDemo03GradeByStudentId(Long studentId) { + return demo03GradeInnerMapper.selectByStudentId(studentId); + } + + private void createDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) { + if (demo03Grade == null) { + return; + } + demo03Grade.setStudentId(studentId); + demo03GradeInnerMapper.insert(demo03Grade); + } + + private void updateDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) { + if (demo03Grade == null) { + return; + } + demo03Grade.setStudentId(studentId).clean(); + demo03GradeInnerMapper.insertOrUpdate(demo03Grade); + } + + private void deleteDemo03GradeByStudentId(Long studentId) { + demo03GradeInnerMapper.deleteByStudentId(studentId); + } + + private void deleteDemo03GradeByStudentIds(List studentIds) { + demo03GradeInnerMapper.deleteByStudentIds(studentIds); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/normal/Demo03StudentNormalService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/normal/Demo03StudentNormalService.java new file mode 100644 index 0000000..5256855 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/normal/Demo03StudentNormalService.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo03.normal; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo.Demo03StudentNormalPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo.Demo03StudentNormalSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 学生 Service 接口 + * + * @author 芋道源码 + */ +public interface Demo03StudentNormalService { + + /** + * 创建学生 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemo03Student(@Valid Demo03StudentNormalSaveReqVO createReqVO); + + /** + * 更新学生 + * + * @param updateReqVO 更新信息 + */ + void updateDemo03Student(@Valid Demo03StudentNormalSaveReqVO updateReqVO); + + /** + * 删除学生 + * + * @param id 编号 + */ + void deleteDemo03Student(Long id); + + /** + * 批量删除学生 + * + * @param ids 编号 + */ + void deleteDemo03StudentList(List ids); + + /** + * 获得学生 + * + * @param id 编号 + * @return 学生 + */ + Demo03StudentDO getDemo03Student(Long id); + + /** + * 获得学生分页 + * + * @param pageReqVO 分页查询 + * @return 学生分页 + */ + PageResult getDemo03StudentPage(Demo03StudentNormalPageReqVO pageReqVO); + + // ==================== 子表(学生课程) ==================== + + /** + * 获得学生课程列表 + * + * @param studentId 学生编号 + * @return 学生课程列表 + */ + List getDemo03CourseListByStudentId(Long studentId); + + // ==================== 子表(学生班级) ==================== + + /** + * 获得学生班级 + * + * @param studentId 学生编号 + * @return 学生班级 + */ + Demo03GradeDO getDemo03GradeByStudentId(Long studentId); + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/normal/Demo03StudentNormalServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/normal/Demo03StudentNormalServiceImpl.java new file mode 100644 index 0000000..540f7be --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/normal/Demo03StudentNormalServiceImpl.java @@ -0,0 +1,194 @@ +package cn.iocoder.yudao.module.infra.service.demo.demo03.normal; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo.Demo03StudentNormalPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo.Demo03StudentNormalSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.normal.Demo03CourseNormalMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.normal.Demo03GradeNormalMapper; +import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.normal.Demo03StudentNormalMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DEMO03_STUDENT_NOT_EXISTS; + +/** + * 学生 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class Demo03StudentNormalServiceImpl implements Demo03StudentNormalService { + + @Resource + private Demo03StudentNormalMapper demo03StudentNormalMapper; + @Resource + private Demo03CourseNormalMapper demo03CourseNormalMapper; + @Resource + private Demo03GradeNormalMapper demo03GradeNormalMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createDemo03Student(Demo03StudentNormalSaveReqVO createReqVO) { + // 插入 + Demo03StudentDO demo03Student = BeanUtils.toBean(createReqVO, Demo03StudentDO.class); + demo03StudentNormalMapper.insert(demo03Student); + + // 插入子表 + createDemo03CourseList(demo03Student.getId(), createReqVO.getDemo03Courses()); + createDemo03Grade(demo03Student.getId(), createReqVO.getDemo03Grade()); + // 返回 + return demo03Student.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDemo03Student(Demo03StudentNormalSaveReqVO updateReqVO) { + // 校验存在 + validateDemo03StudentExists(updateReqVO.getId()); + // 更新 + Demo03StudentDO updateObj = BeanUtils.toBean(updateReqVO, Demo03StudentDO.class); + demo03StudentNormalMapper.updateById(updateObj); + + // 更新子表 + updateDemo03CourseList(updateReqVO.getId(), updateReqVO.getDemo03Courses()); + updateDemo03Grade(updateReqVO.getId(), updateReqVO.getDemo03Grade()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDemo03Student(Long id) { + // 校验存在 + validateDemo03StudentExists(id); + // 删除 + demo03StudentNormalMapper.deleteById(id); + + // 删除子表 + deleteDemo03CourseByStudentId(id); + deleteDemo03GradeByStudentId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDemo03StudentList(List ids) { + // 校验存在 + validateDemo03StudentExists(ids); + // 删除 + demo03StudentNormalMapper.deleteByIds(ids); + + // 删除子表 + deleteDemo03CourseByStudentIds(ids); + deleteDemo03GradeByStudentIds(ids); + } + + private void validateDemo03StudentExists(List ids) { + List list = demo03StudentNormalMapper.selectByIds(ids); + if (CollUtil.isEmpty(list) || list.size() != ids.size()) { + throw exception(DEMO03_STUDENT_NOT_EXISTS); + } + } + + private void validateDemo03StudentExists(Long id) { + if (demo03StudentNormalMapper.selectById(id) == null) { + throw exception(DEMO03_STUDENT_NOT_EXISTS); + } + } + + @Override + public Demo03StudentDO getDemo03Student(Long id) { + return demo03StudentNormalMapper.selectById(id); + } + + @Override + public PageResult getDemo03StudentPage(Demo03StudentNormalPageReqVO pageReqVO) { + return demo03StudentNormalMapper.selectPage(pageReqVO); + } + + // ==================== 子表(学生课程) ==================== + + @Override + public List getDemo03CourseListByStudentId(Long studentId) { + return demo03CourseNormalMapper.selectListByStudentId(studentId); + } + + private void createDemo03CourseList(Long studentId, List list) { + list.forEach(o -> o.setStudentId(studentId).clean()); + demo03CourseNormalMapper.insertBatch(list); + } + + private void updateDemo03CourseList(Long studentId, List list) { + list.forEach(o -> o.setStudentId(studentId).clean()); + List oldList = demo03CourseNormalMapper.selectListByStudentId(studentId); + List> diffList = diffList(oldList, list, (oldVal, newVal) -> { + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId()); + if (same) { + newVal.setId(oldVal.getId()); + } + return same; + }); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + demo03CourseNormalMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + demo03CourseNormalMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + demo03CourseNormalMapper.deleteByIds(convertList(diffList.get(2), Demo03CourseDO::getId)); + } + } + + private void deleteDemo03CourseByStudentId(Long studentId) { + demo03CourseNormalMapper.deleteByStudentId(studentId); + } + + private void deleteDemo03CourseByStudentIds(List studentIds) { + demo03CourseNormalMapper.deleteByStudentIds(studentIds); + } + + // ==================== 子表(学生班级) ==================== + + @Override + public Demo03GradeDO getDemo03GradeByStudentId(Long studentId) { + return demo03GradeNormalMapper.selectByStudentId(studentId); + } + + private void createDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) { + if (demo03Grade == null) { + return; + } + demo03Grade.setStudentId(studentId); + demo03GradeNormalMapper.insert(demo03Grade); + } + + private void updateDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) { + if (demo03Grade == null) { + return; + } + demo03Grade.setStudentId(studentId).clean(); + demo03GradeNormalMapper.insertOrUpdate(demo03Grade); + } + + private void deleteDemo03GradeByStudentId(Long studentId) { + demo03GradeNormalMapper.deleteByStudentId(studentId); + } + + private void deleteDemo03GradeByStudentIds(List studentIds) { + demo03GradeNormalMapper.deleteByStudentIds(studentIds); + } + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigService.java new file mode 100644 index 0000000..6c69c6e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigService.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * 文件配置 Service 接口 + * + * @author 芋道源码 + */ +public interface FileConfigService { + + /** + * 创建文件配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createFileConfig(@Valid FileConfigSaveReqVO createReqVO); + + /** + * 更新文件配置 + * + * @param updateReqVO 更新信息 + */ + void updateFileConfig(@Valid FileConfigSaveReqVO updateReqVO); + + /** + * 更新文件配置为 Master + * + * @param id 编号 + */ + void updateFileConfigMaster(Long id); + + /** + * 删除文件配置 + * + * @param id 编号 + */ + void deleteFileConfig(Long id); + + /** + * 批量删除文件配置 + * + * @param ids 编号列表 + */ + void deleteFileConfigList(List ids); + + /** + * 获得文件配置 + * + * @param id 编号 + * @return 文件配置 + */ + FileConfigDO getFileConfig(Long id); + + /** + * 获得文件配置分页 + * + * @param pageReqVO 分页查询 + * @return 文件配置分页 + */ + PageResult getFileConfigPage(FileConfigPageReqVO pageReqVO); + + /** + * 测试文件配置是否正确,通过上传文件 + * + * @param id 编号 + * @return 文件 URL + */ + String testFileConfig(Long id) throws Exception; + + /** + * 获得指定编号的文件客户端 + * + * @param id 配置编号 + * @return 文件客户端 + */ + FileClient getFileClient(Long id); + + /** + * 获得 Master 文件客户端 + * + * @return 文件客户端 + */ + FileClient getMasterFileClient(); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImpl.java new file mode 100644 index 0000000..4e3bf80 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileConfigServiceImpl.java @@ -0,0 +1,207 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; +import cn.iocoder.yudao.module.infra.convert.file.FileConfigConvert; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO; +import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientFactory; +import cn.iocoder.yudao.module.infra.framework.file.core.enums.FileStorageEnum; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import jakarta.annotation.Resource; +import jakarta.validation.Validator; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_NOT_EXISTS; + +/** + * 文件配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class FileConfigServiceImpl implements FileConfigService { + + private static final Long CACHE_MASTER_ID = 0L; + + /** + * {@link FileClient} 缓存,通过它异步刷新 fileClientFactory + */ + @Getter + private final LoadingCache clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L), + new CacheLoader() { + + @Override + public FileClient load(Long id) { + FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ? + fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id); + if (config != null) { + fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig()); + } + return fileClientFactory.getFileClient(null == config ? id : config.getId()); + } + + }); + + @Resource + private FileClientFactory fileClientFactory; + + @Resource + private FileConfigMapper fileConfigMapper; + + @Resource + private Validator validator; + + @Override + public Long createFileConfig(FileConfigSaveReqVO createReqVO) { + FileConfigDO fileConfig = FileConfigConvert.INSTANCE.convert(createReqVO) + .setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig())) + .setMaster(false); // 默认非 master + fileConfigMapper.insert(fileConfig); + return fileConfig.getId(); + } + + @Override + public void updateFileConfig(FileConfigSaveReqVO updateReqVO) { + // 校验存在 + FileConfigDO config = validateFileConfigExists(updateReqVO.getId()); + // 更新 + FileConfigDO updateObj = FileConfigConvert.INSTANCE.convert(updateReqVO) + .setConfig(parseClientConfig(config.getStorage(), updateReqVO.getConfig())); + fileConfigMapper.updateById(updateObj); + + // 清空缓存 + clearCache(config.getId(), null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateFileConfigMaster(Long id) { + // 校验存在 + validateFileConfigExists(id); + // 更新其它为非 master + fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false)); + // 更新 + fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true)); + + // 清空缓存 + clearCache(null, true); + } + + private FileClientConfig parseClientConfig(Integer storage, Map config) { + // 获取配置类 + Class configClass = FileStorageEnum.getByStorage(storage) + .getConfigClass(); + FileClientConfig clientConfig = JsonUtils.parseObject2(JsonUtils.toJsonString(config), configClass); + // 参数校验 + ValidationUtils.validate(validator, clientConfig); + // 设置参数 + return clientConfig; + } + + @Override + public void deleteFileConfig(Long id) { + // 校验存在 + FileConfigDO config = validateFileConfigExists(id); + if (Boolean.TRUE.equals(config.getMaster())) { + throw exception(FILE_CONFIG_DELETE_FAIL_MASTER); + } + // 删除 + fileConfigMapper.deleteById(id); + + // 清空缓存 + clearCache(id, null); + } + + @Override + public void deleteFileConfigList(List ids) { + // 校验是否有主配置 + List configs = fileConfigMapper.selectByIds(ids); + for (FileConfigDO config : configs) { + if (Boolean.TRUE.equals(config.getMaster())) { + throw exception(FILE_CONFIG_DELETE_FAIL_MASTER); + } + } + + // 批量删除 + fileConfigMapper.deleteByIds(ids); + + // 清空缓存 + ids.forEach(id -> clearCache(id, null)); + } + + /** + * 清空指定文件配置 + * + * @param id 配置编号 + * @param master 是否主配置 + */ + private void clearCache(Long id, Boolean master) { + if (id != null) { + clientCache.invalidate(id); + } + if (Boolean.TRUE.equals(master)) { + clientCache.invalidate(CACHE_MASTER_ID); + } + } + + private FileConfigDO validateFileConfigExists(Long id) { + FileConfigDO config = fileConfigMapper.selectById(id); + if (config == null) { + throw exception(FILE_CONFIG_NOT_EXISTS); + } + return config; + } + + @Override + public FileConfigDO getFileConfig(Long id) { + return fileConfigMapper.selectById(id); + } + + @Override + public PageResult getFileConfigPage(FileConfigPageReqVO pageReqVO) { + return fileConfigMapper.selectPage(pageReqVO); + } + + @Override + public String testFileConfig(Long id) throws Exception { + // 校验存在 + validateFileConfigExists(id); + // 上传文件 + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg"); + } + + @Override + public FileClient getFileClient(Long id) { + return clientCache.getUnchecked(id); + } + + @Override + public FileClient getMasterFileClient() { + return clientCache.getUnchecked(CACHE_MASTER_ID); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java new file mode 100644 index 0000000..5b15ad8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import jakarta.validation.constraints.NotEmpty; + +import java.util.List; + +/** + * 文件 Service 接口 + * + * @author 芋道源码 + */ +public interface FileService { + + /** + * 获得文件分页 + * + * @param pageReqVO 分页查询 + * @return 文件分页 + */ + PageResult getFilePage(FilePageReqVO pageReqVO); + + /** + * 保存文件,并返回文件的访问路径 + * + * @param content 文件内容 + * @param name 文件名称,允许空 + * @param directory 目录,允许空 + * @param type 文件的 MIME 类型,允许空 + * @return 文件路径 + */ + String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content, + String name, String directory, String type); + + /** + * 生成文件预签名地址信息 + * + * @param name 文件名 + * @param directory 目录 + * @return 预签名地址信息 + */ + FilePresignedUrlRespVO getFilePresignedUrl(@NotEmpty(message = "文件名不能为空") String name, + String directory); + + /** + * 创建文件 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createFile(FileCreateReqVO createReqVO); + + /** + * 删除文件 + * + * @param id 编号 + */ + void deleteFile(Long id) throws Exception; + + /** + * 批量删除文件 + * + * @param ids 编号列表 + */ + void deleteFileList(List ids) throws Exception; + + /** + * 获得文件内容 + * + * @param configId 配置编号 + * @param path 文件路径 + * @return 文件内容 + */ + byte[] getFileContent(Long configId, String path) throws Exception; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java new file mode 100644 index 0000000..98447fb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -0,0 +1,193 @@ +package cn.iocoder.yudao.module.infra.service.file; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; +import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; +import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; +import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; + +/** + * 文件 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class FileServiceImpl implements FileService { + + /** + * 上传文件的前缀,是否包含日期(yyyyMMdd) + * + * 目的:按照日期,进行分目录 + */ + static boolean PATH_PREFIX_DATE_ENABLE = true; + /** + * 上传文件的后缀,是否包含时间戳 + * + * 目的:保证文件的唯一性,避免覆盖 + * 定制:可按需调整成 UUID、或者其他方式 + */ + static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true; + + @Resource + private FileConfigService fileConfigService; + + @Resource + private FileMapper fileMapper; + + @Override + public PageResult getFilePage(FilePageReqVO pageReqVO) { + return fileMapper.selectPage(pageReqVO); + } + + @Override + @SneakyThrows + public String createFile(byte[] content, String name, String directory, String type) { + // 1.1 处理 type 为空的情况 + if (StrUtil.isEmpty(type)) { + type = FileTypeUtils.getMineType(content, name); + } + // 1.2 处理 name 为空的情况 + if (StrUtil.isEmpty(name)) { + name = DigestUtil.sha256Hex(content); + } + if (StrUtil.isEmpty(FileUtil.extName(name))) { + // 如果 name 没有后缀 type,则补充后缀 + String extension = FileTypeUtils.getExtension(type); + if (StrUtil.isNotEmpty(extension)) { + name = name + extension; + } + } + + // 2.1 生成上传的 path,需要保证唯一 + String path = generateUploadPath(name, directory); + // 2.2 上传到文件存储器 + FileClient client = fileConfigService.getMasterFileClient(); + Assert.notNull(client, "客户端(master) 不能为空"); + String url = client.upload(content, path, type); + + // 3. 保存到数据库 + fileMapper.insert(new FileDO().setConfigId(client.getId()) + .setName(name).setPath(path).setUrl(url) + .setType(type).setSize(content.length)); + return url; + } + + @VisibleForTesting + String generateUploadPath(String name, String directory) { + // 1. 生成前缀、后缀 + String prefix = null; + if (PATH_PREFIX_DATE_ENABLE) { + prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN); + } + String suffix = null; + if (PATH_SUFFIX_TIMESTAMP_ENABLE) { + suffix = String.valueOf(System.currentTimeMillis()); + } + + // 2.1 先拼接 suffix 后缀 + if (StrUtil.isNotEmpty(suffix)) { + String ext = FileUtil.extName(name); + if (StrUtil.isNotEmpty(ext)) { + name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext; + } else { + name = name + StrUtil.C_UNDERLINE + suffix; + } + } + // 2.2 再拼接 prefix 前缀 + if (StrUtil.isNotEmpty(prefix)) { + name = prefix + StrUtil.SLASH + name; + } + // 2.3 最后拼接 directory 目录 + if (StrUtil.isNotEmpty(directory)) { + name = directory + StrUtil.SLASH + name; + } + return name; + } + + @Override + @SneakyThrows + public FilePresignedUrlRespVO getFilePresignedUrl(String name, String directory) { + // 1. 生成上传的 path,需要保证唯一 + String path = generateUploadPath(name, directory); + + // 2. 获取文件预签名地址 + FileClient fileClient = fileConfigService.getMasterFileClient(); + FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path); + return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class, + object -> object.setConfigId(fileClient.getId()).setPath(path)); + } + + @Override + public Long createFile(FileCreateReqVO createReqVO) { + FileDO file = BeanUtils.toBean(createReqVO, FileDO.class); + fileMapper.insert(file); + return file.getId(); + } + + @Override + public void deleteFile(Long id) throws Exception { + // 校验存在 + FileDO file = validateFileExists(id); + + // 从文件存储器中删除 + FileClient client = fileConfigService.getFileClient(file.getConfigId()); + Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId()); + client.delete(file.getPath()); + + // 删除记录 + fileMapper.deleteById(id); + } + + @Override + @SneakyThrows + public void deleteFileList(List ids) { + // 删除文件 + List files = fileMapper.selectByIds(ids); + for (FileDO file : files) { + // 获取客户端 + FileClient client = fileConfigService.getFileClient(file.getConfigId()); + Assert.notNull(client, "客户端({}) 不能为空", file.getPath()); + // 删除文件 + client.delete(file.getPath()); + } + + // 删除记录 + fileMapper.deleteByIds(ids); + } + + private FileDO validateFileExists(Long id) { + FileDO fileDO = fileMapper.selectById(id); + if (fileDO == null) { + throw exception(FILE_NOT_EXISTS); + } + return fileDO; + } + + @Override + public byte[] getFileContent(Long configId, String path) throws Exception { + FileClient client = fileConfigService.getFileClient(configId); + Assert.notNull(client, "客户端({}) 不能为空", configId); + return client.getContent(path); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogService.java new file mode 100644 index 0000000..2efff75 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogService.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.quartz.core.service.JobLogFrameworkService; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; + +/** + * Job 日志 Service 接口 + * + * @author 芋道源码 + */ +public interface JobLogService extends JobLogFrameworkService { + + /** + * 获得定时任务 + * + * @param id 编号 + * @return 定时任务 + */ + JobLogDO getJobLog(Long id); + + /** + * 获得定时任务分页 + * + * @param pageReqVO 分页查询 + * @return 定时任务分页 + */ + PageResult getJobLogPage(JobLogPageReqVO pageReqVO); + + /** + * 清理 exceedDay 天前的任务日志 + * + * @param exceedDay 超过多少天就进行清理 + * @param deleteLimit 清理的间隔条数 + */ + Integer cleanJobLog(Integer exceedDay, Integer deleteLimit); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImpl.java new file mode 100644 index 0000000..a2abaff --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobLogServiceImpl.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO; +import cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper; +import cn.iocoder.yudao.module.infra.enums.job.JobLogStatusEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; + +/** + * Job 日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class JobLogServiceImpl implements JobLogService { + + @Resource + private JobLogMapper jobLogMapper; + + @Override + public Long createJobLog(Long jobId, LocalDateTime beginTime, + String jobHandlerName, String jobHandlerParam, Integer executeIndex) { + JobLogDO log = JobLogDO.builder().jobId(jobId).handlerName(jobHandlerName) + .handlerParam(jobHandlerParam).executeIndex(executeIndex) + .beginTime(beginTime).status(JobLogStatusEnum.RUNNING.getStatus()).build(); + jobLogMapper.insert(log); + return log.getId(); + } + + @Override + @Async + public void updateJobLogResultAsync(Long logId, LocalDateTime endTime, Integer duration, boolean success, String result) { + try { + JobLogDO updateObj = JobLogDO.builder().id(logId).endTime(endTime).duration(duration) + .status(success ? JobLogStatusEnum.SUCCESS.getStatus() : JobLogStatusEnum.FAILURE.getStatus()) + .result(result).build(); + jobLogMapper.updateById(updateObj); + } catch (Exception ex) { + log.error("[updateJobLogResultAsync][logId({}) endTime({}) duration({}) success({}) result({})]", + logId, endTime, duration, success, result); + } + } + + @Override + @SuppressWarnings("DuplicatedCode") + public Integer cleanJobLog(Integer exceedDay, Integer deleteLimit) { + int count = 0; + LocalDateTime expireDate = LocalDateTime.now().minusDays(exceedDay); + // 循环删除,直到没有满足条件的数据 + for (int i = 0; i < Short.MAX_VALUE; i++) { + int deleteCount = jobLogMapper.deleteByCreateTimeLt(expireDate, deleteLimit); + count += deleteCount; + // 达到删除预期条数,说明到底了 + if (deleteCount < deleteLimit) { + break; + } + } + return count; + } + + @Override + public JobLogDO getJobLog(Long id) { + return jobLogMapper.selectById(id); + } + + @Override + public PageResult getJobLogPage(JobLogPageReqVO pageReqVO) { + return jobLogMapper.selectPage(pageReqVO); + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobService.java new file mode 100644 index 0000000..b830b08 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobService.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import jakarta.validation.Valid; +import org.quartz.SchedulerException; + +import java.util.List; + +/** + * 定时任务 Service 接口 + * + * @author 芋道源码 + */ +public interface JobService { + + /** + * 创建定时任务 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createJob(@Valid JobSaveReqVO createReqVO) throws SchedulerException; + + /** + * 更新定时任务 + * + * @param updateReqVO 更新信息 + */ + void updateJob(@Valid JobSaveReqVO updateReqVO) throws SchedulerException; + + /** + * 更新定时任务的状态 + * + * @param id 任务编号 + * @param status 状态 + */ + void updateJobStatus(Long id, Integer status) throws SchedulerException; + + /** + * 触发定时任务 + * + * @param id 任务编号 + */ + void triggerJob(Long id) throws SchedulerException; + + /** + * 同步定时任务 + * + * 目的:自己存储的 Job 信息,强制同步到 Quartz 中 + */ + void syncJob() throws SchedulerException; + + /** + * 删除定时任务 + * + * @param id 编号 + */ + void deleteJob(Long id) throws SchedulerException; + + /** + * 批量删除定时任务 + * + * @param ids 编号列表 + */ + void deleteJobList(List ids) throws SchedulerException; + + /** + * 获得定时任务 + * + * @param id 编号 + * @return 定时任务 + */ + JobDO getJob(Long id); + + /** + * 获得定时任务分页 + * + * @param pageReqVO 分页查询 + * @return 定时任务分页 + */ + PageResult getJobPage(JobPageReqVO pageReqVO); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java new file mode 100644 index 0000000..10debe7 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java @@ -0,0 +1,215 @@ +package cn.iocoder.yudao.module.infra.service.job; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; +import cn.iocoder.yudao.framework.quartz.core.util.CronUtils; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; +import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper; +import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.containsAny; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; + +/** + * 定时任务 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class JobServiceImpl implements JobService { + + @Resource + private JobMapper jobMapper; + + @Resource + private SchedulerManager schedulerManager; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createJob(JobSaveReqVO createReqVO) throws SchedulerException { + validateCronExpression(createReqVO.getCronExpression()); + // 1.1 校验唯一性 + if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) { + throw exception(JOB_HANDLER_EXISTS); + } + // 1.2 校验 JobHandler 是否存在 + validateJobHandlerExists(createReqVO.getHandlerName()); + + // 2. 插入 JobDO + JobDO job = BeanUtils.toBean(createReqVO, JobDO.class); + job.setStatus(JobStatusEnum.INIT.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + + // 3.1 添加 Job 到 Quartz 中 + schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), + createReqVO.getRetryCount(), createReqVO.getRetryInterval()); + // 3.2 更新 JobDO + JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.NORMAL.getStatus()).build(); + jobMapper.updateById(updateObj); + return job.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateJob(JobSaveReqVO updateReqVO) throws SchedulerException { + validateCronExpression(updateReqVO.getCronExpression()); + // 1.1 校验存在 + JobDO job = validateJobExists(updateReqVO.getId()); + // 1.2 只有开启状态,才可以修改.原因是,如果出暂停状态,修改 Quartz Job 时,会导致任务又开始执行 + if (!job.getStatus().equals(JobStatusEnum.NORMAL.getStatus())) { + throw exception(JOB_UPDATE_ONLY_NORMAL_STATUS); + } + // 1.3 校验 JobHandler 是否存在 + validateJobHandlerExists(updateReqVO.getHandlerName()); + + // 2. 更新 JobDO + JobDO updateObj = BeanUtils.toBean(updateReqVO, JobDO.class); + fillJobMonitorTimeoutEmpty(updateObj); + jobMapper.updateById(updateObj); + + // 3. 更新 Job 到 Quartz 中 + schedulerManager.updateJob(job.getHandlerName(), updateReqVO.getHandlerParam(), updateReqVO.getCronExpression(), + updateReqVO.getRetryCount(), updateReqVO.getRetryInterval()); + } + + private void validateJobHandlerExists(String handlerName) { + try { + Object handler = SpringUtil.getBean(handlerName); + assert handler != null; + if (!(handler instanceof JobHandler)) { + throw exception(JOB_HANDLER_BEAN_TYPE_ERROR); + } + } catch (NoSuchBeanDefinitionException e) { + throw exception(JOB_HANDLER_BEAN_NOT_EXISTS); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateJobStatus(Long id, Integer status) throws SchedulerException { + // 校验 status + if (!containsAny(status, JobStatusEnum.NORMAL.getStatus(), JobStatusEnum.STOP.getStatus())) { + throw exception(JOB_CHANGE_STATUS_INVALID); + } + // 校验存在 + JobDO job = validateJobExists(id); + // 校验是否已经为当前状态 + if (job.getStatus().equals(status)) { + throw exception(JOB_CHANGE_STATUS_EQUALS); + } + // 更新 Job 状态 + JobDO updateObj = JobDO.builder().id(id).status(status).build(); + jobMapper.updateById(updateObj); + + // 更新状态 Job 到 Quartz 中 + if (JobStatusEnum.NORMAL.getStatus().equals(status)) { // 开启 + schedulerManager.resumeJob(job.getHandlerName()); + } else { // 暂停 + schedulerManager.pauseJob(job.getHandlerName()); + } + } + + @Override + public void triggerJob(Long id) throws SchedulerException { + // 校验存在 + JobDO job = validateJobExists(id); + + // 触发 Quartz 中的 Job + schedulerManager.triggerJob(job.getId(), job.getHandlerName(), job.getHandlerParam()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncJob() throws SchedulerException { + // 1. 查询 Job 配置 + List jobList = jobMapper.selectList(); + + // 2. 遍历处理 + for (JobDO job : jobList) { + // 2.1 先删除,再创建 + schedulerManager.deleteJob(job.getHandlerName()); + schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), + job.getRetryCount(), job.getRetryInterval()); + // 2.2 如果 status 为暂停,则需要暂停 + if (Objects.equals(job.getStatus(), JobStatusEnum.STOP.getStatus())) { + schedulerManager.pauseJob(job.getHandlerName()); + } + log.info("[syncJob][id({}) handlerName({}) 同步完成]", job.getId(), job.getHandlerName()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJob(Long id) throws SchedulerException { + // 校验存在 + JobDO job = validateJobExists(id); + // 更新 + jobMapper.deleteById(id); + + // 删除 Job 到 Quartz 中 + schedulerManager.deleteJob(job.getHandlerName()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJobList(List ids) throws SchedulerException { + // 批量删除 + List jobs = jobMapper.selectByIds(ids); + jobMapper.deleteByIds(ids); + + // 删除 Job 到 Quartz 中 + for (JobDO job : jobs) { + schedulerManager.deleteJob(job.getHandlerName()); + } + } + + private JobDO validateJobExists(Long id) { + JobDO job = jobMapper.selectById(id); + if (job == null) { + throw exception(JOB_NOT_EXISTS); + } + return job; + } + + private void validateCronExpression(String cronExpression) { + if (!CronUtils.isValid(cronExpression)) { + throw exception(JOB_CRON_EXPRESSION_VALID); + } + } + + @Override + public JobDO getJob(Long id) { + return jobMapper.selectById(id); + } + + @Override + public PageResult getJobPage(JobPageReqVO pageReqVO) { + return jobMapper.selectPage(pageReqVO); + } + + private static void fillJobMonitorTimeoutEmpty(JobDO job) { + if (job.getMonitorTimeout() == null) { + job.setMonitorTimeout(0); + } + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogService.java new file mode 100644 index 0000000..65faa3f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogService.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; + +/** + * API 访问日志 Service 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogService { + + /** + * 创建 API 访问日志 + * + * @param createReqDTO API 访问日志 + */ + void createApiAccessLog(ApiAccessLogCreateReqDTO createReqDTO); + + /** + * 获得 API 访问日志分页 + * + * @param pageReqVO 分页查询 + * @return API 访问日志分页 + */ + PageResult getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO); + + /** + * 清理 exceedDay 天前的访问日志 + * + * @param exceedDay 超过多少天就进行清理 + * @param deleteLimit 清理的间隔条数 + */ + Integer cleanAccessLog(Integer exceedDay, Integer deleteLimit); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java new file mode 100644 index 0000000..d47f192 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiAccessLogMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO.REQUEST_PARAMS_MAX_LENGTH; +import static cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO.RESULT_MSG_MAX_LENGTH; + +/** + * API 访问日志 Service 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@Service +@Validated +public class ApiAccessLogServiceImpl implements ApiAccessLogService { + + @Resource + private ApiAccessLogMapper apiAccessLogMapper; + + @Override + public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { + ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class); + apiAccessLog.setRequestParams(StrUtils.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); + apiAccessLog.setResultMsg(StrUtils.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH)); + if (TenantContextHolder.getTenantId() != null) { + apiAccessLogMapper.insert(apiAccessLog); + } else { + // 极端情况下,上下文中没有租户时,此时忽略租户上下文,避免插入失败! + TenantUtils.executeIgnore(() -> apiAccessLogMapper.insert(apiAccessLog)); + } + } + + @Override + public PageResult getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO) { + return apiAccessLogMapper.selectPage(pageReqVO); + } + + @Override + @SuppressWarnings("DuplicatedCode") + public Integer cleanAccessLog(Integer exceedDay, Integer deleteLimit) { + int count = 0; + LocalDateTime expireDate = LocalDateTime.now().minusDays(exceedDay); + // 循环删除,直到没有满足条件的数据 + for (int i = 0; i < Short.MAX_VALUE; i++) { + int deleteCount = apiAccessLogMapper.deleteByCreateTimeLt(expireDate, deleteLimit); + count += deleteCount; + // 达到删除预期条数,说明到底了 + if (deleteCount < deleteLimit) { + break; + } + } + return count; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogService.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogService.java new file mode 100644 index 0000000..b05ccf3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogService.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; + +/** + * API 错误日志 Service 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogService { + + /** + * 创建 API 错误日志 + * + * @param createReqDTO API 错误日志 + */ + void createApiErrorLog(ApiErrorLogCreateReqDTO createReqDTO); + + /** + * 获得 API 错误日志分页 + * + * @param pageReqVO 分页查询 + * @return API 错误日志分页 + */ + PageResult getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO); + + /** + * 更新 API 错误日志已处理 + * + * @param id API 日志编号 + * @param processStatus 处理结果 + * @param processUserId 处理人 + */ + void updateApiErrorLogProcess(Long id, Integer processStatus, Long processUserId); + + /** + * 清理 exceedDay 天前的错误日志 + * + * @param exceedDay 超过多少天就进行清理 + * @param deleteLimit 清理的间隔条数 + */ + Integer cleanErrorLog(Integer exceedDay, Integer deleteLimit); + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java new file mode 100644 index 0000000..e91c098 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.infra.service.logger; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper; +import cn.iocoder.yudao.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO.REQUEST_PARAMS_MAX_LENGTH; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_NOT_FOUND; +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_PROCESSED; + +/** + * API 错误日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class ApiErrorLogServiceImpl implements ApiErrorLogService { + + @Resource + private ApiErrorLogMapper apiErrorLogMapper; + + @Override + public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { + ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) + .setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); + apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); + if (TenantContextHolder.getTenantId() != null) { + apiErrorLogMapper.insert(apiErrorLog); + } else { + // 极端情况下,上下文中没有租户时,此时忽略租户上下文,避免插入失败! + TenantUtils.executeIgnore(() -> apiErrorLogMapper.insert(apiErrorLog)); + } + } + + @Override + public PageResult getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO) { + return apiErrorLogMapper.selectPage(pageReqVO); + } + + @Override + public void updateApiErrorLogProcess(Long id, Integer processStatus, Long processUserId) { + ApiErrorLogDO errorLog = apiErrorLogMapper.selectById(id); + if (errorLog == null) { + throw exception(API_ERROR_LOG_NOT_FOUND); + } + if (!ApiErrorLogProcessStatusEnum.INIT.getStatus().equals(errorLog.getProcessStatus())) { + throw exception(API_ERROR_LOG_PROCESSED); + } + // 标记处理 + apiErrorLogMapper.updateById(ApiErrorLogDO.builder().id(id).processStatus(processStatus) + .processUserId(processUserId).processTime(LocalDateTime.now()).build()); + } + + @Override + @SuppressWarnings("DuplicatedCode") + public Integer cleanErrorLog(Integer exceedDay, Integer deleteLimit) { + int count = 0; + LocalDateTime expireDate = LocalDateTime.now().minusDays(exceedDay); + // 循环删除,直到没有满足条件的数据 + for (int i = 0; i < Short.MAX_VALUE; i++) { + int deleteCount = apiErrorLogMapper.deleteByCreateTimeLt(expireDate, deleteLimit); + count += deleteCount; + // 达到删除预期条数,说明到底了 + if (deleteCount < deleteLimit) { + break; + } + } + return count; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java new file mode 100644 index 0000000..a3b7150 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.infra.websocket; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; +import cn.iocoder.yudao.module.infra.websocket.message.DemoReceiveMessage; +import cn.iocoder.yudao.module.infra.websocket.message.DemoSendMessage; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +import jakarta.annotation.Resource; + +/** + * WebSocket 示例:单发消息 + * + * @author 芋道源码 + */ +@Component +public class DemoWebSocketMessageListener implements WebSocketMessageListener { + + @Resource + private WebSocketMessageSender webSocketMessageSender; + + @Override + public void onMessage(WebSocketSession session, DemoSendMessage message) { + Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session); + // 情况一:单发 + if (message.getToUserId() != null) { + DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId) + .setText(message.getText()).setSingle(true); + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getToUserId(), // 给指定用户 + "demo-message-receive", toMessage); + return; + } + // 情况二:群发 + DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId) + .setText(message.getText()).setSingle(false); + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), // 给所有用户 + "demo-message-receive", toMessage); + } + + @Override + public String getType() { + return "demo-message-send"; + } + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoReceiveMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoReceiveMessage.java new file mode 100644 index 0000000..03a246c --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoReceiveMessage.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.infra.websocket.message; + +import lombok.Data; + +/** + * 示例:server -> client 同步消息 + * + * @author 芋道源码 + */ +@Data +public class DemoReceiveMessage { + + /** + * 接收人的编号 + */ + private Long fromUserId; + /** + * 内容 + */ + private String text; + + /** + * 是否单聊 + */ + private Boolean single; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoSendMessage.java b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoSendMessage.java new file mode 100644 index 0000000..f0c14f5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoSendMessage.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.infra.websocket.message; + +import lombok.Data; + +/** + * 示例:client -> server 发送消息 + * + * @author 芋道源码 + */ +@Data +public class DemoSendMessage { + + /** + * 发送给谁 + * + * 如果为空,说明发送给所有人 + */ + private Long toUserId; + /** + * 内容 + */ + private String text; + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/controller.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/controller.vm new file mode 100644 index 0000000..9f63e24 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/controller.vm @@ -0,0 +1,271 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}; + +import org.springframework.web.bind.annotation.*; +import ${jakartaPackage}.annotation.Resource; +import org.springframework.validation.annotation.Validated; +#if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end + +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import ${jakartaPackage}.validation.constraints.*; +import ${jakartaPackage}.validation.*; +import ${jakartaPackage}.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import ${PageParamClassName}; +import ${PageResultClassName}; +import ${CommonResultClassName}; +import ${BeanUtils}; +import static ${CommonResultClassName}.success; + +import ${ExcelUtilsClassName}; + +import ${ApiAccessLogClassName}; +import static ${OperateTypeEnumClassName}.*; + +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; +#end +import ${basePackage}.module.${table.moduleName}.service.${table.businessName}.${table.className}Service; + +@Tag(name = "${sceneEnum.name} - ${table.classComment}") +@RestController +##二级的 businessName 暂时不算在 HTTP 路径上,可以根据需要写 +@RequestMapping("/${table.moduleName}/${simpleClassName_strikeCase}") +@Validated +public class ${sceneEnum.prefixClass}${table.className}Controller { + + @Resource + private ${table.className}Service ${classNameVar}Service; + + @PostMapping("/create") + @Operation(summary = "创建${table.classComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')") +#end + public CommonResult<${primaryColumn.javaType}> create${simpleClassName}(@Valid @RequestBody ${saveReqVOClass} ${saveReqVOVar}) { + return success(${classNameVar}Service.create${simpleClassName}(${saveReqVOVar})); + } + + @PutMapping("/update") + @Operation(summary = "更新${table.classComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')") +#end + public CommonResult update${simpleClassName}(@Valid @RequestBody ${updateReqVOClass} ${updateReqVOVar}) { + ${classNameVar}Service.update${simpleClassName}(${updateReqVOVar}); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除${table.classComment}") + @Parameter(name = "id", description = "编号", required = true) +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')") +#end + public CommonResult delete${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { + ${classNameVar}Service.delete${simpleClassName}(id); + return success(true); + } + +#if ( $table.templateType != 2 && $deleteBatchEnable) + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除${table.classComment}") + #if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')") + #end + public CommonResult delete${simpleClassName}List(@RequestParam("ids") List<${primaryColumn.javaType}> ids) { + ${classNameVar}Service.delete${simpleClassName}ListByIds(ids); + return success(true); + } +#end + + @GetMapping("/get") + @Operation(summary = "获得${table.classComment}") + @Parameter(name = "id", description = "编号", required = true, example = "1024") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") +#end + public CommonResult<${respVOClass}> get${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { + ${table.className}DO ${classNameVar} = ${classNameVar}Service.get${simpleClassName}(id); +#if ($voType == 10) + return success(BeanUtils.toBean(${classNameVar}, ${respVOClass}.class)); +#else + return success(${classNameVar}); +#end + } + +#if ( $table.templateType != 2 ) + @GetMapping("/page") + @Operation(summary = "获得${table.classComment}分页") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") +#end + public CommonResult> get${simpleClassName}Page(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) { + PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(pageReqVO); +#if ($voType == 10) + return success(BeanUtils.toBean(pageResult, ${respVOClass}.class)); +#else + return success(pageResult); +#end + } + +## 特殊:树表专属逻辑(树不需要分页接口) +#else + @GetMapping("/list") + @Operation(summary = "获得${table.classComment}列表") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") +#end + public CommonResult> get${simpleClassName}List(@Valid ${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO) { + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(listReqVO); +#if ($voType == 10) + return success(BeanUtils.toBean(list, ${respVOClass}.class)); +#else + return success(list); +#end + } + +#end + @GetMapping("/export-excel") + @Operation(summary = "导出${table.classComment} Excel") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:export')") +#end + @ApiAccessLog(operateType = EXPORT) +#if ( $table.templateType != 2 ) + public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}Page(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${respVOClass}.class, + BeanUtils.toBean(list, ${respVOClass}.class)); + } +## 特殊:树表专属逻辑(树不需要分页接口) +#else + public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO, + HttpServletResponse response) throws IOException { + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(listReqVO); + // 导出 Excel + ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${table.className}RespVO.class, + BeanUtils.toBean(list, ${table.className}RespVO.class)); + } +#end + +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 +#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 +#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) +#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) +#set ($subClassNameVar = $subClassNameVars.get($index)) + // ==================== 子表($subTable.classComment) ==================== + +## 情况一:MASTER_ERP 时,需要分查询页子表 +#if ( $table.templateType == 11 ) + @GetMapping("/${subSimpleClassName_strikeCase}/page") + @Operation(summary = "获得${subTable.classComment}分页") + @Parameter(name = "${subJoinColumn.javaField}", description = "${subJoinColumn.columnComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") +#end + public CommonResult> get${subSimpleClassName}Page(PageParam pageReqVO, + @RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return success(${classNameVar}Service.get${subSimpleClassName}Page(pageReqVO, ${subJoinColumn.javaField})); + } + +## 情况二:非 MASTER_ERP 时,需要列表查询子表 +#else + #if ( $subTable.subJoinMany ) + @GetMapping("/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}") + @Operation(summary = "获得${subTable.classComment}列表") + @Parameter(name = "${subJoinColumn.javaField}", description = "${subJoinColumn.columnComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") +#end + public CommonResult> get${subSimpleClassName}ListBy${SubJoinColumnName}(@RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return success(${classNameVar}Service.get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField})); + } + + #else + @GetMapping("/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}") + @Operation(summary = "获得${subTable.classComment}") + @Parameter(name = "${subJoinColumn.javaField}", description = "${subJoinColumn.columnComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") +#end + public CommonResult<${subTable.className}DO> get${subSimpleClassName}By${SubJoinColumnName}(@RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return success(${classNameVar}Service.get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField})); + } + + #end +#end +## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 +#if ( $table.templateType == 11 ) + @PostMapping("/${subSimpleClassName_strikeCase}/create") + @Operation(summary = "创建${subTable.classComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')") +#end + public CommonResult<${subPrimaryColumn.javaType}> create${subSimpleClassName}(@Valid @RequestBody ${subTable.className}DO ${subClassNameVar}) { + return success(${classNameVar}Service.create${subSimpleClassName}(${subClassNameVar})); + } + + @PutMapping("/${subSimpleClassName_strikeCase}/update") + @Operation(summary = "更新${subTable.classComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')") +#end + public CommonResult update${subSimpleClassName}(@Valid @RequestBody ${subTable.className}DO ${subClassNameVar}) { + ${classNameVar}Service.update${subSimpleClassName}(${subClassNameVar}); + return success(true); + } + + @DeleteMapping("/${subSimpleClassName_strikeCase}/delete") + @Parameter(name = "id", description = "编号", required = true) + @Operation(summary = "删除${subTable.classComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')") +#end + public CommonResult delete${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) { + ${classNameVar}Service.delete${subSimpleClassName}(id); + return success(true); + } + +#if ($deleteBatchEnable) + @DeleteMapping("/${subSimpleClassName_strikeCase}/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除${subTable.classComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')") +#end + public CommonResult delete${subSimpleClassName}List(@RequestParam("ids") List<${subPrimaryColumn.javaType}> ids) { + ${classNameVar}Service.delete${subSimpleClassName}ListByIds(ids); + return success(true); + } +#end + + @GetMapping("/${subSimpleClassName_strikeCase}/get") + @Operation(summary = "获得${subTable.classComment}") + @Parameter(name = "id", description = "编号", required = true) +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") +#end + public CommonResult<${subTable.className}DO> get${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) { + return success(${classNameVar}Service.get${subSimpleClassName}(id)); + } + +#end +#end +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/listReqVO.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/listReqVO.vm new file mode 100644 index 0000000..46b6a25 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/listReqVO.vm @@ -0,0 +1,45 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import ${PageParamClassName}; +#foreach ($column in $columns) +#if (${column.javaType} == "BigDecimal") +import java.math.BigDecimal; +#break +#end +#end +## 处理 LocalDateTime 字段的引入 +#foreach ($column in $columns) +#if (${column.listOperation} && ${column.javaType} == "LocalDateTime") +import java.time.LocalDateTime; +import org.springframework.format.annotation.DateTimeFormat; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end +## 字段模板 +#macro(columnTpl $prefix $prefixStr) + @Schema(description = "${prefixStr}${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; +#end + +@Schema(description = "${sceneEnum.name} - ${table.classComment}列表 Request VO") +@Data +public class ${sceneEnum.prefixClass}${table.className}ListReqVO { + +#foreach ($column in $columns) +#if (${column.listOperation})##查询操作 +#if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 + @Schema(description = "${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private ${column.javaType}[] ${column.javaField}; +#else##情况二,非 Between 的时间 + #columnTpl('', '') +#end + +#end +#end +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/pageReqVO.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/pageReqVO.vm new file mode 100644 index 0000000..d0367a3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/pageReqVO.vm @@ -0,0 +1,45 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import ${PageParamClassName}; +#foreach ($column in $columns) +#if (${column.javaType} == "BigDecimal") +import java.math.BigDecimal; +#break +#end +#end +## 处理 LocalDateTime 字段的引入 +#foreach ($column in $columns) +#if (${column.listOperationCondition} && ${column.javaType} == "LocalDateTime") +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end +## 字段模板 +#macro(columnTpl $prefix $prefixStr) + @Schema(description = "${prefixStr}${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; +#end + +@Schema(description = "${sceneEnum.name} - ${table.classComment}分页 Request VO") +@Data +public class ${sceneEnum.prefixClass}${table.className}PageReqVO extends PageParam { + +#foreach ($column in $columns) +#if (${column.listOperation})##查询操作 +#if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 + @Schema(description = "${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private ${column.javaType}[] ${column.javaField}; +#else##情况二,非 Between 的时间 + #columnTpl('', '') +#end + +#end +#end +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm new file mode 100644 index 0000000..24c3519 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm @@ -0,0 +1,53 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +## 处理 BigDecimal 字段的引入 +#foreach ($column in $columns) +#if (${column.javaType} == "BigDecimal") +import java.math.BigDecimal; +#break +#end +#end +## 处理 LocalDateTime 字段的引入 +#foreach ($column in $columns) +#if (${column.listOperationResult} && ${column.javaType} == "LocalDateTime") +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +#break +#end +#end +## 处理 Excel 导出 +import com.alibaba.excel.annotation.*; +#foreach ($column in $columns) +#if ("$!column.dictType" != "")## 有设置数据字典 +import ${DictFormatClassName}; +import ${DictConvertClassName}; +#break +#end +#end + +@Schema(description = "${sceneEnum.name} - ${table.classComment} Response VO") +@Data +@ExcelIgnoreUnannotated +public class ${sceneEnum.prefixClass}${table.className}RespVO { + +## 逐个处理字段 +#foreach ($column in $columns) +#if (${column.listOperationResult}) +## 1. 处理 Swagger 注解 + @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end) +## 2. 处理 Excel 导出 +#if ("$!column.dictType" != "")##处理枚举值 + @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class) + @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 +#else + @ExcelProperty("${column.columnComment}") +#end +## 3. 处理字段定义 + private ${column.javaType} ${column.javaField}; + +#end +#end +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/saveReqVO.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/saveReqVO.vm new file mode 100644 index 0000000..b432c75 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/controller/vo/saveReqVO.vm @@ -0,0 +1,64 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import ${jakartaPackage}.validation.constraints.*; +## 处理 BigDecimal 字段的引入 +#foreach ($column in $columns) +#if (${column.javaType} == "BigDecimal") +import java.math.BigDecimal; +#break +#end +#end +## 处理 LocalDateTime 字段的引入 +#foreach ($column in $columns) +#if ((${column.createOperation} || ${column.updateOperation}) && ${column.javaType} == "LocalDateTime") +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +#break +#end +#end +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; +#end + +@Schema(description = "${sceneEnum.name} - ${table.classComment}新增/修改 Request VO") +@Data +public class ${sceneEnum.prefixClass}${table.className}SaveReqVO { + +## 逐个处理字段 +#foreach ($column in $columns) +#if (${column.createOperation} || ${column.updateOperation}) +## 1. 处理 Swagger 注解 + @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end) +## 2. 处理 Validator 参数校验 +#if (!${column.nullable} && !${column.primaryKey}) +#if (${column.javaType} == 'String') + @NotEmpty(message = "${column.columnComment}不能为空") +#else + @NotNull(message = "${column.columnComment}不能为空") +#end +#end +## 3. 处理字段定义 + private ${column.javaType} ${column.javaField}; + +#end +#end +## 特殊:主子表专属逻辑(非 ERP 模式) +#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) + #if ( $subTable.subJoinMany) + @Schema(description = "${subTable.classComment}列表") + private List<${subTable.className}DO> ${subClassNameVars.get($index)}s; + + #else + @Schema(description = "${subTable.classComment}") + private ${subTable.className}DO ${subClassNameVars.get($index)}; + + #end +#end +#end +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm new file mode 100644 index 0000000..baf53f5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm @@ -0,0 +1,103 @@ +package ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}; + +import lombok.*; +import java.util.*; +#foreach ($column in $columns) +#if (${column.javaType} == "BigDecimal") +import java.math.BigDecimal; +#end +#if (${column.javaType} == "LocalDateTime") +import java.time.LocalDateTime; +#end +#end +import com.baomidou.mybatisplus.annotation.*; +import ${BaseDOClassName}; +## 处理 Excel 导出 + Schema 注解(仅 DO 模式) +#if ($voType == 20) +import io.swagger.v3.oas.annotations.media.Schema; +import com.alibaba.excel.annotation.*; +#foreach ($column in $columns) + #if ("$!column.dictType" != "")## 有设置数据字典 + import ${DictFormatClassName}; + import ${DictConvertClassName}; + #break + #end +#end +#end + +/** + * ${table.classComment} DO + * + * @author ${table.author} + */ +@TableName("${table.tableName.toLowerCase()}") +@KeySequence("${table.tableName.toLowerCase()}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +## 处理 Excel 导出 + Schema 注解(仅 DO 模式) +#if ($voType == 20) +@Schema(description = "${sceneEnum.name} - ${table.classComment} Response VO") +@ExcelIgnoreUnannotated +#end +public class ${table.className}DO extends BaseDO { + +## 特殊:树表专属逻辑 +#if ( $table.templateType == 2 ) + public static final Long ${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT = 0L; + +#end +#foreach ($column in $columns) +#if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段 + /** + * ${column.columnComment} + #if ("$!column.dictType" != "")##处理枚举值 + * + * 枚举 {@link TODO ${column.dictType} 对应的类} + #end + */ + #if (${column.primaryKey})##处理主键 + @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#end + #end +#if ($voType == 20) +## 1. 处理 Swagger 注解 + @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end) +## 2. 处理 Excel 导出 +#if ("$!column.dictType" != "")##处理枚举值 + @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class) + @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 +#else + @ExcelProperty("${column.columnComment}") +#end +#end +## 3. 处理字段定义 + private ${column.javaType} ${column.javaField}; +#end +#end + +## 特殊:主子表专属逻辑(非 ERP 模式) +#if ( $voType == 20 && $subTables && $subTables.size() > 0 && $table.templateType != 11 ) + #foreach ($subTable in $subTables) + #set ($index = $foreach.count - 1) + #if ( $subTable.subJoinMany) + /** + * ${subTable.classComment}列表 + */ + @Schema(description = "${subTable.classComment}列表") + @TableField(exist = false) + private List<${subTable.className}DO> ${subClassNameVars.get($index)}s; + #else + /** + * ${subTable.classComment} + */ + @Schema(description = "${subTable.classComment}") + @TableField(exist = false) + private ${subTable.className}DO ${subClassNameVars.get($index)}; + #end + #end +#end + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/do_sub.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/do_sub.vm new file mode 100644 index 0000000..0dfc38f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/do_sub.vm @@ -0,0 +1,69 @@ +#set ($subTable = $subTables.get($subIndex))##当前表 +#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 +package ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}; + +import lombok.*; +import java.util.*; +#foreach ($column in $subColumns) +#if (${column.javaType} == "BigDecimal") +import java.math.BigDecimal; +#end +#if (${column.javaType} == "LocalDateTime") +import java.time.LocalDateTime; +#end +#end +import com.baomidou.mybatisplus.annotation.*; +import ${BaseDOClassName}; +## 处理 Schema 注解(仅 DO 模式) +#if ($voType == 20) +import io.swagger.v3.oas.annotations.media.Schema; +#foreach ($column in $columns) + #if ("$!column.dictType" != "")## 有设置数据字典 + import ${DictFormatClassName}; + import ${DictConvertClassName}; + #break + #end +#end +#end + +/** + * ${subTable.classComment} DO + * + * @author ${subTable.author} + */ +@TableName("${subTable.tableName.toLowerCase()}") +@KeySequence("${subTable.tableName.toLowerCase()}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +## 处理 Schema 注解(仅 DO 模式) +#if ($voType == 20) +@Schema(description = "${sceneEnum.name} - ${table.classComment} Response VO") +#end +public class ${subTable.className}DO extends BaseDO { + +#foreach ($column in $subColumns) +#if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段 + /** + * ${column.columnComment} + #if ("$!column.dictType" != "")##处理枚举值 + * + * 枚举 {@link TODO ${column.dictType} 对应的类} + #end + */ + #if (${column.primaryKey})##处理主键 + @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#end + #end +#if ($voType == 20) +## 1. 处理 Swagger 注解 + @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end) +#end +## 2. 处理字段定义 + private ${column.javaType} ${column.javaField}; +#end +#end + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/mapper.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/mapper.vm new file mode 100644 index 0000000..b98b471 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/mapper.vm @@ -0,0 +1,82 @@ +package ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}; + +import java.util.*; + +import ${PageResultClassName}; +import ${QueryWrapperClassName}; +import ${BaseMapperClassName}; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import org.apache.ibatis.annotations.Mapper; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; + +## 字段模板 +#macro(listCondition) +#foreach ($column in $columns) +#if (${column.listOperation}) +#set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 +#if (${column.listOperationCondition} == "=")##情况一,= 的时候 + .eqIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "!=")##情况二,!= 的时候 + .neIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == ">")##情况三,> 的时候 + .gtIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == ">=")##情况四,>= 的时候 + .geIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "<")##情况五,< 的时候 + .ltIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "<=")##情况五,<= 的时候 + .leIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "LIKE")##情况七,Like 的时候 + .likeIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "BETWEEN")##情况八,Between 的时候 + .betweenIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#end +#end +#end +/** + * ${table.classComment} Mapper + * + * @author ${table.author} + */ +@Mapper +public interface ${table.className}Mapper extends BaseMapperX<${table.className}DO> { + +## 特殊:树表专属逻辑(树不需要分页接口) +#if ( $table.templateType != 2 ) + default PageResult<${table.className}DO> selectPage(${sceneEnum.prefixClass}${table.className}PageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX<${table.className}DO>() + #listCondition() + .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 + + } +#else + default List<${table.className}DO> selectList(${sceneEnum.prefixClass}${table.className}ListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX<${table.className}DO>() + #listCondition() + .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 + + } +#end + +## 特殊:树表专属逻辑 +#if ( $table.templateType == 2 ) +#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 +#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 + default ${table.className}DO selectBy${TreeParentJavaField}And${TreeNameJavaField}(Long ${treeParentColumn.javaField}, String ${treeNameColumn.javaField}) { + return selectOne(${table.className}DO::get${TreeParentJavaField}, ${treeParentColumn.javaField}, ${table.className}DO::get${TreeNameJavaField}, ${treeNameColumn.javaField}); + } + + default Long selectCountBy${TreeParentJavaField}(${treeParentColumn.javaType} ${treeParentColumn.javaField}) { + return selectCount(${table.className}DO::get${TreeParentJavaField}, ${treeParentColumn.javaField}); + } + +#end +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/mapper.xml.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/mapper.xml.vm new file mode 100644 index 0000000..290378d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/mapper.xml.vm @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/mapper_sub.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/mapper_sub.vm new file mode 100644 index 0000000..40f2ff5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/dal/mapper_sub.vm @@ -0,0 +1,63 @@ +#set ($subTable = $subTables.get($subIndex))##当前表 +#set ($subColumns = $subJoinColumnsList.get($subIndex))##当前字段数组 +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 +package ${basePackage}.module.${subTable.moduleName}.dal.mysql.${subTable.businessName}; + +import java.util.*; + +import ${PageResultClassName}; +import ${PageParamClassName}; +import ${QueryWrapperClassName}; +import ${BaseMapperClassName}; +import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; +import org.apache.ibatis.annotations.Mapper; + +/** + * ${subTable.classComment} Mapper + * + * @author ${subTable.author} + */ +@Mapper +public interface ${subTable.className}Mapper extends BaseMapperX<${subTable.className}DO> { + +## 情况一:MASTER_ERP 时,需要分查询页子表 +#if ( $table.templateType == 11 ) + default PageResult<${subTable.className}DO> selectPage(PageParam reqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return selectPage(reqVO, new LambdaQueryWrapperX<${subTable.className}DO>() + .eq(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}) + .orderByDesc(${subTable.className}DO::getId));## 大多数情况下,id 倒序 + + } +## 主表与子表是一对一时 + #if (!$subTable.subJoinMany) + default ${subTable.className}DO selectBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return selectOne(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); + } + #end + +## 情况二:非 MASTER_ERP 时,需要列表查询子表 +#else + #if ( $subTable.subJoinMany) + default List<${subTable.className}DO> selectListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return selectList(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); + } + + #else + default ${subTable.className}DO selectBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return selectOne(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); + } + + #end + #end + default int deleteBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return delete(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}); + } + +#if ( $table.templateType != 2 && $deleteBatchEnable) + default int deleteBy${SubJoinColumnName}s(List<${subJoinColumn.javaType}> ${subJoinColumn.javaField}s) { + return deleteBatch(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField}s); + } +#end + +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/enums/errorcode.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/enums/errorcode.vm new file mode 100644 index 0000000..0d1343b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/enums/errorcode.vm @@ -0,0 +1,22 @@ +// TODO 待办:请将下面的错误码复制到 yudao-module-${table.moduleName} 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! +// ========== ${table.classComment} TODO 补充编号 ========== +ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${table.classComment}不存在"); +## 特殊:树表专属逻辑 +#if ( $table.templateType == 2 ) +ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_EXITS_CHILDREN = new ErrorCode(TODO 补充编号, "存在存在子${table.classComment},无法删除"); +ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_NOT_EXITS = new ErrorCode(TODO 补充编号,"父级${table.classComment}不存在"); +ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_ERROR = new ErrorCode(TODO 补充编号, "不能设置自己为父${table.classComment}"); +ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE = new ErrorCode(TODO 补充编号, "已经存在该${treeNameColumn.columnComment}的${table.classComment}"); +ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_IS_CHILD = new ErrorCode(TODO 补充编号, "不能设置自己的子${table.className}为父${table.className}"); +#end +## 特殊:主子表专属逻辑 +#if ( $table.templateType == 11 )## 特殊:ERP 情况 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($simpleClassNameUnderlineCase = $simpleClassNameUnderlineCases.get($index)) +ErrorCode ${simpleClassNameUnderlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${subTable.classComment}不存在"); +#if ( !$subTable.subJoinMany ) +ErrorCode ${simpleClassNameUnderlineCase.toUpperCase()}_EXISTS = new ErrorCode(TODO 补充编号, "${subTable.classComment}已存在"); +#end +#end +#end \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/service/service.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/service/service.vm new file mode 100644 index 0000000..1cc68bf --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/service/service.vm @@ -0,0 +1,165 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import java.util.*; +import ${jakartaPackage}.validation.*; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; +#end +import ${PageResultClassName}; +import ${PageParamClassName}; + +/** + * ${table.classComment} Service 接口 + * + * @author ${table.author} + */ +public interface ${table.className}Service { + + /** + * 创建${table.classComment} + * + * @param ${saveReqVOVar} 创建信息 + * @return 编号 + */ + ${primaryColumn.javaType} create${simpleClassName}(@Valid ${saveReqVOClass} ${saveReqVOVar}); + + /** + * 更新${table.classComment} + * + * @param ${updateReqVOVar} 更新信息 + */ + void update${simpleClassName}(@Valid ${updateReqVOClass} ${updateReqVOVar}); + + /** + * 删除${table.classComment} + * + * @param id 编号 + */ + void delete${simpleClassName}(${primaryColumn.javaType} id); + +#if ( $table.templateType != 2 && $deleteBatchEnable) + /** + * 批量删除${table.classComment} + * + * @param ids 编号 + */ + void delete${simpleClassName}ListByIds(List<${primaryColumn.javaType}> ids); +#end + + /** + * 获得${table.classComment} + * + * @param id 编号 + * @return ${table.classComment} + */ + ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id); + +## 特殊:树表专属逻辑(树不需要分页接口) +#if ( $table.templateType != 2 ) + /** + * 获得${table.classComment}分页 + * + * @param pageReqVO 分页查询 + * @return ${table.classComment}分页 + */ + PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO); +#else + /** + * 获得${table.classComment}列表 + * + * @param listReqVO 查询条件 + * @return ${table.classComment}列表 + */ + List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO); +#end + +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 +#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 +#set ($subClassNameVar = $subClassNameVars.get($index)) + // ==================== 子表($subTable.classComment) ==================== + +## 情况一:MASTER_ERP 时,需要分查询页子表 +#if ( $table.templateType == 11 ) + /** + * 获得${subTable.classComment}分页 + * + * @param pageReqVO 分页查询 + * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment} + * @return ${subTable.classComment}分页 + */ + PageResult<${subTable.className}DO> get${subSimpleClassName}Page(PageParam pageReqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}); + +## 情况二:非 MASTER_ERP 时,需要列表查询子表 +#else + #if ( $subTable.subJoinMany ) + /** + * 获得${subTable.classComment}列表 + * + * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment} + * @return ${subTable.classComment}列表 + */ + List<${subTable.className}DO> get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}); + + #else + /** + * 获得${subTable.classComment} + * + * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment} + * @return ${subTable.classComment} + */ + ${subTable.className}DO get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}); + + #end +#end +## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 +#if ( $table.templateType == 11 ) + /** + * 创建${subTable.classComment} + * + * @param ${subClassNameVar} 创建信息 + * @return 编号 + */ + ${subPrimaryColumn.javaType} create${subSimpleClassName}(@Valid ${subTable.className}DO ${subClassNameVar}); + + /** + * 更新${subTable.classComment} + * + * @param ${subClassNameVar} 更新信息 + */ + void update${subSimpleClassName}(@Valid ${subTable.className}DO ${subClassNameVar}); + + /** + * 删除${subTable.classComment} + * + * @param id 编号 + */ + void delete${subSimpleClassName}(${subPrimaryColumn.javaType} id); + +#if ($deleteBatchEnable) + /** + * 批量删除${subTable.classComment} + * + * @param ids 编号 + */ + void delete${subSimpleClassName}ListByIds(List<${subPrimaryColumn.javaType}> ids); +#end + + /** + * 获得${subTable.classComment} + * + * @param id 编号 + * @return ${subTable.classComment} + */ + ${subTable.className}DO get${subSimpleClassName}(${subPrimaryColumn.javaType} id); + +#end +#end +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm new file mode 100644 index 0000000..42afdf9 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm @@ -0,0 +1,419 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import cn.hutool.core.collection.CollUtil; +import org.springframework.stereotype.Service; +import ${jakartaPackage}.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO; +#end +import ${PageResultClassName}; +import ${PageParamClassName}; +import ${BeanUtils}; + +import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +import ${basePackage}.module.${subTable.moduleName}.dal.mysql.${subTable.businessName}.${subTable.className}Mapper; +#end + +import static ${ServiceExceptionUtilClassName}.exception; +import static ${CollectionUtilsClassName}.convertList; +import static ${CollectionUtilsClassName}.diffList; +import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; + +/** + * ${table.classComment} Service 实现类 + * + * @author ${table.author} + */ +@Service +@Validated +public class ${table.className}ServiceImpl implements ${table.className}Service { + + @Resource + private ${table.className}Mapper ${classNameVar}Mapper; +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) + @Resource + private ${subTable.className}Mapper ${subClassNameVars.get($index)}Mapper; +#end + + @Override +## 特殊:主子表专属逻辑(非 ERP 模式) +#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) + @Transactional(rollbackFor = Exception.class) +#end + public ${primaryColumn.javaType} create${simpleClassName}(${saveReqVOClass} ${saveReqVOVar}) { +## 特殊:树表专属逻辑 +#if ( $table.templateType == 2 ) +#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 +#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 + // 校验${treeParentColumn.columnComment}的有效性 + validateParent${simpleClassName}(null, ${saveReqVOVar}.get${TreeParentJavaField}()); + // 校验${treeNameColumn.columnComment}的唯一性 + validate${simpleClassName}${TreeNameJavaField}Unique(null, ${saveReqVOVar}.get${TreeParentJavaField}(), ${saveReqVOVar}.get${TreeNameJavaField}()); + +#end + // 插入 +#if ($voType == 10) + ${table.className}DO ${classNameVar} = BeanUtils.toBean(createReqVO, ${table.className}DO.class); + ${classNameVar}Mapper.insert(${classNameVar}); +#else + ${saveReqVOVar}.clean() // 清理掉创建、更新时间等相关属性值 + ${classNameVar}Mapper.insert(${saveReqVOVar}); +#end + +## 特殊:主子表专属逻辑(非 ERP 模式) +#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) + + // 插入子表 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + #if ( $subTable.subJoinMany) + create${subSimpleClassName}List(${classNameVar}.getId(), ${saveReqVOVar}.get${subSimpleClassNames.get($index)}s()); + #else + create${subSimpleClassName}(${classNameVar}.getId(), ${saveReqVOVar}.get${subSimpleClassNames.get($index)}()); + #end +#end +#end + // 返回 + return ${classNameVar}.getId(); + } + + @Override +## 特殊:主子表专属逻辑(非 ERP 模式) +#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) + @Transactional(rollbackFor = Exception.class) +#end + public void update${simpleClassName}(${updateReqVOClass} ${updateReqVOVar}) { + // 校验存在 + validate${simpleClassName}Exists(${updateReqVOVar}.getId()); +## 特殊:树表专属逻辑 +#if ( $table.templateType == 2 ) +#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 +#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 + // 校验${treeParentColumn.columnComment}的有效性 + validateParent${simpleClassName}(${updateReqVOVar}.getId(), ${updateReqVOVar}.get${TreeParentJavaField}()); + // 校验${treeNameColumn.columnComment}的唯一性 + validate${simpleClassName}${TreeNameJavaField}Unique(${updateReqVOVar}.getId(), ${updateReqVOVar}.get${TreeParentJavaField}(), ${updateReqVOVar}.get${TreeNameJavaField}()); + +#end + // 更新 +#if ($voType == 10) + ${table.className}DO updateObj = BeanUtils.toBean(updateReqVO, ${table.className}DO.class); + ${classNameVar}Mapper.updateById(updateObj); +#else + ${updateReqVOVar}.clean() // 清理掉创建、更新时间等相关属性值 + ${classNameVar}Mapper.updateById(${updateReqVOVar}); +#end +## 特殊:主子表专属逻辑(非 ERP 模式) +#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11) + + // 更新子表 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + #if ( $subTable.subJoinMany) + update${subSimpleClassName}List(${updateReqVOVar}.getId(), ${updateReqVOVar}.get${subSimpleClassNames.get($index)}s()); + #else + update${subSimpleClassName}(${updateReqVOVar}.getId(), ${updateReqVOVar}.get${subSimpleClassNames.get($index)}()); + #end +#end +#end + } + + @Override +## 特殊:主子表专属逻辑 +#if ( $subTables && $subTables.size() > 0) + @Transactional(rollbackFor = Exception.class) +#end + public void delete${simpleClassName}(${primaryColumn.javaType} id) { + // 校验存在 + validate${simpleClassName}Exists(id); +## 特殊:树表专属逻辑 +#if ( $table.templateType == 2 ) +#set ($ParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 + // 校验是否有子${table.classComment} + if (${classNameVar}Mapper.selectCountBy${ParentJavaField}(id) > 0) { + throw exception(${simpleClassName_underlineCase.toUpperCase()}_EXITS_CHILDREN); + } +#end + // 删除 + ${classNameVar}Mapper.deleteById(id); +## 特殊:主子表专属逻辑 +#if ( $subTables && $subTables.size() > 0) + + // 删除子表 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + delete${subSimpleClassName}By${SubJoinColumnName}(id); +#end +#end + } + +#if ( $table.templateType != 2 && $deleteBatchEnable) + @Override + ## 特殊:主子表专属逻辑 + #if ( $subTables && $subTables.size() > 0) + @Transactional(rollbackFor = Exception.class) + #end + public void delete${simpleClassName}ListByIds(List<${primaryColumn.javaType}> ids) { + // 删除 + ${classNameVar}Mapper.deleteByIds(ids); + ## 特殊:主子表专属逻辑 + #if ( $subTables && $subTables.size() > 0) + + // 删除子表 + #foreach ($subTable in $subTables) + #set ($index = $foreach.count - 1) + #set ($subSimpleClassName = $subSimpleClassNames.get($index)) + #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 + #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + delete${subSimpleClassName}By${SubJoinColumnName}s(ids); + #end + #end + } + +#end + + private void validate${simpleClassName}Exists(${primaryColumn.javaType} id) { + if (${classNameVar}Mapper.selectById(id) == null) { + throw exception(${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); + } + } + +## 特殊:树表专属逻辑 +#if ( $table.templateType == 2 ) +#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写 +#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写 + private void validateParent${simpleClassName}(Long id, Long ${treeParentColumn.javaField}) { + if (${treeParentColumn.javaField} == null || ${simpleClassName}DO.${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT.equals(${treeParentColumn.javaField})) { + return; + } + // 1. 不能设置自己为父${table.classComment} + if (Objects.equals(id, ${treeParentColumn.javaField})) { + throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_ERROR); + } + // 2. 父${table.classComment}不存在 + ${simpleClassName}DO parent${simpleClassName} = ${classNameVar}Mapper.selectById(${treeParentColumn.javaField}); + if (parent${simpleClassName} == null) { + throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_NOT_EXITS); + } + // 3. 递归校验父${table.classComment},如果父${table.classComment}是自己的子${table.classComment},则报错,避免形成环路 + if (id == null) { // id 为空,说明新增,不需要考虑环路 + return; + } + for (int i = 0; i < Short.MAX_VALUE; i++) { + // 3.1 校验环路 + ${treeParentColumn.javaField} = parent${simpleClassName}.get${TreeParentJavaField}(); + if (Objects.equals(id, ${treeParentColumn.javaField})) { + throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_IS_CHILD); + } + // 3.2 继续递归下一级父${table.classComment} + if (${treeParentColumn.javaField} == null || ${simpleClassName}DO.${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT.equals(${treeParentColumn.javaField})) { + break; + } + parent${simpleClassName} = ${classNameVar}Mapper.selectById(${treeParentColumn.javaField}); + if (parent${simpleClassName} == null) { + break; + } + } + } + + private void validate${simpleClassName}${TreeNameJavaField}Unique(Long id, Long ${treeParentColumn.javaField}, String ${treeNameColumn.javaField}) { + ${simpleClassName}DO ${classNameVar} = ${classNameVar}Mapper.selectBy${TreeParentJavaField}And${TreeNameJavaField}(${treeParentColumn.javaField}, ${treeNameColumn.javaField}); + if (${classNameVar} == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的${table.classComment} + if (id == null) { + throw exception(${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE); + } + if (!Objects.equals(${classNameVar}.getId(), id)) { + throw exception(${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE); + } + } + +#end + @Override + public ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id) { + return ${classNameVar}Mapper.selectById(id); + } + +## 特殊:树表专属逻辑(树不需要分页接口) +#if ( $table.templateType != 2 ) + @Override + public PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) { + return ${classNameVar}Mapper.selectPage(pageReqVO); + } +#else + @Override + public List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO) { + return ${classNameVar}Mapper.selectList(listReqVO); + } +#end + +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($simpleClassNameUnderlineCase = $simpleClassNameUnderlineCases.get($index)) +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 +#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 +#set ($subClassNameVar = $subClassNameVars.get($index)) + // ==================== 子表($subTable.classComment) ==================== + +## 情况一:MASTER_ERP 时,需要分查询页子表 +#if ( $table.templateType == 11 ) + @Override + public PageResult<${subTable.className}DO> get${subSimpleClassName}Page(PageParam pageReqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return ${subClassNameVars.get($index)}Mapper.selectPage(pageReqVO, ${subJoinColumn.javaField}); + } + +## 情况二:非 MASTER_ERP 时,需要列表查询子表 +#else + #if ( $subTable.subJoinMany ) + @Override + public List<${subTable.className}DO> get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return ${subClassNameVars.get($index)}Mapper.selectListBy${SubJoinColumnName}(${subJoinColumn.javaField}); + } + + #else + @Override + public ${subTable.className}DO get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) { + return ${subClassNameVars.get($index)}Mapper.selectBy${SubJoinColumnName}(${subJoinColumn.javaField}); + } + + #end +#end +## 情况一:MASTER_ERP 时,支持单个的新增、修改、删除操作 +#if ( $table.templateType == 11 ) + @Override + public ${subPrimaryColumn.javaType} create${subSimpleClassName}(${subTable.className}DO ${subClassNameVar}) { +## 特殊:一对一时,需要保证只有一条,不能重复插入 +#if ( !$subTable.subJoinMany) + // 校验是否已经存在 + if (${subClassNameVars.get($index)}Mapper.selectBy${SubJoinColumnName}(${subClassNameVar}.get${SubJoinColumnName}()) != null) { + throw exception(${simpleClassNameUnderlineCase.toUpperCase()}_EXISTS); + } + // 插入 +#end + ${subClassNameVar}.clean(); // 清理掉创建、更新时间等相关属性值 + ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar}); + return ${subClassNameVar}.getId(); + } + + @Override + public void update${subSimpleClassName}(${subTable.className}DO ${subClassNameVar}) { + // 校验存在 + validate${subSimpleClassName}Exists(${subClassNameVar}.getId()); + // 更新 + ${subClassNameVar}.clean(); // 解决更新情况下:updateTime 不更新 + ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar}); + } + + @Override + public void delete${subSimpleClassName}(${subPrimaryColumn.javaType} id) { + // 删除 + ${subClassNameVars.get($index)}Mapper.deleteById(id); + } + +#if ($deleteBatchEnable) + @Override + public void delete${subSimpleClassName}ListByIds(List<${subPrimaryColumn.javaType}> ids) { + // 删除 + ${subClassNameVars.get($index)}Mapper.deleteByIds(ids); + } +#end + + @Override + public ${subTable.className}DO get${subSimpleClassName}(${subPrimaryColumn.javaType} id) { + return ${subClassNameVars.get($index)}Mapper.selectById(id); + } + + private void validate${subSimpleClassName}Exists(${subPrimaryColumn.javaType} id) { + if (${subClassNameVar}Mapper.selectById(id) == null) { + throw exception(${simpleClassNameUnderlineCase.toUpperCase()}_NOT_EXISTS); + } + } + +## 情况二:非 MASTER_ERP 时,支持批量的新增、修改操作 +#else + #if ( $subTable.subJoinMany) + private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) { + list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean()); + ${subClassNameVars.get($index)}Mapper.insertBatch(list); + } + + private void update${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) { + list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean()); + List<${subTable.className}DO> oldList = ${subClassNameVar}Mapper.selectListBy${SubJoinColumnName}(${subJoinColumn.javaField}); + List> diffList = diffList(oldList, list, (oldVal, newVal) -> { + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId()); + if (same) { + newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新 + } + return same; + }); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + ${subClassNameVar}Mapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + ${subClassNameVar}Mapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + ${subClassNameVar}Mapper.deleteByIds(convertList(diffList.get(2), ${subTable.className}DO::getId)); + } + } + + #else + private void create${subSimpleClassName}(${primaryColumn.javaType} ${subJoinColumn.javaField}, ${subTable.className}DO ${subClassNameVar}) { + if (${subClassNameVar} == null) { + return; + } + ${subClassNameVar}.set$SubJoinColumnName(${subJoinColumn.javaField}); + ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar}); + } + + private void update${subSimpleClassName}(${primaryColumn.javaType} ${subJoinColumn.javaField}, ${subTable.className}DO ${subClassNameVar}) { + if (${subClassNameVar} == null) { + return; + } + ${subClassNameVar}.set$SubJoinColumnName(${subJoinColumn.javaField}).clean();// 解决更新情况下:updateTime 不更新 + ${subClassNameVars.get($index)}Mapper.insertOrUpdate(${subClassNameVar}); + } + + #end +#end + private void delete${subSimpleClassName}By${SubJoinColumnName}(${primaryColumn.javaType} ${subJoinColumn.javaField}) { + ${subClassNameVars.get($index)}Mapper.deleteBy${SubJoinColumnName}(${subJoinColumn.javaField}); + } + +#if ( $table.templateType != 2 && $deleteBatchEnable) + private void delete${subSimpleClassName}By${SubJoinColumnName}s(List<${primaryColumn.javaType}> ${subJoinColumn.javaField}s) { + ${subClassNameVars.get($index)}Mapper.deleteBy${SubJoinColumnName}s(${subJoinColumn.javaField}s); + } +#end + +#end +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/test/serviceTest.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/test/serviceTest.vm new file mode 100644 index 0000000..bfd4600 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/java/test/serviceTest.vm @@ -0,0 +1,168 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; + +import ${jakartaPackage}.annotation.Resource; + +import ${baseFrameworkPackage}.test.core.ut.BaseDbUnitTest; + +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; +import ${PageResultClassName}; + +import ${jakartaPackage}.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.*; +import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; +import static ${baseFrameworkPackage}.test.core.util.AssertUtils.*; +import static ${baseFrameworkPackage}.test.core.util.RandomUtils.*; +import static ${LocalDateTimeUtilsClassName}.*; +import static ${ObjectUtilsClassName}.*; +import static ${DateUtilsClassName}.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +## 字段模板 +#macro(getPageCondition $VO) + // mock 数据 + ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class, o -> { // 等会查询到 + #foreach ($column in $columns) + #if (${column.listOperation}) + #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 + o.set$JavaField(null); + #end + #end + }); + ${classNameVar}Mapper.insert(db${simpleClassName}); + #foreach ($column in $columns) + #if (${column.listOperation}) + #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 + // 测试 ${column.javaField} 不匹配 + ${classNameVar}Mapper.insert(cloneIgnoreId(db${simpleClassName}, o -> o.set$JavaField(null))); + #end + #end + // 准备参数 + ${sceneEnum.prefixClass}${table.className}${VO} reqVO = new ${sceneEnum.prefixClass}${table.className}${VO}(); + #foreach ($column in $columns) + #if (${column.listOperation}) + #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 + #if (${column.listOperationCondition} == "BETWEEN")## BETWEEN 的情况 + reqVO.set${JavaField}(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + #else + reqVO.set$JavaField(null); + #end + #end + #end +#end +/** + * {@link ${table.className}ServiceImpl} 的单元测试类 + * + * @author ${table.author} + */ +@Import(${table.className}ServiceImpl.class) +public class ${table.className}ServiceImplTest extends BaseDbUnitTest { + + @Resource + private ${table.className}ServiceImpl ${classNameVar}Service; + + @Resource + private ${table.className}Mapper ${classNameVar}Mapper; + + @Test + public void testCreate${simpleClassName}_success() { + // 准备参数 + ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class).setId(null); + + // 调用 + ${primaryColumn.javaType} ${classNameVar}Id = ${classNameVar}Service.create${simpleClassName}(createReqVO); + // 断言 + assertNotNull(${classNameVar}Id); + // 校验记录的属性是否正确 + ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(${classNameVar}Id); + assertPojoEquals(createReqVO, ${classNameVar}, "id"); + } + + @Test + public void testUpdate${simpleClassName}_success() { + // mock 数据 + ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); + ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 + // 准备参数 + ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class, o -> { + o.setId(db${simpleClassName}.getId()); // 设置更新的 ID + }); + + // 调用 + ${classNameVar}Service.update${simpleClassName}(updateReqVO); + // 校验是否更新正确 + ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(updateReqVO.getId()); // 获取最新的 + assertPojoEquals(updateReqVO, ${classNameVar}); + } + + @Test + public void testUpdate${simpleClassName}_notExists() { + // 准备参数 + ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> ${classNameVar}Service.update${simpleClassName}(updateReqVO), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); + } + + @Test + public void testDelete${simpleClassName}_success() { + // mock 数据 + ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); + ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 + // 准备参数 + ${primaryColumn.javaType} id = db${simpleClassName}.getId(); + + // 调用 + ${classNameVar}Service.delete${simpleClassName}(id); + // 校验数据不存在了 + assertNull(${classNameVar}Mapper.selectById(id)); + } + + @Test + public void testDelete${simpleClassName}_notExists() { + // 准备参数 + ${primaryColumn.javaType} id = random${primaryColumn.javaType}Id(); + + // 调用, 并断言异常 + assertServiceException(() -> ${classNameVar}Service.delete${simpleClassName}(id), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); + } + +## 特殊:树表专属逻辑(树不需要分页接口) +#if ( $table.templateType != 2 ) + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGet${simpleClassName}Page() { + #getPageCondition("PageReqVO") + + // 调用 + PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(db${simpleClassName}, pageResult.getList().get(0)); + } +#else + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGet${simpleClassName}List() { + #getPageCondition("ListReqVO") + + // 调用 + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(db${simpleClassName}, list.get(0)); + } +#end + +} \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/sql/h2.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/sql/h2.vm new file mode 100644 index 0000000..a073fdb --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/sql/h2.vm @@ -0,0 +1,37 @@ +-- 将该建表 SQL 语句,添加到 yudao-module-${table.moduleName}-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "${table.tableName.toLowerCase()}" ( +#foreach ($column in $columns) +#if (${column.javaType} == 'Long') + #set ($dataType='bigint') +#elseif (${column.javaType} == 'Integer') + #set ($dataType='int') +#elseif (${column.javaType} == 'Boolean') + #set ($dataType='bit') +#elseif (${column.javaType} == 'Date') + #set ($dataType='datetime') +#else + #set ($dataType='varchar') +#end + #if (${column.primaryKey})##处理主键 + "${column.javaField}"#if (${column.javaType} == 'String') ${dataType} NOT NULL#else ${dataType} NOT NULL GENERATED BY DEFAULT AS IDENTITY#end, + #else + #if (${column.columnName} == 'create_time') + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + #elseif (${column.columnName} == 'update_time') + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + #elseif (${column.columnName} == 'creator' || ${column.columnName} == 'updater') + "${column.columnName}" ${dataType} DEFAULT '', + #elseif (${column.columnName} == 'deleted') + "deleted" bit NOT NULL DEFAULT FALSE, + #elseif (${column.columnName} == 'tenant_id') + "tenant_id" bigint NOT NULL DEFAULT 0, + #else + "${column.columnName.toLowerCase()}" ${dataType}#if (${column.nullable} == false) NOT NULL#end, + #end + #end +#end + PRIMARY KEY ("${primaryColumn.columnName.toLowerCase()}") +) COMMENT '${table.tableComment}'; + +-- 将该删表 SQL 语句,添加到 yudao-module-${table.moduleName}-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "${table.tableName}"; \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/sql/sql.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/sql/sql.vm new file mode 100644 index 0000000..41b107d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/sql/sql.vm @@ -0,0 +1,28 @@ +-- 菜单 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status, component_name +) +VALUES ( + '${table.classComment}管理', '', 2, 0, ${table.parentMenuId}, + '${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}' +); + +-- 按钮父菜单ID +-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码 +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +#set ($functionNames = ['查询', '创建', '更新', '删除', '导出']) +#set ($functionOps = ['query', 'create', 'update', 'delete', 'export']) +#foreach ($functionName in $functionNames) +#set ($index = $foreach.count - 1) +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId, + '', '', '', 0 +); +#end \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/api/api.js.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/api/api.js.vm new file mode 100644 index 0000000..0d9b66f --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/api/api.js.vm @@ -0,0 +1,160 @@ +import request from '@/utils/request' +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +// 创建${table.classComment} +export function create${simpleClassName}(data) { + return request({ + url: '${baseURL}/create', + method: 'post', + data: data + }) +} + +// 更新${table.classComment} +export function update${simpleClassName}(data) { + return request({ + url: '${baseURL}/update', + method: 'put', + data: data + }) +} + +// 删除${table.classComment} +export function delete${simpleClassName}(id) { + return request({ + url: '${baseURL}/delete?id=' + id, + method: 'delete' + }) +} + +#if ( $table.templateType != 2 && $deleteBatchEnable) +/** 批量删除${table.classComment} */ +export function delete${simpleClassName}List(ids) { + return request({ + url: `${baseURL}/delete-list?ids=${ids.join(',')}`, + method: 'delete' + }) +} +#end + +// 获得${table.classComment} +export function get${simpleClassName}(id) { + return request({ + url: '${baseURL}/get?id=' + id, + method: 'get' + }) +} + +#if ( $table.templateType != 2 ) +// 获得${table.classComment}分页 +export function get${simpleClassName}Page(params) { + return request({ + url: '${baseURL}/page', + method: 'get', + params + }) +} +#else +// 获得${table.classComment}列表 +export function get${simpleClassName}List(params) { + return request({ + url: '${baseURL}/list', + method: 'get', + params + }) +} +#end +// 导出${table.classComment} Excel +export function export${simpleClassName}Excel(params) { + return request({ + url: '${baseURL}/export-excel', + method: 'get', + params, + responseType: 'blob' + }) +} +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) + #set ($index = $foreach.count - 1) + #set ($subSimpleClassName = $subSimpleClassNames.get($index)) + #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 + #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 + #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) + #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) + #set ($subClassNameVar = $subClassNameVars.get($index)) + +// ==================== 子表($subTable.classComment) ==================== + ## 情况一:MASTER_ERP 时,需要分查询页子表 + #if ($table.templateType == 11) + // 获得${subTable.classComment}分页 + export function get${subSimpleClassName}Page(params) { + return request({ + url: '${baseURL}/${subSimpleClassName_strikeCase}/page', + method: 'get', + params + }) + } + ## 情况二:非 MASTER_ERP 时,需要列表查询子表 + #else + #if ($subTable.subJoinMany) + // 获得${subTable.classComment}列表 + export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}) { + return request({ + url: '${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=' + ${subJoinColumn.javaField}, + method: 'get' + }) + } + #else + // 获得${subTable.classComment} + export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}) { + return request({ + url: '${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=' + ${subJoinColumn.javaField}, + method: 'get' + }) + } + #end + #end + ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 + #if ($table.templateType == 11) + // 新增${subTable.classComment} + export function create${subSimpleClassName}(data) { + return request({ + url: '${baseURL}/${subSimpleClassName_strikeCase}/create', + method: 'post', + data + }) + } + // 修改${subTable.classComment} + export function update${subSimpleClassName}(data) { + return request({ + url: '${baseURL}/${subSimpleClassName_strikeCase}/update', + method: 'post', + data + }) + } + // 删除${subTable.classComment} + export function delete${subSimpleClassName}(id) { + return request({ + url: '${baseURL}/${subSimpleClassName_strikeCase}/delete?id=' + id, + method: 'delete' + }) + } + #if ($deleteBatchEnable) + /** 批量删除${subTable.classComment} */ + export function delete${subSimpleClassName}List(ids) { + return request({ + url: `${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`, + method: 'delete' + }) + } + #end + // 获得${subTable.classComment} + export function get${subSimpleClassName}(id) { + return request({ + url: '${baseURL}/${subSimpleClassName_strikeCase}/get?id=' + id, + method: 'get' + }) + } + #end +#end \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm new file mode 100644 index 0000000..99aa91a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm @@ -0,0 +1,205 @@ +#set ($subTable = $subTables.get($subIndex))##当前表 +#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm new file mode 100644 index 0000000..ca266be --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm @@ -0,0 +1,2 @@ +## 主表的 normal 和 inner 使用相同的 form 表单 +#parse("codegen/vue/views/components/form_sub_normal.vue.vm") \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm new file mode 100644 index 0000000..48a404a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm @@ -0,0 +1,347 @@ +#set ($subTable = $subTables.get($subIndex))##当前表 +#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm new file mode 100644 index 0000000..e130558 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm @@ -0,0 +1,209 @@ +#set ($subTable = $subTables.get($subIndex))##当前表 +#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm new file mode 100644 index 0000000..90b8e41 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm @@ -0,0 +1,4 @@ +## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点: +## 1)inner 使用 list 不分页,erp 使用 page 分页 +## 2)erp 支持单个子表的新增、修改、删除,inner 不支持 +#parse("codegen/vue/views/components/list_sub_erp.vue.vm") \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/form.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/form.vue.vm new file mode 100644 index 0000000..634d05d --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/form.vue.vm @@ -0,0 +1,320 @@ + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/index.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/index.vue.vm new file mode 100644 index 0000000..30014a8 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue/views/index.vue.vm @@ -0,0 +1,385 @@ + + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm new file mode 100644 index 0000000..c9f7361 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm @@ -0,0 +1,166 @@ +import request from '@/config/axios' +import type { Dayjs } from 'dayjs'; +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) + #set ($index = $foreach.count - 1) + #set ($subSimpleClassName = $subSimpleClassNames.get($index)) + #set ($subColumns = $subColumnsList.get($index))##当前字段数组 +/** ${subTable.classComment}信息 */ +export interface ${subSimpleClassName} { + #foreach ($column in $subColumns) + #if ($column.createOperation || $column.updateOperation) + #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment} + #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime") + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment} + #else + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment} + #end + #end + #end +} + +#end +/** ${table.classComment}信息 */ +export interface ${simpleClassName} { + #foreach ($column in $columns) + #if ($column.createOperation || $column.updateOperation) + #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment} + #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime") + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment} + #else + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment} + #end + #end + #end + #if ( $table.templateType == 2 ) + children?: ${simpleClassName}[]; + #end + ## 特殊:主子表专属逻辑 + #if ( $table.templateType == 10 || $table.templateType == 12 ) + #foreach ($subTable in $subTables) + #set ($index = $foreach.count - 1) + #set ($subSimpleClassName = $subSimpleClassNames.get($index)) + #if ( $subTable.subJoinMany ) + ${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[] + #else + ${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName} + #end + #end + #end +} + +// ${table.classComment} API +export const ${simpleClassName}Api = { +#if ( $table.templateType != 2 ) + // 查询${table.classComment}分页 + get${simpleClassName}Page: async (params: any) => { + return await request.get({ url: `${baseURL}/page`, params }) + }, +#else + // 查询${table.classComment}列表 + get${simpleClassName}List: async (params) => { + return await request.get({ url: `${baseURL}/list`, params }) + }, +#end + + // 查询${table.classComment}详情 + get${simpleClassName}: async (id: number) => { + return await request.get({ url: `${baseURL}/get?id=` + id }) + }, + + // 新增${table.classComment} + create${simpleClassName}: async (data: ${simpleClassName}) => { + return await request.post({ url: `${baseURL}/create`, data }) + }, + + // 修改${table.classComment} + update${simpleClassName}: async (data: ${simpleClassName}) => { + return await request.put({ url: `${baseURL}/update`, data }) + }, + + // 删除${table.classComment} + delete${simpleClassName}: async (id: number) => { + return await request.delete({ url: `${baseURL}/delete?id=` + id }) + }, + +#if ( $table.templateType != 2 && $deleteBatchEnable) + /** 批量删除${table.classComment} */ + delete${simpleClassName}List: async (ids: number[]) => { + return await request.delete({ url: `${baseURL}/delete-list?ids=${ids.join(',')}` }) + }, +#end + + // 导出${table.classComment} Excel + export${simpleClassName}: async (params) => { + return await request.download({ url: `${baseURL}/export-excel`, params }) + }, +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) +#set ($index = $foreach.count - 1) +#set ($subSimpleClassName = $subSimpleClassNames.get($index)) +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 +#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 +#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) +#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) +#set ($subClassNameVar = $subClassNameVars.get($index)) + +// ==================== 子表($subTable.classComment) ==================== +## 情况一:MASTER_ERP 时,需要分查询页子表 +#if ( $table.templateType == 11 ) + + // 获得${subTable.classComment}分页 + get${subSimpleClassName}Page: async (params) => { + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/page`, params }) + }, +## 情况二:非 MASTER_ERP 时,需要列表查询子表 +#else + #if ( $subTable.subJoinMany ) + + // 获得${subTable.classComment}列表 + get${subSimpleClassName}ListBy${SubJoinColumnName}: async (${subJoinColumn.javaField}) => { + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} }) + }, + #else + + // 获得${subTable.classComment} + get${subSimpleClassName}By${SubJoinColumnName}: async (${subJoinColumn.javaField}) => { + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} }) + }, + #end +#end +## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 +#if ( $table.templateType == 11 ) + // 新增${subTable.classComment} + create${subSimpleClassName}: async (data: ${subSimpleClassName}) => { + return await request.post({ url: `${baseURL}/${subSimpleClassName_strikeCase}/create`, data }) + }, + + // 修改${subTable.classComment} + update${subSimpleClassName}: async (data: ${subSimpleClassName}) => { + return await request.put({ url: `${baseURL}/${subSimpleClassName_strikeCase}/update`, data }) + }, + + // 删除${subTable.classComment} + delete${subSimpleClassName}: async (id: number) => { + return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id }) + }, + + #if ($deleteBatchEnable) + /** 批量删除${subTable.classComment} */ + delete${subSimpleClassName}List: async (ids: number[]) => { + return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}` }) + }, + #end + + // 获得${subTable.classComment} + get${subSimpleClassName}: async (id: number) => { + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id }) + }, +#end +#end +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm new file mode 100644 index 0000000..8277e9b --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm @@ -0,0 +1,204 @@ +#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 + + \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm new file mode 100644 index 0000000..d8542c3 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm @@ -0,0 +1,2 @@ +## 主表的 normal 和 inner 使用相同的 form 表单 +#parse("codegen/vue3/views/components/form_sub_normal.vue.vm") \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm new file mode 100644 index 0000000..2212d4a --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm @@ -0,0 +1,360 @@ +#set ($subTable = $subTables.get($subIndex))##当前表 +#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + + \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm new file mode 100644 index 0000000..c0dddc0 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm @@ -0,0 +1,229 @@ +#set ($subTable = $subTables.get($subIndex))##当前表 +#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组 +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex)) +#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + + \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm new file mode 100644 index 0000000..3fe6488 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm @@ -0,0 +1,4 @@ +## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点: +## 1)inner 使用 list 不分页,erp 使用 page 分页 +## 2)erp 支持单个子表的新增、修改、删除,inner 不支持 +#parse("codegen/vue3/views/components/list_sub_erp.vue.vm") \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/form.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/form.vue.vm new file mode 100644 index 0000000..466ea45 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/form.vue.vm @@ -0,0 +1,300 @@ + + \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm new file mode 100644 index 0000000..f6ce0e5 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm @@ -0,0 +1,420 @@ + + + \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/api/api.ts.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/api/api.ts.vm new file mode 100644 index 0000000..b7f2651 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/api/api.ts.vm @@ -0,0 +1,32 @@ +import { defHttp } from '@/utils/http/axios' +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +// 查询${table.classComment}列表 +export function get${simpleClassName}Page(params) { + return defHttp.get({ url: '${baseURL}/page', params }) +} + +// 查询${table.classComment}详情 +export function get${simpleClassName}(id: number) { + return defHttp.get({ url: `${baseURL}/get?id=${id}` }) +} + +// 新增${table.classComment} +export function create${simpleClassName}(data) { + return defHttp.post({ url: '${baseURL}/create', data }) +} + +// 修改${table.classComment} +export function update${simpleClassName}(data) { + return defHttp.put({ url: '${baseURL}/update', data }) +} + +// 删除${table.classComment} +export function delete${simpleClassName}(id: number) { + return defHttp.delete({ url: `${baseURL}/delete?id=${id}` }) +} + +// 导出${table.classComment} Excel +export function export${simpleClassName}(params) { + return defHttp.download({ url: '${baseURL}/export-excel', params }, '${table.classComment}.xls') +} diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/views/data.ts.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/views/data.ts.vm new file mode 100644 index 0000000..56f4e82 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/views/data.ts.vm @@ -0,0 +1,260 @@ +import type {BasicColumn, FormSchema} from '@/components/Table' +import {useRender} from '@/components/Table' +import {DICT_TYPE, getDictOptions} from '@/utils/dict' + +export const columns: BasicColumn[] = [ +#foreach($column in $columns) +#if ($column.listOperationResult) + #set ($dictType=$column.dictType) + #set ($javaField = $column.javaField) + #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + #set ($comment=$column.columnComment) +#if ($column.javaType == "LocalDateTime")## 时间类型 + { + title: '${comment}', + dataIndex: '${javaField}', + width: 180, + customRender: ({ text }) => { + return useRender.renderDate(text) + }, + }, +#elseif("" != $column.dictType)## 数据字典 + { + title: '${comment}', + dataIndex: '${javaField}', + width: 180, + customRender: ({ text }) => { + return useRender.renderDict(text, DICT_TYPE.$dictType.toUpperCase()) + }, + }, +#else + { + title: '${comment}', + dataIndex: '${javaField}', + width: 160, + }, +#end +#end +#end +] + +export const searchFormSchema: FormSchema[] = [ +#foreach($column in $columns) +#if ($column.listOperation) + #set ($dictType=$column.dictType) + #set ($javaType = $column.javaType) + #set ($javaField = $column.javaField) + #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + #set ($comment=$column.columnComment) + #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") + #set ($dictMethod = "number") + #elseif ($javaType == "String") + #set ($dictMethod = "string") + #elseif ($javaType == "Boolean") + #set ($dictMethod = "boolean") + #end + { + label: '${comment}', + field: '${javaField}', + #if ($column.htmlType == "input") + component: 'Input', + #elseif ($column.htmlType == "select") + component: 'Select', + componentProps: { + #if ("" != $dictType)## 设置了 dictType 数据字典的情况 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), + #else## 未设置 dictType 数据字典的情况 + options: [], + #end + }, + #elseif ($column.htmlType == "radio") + component: 'RadioButtonGroup', + componentProps: { + #if ("" != $dictType)## 设置了 dictType 数据字典的情况 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), + #else## 未设置 dictType 数据字典的情况 + options: [], + #end + }, + #elseif($column.htmlType == "datetime") + component: 'RangePicker', + #end + colProps: { span: 8 }, + }, +#end +#end +] + +export const createFormSchema: FormSchema[] = [ + { + label: '编号', + field: 'id', + show: false, + component: 'Input', + }, +#foreach($column in $columns) +#if ($column.createOperation) + #set ($dictType = $column.dictType) + #set ($javaType = $column.javaType) + #set ($javaField = $column.javaField) + #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + #set ($comment = $column.columnComment) + #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") + #set ($dictMethod = "number") + #elseif ($javaType == "String") + #set ($dictMethod = "string") + #elseif ($javaType == "Boolean") + #set ($dictMethod = "boolean") + #end +#if (!$column.primaryKey)## 忽略主键,不用在表单里 + { + label: '${comment}', + field: '${javaField}', + #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 + required: true, + #end + #if ($column.htmlType == "input") + component: 'Input', + #elseif($column.htmlType == "imageUpload")## 图片上传 + component: 'FileUpload', + componentProps: { + fileType: 'image', + maxCount: 1, + }, + #elseif($column.htmlType == "fileUpload")## 文件上传 + component: 'FileUpload', + componentProps: { + fileType: 'file', + maxCount: 1, + }, + #elseif($column.htmlType == "editor")## 文本编辑器 + component: 'Editor', + #elseif($column.htmlType == "select")## 下拉框 + component: 'Select', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "checkbox")## 多选框 + component: 'Checkbox', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "radio")## 单选框 + component: 'RadioButtonGroup', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "datetime")## 时间框 + component: 'DatePicker', + componentProps: { + showTime: true, + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'x', + }, + #elseif($column.htmlType == "textarea")## 文本域 + component: 'InputTextArea', + #end + }, +#end +#end +#end +] + +export const updateFormSchema: FormSchema[] = [ + { + label: '编号', + field: 'id', + show: false, + component: 'Input', + }, +#foreach($column in $columns) +#if ($column.updateOperation) +#set ($dictType = $column.dictType) +#set ($javaType = $column.javaType) +#set ($javaField = $column.javaField) +#set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#set ($comment = $column.columnComment) +#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") + #set ($dictMethod = "number") +#elseif ($javaType == "String") + #set ($dictMethod = "string") +#elseif ($javaType == "Boolean") + #set ($dictMethod = "boolean") +#end + #if (!$column.primaryKey)## 忽略主键,不用在表单里 + { + label: '${comment}', + field: '${javaField}', + #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 + required: true, + #end + #if ($column.htmlType == "input") + component: 'Input', + #elseif($column.htmlType == "imageUpload")## 图片上传 + component: 'FileUpload', + componentProps: { + fileType: 'image', + maxCount: 1, + }, + #elseif($column.htmlType == "fileUpload")## 文件上传 + component: 'FileUpload', + componentProps: { + fileType: 'file', + maxCount: 1, + }, + #elseif($column.htmlType == "editor")## 文本编辑器 + component: 'Editor', + #elseif($column.htmlType == "select")## 下拉框 + component: 'Select', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "checkbox")## 多选框 + component: 'Checkbox', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "radio")## 单选框 + component: 'RadioButtonGroup', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "datetime")## 时间框 + component: 'DatePicker', + componentProps: { + showTime: true, + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'x', + }, + #elseif($column.htmlType == "textarea")## 文本域 + component: 'InputTextArea', + #end + }, + #end +#end +#end +] \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/views/form.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/views/form.vue.vm new file mode 100644 index 0000000..1804365 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/views/form.vue.vm @@ -0,0 +1,58 @@ + + \ No newline at end of file diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/views/index.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/views/index.vue.vm new file mode 100644 index 0000000..9e59b12 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben/views/index.vue.vm @@ -0,0 +1,92 @@ + + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/api/api.ts.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/api/api.ts.vm new file mode 100644 index 0000000..090b148 --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/api/api.ts.vm @@ -0,0 +1,167 @@ +import type { PageParam, PageResult } from '@vben/request'; +import type { Dayjs } from 'dayjs'; + +import { requestClient } from '#/api/request'; +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +export namespace ${simpleClassName}Api { + ## 特殊:主子表专属逻辑 + #foreach ($subTable in $subTables) + #set ($index = $foreach.count - 1) + #set ($subSimpleClassName = $subSimpleClassNames.get($index)) + #set ($subColumns = $subColumnsList.get($index))##当前字段数组 + /** ${subTable.classComment}信息 */ + export interface ${subSimpleClassName} { + #foreach ($column in $subColumns) + #if ($column.createOperation || $column.updateOperation) + #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment} + #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime") + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment} + #else + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment} + #end + #end + #end + } + + #end + /** ${table.classComment}信息 */ + export interface ${simpleClassName} { + #foreach ($column in $columns) + #if ($column.createOperation || $column.updateOperation) + #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment} + #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime") + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment} + #else + ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment} + #end + #end + #end + #if ( $table.templateType == 2 ) + children?: ${simpleClassName}[]; + #end + ## 特殊:主子表专属逻辑 + #if ( $table.templateType == 10 || $table.templateType == 12 ) + #foreach ($subTable in $subTables) + #set ($index = $foreach.count - 1) + #set ($subSimpleClassName = $subSimpleClassNames.get($index)) + #if ( $subTable.subJoinMany ) + ${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[] + #else + ${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName} + #end + #end + #end + } +} + +#if ( $table.templateType != 2 ) +/** 查询${table.classComment}分页 */ +export function get${simpleClassName}Page(params: PageParam) { + return requestClient.get>('${baseURL}/page', { params }); +} +#else +/** 查询${table.classComment}列表 */ +export function get${simpleClassName}List(params: any) { + return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params }); +} +#end + +/** 查询${table.classComment}详情 */ +export function get${simpleClassName}(id: number) { + return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`); +} + +/** 新增${table.classComment} */ +export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) { + return requestClient.post('${baseURL}/create', data); +} + +/** 修改${table.classComment} */ +export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) { + return requestClient.put('${baseURL}/update', data); +} + +/** 删除${table.classComment} */ +export function delete${simpleClassName}(id: number) { + return requestClient.delete(`${baseURL}/delete?id=${id}`); +} + +#if ( $table.templateType != 2 && $deleteBatchEnable) +/** 批量删除${table.classComment} */ +export function delete${simpleClassName}List(ids: number[]) { + return requestClient.delete(`${baseURL}/delete-list?ids=${ids.join(',')}`) +} +#end + +/** 导出${table.classComment} */ +export function export${simpleClassName}(params: any) { + return requestClient.download('${baseURL}/export-excel', params); +} + +## 特殊:主子表专属逻辑 +#foreach ($subTable in $subTables) + #set ($index = $foreach.count - 1) + #set ($subSimpleClassName = $subSimpleClassNames.get($index)) + #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段 + #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段 + #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写 + #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index)) + #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index)) + #set ($subClassNameVar = $subClassNameVars.get($index)) + +// ==================== 子表($subTable.classComment) ==================== + +## 情况一:MASTER_ERP 时,需要分查询页子表 +#if ( $table.templateType == 11 ) +/** 获得${subTable.classComment}分页 */ +export function get${subSimpleClassName}Page(params: PageParam) { + return requestClient.get>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params }); +} + ## 情况二:非 MASTER_ERP 时,需要列表查询子表 +#else +#if ( $subTable.subJoinMany ) +/** 获得${subTable.classComment}列表 */ +export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) { + return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`); +} +#else +/** 获得${subTable.classComment} */ +export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) { + return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`); +} +#end +#end +## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作 +#if ( $table.templateType == 11 ) +/** 新增${subTable.classComment} */ +export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) { + return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data); +} + +/** 修改${subTable.classComment} */ +export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) { + return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data); +} + +/** 删除${subTable.classComment} */ +export function delete${subSimpleClassName}(id: number) { + return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`); +} + +#if ($deleteBatchEnable) +/** 批量删除${subTable.classComment} */ +export function delete${subSimpleClassName}List(ids: number[]) { + return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`) +} +#end + +/** 获得${subTable.classComment} */ +export function get${subSimpleClassName}(id: number) { + return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`); +} +#end +#end + diff --git a/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm new file mode 100644 index 0000000..90c0a6e --- /dev/null +++ b/ruoyi-vue-pro-master-jdk17/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm @@ -0,0 +1,324 @@ + + +