diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7acc5d47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +###################################################################### +# 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 + +/mes-ui-app/unpackage/ diff --git a/.image/Java监控.jpg b/.image/Java监控.jpg new file mode 100644 index 00000000..6ad522ab Binary files /dev/null and b/.image/Java监控.jpg differ diff --git a/.image/MySQL.jpg b/.image/MySQL.jpg new file mode 100644 index 00000000..64a1940c Binary files /dev/null and b/.image/MySQL.jpg differ diff --git a/.image/OA请假-列表.jpg b/.image/OA请假-列表.jpg new file mode 100644 index 00000000..787bb73f Binary files /dev/null and b/.image/OA请假-列表.jpg differ diff --git a/.image/OA请假-发起.jpg b/.image/OA请假-发起.jpg new file mode 100644 index 00000000..1a7342d7 Binary files /dev/null and b/.image/OA请假-发起.jpg differ diff --git a/.image/OA请假-详情.jpg b/.image/OA请假-详情.jpg new file mode 100644 index 00000000..a83e7c14 Binary files /dev/null and b/.image/OA请假-详情.jpg differ diff --git a/.image/Redis.jpg b/.image/Redis.jpg new file mode 100644 index 00000000..95693526 Binary files /dev/null and b/.image/Redis.jpg differ diff --git a/.image/admin-uniapp/01.png b/.image/admin-uniapp/01.png new file mode 100644 index 00000000..0f65d99e Binary files /dev/null and b/.image/admin-uniapp/01.png differ diff --git a/.image/admin-uniapp/02.png b/.image/admin-uniapp/02.png new file mode 100644 index 00000000..05ec781c Binary files /dev/null and b/.image/admin-uniapp/02.png differ diff --git a/.image/admin-uniapp/03.png b/.image/admin-uniapp/03.png new file mode 100644 index 00000000..f400c688 Binary files /dev/null and b/.image/admin-uniapp/03.png differ diff --git a/.image/admin-uniapp/04.png b/.image/admin-uniapp/04.png new file mode 100644 index 00000000..d5d5ea07 Binary files /dev/null and b/.image/admin-uniapp/04.png differ diff --git a/.image/admin-uniapp/05.png b/.image/admin-uniapp/05.png new file mode 100644 index 00000000..1de6d8ae Binary files /dev/null and b/.image/admin-uniapp/05.png differ diff --git a/.image/admin-uniapp/06.png b/.image/admin-uniapp/06.png new file mode 100644 index 00000000..400ae90b Binary files /dev/null and b/.image/admin-uniapp/06.png differ diff --git a/.image/admin-uniapp/07.png b/.image/admin-uniapp/07.png new file mode 100644 index 00000000..2ed8c0ff Binary files /dev/null and b/.image/admin-uniapp/07.png differ diff --git a/.image/admin-uniapp/08.png b/.image/admin-uniapp/08.png new file mode 100644 index 00000000..090e64a6 Binary files /dev/null and b/.image/admin-uniapp/08.png differ diff --git a/.image/admin-uniapp/09.png b/.image/admin-uniapp/09.png new file mode 100644 index 00000000..f2032c8a Binary files /dev/null and b/.image/admin-uniapp/09.png differ diff --git a/.image/common/mall-feature.png b/.image/common/mall-feature.png new file mode 100644 index 00000000..cca05c0e Binary files /dev/null and b/.image/common/mall-feature.png differ diff --git a/.image/common/mall-preview.png b/.image/common/mall-preview.png new file mode 100644 index 00000000..f939214b Binary files /dev/null and b/.image/common/mall-preview.png differ diff --git a/.image/common/mes-cloud-architecture.png b/.image/common/mes-cloud-architecture.png new file mode 100644 index 00000000..59416d80 Binary files /dev/null and b/.image/common/mes-cloud-architecture.png differ diff --git a/.image/common/mes-roadmap.png b/.image/common/mes-roadmap.png new file mode 100644 index 00000000..f4becc98 Binary files /dev/null and b/.image/common/mes-roadmap.png differ diff --git a/.image/common/project-vs.png b/.image/common/project-vs.png new file mode 100644 index 00000000..561e092f Binary files /dev/null and b/.image/common/project-vs.png differ diff --git a/.image/common/ruoyi-vue-pro-architecture.png b/.image/common/ruoyi-vue-pro-architecture.png new file mode 100644 index 00000000..7bd7d59a Binary files /dev/null and b/.image/common/ruoyi-vue-pro-architecture.png differ diff --git a/.image/common/ruoyi-vue-pro-biz.png b/.image/common/ruoyi-vue-pro-biz.png new file mode 100644 index 00000000..24a385ab Binary files /dev/null and b/.image/common/ruoyi-vue-pro-biz.png differ diff --git a/.image/个人中心.jpg b/.image/个人中心.jpg new file mode 100644 index 00000000..ce57f6e1 Binary files /dev/null and b/.image/个人中心.jpg differ diff --git a/.image/代码生成.jpg b/.image/代码生成.jpg new file mode 100644 index 00000000..751603ef Binary files /dev/null and b/.image/代码生成.jpg differ diff --git a/.image/令牌管理.jpg b/.image/令牌管理.jpg new file mode 100644 index 00000000..04abf4d2 Binary files /dev/null and b/.image/令牌管理.jpg differ diff --git a/.image/任务列表-审批.jpg b/.image/任务列表-审批.jpg new file mode 100644 index 00000000..cba312a0 Binary files /dev/null and b/.image/任务列表-审批.jpg differ diff --git a/.image/任务列表-已办.jpg b/.image/任务列表-已办.jpg new file mode 100644 index 00000000..7a8d0fb1 Binary files /dev/null and b/.image/任务列表-已办.jpg differ diff --git a/.image/任务列表-待办.jpg b/.image/任务列表-待办.jpg new file mode 100644 index 00000000..a90323fb Binary files /dev/null and b/.image/任务列表-待办.jpg differ diff --git a/.image/任务日志.jpg b/.image/任务日志.jpg new file mode 100644 index 00000000..599e50a9 Binary files /dev/null and b/.image/任务日志.jpg differ diff --git a/.image/商户信息.jpg b/.image/商户信息.jpg new file mode 100644 index 00000000..483eace1 Binary files /dev/null and b/.image/商户信息.jpg differ diff --git a/.image/在线用户.jpg b/.image/在线用户.jpg new file mode 100644 index 00000000..b183009b Binary files /dev/null and b/.image/在线用户.jpg differ diff --git a/.image/大屏设计器-列表.jpg b/.image/大屏设计器-列表.jpg new file mode 100644 index 00000000..9a45c3bd Binary files /dev/null and b/.image/大屏设计器-列表.jpg differ diff --git a/.image/大屏设计器-编辑.jpg b/.image/大屏设计器-编辑.jpg new file mode 100644 index 00000000..63298a0c Binary files /dev/null and b/.image/大屏设计器-编辑.jpg differ diff --git a/.image/大屏设计器-预览.jpg b/.image/大屏设计器-预览.jpg new file mode 100644 index 00000000..501d9ea2 Binary files /dev/null and b/.image/大屏设计器-预览.jpg differ diff --git a/.image/字典数据.jpg b/.image/字典数据.jpg new file mode 100644 index 00000000..8298c893 Binary files /dev/null and b/.image/字典数据.jpg differ diff --git a/.image/字典类型.jpg b/.image/字典类型.jpg new file mode 100644 index 00000000..6613392f Binary files /dev/null and b/.image/字典类型.jpg differ diff --git a/.image/定时任务.jpg b/.image/定时任务.jpg new file mode 100644 index 00000000..d5bbd851 Binary files /dev/null and b/.image/定时任务.jpg differ diff --git a/.image/岗位管理.jpg b/.image/岗位管理.jpg new file mode 100644 index 00000000..42b64d2c Binary files /dev/null and b/.image/岗位管理.jpg differ diff --git a/.image/应用信息-列表.jpg b/.image/应用信息-列表.jpg new file mode 100644 index 00000000..da419a24 Binary files /dev/null and b/.image/应用信息-列表.jpg differ diff --git a/.image/应用信息-编辑.jpg b/.image/应用信息-编辑.jpg new file mode 100644 index 00000000..913cfbc8 Binary files /dev/null and b/.image/应用信息-编辑.jpg differ diff --git a/.image/应用管理.jpg b/.image/应用管理.jpg new file mode 100644 index 00000000..6e7789fc Binary files /dev/null and b/.image/应用管理.jpg differ diff --git a/.image/我的流程-列表.jpg b/.image/我的流程-列表.jpg new file mode 100644 index 00000000..223d17af Binary files /dev/null and b/.image/我的流程-列表.jpg differ diff --git a/.image/我的流程-发起.jpg b/.image/我的流程-发起.jpg new file mode 100644 index 00000000..7a833062 Binary files /dev/null and b/.image/我的流程-发起.jpg differ diff --git a/.image/我的流程-详情.jpg b/.image/我的流程-详情.jpg new file mode 100644 index 00000000..6a015418 Binary files /dev/null and b/.image/我的流程-详情.jpg differ diff --git a/.image/报表设计器-图形报表.jpg b/.image/报表设计器-图形报表.jpg new file mode 100644 index 00000000..681b3185 Binary files /dev/null and b/.image/报表设计器-图形报表.jpg differ diff --git a/.image/报表设计器-打印设计.jpg b/.image/报表设计器-打印设计.jpg new file mode 100644 index 00000000..bb86da64 Binary files /dev/null and b/.image/报表设计器-打印设计.jpg differ diff --git a/.image/报表设计器-数据报表.jpg b/.image/报表设计器-数据报表.jpg new file mode 100644 index 00000000..9ca5b9b6 Binary files /dev/null and b/.image/报表设计器-数据报表.jpg differ diff --git a/.image/操作日志.jpg b/.image/操作日志.jpg new file mode 100644 index 00000000..4a0611a3 Binary files /dev/null and b/.image/操作日志.jpg differ diff --git a/.image/支付订单.jpg b/.image/支付订单.jpg new file mode 100644 index 00000000..0a56dd74 Binary files /dev/null and b/.image/支付订单.jpg differ diff --git a/.image/敏感词.jpg b/.image/敏感词.jpg new file mode 100644 index 00000000..92a53974 Binary files /dev/null and b/.image/敏感词.jpg differ diff --git a/.image/数据库文档.jpg b/.image/数据库文档.jpg new file mode 100644 index 00000000..a4339d96 Binary files /dev/null and b/.image/数据库文档.jpg differ diff --git a/.image/文件管理.jpg b/.image/文件管理.jpg new file mode 100644 index 00000000..054b19f1 Binary files /dev/null and b/.image/文件管理.jpg differ diff --git a/.image/文件管理2.jpg b/.image/文件管理2.jpg new file mode 100644 index 00000000..b12e5c3c Binary files /dev/null and b/.image/文件管理2.jpg differ diff --git a/.image/文件配置.jpg b/.image/文件配置.jpg new file mode 100644 index 00000000..e618049a Binary files /dev/null and b/.image/文件配置.jpg differ diff --git a/.image/日志中心.jpg b/.image/日志中心.jpg new file mode 100644 index 00000000..27c1c6cb Binary files /dev/null and b/.image/日志中心.jpg differ diff --git a/.image/流程模型-列表.jpg b/.image/流程模型-列表.jpg new file mode 100644 index 00000000..ffdc5840 Binary files /dev/null and b/.image/流程模型-列表.jpg differ diff --git a/.image/流程模型-定义.jpg b/.image/流程模型-定义.jpg new file mode 100644 index 00000000..18b316c7 Binary files /dev/null and b/.image/流程模型-定义.jpg differ diff --git a/.image/流程模型-设计.jpg b/.image/流程模型-设计.jpg new file mode 100644 index 00000000..96149690 Binary files /dev/null and b/.image/流程模型-设计.jpg differ diff --git a/.image/流程表单.jpg b/.image/流程表单.jpg new file mode 100644 index 00000000..60669c14 Binary files /dev/null and b/.image/流程表单.jpg differ diff --git a/.image/生成效果.jpg b/.image/生成效果.jpg new file mode 100644 index 00000000..98ff2cca Binary files /dev/null and b/.image/生成效果.jpg differ diff --git a/.image/用户分组.jpg b/.image/用户分组.jpg new file mode 100644 index 00000000..39af1cd1 Binary files /dev/null and b/.image/用户分组.jpg differ diff --git a/.image/用户管理.jpg b/.image/用户管理.jpg new file mode 100644 index 00000000..844604a6 Binary files /dev/null and b/.image/用户管理.jpg differ diff --git a/.image/登录.jpg b/.image/登录.jpg new file mode 100644 index 00000000..b782b988 Binary files /dev/null and b/.image/登录.jpg differ diff --git a/.image/登录日志.jpg b/.image/登录日志.jpg new file mode 100644 index 00000000..25662d97 Binary files /dev/null and b/.image/登录日志.jpg differ diff --git a/.image/短信日志.jpg b/.image/短信日志.jpg new file mode 100644 index 00000000..ada8e56d Binary files /dev/null and b/.image/短信日志.jpg differ diff --git a/.image/短信模板.jpg b/.image/短信模板.jpg new file mode 100644 index 00000000..09381cc5 Binary files /dev/null and b/.image/短信模板.jpg differ diff --git a/.image/短信渠道.jpg b/.image/短信渠道.jpg new file mode 100644 index 00000000..df3a5c39 Binary files /dev/null and b/.image/短信渠道.jpg differ diff --git a/.image/租户套餐.png b/.image/租户套餐.png new file mode 100644 index 00000000..96631679 Binary files /dev/null and b/.image/租户套餐.png differ diff --git a/.image/租户管理.jpg b/.image/租户管理.jpg new file mode 100644 index 00000000..647416a9 Binary files /dev/null and b/.image/租户管理.jpg differ diff --git a/.image/系统接口.jpg b/.image/系统接口.jpg new file mode 100644 index 00000000..6d39d421 Binary files /dev/null and b/.image/系统接口.jpg differ diff --git a/.image/菜单管理.jpg b/.image/菜单管理.jpg new file mode 100644 index 00000000..ad3b7979 Binary files /dev/null and b/.image/菜单管理.jpg differ diff --git a/.image/表单构建.jpg b/.image/表单构建.jpg new file mode 100644 index 00000000..81f03746 Binary files /dev/null and b/.image/表单构建.jpg differ diff --git a/.image/角色管理.jpg b/.image/角色管理.jpg new file mode 100644 index 00000000..eed776e8 Binary files /dev/null and b/.image/角色管理.jpg differ diff --git a/.image/访问日志.jpg b/.image/访问日志.jpg new file mode 100644 index 00000000..ef301aad Binary files /dev/null and b/.image/访问日志.jpg differ diff --git a/.image/退款订单.jpg b/.image/退款订单.jpg new file mode 100644 index 00000000..2c6c6c9e Binary files /dev/null and b/.image/退款订单.jpg differ diff --git a/.image/通知公告.jpg b/.image/通知公告.jpg new file mode 100644 index 00000000..97bb42fe Binary files /dev/null and b/.image/通知公告.jpg differ diff --git a/.image/部门管理.jpg b/.image/部门管理.jpg new file mode 100644 index 00000000..6eab2330 Binary files /dev/null and b/.image/部门管理.jpg differ diff --git a/.image/配置管理.jpg b/.image/配置管理.jpg new file mode 100644 index 00000000..0abaec93 Binary files /dev/null and b/.image/配置管理.jpg differ diff --git a/.image/链路追踪.jpg b/.image/链路追踪.jpg new file mode 100644 index 00000000..12f7aa8e Binary files /dev/null and b/.image/链路追踪.jpg differ diff --git a/.image/错误日志.jpg b/.image/错误日志.jpg new file mode 100644 index 00000000..eb615ea3 Binary files /dev/null and b/.image/错误日志.jpg differ diff --git a/.image/错误码管理.jpg b/.image/错误码管理.jpg new file mode 100644 index 00000000..ea91dde1 Binary files /dev/null and b/.image/错误码管理.jpg differ diff --git a/.image/首页.jpg b/.image/首页.jpg new file mode 100644 index 00000000..10a7fde7 Binary files /dev/null and b/.image/首页.jpg differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..bd9da623 --- /dev/null +++ b/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/lombok.config b/lombok.config new file mode 100644 index 00000000..a8e8ce67 --- /dev/null +++ b/lombok.config @@ -0,0 +1,4 @@ +config.stopBubbling = true +lombok.tostring.callsuper=CALL +lombok.equalsandhashcode.callsuper=CALL +lombok.accessors.chain=true diff --git a/mes-dependencies/pom.xml b/mes-dependencies/pom.xml new file mode 100644 index 00000000..338be67c --- /dev/null +++ b/mes-dependencies/pom.xml @@ -0,0 +1,685 @@ + + + 4.0.0 + + com.chanko.yunxi + mes-dependencies + ${revision} + pom + + ${project.artifactId} + 基础 bom 文件,管理整个项目的依赖版本 + https://github.com/YunaiV/ruoyi-vue-pro + + + 2.0.0-jdk8-snapshot + 1.5.0 + + 2.7.18 + + 1.6.15 + 4.3.0 + 2.5 + + 1.2.20 + 3.5.4 + 3.5.4 + 3.6.1 + 1.4.7 + 3.18.0 + 8.1.3.62 + + 2.2.3 + + 2.2.5 + 1.7.1 + + 8.12.0 + 2.7.11 + 0.33.0 + + 7.2.11.RELEASE + 1.0.7 + 4.11.0 + + 6.8.0 + + 1.0.10 + 1.16.2 + 1.18.30 + 1.5.5.Final + 5.8.22 + 3.3.2 + 2.3 + 1.0.5 + 1.2.83 + 32.1.3-jre + 5.1.0 + 2.14.2 + 3.10.0 + 0.1.55 + 2.7.0 + 2.7.0 + + 3.5.0 + 4.11.0 + 2.11.0 + 8.5.6 + 4.6.4 + 2.2.1 + 3.1.880 + 1.0.8 + 1.6.6 + 2.12.2 + 4.5.7.B + 2.2.9 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + com.chanko.yunxi + mes-spring-boot-starter-banner + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-operatelog + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-dict + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-sms + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-pay + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-tenant + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-data-permission + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-social + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-error-code + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-ip + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-captcha + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-desensitize + ${revision} + + + + + + org.springframework.boot + spring-boot-configuration-processor + ${spring.boot.version} + + + + + com.chanko.yunxi + mes-spring-boot-starter-web + ${revision} + + + + com.chanko.yunxi + mes-spring-boot-starter-security + ${revision} + + + + com.chanko.yunxi + mes-spring-boot-starter-websocket + ${revision} + + + + com.github.xiaoymin + knife4j-openapi3-spring-boot-starter + ${knife4j.version} + + + org.springdoc + springdoc-openapi-ui + ${springdoc.version} + + + + + com.chanko.yunxi + mes-spring-boot-starter-mybatis + ${revision} + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus-generator.version} + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${dynamic-datasource.version} + + + com.github.yulichang + mybatis-plus-join-boot-starter + ${mybatis-plus-join.version} + + + + com.chanko.yunxi + mes-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} + + + + + com.chanko.yunxi + mes-spring-boot-starter-job + ${revision} + + + + + com.chanko.yunxi + mes-spring-boot-starter-mq + ${revision} + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + ${rocketmq-spring.version} + + + + + com.chanko.yunxi + mes-spring-boot-starter-protection + ${revision} + + + + com.baomidou + lock4j-redisson-spring-boot-starter + ${lock4j.version} + + + redisson-spring-boot-starter + org.redisson + + + + + + io.github.resilience4j + resilience4j-ratelimiter + ${resilience4j.version} + + + io.github.resilience4j + resilience4j-spring-boot2 + ${resilience4j.version} + + + + + com.chanko.yunxi + mes-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} + + + + 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 + + + + + + com.github.fppt + jedis-mock + ${jedis-mock.version} + + + + uk.co.jemos.podam + podam + ${podam.version} + + + + + com.chanko.yunxi + mes-spring-boot-starter-flowable + ${revision} + + + org.flowable + flowable-spring-boot-starter-process + ${flowable.version} + + + org.flowable + flowable-spring-boot-starter-actuator + ${flowable.version} + + + + + + com.chanko.yunxi + mes-common + ${revision} + + + + com.chanko.yunxi + mes-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.version} + + + + com.alibaba + easyexcel + ${easyexcel.verion} + + + commons-io + commons-io + ${commons-io.version} + + + org.apache.tika + tika-core + ${tika-core.version} + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + + com.alibaba + fastjson + ${fastjson.version} + + + + cn.smallbun.screw + screw-core + ${screw.version} + + + org.freemarker + freemarker + + + com.alibaba + fastjson + + + + + + com.google.guava + guava + ${guava.version} + + + + com.google.inject + guice + ${guice.version} + + + + com.alibaba + transmittable-thread-local + ${transmittable-thread-local.version} + + + + commons-net + commons-net + ${commons-net.version} + + + + com.jcraft + jsch + ${jsch.version} + + + + com.xingyuv + spring-boot-starter-captcha-plus + ${captcha-plus.version} + + + + org.lionsoul + ip2region + ${ip2region.version} + + + + org.jsoup + jsoup + ${jsoup.version} + + + + + com.squareup.okio + okio + ${okio.version} + + + com.squareup.okhttp3 + okhttp + ${okhttp3.version} + + + com.chanko.yunxi + mes-spring-boot-starter-file + ${revision} + + + io.minio + minio + ${minio.version} + + + + + com.aliyun + aliyun-java-sdk-core + ${aliyun-java-sdk-core.version} + + + opentracing-api + io.opentracing + + + opentracing-util + io.opentracing + + + + + com.aliyun + aliyun-java-sdk-dysmsapi + ${aliyun-java-sdk-dysmsapi.version} + + + com.tencentcloudapi + tencentcloud-sdk-java-sms + ${tencentcloud-sdk-java.version} + + + + + com.xingyuv + spring-boot-starter-justauth + ${justauth.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-boot-starter + ${jimureport.version} + + + com.alibaba + druid + + + + + xerces + xercesImpl + ${xercesImpl.version} + + + + + com.bstek.ureport + ureport2-console + ${ureport2.version} + + + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + ${flatten-maven-plugin.version} + + resolveCiFriendliesOnly + true + + + + + flatten + + flatten + process-resources + + + + clean + + flatten.clean + clean + + + + + + + diff --git a/mes-framework/mes-common/pom.xml b/mes-framework/mes-common/pom.xml new file mode 100644 index 00000000..fb1e30a9 --- /dev/null +++ b/mes-framework/mes-common/pom.xml @@ -0,0 +1,144 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-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-ui + 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 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/core/IntArrayValuable.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/core/IntArrayValuable.java new file mode 100644 index 00000000..75764dbb --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/core/IntArrayValuable.java @@ -0,0 +1,15 @@ +package com.chanko.yunxi.mes.heli.framework.common.core; + +/** + * 可生成 Int 数组的接口 + * + * @author 芋道源码 + */ +public interface IntArrayValuable { + + /** + * @return int 数组 + */ + int[] array(); + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/core/KeyValue.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/core/KeyValue.java new file mode 100644 index 00000000..b0dbd58c --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/core/KeyValue.java @@ -0,0 +1,22 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/CommonStatusEnum.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/CommonStatusEnum.java new file mode 100644 index 00000000..d5dd445f --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/CommonStatusEnum.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.framework.common.enums; + +import cn.hutool.core.util.ObjUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 通用状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum CommonStatusEnum implements IntArrayValuable { + + ENABLE(0, "开启"), + DISABLE(1, "关闭"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray(); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] 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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/DocumentEnum.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/DocumentEnum.java new file mode 100644 index 00000000..8b7d10c0 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/DocumentEnum.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/TerminalEnum.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/TerminalEnum.java new file mode 100644 index 00000000..1ce4947b --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/TerminalEnum.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.framework.common.enums; + +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 终端的枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum TerminalEnum implements IntArrayValuable { + + WECHAT_MINI_PROGRAM(10, "微信小程序"), + WECHAT_WAP(11, "微信公众号"), + H5(20, "H5 网页"), + APP(31, "手机 App"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray(); + + /** + * 终端 + */ + private final Integer terminal; + /** + * 终端名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/UserTypeEnum.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/UserTypeEnum.java new file mode 100644 index 00000000..e29d9201 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/UserTypeEnum.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.framework.common.enums; + +import cn.hutool.core.util.ArrayUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 全局用户类型枚举 + */ +@AllArgsConstructor +@Getter +public enum UserTypeEnum implements IntArrayValuable { + + MEMBER(1, "会员"), // 面向 c 端,普通用户 + ADMIN(2, "管理员"); // 面向 b 端,管理后台 + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray(); + + /** + * 类型 + */ + 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 int[] array() { + return ARRAYS; + } +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/WebFilterOrderEnum.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/WebFilterOrderEnum.java new file mode 100644 index 00000000..218b487b --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/enums/WebFilterOrderEnum.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/ErrorCode.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/ErrorCode.java new file mode 100644 index 00000000..e3422510 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/ErrorCode.java @@ -0,0 +1,32 @@ +package com.chanko.yunxi.mes.heli.framework.common.exception; + +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/ServerException.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/ServerException.java new file mode 100644 index 00000000..60130fee --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/ServerException.java @@ -0,0 +1,60 @@ +package com.chanko.yunxi.mes.heli.framework.common.exception; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/ServiceException.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/ServiceException.java new file mode 100644 index 00000000..46709d79 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/ServiceException.java @@ -0,0 +1,60 @@ +package com.chanko.yunxi.mes.heli.framework.common.exception; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/enums/GlobalErrorCodeConstants.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/enums/GlobalErrorCodeConstants.java new file mode 100644 index 00000000..588091bd --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/enums/GlobalErrorCodeConstants.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.framework.common.exception.enums; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/enums/ServiceErrorCodeRange.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/enums/ServiceErrorCodeRange.java new file mode 100644 index 00000000..1a65dfae --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/enums/ServiceErrorCodeRange.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.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) + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/util/ServiceExceptionUtil.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/util/ServiceExceptionUtil.java new file mode 100644 index 00000000..ea30faf8 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/exception/util/ServiceExceptionUtil.java @@ -0,0 +1,127 @@ +package com.chanko.yunxi.mes.heli.framework.common.exception.util; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ErrorCode; +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * {@link ServiceException} 工具类 + * + * 目的在于,格式化异常信息提示。 + * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 + * + * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式: + * + * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration + * 2. 异常提示信息,写在 .properties 等等配置文件 + * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新 + * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新 + */ +@Slf4j +public class ServiceExceptionUtil { + + /** + * 错误码提示模板 + */ + private static final ConcurrentMap MESSAGES = new ConcurrentHashMap<>(); + + public static void putAll(Map messages) { + ServiceExceptionUtil.MESSAGES.putAll(messages); + } + + public static void put(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.put(code, message); + } + + public static void delete(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.remove(code, message); + } + + // ========== 和 ServiceException 的集成 ========== + + public static ServiceException exception(ErrorCode errorCode) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern); + } + + public static ServiceException exception(ErrorCode errorCode, Object... params) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern, params); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @return 异常 + */ + public static ServiceException exception(Integer code) { + return exception0(code, MESSAGES.get(code)); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @param params 消息提示的占位符对应的参数 + * @return 异常 + */ + public static ServiceException exception(Integer code, Object... params) { + return exception0(code, MESSAGES.get(code), 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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/package-info.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/package-info.java new file mode 100644 index 00000000..939ba68f --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/package-info.java @@ -0,0 +1,6 @@ +/** + * 基础的通用类,和框架无关 + * + * 例如说,CommonResult 为通用返回 + */ +package com.chanko.yunxi.mes.heli.framework.common; diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/CommonResult.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/CommonResult.java new file mode 100644 index 00000000..93ec055e --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/CommonResult.java @@ -0,0 +1,112 @@ +package com.chanko.yunxi.mes.heli.framework.common.pojo; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ErrorCode; +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.springframework.util.Assert; + +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.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + 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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/PageParam.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/PageParam.java new file mode 100644 index 00000000..5dacf616 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/PageParam.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.framework.common.pojo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.Max; +import javax.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/PageResult.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/PageResult.java new file mode 100644 index 00000000..32ba37f3 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/PageResult.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/SortingField.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/SortingField.java new file mode 100644 index 00000000..f0d46f3c --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/pojo/SortingField.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/cache/CacheUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/cache/CacheUtils.java new file mode 100644 index 00000000..8a12983e --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/cache/CacheUtils.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.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 { + + public static LoadingCache buildAsyncReloadingCache(Duration duration, CacheLoader loader) { + return CacheBuilder.newBuilder() + // 只阻塞当前数据加载线程,其他线程返回旧值 + .refreshAfterWrite(duration) + // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 + .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置 + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/ArrayUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/ArrayUtils.java new file mode 100644 index 00000000..ed529dc3 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/ArrayUtils.java @@ -0,0 +1,58 @@ +package com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/CollectionUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/CollectionUtils.java new file mode 100644 index 00000000..4c429a8e --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/CollectionUtils.java @@ -0,0 +1,309 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +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 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 List convertListByFlatMap(Collection from, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().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).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, 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().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).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(List from, Predicate predicate) { + return findFirst(from, predicate, Function.identity()); + } + + public static U findFirst(List 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 > V getSumValue(List from, Function valueFunc, + BinaryOperator accumulator) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + return from.stream().map(valueFunc).reduce(accumulator).get(); + } + + public static void addIfNotNull(Collection coll, T item) { + if (item == null) { + return; + } + coll.add(item); + } + + public static Collection singleton(T deptId) { + return deptId == null ? Collections.emptyList() : Collections.singleton(deptId); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/MapUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/MapUtils.java new file mode 100644 index 00000000..b66d421e --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/MapUtils.java @@ -0,0 +1,66 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.chanko.yunxi.mes.heli.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,然后进一步处理 + * 注意,如果查找到的 value 为 null 时,不进行处理 + * + * @param map 哈希表 + * @param key key + * @param consumer 进一步处理的逻辑 + */ + public static void findAndThen(Map map, K key, Consumer consumer) { + if (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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/SetUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/SetUtils.java new file mode 100644 index 00000000..0ad8ad61 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/SetUtils.java @@ -0,0 +1,19 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/date/DateUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/date/DateUtils.java new file mode 100644 index 00000000..053e8f2f --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/date/DateUtils.java @@ -0,0 +1,180 @@ +package com.chanko.yunxi.mes.heli.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"; + + public static final String FORMAT_HOUR_MINUTE_SECOND = "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(Date time) { + return System.currentTimeMillis() > time.getTime(); + } + + public static boolean isExpired(LocalDateTime time) { + LocalDateTime now = LocalDateTime.now(); + return now.isAfter(time); + } + + public static long diff(Date endTime, Date startTime) { + return endTime.getTime() - startTime.getTime(); + } + + /** + * 创建指定时间 + * + * @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 field 日历字段.
eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,
Calendar.HOUR_OF_DAY等. + * @param amount 相差的数值 + * @return 计算后的日志 + */ + public static Date addDate(int field, int amount) { + return addDate(null, field, amount); + } + + /** + * 计算当期时间相差的日期 + * + * @param date 设置时间 + * @param field 日历字段 例如说,{@link Calendar#DAY_OF_MONTH} 等 + * @param amount 相差的数值 + * @return 计算后的日志 + */ + public static Date addDate(Date date, int field, int amount) { + if (amount == 0) { + return date; + } + Calendar c = Calendar.getInstance(); + if (date != null) { + c.setTime(date); + } + c.add(field, amount); + return c.getTime(); + } + + /** + * 是否今天 + * + * @param date 日期 + * @return 是否 + */ + public static boolean isToday(LocalDateTime date) { + return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now()); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/date/LocalDateTimeUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/date/LocalDateTimeUtils.java new file mode 100644 index 00000000..042068b8 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/date/LocalDateTimeUtils.java @@ -0,0 +1,124 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.date; + +import cn.hutool.core.date.LocalDateTimeUtil; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.TemporalAdjusters; + +/** + * 时间工具类,用于 {@link java.time.LocalDateTime} + * + * @author 芋道源码 + */ +public class LocalDateTimeUtils { + + /** + * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值 + */ + public static LocalDateTime EMPTY = buildTime(1970, 1, 1); + + 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 结束时间 + * @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); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/http/HttpUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/http/HttpUtils.java new file mode 100644 index 00000000..13ac7356 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/http/HttpUtils.java @@ -0,0 +1,126 @@ +package com.chanko.yunxi.mes.heli.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 org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Map; + +/** + * HTTP 工具类 + * + * @author 芋道源码 + */ +public class HttpUtils { + + @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; + } + + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/FileUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/FileUtils.java new file mode 100644 index 00000000..ec3a885c --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/FileUtils.java @@ -0,0 +1,84 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.io; + +import cn.hutool.core.io.FileTypeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import lombok.SneakyThrows; + +import java.io.ByteArrayInputStream; +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; + } + + /** + * 生成文件路径 + * + * @param content 文件内容 + * @param originalName 原始文件名 + * @return path,唯一不可重复 + */ + public static String generatePath(byte[] content, String originalName) { + String sha256Hex = DigestUtil.sha256Hex(content); + // 情况一:如果存在 name,则优先使用 name 的后缀 + if (StrUtil.isNotBlank(originalName)) { + String extName = FileNameUtil.extName(originalName); + return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName; + } + // 情况二:基于 content 计算 + return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content)); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/IoUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/IoUtils.java new file mode 100644 index 00000000..30d64dfc --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/IoUtils.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/json/JsonUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/json/JsonUtils.java new file mode 100644 index 00000000..7a2a0b44 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/json/JsonUtils.java @@ -0,0 +1,187 @@ +package com.chanko.yunxi.mes.heli.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); + } + } + + 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); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/monitor/TracerUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/monitor/TracerUtils.java new file mode 100644 index 00000000..abbfc082 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/monitor/TracerUtils.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/MoneyUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/MoneyUtils.java new file mode 100644 index 00000000..748ef823 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/MoneyUtils.java @@ -0,0 +1,73 @@ +package com.chanko.yunxi.mes.heli.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 { + + /** + * 计算百分比金额,四舍五入 + * + * @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 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(); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/NumberUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/NumberUtils.java new file mode 100644 index 00000000..23ba3739 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/NumberUtils.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.number; + +import cn.hutool.core.util.StrUtil; + +/** + * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 + * + * @author 芋道源码 + */ +public class NumberUtils { + + public static Long parseLong(String str) { + return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; + } + + /** + * 通过经纬度获取地球上两点之间的距离 + * + * 参考 <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; + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/BeanUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/BeanUtils.java new file mode 100644 index 00000000..cfe6f00b --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/BeanUtils.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.object; + +import cn.hutool.core.bean.BeanUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; + +import java.util.List; + +/** + * 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 List toBean(List source, Class targetType) { + if (source == null) { + return null; + } + return CollectionUtils.convertList(source, s -> toBean(s, targetType)); + } + + public static PageResult toBean(PageResult source, Class targetType) { + if (source == null) { + return null; + } + return new PageResult<>(toBean(source.getList(), targetType), source.getTotal()); + } + +} \ No newline at end of file diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/ObjectUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/ObjectUtils.java new file mode 100644 index 00000000..52b1d6b2 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/ObjectUtils.java @@ -0,0 +1,63 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/PageUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/PageUtils.java new file mode 100644 index 00000000..053316e3 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/PageUtils.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.object; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; + +/** + * {@link com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam} 工具类 + * + * @author 芋道源码 + */ +public class PageUtils { + + public static int getStart(PageParam pageParam) { + return (pageParam.getPageNo() - 1) * pageParam.getPageSize(); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/package-info.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/package-info.java new file mode 100644 index 00000000..27614eaf --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/package-info.java @@ -0,0 +1,7 @@ +/** + * 对于工具类的选择,优先查找 Hutool 中有没对应的方法 + * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分 + * + * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。 + */ +package com.chanko.yunxi.mes.heli.framework.common.util; diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/servlet/ServletUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/servlet/ServletUtils.java new file mode 100644 index 00000000..c18c140a --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/servlet/ServletUtils.java @@ -0,0 +1,113 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.servlet; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +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 javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +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); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + /** + * 返回附件 + * + * @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=" + URLEncoder.encode(filename, "UTF-8")); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + // 输出附件 + IoUtil.write(response.getOutputStream(), false, content); + } + + /** + * @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 ServletUtil.getClientIP(request); + } + + // TODO @疯狂:terminal 还是从 ServletUtils 里拿,更容易全局治理; + + public static boolean isJsonRequest(ServletRequest request) { + return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); + } + + public static String getBody(HttpServletRequest request) { + return ServletUtil.getBody(request); + } + + public static byte[] getBodyBytes(HttpServletRequest request) { + return ServletUtil.getBodyBytes(request); + } + + public static String getClientIP(HttpServletRequest request) { + return ServletUtil.getClientIP(request); + } + + public static Map getParamMap(HttpServletRequest request) { + return ServletUtil.getParamMap(request); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringAopUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringAopUtils.java new file mode 100644 index 00000000..d384c01f --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringAopUtils.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.spring; + +import cn.hutool.core.bean.BeanUtil; +import org.springframework.aop.framework.AdvisedSupport; +import org.springframework.aop.framework.AopProxy; +import org.springframework.aop.support.AopUtils; + +/** + * Spring AOP 工具类 + * + * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现 + */ +public class SpringAopUtils { + + /** + * 获取代理的目标对象 + * + * @param proxy 代理对象 + * @return 目标对象 + */ + public static Object getTarget(Object proxy) throws Exception { + // 不是代理对象 + if (!AopUtils.isAopProxy(proxy)) { + return proxy; + } + // Jdk 代理 + if (AopUtils.isJdkDynamicProxy(proxy)) { + return getJdkDynamicProxyTargetObject(proxy); + } + // Cglib 代理 + return getCglibProxyTargetObject(proxy); + } + + private static Object getCglibProxyTargetObject(Object proxy) throws Exception { + Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + + private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { + AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringExpressionUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringExpressionUtils.java new file mode 100644 index 00000000..aae79b3d --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringExpressionUtils.java @@ -0,0 +1,89 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.spring; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +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.EvaluationContext; +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; + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/string/StrUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/string/StrUtils.java new file mode 100644 index 00000000..47b895ed --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/string/StrUtils.java @@ -0,0 +1,69 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.string; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +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 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")); + } + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/validation/ValidationUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/validation/ValidationUtils.java new file mode 100644 index 00000000..08e200cc --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/validation/ValidationUtils.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.framework.common.util.validation; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import org.springframework.util.StringUtils; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnum.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnum.java new file mode 100644 index 00000000..f35da7df --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnum.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.framework.common.validation; + +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; + +import javax.validation.Constraint; +import javax.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 实现 EnumValuable 接口的 + */ + Class value(); + + String message() default "必须在指定范围 {value}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumCollectionValidator.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumCollectionValidator.java new file mode 100644 index 00000000..05f06ea3 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumCollectionValidator.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.framework.common.validation; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class InEnumCollectionValidator implements ConstraintValidator> { + + private List values; + + @Override + public void initialize(InEnum annotation) { + IntArrayValuable[] values = annotation.value().getEnumConstants(); + if (values.length == 0) { + this.values = Collections.emptyList(); + } else { + this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); + } + } + + @Override + public boolean isValid(Collection list, ConstraintValidatorContext context) { + // 校验通过 + if (CollUtil.containsAll(values, list)) { + return true; + } + // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值) + context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() + .replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句 + return false; + } + +} + diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumValidator.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumValidator.java new file mode 100644 index 00000000..804b3f02 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumValidator.java @@ -0,0 +1,44 @@ +package com.chanko.yunxi.mes.heli.framework.common.validation; + +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class InEnumValidator implements ConstraintValidator { + + private List values; + + @Override + public void initialize(InEnum annotation) { + IntArrayValuable[] values = annotation.value().getEnumConstants(); + if (values.length == 0) { + this.values = Collections.emptyList(); + } else { + this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); + } + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + // 为空时,默认不校验,即认为通过 + if (value == null) { + return true; + } + // 校验通过 + if (values.contains(value)) { + return true; + } + // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值) + context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() + .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句 + return false; + } + +} + diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Mobile.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Mobile.java new file mode 100644 index 00000000..ee864232 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Mobile.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.framework.common.validation; + +import javax.validation.Constraint; +import javax.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/MobileValidator.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/MobileValidator.java new file mode 100644 index 00000000..de65987d --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/MobileValidator.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.common.validation; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.validation.ValidationUtils; + +import javax.validation.ConstraintValidator; +import javax.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Telephone.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Telephone.java new file mode 100644 index 00000000..ba0b9969 --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Telephone.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.framework.common.validation; + +import javax.validation.Constraint; +import javax.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/TelephoneValidator.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/TelephoneValidator.java new file mode 100644 index 00000000..5e458c2d --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/TelephoneValidator.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.common.validation; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.PhoneUtil; + +import javax.validation.ConstraintValidator; +import javax.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/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/package-info.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/package-info.java new file mode 100644 index 00000000..74bbccbd --- /dev/null +++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Hibernate Validator 实现参数校验 + */ +package com.chanko.yunxi.mes.heli.framework.common.validation; diff --git a/mes-framework/mes-common/src/test/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/CollectionUtilsTest.java b/mes-framework/mes-common/src/test/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/CollectionUtilsTest.java new file mode 100644 index 00000000..9cc9117e --- /dev/null +++ b/mes-framework/mes-common/src/test/java/com/chanko/yunxi/mes/heli/framework/common/util/collection/CollectionUtilsTest.java @@ -0,0 +1,64 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-common/《芋道 Spring Boot 参数校验 Validation 入门》.md b/mes-framework/mes-common/《芋道 Spring Boot 参数校验 Validation 入门》.md new file mode 100644 index 00000000..114ddfad --- /dev/null +++ b/mes-framework/mes-common/《芋道 Spring Boot 参数校验 Validation 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-banner/pom.xml b/mes-framework/mes-spring-boot-starter-banner/pom.xml new file mode 100644 index 00000000..a39d6745 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-banner/pom.xml @@ -0,0 +1,30 @@ + + + + mes-framework + com.chanko.yunxi + ${revision} + + 4.0.0 + mes-spring-boot-starter-banner + jar + + ${project.artifactId} + Banner 用于在 console 控制台,打印开发文档、接口文档等 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + org.springframework.boot + spring-boot-starter + + + + diff --git a/mes-framework/mes-spring-boot-starter-banner/src/main/java/com/chanko/yunxi/mes/heli/framework/banner/config/MesBannerAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-banner/src/main/java/com/chanko/yunxi/mes/heli/framework/banner/config/MesBannerAutoConfiguration.java new file mode 100644 index 00000000..4fc2e536 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-banner/src/main/java/com/chanko/yunxi/mes/heli/framework/banner/config/MesBannerAutoConfiguration.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.framework.banner.config; + +import com.chanko.yunxi.mes.heli.framework.banner.core.BannerApplicationRunner; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * Banner 的自动配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class MesBannerAutoConfiguration { + + @Bean + public BannerApplicationRunner bannerApplicationRunner() { + return new BannerApplicationRunner(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-banner/src/main/java/com/chanko/yunxi/mes/heli/framework/banner/core/BannerApplicationRunner.java b/mes-framework/mes-spring-boot-starter-banner/src/main/java/com/chanko/yunxi/mes/heli/framework/banner/core/BannerApplicationRunner.java new file mode 100644 index 00000000..69448085 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-banner/src/main/java/com/chanko/yunxi/mes/heli/framework/banner/core/BannerApplicationRunner.java @@ -0,0 +1,60 @@ +package com.chanko.yunxi.mes.heli.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("com.chanko.yunxi.mes.heli.module.report.framework.security.config.SecurityConfiguration")) { + System.out.println("[报表模块 mes-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]"); + } + // 工作流 + if (isNotPresent("com.chanko.yunxi.mes.heli.framework.flowable.config.MesFlowableConfiguration")) { + System.out.println("[工作流模块 mes-module-bpm - 已禁用][参考 https://doc.iocoder.cn/bpm/ 开启]"); + } + // 微信公众号 + if (isNotPresent("com.chanko.yunxi.mes.heli.module.mp.framework.mp.config.MpConfiguration")) { + System.out.println("[微信公众号 mes-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + } + // 商城系统 + if (isNotPresent("com.chanko.yunxi.mes.heli.module.trade.framework.web.config.TradeWebConfiguration")) { + System.out.println("[商城系统 mes-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + } + // 支付平台 + if (isNotPresent("com.chanko.yunxi.mes.heli.module.pay.framework.pay.config.PayConfiguration")) { + System.out.println("[支付系统 mes-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + } + }); + } + + private static boolean isNotPresent(String className) { + return !ClassUtils.isPresent(className, ClassUtils.getDefaultClassLoader()); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-banner/src/main/java/com/chanko/yunxi/mes/heli/framework/banner/package-info.java b/mes-framework/mes-spring-boot-starter-banner/src/main/java/com/chanko/yunxi/mes/heli/framework/banner/package-info.java new file mode 100644 index 00000000..33acea8e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-banner/src/main/java/com/chanko/yunxi/mes/heli/framework/banner/package-info.java @@ -0,0 +1,6 @@ +/** + * Banner 用于在 console 控制台,打印开发文档、接口文档等 + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.framework.banner; diff --git a/mes-framework/mes-spring-boot-starter-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..b27d4cac --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.banner.config.MesBannerAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-banner/src/main/resources/banner.txt b/mes-framework/mes-spring-boot-starter-banner/src/main/resources/banner.txt new file mode 100644 index 00000000..bd231a86 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-banner/src/main/resources/banner.txt @@ -0,0 +1,17 @@ +芋道源码 http://www.iocoder.cn +Application Version: ${mes.info.version} +Spring Boot Version: ${spring-boot.version} + +.__ __. ______ .______ __ __ _______ +| \ | | / __ \ | _ \ | | | | / _____| +| \| | | | | | | |_) | | | | | | | __ +| . ` | | | | | | _ < | | | | | | |_ | +| |\ | | `--' | | |_) | | `--' | | |__| | +|__| \__| \______/ |______/ \______/ \______| + +███╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ +████╗ ██║██╔═══██╗ ██╔══██╗██║ ██║██╔════╝ +██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║██║ ███╗ +██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║██║ ██║ +██║ ╚████║╚██████╔╝ ██████╔╝╚██████╔╝╚██████╔╝ +╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ diff --git a/mes-framework/mes-spring-boot-starter-biz-data-permission/pom.xml b/mes-framework/mes-spring-boot-starter-biz-data-permission/pom.xml new file mode 100644 index 00000000..27d68a1b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/pom.xml @@ -0,0 +1,46 @@ + + + + mes-framework + com.chanko.yunxi + ${revision} + + 4.0.0 + mes-spring-boot-starter-biz-data-permission + jar + + ${project.artifactId} + 数据权限 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + com.chanko.yunxi + mes-spring-boot-starter-security + true + + + + + com.chanko.yunxi + mes-spring-boot-starter-mybatis + + + + + com.chanko.yunxi + mes-module-system-api + ${revision} + + + + + diff --git a/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/config/MesDataPermissionAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/config/MesDataPermissionAutoConfiguration.java new file mode 100644 index 00000000..99857daa --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/config/MesDataPermissionAutoConfiguration.java @@ -0,0 +1,44 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.config; + +import com.chanko.yunxi.mes.heli.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.db.DataPermissionDatabaseInterceptor; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.DataPermissionRule; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.DataPermissionRuleFactory; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +/** + * 数据权限的自动配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class MesDataPermissionAutoConfiguration { + + @Bean + public DataPermissionRuleFactory dataPermissionRuleFactory(List rules) { + return new DataPermissionRuleFactoryImpl(rules); + } + + @Bean + public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor, + DataPermissionRuleFactory ruleFactory) { + // 创建 DataPermissionDatabaseInterceptor 拦截器 + DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory); + // 添加到 interceptor 中 + // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 + MyBatisUtils.addInterceptor(interceptor, inner, 0); + return inner; + } + + @Bean + public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { + return new DataPermissionAnnotationAdvisor(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/config/MesDeptDataPermissionAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/config/MesDeptDataPermissionAutoConfiguration.java new file mode 100644 index 00000000..63492350 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/config/MesDeptDataPermissionAutoConfiguration.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.config; + +import com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.dept.DeptDataPermissionRule; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; +import com.chanko.yunxi.mes.heli.framework.security.core.LoginUser; +import com.chanko.yunxi.mes.heli.module.system.api.permission.PermissionApi; +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 = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class}) +public class MesDeptDataPermissionAutoConfiguration { + + @Bean + public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi, + List customizers) { + // 创建 DeptDataPermissionRule 对象 + DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); + // 补全表配置 + customizers.forEach(customizer -> customizer.customize(rule)); + return rule; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/annotation/DataPermission.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/annotation/DataPermission.java new file mode 100644 index 00000000..1f5dbb0e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/annotation/DataPermission.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.core.annotation; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java new file mode 100644 index 00000000..1a2bb0b5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.core.aop; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java new file mode 100644 index 00000000..60658eeb --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.core.aop; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/aop/DataPermissionContextHolder.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/aop/DataPermissionContextHolder.java new file mode 100644 index 00000000..ede98c1b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/aop/DataPermissionContextHolder.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.core.aop; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java new file mode 100644 index 00000000..f59dbbb2 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java @@ -0,0 +1,641 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.core.db; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.SetUtils; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.DataPermissionRule; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.DataPermissionRuleFactory; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.update.Update; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.Connection; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现 + * 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, List)} 方法 + * + * 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。 + * 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更! + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor { + + private final DataPermissionRuleFactory ruleFactory; + + @Getter + private final MappedStatementCache mappedStatementCache = new MappedStatementCache(); + + @Override // SELECT 场景 + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // 获得 Mapper 对应的数据权限的规则 + List rules = ruleFactory.getDataPermissionRule(ms.getId()); + if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 + return; + } + + PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); + try { + // 初始化上下文 + ContextHolder.init(rules); + // 处理 SQL + mpBs.sql(parserSingle(mpBs.sql(), null)); + } finally { + // 添加是否需要重写的缓存 + addMappedStatementCache(ms); + // 清空上下文 + ContextHolder.clear(); + } + } + + @Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限) + public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { + PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); + MappedStatement ms = mpSh.mappedStatement(); + SqlCommandType sct = ms.getSqlCommandType(); + if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { + // 获得 Mapper 对应的数据权限的规则 + List rules = ruleFactory.getDataPermissionRule(ms.getId()); + if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 + return; + } + + PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); + try { + // 初始化上下文 + ContextHolder.init(rules); + // 处理 SQL + mpBs.sql(parserMulti(mpBs.sql(), null)); + } finally { + // 添加是否需要重写的缓存 + addMappedStatementCache(ms); + // 清空上下文 + ContextHolder.clear(); + } + } + } + + @Override + protected void processSelect(Select select, int index, String sql, Object obj) { + processSelectBody(select.getSelectBody()); + List withItemsList = select.getWithItemsList(); + if (!CollectionUtils.isEmpty(withItemsList)) { + withItemsList.forEach(this::processSelectBody); + } + } + + /** + * update 语句处理 + */ + @Override + protected void processUpdate(Update update, int index, String sql, Object obj) { + final Table table = update.getTable(); + update.setWhere(this.builderExpression(update.getWhere(), table)); + } + + /** + * delete 语句处理 + */ + @Override + protected void processDelete(Delete delete, int index, String sql, Object obj) { + delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable())); + } + + // ========== 和 TenantLineInnerInterceptor 一致的逻辑 ========== + + protected void processSelectBody(SelectBody selectBody) { + if (selectBody == null) { + return; + } + if (selectBody instanceof PlainSelect) { + processPlainSelect((PlainSelect) selectBody); + } else if (selectBody instanceof WithItem) { + WithItem withItem = (WithItem) selectBody; + processSelectBody(withItem.getSubSelect().getSelectBody()); + } else { + SetOperationList operationList = (SetOperationList) selectBody; + List selectBodyList = operationList.getSelects(); + if (CollectionUtils.isNotEmpty(selectBodyList)) { + selectBodyList.forEach(this::processSelectBody); + } + } + } + + /** + * 处理 PlainSelect + */ + protected void processPlainSelect(PlainSelect plainSelect) { + //#3087 github + List selectItems = plainSelect.getSelectItems(); + if (CollectionUtils.isNotEmpty(selectItems)) { + selectItems.forEach(this::processSelectItem); + } + + // 处理 where 中的子查询 + Expression where = plainSelect.getWhere(); + processWhereSubSelect(where); + + // 处理 fromItem + FromItem fromItem = plainSelect.getFromItem(); + List list = processFromItem(fromItem); + List
mainTables = new ArrayList<>(list); + + // 处理 join + List joins = plainSelect.getJoins(); + if (CollectionUtils.isNotEmpty(joins)) { + mainTables = processJoins(mainTables, joins); + } + + // 当有 mainTable 时,进行 where 条件追加 + if (CollectionUtils.isNotEmpty(mainTables)) { + plainSelect.setWhere(builderExpression(where, mainTables)); + } + } + + private List
processFromItem(FromItem fromItem) { + // 处理括号括起来的表达式 + while (fromItem instanceof ParenthesisFromItem) { + fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); + } + + List
mainTables = new ArrayList<>(); + // 无 join 时的处理逻辑 + if (fromItem instanceof Table) { + Table fromTable = (Table) fromItem; + mainTables.add(fromTable); + } else if (fromItem instanceof SubJoin) { + // SubJoin 类型则还需要添加上 where 条件 + List
tables = processSubJoin((SubJoin) fromItem); + mainTables.addAll(tables); + } else { + // 处理下 fromItem + processOtherFromItem(fromItem); + } + return mainTables; + } + + /** + * 处理where条件内的子查询 + *

+ * 支持如下: + * 1. in + * 2. = + * 3. > + * 4. < + * 5. >= + * 6. <= + * 7. <> + * 8. EXISTS + * 9. NOT EXISTS + *

+ * 前提条件: + * 1. 子查询必须放在小括号中 + * 2. 子查询一般放在比较操作符的右边 + * + * @param where where 条件 + */ + protected void processWhereSubSelect(Expression where) { + if (where == null) { + return; + } + if (where instanceof FromItem) { + processOtherFromItem((FromItem) where); + return; + } + if (where.toString().indexOf("SELECT") > 0) { + // 有子查询 + if (where instanceof BinaryExpression) { + // 比较符号 , and , or , 等等 + BinaryExpression expression = (BinaryExpression) where; + processWhereSubSelect(expression.getLeftExpression()); + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof InExpression) { + // in + InExpression expression = (InExpression) where; + Expression inExpression = expression.getRightExpression(); + if (inExpression instanceof SubSelect) { + processSelectBody(((SubSelect) inExpression).getSelectBody()); + } + } else if (where instanceof ExistsExpression) { + // exists + ExistsExpression expression = (ExistsExpression) where; + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof NotExpression) { + // not exists + NotExpression expression = (NotExpression) where; + processWhereSubSelect(expression.getExpression()); + } else if (where instanceof Parenthesis) { + Parenthesis expression = (Parenthesis) where; + processWhereSubSelect(expression.getExpression()); + } + } + } + + protected void processSelectItem(SelectItem selectItem) { + if (selectItem instanceof SelectExpressionItem) { + SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; + if (selectExpressionItem.getExpression() instanceof SubSelect) { + processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody()); + } else if (selectExpressionItem.getExpression() instanceof Function) { + processFunction((Function) selectExpressionItem.getExpression()); + } + } + } + + /** + * 处理函数 + *

支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)

+ *

fixed gitee pulls/141

+ * + * @param function + */ + protected void processFunction(Function function) { + ExpressionList parameters = function.getParameters(); + if (parameters != null) { + parameters.getExpressions().forEach(expression -> { + if (expression instanceof SubSelect) { + processSelectBody(((SubSelect) expression).getSelectBody()); + } else if (expression instanceof Function) { + processFunction((Function) expression); + } + }); + } + } + + /** + * 处理子查询等 + */ + protected void processOtherFromItem(FromItem fromItem) { + // 去除括号 + while (fromItem instanceof ParenthesisFromItem) { + fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); + } + + if (fromItem instanceof SubSelect) { + SubSelect subSelect = (SubSelect) fromItem; + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } else if (fromItem instanceof ValuesList) { + logger.debug("Perform a subQuery, if you do not give us feedback"); + } else if (fromItem instanceof LateralSubSelect) { + LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem; + if (lateralSubSelect.getSubSelect() != null) { + SubSelect subSelect = lateralSubSelect.getSubSelect(); + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } + } + } + + /** + * 处理 sub join + * + * @param subJoin subJoin + * @return Table subJoin 中的主表 + */ + private List
processSubJoin(SubJoin subJoin) { + List
mainTables = new ArrayList<>(); + if (subJoin.getJoinList() != null) { + List
list = processFromItem(subJoin.getLeft()); + mainTables.addAll(list); + mainTables = processJoins(mainTables, subJoin.getJoinList()); + } + return mainTables; + } + + /** + * 处理 joins + * + * @param mainTables 可以为 null + * @param joins join 集合 + * @return List
右连接查询的 Table 列表 + */ + private List
processJoins(List
mainTables, List joins) { + // join 表达式中最终的主表 + Table mainTable = null; + // 当前 join 的左表 + Table leftTable = null; + + if (mainTables == null) { + mainTables = new ArrayList<>(); + } else if (mainTables.size() == 1) { + mainTable = mainTables.get(0); + leftTable = mainTable; + } + + //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名 + Deque> onTableDeque = new LinkedList<>(); + for (Join join : joins) { + // 处理 on 表达式 + FromItem joinItem = join.getRightItem(); + + // 获取当前 join 的表,subJoint 可以看作是一张表 + List
joinTables = null; + if (joinItem instanceof Table) { + joinTables = new ArrayList<>(); + joinTables.add((Table) joinItem); + } else if (joinItem instanceof SubJoin) { + joinTables = processSubJoin((SubJoin) joinItem); + } + + if (joinTables != null) { + + // 如果是隐式内连接 + if (join.isSimple()) { + mainTables.addAll(joinTables); + continue; + } + + // 当前表是否忽略 + Table joinTable = joinTables.get(0); + + List
onTables = null; + // 如果不要忽略,且是右连接,则记录下当前表 + if (join.isRight()) { + mainTable = joinTable; + if (leftTable != null) { + onTables = Collections.singletonList(leftTable); + } + } else if (join.isLeft()) { + onTables = Collections.singletonList(joinTable); + } else if (join.isInner()) { + if (mainTable == null) { + onTables = Collections.singletonList(joinTable); + } else { + onTables = Arrays.asList(mainTable, joinTable); + } + mainTable = null; + } + + mainTables = new ArrayList<>(); + if (mainTable != null) { + mainTables.add(mainTable); + } + + // 获取 join 尾缀的 on 表达式列表 + Collection originOnExpressions = join.getOnExpressions(); + // 正常 join on 表达式只有一个,立刻处理 + if (originOnExpressions.size() == 1 && onTables != null) { + List onExpressions = new LinkedList<>(); + onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables)); + join.setOnExpressions(onExpressions); + leftTable = joinTable; + continue; + } + // 表名压栈,忽略的表压入 null,以便后续不处理 + onTableDeque.push(onTables); + // 尾缀多个 on 表达式的时候统一处理 + if (originOnExpressions.size() > 1) { + Collection onExpressions = new LinkedList<>(); + for (Expression originOnExpression : originOnExpressions) { + List
currentTableList = onTableDeque.poll(); + if (CollectionUtils.isEmpty(currentTableList)) { + onExpressions.add(originOnExpression); + } else { + onExpressions.add(builderExpression(originOnExpression, currentTableList)); + } + } + join.setOnExpressions(onExpressions); + } + leftTable = joinTable; + } else { + processOtherFromItem(joinItem); + leftTable = null; + } + } + + return mainTables; + } + + // ========== 和 TenantLineInnerInterceptor 存在差异的逻辑:关键,实现权限条件的拼接 ========== + + /** + * 处理条件 + * + * @param currentExpression 当前 where 条件 + * @param table 单个表 + */ + protected Expression builderExpression(Expression currentExpression, Table table) { + return this.builderExpression(currentExpression, Collections.singletonList(table)); + } + + /** + * 处理条件 + * + * @param currentExpression 当前 where 条件 + * @param tables 多个表 + */ + protected Expression builderExpression(Expression currentExpression, List
tables) { + // 没有表需要处理直接返回 + if (CollectionUtils.isEmpty(tables)) { + return currentExpression; + } + + // 第一步,获得 Table 对应的数据权限条件 + Expression dataPermissionExpression = null; + for (Table table : tables) { + // 构建每个表的权限 Expression 条件 + Expression expression = buildDataPermissionExpression(table); + if (expression == null) { + continue; + } + // 合并到 dataPermissionExpression 中 + dataPermissionExpression = dataPermissionExpression == null ? expression + : new AndExpression(dataPermissionExpression, expression); + } + + // 第二步,合并多个 Expression 条件 + if (dataPermissionExpression == null) { + return currentExpression; + } + if (currentExpression == null) { + return dataPermissionExpression; + } + // ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression + if (currentExpression instanceof OrExpression) { + return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression); + } + // ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression + return new AndExpression(currentExpression, dataPermissionExpression); + } + + /** + * 构建指定表的数据权限的 Expression 过滤条件 + * + * @param table 表 + * @return Expression 过滤条件 + */ + private Expression buildDataPermissionExpression(Table table) { + // 生成条件 + Expression allExpression = null; + for (DataPermissionRule rule : ContextHolder.getRules()) { + // 判断表名是否匹配 + if (!rule.getTableNames().contains(table.getName())) { + continue; + } + // 如果有匹配的规则,说明可重写。 + // 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。 + // 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。 + ContextHolder.setRewrite(true); + + // 单条规则的条件 + String tableName = MyBatisUtils.getTableName(table); + Expression oneExpress = rule.getExpression(tableName, table.getAlias()); + if (oneExpress == null){ + continue; + } + // 拼接到 allExpression 中 + allExpression = allExpression == null ? oneExpress + : new AndExpression(allExpression, oneExpress); + } + + return allExpression; + } + + /** + * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中 + * + * @param ms MappedStatement + */ + private void addMappedStatementCache(MappedStatement ms) { + if (ContextHolder.getRewrite()) { + return; + } + // 无重写,进行添加 + mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules()); + } + + /** + * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则 + * + * @author 芋道源码 + */ + static final class ContextHolder { + + /** + * 该 {@link MappedStatement} 对应的规则 + */ + private static final ThreadLocal> RULES = ThreadLocal.withInitial(Collections::emptyList); + /** + * SQL 是否进行重写 + */ + private static final ThreadLocal REWRITE = ThreadLocal.withInitial(() -> Boolean.FALSE); + + public static void init(List rules) { + RULES.set(rules); + REWRITE.set(false); + } + + public static void clear() { + RULES.remove(); + REWRITE.remove(); + } + + public static boolean getRewrite() { + return REWRITE.get(); + } + + public static void setRewrite(boolean rewrite) { + REWRITE.set(rewrite); + } + + public static List getRules() { + return RULES.get(); + } + + } + + /** + * {@link MappedStatement} 缓存 + * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效 + * 如果无效,则可以避免 SQL 的解析,加快速度 + * + * @author 芋道源码 + */ + static final class MappedStatementCache { + + /** + * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存 + * + * value:{@link MappedStatement#getId()} 编号 + */ + @Getter + private final Map, Set> noRewritableMappedStatements = new ConcurrentHashMap<>(); + + /** + * 判断是否无需重写 + * ps:虽然有点中文式英语,但是容易读懂即可 + * + * @param ms MappedStatement + * @param rules 数据权限规则数组 + * @return 是否无需重写 + */ + public boolean noRewritable(MappedStatement ms, List rules) { + // 如果规则为空,说明无需重写 + if (CollUtil.isEmpty(rules)) { + return true; + } + // 任一规则不在 noRewritableMap 中,则说明可能需要重写 + for (DataPermissionRule rule : rules) { + Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); + if (!CollUtil.contains(mappedStatementIds, ms.getId())) { + return false; + } + } + return true; + } + + /** + * 添加无需重写的 MappedStatement + * + * @param ms MappedStatement + * @param rules 数据权限规则数组 + */ + public void addNoRewritable(MappedStatement ms, List rules) { + for (DataPermissionRule rule : rules) { + Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); + if (CollUtil.isNotEmpty(mappedStatementIds)) { + mappedStatementIds.add(ms.getId()); + } else { + noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId())); + } + } + } + + /** + * 清空缓存 + * 目前主要提供给单元测试 + */ + public void clear() { + noRewritableMappedStatements.clear(); + } + + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/DataPermissionRule.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/DataPermissionRule.java new file mode 100644 index 00000000..d4af376a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/DataPermissionRule.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/DataPermissionRuleFactory.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/DataPermissionRuleFactory.java new file mode 100644 index 00000000..e02bfdb8 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/DataPermissionRuleFactory.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java new file mode 100644 index 00000000..3e9713c7 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java @@ -0,0 +1,62 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.core.rule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.annotation.DataPermission; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java new file mode 100644 index 00000000..266175df --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java @@ -0,0 +1,205 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.DataPermissionRule; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.util.MyBatisUtils; +import com.chanko.yunxi.mes.heli.framework.security.core.LoginUser; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; +import com.chanko.yunxi.mes.heli.module.system.api.permission.PermissionApi; +import com.chanko.yunxi.mes.heli.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.*; +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 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 不进行修改,则会导致用户看不到之前的数据。【mes-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 PermissionApi 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 Parenthesis(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), + 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/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java new file mode 100644 index 00000000..ed66e23e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/dept/package-info.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/dept/package-info.java new file mode 100644 index 00000000..87e78abf --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/rule/dept/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于部门的数据权限规则 + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.dept; diff --git a/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/util/DataPermissionUtils.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/util/DataPermissionUtils.java new file mode 100644 index 00000000..8e5445a2 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/core/util/DataPermissionUtils.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.framework.datapermission.core.util; + +import com.chanko.yunxi.mes.heli.framework.datapermission.core.annotation.DataPermission; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.aop.DataPermissionContextHolder; +import lombok.SneakyThrows; + +/** + * 数据权限 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) { + DataPermission dataPermission = getDisableDataPermissionDisable(); + DataPermissionContextHolder.add(dataPermission); + try { + // 执行 runnable + runnable.run(); + } finally { + DataPermissionContextHolder.remove(); + } + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/package-info.java b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/package-info.java new file mode 100644 index 00000000..b569d7df --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/java/com/chanko/yunxi/mes/heli/framework/datapermission/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 + */ +package com.chanko.yunxi.mes.heli.framework.datapermission; diff --git a/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..93028694 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.chanko.yunxi.mes.heli.framework.datapermission.config.MesDataPermissionAutoConfiguration +com.chanko.yunxi.mes.heli.framework.datapermission.config.MesDeptDataPermissionAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-biz-dict/pom.xml b/mes-framework/mes-spring-boot-starter-biz-dict/pom.xml new file mode 100644 index 00000000..019d25f7 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-dict/pom.xml @@ -0,0 +1,44 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-biz-dict + jar + + ${project.artifactId} + 字典类型、数据 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter + + + + + com.chanko.yunxi + mes-module-system-api + ${revision} + + + + + com.google.guava + guava + + + + diff --git a/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/config/MesDictAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/config/MesDictAutoConfiguration.java new file mode 100644 index 00000000..fd1b4e95 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/config/MesDictAutoConfiguration.java @@ -0,0 +1,18 @@ +package com.chanko.yunxi.mes.heli.framework.dict.config; + +import com.chanko.yunxi.mes.heli.framework.dict.core.util.DictFrameworkUtils; +import com.chanko.yunxi.mes.heli.module.system.api.dict.DictDataApi; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class MesDictAutoConfiguration { + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) { + DictFrameworkUtils.init(dictDataApi); + return new DictFrameworkUtils(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/core/package-info.java b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/core/package-info.java new file mode 100644 index 00000000..e869d72d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.chanko.yunxi.mes.heli.framework.dict.core; diff --git a/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/core/util/DictFrameworkUtils.java b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/core/util/DictFrameworkUtils.java new file mode 100644 index 00000000..a0267c5e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/core/util/DictFrameworkUtils.java @@ -0,0 +1,75 @@ +package com.chanko.yunxi.mes.heli.framework.dict.core.util; + +import cn.hutool.core.util.ObjectUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import com.chanko.yunxi.mes.heli.framework.common.util.cache.CacheUtils; +import com.chanko.yunxi.mes.heli.module.system.api.dict.DictDataApi; +import com.chanko.yunxi.mes.heli.module.system.api.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; + +/** + * 字典工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class DictFrameworkUtils { + + private static DictDataApi dictDataApi; + + private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO(); + + /** + * 针对 {@link #getDictDataLabel(String, String)} 的缓存 + */ + private static final LoadingCache, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( + Duration.ofMinutes(1L), // 过期时间 1 分钟 + new CacheLoader, DictDataRespDTO>() { + + @Override + public DictDataRespDTO load(KeyValue key) { + return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()), DICT_DATA_NULL); + } + + }); + + /** + * 针对 {@link #parseDictDataValue(String, String)} 的缓存 + */ + private static final LoadingCache, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( + Duration.ofMinutes(1L), // 过期时间 1 分钟 + new CacheLoader, DictDataRespDTO>() { + + @Override + public DictDataRespDTO load(KeyValue key) { + return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()), DICT_DATA_NULL); + } + + }); + + public static void init(DictDataApi dictDataApi) { + DictFrameworkUtils.dictDataApi = dictDataApi; + log.info("[init][初始化 DictFrameworkUtils 成功]"); + } + + @SneakyThrows + public static String getDictDataLabel(String dictType, Integer value) { + return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, String.valueOf(value))).getLabel(); + } + + @SneakyThrows + public static String getDictDataLabel(String dictType, String value) { + return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel(); + } + + @SneakyThrows + public static String parseDictDataValue(String dictType, String label) { + return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/package-info.java b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/package-info.java new file mode 100644 index 00000000..83aea5f7 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/java/com/chanko/yunxi/mes/heli/framework/dict/package-info.java @@ -0,0 +1,6 @@ +/** + * 字典数据模块,提供 {@link com.chanko.yunxi.mes.heli.framework.dict.core.util.DictFrameworkUtils} 工具类 + * + * 通过将字典缓存在内存中,保证性能 + */ +package com.chanko.yunxi.mes.heli.framework.dict; diff --git a/mes-framework/mes-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..778a34c0 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.dict.config.MesDictAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-biz-error-code/pom.xml b/mes-framework/mes-spring-boot-starter-biz-error-code/pom.xml new file mode 100644 index 00000000..ab82133f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-error-code/pom.xml @@ -0,0 +1,49 @@ + + + + mes-framework + com.chanko.yunxi + ${revision} + + 4.0.0 + mes-spring-boot-starter-biz-error-code + jar + + ${project.artifactId} + + 错误码 ErrorCode 的自动配置功能,提供如下功能: + 1. 远程读取:项目启动时,从 system-server 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提示可配置; + 2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-server 服务加载最新的 ErrorCode 错误码; + 3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑; + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter + + + + + com.chanko.yunxi + mes-module-system-api + ${revision} + + + + jakarta.validation + jakarta.validation-api + provided + + + + diff --git a/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/config/ErrorCodeProperties.java b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/config/ErrorCodeProperties.java new file mode 100644 index 00000000..a3939ff3 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/config/ErrorCodeProperties.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.framework.errorcode.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 错误码的配置属性类 + * + * @author dlyan + */ +@ConfigurationProperties("mes.error-code") +@Data +@Validated +public class ErrorCodeProperties { + + /** + * 是否开启 + */ + private Boolean enable = true; + /** + * 错误码枚举类 + */ + @NotNull(message = "错误码枚举类不能为空") + private List constantsClassList; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/config/MesErrorCodeAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/config/MesErrorCodeAutoConfiguration.java new file mode 100644 index 00000000..abe13323 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/config/MesErrorCodeAutoConfiguration.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.framework.errorcode.config; + +import com.chanko.yunxi.mes.heli.framework.errorcode.core.generator.ErrorCodeAutoGenerator; +import com.chanko.yunxi.mes.heli.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl; +import com.chanko.yunxi.mes.heli.framework.errorcode.core.loader.ErrorCodeLoader; +import com.chanko.yunxi.mes.heli.framework.errorcode.core.loader.ErrorCodeLoaderImpl; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.ErrorCodeApi; +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.scheduling.annotation.EnableScheduling; + +/** + * 错误码配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@ConditionalOnProperty(prefix = "mes.error-code", value = "enable", matchIfMissing = true) // 允许使用 mes.error-code.enable=false 禁用访问日志 +@EnableConfigurationProperties(ErrorCodeProperties.class) +@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码 +public class MesErrorCodeAutoConfiguration { + + @Bean + public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName, + ErrorCodeProperties errorCodeProperties, + ErrorCodeApi errorCodeApi) { + return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(), errorCodeApi); + } + + @Bean + public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName, + ErrorCodeApi errorCodeApi) { + return new ErrorCodeLoaderImpl(applicationName, errorCodeApi); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java new file mode 100644 index 00000000..283fa226 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java @@ -0,0 +1,15 @@ +package com.chanko.yunxi.mes.heli.framework.errorcode.core.generator; + +/** + * 错误码的自动生成器 + * + * @author dylan + */ +public interface ErrorCodeAutoGenerator { + + /** + * 将配置类到错误码写入数据库 + */ + void execute(); + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java new file mode 100644 index 00000000..1eb274c0 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java @@ -0,0 +1,108 @@ +package com.chanko.yunxi.mes.heli.framework.errorcode.core.generator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import com.chanko.yunxi.mes.heli.framework.common.exception.ErrorCode; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.ErrorCodeApi; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * ErrorCodeAutoGenerator 的实现类 + * 目的是,扫描指定的 {@link #constantsClassList} 类,写入到 system 服务中 + * + * @author dylan + */ +@RequiredArgsConstructor +@Slf4j +public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator { + + /** + * 应用分组 + */ + private final String applicationName; + /** + * 错误码枚举类 + */ + private final List constantsClassList; + /** + * 错误码 Api + */ + private final ErrorCodeApi errorCodeApi; + + @Override + @EventListener(ApplicationReadyEvent.class) + @Async // 异步,保证项目的启动过程,毕竟非关键流程 + public void execute() { + // 第一步,解析错误码 + List autoGenerateDTOs = parseErrorCode(); + log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size()); + + // 第二步,写入到 system 服务 + try { + errorCodeApi.autoGenerateErrorCodeList(autoGenerateDTOs); + log.info("[execute][写入到 system 组件完成]"); + } catch (Exception ex) { + log.error("[execute][写入到 system 组件失败({})]", ExceptionUtil.getRootCauseMessage(ex)); + } + } + + /** + * 解析 constantsClassList 变量,转换成错误码数组 + * + * @return 错误码数组 + */ + private List parseErrorCode() { + // 校验 errorCodeConstantsClass 参数 + if (CollUtil.isEmpty(constantsClassList)) { + log.info("[execute][未配置 mes.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]"); + return new ArrayList<>(); + } + + // 解析错误码 + List autoGenerateDTOs = new ArrayList<>(); + constantsClassList.forEach(constantsClass -> { + try { + // 解析错误码枚举类 + Class errorCodeConstantsClazz = ClassUtil.loadClass(constantsClass); + // 解析错误码 + autoGenerateDTOs.addAll(parseErrorCode(errorCodeConstantsClazz)); + } catch (Exception ex) { + log.warn("[parseErrorCode][constantsClass({}) 加载失败({})]", constantsClass, + ExceptionUtil.getRootCauseMessage(ex)); + } + }); + return autoGenerateDTOs; + } + + /** + * 解析错误码类,获得错误码数组 + * + * @return 错误码数组 + */ + private List parseErrorCode(Class constantsClass) { + List autoGenerateDTOs = new ArrayList<>(); + Arrays.stream(constantsClass.getFields()).forEach(field -> { + if (field.getType() != ErrorCode.class) { + return; + } + // 转换成 ErrorCodeAutoGenerateReqDTO 对象 + ErrorCode errorCode = (ErrorCode) ReflectUtil.getFieldValue(constantsClass, field); + autoGenerateDTOs.add(new ErrorCodeAutoGenerateReqDTO().setApplicationName(applicationName) + .setCode(errorCode.getCode()).setMessage(errorCode.getMsg())); + }); + return autoGenerateDTOs; + } + +} + diff --git a/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/loader/ErrorCodeLoader.java b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/loader/ErrorCodeLoader.java new file mode 100644 index 00000000..58c939c5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/loader/ErrorCodeLoader.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.framework.errorcode.core.loader; + +import com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil; + +/** + * 错误码加载器 + * + * 注意,错误码最终加载到 {@link ServiceExceptionUtil} 的 MESSAGES 变量中! + * + * @author dlyan + */ +public interface ErrorCodeLoader { + + /** + * 添加错误码 + * + * @param code 错误码的编号 + * @param msg 错误码的提示 + */ + default void putErrorCode(Integer code, String msg) { + ServiceExceptionUtil.put(code, msg); + } + + /** + * 刷新错误码 + */ + void refreshErrorCodes(); + + /** + * 加载错误码 + */ + void loadErrorCodes(); + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java new file mode 100644 index 00000000..76147824 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java @@ -0,0 +1,82 @@ +package com.chanko.yunxi.mes.heli.framework.errorcode.core.loader; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.ErrorCodeApi; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * ErrorCodeLoader 的实现类,从 infra 的数据库中,加载错误码。 + * + * 考虑到错误码会刷新,所以按照 {@link #REFRESH_ERROR_CODE_PERIOD} 频率,增量加载错误码。 + * + * @author dlyan + */ +@RequiredArgsConstructor +@Slf4j +public class ErrorCodeLoaderImpl implements ErrorCodeLoader { + + /** + * 刷新错误码的频率,单位:毫秒 + */ + private static final int REFRESH_ERROR_CODE_PERIOD = 60 * 1000; + + /** + * 应用分组 + */ + private final String applicationName; + /** + * 错误码 Api + */ + private final ErrorCodeApi errorCodeApi; + + /** + * 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private LocalDateTime maxUpdateTime; + + @Override + @EventListener(ApplicationReadyEvent.class) + @Async // 异步,保证项目的启动过程,毕竟非关键流程 + public void loadErrorCodes() { + loadErrorCodes0(); + } + + @Override + @Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD) + public void refreshErrorCodes() { + loadErrorCodes0(); + } + + private void loadErrorCodes0() { + try { + // 加载错误码 + List errorCodeRespDTOs = errorCodeApi.getErrorCodeList(applicationName, maxUpdateTime); + if (CollUtil.isEmpty(errorCodeRespDTOs)) { + return; + } + log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size()); + + // 刷新错误码的缓存 + errorCodeRespDTOs.forEach(errorCodeRespDTO -> { + // 写入到错误码的缓存 + putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage()); + // 记录下更新时间,方便增量更新 + maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime()); + }); + } catch (Exception ex) { + log.error("[loadErrorCodes0][加载错误码失败({})]", ExceptionUtil.getRootCauseMessage(ex)); + } + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/package-info.java b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/package-info.java new file mode 100644 index 00000000..9ad1a14a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/java/com/chanko/yunxi/mes/heli/framework/errorcode/package-info.java @@ -0,0 +1,10 @@ +/** + * 错误码 ErrorCode 的自动配置功能,提供如下功能: + * + * 1. 远程读取:项目启动时,从 system-service 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置; + * 2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-service 服务加载最新的 ErrorCode 错误码; + * 3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑; + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.framework.errorcode; diff --git a/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..f236689b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-error-code/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.errorcode.config.MesErrorCodeAutoConfiguration diff --git a/mes-framework/mes-spring-boot-starter-biz-ip/pom.xml b/mes-framework/mes-spring-boot-starter-biz-ip/pom.xml new file mode 100644 index 00000000..581431e4 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-ip/pom.xml @@ -0,0 +1,48 @@ + + + + mes-framework + com.chanko.yunxi + ${revision} + + 4.0.0 + mes-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 + + + + com.chanko.yunxi + mes-common + + + + + org.lionsoul + ip2region + + + + org.projectlombok + lombok + + + + org.slf4j + slf4j-api + provided + + + + + diff --git a/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/Area.java b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/Area.java new file mode 100644 index 00000000..3d7088c5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/Area.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.framework.ip.core; + +import com.chanko.yunxi.mes.heli.framework.ip.core.enums.AreaTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 区域节点,包括国家、省份、城市、地区等信息 + * + * 数据可见 resources/area.csv 文件 + * + * @author 芋道源码 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +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; + + /** + * 父节点 + */ + private Area parent; + /** + * 子节点 + */ + private List children; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/enums/AreaTypeEnum.java b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/enums/AreaTypeEnum.java new file mode 100644 index 00000000..b3610315 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/enums/AreaTypeEnum.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.framework.ip.core.enums; + +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 区域类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum AreaTypeEnum implements IntArrayValuable { + + COUNTRY(1, "国家"), + PROVINCE(2, "省份"), + CITY(3, "城市"), + DISTRICT(4, "地区"), // 县、镇、区等 + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AreaTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } +} diff --git a/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/utils/AreaUtils.java b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/utils/AreaUtils.java new file mode 100644 index 00000000..9be299e8 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/utils/AreaUtils.java @@ -0,0 +1,162 @@ +package com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.object.ObjectUtils; +import com.chanko.yunxi.mes.heli.framework.ip.core.Area; +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 区域工具类 + * + * @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 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/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/utils/IPUtils.java b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/utils/IPUtils.java new file mode 100644 index 00000000..5ef715b2 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/core/utils/IPUtils.java @@ -0,0 +1,87 @@ +package com.chanko.yunxi.mes.heli.framework.ip.core.utils; + +import cn.hutool.core.io.resource.ResourceUtil; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/package-info.java b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/framework/ip/package-info.java new file mode 100644 index 00000000..62c58b42 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/java/com/chanko/yunxi/mes/heli/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 com.chanko.yunxi.mes.heli.framework.ip; diff --git a/mes-framework/mes-spring-boot-starter-biz-ip/src/main/resources/area.csv b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/resources/area.csv new file mode 100644 index 00000000..c9a3a0e7 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/resources/area.csv @@ -0,0 +1,3639 @@ +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 +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/mes-framework/mes-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb new file mode 100644 index 00000000..25522736 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb differ diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/pom.xml b/mes-framework/mes-spring-boot-starter-biz-operatelog/pom.xml new file mode 100644 index 00000000..15b7ef59 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/pom.xml @@ -0,0 +1,51 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-biz-operatelog + jar + + ${project.artifactId} + 操作日志 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.chanko.yunxi + mes-spring-boot-starter-web + provided + + + + + com.chanko.yunxi + mes-module-system-api + ${revision} + + + + + com.google.guava + guava + + + + diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/config/MesOperateLogAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/config/MesOperateLogAutoConfiguration.java new file mode 100644 index 00000000..129243ff --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/config/MesOperateLogAutoConfiguration.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.framework.operatelog.config; + +import com.chanko.yunxi.mes.heli.framework.operatelog.core.aop.OperateLogAspect; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.service.OperateLogFrameworkService; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.service.OperateLogFrameworkServiceImpl; +import com.chanko.yunxi.mes.heli.module.system.api.logger.OperateLogApi; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class MesOperateLogAutoConfiguration { + + @Bean + public OperateLogAspect operateLogAspect() { + return new OperateLogAspect(); + } + + @Bean + public OperateLogFrameworkService operateLogFrameworkService(OperateLogApi operateLogApi) { + return new OperateLogFrameworkServiceImpl(operateLogApi); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/annotations/OperateLog.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/annotations/OperateLog.java new file mode 100644 index 00000000..022156e4 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/annotations/OperateLog.java @@ -0,0 +1,57 @@ +package com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations; + +import com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +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 OperateLog { + + // ========== 模块字段 ========== + + /** + * 操作模块 + * + * 为空时,会尝试读取 {@link Tag#name()} 属性 + */ + String module() default ""; + /** + * 操作名 + * + * 为空时,会尝试读取 {@link Operation#summary()} 属性 + */ + String name() default ""; + /** + * 操作分类 + * + * 实际并不是数组,因为枚举不能设置 null 作为默认值 + */ + OperateTypeEnum[] type() default {}; + + // ========== 开关字段 ========== + + /** + * 是否记录操作日志 + */ + boolean enable() default true; + /** + * 是否记录方法参数 + */ + boolean logArgs() default true; + /** + * 是否记录方法结果的数据 + */ + boolean logResultData() default true; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/aop/OperateLogAspect.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/aop/OperateLogAspect.java new file mode 100644 index 00000000..15400048 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/aop/OperateLogAspect.java @@ -0,0 +1,375 @@ +package com.chanko.yunxi.mes.heli.framework.operatelog.core.aop; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.monitor.TracerUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.service.OperateLog; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.service.OperateLogFrameworkService; +import com.chanko.yunxi.mes.heli.framework.web.core.util.WebFrameworkUtils; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.time.LocalDateTime; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; + +/** + * 拦截使用 @OperateLog 注解,如果满足条件,则生成操作日志。 + * 满足如下任一条件,则会进行记录: + * 1. 使用 @ApiOperation + 非 @GetMapping + * 2. 使用 @OperateLog 注解 + *

+ * 但是,如果声明 @OperateLog 注解时,将 enable 属性设置为 false 时,强制不记录。 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class OperateLogAspect { + + /** + * 用于记录操作内容的上下文 + * + * @see OperateLog#getContent() + */ + private static final ThreadLocal CONTENT = new ThreadLocal<>(); + /** + * 用于记录拓展字段的上下文 + * + * @see OperateLog#getExts() + */ + private static final ThreadLocal> EXTS = new ThreadLocal<>(); + + @Resource + private OperateLogFrameworkService operateLogFrameworkService; + + @Around("@annotation(operation)") + public Object around(ProceedingJoinPoint joinPoint, Operation operation) throws Throwable { + // 可能也添加了 @ApiOperation 注解 + com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog operateLog = getMethodAnnotation(joinPoint, + com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog.class); + return around0(joinPoint, operateLog, operation); + } + + @Around("!@annotation(io.swagger.v3.oas.annotations.Operation) && @annotation(operateLog)") + // 兼容处理,只添加 @OperateLog 注解的情况 + public Object around(ProceedingJoinPoint joinPoint, + com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog operateLog) throws Throwable { + return around0(joinPoint, operateLog, null); + } + + private Object around0(ProceedingJoinPoint joinPoint, + com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog operateLog, + Operation operation) throws Throwable { + // 目前,只有管理员,才记录操作日志!所以非管理员,直接调用,不进行记录 + Integer userType = WebFrameworkUtils.getLoginUserType(); + if (!Objects.equals(userType, UserTypeEnum.ADMIN.getValue())) { + return joinPoint.proceed(); + } + + // 记录开始时间 + LocalDateTime startTime = LocalDateTime.now(); + try { + // 执行原有方法 + Object result = joinPoint.proceed(); + // 记录正常执行时的操作日志 + this.log(joinPoint, operateLog, operation, startTime, result, null); + return result; + } catch (Throwable exception) { + this.log(joinPoint, operateLog, operation, startTime, null, exception); + throw exception; + } finally { + clearThreadLocal(); + } + } + + public static void setContent(String content) { + CONTENT.set(content); + } + + public static void addExt(String key, Object value) { + if (EXTS.get() == null) { + EXTS.set(new HashMap<>()); + } + EXTS.get().put(key, value); + } + + private static void clearThreadLocal() { + CONTENT.remove(); + EXTS.remove(); + } + + private void log(ProceedingJoinPoint joinPoint, + com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog operateLog, + Operation operation, + LocalDateTime startTime, Object result, Throwable exception) { + try { + // 判断不记录的情况 + if (!isLogEnable(joinPoint, operateLog)) { + return; + } + // 真正记录操作日志 + this.log0(joinPoint, operateLog, operation, startTime, result, exception); + } catch (Throwable ex) { + log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]", + joinPoint, operateLog, operation, result, exception, ex); + } + } + + private void log0(ProceedingJoinPoint joinPoint, + com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog operateLog, + Operation operation, + LocalDateTime startTime, Object result, Throwable exception) { + OperateLog operateLogObj = new OperateLog(); + // 补全通用字段 + operateLogObj.setTraceId(TracerUtils.getTraceId()); + operateLogObj.setStartTime(startTime); + // 补充用户信息 + fillUserFields(operateLogObj); + // 补全模块信息 + fillModuleFields(operateLogObj, joinPoint, operateLog, operation); + // 补全请求信息 + fillRequestFields(operateLogObj); + // 补全方法信息 + fillMethodFields(operateLogObj, joinPoint, operateLog, startTime, result, exception); + + // 异步记录日志 + operateLogFrameworkService.createOperateLog(operateLogObj); + } + + private static void fillUserFields(OperateLog operateLogObj) { + operateLogObj.setUserId(WebFrameworkUtils.getLoginUserId()); + operateLogObj.setUserType(WebFrameworkUtils.getLoginUserType()); + } + + private static void fillModuleFields(OperateLog operateLogObj, + ProceedingJoinPoint joinPoint, + com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog operateLog, + Operation operation) { + // module 属性 + if (operateLog != null) { + operateLogObj.setModule(operateLog.module()); + } + if (StrUtil.isEmpty(operateLogObj.getModule())) { + Tag tag = getClassAnnotation(joinPoint, Tag.class); + if (tag != null) { + // 优先读取 @Tag 的 name 属性 + if (StrUtil.isNotEmpty(tag.name())) { + operateLogObj.setModule(tag.name()); + } + // 没有的话,读取 @API 的 description 属性 + if (StrUtil.isEmpty(operateLogObj.getModule()) && ArrayUtil.isNotEmpty(tag.description())) { + operateLogObj.setModule(tag.description()); + } + } + } + // name 属性 + if (operateLog != null) { + operateLogObj.setName(operateLog.name()); + } + if (StrUtil.isEmpty(operateLogObj.getName()) && operation != null) { + operateLogObj.setName(operation.summary()); + } + // type 属性 + if (operateLog != null && ArrayUtil.isNotEmpty(operateLog.type())) { + operateLogObj.setType(operateLog.type()[0].getType()); + } + if (operateLogObj.getType() == null) { + RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint)); + OperateTypeEnum operateLogType = convertOperateLogType(requestMethod); + operateLogObj.setType(operateLogType != null ? operateLogType.getType() : null); + } + // content 和 exts 属性 + operateLogObj.setContent(CONTENT.get()); + operateLogObj.setExts(EXTS.get()); + } + + private static void fillRequestFields(OperateLog operateLogObj) { + // 获得 Request 对象 + HttpServletRequest request = ServletUtils.getRequest(); + if (request == null) { + return; + } + // 补全请求信息 + operateLogObj.setRequestMethod(request.getMethod()); + operateLogObj.setRequestUrl(request.getRequestURI()); + operateLogObj.setUserIp(ServletUtils.getClientIP(request)); + operateLogObj.setUserAgent(ServletUtils.getUserAgent(request)); + } + + private static void fillMethodFields(OperateLog operateLogObj, + ProceedingJoinPoint joinPoint, + com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog operateLog, + LocalDateTime startTime, Object result, Throwable exception) { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + operateLogObj.setJavaMethod(methodSignature.toString()); + if (operateLog == null || operateLog.logArgs()) { + operateLogObj.setJavaMethodArgs(obtainMethodArgs(joinPoint)); + } + if (operateLog == null || operateLog.logResultData()) { + operateLogObj.setResultData(obtainResultData(result)); + } + operateLogObj.setDuration((int) (LocalDateTimeUtil.between(startTime, LocalDateTime.now()).toMillis())); + // (正常)处理 resultCode 和 resultMsg 字段 + if (result instanceof CommonResult) { + CommonResult commonResult = (CommonResult) result; + operateLogObj.setResultCode(commonResult.getCode()); + operateLogObj.setResultMsg(commonResult.getMsg()); + } else { + operateLogObj.setResultCode(SUCCESS.getCode()); + } + // (异常)处理 resultCode 和 resultMsg 字段 + if (exception != null) { + operateLogObj.setResultCode(INTERNAL_SERVER_ERROR.getCode()); + operateLogObj.setResultMsg(ExceptionUtil.getRootCauseMessage(exception)); + } + } + + private static boolean isLogEnable(ProceedingJoinPoint joinPoint, + com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog operateLog) { + // 有 @OperateLog 注解的情况下 + if (operateLog != null) { + return operateLog.enable(); + } + // 没有 @ApiOperation 注解的情况下,只记录 POST、PUT、DELETE 的情况 + return obtainFirstLogRequestMethod(obtainRequestMethod(joinPoint)) != null; + } + + private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) { + if (ArrayUtil.isEmpty(requestMethods)) { + return null; + } + return Arrays.stream(requestMethods).filter(requestMethod -> + requestMethod == RequestMethod.POST + || requestMethod == RequestMethod.PUT + || requestMethod == RequestMethod.DELETE) + .findFirst().orElse(null); + } + + private static RequestMethod obtainFirstMatchRequestMethod(RequestMethod[] requestMethods) { + if (ArrayUtil.isEmpty(requestMethods)) { + return null; + } + // 优先,匹配最优的 POST、PUT、DELETE + RequestMethod result = obtainFirstLogRequestMethod(requestMethods); + if (result != null) { + return result; + } + // 然后,匹配次优的 GET + result = Arrays.stream(requestMethods).filter(requestMethod -> requestMethod == RequestMethod.GET) + .findFirst().orElse(null); + if (result != null) { + return result; + } + // 兜底,获得第一个 + return requestMethods[0]; + } + + private static OperateTypeEnum convertOperateLogType(RequestMethod requestMethod) { + if (requestMethod == null) { + return null; + } + 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; + } + } + + private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) { + RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类,可以处理 @RequestMapping 别名注解 + ((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class); + return requestMapping != null ? requestMapping.method() : new RequestMethod[]{}; + } + + @SuppressWarnings("SameParameterValue") + private static T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { + return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass); + } + + @SuppressWarnings("SameParameterValue") + private static T getClassAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { + return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass); + } + + private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) { + // TODO 提升:参数脱敏和忽略 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + String[] argNames = methodSignature.getParameterNames(); + Object[] argValues = joinPoint.getArgs(); + // 拼接参数 + Map args = Maps.newHashMapWithExpectedSize(argValues.length); + for (int i = 0; i < argNames.length; i++) { + String argName = argNames[i]; + Object argValue = argValues[i]; + // 被忽略时,标记为 ignore 字符串,避免和 null 混在一起 + args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]"); + } + return JsonUtils.toJsonString(args); + } + + private static String obtainResultData(Object result) { + // TODO 提升:结果脱敏和忽略 + if (result instanceof CommonResult) { + result = ((CommonResult) result).getData(); + } + return JsonUtils.toJsonString(result); + } + + private static boolean isIgnoreArgs(Object object) { + Class clazz = object.getClass(); + // 处理数组的情况 + if (clazz.isArray()) { + return IntStream.range(0, Array.getLength(object)) + .anyMatch(index -> isIgnoreArgs(Array.get(object, index))); + } + // 递归,处理数组、Collection、Map 的情况 + if (Collection.class.isAssignableFrom(clazz)) { + return ((Collection) object).stream() + .anyMatch((Predicate) OperateLogAspect::isIgnoreArgs); + } + if (Map.class.isAssignableFrom(clazz)) { + return isIgnoreArgs(((Map) object).values()); + } + // obj + return object instanceof MultipartFile + || object instanceof HttpServletRequest + || object instanceof HttpServletResponse + || object instanceof BindingResult; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/enums/OperateTypeEnum.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/enums/OperateTypeEnum.java new file mode 100644 index 00000000..85da30ad --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/enums/OperateTypeEnum.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.framework.operatelog.core.enums; + +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作日志的操作类型 + * + * @author ruoyi + */ +@Getter +@AllArgsConstructor +public enum OperateTypeEnum { + + /** + * 查询 + * + * 绝大多数情况下,不会记录查询动作,因为过于大量显得没有意义。 + * 在有需要的时候,通过声明 {@link OperateLog} 注解来记录 + */ + GET(1), + /** + * 新增 + */ + CREATE(2), + /** + * 修改 + */ + UPDATE(3), + /** + * 删除 + */ + DELETE(4), + /** + * 导出 + */ + EXPORT(5), + /** + * 导入 + */ + IMPORT(6), + /** + * 其它 + * + * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 + */ + OTHER(0); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/package-info.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/package-info.java new file mode 100644 index 00000000..468e0b53 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/package-info.java @@ -0,0 +1 @@ +package com.chanko.yunxi.mes.heli.framework.operatelog.core; diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/service/OperateLog.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/service/OperateLog.java new file mode 100644 index 00000000..a1b1e207 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/service/OperateLog.java @@ -0,0 +1,110 @@ +package com.chanko.yunxi.mes.heli.framework.operatelog.core.service; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 操作日志 + * + * @author 芋道源码 + */ +@Data +public class OperateLog { + + /** + * 链路追踪编号 + */ + private String traceId; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + + /** + * 操作模块 + */ + private String module; + + /** + * 操作名 + */ + private String name; + + /** + * 操作分类 + */ + private Integer type; + + /** + * 操作明细 + */ + private String content; + + /** + * 拓展字段 + */ + private Map exts; + + /** + * 请求方法名 + */ + private String requestMethod; + + /** + * 请求地址 + */ + private String requestUrl; + + /** + * 用户 IP + */ + private String userIp; + + /** + * 浏览器 UserAgent + */ + private String userAgent; + + /** + * Java 方法名 + */ + private String javaMethod; + + /** + * Java 方法的参数 + */ + private String javaMethodArgs; + + /** + * 开始时间 + */ + private LocalDateTime startTime; + + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + + /** + * 结果码 + */ + private Integer resultCode; + + /** + * 结果提示 + */ + private String resultMsg; + + /** + * 结果数据 + */ + private String resultData; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/service/OperateLogFrameworkService.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/service/OperateLogFrameworkService.java new file mode 100644 index 00000000..75651b48 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/service/OperateLogFrameworkService.java @@ -0,0 +1,17 @@ +package com.chanko.yunxi.mes.heli.framework.operatelog.core.service; + +/** + * 操作日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface OperateLogFrameworkService { + + /** + * 记录操作日志 + * + * @param operateLog 操作日志请求 + */ + void createOperateLog(OperateLog operateLog); + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java new file mode 100644 index 00000000..b14253ba --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.framework.operatelog.core.service; + +import cn.hutool.core.bean.BeanUtil; +import com.chanko.yunxi.mes.heli.module.system.api.logger.OperateLogApi; +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.OperateLogCreateReqDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; + +/** + * 操作日志 Framework Service 实现类 + * + * 基于 {@link OperateLogApi} 实现,记录操作日志 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class OperateLogFrameworkServiceImpl implements OperateLogFrameworkService { + + private final OperateLogApi operateLogApi; + + @Override + @Async + public void createOperateLog(OperateLog operateLog) { + OperateLogCreateReqDTO reqDTO = BeanUtil.toBean(operateLog, OperateLogCreateReqDTO.class); + operateLogApi.createOperateLog(reqDTO); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/util/OperateLogUtils.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/util/OperateLogUtils.java new file mode 100644 index 00000000..2958c054 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/core/util/OperateLogUtils.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.framework.operatelog.core.util; + +import com.chanko.yunxi.mes.heli.framework.operatelog.core.aop.OperateLogAspect; + +/** + * 操作日志工具类 + * 目前主要的作用,是提供给业务代码,记录操作明细和拓展字段 + * + * @author 芋道源码 + */ +public class OperateLogUtils { + + public static void setContent(String content) { + OperateLogAspect.setContent(content); + } + + public static void addExt(String key, Object value) { + OperateLogAspect.addExt(key, value); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/package-info.java b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/package-info.java new file mode 100644 index 00000000..f2a08175 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/java/com/chanko/yunxi/mes/heli/framework/operatelog/package-info.java @@ -0,0 +1,6 @@ +/** + * 用户操作日志:记录用户的操作,用于对用户的操作的审计与追溯,永久保存。 + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.framework.operatelog; diff --git a/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..f8e77fe3 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-operatelog/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.operatelog.config.MesOperateLogAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/pom.xml b/mes-framework/mes-spring-boot-starter-biz-sms/pom.xml new file mode 100644 index 00000000..636a66e5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/pom.xml @@ -0,0 +1,75 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-biz-sms + jar + + ${project.artifactId} + 短信拓展,支持阿里云、腾讯云 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter + + + + + io.opentracing + opentracing-util + + + + + com.google.guava + guava + true + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + jakarta.validation + jakarta.validation-api + + + + + + + com.aliyun + aliyun-java-sdk-core + + + com.aliyun + aliyun-java-sdk-dysmsapi + + + com.tencentcloudapi + tencentcloud-sdk-java-sms + + + + + diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/config/MesSmsAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/config/MesSmsAutoConfiguration.java new file mode 100644 index 00000000..587a9654 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/config/MesSmsAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.framework.sms.config; + +import com.chanko.yunxi.mes.heli.framework.sms.core.client.SmsClientFactory; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.SmsClientFactoryImpl; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * 短信配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class MesSmsAutoConfiguration { + + @Bean + public SmsClientFactory smsClientFactory() { + return new SmsClientFactoryImpl(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/SmsClient.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/SmsClient.java new file mode 100644 index 00000000..cfb4d558 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/SmsClient.java @@ -0,0 +1,54 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client; + +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsSendRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsTemplateRespDTO; + +import java.util.List; + +/** + * 短信客户端,用于对接各短信平台的 SDK,实现短信发送等功能 + * + * @author zzf + * @since 2021/1/25 14:14 + */ +public interface SmsClient { + + /** + * 获得渠道编号 + * + * @return 渠道编号 + */ + Long getId(); + + /** + * 发送消息 + * + * @param logId 日志编号 + * @param mobile 手机号 + * @param apiTemplateId 短信 API 的模板编号 + * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 + * @return 短信发送结果 + */ + SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId, + List> templateParams) throws Throwable; + + /** + * 解析接收短信的接收结果 + * + * @param text 结果 + * @return 结果内容 + * @throws Throwable 当解析 text 发生异常时,则会抛出异常 + */ + List parseSmsReceiveStatus(String text) throws Throwable; + + /** + * 查询指定的短信模板 + * + * @param apiTemplateId 短信 API 的模板编号 + * @return 短信模板 + */ + SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/SmsClientFactory.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/SmsClientFactory.java new file mode 100644 index 00000000..dd957f00 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/SmsClientFactory.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client; + +import com.chanko.yunxi.mes.heli.framework.sms.core.property.SmsChannelProperties; + +/** + * 短信客户端的工厂接口 + * + * @author zzf + * @since 2021/1/28 14:01 + */ +public interface SmsClientFactory { + + /** + * 获得短信 Client + * + * @param channelId 渠道编号 + * @return 短信 Client + */ + SmsClient getSmsClient(Long channelId); + + /** + * 获得短信 Client + * + * @param channelCode 渠道编码 + * @return 短信 Client + */ + SmsClient getSmsClient(String channelCode); + + /** + * 创建短信 Client + * + * @param properties 配置对象 + */ + void createOrUpdateSmsClient(SmsChannelProperties properties); + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/dto/SmsReceiveRespDTO.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/dto/SmsReceiveRespDTO.java new file mode 100644 index 00000000..792bc8be --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/dto/SmsReceiveRespDTO.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 消息接收 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsReceiveRespDTO { + + /** + * 是否接收成功 + */ + private Boolean success; + /** + * API 接收结果的编码 + */ + private String errorCode; + /** + * API 接收结果的说明 + */ + private String errorMsg; + + /** + * 手机号 + */ + private String mobile; + /** + * 用户接收时间 + */ + private LocalDateTime receiveTime; + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + /** + * 短信日志编号 + * + * 对应 SysSmsLogDO 的编号 + */ + private Long logId; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/dto/SmsSendRespDTO.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/dto/SmsSendRespDTO.java new file mode 100644 index 00000000..5d70d4e9 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/dto/SmsSendRespDTO.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client.dto; + +import lombok.Data; + +/** + * 短信发送 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsSendRespDTO { + + /** + * 是否成功 + */ + private Boolean success; + + /** + * API 请求编号 + */ + private String apiRequestId; + + // ==================== 成功时字段 ==================== + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + + // ==================== 失败时字段 ==================== + + /** + * API 返回错误码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiCode; + /** + * API 返回提示 + */ + private String apiMsg; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/dto/SmsTemplateRespDTO.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/dto/SmsTemplateRespDTO.java new file mode 100644 index 00000000..526ce176 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/dto/SmsTemplateRespDTO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client.dto; + +import com.chanko.yunxi.mes.heli.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import lombok.Data; + +/** + * 短信模板 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsTemplateRespDTO { + + /** + * 模板编号 + */ + private String id; + /** + * 短信内容 + */ + private String content; + /** + * 审核状态 + * + * 枚举 {@link SmsTemplateAuditStatusEnum} + */ + private Integer auditStatus; + /** + * 审核未通过的理由 + */ + private String auditReason; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/AbstractSmsClient.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/AbstractSmsClient.java new file mode 100644 index 00000000..d10fadb6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/AbstractSmsClient.java @@ -0,0 +1,54 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client.impl; + +import com.chanko.yunxi.mes.heli.framework.sms.core.client.SmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; + +/** + * 短信客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author zzf + * @since 2021/2/1 9:28 + */ +@Slf4j +public abstract class AbstractSmsClient implements SmsClient { + + /** + * 短信渠道配置 + */ + protected volatile SmsChannelProperties properties; + + public AbstractSmsClient(SmsChannelProperties properties) { + this.properties = properties; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.debug("[init][配置({}) 初始化完成]", properties); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(SmsChannelProperties properties) { + // 判断是否更新 + if (properties.equals(this.properties)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", properties); + this.properties = properties; + // 初始化 + this.init(); + } + + @Override + public Long getId() { + return properties.getId(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/SmsClientFactoryImpl.java new file mode 100644 index 00000000..98b126fc --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -0,0 +1,90 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client.impl; + +import com.chanko.yunxi.mes.heli.framework.sms.core.client.SmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.SmsClientFactory; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.aliyun.AliyunSmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.tencent.TencentSmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.enums.SmsChannelEnum; +import com.chanko.yunxi.mes.heli.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 短信客户端工厂接口 + * + * @author zzf + */ +@Validated +@Slf4j +public class SmsClientFactoryImpl implements SmsClientFactory { + + /** + * 短信客户端 Map + * key:渠道编号,使用 {@link SmsChannelProperties#getId()} + */ + private final ConcurrentMap channelIdClients = new ConcurrentHashMap<>(); + + /** + * 短信客户端 Map + * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} + * + * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。 + * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients} + */ + private final ConcurrentMap channelCodeClients = new ConcurrentHashMap<>(); + + public SmsClientFactoryImpl() { + // 初始化 channelCodeClients 集合 + Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { + // 创建一个空的 SmsChannelProperties 对象 + SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) + .setApiKey("default default").setApiSecret("default"); + // 创建 Sms 客户端 + AbstractSmsClient smsClient = createSmsClient(properties); + channelCodeClients.put(channel.getCode(), smsClient); + }); + } + + @Override + public SmsClient getSmsClient(Long channelId) { + return channelIdClients.get(channelId); + } + + @Override + public SmsClient getSmsClient(String channelCode) { + return channelCodeClients.get(channelCode); + } + + @Override + public void createOrUpdateSmsClient(SmsChannelProperties properties) { + AbstractSmsClient client = channelIdClients.get(properties.getId()); + if (client == null) { + client = this.createSmsClient(properties); + client.init(); + channelIdClients.put(client.getId(), client); + } else { + client.refresh(properties); + } + } + + private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { + SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode()); + Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum)); + // 创建客户端 + switch (channelEnum) { + case ALIYUN: return new AliyunSmsClient(properties); + case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); + case TENCENT: return new TencentSmsClient(properties); + } + // 创建失败,错误日志 + 抛出异常 + log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java new file mode 100644 index 00000000..866212b4 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java @@ -0,0 +1,183 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.aliyun; + +import cn.hutool.core.lang.Assert; +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.MapUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsSendRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.AbstractSmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.chanko.yunxi.mes.heli.framework.sms.core.property.SmsChannelProperties; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertList; +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 阿里短信客户端的实现类 + * + * @author zzf + * @since 2021/1/25 14:17 + */ +@Slf4j +public class AliyunSmsClient extends AbstractSmsClient { + + /** + * 调用成功 code + */ + public static final String API_CODE_SUCCESS = "OK"; + + /** + * REGION, 使用杭州 + */ + private static final String ENDPOINT = "cn-hangzhou"; + + /** + * 阿里云客户端 + */ + private volatile IAcsClient client; + + public AliyunSmsClient(SmsChannelProperties properties) { + super(properties); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret()); + client = new DefaultAcsClient(profile); + } + + @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, + List> templateParams) throws Throwable { + // 构建请求 + SendSmsRequest request = new SendSmsRequest(); + request.setPhoneNumbers(mobile); + request.setSignName(properties.getSignature()); + request.setTemplateCode(apiTemplateId); + request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams))); + request.setOutId(String.valueOf(sendLogId)); + // 执行请求 + SendSmsResponse response = client.getAcsResponse(request); + return new SmsSendRespDTO().setSuccess(Objects.equals(response.getCode(), API_CODE_SUCCESS)).setSerialNo(response.getBizId()) + .setApiRequestId(response.getRequestId()).setApiCode(response.getCode()).setApiMsg(response.getMessage()); + } + + @Override + public List parseSmsReceiveStatus(String text) { + List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(status.getSuccess()) + .setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg()) + .setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime()) + .setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()))); + } + + @Override + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { + // 构建请求 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + request.setTemplateCode(apiTemplateId); + // 执行请求 + QuerySmsTemplateResponse response = client.getAcsResponse(request); + if (response.getTemplateStatus() == null) { + return null; + } + return new SmsTemplateRespDTO().setId(response.getTemplateCode()).setContent(response.getTemplateContent()) + .setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason()); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(Integer templateStatus) { + switch (templateStatus) { + case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); + } + } + + /** + * 短信接收状态 + * + * 参见 文档 + * + * @author 芋道源码 + */ + @Data + public static class SmsReceiveStatus { + + /** + * 手机号 + */ + @JsonProperty("phone_number") + private String phoneNumber; + /** + * 发送时间 + */ + @JsonProperty("send_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime sendTime; + /** + * 状态报告时间 + */ + @JsonProperty("report_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime reportTime; + /** + * 是否接收成功 + */ + private Boolean success; + /** + * 状态报告说明 + */ + @JsonProperty("err_msg") + private String errMsg; + /** + * 状态报告编码 + */ + @JsonProperty("err_code") + private String errCode; + /** + * 发送序列号 + */ + @JsonProperty("biz_id") + private String bizId; + /** + * 用户序列号 + * + * 这里我们传递的是 SysSmsLogDO 的日志编号 + */ + @JsonProperty("out_id") + private String outId; + /** + * 短信长度,例如说 1、2、3 + * + * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送 + */ + @JsonProperty("sms_size") + private Integer smsSize; + + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java new file mode 100644 index 00000000..e7f41582 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java @@ -0,0 +1,96 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.debug; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; +import cn.hutool.http.HttpUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.MapUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsSendRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.AbstractSmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.chanko.yunxi.mes.heli.framework.sms.core.property.SmsChannelProperties; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 基于钉钉 WebHook 实现的调试的短信客户端实现类 + * + * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。 + * + * @author 芋道源码 + */ +public class DebugDingTalkSmsClient extends AbstractSmsClient { + + public DebugDingTalkSmsClient(SmsChannelProperties properties) { + super(properties); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + } + + @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + // 构建请求 + String url = buildUrl("robot/send"); + Map params = new HashMap<>(); + params.put("msgtype", "text"); + String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s", + mobile, sendLogId, MapUtils.convertMap(templateParams)); + params.put("text", MapUtil.builder().put("content", content).build()); + // 执行请求 + String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params)); + // 解析结果 + Map responseObj = JsonUtils.parseObject(responseText, Map.class); + String errorCode = MapUtil.getStr(responseObj, "errcode"); + return new SmsSendRespDTO().setSuccess(Objects.equals(errorCode, "0")).setSerialNo(StrUtil.uuid()) + .setApiCode(errorCode).setApiMsg(MapUtil.getStr(responseObj, "errorMsg")); + } + + /** + * 构建请求地址 + * + * 参见 文档 + * + * @param path 请求路径 + * @return 请求地址 + */ + @SuppressWarnings("SameParameterValue") + private String buildUrl(String path) { + // 生成 timestamp + long timestamp = System.currentTimeMillis(); + // 生成 sign + String secret = properties.getApiSecret(); + String stringToSign = timestamp + "\n" + secret; + byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign); + String sign = Base64.encode(signData); + // 构建最终 URL + return String.format("https://oapi.dingtalk.com/%s?access_token=%s×tamp=%d&sign=%s", + path, properties.getApiKey(), timestamp, sign); + } + + @Override + public List parseSmsReceiveStatus(String text) { + throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调"); + } + + @Override + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) { + return new SmsTemplateRespDTO().setId(apiTemplateId).setContent("") + .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(""); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/tencent/TencentSmsClient.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/tencent/TencentSmsClient.java new file mode 100644 index 00000000..e6cf47ef --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/client/impl/tencent/TencentSmsClient.java @@ -0,0 +1,219 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.tencent; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.ArrayUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsSendRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.impl.AbstractSmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.chanko.yunxi.mes.heli.framework.sms.core.property.SmsChannelProperties; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.sms.v20210111.SmsClient; +import com.tencentcloudapi.sms.v20210111.models.*; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertList; +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 腾讯云短信功能实现 + * + * 参见 文档 + * + * @author shiwp + */ +public class TencentSmsClient extends AbstractSmsClient { + + /** + * 调用成功 code + */ + public static final String API_CODE_SUCCESS = "Ok"; + + /** + * REGION,使用南京 + */ + private static final String ENDPOINT = "ap-nanjing"; + + /** + * 是否国际/港澳台短信: + * + * 0:表示国内短信。 + * 1:表示国际/港澳台短信。 + */ + private static final long INTERNATIONAL_CHINA = 0L; + + private SmsClient client; + + public TencentSmsClient(SmsChannelProperties properties) { + super(properties); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + validateSdkAppId(properties); + } + + @Override + protected void doInit() { + // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey + Credential credential = new Credential(getApiKey(), properties.getApiSecret()); + client = new SmsClient(credential, ENDPOINT); + } + + /** + * 参数校验腾讯云的 SDK AppId + * + * 原因是:腾讯云发放短信的时候,需要额外的参数 sdkAppId + * + * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 + * + * @param properties 配置 + */ + private static void validateSdkAppId(SmsChannelProperties properties) { + String combineKey = properties.getApiKey(); + Assert.notEmpty(combineKey, "apiKey 不能为空"); + String[] keys = combineKey.trim().split(" "); + Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]"); + } + + private String getSdkAppId() { + return StrUtil.subAfter(properties.getApiKey(), " ", true); + } + + private String getApiKey() { + return StrUtil.subBefore(properties.getApiKey(), " ", true); + } + + @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + // 构建请求 + SendSmsRequest request = new SendSmsRequest(); + request.setSmsSdkAppId(getSdkAppId()); + request.setPhoneNumberSet(new String[]{mobile}); + request.setSignName(properties.getSignature()); + request.setTemplateId(apiTemplateId); + request.setTemplateParamSet(ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue()))); + request.setSessionContext(JsonUtils.toJsonString(new SessionContext().setLogId(sendLogId))); + // 执行请求 + SendSmsResponse response = client.SendSms(request); + SendStatus status = response.getSendStatusSet()[0]; + return new SmsSendRespDTO().setSuccess(Objects.equals(status.getCode(), API_CODE_SUCCESS)).setSerialNo(status.getSerialNo()) + .setApiRequestId(response.getRequestId()).setApiCode(status.getCode()).setApiMsg(status.getMessage()); + } + + @Override + public List parseSmsReceiveStatus(String text) { + List callback = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return convertList(callback, status -> new SmsReceiveRespDTO() + .setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus())) + .setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription()) + .setMobile(status.getMobile()).setReceiveTime(status.getReceiveTime()) + .setSerialNo(status.getSerialNo()).setLogId(status.getSessionContext().getLogId())); + } + + @Override + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { + // 构建请求 + DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest(); + request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)}); + request.setInternational(INTERNATIONAL_CHINA); + // 执行请求 + DescribeSmsTemplateListResponse response = client.DescribeSmsTemplateList(request); + DescribeTemplateListStatus status = response.getDescribeTemplateStatusSet()[0]; + if (status == null || status.getStatusCode() == null) { + return null; + } + return new SmsTemplateRespDTO().setId(status.getTemplateId().toString()).setContent(status.getTemplateContent()) + .setAuditStatus(convertSmsTemplateAuditStatus(status.getStatusCode().intValue())).setAuditReason(status.getReviewReply()); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(int templateStatus) { + switch (templateStatus) { + case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); + } + } + + @Data + private static class SmsReceiveStatus { + + /** + * 短信接受成功 code + */ + public static final String SUCCESS_CODE = "SUCCESS"; + + /** + * 用户实际接收到短信的时间 + */ + @JsonProperty("user_receive_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime receiveTime; + + /** + * 国家(或地区)码 + */ + @JsonProperty("nationcode") + private String nationCode; + + /** + * 手机号码 + */ + private String mobile; + + /** + * 实际是否收到短信接收状态,SUCCESS(成功)、FAIL(失败) + */ + @JsonProperty("report_status") + private String status; + + /** + * 用户接收短信状态码错误信息 + */ + @JsonProperty("errmsg") + private String errCode; + + /** + * 用户接收短信状态描述 + */ + @JsonProperty("description") + private String description; + + /** + * 本次发送标识 ID(与发送接口返回的SerialNo对应) + */ + @JsonProperty("sid") + private String serialNo; + + /** + * 用户的 session 内容(与发送接口的请求参数 SessionContext 一致) + */ + @JsonProperty("ext") + private SessionContext sessionContext; + + } + + @VisibleForTesting + @Data + static class SessionContext { + + /** + * 发送短信记录id + */ + private Long logId; + + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/enums/SmsChannelEnum.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/enums/SmsChannelEnum.java new file mode 100644 index 00000000..b64caa7d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/enums/SmsChannelEnum.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信渠道枚举 + * + * @author zzf + * @since 2021/1/25 10:56 + */ +@Getter +@AllArgsConstructor +public enum SmsChannelEnum { + + DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"), + ALIYUN("ALIYUN", "阿里云"), + TENCENT("TENCENT", "腾讯云"), +// HUA_WEI("HUA_WEI", "华为云"), + ; + + /** + * 编码 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + public static SmsChannelEnum getByCode(String code) { + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java new file mode 100644 index 00000000..15eada2d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java @@ -0,0 +1,50 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.enums; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ErrorCode; + +/** + * 短信框架的错误码枚举 + * + * 短信框架,使用 2-001-000-000 段 + * + * @author 芋道源码 + */ +public interface SmsFrameworkErrorCodeConstants { + + ErrorCode SMS_UNKNOWN = new ErrorCode(2_001_000_000, "未知错误,需要解析"); + + // ========== 权限 / 限流等相关 2-001-000-100 ========== + + ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2_001_000_100, "没有发送短信的权限"); + ErrorCode SMS_IP_DENY = new ErrorCode(2_001_000_100, "IP 不允许发送短信"); + + // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。 + ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2_001_000_102, "指定手机的发送限流"); + // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。 + ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2_001_000_103, "每天的发送限流"); + + ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2_001_000_104, "短信内容有敏感词"); + + // 腾讯云:为避免骚扰用户,营销短信只允许在8点到22点发送。 + ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2_001_000_105, "营销短信发送时间限制"); + + // ========== 模板相关 2-001-000-200 ========== + ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2_001_000_200, "短信模板不合法"); // 包括短信模板不存在 + ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2_001_000_201, "模板参数不正确"); + + // ========== 签名相关 2-001-000-300 ========== + ErrorCode SMS_SIGN_INVALID = new ErrorCode(2_001_000_300, "短信签名不可用"); + + // ========== 账户相关 2-001-000-400 ========== + ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2_001_000_400, "账户余额不足"); + ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2_001_000_401, "apiKey 不存在"); + + // ========== 其它相关 2-001-000-900 开头 ========== + ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2_001_000_900, "请求参数缺失"); + ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2_001_000_901, "手机格式不正确"); + ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2_001_000_902, "手机号在黑名单中"); + ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2_001_000_903, "SdkAppId不合法"); + + ErrorCode EXCEPTION = new ErrorCode(2_001_000_999, "调用异常"); + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java new file mode 100644 index 00000000..91d787ff --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信模板的审核状态枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum SmsTemplateAuditStatusEnum { + + CHECKING(1), + SUCCESS(2), + FAIL(3); + + private final Integer status; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/property/SmsChannelProperties.java b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/property/SmsChannelProperties.java new file mode 100644 index 00000000..1298bc3b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/java/com/chanko/yunxi/mes/heli/framework/sms/core/property/SmsChannelProperties.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.framework.sms.core.property; + +import com.chanko.yunxi.mes.heli.framework.sms.core.enums.SmsChannelEnum; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信渠道配置类 + * + * @author zzf + * @since 2021/1/25 17:01 + */ +@Data +@Validated +public class SmsChannelProperties { + + /** + * 渠道编号 + */ + @NotNull(message = "短信渠道 ID 不能为空") + private Long id; + /** + * 短信签名 + */ + @NotEmpty(message = "短信签名不能为空") + private String signature; + /** + * 渠道编码 + * + * 枚举 {@link SmsChannelEnum} + */ + @NotEmpty(message = "渠道编码不能为空") + private String code; + /** + * 短信 API 的账号 + */ + @NotEmpty(message = "短信 API 的账号不能为空") + private String apiKey; + /** + * 短信 API 的密钥 + */ + @NotEmpty(message = "短信 API 的密钥不能为空") + private String apiSecret; + /** + * 短信发送回调 URL + */ + private String callbackUrl; + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..5ee562a6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.sms.config.MesSmsAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/pom.xml b/mes-framework/mes-spring-boot-starter-biz-tenant/pom.xml new file mode 100644 index 00000000..a5b47f29 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/pom.xml @@ -0,0 +1,76 @@ + + + + mes-framework + com.chanko.yunxi + ${revision} + + 4.0.0 + mes-spring-boot-starter-biz-tenant + jar + + ${project.artifactId} + 多租户 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + com.chanko.yunxi + mes-spring-boot-starter-security + + + + + com.chanko.yunxi + mes-spring-boot-starter-mybatis + + + + com.chanko.yunxi + mes-spring-boot-starter-redis + + + + + com.chanko.yunxi + mes-spring-boot-starter-job + + + + + com.chanko.yunxi + mes-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 + + + + + com.google.guava + guava + + + + diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/config/MesTenantAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/config/MesTenantAutoConfiguration.java new file mode 100644 index 00000000..b2b04cf6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/config/MesTenantAutoConfiguration.java @@ -0,0 +1,132 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.config; + +import com.chanko.yunxi.mes.heli.framework.common.enums.WebFilterOrderEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.util.MyBatisUtils; +import com.chanko.yunxi.mes.heli.framework.redis.config.MesCacheProperties; +import com.chanko.yunxi.mes.heli.framework.tenant.core.aop.TenantIgnoreAspect; +import com.chanko.yunxi.mes.heli.framework.tenant.core.db.TenantDatabaseInterceptor; +import com.chanko.yunxi.mes.heli.framework.tenant.core.job.TenantJobAspect; +import com.chanko.yunxi.mes.heli.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer; +import com.chanko.yunxi.mes.heli.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor; +import com.chanko.yunxi.mes.heli.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer; +import com.chanko.yunxi.mes.heli.framework.tenant.core.redis.TenantRedisCacheManager; +import com.chanko.yunxi.mes.heli.framework.tenant.core.security.TenantSecurityWebFilter; +import com.chanko.yunxi.mes.heli.framework.tenant.core.service.TenantFrameworkService; +import com.chanko.yunxi.mes.heli.framework.tenant.core.service.TenantFrameworkServiceImpl; +import com.chanko.yunxi.mes.heli.framework.tenant.core.web.TenantContextWebFilter; +import com.chanko.yunxi.mes.heli.framework.web.config.WebProperties; +import com.chanko.yunxi.mes.heli.framework.web.core.handler.GlobalExceptionHandler; +import com.chanko.yunxi.mes.heli.module.system.api.tenant.TenantApi; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +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; +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 java.util.Objects; + +@AutoConfiguration +@ConditionalOnProperty(prefix = "mes.tenant", value = "enable", matchIfMissing = true) // 允许使用 mes.tenant.enable=false 禁用多租户 +@EnableConfigurationProperties(TenantProperties.class) +public class MesTenantAutoConfiguration { + + @Bean + public TenantFrameworkService tenantFrameworkService(TenantApi 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() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantContextWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); + return registrationBean; + } + + // ========== 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, + MesCacheProperties mesCacheProperties) { + // 创建 RedisCacheWriter 对象 + RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, + BatchStrategies.scan(mesCacheProperties.getRedisScanBatchSize())); + // 创建 TenantRedisCacheManager 对象 + return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/config/TenantProperties.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/config/TenantProperties.java new file mode 100644 index 00000000..db92a90a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/config/TenantProperties.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Collections; +import java.util.Set; + +/** + * 多租户配置 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "mes.tenant") +@Data +public class TenantProperties { + + /** + * 租户是否开启 + */ + private static final Boolean ENABLE_DEFAULT = true; + + /** + * 是否开启 + */ + private Boolean enable = ENABLE_DEFAULT; + + /** + * 需要忽略多租户的请求 + * + * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API! + */ + private Set ignoreUrls = Collections.emptySet(); + + /** + * 需要忽略多租户的表 + * + * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 + */ + private Set ignoreTables = Collections.emptySet(); + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/aop/TenantIgnore.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/aop/TenantIgnore.java new file mode 100644 index 00000000..cb6b855c --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/aop/TenantIgnore.java @@ -0,0 +1,18 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.aop; + +import java.lang.annotation.*; + +/** + * 忽略租户,标记指定方法不进行租户的自动过滤 + * + * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤: + * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的 + * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface TenantIgnore { +} diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/aop/TenantIgnoreAspect.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/aop/TenantIgnoreAspect.java new file mode 100644 index 00000000..7168bfa6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/aop/TenantIgnoreAspect.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.aop; + +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.chanko.yunxi.mes.heli.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 { + TenantContextHolder.setIgnore(true); + // 执行逻辑 + return joinPoint.proceed(); + } finally { + TenantContextHolder.setIgnore(oldIgnore); + } + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/context/TenantContextHolder.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/context/TenantContextHolder.java new file mode 100644 index 00000000..97b7331c --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/context/TenantContextHolder.java @@ -0,0 +1,79 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.context; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.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(); + } + + /** + * 获得租户编号 String + * + * @return 租户编号 + */ + public static String getTenantIdStr() { + Long tenantId = getTenantId(); + return StrUtil.toStringOrNull(tenantId); + } + + /** + * 获得租户编号。如果不存在,则抛出 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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/db/TenantBaseDO.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/db/TenantBaseDO.java new file mode 100644 index 00000000..ee09adfb --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/db/TenantBaseDO.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.db; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/db/TenantDatabaseInterceptor.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/db/TenantDatabaseInterceptor.java new file mode 100644 index 00000000..96f8e270 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/db/TenantDatabaseInterceptor.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.db; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.tenant.config.TenantProperties; +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; + +import java.util.HashSet; +import java.util.Set; + +/** + * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 + * + * @author 芋道源码 + */ +public class TenantDatabaseInterceptor implements TenantLineHandler { + + private final Set ignoreTables = new HashSet<>(); + + public TenantDatabaseInterceptor(TenantProperties properties) { + // 不同 DB 下,大小写的习惯不同,所以需要都添加进去 + properties.getIgnoreTables().forEach(table -> { + ignoreTables.add(table.toLowerCase()); + ignoreTables.add(table.toUpperCase()); + }); + // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错 + ignoreTables.add("DUAL"); + } + + @Override + public Expression getTenantId() { + return new LongValue(TenantContextHolder.getRequiredTenantId()); + } + + @Override + public boolean ignoreTable(String tableName) { + return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户 + || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表 + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/job/TenantJob.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/job/TenantJob.java new file mode 100644 index 00000000..a13a68b6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/job/TenantJob.java @@ -0,0 +1,14 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/job/TenantJobAspect.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/job/TenantJobAspect.java new file mode 100644 index 00000000..dd9079aa --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/job/TenantJobAspect.java @@ -0,0 +1,56 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.job; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.tenant.core.service.TenantFrameworkService; +import com.chanko.yunxi.mes.heli.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 { + joinPoint.proceed(); + } catch (Throwable e) { + results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); + } + }); + }); + return JsonUtils.toJsonString(results); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java new file mode 100644 index 00000000..8f1f1149 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java new file mode 100644 index 00000000..36976884 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java @@ -0,0 +1,47 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.mq.kafka; + +import cn.hutool.core.util.ReflectUtil; +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java new file mode 100644 index 00000000..8ca2fd3c --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java new file mode 100644 index 00000000..0412423a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.mq.rabbitmq; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java new file mode 100644 index 00000000..2ce743ca --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.mq.redis; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.message.AbstractRedisMessage; +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; + +import static com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java new file mode 100644 index 00000000..fb41c48d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.mq.rocketmq; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java new file mode 100644 index 00000000..e3ce0b22 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java @@ -0,0 +1,53 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java new file mode 100644 index 00000000..50d04a62 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.mq.rocketmq; + +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.hook.SendMessageHook; + +import static com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/redis/TenantRedisCacheManager.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/redis/TenantRedisCacheManager.java new file mode 100644 index 00000000..e5025376 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/redis/TenantRedisCacheManager.java @@ -0,0 +1,38 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.redis; + +import com.chanko.yunxi.mes.heli.framework.redis.core.TimeoutRedisCacheManager; +import com.chanko.yunxi.mes.heli.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; + +/** + * 多租户的 {@link RedisCacheManager} 实现类 + * + * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀 + * + * @author airhead + */ +@Slf4j +public class TenantRedisCacheManager extends TimeoutRedisCacheManager { + + public TenantRedisCacheManager(RedisCacheWriter cacheWriter, + RedisCacheConfiguration defaultCacheConfiguration) { + super(cacheWriter, defaultCacheConfiguration); + } + + @Override + public Cache getCache(String name) { + // 如果开启多租户,则 name 拼接租户后缀 + if (!TenantContextHolder.isIgnore() + && TenantContextHolder.getTenantId() != null) { + name = name + ":" + TenantContextHolder.getTenantId(); + } + + // 继续基于父方法 + return super.getCache(name); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/security/TenantSecurityWebFilter.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/security/TenantSecurityWebFilter.java new file mode 100644 index 00000000..a1e9db8f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/security/TenantSecurityWebFilter.java @@ -0,0 +1,117 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.security; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.security.core.LoginUser; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; +import com.chanko.yunxi.mes.heli.framework.tenant.config.TenantProperties; +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.chanko.yunxi.mes.heli.framework.tenant.core.service.TenantFrameworkService; +import com.chanko.yunxi.mes.heli.framework.web.config.WebProperties; +import com.chanko.yunxi.mes.heli.framework.web.core.filter.ApiRequestFilter; +import com.chanko.yunxi.mes.heli.framework.web.core.handler.GlobalExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.AntPathMatcher; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/service/TenantFrameworkService.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/service/TenantFrameworkService.java new file mode 100644 index 00000000..832e075f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/service/TenantFrameworkService.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/service/TenantFrameworkServiceImpl.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/service/TenantFrameworkServiceImpl.java new file mode 100644 index 00000000..95cd7232 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/service/TenantFrameworkServiceImpl.java @@ -0,0 +1,73 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.service; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.framework.common.util.cache.CacheUtils; +import com.chanko.yunxi.mes.heli.module.system.api.tenant.TenantApi; +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 TenantApi 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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/util/TenantUtils.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/util/TenantUtils.java new file mode 100644 index 00000000..a4f2f2b9 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/util/TenantUtils.java @@ -0,0 +1,93 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.util; + +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; + +import java.util.Map; +import java.util.concurrent.Callable; + +import static com.chanko.yunxi.mes.heli.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 逻辑 + */ + 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); + } + } + + /** + * 将多租户编号,添加到 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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/web/TenantContextWebFilter.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/web/TenantContextWebFilter.java new file mode 100644 index 00000000..63f3fe8f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/core/web/TenantContextWebFilter.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.framework.tenant.core.web; + +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.chanko.yunxi.mes.heli.framework.web.core.util.WebFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/package-info.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/framework/tenant/package-info.java new file mode 100644 index 00000000..f485162e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/com/chanko/yunxi/mes/heli/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 com.chanko.yunxi.mes.heli.framework.quartz.config.MesAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} + * 2)Spring Security: + * TransmittableThreadLocalSecurityContextHolderStrategy + * 和 MesSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 + * + */ +package com.chanko.yunxi.mes.heli.framework.tenant; diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java new file mode 100644 index 00000000..85de8419 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java @@ -0,0 +1,269 @@ +/* + * Copyright 2002-2021 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 com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.chanko.yunxi.mes.heli.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 java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; + +import static com.chanko.yunxi.mes.heli.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 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.getMessage() : "Illegal argument"); + throw new IllegalStateException(formatInvokeError(text, args), ex); + } + catch (InvocationTargetException ex) { + // Unwrap for HandlerExceptionResolvers ... + Throwable targetException = ex.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + else if (targetException instanceof Error) { + throw (Error) targetException; + } + else if (targetException instanceof Exception) { + throw (Exception) targetException; + } + else { + throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException); + } + } + } + + MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) { + return new AsyncResultMethodParameter(returnValue); + } + + private class AsyncResultMethodParameter extends HandlerMethodParameter { + + @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/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..934eda1c --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ + com.chanko.yunxi.mes.heli.framework.tenant.core.mq.kafka.TenantKafkaEnvironmentPostProcessor diff --git a/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..e0861ade --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.tenant.config.MesTenantAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-captcha/pom.xml b/mes-framework/mes-spring-boot-starter-captcha/pom.xml new file mode 100644 index 00000000..d0e147b1 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-captcha/pom.xml @@ -0,0 +1,38 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-captcha + jar + + ${project.artifactId} + 验证码拓展 + 1. 基于 aj-captcha 实现滑块验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/ + + + + + com.xingyuv + spring-boot-starter-captcha-plus + + + + org.springframework.boot + spring-boot-starter + + + + + com.chanko.yunxi + mes-spring-boot-starter-redis + + + + + diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/config/MesCaptchaConfiguration.java b/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/config/MesCaptchaConfiguration.java new file mode 100644 index 00000000..b11ca05a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/config/MesCaptchaConfiguration.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.framework.captcha.config; + +import com.chanko.yunxi.mes.heli.framework.captcha.core.service.RedisCaptchaServiceImpl; +import com.xingyuv.captcha.properties.AjCaptchaProperties; +import com.xingyuv.captcha.service.CaptchaCacheService; +import com.xingyuv.captcha.service.impl.CaptchaServiceFactory; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +import javax.annotation.Resource; + +@AutoConfiguration +public class MesCaptchaConfiguration { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Bean + public CaptchaCacheService captchaCacheService(AjCaptchaProperties config) { + // 缓存类型 redis/local/.... + CaptchaCacheService ret = CaptchaServiceFactory.getCache(config.getCacheType().name()); + if (ret instanceof RedisCaptchaServiceImpl) { + ((RedisCaptchaServiceImpl) ret).setStringRedisTemplate(stringRedisTemplate); + } + return ret; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/core/enums/CaptchaRedisKeyConstants.java b/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/core/enums/CaptchaRedisKeyConstants.java new file mode 100644 index 00000000..ddc112c1 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/core/enums/CaptchaRedisKeyConstants.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.framework.captcha.core.enums; + +/** + * 验证码 Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface CaptchaRedisKeyConstants { + + /** + * 验证码的请求限流 + * + * KEY 格式:AJ.CAPTCHA.REQ.LIMIT-%s-%s + * VALUE 数据类型:String // 例如说:验证失败 5 次,get 接口锁定 + * 过期时间:60 秒 + */ + String AJ_CAPTCHA_REQ_LIMIT = "AJ.CAPTCHA.REQ.LIMIT-%s-%s"; + + /** + * 验证码的坐标 + * + * KEY 格式:RUNNING:CAPTCHA:%s // AbstractCaptchaService.REDIS_CAPTCHA_KEY + * VALUE 数据类型:String // PointVO.class {"secretKey":"PP1w2Frr2KEejD2m","x":162,"y":5} + * 过期时间:120 秒 + */ + String AJ_CAPTCHA_RUNNING = "RUNNING:CAPTCHA:%s"; + +} diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/core/service/RedisCaptchaServiceImpl.java b/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/core/service/RedisCaptchaServiceImpl.java new file mode 100644 index 00000000..6a55d505 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/core/service/RedisCaptchaServiceImpl.java @@ -0,0 +1,57 @@ +package com.chanko.yunxi.mes.heli.framework.captcha.core.service; + +import com.xingyuv.captcha.service.CaptchaCacheService; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis 实现验证码的存储 + * + * @author 星语 + */ +@NoArgsConstructor // 保证 aj-captcha 的 SPI 创建 +@AllArgsConstructor +public class RedisCaptchaServiceImpl implements CaptchaCacheService { + + @Resource // 保证 aj-captcha 的 SPI 创建时的注入 + private StringRedisTemplate stringRedisTemplate; + + @Override + public String type() { + return "redis"; + } + + public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) { + this.stringRedisTemplate = stringRedisTemplate; + } + + @Override + public void set(String key, String value, long expiresInSeconds) { + stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS); + } + + @Override + public boolean exists(String key) { + return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); + } + + @Override + public void delete(String key) { + stringRedisTemplate.delete(key); + } + + @Override + public String get(String key) { + return stringRedisTemplate.opsForValue().get(key); + } + + @Override + public Long increment(String key, long val) { + return stringRedisTemplate.opsForValue().increment(key,val); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/package-info.java b/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/package-info.java new file mode 100644 index 00000000..c0b5464e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-captcha/src/main/java/com/chanko/yunxi/mes/heli/framework/captcha/package-info.java @@ -0,0 +1,7 @@ +/** + * 验证码拓展 + * 1. 基于 aj-captcha 实现滑块验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/ + * + * @author 星语 + */ +package com.chanko.yunxi.mes.heli.framework.captcha; diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaCacheService b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaCacheService new file mode 100644 index 00000000..4165fc51 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaCacheService @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.captcha.core.service.RedisCaptchaServiceImpl diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..ef9202cb --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.captcha.config.MesCaptchaConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg1.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg1.png new file mode 100644 index 00000000..c4814576 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg1.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg2.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg2.png new file mode 100644 index 00000000..bf8fb38f Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg2.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg3.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg3.png new file mode 100644 index 00000000..f871d3d1 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg3.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg4.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg4.png new file mode 100644 index 00000000..2e3d8716 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg4.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg5.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg5.png new file mode 100644 index 00000000..fe383b72 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg5.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg6.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg6.png new file mode 100644 index 00000000..5024ceb2 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg6.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg7.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg7.png new file mode 100644 index 00000000..efe76f8d Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg7.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg8.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg8.png new file mode 100644 index 00000000..2727aa32 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg8.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg9.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg9.png new file mode 100644 index 00000000..4463aa2f Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg9.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/1.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/1.png new file mode 100644 index 00000000..ef113247 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/1.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/10.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/10.png new file mode 100644 index 00000000..297e44cf Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/10.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/11.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/11.png new file mode 100644 index 00000000..d9b1da8d Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/11.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/12.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/12.png new file mode 100644 index 00000000..07e7313b Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/12.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/13.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/13.png new file mode 100644 index 00000000..82c3dd96 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/13.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/14.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/14.png new file mode 100644 index 00000000..0b9a8661 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/14.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/15.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/15.png new file mode 100644 index 00000000..86b0d1cf Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/15.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/16.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/16.png new file mode 100644 index 00000000..e90a6e29 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/16.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/17.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/17.png new file mode 100644 index 00000000..a82cbc7c Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/17.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/18.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/18.png new file mode 100644 index 00000000..d3f3cfd0 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/18.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/19.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/19.png new file mode 100644 index 00000000..eb2855bd Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/19.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/8.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/8.png new file mode 100644 index 00000000..3cb5ce1c Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/8.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/9.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/9.png new file mode 100644 index 00000000..384d3541 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/9.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/2.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/2.png new file mode 100644 index 00000000..baf3f06d Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/2.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/3.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/3.png new file mode 100644 index 00000000..ccaf6172 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/3.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/4.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/4.png new file mode 100644 index 00000000..7dab1622 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/4.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg1.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg1.png new file mode 100644 index 00000000..14e73454 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg1.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg10.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg10.png new file mode 100644 index 00000000..1ea1d6d5 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg10.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg2.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg2.png new file mode 100644 index 00000000..0edb3293 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg2.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg3.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg3.png new file mode 100644 index 00000000..91679960 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg3.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg4.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg4.png new file mode 100644 index 00000000..e8e8e6c0 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg4.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg5.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg5.png new file mode 100644 index 00000000..66a3181e Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg5.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg6.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg6.png new file mode 100644 index 00000000..9b0f5d8c Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg6.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg7.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg7.png new file mode 100644 index 00000000..db41c74a Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg7.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg8.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg8.png new file mode 100644 index 00000000..34968130 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg8.png differ diff --git a/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg9.png b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg9.png new file mode 100644 index 00000000..4e7b4775 Binary files /dev/null and b/mes-framework/mes-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg9.png differ diff --git a/mes-framework/mes-spring-boot-starter-desensitize/pom.xml b/mes-framework/mes-spring-boot-starter-desensitize/pom.xml new file mode 100644 index 00000000..e4c8d685 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + mes-framework + com.chanko.yunxi + ${revision} + + + mes-spring-boot-starter-desensitize + 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏 + + + + com.chanko.yunxi + mes-common + + + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + + diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/base/annotation/DesensitizeBy.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/base/annotation/DesensitizeBy.java new file mode 100644 index 00000000..b351c624 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/base/annotation/DesensitizeBy.java @@ -0,0 +1,32 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.handler.DesensitizationHandler; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/base/handler/DesensitizationHandler.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/base/handler/DesensitizationHandler.java new file mode 100644 index 00000000..8b73d008 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/base/handler/DesensitizationHandler.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.base.handler; + +import java.lang.annotation.Annotation; + +/** + * 脱敏处理器接口 + * + * @author gaibu + */ +public interface DesensitizationHandler { + + /** + * 脱敏 + * + * @param origin 原始字符串 + * @param annotation 注解信息 + * @return 脱敏后的字符串 + */ + String desensitize(String origin, T annotation); + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java new file mode 100644 index 00000000..0abeaa6f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java @@ -0,0 +1,92 @@ +package com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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.getCurrentValue(); + Class currentValueClass = currentValue.getClass(); + return ReflectUtil.getField(currentValueClass, currentName); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/package-info.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/package-info.java new file mode 100644 index 00000000..2d1d8523 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏 + */ +package com.chanko.yunxi.mes.heli.framework.desensitize.core; diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/annotation/EmailDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/annotation/EmailDesensitize.java new file mode 100644 index 00000000..3af771e3 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/annotation/EmailDesensitize.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.regex.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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"; +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/annotation/RegexDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/annotation/RegexDesensitize.java new file mode 100644 index 00000000..8771eb0d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/annotation/RegexDesensitize.java @@ -0,0 +1,38 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.regex.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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 "******"; +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java new file mode 100644 index 00000000..1d94a569 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java @@ -0,0 +1,38 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.regex.handler; + +import com.chanko.yunxi.mes.heli.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) { + 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/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java new file mode 100644 index 00000000..96320c6e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.regex.handler; + +import com.chanko.yunxi.mes.heli.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(); + } +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java new file mode 100644 index 00000000..40ec22b8 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java @@ -0,0 +1,22 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.regex.handler; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/BankCardDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/BankCardDesensitize.java new file mode 100644 index 00000000..b53ee577 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/BankCardDesensitize.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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 "*"; + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java new file mode 100644 index 00000000..0f8b3466 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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 "*"; + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java new file mode 100644 index 00000000..e94d32d0 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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 "*"; + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java new file mode 100644 index 00000000..19ef8565 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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 "*"; + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/IdCardDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/IdCardDesensitize.java new file mode 100644 index 00000000..c1069ef6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/IdCardDesensitize.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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 "*"; + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/MobileDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/MobileDesensitize.java new file mode 100644 index 00000000..d53489e0 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/MobileDesensitize.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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 "*"; + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/PasswordDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/PasswordDesensitize.java new file mode 100644 index 00000000..7eb65872 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/PasswordDesensitize.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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 "*"; + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/SliderDesensitize.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/SliderDesensitize.java new file mode 100644 index 00000000..f62baeb1 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/annotation/SliderDesensitize.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.annotation; + +import com.chanko.yunxi.mes.heli.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.chanko.yunxi.mes.heli.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; +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java new file mode 100644 index 00000000..3459374a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java @@ -0,0 +1,78 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.handler; + +import com.chanko.yunxi.mes.heli.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) { + int prefixKeep = getPrefixKeep(annotation); + int suffixKeep = getSuffixKeep(annotation); + String replacer = getReplacer(annotation); + int length = origin.length(); + + // 情况一:原始字符串长度小于等于保留长度,则原始字符串全部替换 + if (prefixKeep >= length || suffixKeep >= length) { + return buildReplacerByLength(replacer, length); + } + + // 情况二:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换 + if ((prefixKeep + suffixKeep) >= length) { + return buildReplacerByLength(replacer, length); + } + + // 情况三:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串 + int interval = length - prefixKeep - suffixKeep; + return origin.substring(0, prefixKeep) + + buildReplacerByLength(replacer, interval) + + origin.substring(prefixKeep + interval); + } + + /** + * 根据长度循环构建替换符 + * + * @param replacer 替换符 + * @param length 长度 + * @return 构建后的替换符 + */ + private String buildReplacerByLength(String replacer, int length) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < length; i++) { + builder.append(replacer); + } + return builder.toString(); + } + + /** + * 前缀保留长度 + * + * @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/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/BankCardDesensitization.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/BankCardDesensitization.java new file mode 100644 index 00000000..b0f8f30d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/BankCardDesensitization.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.handler; + +import com.chanko.yunxi.mes.heli.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(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java new file mode 100644 index 00000000..c1a97bf2 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.handler; + +import com.chanko.yunxi.mes.heli.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(); + } +} diff --git a/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java new file mode 100644 index 00000000..a0b9dd8f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.handler; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java new file mode 100644 index 00000000..15854e38 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.handler; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java new file mode 100644 index 00000000..b768cd6b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.handler; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/IdCardDesensitization.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/IdCardDesensitization.java new file mode 100644 index 00000000..1037a568 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/IdCardDesensitization.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.handler; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/MobileDesensitization.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/MobileDesensitization.java new file mode 100644 index 00000000..c7c23fbf --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/MobileDesensitization.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.handler; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/PasswordDesensitization.java b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/PasswordDesensitization.java new file mode 100644 index 00000000..87336a20 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-desensitize/src/main/java/com/chanko/yunxi/mes/heli/framework/desensitize/core/slider/handler/PasswordDesensitization.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.desensitize.core.slider.handler; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-excel/pom.xml b/mes-framework/mes-spring-boot-starter-excel/pom.xml new file mode 100644 index 00000000..47ce7143 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-excel/pom.xml @@ -0,0 +1,51 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-excel + jar + + ${project.artifactId} + Excel 拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + com.chanko.yunxi + mes-spring-boot-starter-biz-dict + true + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + + com.alibaba + easyexcel + + + + diff --git a/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/annotations/DictFormat.java b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/annotations/DictFormat.java new file mode 100644 index 00000000..42ecc2f2 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/annotations/DictFormat.java @@ -0,0 +1,22 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/convert/DictConvert.java b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/convert/DictConvert.java new file mode 100644 index 00000000..3dc8b0a6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/convert/DictConvert.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.framework.excel.core.convert; + +import cn.hutool.core.convert.Convert; +import com.chanko.yunxi.mes.heli.framework.dict.core.util.DictFrameworkUtils; +import com.chanko.yunxi.mes.heli.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.getDictDataLabel(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/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/convert/JsonConvert.java b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/convert/JsonConvert.java new file mode 100644 index 00000000..fc37cf16 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/convert/JsonConvert.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.framework.excel.core.convert; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/convert/MoneyConvert.java b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/convert/MoneyConvert.java new file mode 100644 index 00000000..0f8d26e3 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/convert/MoneyConvert.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/util/ExcelUtils.java b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/util/ExcelUtils.java new file mode 100644 index 00000000..0a5678bf --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/core/util/ExcelUtils.java @@ -0,0 +1,51 @@ +package com.chanko.yunxi.mes.heli.framework.excel.core.util; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.converters.longconverter.LongStringConverter; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +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 宽度 + .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 + .sheet(sheetName).doWrite(data); + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); + 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/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/package-info.java b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/package-info.java new file mode 100644 index 00000000..1f52950e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-excel/src/main/java/com/chanko/yunxi/mes/heli/framework/excel/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 EasyExcel 实现 Excel 相关的操作 + */ +package com.chanko.yunxi.mes.heli.framework.excel; diff --git a/mes-framework/mes-spring-boot-starter-file/pom.xml b/mes-framework/mes-spring-boot-starter-file/pom.xml new file mode 100644 index 00000000..ac4c909d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/pom.xml @@ -0,0 +1,77 @@ + + + + mes-framework + com.chanko.yunxi + ${revision} + + 4.0.0 + mes-spring-boot-starter-file + + ${project.artifactId} + 文件客户端,支持多种存储器 + 1. file:本地磁盘 + 2. ftp:FTP 服务器 + 2. sftp:SFTP 服务器 + 4. db:数据库 + 5. s3:支持 S3 协议的云存储服务,例如说 MinIO、阿里云、华为云、腾讯云、七牛云等等 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.slf4j + slf4j-api + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + commons-net + commons-net + + + com.jcraft + jsch + + + + org.apache.tika + tika-core + + + + + io.minio + minio + + + + + diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/config/MesFileAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/config/MesFileAutoConfiguration.java new file mode 100644 index 00000000..c9022542 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/config/MesFileAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.framework.file.config; + +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientFactory; +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientFactoryImpl; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * 文件配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class MesFileAutoConfiguration { + + @Bean + public FileClientFactory fileClientFactory() { + return new FileClientFactoryImpl(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/AbstractFileClient.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/AbstractFileClient.java new file mode 100644 index 00000000..047235ce --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/AbstractFileClient.java @@ -0,0 +1,69 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClient.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClient.java new file mode 100644 index 00000000..0d002c9b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClient.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client; + +/** + * 文件客户端 + * + * @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; + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClientConfig.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClientConfig.java new file mode 100644 index 00000000..80c95ee8 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClientConfig.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClientFactory.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClientFactory.java new file mode 100644 index 00000000..1bc20547 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClientFactory.java @@ -0,0 +1,22 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client; + +public interface FileClientFactory { + + /** + * 获得文件客户端 + * + * @param configId 配置编号 + * @return 文件客户端 + */ + FileClient getFileClient(Long configId); + + /** + * 创建文件客户端 + * + * @param configId 配置编号 + * @param storage 存储器的枚举 {@link com.chanko.yunxi.mes.heli.framework.file.core.enums.FileStorageEnum} + * @param config 文件配置 + */ + void createOrUpdateFileClient(Long configId, Integer storage, Config config); + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClientFactoryImpl.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClientFactoryImpl.java new file mode 100644 index 00000000..d35e8f8a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/FileClientFactoryImpl.java @@ -0,0 +1,56 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/db/DBFileClient.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/db/DBFileClient.java new file mode 100644 index 00000000..f941cf28 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/db/DBFileClient.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.db; + +import cn.hutool.extra.spring.SpringUtil; +import com.chanko.yunxi.mes.heli.framework.file.core.client.AbstractFileClient; + +/** + * 基于 DB 存储的文件客户端的配置类 + * + * @author 芋道源码 + */ +public class DBFileClient extends AbstractFileClient { + + private DBFileContentFrameworkDAO dao; + + public DBFileClient(Long id, DBFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + } + + @Override + public String upload(byte[] content, String path, String type) { + getDao().insert(getId(), path, content); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + getDao().delete(getId(), path); + } + + @Override + public byte[] getContent(String path) { + return getDao().selectContent(getId(), path); + } + + private DBFileContentFrameworkDAO getDao() { + // 延迟获取,因为 SpringUtil 初始化太慢 + if (dao == null) { + dao = SpringUtil.getBean(DBFileContentFrameworkDAO.class); + } + return dao; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/db/DBFileClientConfig.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/db/DBFileClientConfig.java new file mode 100644 index 00000000..08500542 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/db/DBFileClientConfig.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.db; + +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; + +/** + * 基于 DB 存储的文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class DBFileClientConfig implements FileClientConfig { + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/db/DBFileContentFrameworkDAO.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/db/DBFileContentFrameworkDAO.java new file mode 100644 index 00000000..1b66ca60 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/db/DBFileContentFrameworkDAO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.db; + +/** + * 文件内容 Framework DAO 接口 + * + * @author 芋道源码 + */ +public interface DBFileContentFrameworkDAO { + + /** + * 插入文件内容 + * + * @param configId 配置编号 + * @param path 路径 + * @param content 内容 + */ + void insert(Long configId, String path, byte[] content); + + /** + * 删除文件内容 + * + * @param configId 配置编号 + * @param path 路径 + */ + void delete(Long configId, String path); + + /** + * 获得文件内容 + * + * @param configId 配置编号 + * @param path 路径 + * @return 内容 + */ + byte[] selectContent(Long configId, String path); + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/ftp/FtpFileClient.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/ftp/FtpFileClient.java new file mode 100644 index 00000000..b072560a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/ftp/FtpFileClient.java @@ -0,0 +1,77 @@ +package com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.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() { + // 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况 + config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH)); + // ftp的路径是 / 结尾 + if (!config.getBasePath().endsWith(StrUtil.SLASH)) { + config.setBasePath(config.getBasePath() + StrUtil.SLASH); + } + // 初始化 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); + ftp.reconnectIfTimeout(); + boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); + if (!success) { + throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); + } + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + ftp.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(); + ftp.reconnectIfTimeout(); + ftp.download(dir, fileName, out); + return out.toByteArray(); + } + + private String getFilePath(String path) { + return config.getBasePath() + path; + } + +} \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/ftp/FtpFileClientConfig.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/ftp/FtpFileClientConfig.java new file mode 100644 index 00000000..f273db23 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/ftp/FtpFileClientConfig.java @@ -0,0 +1,59 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.ftp; + +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.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/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/local/LocalFileClient.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/local/LocalFileClient.java new file mode 100644 index 00000000..acb5c626 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/local/LocalFileClient.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.local; + +import cn.hutool.core.io.FileUtil; +import com.chanko.yunxi.mes.heli.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() { + // 补全风格。例如说 Linux 是 /,Windows 是 \ + if (!config.getBasePath().endsWith(File.separator)) { + config.setBasePath(config.getBasePath() + File.separator); + } + } + + @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() + path; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/local/LocalFileClientConfig.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/local/LocalFileClientConfig.java new file mode 100644 index 00000000..6fba1b9f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/local/LocalFileClientConfig.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.local; + +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.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/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/s3/S3FileClient.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/s3/S3FileClient.java new file mode 100644 index 00000000..14cd6934 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/s3/S3FileClient.java @@ -0,0 +1,120 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.s3; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import com.chanko.yunxi.mes.heli.framework.file.core.client.AbstractFileClient; +import io.minio.*; + +import java.io.ByteArrayInputStream; + +import static com.chanko.yunxi.mes.heli.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN; +import static com.chanko.yunxi.mes.heli.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_TENCENT; + +/** + * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务 + *

+ * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库 + * + * @author 芋道源码 + */ +public class S3FileClient extends AbstractFileClient { + + private MinioClient client; + + public S3FileClient(Long id, S3FileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 补全 domain + if (StrUtil.isEmpty(config.getDomain())) { + config.setDomain(buildDomain()); + } + // 初始化客户端 + client = MinioClient.builder() + .endpoint(buildEndpointURL()) // Endpoint URL + .region(buildRegion()) // Region + .credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥 + .build(); + } + + /** + * 基于 endpoint 构建调用云服务的 URL 地址 + * + * @return URI 地址 + */ + private String buildEndpointURL() { + // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO + if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) { + return config.getEndpoint(); + } + return StrUtil.format("https://{}", config.getEndpoint()); + } + + /** + * 基于 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()); + } + + /** + * 基于 bucket 构建 region 地区 + * + * @return region 地区 + */ + private String buildRegion() { + // 阿里云必须有 region,否则会报错 + if (config.getEndpoint().contains(ENDPOINT_ALIYUN)) { + return StrUtil.subBefore(config.getEndpoint(), '.', false) + .replaceAll("-internal", "")// 去除内网 Endpoint 的后缀 + .replaceAll("https://", ""); + } + // 腾讯云必须有 region,否则会报错 + if (config.getEndpoint().contains(ENDPOINT_TENCENT)) { + return StrUtil.subAfter(config.getEndpoint(), "cos.", false) + .replaceAll("." + ENDPOINT_TENCENT, ""); // 去除 Endpoint + } + return null; + } + + @Override + public String upload(byte[] content, String path, String type) throws Exception { + // 执行上传 + client.putObject(PutObjectArgs.builder() + .bucket(config.getBucket()) // bucket 必须传递 + .contentType(type) + .object(path) // 相对路径作为 key + .stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容 + .build()); + // 拼接返回路径 + return config.getDomain() + "/" + path; + } + + @Override + public void delete(String path) throws Exception { + client.removeObject(RemoveObjectArgs.builder() + .bucket(config.getBucket()) // bucket 必须传递 + .object(path) // 相对路径作为 key + .build()); + } + + @Override + public byte[] getContent(String path) throws Exception { + GetObjectResponse response = client.getObject(GetObjectArgs.builder() + .bucket(config.getBucket()) // bucket 必须传递 + .object(path) // 相对路径作为 key + .build()); + return IoUtil.readBytes(response); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/s3/S3FileClientConfig.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/s3/S3FileClientConfig.java new file mode 100644 index 00000000..5ea21285 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/s3/S3FileClientConfig.java @@ -0,0 +1,77 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.s3; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientConfig; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.AssertTrue; +import javax.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"; + + /** + * 节点地址 + * 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://developer.huaweicloud.com/endpoint?OBS + */ + @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 + */ + @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 + */ + @NotNull(message = "accessKey 不能为空") + private String accessKey; + /** + * 访问 Secret + */ + @NotNull(message = "accessSecret 不能为空") + private String accessSecret; + + @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/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/sftp/SftpFileClient.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/sftp/SftpFileClient.java new file mode 100644 index 00000000..e5aef0f3 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/sftp/SftpFileClient.java @@ -0,0 +1,61 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.sftp; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.extra.ssh.Sftp; +import com.chanko.yunxi.mes.heli.framework.common.util.io.FileUtils; +import com.chanko.yunxi.mes.heli.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() { + // 补全风格。例如说 Linux 是 /,Windows 是 \ + if (!config.getBasePath().endsWith(File.separator)) { + config.setBasePath(config.getBasePath() + File.separator); + } + // 初始化 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); + sftp.upload(filePath, file); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + sftp.delFile(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + File destFile = FileUtils.createTempFile(); + sftp.download(filePath, destFile); + return FileUtil.readBytes(destFile); + } + + private String getFilePath(String path) { + return config.getBasePath() + path; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/sftp/SftpFileClientConfig.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/sftp/SftpFileClientConfig.java new file mode 100644 index 00000000..bce41906 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/client/sftp/SftpFileClientConfig.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.client.sftp; + +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.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/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/enums/FileStorageEnum.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/enums/FileStorageEnum.java new file mode 100644 index 00000000..7c0b36ea --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/enums/FileStorageEnum.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClient; +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientConfig; +import com.chanko.yunxi.mes.heli.framework.file.core.client.db.DBFileClient; +import com.chanko.yunxi.mes.heli.framework.file.core.client.db.DBFileClientConfig; +import com.chanko.yunxi.mes.heli.framework.file.core.client.ftp.FtpFileClient; +import com.chanko.yunxi.mes.heli.framework.file.core.client.ftp.FtpFileClientConfig; +import com.chanko.yunxi.mes.heli.framework.file.core.client.local.LocalFileClient; +import com.chanko.yunxi.mes.heli.framework.file.core.client.local.LocalFileClientConfig; +import com.chanko.yunxi.mes.heli.framework.file.core.client.s3.S3FileClient; +import com.chanko.yunxi.mes.heli.framework.file.core.client.s3.S3FileClientConfig; +import com.chanko.yunxi.mes.heli.framework.file.core.client.sftp.SftpFileClient; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/utils/FileTypeUtils.java b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/utils/FileTypeUtils.java new file mode 100644 index 00000000..551866ac --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/java/com/chanko/yunxi/mes/heli/framework/file/core/utils/FileTypeUtils.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.framework.file.core.utils; + +import com.alibaba.ttl.TransmittableThreadLocal; +import lombok.SneakyThrows; +import org.apache.tika.Tika; + +/** + * 文件类型 Utils + * + * @author 芋道源码 + */ +public class FileTypeUtils { + + private static final ThreadLocal TIKA = TransmittableThreadLocal.withInitial(Tika::new); + + /** + * 获得文件的 mineType,对于doc,jar等文件会有误差 + * + * @param data 文件内容 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + @SneakyThrows + public static String getMineType(byte[] data) { + return TIKA.get().detect(data); + } + + /** + * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用jar文件时,通过名字更为准确 + * + * @param name 文件名 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + public static String getMineType(String name) { + return TIKA.get().detect(name); + } + + /** + * 在拥有文件和数据的情况下,最好使用此方法,最为准确 + * + * @param data 文件内容 + * @param name 文件名 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + public static String getMineType(byte[] data, String name) { + return TIKA.get().detect(data, name); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..ac878bd5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.file.config.MesFileAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-job/pom.xml b/mes-framework/mes-spring-boot-starter-job/pom.xml new file mode 100644 index 00000000..380ab16b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/pom.xml @@ -0,0 +1,41 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-job + jar + + ${project.artifactId} + 任务拓展 + 1. 定时任务,基于 Quartz 拓展 + 2. 异步任务,基于 Spring Async 拓展 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter-quartz + + + + + jakarta.validation + jakarta.validation-api + + + + + diff --git a/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/config/MesAsyncAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/config/MesAsyncAutoConfiguration.java new file mode 100644 index 00000000..1df6090a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/config/MesAsyncAutoConfiguration.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.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; + +/** + * 异步任务 Configuration + */ +@AutoConfiguration +@EnableAsync +public class MesAsyncAutoConfiguration { + + @Bean + public BeanPostProcessor threadPoolTaskExecutorBeanPostProcessor() { + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof ThreadPoolTaskExecutor)) { + return bean; + } + // 修改提交的任务,接入 TransmittableThreadLocal + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; + executor.setTaskDecorator(TtlRunnable::get); + return executor; + } + + }; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/config/MesQuartzAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/config/MesQuartzAutoConfiguration.java new file mode 100644 index 00000000..3f3d5072 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/config/MesQuartzAutoConfiguration.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.framework.quartz.config; + +import com.chanko.yunxi.mes.heli.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 MesQuartzAutoConfiguration { + + @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/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/enums/JobDataKeyEnum.java b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/enums/JobDataKeyEnum.java new file mode 100644 index 00000000..1befb279 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/enums/JobDataKeyEnum.java @@ -0,0 +1,14 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/handler/JobHandler.java b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/handler/JobHandler.java new file mode 100644 index 00000000..4f043bf6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/handler/JobHandler.java @@ -0,0 +1,19 @@ +package com.chanko.yunxi.mes.heli.framework.quartz.core.handler; + +/** + * 任务处理器 + * + * @author 芋道源码 + */ +public interface JobHandler { + + /** + * 执行任务 + * + * @param param 参数 + * @return 结果 + * @throws Exception 异常 + */ + String execute(String param) throws Exception; + +} diff --git a/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/handler/JobHandlerInvoker.java b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/handler/JobHandlerInvoker.java new file mode 100644 index 00000000..99b763ac --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/handler/JobHandlerInvoker.java @@ -0,0 +1,114 @@ +package com.chanko.yunxi.mes.heli.framework.quartz.core.handler; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; +import com.chanko.yunxi.mes.heli.framework.quartz.core.enums.JobDataKeyEnum; +import com.chanko.yunxi.mes.heli.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 javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +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/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/scheduler/SchedulerManager.java b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/scheduler/SchedulerManager.java new file mode 100644 index 00000000..4b628cb7 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/scheduler/SchedulerManager.java @@ -0,0 +1,146 @@ +package com.chanko.yunxi.mes.heli.framework.quartz.core.scheduler; + +import com.chanko.yunxi.mes.heli.framework.quartz.core.enums.JobDataKeyEnum; +import com.chanko.yunxi.mes.heli.framework.quartz.core.handler.JobHandlerInvoker; +import org.quartz.*; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; +import static com.chanko.yunxi.mes.heli.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); + // 新增调度 + 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(); + 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/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/service/JobLogFrameworkService.java b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/service/JobLogFrameworkService.java new file mode 100644 index 00000000..92c50c87 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/service/JobLogFrameworkService.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.framework.quartz.core.service; + +import javax.validation.constraints.NotEmpty; +import javax.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/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/util/CronUtils.java b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/util/CronUtils.java new file mode 100644 index 00000000..6c6f83b1 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/core/util/CronUtils.java @@ -0,0 +1,56 @@ +package com.chanko.yunxi.mes.heli.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) { + // 获得 CronExpression 对象 + CronExpression cron; + try { + cron = new CronExpression(cronExpression); + } catch (ParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + // 从当前开始计算,n 个满足条件的 + Date now = new Date(); + List nextTimes = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + Date nextTime = cron.getNextValidTimeAfter(now); + nextTimes.add(LocalDateTimeUtil.of(nextTime)); + // 切换现在,为下一个触发时间; + now = nextTime; + } + return nextTimes; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/package-info.java b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/package-info.java new file mode 100644 index 00000000..fe71c02a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/java/com/chanko/yunxi/mes/heli/framework/quartz/package-info.java @@ -0,0 +1,7 @@ +/** + * 1. 定时任务,采用 Quartz 实现进程内的任务执行。 + * 考虑到高可用,使用 Quartz 自带的 MySQL 集群方案。 + * + * 2. 异步任务,采用 Spring Async 异步执行。 + */ +package com.chanko.yunxi.mes.heli.framework.quartz; diff --git a/mes-framework/mes-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..e5c927c2 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.chanko.yunxi.mes.heli.framework.quartz.config.MesQuartzAutoConfiguration +com.chanko.yunxi.mes.heli.framework.quartz.config.MesAsyncAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md b/mes-framework/mes-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md new file mode 100644 index 00000000..bf6441cf --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md b/mes-framework/mes-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md new file mode 100644 index 00000000..687b80ee --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-monitor/pom.xml b/mes-framework/mes-spring-boot-starter-monitor/pom.xml new file mode 100644 index 00000000..efc25a48 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/pom.xml @@ -0,0 +1,73 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-monitor + jar + + ${project.artifactId} + 服务监控,提供链路追踪、日志服务、指标收集等等功能 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-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/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/config/MesMetricsAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/config/MesMetricsAutoConfiguration.java new file mode 100644 index 00000000..2a53f76e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/config/MesMetricsAutoConfiguration.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.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 = "mes.metrics", value = "enable", matchIfMissing = true) // 允许使用 mes.metrics.enable=false 禁用 Metrics +public class MesMetricsAutoConfiguration { + + @Bean + public MeterRegistryCustomizer metricsCommonTags( + @Value("${spring.application.name}") String applicationName) { + return registry -> registry.config().commonTags("application", applicationName); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/config/MesTracerAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/config/MesTracerAutoConfiguration.java new file mode 100644 index 00000000..04c7f96d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/config/MesTracerAutoConfiguration.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.framework.tracer.config; + +import com.chanko.yunxi.mes.heli.framework.common.enums.WebFilterOrderEnum; +import com.chanko.yunxi.mes.heli.framework.tracer.core.aop.BizTraceAspect; +import com.chanko.yunxi.mes.heli.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 = "mes.tracer", value = "enable", matchIfMissing = true) +public class MesTracerAutoConfiguration { + + // 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/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/config/TracerProperties.java b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/config/TracerProperties.java new file mode 100644 index 00000000..944f4b44 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/config/TracerProperties.java @@ -0,0 +1,14 @@ +package com.chanko.yunxi.mes.heli.framework.tracer.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * BizTracer配置类 + * + * @author 麻薯 + */ +@ConfigurationProperties("mes.tracer") +@Data +public class TracerProperties { +} diff --git a/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/annotation/BizTrace.java b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/annotation/BizTrace.java new file mode 100644 index 00000000..866c557b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/annotation/BizTrace.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/aop/BizTraceAspect.java b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/aop/BizTraceAspect.java new file mode 100644 index 00000000..15b5c567 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/aop/BizTraceAspect.java @@ -0,0 +1,77 @@ +package com.chanko.yunxi.mes.heli.framework.tracer.core.aop; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.tracer.core.annotation.BizTrace; +import com.chanko.yunxi.mes.heli.framework.common.util.spring.SpringExpressionUtils; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/filter/TraceFilter.java b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/filter/TraceFilter.java new file mode 100644 index 00000000..9f4b0442 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/filter/TraceFilter.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.framework.tracer.core.filter; + +import com.chanko.yunxi.mes.heli.framework.common.util.monitor.TracerUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.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/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/util/TracerFrameworkUtils.java b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/util/TracerFrameworkUtils.java new file mode 100644 index 00000000..c4e62c6b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/core/util/TracerFrameworkUtils.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/package-info.java b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/package-info.java new file mode 100644 index 00000000..dea096ef --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/src/main/java/com/chanko/yunxi/mes/heli/framework/tracer/package-info.java @@ -0,0 +1,6 @@ +/** + * 使用 SkyWalking 组件,作为链路追踪、日志中心。 + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.framework.tracer; diff --git a/mes-framework/mes-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..650de652 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.chanko.yunxi.mes.heli.framework.tracer.config.MesTracerAutoConfiguration +com.chanko.yunxi.mes.heli.framework.tracer.config.MesMetricsAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/mes-framework/mes-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md new file mode 100644 index 00000000..03cd141a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md b/mes-framework/mes-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md new file mode 100644 index 00000000..5a9d459e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md b/mes-framework/mes-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md new file mode 100644 index 00000000..8363454a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-mq/pom.xml b/mes-framework/mes-spring-boot-starter-mq/pom.xml new file mode 100644 index 00000000..c9ca4755 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/pom.xml @@ -0,0 +1,43 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-mq + jar + + ${project.artifactId} + 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + com.chanko.yunxi + mes-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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/package-info.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/package-info.java new file mode 100644 index 00000000..87626baa --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种 + */ +package com.chanko.yunxi.mes.heli.framework.mq; diff --git a/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/rabbitmq/config/MesRabbitMQAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/rabbitmq/config/MesRabbitMQAutoConfiguration.java new file mode 100644 index 00000000..9e5519cd --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/rabbitmq/config/MesRabbitMQAutoConfiguration.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.framework.mq.rabbitmq.config; + +import cn.hutool.core.util.ReflectUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.utils.SerializationUtils; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + +import java.lang.reflect.Field; + +/** + * RabbitMQ 消息队列配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@Slf4j +@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") +public class MesRabbitMQAutoConfiguration { + + static { + // 强制设置 SerializationUtils 的 TRUST_ALL 为 true,避免 RabbitMQ Consumer 反序列化消息报错 + // 为什么不通过设置 spring.amqp.deserialization.trust.all 呢?因为可能在 SerializationUtils static 初始化后 + Field trustAllField = ReflectUtil.getField(SerializationUtils.class, "TRUST_ALL"); + ReflectUtil.removeFinalModify(trustAllField); + ReflectUtil.setFieldValue(SerializationUtils.class, trustAllField, true); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/rabbitmq/core/package-info.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/rabbitmq/core/package-info.java new file mode 100644 index 00000000..e582e87e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/rabbitmq/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,无特殊逻辑 + */ +package com.chanko.yunxi.mes.heli.framework.mq.rabbitmq.core; \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/rabbitmq/package-info.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/rabbitmq/package-info.java new file mode 100644 index 00000000..3ff09b40 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/rabbitmq/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列,基于 RabbitMQ 提供 + */ +package com.chanko.yunxi.mes.heli.framework.mq.rabbitmq; diff --git a/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/config/MesRedisMQConsumerAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/config/MesRedisMQConsumerAutoConfiguration.java new file mode 100644 index 00000000..94a135eb --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/config/MesRedisMQConsumerAutoConfiguration.java @@ -0,0 +1,151 @@ +package com.chanko.yunxi.mes.heli.framework.mq.redis.config; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.system.SystemUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.DocumentEnum; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.RedisMQTemplate; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.job.RedisPendingMessageResendJob; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; +import com.chanko.yunxi.mes.heli.framework.redis.config.MesRedisAutoConfiguration; +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 = MesRedisAutoConfiguration.class) +public class MesRedisMQConsumerAutoConfiguration { + + /** + * 创建 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 集群消费的容器 + * + * 基础知识: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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/config/MesRedisMQProducerAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/config/MesRedisMQProducerAutoConfiguration.java new file mode 100644 index 00000000..ebc7f07f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/config/MesRedisMQProducerAutoConfiguration.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.framework.mq.redis.config; + +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.RedisMQTemplate; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import com.chanko.yunxi.mes.heli.framework.redis.config.MesRedisAutoConfiguration; +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 = MesRedisAutoConfiguration.class) +public class MesRedisMQProducerAutoConfiguration { + + @Bean + public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate, + List interceptors) { + RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate); + // 添加拦截器 + interceptors.forEach(redisMQTemplate::addInterceptor); + return redisMQTemplate; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/RedisMQTemplate.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/RedisMQTemplate.java new file mode 100644 index 00000000..1d4cf80a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/RedisMQTemplate.java @@ -0,0 +1,87 @@ +package com.chanko.yunxi.mes.heli.framework.mq.redis.core; + +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.message.AbstractRedisMessage; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java new file mode 100644 index 00000000..97d0672b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/interceptor/RedisMessageInterceptor.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.framework.mq.redis.core.interceptor; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/job/RedisPendingMessageResendJob.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/job/RedisPendingMessageResendJob.java new file mode 100644 index 00000000..c7bfab1b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/job/RedisPendingMessageResendJob.java @@ -0,0 +1,100 @@ +package com.chanko.yunxi.mes.heli.framework.mq.redis.core.job; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.RedisMQTemplate; +import com.chanko.yunxi.mes.heli.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:pending:msg: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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/message/AbstractRedisMessage.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/message/AbstractRedisMessage.java new file mode 100644 index 00000000..6a543790 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/message/AbstractRedisMessage.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java new file mode 100644 index 00000000..13ba72e9 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/pubsub/AbstractRedisChannelMessage.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.framework.mq.redis.core.pubsub; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java new file mode 100644 index 00000000..83931b6f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/pubsub/AbstractRedisChannelMessageListener.java @@ -0,0 +1,103 @@ +package com.chanko.yunxi.mes.heli.framework.mq.redis.core.pubsub; + +import cn.hutool.core.util.TypeUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.RedisMQTemplate; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java new file mode 100644 index 00000000..ae806128 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/stream/AbstractRedisStreamMessage.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.framework.mq.redis.core.stream; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java new file mode 100644 index 00000000..1db471f6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java @@ -0,0 +1,113 @@ +package com.chanko.yunxi.mes.heli.framework.mq.redis.core.stream; + +import cn.hutool.core.util.TypeUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.RedisMQTemplate; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.interceptor.RedisMessageInterceptor; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/package-info.java b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/package-info.java new file mode 100644 index 00000000..f99b4c33 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/java/com/chanko/yunxi/mes/heli/framework/mq/redis/package-info.java @@ -0,0 +1,6 @@ +/** + * 消息队列,基于 Redis 提供: + * 1. 基于 Pub/Sub 实现广播消费 + * 2. 基于 Stream 实现集群消费 + */ +package com.chanko.yunxi.mes.heli.framework.mq.redis; diff --git a/mes-framework/mes-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..b5eeb139 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +com.chanko.yunxi.mes.heli.framework.mq.redis.config.MesRedisMQProducerAutoConfiguration +com.chanko.yunxi.mes.heli.framework.mq.redis.config.MesRedisMQConsumerAutoConfiguration +com.chanko.yunxi.mes.heli.framework.mq.rabbitmq.config.MesRabbitMQAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 事件机制 Event 入门》.md b/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 事件机制 Event 入门》.md new file mode 100644 index 00000000..8a599c14 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 事件机制 Event 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 Kafka 入门》.md b/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 Kafka 入门》.md new file mode 100644 index 00000000..017cb070 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 Kafka 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RabbitMQ 入门》.md b/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RabbitMQ 入门》.md new file mode 100644 index 00000000..dffc516e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RabbitMQ 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RocketMQ 入门》.md b/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RocketMQ 入门》.md new file mode 100644 index 00000000..8a599c14 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mq/《芋道 Spring Boot 消息队列 RocketMQ 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-mybatis/pom.xml b/mes-framework/mes-spring-boot-starter-mybatis/pom.xml new file mode 100644 index 00000000..3f52616d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/pom.xml @@ -0,0 +1,76 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-mybatis + jar + + ${project.artifactId} + 数据库连接池、多数据源、事务、MyBatis 拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + com.chanko.yunxi + mes-spring-boot-starter-web + 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 + + + + com.alibaba + druid-spring-boot-starter + + + com.baomidou + mybatis-plus-boot-starter + + + com.baomidou + dynamic-datasource-spring-boot-starter + + + + com.github.yulichang + mybatis-plus-join-boot-starter + + + + diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/config/MesDataSourceAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/config/MesDataSourceAutoConfiguration.java new file mode 100644 index 00000000..7034e76b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/config/MesDataSourceAutoConfiguration.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.framework.datasource.config; + +import com.chanko.yunxi.mes.heli.framework.datasource.core.filter.DruidAdRemoveFilter; +import com.alibaba.druid.spring.boot.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 MesDataSourceAutoConfiguration { + + /** + * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 + */ + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/core/enums/DataSourceEnum.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/core/enums/DataSourceEnum.java new file mode 100644 index 00000000..6b3f36e5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/core/enums/DataSourceEnum.java @@ -0,0 +1,22 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/core/filter/DruidAdRemoveFilter.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/core/filter/DruidAdRemoveFilter.java new file mode 100644 index 00000000..756eb592 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/core/filter/DruidAdRemoveFilter.java @@ -0,0 +1,38 @@ +package com.chanko.yunxi.mes.heli.framework.datasource.core.filter; + +import com.alibaba.druid.util.Utils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/package-info.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/package-info.java new file mode 100644 index 00000000..178b791e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/datasource/package-info.java @@ -0,0 +1,5 @@ +/** + * 数据库连接池,采用 Druid + * 多数据源,采用爆米花 + */ +package com.chanko.yunxi.mes.heli.framework.datasource; diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java new file mode 100644 index 00000000..4c49766f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java @@ -0,0 +1,108 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.config; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.SetUtils; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.enums.SqlConstants; +import com.chanko.yunxi.mes.heli.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); + + // 初始化 SQL 静态变量 + SqlConstants.init(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 等直接自增的数据库 + 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; + } + // 设置 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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/config/MesMybatisAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/config/MesMybatisAutoConfiguration.java new file mode 100644 index 00000000..c07b0476 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/config/MesMybatisAutoConfiguration.java @@ -0,0 +1,63 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.config; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.handler.DefaultDBFieldHandler; +import com.baomidou.mybatisplus.annotation.DbType; +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.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; + +/** + * MyBaits 配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@MapperScan(value = "${mes.info.base-package}", annotationClass = Mapper.class, + lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试 +public class MesMybatisAutoConfiguration { + + @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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/dataobject/BaseDO.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/dataobject/BaseDO.java new file mode 100644 index 00000000..f77c1515 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/dataobject/BaseDO.java @@ -0,0 +1,50 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; +import org.apache.ibatis.type.JdbcType; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 基础实体对象 + * + * @author 芋道源码 + */ +@Data +public abstract class BaseDO implements Serializable { + + /** + * 创建时间 + */ + @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; + +} diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/enums/SqlConstants.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/enums/SqlConstants.java new file mode 100644 index 00000000..638ec11f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/enums/SqlConstants.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.enums; + +import com.baomidou.mybatisplus.annotation.DbType; + +/** + * SQL相关常量类 + * + * @author 芋道源码 + */ +public class SqlConstants { + + /** + * 数据库的类型 + */ + public static DbType DB_TYPE; + + public static void init(DbType dbType) { + DB_TYPE = dbType; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/handler/DefaultDBFieldHandler.java new file mode 100644 index 00000000..1d7d21d9 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/handler/DefaultDBFieldHandler.java @@ -0,0 +1,62 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.handler; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.web.core.util.WebFrameworkUtils; +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 = WebFrameworkUtils.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 = WebFrameworkUtils.getLoginUserId(); + if (Objects.nonNull(userId) && Objects.isNull(modifier)) { + setFieldValByName("updater", userId.toString(), metaObject); + } + } +} diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/mapper/BaseMapperX.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/mapper/BaseMapperX.java new file mode 100644 index 00000000..47465e36 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/mapper/BaseMapperX.java @@ -0,0 +1,166 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.util.MyBatisUtils; +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 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(PageParam pageParam, @Param("ew") Wrapper queryWrapper) { + // 特殊:不分页,直接查询全部 + if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { + List list = selectList(queryWrapper); + return new PageResult<>(list, (long) list.size()); + } + + // MyBatis Plus 查询 + IPage mpPage = MyBatisUtils.buildPage(pageParam); + selectPage(mpPage, queryWrapper); + // 转换返回 + 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)); + } + + 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)); + } + + @Deprecated + default List selectList(SFunction leField, SFunction geField, Object value) { + return selectList(new LambdaQueryWrapper().le(leField, value).ge(geField, value)); + } + + 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) { + return Db.saveBatch(entities); + } + + /** + * 批量插入,适合大量数据插入 + * + * @param entities 实体们 + * @param size 插入数量 Db.saveBatch 默认为 1000 + */ + default Boolean insertBatch(Collection entities, int size) { + 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 Boolean insertOrUpdate(T entity) { + return Db.saveOrUpdate(entity); + } + + default Boolean insertOrUpdateBatch(Collection collection) { + return Db.saveOrUpdateBatch(collection); + } + + 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)); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/query/LambdaQueryWrapperX.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/query/LambdaQueryWrapperX.java new file mode 100644 index 00000000..45273fbd --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/query/LambdaQueryWrapperX.java @@ -0,0 +1,135 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.query; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/query/MPJLambdaWrapperX.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/query/MPJLambdaWrapperX.java new file mode 100644 index 00000000..de000bb9 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/query/MPJLambdaWrapperX.java @@ -0,0 +1,313 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.query; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.github.yulichang.toolkit.MPJWrappers; +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 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class MPJLambdaWrapperX extends MPJLambdaWrapper { + + public MPJLambdaWrapperX likeIfPresent(SFunction column, String val) { + MPJWrappers.lambdaJoin().like(column, 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 val1, Object val2) { + if (val1 != null && val2 != null) { + return (MPJLambdaWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (MPJLambdaWrapperX) ge(column, val1); + } + if (val2 != null) { + return (MPJLambdaWrapperX) le(column, val2); + } + 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); + } + + // ========== 重写父类方法,方便链式调用 ========== + + @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; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/query/QueryWrapperX.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/query/QueryWrapperX.java new file mode 100644 index 00000000..72cc3785 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/query/QueryWrapperX.java @@ -0,0 +1,166 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.query; + +import cn.hutool.core.lang.Assert; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.enums.SqlConstants; +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) { + Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型"); + switch (SqlConstants.DB_TYPE) { + case ORACLE: + case ORACLE_12C: + super.eq("ROWNUM", n); + break; + case SQL_SERVER: + case SQL_SERVER2005: + super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段 + break; + default: + super.last("LIMIT " + n); + } + return this; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/EncryptTypeHandler.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/EncryptTypeHandler.java new file mode 100644 index 00000000..ce17a341 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/EncryptTypeHandler.java @@ -0,0 +1,75 @@ +package com.chanko.yunxi.mes.heli.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 cn.hutool.crypto.symmetric.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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/IntegerListTypeHandler.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/IntegerListTypeHandler.java new file mode 100644 index 00000000..b8d182bf --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/IntegerListTypeHandler.java @@ -0,0 +1,56 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/JsonLongSetTypeHandler.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/JsonLongSetTypeHandler.java new file mode 100644 index 00000000..66e00701 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/JsonLongSetTypeHandler.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.type; + +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.fasterxml.jackson.core.type.TypeReference; + +import java.util.Set; + +/** + * 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现 + * 在我们将字符串反序列化为 Set 并且泛型为 Long 时,如果每个元素的数值太小,会被处理成 Integer 类型,导致可能存在隐性的 BUG。 + * + * 例如说哦,SysUserDO 的 postIds 属性 + * + * @author 芋道源码 + */ +public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference>(){}; + + @Override + protected Object parse(String json) { + return JsonUtils.parseObject(json, TYPE_REFERENCE); + } + + @Override + protected String toJson(Object obj) { + return JsonUtils.toJsonString(obj); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/LongListTypeHandler.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/LongListTypeHandler.java new file mode 100644 index 00000000..c7e9bdd9 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/LongListTypeHandler.java @@ -0,0 +1,57 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/StringListTypeHandler.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/StringListTypeHandler.java new file mode 100644 index 00000000..bb50bc6c --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/type/StringListTypeHandler.java @@ -0,0 +1,58 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/util/JdbcUtils.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/util/JdbcUtils.java new file mode 100644 index 00000000..33564dfb --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/util/JdbcUtils.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.util; + +import com.baomidou.mybatisplus.annotation.DbType; + +import java.sql.Connection; +import java.sql.DriverManager; + +/** + * 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) { + String name = com.alibaba.druid.util.JdbcUtils.getDbType(url, null); + return DbType.getDbType(name); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/util/MyBatisUtils.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/util/MyBatisUtils.java new file mode 100644 index 00000000..94cbb40f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/core/util/MyBatisUtils.java @@ -0,0 +1,88 @@ +package com.chanko.yunxi.mes.heli.framework.mybatis.core.util; + +import cn.hutool.core.collection.CollectionUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.SortingField; +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; +import java.util.stream.Collectors; + +/** + * 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 (!CollectionUtil.isEmpty(sortingFields)) { + page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? + OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField())) + .collect(Collectors.toList())); + } + return page; + } + + /** + * 将拦截器添加到链中 + * 由于 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); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/package-info.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/package-info.java new file mode 100644 index 00000000..120811c1 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/mybatis/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 MyBatis Plus 提升使用 MyBatis 的开发效率 + */ +package com.chanko.yunxi.mes.heli.framework.mybatis; diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/package-info.java b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/package-info.java new file mode 100644 index 00000000..15548329 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/java/com/chanko/yunxi/mes/heli/framework/package-info.java @@ -0,0 +1 @@ +package com.chanko.yunxi.mes.heli.framework; diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories b/mes-framework/mes-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..156ba1cd --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ + com.chanko.yunxi.mes.heli.framework.mybatis.config.IdTypeEnvironmentPostProcessor diff --git a/mes-framework/mes-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..8afa02f5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.chanko.yunxi.mes.heli.framework.datasource.config.MesDataSourceAutoConfiguration +com.chanko.yunxi.mes.heli.framework.mybatis.config.MesMybatisAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md b/mes-framework/mes-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md new file mode 100644 index 00000000..4ad6844a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md b/mes-framework/mes-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md new file mode 100644 index 00000000..346cc61a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md b/mes-framework/mes-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md new file mode 100644 index 00000000..e2b82dc5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-protection/pom.xml b/mes-framework/mes-spring-boot-starter-protection/pom.xml new file mode 100644 index 00000000..4b3b0745 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/pom.xml @@ -0,0 +1,39 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-protection + jar + + ${project.artifactId} + 服务保证,提供分布式锁、幂等、限流、熔断等等功能 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + com.chanko.yunxi + mes-spring-boot-starter-redis + + + + + com.baomidou + lock4j-redisson-spring-boot-starter + true + + + + io.github.resilience4j + resilience4j-spring-boot2 + true + + + + diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/config/MesIdempotentConfiguration.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/config/MesIdempotentConfiguration.java new file mode 100644 index 00000000..8e7f59cd --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/config/MesIdempotentConfiguration.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.framework.idempotent.config; + +import com.chanko.yunxi.mes.heli.framework.idempotent.core.aop.IdempotentAspect; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.redis.IdempotentRedisDAO; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import com.chanko.yunxi.mes.heli.framework.redis.config.MesRedisAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.List; + +@AutoConfiguration(after = MesRedisAutoConfiguration.class) +public class MesIdempotentConfiguration { + + @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 ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { + return new ExpressionIdempotentKeyResolver(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/annotation/Idempotent.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/annotation/Idempotent.java new file mode 100644 index 00000000..502e2951 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/annotation/Idempotent.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.framework.idempotent.core.annotation; + +import com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver.IdempotentKeyResolver; + +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 解析器 + */ + Class keyResolver() default DefaultIdempotentKeyResolver.class; + /** + * 使用的 Key 参数 + */ + String keyArg() default ""; + +} diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/aop/IdempotentAspect.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/aop/IdempotentAspect.java new file mode 100644 index 00000000..b7d87e2a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/aop/IdempotentAspect.java @@ -0,0 +1,56 @@ +package com.chanko.yunxi.mes.heli.framework.idempotent.core.aop; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.annotation.Idempotent; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.redis.IdempotentRedisDAO; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +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 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; + } + + @Before("@annotation(idempotent)") + public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) { + // 获得 IdempotentKeyResolver + IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); + Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); + // 解析 Key + String key = keyResolver.resolver(joinPoint, idempotent); + + // 锁定 Key。 + boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); + // 锁定失败,抛出异常 + if (!success) { + log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); + throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); + } + } + +} diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java new file mode 100644 index 00000000..41cd332b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java @@ -0,0 +1,22 @@ +package com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java new file mode 100644 index 00000000..ce1a9dd2 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.annotation.Idempotent; +import com.chanko.yunxi.mes.heli.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 = StrUtil.join(",", joinPoint.getArgs()); + return SecureUtil.md5(methodName + argsStr); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java new file mode 100644 index 00000000..d265ea19 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java @@ -0,0 +1,63 @@ +package com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.core.util.ArrayUtil; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.annotation.Idempotent; +import com.chanko.yunxi.mes.heli.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +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 LocalVariableTableParameterNameDiscoverer(); + 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/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/redis/IdempotentRedisDAO.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/redis/IdempotentRedisDAO.java new file mode 100644 index 00000000..329916d5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/core/redis/IdempotentRedisDAO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.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); + } + + private static String formatKey(String key) { + return String.format(IDEMPOTENT, key); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/package-info.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/idempotent/package-info.java new file mode 100644 index 00000000..959621a7 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/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 com.chanko.yunxi.mes.heli.framework.idempotent; diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/config/MesLock4jConfiguration.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/config/MesLock4jConfiguration.java new file mode 100644 index 00000000..98b0bb58 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/config/MesLock4jConfiguration.java @@ -0,0 +1,18 @@ +package com.chanko.yunxi.mes.heli.framework.lock4j.config; + +import com.chanko.yunxi.mes.heli.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 MesLock4jConfiguration { + + @Bean + public DefaultLockFailureStrategy lockFailureStrategy() { + return new DefaultLockFailureStrategy(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/core/DefaultLockFailureStrategy.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/core/DefaultLockFailureStrategy.java new file mode 100644 index 00000000..afd816f6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/core/DefaultLockFailureStrategy.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.framework.lock4j.core; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/core/Lock4jRedisKeyConstants.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/core/Lock4jRedisKeyConstants.java new file mode 100644 index 00000000..0bd291d8 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/core/Lock4jRedisKeyConstants.java @@ -0,0 +1,19 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/package-info.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/package-info.java new file mode 100644 index 00000000..8925146e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/lock4j/package-info.java @@ -0,0 +1,4 @@ +/** + * 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目 + */ +package com.chanko.yunxi.mes.heli.framework.lock4j; diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/resilience4j/package-info.java b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/resilience4j/package-info.java new file mode 100644 index 00000000..357f1881 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/resilience4j/package-info.java @@ -0,0 +1,9 @@ +/** + * 使用 Resilience4j 组件,实现服务保障,包括: + * 1. 熔断器 + * 2. 限流器 + * 3. 舱壁隔离 + * 4. 重试 + * 5. 限时器 + */ +package com.chanko.yunxi.mes.heli.framework.resilience4j; diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md new file mode 100644 index 00000000..8f664a4b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/java/com/chanko/yunxi/mes/heli/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..25df4372 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.chanko.yunxi.mes.heli.framework.idempotent.config.MesIdempotentConfiguration +com.chanko.yunxi.mes.heli.framework.lock4j.config.MesLock4jConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-redis/pom.xml b/mes-framework/mes-spring-boot-starter-redis/pom.xml new file mode 100644 index 00000000..c6ac959c --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-redis/pom.xml @@ -0,0 +1,41 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-redis + jar + + ${project.artifactId} + Redis 封装拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + org.redisson + redisson-spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-cache + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + diff --git a/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/config/MesCacheAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/config/MesCacheAutoConfiguration.java new file mode 100644 index 00000000..3ddf471f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/config/MesCacheAutoConfiguration.java @@ -0,0 +1,82 @@ +package com.chanko.yunxi.mes.heli.framework.redis.config; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.redis.config.MesRedisAutoConfiguration.buildRedisSerializer; + +/** + * Cache 配置类,基于 Redis 实现 + */ +@AutoConfiguration +@EnableConfigurationProperties({CacheProperties.class, MesCacheProperties.class}) +@EnableCaching +public class MesCacheAutoConfiguration { + + /** + * 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/mes-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, + MesCacheProperties mesCacheProperties) { + // 创建 RedisCacheWriter 对象 + RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, + BatchStrategies.scan(mesCacheProperties.getRedisScanBatchSize())); + // 创建 TenantRedisCacheManager 对象 + return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/config/MesCacheProperties.java b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/config/MesCacheProperties.java new file mode 100644 index 00000000..744e29ff --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/config/MesCacheProperties.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.framework.redis.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * Cache 配置项 + * + * @author Wanwan + */ +@ConfigurationProperties("mes.cache") +@Data +@Validated +public class MesCacheProperties { + + /** + * {@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/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/config/MesRedisAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/config/MesRedisAutoConfiguration.java new file mode 100644 index 00000000..a42355f1 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/config/MesRedisAutoConfiguration.java @@ -0,0 +1,44 @@ +package com.chanko.yunxi.mes.heli.framework.redis.config; + +import cn.hutool.core.util.ReflectUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +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 +public class MesRedisAutoConfiguration { + + /** + * 创建 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/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/core/TimeoutRedisCacheManager.java b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/core/TimeoutRedisCacheManager.java new file mode 100644 index 00000000..d8f7f69d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/core/TimeoutRedisCacheManager.java @@ -0,0 +1,83 @@ +package com.chanko.yunxi.mes.heli.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) { + // 移除 # 后面的 : 以及后面的内容,避免影响解析 + names[1] = StrUtil.subBefore(names[1], StrUtil.COLON, false); + // 解析时间 + Duration duration = parseDuration(names[1]); + cacheConfig = cacheConfig.entryTtl(duration); + } + return super.createRedisCache(name, 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/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/package-info.java b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/package-info.java new file mode 100644 index 00000000..142f1d09 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-redis/src/main/java/com/chanko/yunxi/mes/heli/framework/redis/package-info.java @@ -0,0 +1,4 @@ +/** + * 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端 + */ +package com.chanko.yunxi.mes.heli.framework.redis; diff --git a/mes-framework/mes-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..ec124a83 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.chanko.yunxi.mes.heli.framework.redis.config.MesRedisAutoConfiguration +com.chanko.yunxi.mes.heli.framework.redis.config.MesCacheAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md b/mes-framework/mes-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md new file mode 100644 index 00000000..85d6e8a7 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md b/mes-framework/mes-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md new file mode 100644 index 00000000..abade9b3 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-security/pom.xml b/mes-framework/mes-spring-boot-starter-security/pom.xml new file mode 100644 index 00000000..9ebf340b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/pom.xml @@ -0,0 +1,61 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-security + jar + + ${project.artifactId} + 用户的认证、权限的校验 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.chanko.yunxi + mes-spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.google.guava + guava + + + + + com.chanko.yunxi + mes-module-system-api + ${revision} + + + + diff --git a/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/AuthorizeRequestsCustomizer.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/AuthorizeRequestsCustomizer.java new file mode 100644 index 00000000..a8c962ee --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/AuthorizeRequestsCustomizer.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.framework.security.config; + +import com.chanko.yunxi.mes.heli.framework.web.config.WebProperties; +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.ExpressionUrlAuthorizationConfigurer; + +import javax.annotation.Resource; + +/** + * 自定义的 URL 的安全配置 + * 目的:每个 Maven Module 可以自定义规则! + * + * @author 芋道源码 + */ +public abstract class AuthorizeRequestsCustomizer + implements Customizer.ExpressionInterceptUrlRegistry>, 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/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/MesSecurityAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/MesSecurityAutoConfiguration.java new file mode 100644 index 00000000..67b9bdce --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/MesSecurityAutoConfiguration.java @@ -0,0 +1,102 @@ +package com.chanko.yunxi.mes.heli.framework.security.config; + +import com.chanko.yunxi.mes.heli.framework.security.core.aop.PreAuthenticatedAspect; +import com.chanko.yunxi.mes.heli.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; +import com.chanko.yunxi.mes.heli.framework.security.core.filter.TokenAuthenticationFilter; +import com.chanko.yunxi.mes.heli.framework.security.core.handler.AccessDeniedHandlerImpl; +import com.chanko.yunxi.mes.heli.framework.security.core.handler.AuthenticationEntryPointImpl; +import com.chanko.yunxi.mes.heli.framework.security.core.service.SecurityFrameworkService; +import com.chanko.yunxi.mes.heli.framework.security.core.service.SecurityFrameworkServiceImpl; +import com.chanko.yunxi.mes.heli.framework.web.core.handler.GlobalExceptionHandler; +import com.chanko.yunxi.mes.heli.module.system.api.oauth2.OAuth2TokenApi; +import com.chanko.yunxi.mes.heli.module.system.api.permission.PermissionApi; +import org.springframework.beans.factory.config.MethodInvokingFactoryBean; +import org.springframework.boot.autoconfigure.AutoConfiguration; +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; + +import javax.annotation.Resource; + +/** + * Spring Security 自动配置类,主要用于相关组件的配置 + * + * 注意,不能和 {@link MesWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。 + * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。 + * + * @author 芋道源码 + */ +@AutoConfiguration +@EnableConfigurationProperties(SecurityProperties.class) +public class MesSecurityAutoConfiguration { + + @Resource + private SecurityProperties securityProperties; + + /** + * 处理用户未登录拦截的切面的 Bean + */ + @Bean + public PreAuthenticatedAspect preAuthenticatedAspect() { + return new PreAuthenticatedAspect(); + } + + /** + * 认证失败处理类 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, + OAuth2TokenApi oauth2TokenApi) { + return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi); + } + + @Bean("ss") // 使用 Spring Security 的缩写,方便使用 + public SecurityFrameworkService securityFrameworkService(PermissionApi 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/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/MesWebSecurityConfigurerAdapter.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/MesWebSecurityConfigurerAdapter.java new file mode 100644 index 00000000..1c10dd40 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/MesWebSecurityConfigurerAdapter.java @@ -0,0 +1,197 @@ +package com.chanko.yunxi.mes.heli.framework.security.config; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.security.core.filter.TokenAuthenticationFilter; +import com.chanko.yunxi.mes.heli.framework.web.config.WebProperties; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import org.springframework.boot.autoconfigure.AutoConfiguration; +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.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +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 javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 自定义的 Spring Security 配置适配器实现 + * + * @author 芋道源码 + */ +@AutoConfiguration +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class MesWebSecurityConfigurerAdapter { + + @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().and() + // CSRF 禁用,因为不使用 Session + .csrf().disable() + // 基于 token 机制,所以不需要 Session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .headers().frameOptions().disable().and() + // 一堆自定义的 Spring Security 处理器 + .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler); + // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 + + // 获得 @PermitAll 带来的 URL 列表,免登录 + Multimap permitAllUrls = getPermitAllUrlsFromAnnotations(); + // 设置每个请求的权限 + httpSecurity + // ①:全局共享规则 + .authorizeRequests() + // 1.1 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() + // 1.2 设置 @PermitAll 无需认证 + .antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() + // 1.3 基于 mes.security.permit-all-urls 无需认证 + .antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() + // 1.4 设置 App API 无需认证 + .antMatchers(buildAppApi("/**")).permitAll() + // 1.5 验证码captcha 允许匿名访问 + .antMatchers("/captcha/get", "/captcha/check").permitAll() + // ②:每个项目的自定义规则 + .and().authorizeRequests(registry -> // 下面,循环设置自定义规则 + authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) + // ③:兜底规则,必须认证 + .authorizeRequests() + .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; + } + if (entry.getKey().getPatternsCondition() == null) { + continue; + } + Set urls = entry.getKey().getPatternsCondition().getPatterns(); + // 特殊:使用 @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); + 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; + } + }); + } + return result; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/SecurityProperties.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/SecurityProperties.java new file mode 100644 index 00000000..38ad1adf --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/config/SecurityProperties.java @@ -0,0 +1,51 @@ +package com.chanko.yunxi.mes.heli.framework.security.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.List; + +@ConfigurationProperties(prefix = "mes.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/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/LoginUser.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/LoginUser.java new file mode 100644 index 00000000..bc113d88 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/LoginUser.java @@ -0,0 +1,59 @@ +package com.chanko.yunxi.mes.heli.framework.security.core; + +import cn.hutool.core.map.MapUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 登录用户信息 + * + * @author 芋道源码 + */ +@Data +public class LoginUser { + + /** + * 用户编号 + */ + private Long id; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围 + */ + private List scopes; + + // ========== 上下文 ========== + /** + * 上下文字段,不进行持久化 + * + * 1. 用于基于 LoginUser 维度的临时缓存 + */ + @JsonIgnore + private Map context; + + 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/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/annotations/PreAuthenticated.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/annotations/PreAuthenticated.java new file mode 100644 index 00000000..b004eb57 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/annotations/PreAuthenticated.java @@ -0,0 +1,17 @@ +package com.chanko.yunxi.mes.heli.framework.security.core.annotations; + +import java.lang.annotation.*; + +/** + * 声明用户需要登录 + * + * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登录 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface PreAuthenticated { +} diff --git a/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/aop/PreAuthenticatedAspect.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/aop/PreAuthenticatedAspect.java new file mode 100644 index 00000000..02453a85 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/aop/PreAuthenticatedAspect.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.security.core.aop; + +import com.chanko.yunxi.mes.heli.framework.security.core.annotations.PreAuthenticated; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; + +@Aspect +@Slf4j +public class PreAuthenticatedAspect { + + @Around("@annotation(preAuthenticated)") + public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable { + if (SecurityFrameworkUtils.getLoginUser() == null) { + throw exception(UNAUTHORIZED); + } + return joinPoint.proceed(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java new file mode 100644 index 00000000..dc7179bd --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/filter/TokenAuthenticationFilter.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 00000000..5e9d2abf --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,117 @@ +package com.chanko.yunxi.mes.heli.framework.security.core.filter; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.security.config.SecurityProperties; +import com.chanko.yunxi.mes.heli.framework.security.core.LoginUser; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; +import com.chanko.yunxi.mes.heli.framework.web.core.handler.GlobalExceptionHandler; +import com.chanko.yunxi.mes.heli.framework.web.core.util.WebFrameworkUtils; +import com.chanko.yunxi.mes.heli.module.system.api.oauth2.OAuth2TokenApi; +import com.chanko.yunxi.mes.heli.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +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 OAuth2TokenApi 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()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()); + } 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/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/handler/AccessDeniedHandlerImpl.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 00000000..80663a20 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.framework.security.core.handler; + +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; +import com.chanko.yunxi.mes.heli.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 org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; +import static com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; + +/** + * 访问一个需要认证的 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/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/handler/AuthenticationEntryPointImpl.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 00000000..e212f39c --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.framework.security.core.handler; + +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.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 javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/service/SecurityFrameworkService.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/service/SecurityFrameworkService.java new file mode 100644 index 00000000..4046e675 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/service/SecurityFrameworkService.java @@ -0,0 +1,59 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/service/SecurityFrameworkServiceImpl.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/service/SecurityFrameworkServiceImpl.java new file mode 100644 index 00000000..788edfa5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/service/SecurityFrameworkServiceImpl.java @@ -0,0 +1,57 @@ +package com.chanko.yunxi.mes.heli.framework.security.core.service; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.security.core.LoginUser; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; +import com.chanko.yunxi.mes.heli.module.system.api.permission.PermissionApi; +import lombok.AllArgsConstructor; + +import java.util.Arrays; + +import static com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 默认的 {@link SecurityFrameworkService} 实现类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { + + private final PermissionApi permissionApi; + + @Override + public boolean hasPermission(String permission) { + return hasAnyPermissions(permission); + } + + @Override + public boolean hasAnyPermissions(String... permissions) { + return permissionApi.hasAnyPermissions(getLoginUserId(), permissions); + } + + @Override + public boolean hasRole(String role) { + return hasAnyRoles(role); + } + + @Override + public boolean hasAnyRoles(String... roles) { + return permissionApi.hasAnyRoles(getLoginUserId(), roles); + } + + @Override + public boolean hasScope(String scope) { + return hasAnyScopes(scope); + } + + @Override + public boolean hasAnyScopes(String... scope) { + LoginUser user = SecurityFrameworkUtils.getLoginUser(); + if (user == null) { + return false; + } + return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope)); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/util/SecurityFrameworkUtils.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/util/SecurityFrameworkUtils.java new file mode 100644 index 00000000..32b12530 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/core/util/SecurityFrameworkUtils.java @@ -0,0 +1,117 @@ +package com.chanko.yunxi.mes.heli.framework.security.core.util; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.security.core.LoginUser; +import com.chanko.yunxi.mes.heli.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 javax.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; + } + + /** + * 设置当前用户 + * + * @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 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 + 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; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/package-info.java b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/package-info.java new file mode 100644 index 00000000..3ed7cad7 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/java/com/chanko/yunxi/mes/heli/framework/security/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于 Spring Security 框架 + * 实现安全认证功能 + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.framework.security; diff --git a/mes-framework/mes-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..32eae92d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.chanko.yunxi.mes.heli.framework.security.config.MesSecurityAutoConfiguration +com.chanko.yunxi.mes.heli.framework.security.config.MesWebSecurityConfigurerAdapter \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md b/mes-framework/mes-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md new file mode 100644 index 00000000..93359ff3 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md @@ -0,0 +1,2 @@ +* 芋道 Spring Security 入门: +* Spring Security 基本概念: diff --git a/mes-framework/mes-spring-boot-starter-web/pom.xml b/mes-framework/mes-spring-boot-starter-web/pom.xml new file mode 100644 index 00000000..171dc70f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/pom.xml @@ -0,0 +1,66 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-web + jar + + ${project.artifactId} + Web 框架,全局异常、API 日志等 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + com.github.xiaoymin + knife4j-openapi3-spring-boot-starter + + + org.springdoc + springdoc-openapi-ui + + + + org.springframework.security + spring-security-core + provided + + + + + com.chanko.yunxi + mes-module-infra-api + ${revision} + + + + + org.jsoup + jsoup + + + + + diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/config/MesApiLogAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/config/MesApiLogAutoConfiguration.java new file mode 100644 index 00000000..d7f00005 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/config/MesApiLogAutoConfiguration.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.framework.apilog.config; + +import com.chanko.yunxi.mes.heli.framework.apilog.core.filter.ApiAccessLogFilter; +import com.chanko.yunxi.mes.heli.framework.apilog.core.service.ApiAccessLogFrameworkService; +import com.chanko.yunxi.mes.heli.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl; +import com.chanko.yunxi.mes.heli.framework.apilog.core.service.ApiErrorLogFrameworkService; +import com.chanko.yunxi.mes.heli.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl; +import com.chanko.yunxi.mes.heli.framework.common.enums.WebFilterOrderEnum; +import com.chanko.yunxi.mes.heli.framework.web.config.WebProperties; +import com.chanko.yunxi.mes.heli.framework.web.config.MesWebAutoConfiguration; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.ApiAccessLogApi; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.ApiErrorLogApi; +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 javax.servlet.Filter; + +@AutoConfiguration(after = MesWebAutoConfiguration.class) +public class MesApiLogAutoConfiguration { + + @Bean + public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) { + return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi); + } + + @Bean + public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) { + return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi); + } + + /** + * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 + */ + @Bean + @ConditionalOnProperty(prefix = "mes.access-log", value = "enable", matchIfMissing = true) // 允许使用 mes.access-log.enable=false 禁用访问日志 + public FilterRegistrationBean apiAccessLogFilter(WebProperties webProperties, + @Value("${spring.application.name}") String applicationName, + ApiAccessLogFrameworkService apiAccessLogFrameworkService) { + ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService); + 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; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/filter/ApiAccessLogFilter.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/filter/ApiAccessLogFilter.java new file mode 100644 index 00000000..905a3157 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/filter/ApiAccessLogFilter.java @@ -0,0 +1,110 @@ +package com.chanko.yunxi.mes.heli.framework.apilog.core.filter; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import com.chanko.yunxi.mes.heli.framework.apilog.core.service.ApiAccessLog; +import com.chanko.yunxi.mes.heli.framework.apilog.core.service.ApiAccessLogFrameworkService; +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.monitor.TracerUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.web.config.WebProperties; +import com.chanko.yunxi.mes.heli.framework.web.core.filter.ApiRequestFilter; +import com.chanko.yunxi.mes.heli.framework.web.core.util.WebFrameworkUtils; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils.toJsonString; + +/** + * API 访问日志 Filter + * + * @author 芋道源码 + */ +@Slf4j +public class ApiAccessLogFilter extends ApiRequestFilter { + + private final String applicationName; + + private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; + + public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) { + super(webProperties); + this.applicationName = applicationName; + this.apiAccessLogFrameworkService = apiAccessLogFrameworkService; + } + + @Override + 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) { + ApiAccessLog accessLog = new ApiAccessLog(); + try { + this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex); + apiAccessLogFrameworkService.createApiAccessLog(accessLog); + } catch (Throwable th) { + log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th); + } + } + + private void buildApiAccessLogDTO(ApiAccessLog accessLog, HttpServletRequest request, LocalDateTime beginTime, + Map queryString, String requestBody, Exception ex) { + // 处理用户信息 + accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置访问结果 + CommonResult result = WebFrameworkUtils.getCommonResult(request); + if (result != null) { + accessLog.setResultCode(result.getCode()); + accessLog.setResultMsg(result.getMsg()); + } else if (ex != null) { + accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()); + accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); + } else { + accessLog.setResultCode(0); + accessLog.setResultMsg(""); + } + // 设置其它字段 + accessLog.setTraceId(TracerUtils.getTraceId()); + accessLog.setApplicationName(applicationName); + accessLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder().put("query", queryString).put("body", requestBody).build(); + accessLog.setRequestParams(toJsonString(requestParams)); + accessLog.setRequestMethod(request.getMethod()); + accessLog.setUserAgent(ServletUtils.getUserAgent(request)); + accessLog.setUserIp(ServletUtils.getClientIP(request)); + // 持续时间 + accessLog.setBeginTime(beginTime); + accessLog.setEndTime(LocalDateTime.now()); + accessLog.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS)); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiAccessLog.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiAccessLog.java new file mode 100644 index 00000000..6ec8e00e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiAccessLog.java @@ -0,0 +1,85 @@ +package com.chanko.yunxi.mes.heli.framework.apilog.core.service; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 访问日志 + * + * @author 芋道源码 + */ +@Data +public class ApiAccessLog { + + /** + * 链路追踪编号 + */ + 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 beginTime; + /** + * 结束请求时间 + */ + @NotNull(message = "结束请求时间不能为空") + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + /** + * 结果码 + */ + @NotNull(message = "错误码不能为空") + private Integer resultCode; + /** + * 结果提示 + */ + private String resultMsg; + +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiAccessLogFrameworkService.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiAccessLogFrameworkService.java new file mode 100644 index 00000000..c330b86a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiAccessLogFrameworkService.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.framework.apilog.core.service; + +/** + * API 访问日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogFrameworkService { + + /** + * 创建 API 访问日志 + * + * @param apiAccessLog API 访问日志 + */ + void createApiAccessLog(ApiAccessLog apiAccessLog); +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java new file mode 100644 index 00000000..07c06455 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.framework.apilog.core.service; + +import cn.hutool.core.bean.BeanUtil; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.ApiAccessLogApi; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; + +/** + * API 访问日志 Framework Service 实现类 + * + * 基于 {@link ApiAccessLogApi} 服务,记录访问日志 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService { + + private final ApiAccessLogApi apiAccessLogApi; + + @Override + @Async + public void createApiAccessLog(ApiAccessLog apiAccessLog) { + ApiAccessLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiAccessLog, ApiAccessLogCreateReqDTO.class); + apiAccessLogApi.createApiAccessLog(reqDTO); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiErrorLog.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiErrorLog.java new file mode 100644 index 00000000..bc8b0d12 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiErrorLog.java @@ -0,0 +1,107 @@ +package com.chanko.yunxi.mes.heli.framework.apilog.core.service; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 错误日志 + * + * @author 芋道源码 + */ +@Data +public class ApiErrorLog { + + /** + * 链路编号 + */ + 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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiErrorLogFrameworkService.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiErrorLogFrameworkService.java new file mode 100644 index 00000000..e4f95200 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiErrorLogFrameworkService.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.framework.apilog.core.service; + +/** + * API 错误日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogFrameworkService { + + /** + * 创建 API 错误日志 + * + * @param apiErrorLog API 错误日志 + */ + void createApiErrorLog(ApiErrorLog apiErrorLog); +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java new file mode 100644 index 00000000..cd547fdf --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.framework.apilog.core.service; + +import cn.hutool.core.bean.BeanUtil; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.ApiErrorLogApi; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; + +/** + * API 错误日志 Framework Service 实现类 + * + * 基于 {@link ApiErrorLogApi} 服务,记录错误日志 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService { + + private final ApiErrorLogApi apiErrorLogApi; + + @Override + @Async + public void createApiErrorLog(ApiErrorLog apiErrorLog) { + ApiErrorLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiErrorLog, ApiErrorLogCreateReqDTO.class); + apiErrorLogApi.createApiErrorLog(reqDTO); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/package-info.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/package-info.java new file mode 100644 index 00000000..394125b6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/apilog/package-info.java @@ -0,0 +1,8 @@ +/** + * API 日志:包含两类 + * 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。 + * 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。 + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.framework.apilog; diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/config/MesJacksonAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/config/MesJacksonAutoConfiguration.java new file mode 100644 index 00000000..41df6cde --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/config/MesJacksonAutoConfiguration.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.framework.jackson.config; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.jackson.core.databind.LocalDateTimeDeserializer; +import com.chanko.yunxi.mes.heli.framework.jackson.core.databind.LocalDateTimeSerializer; +import com.chanko.yunxi.mes.heli.framework.jackson.core.databind.NumberSerializer; +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 MesJacksonAutoConfiguration { + + @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 序列化、反序列化规则 + .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE) + .addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/databind/LocalDateTimeDeserializer.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/databind/LocalDateTimeDeserializer.java new file mode 100644 index 00000000..422789b4 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/databind/LocalDateTimeDeserializer.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.framework.jackson.core.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反序列化规则 + *

+ * 会将毫秒级时间戳反序列化为LocalDateTime + */ +public class LocalDateTimeDeserializer extends JsonDeserializer { + + public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); + } +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/databind/LocalDateTimeSerializer.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/databind/LocalDateTimeSerializer.java new file mode 100644 index 00000000..d9f32b19 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/databind/LocalDateTimeSerializer.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.framework.jackson.core.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序列化规则 + *

+ * 会将LocalDateTime序列化为毫秒级时间戳 + */ +public class LocalDateTimeSerializer extends JsonSerializer { + + public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer(); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/databind/NumberSerializer.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/databind/NumberSerializer.java new file mode 100644 index 00000000..4a2ae87f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/databind/NumberSerializer.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.framework.jackson.core.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/package-info.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/package-info.java new file mode 100644 index 00000000..e89f841e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/jackson/core/package-info.java @@ -0,0 +1 @@ +package com.chanko.yunxi.mes.heli.framework.jackson.core; diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/package-info.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/package-info.java new file mode 100644 index 00000000..92f07f51 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/package-info.java @@ -0,0 +1,4 @@ +/** + * Web 框架,全局异常、API 日志等 + */ +package com.chanko.yunxi.mes.heli.framework; diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/swagger/config/MesSwaggerAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/swagger/config/MesSwaggerAutoConfiguration.java new file mode 100644 index 00000000..bf192f96 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/swagger/config/MesSwaggerAutoConfiguration.java @@ -0,0 +1,155 @@ +package com.chanko.yunxi.mes.heli.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.*; +import org.springdoc.core.customizers.OpenApiBuilderCustomizer; +import org.springdoc.core.customizers.ServerBaseUrlCustomizer; +import org.springdoc.core.providers.JavadocProvider; +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.http.HttpHeaders; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.chanko.yunxi.mes.heli.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 MesSwaggerAutoConfiguration { + + // ========== 全局 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 + 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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/swagger/config/SwaggerProperties.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/swagger/config/SwaggerProperties.java new file mode 100644 index 00000000..2b266e14 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/swagger/config/SwaggerProperties.java @@ -0,0 +1,60 @@ +package com.chanko.yunxi.mes.heli.framework.swagger.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import javax.validation.constraints.NotEmpty; + +/** + * Swagger 配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties("mes.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/swagger/package-info.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/swagger/package-info.java new file mode 100644 index 00000000..58182c48 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/swagger/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于 Swagger + Knife4j 实现 API 接口文档 + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.framework.swagger; diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/config/MesWebAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/config/MesWebAutoConfiguration.java new file mode 100644 index 00000000..3a8b7336 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/config/MesWebAutoConfiguration.java @@ -0,0 +1,128 @@ +package com.chanko.yunxi.mes.heli.framework.web.config; + +import com.chanko.yunxi.mes.heli.framework.apilog.core.service.ApiErrorLogFrameworkService; +import com.chanko.yunxi.mes.heli.framework.common.enums.WebFilterOrderEnum; +import com.chanko.yunxi.mes.heli.framework.web.core.filter.CacheRequestBodyFilter; +import com.chanko.yunxi.mes.heli.framework.web.core.filter.DemoFilter; +import com.chanko.yunxi.mes.heli.framework.web.core.handler.GlobalExceptionHandler; +import com.chanko.yunxi.mes.heli.framework.web.core.handler.GlobalResponseBodyHandler; +import com.chanko.yunxi.mes.heli.framework.web.core.util.WebFrameworkUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +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; + +import javax.annotation.Resource; +import javax.servlet.Filter; + +@AutoConfiguration +@EnableConfigurationProperties(WebProperties.class) +public class MesWebAutoConfiguration 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 + public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) { + return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService); + } + + @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 = "mes.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 + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.build(); + } +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/config/WebProperties.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/config/WebProperties.java new file mode 100644 index 00000000..9beb04df --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/config/WebProperties.java @@ -0,0 +1,66 @@ +package com.chanko.yunxi.mes.heli.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 javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@ConfigurationProperties(prefix = "mes.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 MesWebAutoConfiguration#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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/ApiRequestFilter.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/ApiRequestFilter.java new file mode 100644 index 00000000..621fe604 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/ApiRequestFilter.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.framework.web.core.filter; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.web.config.WebProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.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 请求的地址 + return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(), + webProperties.getAppApi().getPrefix()); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/CacheRequestBodyFilter.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/CacheRequestBodyFilter.java new file mode 100644 index 00000000..e952547b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/CacheRequestBodyFilter.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.framework.web.core.filter; + +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/CacheRequestBodyWrapper.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/CacheRequestBodyWrapper.java new file mode 100644 index 00000000..3bc78c45 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/CacheRequestBodyWrapper.java @@ -0,0 +1,68 @@ +package com.chanko.yunxi.mes.heli.framework.web.core.filter; + +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/DemoFilter.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/DemoFilter.java new file mode 100644 index 00000000..bbcb2643 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/filter/DemoFilter.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.framework.web.core.filter; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.web.core.util.WebFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/handler/GlobalExceptionHandler.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/handler/GlobalExceptionHandler.java new file mode 100644 index 00000000..fde781f5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/handler/GlobalExceptionHandler.java @@ -0,0 +1,325 @@ +package com.chanko.yunxi.mes.heli.framework.web.core.handler; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.apilog.core.service.ApiErrorLog; +import com.chanko.yunxi.mes.heli.framework.apilog.core.service.ApiErrorLogFrameworkService; +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.monitor.TracerUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.web.core.util.WebFrameworkUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.Assert; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +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 javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.ValidationException; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Objects; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants.*; + +/** + * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 + * + * @author 芋道源码 + */ +@RestControllerAdvice +@AllArgsConstructor +@Slf4j +public class GlobalExceptionHandler { + + private final String applicationName; + + private final ApiErrorLogFrameworkService apiErrorLogFrameworkService; + + /** + * 处理所有异常,主要是提供给 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(request, (NoHandlerFoundException) 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("[missingServletRequestParameterExceptionHandler]", 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); + FieldError fieldError = ex.getBindingResult().getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 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())); + } + + /** + * 处理 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(HttpServletRequest req, NoHandlerFoundException ex) { + log.warn("[noHandlerFoundExceptionHandler]", ex); + return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); + } + + /** + * 处理 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())); + } + + /** + * 处理 Resilience4j 限流抛出的异常 + */ + public CommonResult requestNotPermittedExceptionHandler(HttpServletRequest req, Throwable ex) { + log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex); + return CommonResult.error(TOO_MANY_REQUESTS); + } + + /** + * 处理 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) { + log.info("[serviceExceptionHandler]", ex); + 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; + } + + // 情况二:部分特殊的库的处理 + if (Objects.equals("io.github.resilience4j.ratelimiter.RequestNotPermitted", ex.getClass().getName())) { + return requestNotPermittedExceptionHandler(req, ex); + } + + // 情况三:处理异常 + log.error("[defaultExceptionHandler]", ex); + // 插入异常日志 + this.createExceptionLog(req, ex); + // 返回 ERROR CommonResult + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + + private void createExceptionLog(HttpServletRequest req, Throwable e) { + // 插入错误日志 + ApiErrorLog errorLog = new ApiErrorLog(); + try { + // 初始化 errorLog + initExceptionLog(errorLog, req, e); + // 执行插入 errorLog + apiErrorLogFrameworkService.createApiErrorLog(errorLog); + } catch (Throwable th) { + log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); + } + } + + private void initExceptionLog(ApiErrorLog 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(ExceptionUtils.getStackTrace(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", ServletUtils.getParamMap(request)) + .put("body", ServletUtils.getBody(request)).build(); + errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + errorLog.setRequestMethod(request.getMethod()); + errorLog.setUserAgent(ServletUtils.getUserAgent(request)); + errorLog.setUserIp(ServletUtils.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("[报表模块 mes-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[报表模块 mes-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]"); + } + // 2. 工作流 + if (message.contains("bpm_")) { + log.error("[工作流模块 mes-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[工作流模块 mes-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]"); + } + // 3. 微信公众号 + if (message.contains("mp_")) { + log.error("[微信公众号 mes-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[微信公众号 mes-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + } + // 4. 商城系统 + if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) { + log.error("[商城系统 mes-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[商城系统 mes-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + } + // 5. 支付平台 + if (message.contains("pay_")) { + log.error("[支付模块 mes-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[支付模块 mes-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + } + return null; + } + +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/handler/GlobalResponseBodyHandler.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/handler/GlobalResponseBodyHandler.java new file mode 100644 index 00000000..be65959c --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/handler/GlobalResponseBodyHandler.java @@ -0,0 +1,45 @@ +package com.chanko.yunxi.mes.heli.framework.web.core.handler; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/util/WebFrameworkUtils.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/util/WebFrameworkUtils.java new file mode 100644 index 00000000..beb24192 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/core/util/WebFrameworkUtils.java @@ -0,0 +1,127 @@ +package com.chanko.yunxi.mes.heli.framework.web.core.util; + +import cn.hutool.core.util.NumberUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.web.config.WebProperties; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +/** + * 专属于 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"; + + 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; + } + + 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 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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/package-info.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/package-info.java new file mode 100644 index 00000000..0e0fc583 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * 针对 SpringMVC 的基础封装 + */ +package com.chanko.yunxi.mes.heli.framework.web; diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/config/MesXssAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/config/MesXssAutoConfiguration.java new file mode 100644 index 00000000..727e8d6d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/config/MesXssAutoConfiguration.java @@ -0,0 +1,61 @@ +package com.chanko.yunxi.mes.heli.framework.xss.config; + +import com.chanko.yunxi.mes.heli.framework.common.enums.WebFilterOrderEnum; +import com.chanko.yunxi.mes.heli.framework.xss.core.clean.JsoupXssCleaner; +import com.chanko.yunxi.mes.heli.framework.xss.core.clean.XssCleaner; +import com.chanko.yunxi.mes.heli.framework.xss.core.filter.XssFilter; +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.web.config.MesWebAutoConfiguration.createFilterBean; + +@AutoConfiguration +@EnableConfigurationProperties(XssProperties.class) +@ConditionalOnProperty(prefix = "mes.xss", name = "enable", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 +public class MesXssAutoConfiguration 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 = "mes.xss.enable", havingValue = "true") + public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner) { + // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理 + return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/config/XssProperties.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/config/XssProperties.java new file mode 100644 index 00000000..d162cc1d --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/config/XssProperties.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.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 = "mes.xss") +@Validated +@Data +public class XssProperties { + + /** + * 是否开启,默认为 true + */ + private boolean enable = true; + /** + * 需要排除的 URL,默认为空 + */ + private List excludeUrls = Collections.emptyList(); + +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/clean/JsoupXssCleaner.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/clean/JsoupXssCleaner.java new file mode 100644 index 00000000..3c3fecfb --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/clean/JsoupXssCleaner.java @@ -0,0 +1,64 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/clean/XssCleaner.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/clean/XssCleaner.java new file mode 100644 index 00000000..343abf71 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/clean/XssCleaner.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.framework.xss.core.clean; + +/** + * 对 html 文本中的有 Xss 风险的数据进行清理 + */ +public interface XssCleaner { + + /** + * 清理有 Xss 风险的文本 + * + * @param html 原 html + * @return 清理后的 html + */ + String clean(String html); + +} diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/filter/XssFilter.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/filter/XssFilter.java new file mode 100644 index 00000000..63949a80 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/filter/XssFilter.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.framework.xss.core.filter; + +import com.chanko.yunxi.mes.heli.framework.xss.config.XssProperties; +import com.chanko.yunxi.mes.heli.framework.xss.core.clean.XssCleaner; +import lombok.AllArgsConstructor; +import org.springframework.util.PathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/filter/XssRequestWrapper.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/filter/XssRequestWrapper.java new file mode 100644 index 00000000..fd5079b0 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/filter/XssRequestWrapper.java @@ -0,0 +1,92 @@ +package com.chanko.yunxi.mes.heli.framework.xss.core.filter; + +import com.chanko.yunxi.mes.heli.framework.xss.core.clean.XssCleaner; + +import javax.servlet.http.HttpServletRequest; +import javax.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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/json/XssStringJsonDeserializer.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/json/XssStringJsonDeserializer.java new file mode 100644 index 00000000..2ceebd63 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/core/json/XssStringJsonDeserializer.java @@ -0,0 +1,59 @@ +package com.chanko.yunxi.mes.heli.framework.xss.core.json; + +import com.chanko.yunxi.mes.heli.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 lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; + +/** + * XSS 过滤 jackson 反序列化器。 + * 在反序列化的过程中,会对字符串进行 XSS 过滤。 + * + * @author Hccake + */ +@Slf4j +@AllArgsConstructor +public class XssStringJsonDeserializer extends StringDeserializer { + + private final XssCleaner xssCleaner; + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + 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/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/package-info.java b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/package-info.java new file mode 100644 index 00000000..28530074 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/java/com/chanko/yunxi/mes/heli/framework/xss/package-info.java @@ -0,0 +1,6 @@ +/** + * 针对 XSS 的基础封装 + * + * XSS 说明:https://tech.meituan.com/2018/09/27/fe-security.html + */ +package com.chanko.yunxi.mes.heli.framework.xss; diff --git a/mes-framework/mes-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..e96f22a2 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,5 @@ +com.chanko.yunxi.mes.heli.framework.apilog.config.MesApiLogAutoConfiguration +com.chanko.yunxi.mes.heli.framework.jackson.config.MesJacksonAutoConfiguration +com.chanko.yunxi.mes.heli.framework.swagger.config.MesSwaggerAutoConfiguration +com.chanko.yunxi.mes.heli.framework.web.config.MesWebAutoConfiguration +com.chanko.yunxi.mes.heli.framework.xss.config.MesXssAutoConfiguration diff --git a/mes-framework/mes-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md b/mes-framework/mes-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md new file mode 100644 index 00000000..a4f7ecd5 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md b/mes-framework/mes-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md new file mode 100644 index 00000000..0fd67b73 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/mes-spring-boot-starter-websocket/pom.xml b/mes-framework/mes-spring-boot-starter-websocket/pom.xml new file mode 100644 index 00000000..cadc58a8 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/pom.xml @@ -0,0 +1,73 @@ + + + + com.chanko.yunxi + mes-framework + ${revision} + + 4.0.0 + mes-spring-boot-starter-websocket + jar + + ${project.artifactId} + WebSocket 框架,支持多节点的广播 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + com.chanko.yunxi + mes-common + + + + + + com.chanko.yunxi + mes-spring-boot-starter-security + provided + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + com.chanko.yunxi + mes-spring-boot-starter-mq + + + org.springframework.kafka + spring-kafka + true + + + org.springframework.amqp + spring-rabbit + true + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + + + + com.chanko.yunxi + mes-spring-boot-starter-biz-tenant + provided + + + + \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/config/MesWebSocketAutoConfiguration.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/config/MesWebSocketAutoConfiguration.java new file mode 100644 index 00000000..5e13ea41 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/config/MesWebSocketAutoConfiguration.java @@ -0,0 +1,177 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.config; + +import com.chanko.yunxi.mes.heli.framework.mq.redis.config.MesRedisMQConsumerAutoConfiguration; +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.RedisMQTemplate; +import com.chanko.yunxi.mes.heli.framework.websocket.core.handler.JsonWebSocketMessageHandler; +import com.chanko.yunxi.mes.heli.framework.websocket.core.listener.WebSocketMessageListener; +import com.chanko.yunxi.mes.heli.framework.websocket.core.security.LoginUserHandshakeInterceptor; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.local.LocalWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.redis.RedisWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.session.WebSocketSessionHandlerDecorator; +import com.chanko.yunxi.mes.heli.framework.websocket.core.session.WebSocketSessionManager; +import com.chanko.yunxi.mes.heli.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 = MesRedisMQConsumerAutoConfiguration.class) // before MesRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer +@EnableWebSocket // 开启 websocket +@ConditionalOnProperty(prefix = "mes.websocket", value = "enable", matchIfMissing = true) // 允许使用 mes.websocket.enable=false 禁用 websocket +@EnableConfigurationProperties(WebSocketProperties.class) +public class MesWebSocketAutoConfiguration { + + @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(); + } + + // ==================== Sender 相关 ==================== + + @Configuration + @ConditionalOnProperty(prefix = "mes.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true) + public class LocalWebSocketMessageSenderConfiguration { + + @Bean + public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) { + return new LocalWebSocketMessageSender(sessionManager); + } + + } + + @Configuration + @ConditionalOnProperty(prefix = "mes.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true) + 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 = "mes.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true) + public class RocketMQWebSocketMessageSenderConfiguration { + + @Bean + public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender( + WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate, + @Value("${mes.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 = "mes.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true) + 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("${mes.websocket.sender-rabbitmq.exchange}") String exchange) { + return new TopicExchange(exchange, + true, // durable: 是否持久化 + false); // exclusive: 是否排它 + } + + } + + @Configuration + @ConditionalOnProperty(prefix = "mes.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true) + public class KafkaWebSocketMessageSenderConfiguration { + + @Bean + public KafkaWebSocketMessageSender kafkaWebSocketMessageSender( + WebSocketSessionManager sessionManager, KafkaTemplate kafkaTemplate, + @Value("${mes.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/config/WebSocketProperties.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/config/WebSocketProperties.java new file mode 100644 index 00000000..5ea0ee03 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/config/WebSocketProperties.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * WebSocket 配置项 + * + * @author xingyu4j + */ +@ConfigurationProperties("mes.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/handler/JsonWebSocketMessageHandler.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/handler/JsonWebSocketMessageHandler.java new file mode 100644 index 00000000..3d0e09f8 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/handler/JsonWebSocketMessageHandler.java @@ -0,0 +1,83 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.handler; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.tenant.core.util.TenantUtils; +import com.chanko.yunxi.mes.heli.framework.websocket.core.listener.WebSocketMessageListener; +import com.chanko.yunxi.mes.heli.framework.websocket.core.message.JsonWebSocketMessage; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/listener/WebSocketMessageListener.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/listener/WebSocketMessageListener.java new file mode 100644 index 00000000..19770487 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/listener/WebSocketMessageListener.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.listener; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/message/JsonWebSocketMessage.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/message/JsonWebSocketMessage.java new file mode 100644 index 00000000..0b026fa0 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/message/JsonWebSocketMessage.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.message; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/security/LoginUserHandshakeInterceptor.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/security/LoginUserHandshakeInterceptor.java new file mode 100644 index 00000000..fce9e926 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/security/LoginUserHandshakeInterceptor.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.security; + +import com.chanko.yunxi.mes.heli.framework.security.core.LoginUser; +import com.chanko.yunxi.mes.heli.framework.security.core.filter.TokenAuthenticationFilter; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java new file mode 100644 index 00000000..796e3319 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.security; + +import com.chanko.yunxi.mes.heli.framework.security.config.AuthorizeRequestsCustomizer; +import com.chanko.yunxi.mes.heli.framework.websocket.config.WebSocketProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * WebSocket 的权限自定义 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { + + private final WebSocketProperties webSocketProperties; + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + registry.antMatchers(webSocketProperties.getPath()).permitAll(); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/AbstractWebSocketMessageSender.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/AbstractWebSocketMessageSender.java new file mode 100644 index 00000000..c2497b01 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/AbstractWebSocketMessageSender.java @@ -0,0 +1,104 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.sender; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.websocket.core.message.JsonWebSocketMessage; +import com.chanko.yunxi.mes.heli.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)) { + log.info("[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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/WebSocketMessageSender.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/WebSocketMessageSender.java new file mode 100644 index 00000000..9c6dee4b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/WebSocketMessageSender.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.sender; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java new file mode 100644 index 00000000..30b77b4f --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java new file mode 100644 index 00000000..24261c78 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.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 rabbitMQWebSocketMessageSender; + + @RabbitHandler + @KafkaListener( + topics = "${mes.websocket.sender-kafka.topic}", + // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的 + groupId = "${mes.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}") + public void onMessage(KafkaWebSocketMessage message) { + rabbitMQWebSocketMessageSender.send(message.getSessionId(), + message.getUserType(), message.getUserId(), + message.getMessageType(), message.getMessageContent()); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java new file mode 100644 index 00000000..f71eddd6 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java @@ -0,0 +1,67 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.sender.kafka; + +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.WebSocketMessageSender; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java new file mode 100644 index 00000000..b2953e07 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.sender.local; + +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.WebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.session.WebSocketSessionManager; + +/** + * 本地的 {@link WebSocketMessageSender} 实现类 + * + * 注意:仅仅适合单机场景!!! + * + * @author 芋道源码 + */ +public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender { + + public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) { + super(sessionManager); + } + +} diff --git a/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java new file mode 100644 index 00000000..6843bc53 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java new file mode 100644 index 00000000..fd3a6c6c --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.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 = "${mes.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}", + // Consumer 关闭时,该队列就可以被自动删除了 + autoDelete = "true" + ), + exchange = @Exchange( + name = "${mes.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java new file mode 100644 index 00000000..5f1f799b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java @@ -0,0 +1,62 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.sender.rabbitmq; + +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.WebSocketMessageSender; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/redis/RedisWebSocketMessage.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/redis/RedisWebSocketMessage.java new file mode 100644 index 00000000..e9923dec --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/redis/RedisWebSocketMessage.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.sender.redis; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java new file mode 100644 index 00000000..7e0a4ddf --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.sender.redis; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java new file mode 100644 index 00000000..94e03cf9 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java @@ -0,0 +1,57 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.sender.redis; + +import com.chanko.yunxi.mes.heli.framework.mq.redis.core.RedisMQTemplate; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.WebSocketMessageSender; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java new file mode 100644 index 00000000..933b7277 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java new file mode 100644 index 00000000..f3ed0be9 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.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 = "${mes.websocket.sender-rocketmq.topic}", + consumerGroup = "${mes.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java new file mode 100644 index 00000000..3d793cf2 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java @@ -0,0 +1,61 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.sender.rocketmq; + +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.AbstractWebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.WebSocketMessageSender; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java new file mode 100644 index 00000000..3155ad19 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java @@ -0,0 +1,49 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/session/WebSocketSessionManager.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/session/WebSocketSessionManager.java new file mode 100644 index 00000000..135b036e --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/session/WebSocketSessionManager.java @@ -0,0 +1,53 @@ +package com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/session/WebSocketSessionManagerImpl.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/session/WebSocketSessionManagerImpl.java new file mode 100644 index 00000000..13fd7591 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/session/WebSocketSessionManagerImpl.java @@ -0,0 +1,125 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.session; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.security.core.LoginUser; +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/util/WebSocketFrameworkUtils.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/util/WebSocketFrameworkUtils.java new file mode 100644 index 00000000..306ff688 --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/core/util/WebSocketFrameworkUtils.java @@ -0,0 +1,67 @@ +package com.chanko.yunxi.mes.heli.framework.websocket.core.util; + +import com.chanko.yunxi.mes.heli.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/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/package-info.java b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/package-info.java new file mode 100644 index 00000000..0b66119b --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/java/com/chanko/yunxi/mes/heli/framework/websocket/package-info.java @@ -0,0 +1,4 @@ +/** + * WebSocket 框架,支持多节点的广播 + */ +package com.chanko.yunxi.mes.heli.framework.websocket; diff --git a/mes-framework/mes-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mes-framework/mes-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..85cdf69a --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.chanko.yunxi.mes.heli.framework.websocket.config.MesWebSocketAutoConfiguration \ No newline at end of file diff --git a/mes-framework/mes-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md b/mes-framework/mes-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md new file mode 100644 index 00000000..de03ddfd --- /dev/null +++ b/mes-framework/mes-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-framework/pom.xml b/mes-framework/pom.xml new file mode 100644 index 00000000..0ea7dd8e --- /dev/null +++ b/mes-framework/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + mes + com.chanko.yunxi + ${revision} + + pom + + mes-common + mes-spring-boot-starter-banner + mes-spring-boot-starter-mybatis + mes-spring-boot-starter-redis + mes-spring-boot-starter-web + mes-spring-boot-starter-security + + mes-spring-boot-starter-file + mes-spring-boot-starter-monitor + mes-spring-boot-starter-protection + mes-spring-boot-starter-job + mes-spring-boot-starter-mq + + mes-spring-boot-starter-excel + + mes-spring-boot-starter-biz-operatelog + mes-spring-boot-starter-biz-dict + mes-spring-boot-starter-biz-sms + + mes-spring-boot-starter-biz-tenant + mes-spring-boot-starter-biz-data-permission + mes-spring-boot-starter-biz-error-code + mes-spring-boot-starter-biz-ip + + mes-spring-boot-starter-captcha + mes-spring-boot-starter-websocket + mes-spring-boot-starter-desensitize + + + mes-framework + + 该包是技术组件,每个子包,代表一个组件。每个组件包括两部分: + 1. core 包:是该组件的核心封装 + 2. config 包:是该组件基于 Spring 的配置 + + 技术组件,也分成两类: + 1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展 + 2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。 + 如果是业务组件,Maven 名字会包含 biz + + https://github.com/YunaiV/ruoyi-vue-pro + + diff --git a/mes-module-infra/mes-module-infra-api/pom.xml b/mes-module-infra/mes-module-infra-api/pom.xml new file mode 100644 index 00000000..73bfbe31 --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/pom.xml @@ -0,0 +1,33 @@ + + + + com.chanko.yunxi + mes-module-infra + ${revision} + + 4.0.0 + mes-module-infra-api + jar + + ${project.artifactId} + + infra 模块 API,暴露给其它模块调用 + + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + diff --git a/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/file/FileApi.java b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/file/FileApi.java new file mode 100644 index 00000000..3d7e5f0c --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/file/FileApi.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.file; + +/** + * 文件 API 接口 + * + * @author 芋道源码 + */ +public interface FileApi { + + /** + * 保存文件,并返回文件的访问路径 + * + * @param content 文件内容 + * @return 文件路径 + */ + default String createFile(byte[] content) { + return createFile(null, null, content); + } + + /** + * 保存文件,并返回文件的访问路径 + * + * @param path 文件路径 + * @param content 文件内容 + * @return 文件路径 + */ + default String createFile(String path, byte[] content) { + return createFile(null, path, content); + } + + /** + * 保存文件,并返回文件的访问路径 + * + * @param name 文件名称 + * @param path 文件路径 + * @param content 文件内容 + * @return 文件路径 + */ + String createFile(String name, String path, byte[] content); + +} diff --git a/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiAccessLogApi.java b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiAccessLogApi.java new file mode 100644 index 00000000..4673ad55 --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiAccessLogApi.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.logger; + +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * API 访问日志的 API 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogApi { + + /** + * 创建 API 访问日志 + * + * @param createDTO 创建信息 + */ + void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO); + +} diff --git a/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiErrorLogApi.java b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiErrorLogApi.java new file mode 100644 index 00000000..4a4acb37 --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiErrorLogApi.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.logger; + +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * API 错误日志的 API 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogApi { + + /** + * 创建 API 错误日志 + * + * @param createDTO 创建信息 + */ + void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO); + +} diff --git a/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java new file mode 100644 index 00000000..b709b4d4 --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java @@ -0,0 +1,85 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.logger.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +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; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 开始请求时间 + */ + @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/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/dto/ApiErrorLogCreateReqDTO.java b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/dto/ApiErrorLogCreateReqDTO.java new file mode 100644 index 00000000..e296b465 --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/dto/ApiErrorLogCreateReqDTO.java @@ -0,0 +1,107 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.logger.dto; + +import lombok.Data; + +import javax.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/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/package-info.java b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/package-info.java new file mode 100644 index 00000000..4d09f9aa --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/package-info.java @@ -0,0 +1,4 @@ +/** + * infra API 包,定义暴露给其它模块的 API + */ +package com.chanko.yunxi.mes.heli.module.infra.api; diff --git a/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/websocket/WebSocketSenderApi.java b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/websocket/WebSocketSenderApi.java new file mode 100644 index 00000000..0876924b --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/websocket/WebSocketSenderApi.java @@ -0,0 +1,54 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.websocket; + +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/DictTypeConstants.java b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/DictTypeConstants.java new file mode 100644 index 00000000..2055c082 --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/DictTypeConstants.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.module.infra.enums; + +/** + * Infra 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String REDIS_TIMEOUT_TYPE = "infra_redis_timeout_type"; // Redis 超时类型 + + 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 是否类型 + +} diff --git a/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/ErrorCodeConstants.java b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/ErrorCodeConstants.java new file mode 100644 index 00000000..30aad847 --- /dev/null +++ b/mes-module-infra/mes-module-infra-api/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/ErrorCodeConstants.java @@ -0,0 +1,73 @@ +package com.chanko.yunxi.mes.heli.module.infra.enums; + +import com.chanko.yunxi.mes.heli.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 表达式不正确"); + + // ========== 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_003_001_000, "表定义已经存在"); + ErrorCode CODEGEN_IMPORT_TABLE_NULL = new ErrorCode(1_003_001_001, "导入的表不存在"); + ErrorCode CODEGEN_IMPORT_COLUMNS_NULL = new ErrorCode(1_003_001_002, "导入的字段不存在"); + ErrorCode CODEGEN_TABLE_NOT_EXISTS = new ErrorCode(1_003_001_004, "表定义不存在"); + ErrorCode CODEGEN_COLUMN_NOT_EXISTS = new ErrorCode(1_003_001_005, "字段义不存在"); + ErrorCode CODEGEN_SYNC_COLUMNS_NULL = new ErrorCode(1_003_001_006, "同步的字段不存在"); + ErrorCode CODEGEN_SYNC_NONE_CHANGE = new ErrorCode(1_003_001_007, "同步失败,不存在改变"); + ErrorCode CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL = new ErrorCode(1_003_001_008, "数据库的表注释未填写"); + ErrorCode CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL = new ErrorCode(1_003_001_009, "数据库的表字段({})注释未填写"); + ErrorCode CODEGEN_MASTER_TABLE_NOT_EXISTS = new ErrorCode(1_003_001_010, "主表(id={})定义不存在,请检查"); + ErrorCode CODEGEN_SUB_COLUMN_NOT_EXISTS = new ErrorCode(1_003_001_011, "子表的字段(id={})不存在,请检查"); + ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE = new ErrorCode(1_003_001_012, "主表生成代码失败,原因:它没有子表"); + ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_COLUMN = new ErrorCode(1_003_001_013, "主表生成代码失败,原因:它的子表({})没有字段"); + + // ========== 文件配置 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-107-000 ========== + ErrorCode DEMO_STUDENT_NOT_EXISTS = new ErrorCode(1_001_107_000, "学生不存在"); + + // ========== 学生 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_GRADE_NOT_EXISTS = new ErrorCode(1_001_201_008, "学生班级不存在"); + ErrorCode DEMO03_GRADE_EXISTS = new ErrorCode(1_001_201_009, "学生班级已存在"); + +} diff --git a/mes-module-infra/mes-module-infra-biz/pom.xml b/mes-module-infra/mes-module-infra-biz/pom.xml new file mode 100644 index 00000000..fd649e55 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/pom.xml @@ -0,0 +1,119 @@ + + + + com.chanko.yunxi + mes-module-infra + ${revision} + + 4.0.0 + mes-module-infra-biz + jar + + ${project.artifactId} + + infra 模块,主要提供两块能力: + 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + + + + + com.chanko.yunxi + mes-module-system-api + ${revision} + + + com.chanko.yunxi + mes-module-infra-api + ${revision} + + + + + com.chanko.yunxi + mes-spring-boot-starter-biz-operatelog + + + com.chanko.yunxi + mes-spring-boot-starter-biz-tenant + + + + + com.chanko.yunxi + mes-spring-boot-starter-security + + + + com.chanko.yunxi + mes-spring-boot-starter-websocket + + + + + com.chanko.yunxi + mes-spring-boot-starter-mybatis + + + com.baomidou + mybatis-plus-generator + + + + com.chanko.yunxi + mes-spring-boot-starter-redis + + + + + + + com.chanko.yunxi + mes-spring-boot-starter-job + + + + + com.chanko.yunxi + mes-spring-boot-starter-mq + + + + + + com.chanko.yunxi + mes-spring-boot-starter-excel + + + + org.apache.velocity + velocity-engine-core + + + + cn.smallbun.screw + screw-core + + + + + com.chanko.yunxi + mes-spring-boot-starter-monitor + + + + de.codecentric + spring-boot-admin-starter-server + + + + + com.chanko.yunxi + mes-spring-boot-starter-file + + + + + diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/file/FileApiImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/file/FileApiImpl.java new file mode 100644 index 00000000..e23a0632 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/file/FileApiImpl.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.file; + +import com.chanko.yunxi.mes.heli.module.infra.service.file.FileService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 文件 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FileApiImpl implements FileApi { + + @Resource + private FileService fileService; + + @Override + public String createFile(String name, String path, byte[] content) { + return fileService.createFile(name, path, content); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiAccessLogApiImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiAccessLogApiImpl.java new file mode 100644 index 00000000..195659b0 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiAccessLogApiImpl.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.logger; + +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.infra.service.logger.ApiAccessLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * API 访问日志的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiAccessLogApiImpl implements ApiAccessLogApi { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @Override + public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { + apiAccessLogService.createApiAccessLog(createDTO); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiErrorLogApiImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiErrorLogApiImpl.java new file mode 100644 index 00000000..533284d4 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/logger/ApiErrorLogApiImpl.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.logger; + +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.infra.service.logger.ApiErrorLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * API 访问日志的 API 接口 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiErrorLogApiImpl implements ApiErrorLogApi { + + @Resource + private ApiErrorLogService apiErrorLogService; + + @Override + public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { + apiErrorLogService.createApiErrorLog(createDTO); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/package-info.java new file mode 100644 index 00000000..80eafbff --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/package-info.java @@ -0,0 +1 @@ +package com.chanko.yunxi.mes.heli.module.infra.api; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/websocket/WebSocketSenderApiImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/websocket/WebSocketSenderApiImpl.java new file mode 100644 index 00000000..81982a84 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/api/websocket/WebSocketSenderApiImpl.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.infra.api.websocket; + +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.WebSocketMessageSender; +import org.springframework.stereotype.Component; + +import javax.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/CodegenController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/CodegenController.java new file mode 100644 index 00000000..52e8bd14 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/CodegenController.java @@ -0,0 +1,151 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ZipUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import com.chanko.yunxi.mes.heli.module.infra.convert.codegen.CodegenConvert; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.chanko.yunxi.mes.heli.module.infra.service.codegen.CodegenService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@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 = "mes"), + @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(getLoginUserId(), 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 = "预览生成代码") + @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); + // 输出 + ServletUtils.writeAttachment(response, "codegen.zip", outputStream.toByteArray()); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java new file mode 100644 index 00000000..bfc1974f --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java new file mode 100644 index 00000000..f42d4b22 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo; + +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java new file mode 100644 index 00000000..a5a9f25a --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.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/com.chanko.yunxi.mes.heli/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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java new file mode 100644 index 00000000..729416ed --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo; + +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.column.CodegenColumnSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.CodegenTableSaveReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java new file mode 100644 index 00000000..2d672814 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.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 = "true") + private Boolean autoIncrement; + + @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/column/CodegenColumnSaveReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/column/CodegenColumnSaveReqVO.java new file mode 100644 index 00000000..e8aa16af --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/column/CodegenColumnSaveReqVO.java @@ -0,0 +1,85 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.column; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@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 = "true") + @NotNull(message = "是否自增不能为空") + private Boolean autoIncrement; + + @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java new file mode 100644 index 00000000..a9bb367e --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 表定义分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CodegenTablePageReqVO extends PageParam { + + @Schema(description = "表名称,模糊匹配", example = "mes") + private String tableName; + + @Schema(description = "表描述,模糊匹配", example = "芋道") + private String tableComment; + + @Schema(description = "实体,模糊匹配", example = "Mes") + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java new file mode 100644 index 00000000..296f1246 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.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 = "mes") + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/CodegenTableSaveReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/CodegenTableSaveReqVO.java new file mode 100644 index 00000000..df7bdef3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/CodegenTableSaveReqVO.java @@ -0,0 +1,100 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table; + +import cn.hutool.core.util.ObjectUtil; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenSceneEnum; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; + +@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 = "mes") + @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java new file mode 100644 index 00000000..ef805fc0 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/ConfigController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/ConfigController.java new file mode 100644 index 00000000..135677bb --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/ConfigController.java @@ -0,0 +1,106 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.config; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo.*; +import com.chanko.yunxi.mes.heli.module.infra.convert.config.ConfigConvert; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.config.ConfigDO; +import com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants; +import com.chanko.yunxi.mes.heli.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 org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@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); + } + + @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") + @Operation(summary = "导出参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:export')") + @OperateLog(type = EXPORT) + public void exportConfig(@Valid 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/vo/ConfigPageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/vo/ConfigPageReqVO.java new file mode 100644 index 00000000..ed3f8591 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/vo/ConfigPageReqVO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 参数配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/vo/ConfigRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/vo/ConfigRespVO.java new file mode 100644 index 00000000..e3ac7608 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/vo/ConfigRespVO.java @@ -0,0 +1,56 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/vo/ConfigSaveReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/vo/ConfigSaveReqVO.java new file mode 100644 index 00000000..87ed4da6 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/config/vo/ConfigSaveReqVO.java @@ -0,0 +1,45 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/DataSourceConfigController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/DataSourceConfigController.java new file mode 100644 index 00000000..abbabe68 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/DataSourceConfigController.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.db; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.db.vo.DataSourceConfigRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.chanko.yunxi.mes.heli.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 org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.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); + } + + @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/DatabaseDocController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/DatabaseDocController.java new file mode 100644 index 00000000..815e25fa --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/DatabaseDocController.java @@ -0,0 +1,154 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.db; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import cn.smallbun.screw.core.Configuration; +import cn.smallbun.screw.core.engine.EngineConfig; +import cn.smallbun.screw.core.engine.EngineFileType; +import cn.smallbun.screw.core.engine.EngineTemplateType; +import cn.smallbun.screw.core.execute.DocumentationExecute; +import cn.smallbun.screw.core.process.ProcessConfig; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +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 javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +@Tag(name = "管理后台 - 数据库文档") +@RestController +@RequestMapping("/infra/db-doc") +public class DatabaseDocController { + + @Resource + private DynamicDataSourceProperties dynamicDataSourceProperties; + + private static final String FILE_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator + + "db-doc"; + private static final String DOC_FILE_NAME = "数据库文档"; + private static final String DOC_VERSION = "1.0.0"; + private static final String DOC_DESCRIPTION = "文档描述"; + + @GetMapping("/export-html") + @Operation(summary = "导出 html 格式的数据文档") + @Parameter(name = "deleteFile", description = "是否删除在服务器本地生成的数据库文档", example = "true") + public void exportHtml(@RequestParam(defaultValue = "true") Boolean deleteFile, + HttpServletResponse response) throws IOException { + doExportFile(EngineFileType.HTML, deleteFile, response); + } + + @GetMapping("/export-word") + @Operation(summary = "导出 word 格式的数据文档") + @Parameter(name = "deleteFile", description = "是否删除在服务器本地生成的数据库文档", example = "true") + public void exportWord(@RequestParam(defaultValue = "true") Boolean deleteFile, + HttpServletResponse response) throws IOException { + doExportFile(EngineFileType.WORD, deleteFile, response); + } + + @GetMapping("/export-markdown") + @Operation(summary = "导出 markdown 格式的数据文档") + @Parameter(name = "deleteFile", description = "是否删除在服务器本地生成的数据库文档", example = "true") + public void exportMarkdown(@RequestParam(defaultValue = "true") Boolean deleteFile, + HttpServletResponse response) throws IOException { + doExportFile(EngineFileType.MD, deleteFile, response); + } + + private void doExportFile(EngineFileType fileOutputType, Boolean deleteFile, + HttpServletResponse response) throws IOException { + String docFileName = DOC_FILE_NAME + "_" + IdUtil.fastSimpleUUID(); + String filePath = doExportFile(fileOutputType, docFileName); + String downloadFileName = DOC_FILE_NAME + fileOutputType.getFileSuffix(); //下载后的文件名 + try { + // 读取,返回 + ServletUtils.writeAttachment(response, downloadFileName, FileUtil.readBytes(filePath)); + } finally { + handleDeleteFile(deleteFile, filePath); + } + } + + /** + * 输出文件,返回文件路径 + * + * @param fileOutputType 文件类型 + * @param fileName 文件名, 无需 ".docx" 等文件后缀 + * @return 生成的文件所在路径 + */ + private String doExportFile(EngineFileType fileOutputType, String fileName) { + try (HikariDataSource dataSource = buildDataSource()) { + // 创建 screw 的配置 + Configuration config = Configuration.builder() + .version(DOC_VERSION) // 版本 + .description(DOC_DESCRIPTION) // 描述 + .dataSource(dataSource) // 数据源 + .engineConfig(buildEngineConfig(fileOutputType, fileName)) // 引擎配置 + .produceConfig(buildProcessConfig()) // 处理配置 + .build(); + + // 执行 screw,生成数据库文档 + new DocumentationExecute(config).execute(); + + return FILE_OUTPUT_DIR + File.separator + fileName + fileOutputType.getFileSuffix(); + } + } + + private void handleDeleteFile(Boolean deleteFile, String filePath) { + if (!deleteFile) { + return; + } + FileUtil.del(filePath); + } + + /** + * 创建数据源 + */ + // TODO 芋艿:screw 暂时不支持 druid,尴尬 + private HikariDataSource buildDataSource() { + // 获得 DataSource 数据源,目前只支持首个 + String primary = dynamicDataSourceProperties.getPrimary(); + DataSourceProperty dataSourceProperty = dynamicDataSourceProperties.getDatasource().get(primary); + // 创建 HikariConfig 配置类 + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(dataSourceProperty.getUrl()); + hikariConfig.setUsername(dataSourceProperty.getUsername()); + hikariConfig.setPassword(dataSourceProperty.getPassword()); + hikariConfig.addDataSourceProperty("useInformationSchema", "true"); // 设置可以获取 tables remarks 信息 + // 创建数据源 + return new HikariDataSource(hikariConfig); + } + + /** + * 创建 screw 的引擎配置 + */ + private static EngineConfig buildEngineConfig(EngineFileType fileOutputType, String docFileName) { + return EngineConfig.builder() + .fileOutputDir(FILE_OUTPUT_DIR) // 生成文件路径 + .openOutputDir(false) // 打开目录 + .fileType(fileOutputType) // 文件类型 + .produceType(EngineTemplateType.velocity) // 文件类型 + .fileName(docFileName) // 自定义文件名称 + .build(); + } + + /** + * 创建 screw 的处理配置,一般可忽略 + * 指定生成逻辑、当存在指定表、指定表前缀、指定表后缀时,将生成指定表,其余表不生成、并跳过忽略表配置 + */ + private static ProcessConfig buildProcessConfig() { + return ProcessConfig.builder() + .ignoreTablePrefix(Arrays.asList("QRTZ_", "ACT_", "FLW_")) // 忽略表前缀 + .build(); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java new file mode 100644 index 00000000..bd5d3bf3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.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 Integer 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/vo/DataSourceConfigSaveReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/vo/DataSourceConfigSaveReqVO.java new file mode 100644 index 00000000..69c0b928 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/db/vo/DataSourceConfigSaveReqVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.db.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/Demo01ContactController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/Demo01ContactController.java new file mode 100644 index 00000000..46cde220 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/Demo01ContactController.java @@ -0,0 +1,93 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo.Demo01ContactRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; +import com.chanko.yunxi.mes.heli.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 org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@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); + } + + @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')") + @OperateLog(type = 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/vo/Demo01ContactPageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/vo/Demo01ContactPageReqVO.java new file mode 100644 index 00000000..7004eb1c --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/vo/Demo01ContactPageReqVO.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 示例联系人分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java new file mode 100644 index 00000000..6d96b945 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java @@ -0,0 +1,47 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import java.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import com.alibaba.excel.annotation.*; +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; + +@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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/vo/Demo01ContactSaveReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/vo/Demo01ContactSaveReqVO.java new file mode 100644 index 00000000..a09657e2 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo01/vo/Demo01ContactSaveReqVO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/Demo02CategoryController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/Demo02CategoryController.java new file mode 100644 index 00000000..6fbf4618 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/Demo02CategoryController.java @@ -0,0 +1,90 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; +import com.chanko.yunxi.mes.heli.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 org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@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')") + @OperateLog(type = 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryListReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryListReqVO.java new file mode 100644 index 00000000..9933f182 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryListReqVO.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java new file mode 100644 index 00000000..88badf2d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/vo/Demo02CategorySaveReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/vo/Demo02CategorySaveReqVO.java new file mode 100644 index 00000000..05b5d505 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo02/vo/Demo02CategorySaveReqVO.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/Demo03StudentController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/Demo03StudentController.java new file mode 100644 index 00000000..48f8bdd9 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/Demo03StudentController.java @@ -0,0 +1,197 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo.Demo03StudentPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo.Demo03StudentRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo.Demo03StudentSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import com.chanko.yunxi.mes.heli.module.infra.service.demo.demo03.Demo03StudentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 学生") +@RestController +@RequestMapping("/infra/demo03-student") +@Validated +public class Demo03StudentController { + + @Resource + private Demo03StudentService demo03StudentService; + + @PostMapping("/create") + @Operation(summary = "创建学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')") + public CommonResult createDemo03Student(@Valid @RequestBody Demo03StudentSaveReqVO createReqVO) { + return success(demo03StudentService.createDemo03Student(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新学生") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") + public CommonResult updateDemo03Student(@Valid @RequestBody Demo03StudentSaveReqVO updateReqVO) { + demo03StudentService.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) { + demo03StudentService.deleteDemo03Student(id); + 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 = demo03StudentService.getDemo03Student(id); + return success(BeanUtils.toBean(demo03Student, Demo03StudentRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得学生分页") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')") + public CommonResult> getDemo03StudentPage(@Valid Demo03StudentPageReqVO pageReqVO) { + PageResult pageResult = demo03StudentService.getDemo03StudentPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, Demo03StudentRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出学生 Excel") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:export')") + @OperateLog(type = EXPORT) + public void exportDemo03StudentExcel(@Valid Demo03StudentPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = demo03StudentService.getDemo03StudentPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "学生.xls", "数据", Demo03StudentRespVO.class, + BeanUtils.toBean(list, Demo03StudentRespVO.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(demo03StudentService.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(demo03StudentService.createDemo03Course(demo03Course)); + } + + @PutMapping("/demo03-course/update") + @Operation(summary = "更新学生课程") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") + public CommonResult updateDemo03Course(@Valid @RequestBody Demo03CourseDO demo03Course) { + demo03StudentService.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) { + demo03StudentService.deleteDemo03Course(id); + 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(demo03StudentService.getDemo03Course(id)); + } + + @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(demo03StudentService.getDemo03CourseListByStudentId(studentId)); + } + + // ==================== 子表(学生班级) ==================== + + @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(demo03StudentService.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(demo03StudentService.createDemo03Grade(demo03Grade)); + } + + @PutMapping("/demo03-grade/update") + @Operation(summary = "更新学生班级") + @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')") + public CommonResult updateDemo03Grade(@Valid @RequestBody Demo03GradeDO demo03Grade) { + demo03StudentService.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) { + demo03StudentService.deleteDemo03Grade(id); + 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(demo03StudentService.getDemo03Grade(id)); + } + + @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(demo03StudentService.getDemo03GradeByStudentId(studentId)); + } + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/package-info.java new file mode 100644 index 00000000..5276072f --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/package-info.java @@ -0,0 +1 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03; \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/vo/Demo03StudentPageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/vo/Demo03StudentPageReqVO.java new file mode 100644 index 00000000..6d03471b --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/vo/Demo03StudentPageReqVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 学生分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class Demo03StudentPageReqVO 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/vo/Demo03StudentRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/vo/Demo03StudentRespVO.java new file mode 100644 index 00000000..5c2ae541 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/vo/Demo03StudentRespVO.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; +import com.alibaba.excel.annotation.*; +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; + +@Schema(description = "管理后台 - 学生 Response VO") +@Data +@ExcelIgnoreUnannotated +public class Demo03StudentRespVO { + + @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/vo/Demo03StudentSaveReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/vo/Demo03StudentSaveReqVO.java new file mode 100644 index 00000000..65b06c3d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/demo03/vo/Demo03StudentSaveReqVO.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import javax.validation.constraints.*; +import java.time.LocalDateTime; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; + +@Schema(description = "管理后台 - 学生新增/修改 Request VO") +@Data +public class Demo03StudentSaveReqVO { + + @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; + + + private List demo03Courses; + + private Demo03GradeDO demo03Grade; + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/package-info.java new file mode 100644 index 00000000..ffc4591d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/demo/package-info.java @@ -0,0 +1,8 @@ +/** + * 代码生成示例 + * + * 1. demo01:单表(增删改查) + * 2. demo02:单表(树形结构) + * 3. demo03:主子表(标准模式)+ 主子表(ERP 模式)+ 主子表(内嵌模式) + */ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo; \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/FileConfigController.http b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/FileConfigController.http new file mode 100644 index 00000000..347cf66b --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/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: {{adminTenentId}} +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.mes.iocoder.cn", + "region": "oss-cn-beijing" + } +} + +### 请求 /infra/file-config/update 接口 => 成功 +PUT {{baseUrl}}/infra/file-config/update +Content-Type: application/json +tenant-id: {{adminTenentId}} +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.mes.iocoder.cn", + "region": "oss-cn-beijing" + } +} + +### 请求 /infra/file-config/test 接口 => 成功 +GET {{baseUrl}}/infra/file-config/test?id=2 +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/FileConfigController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/FileConfigController.java new file mode 100644 index 00000000..7a604fbe --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/FileConfigController.java @@ -0,0 +1,88 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.file; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config.FileConfigRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file.FileConfigDO; +import com.chanko.yunxi.mes.heli.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 org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.chanko.yunxi.mes.heli.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); + } + + @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/FileController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/FileController.java new file mode 100644 index 00000000..c760da76 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/FileController.java @@ -0,0 +1,95 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.file; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.file.FileRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.file.FileUploadReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file.FileDO; +import com.chanko.yunxi.mes.heli.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.tags.Tag; +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 javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 文件存储") +@RestController +@RequestMapping("/infra/file") +@Validated +@Slf4j +public class FileController { + + @Resource + private FileService fileService; + + @PostMapping("/upload") + @Operation(summary = "上传文件") + @OperateLog(logArgs = false) // 上传文件,没有记录操作日志的必要 + public CommonResult uploadFile(FileUploadReqVO uploadReqVO) throws Exception { + MultipartFile file = uploadReqVO.getFile(); + String path = uploadReqVO.getPath(); + return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); + } + + @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); + } + + @GetMapping("/{configId}/get/**") + @PermitAll + @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; + } + ServletUtils.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java new file mode 100644 index 00000000..6479e9d6 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 文件配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java new file mode 100644 index 00000000..214890d2 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config; + +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/config/FileConfigSaveReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/config/FileConfigSaveReqVO.java new file mode 100644 index 00000000..8b841118 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/config/FileConfigSaveReqVO.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/file/FilePageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/file/FilePageReqVO.java new file mode 100644 index 00000000..c87c39b3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/file/FilePageReqVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.file; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 文件分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FilePageReqVO extends PageParam { + + @Schema(description = "文件路径,模糊匹配", example = "mes") + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/file/FileRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/file/FileRespVO.java new file mode 100644 index 00000000..9c612869 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/file/FileRespVO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.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 = "mes.jpg") + private String path; + + @Schema(description = "原文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes.jpg") + private String name; + + @Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/mes.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java new file mode 100644 index 00000000..d7b090f4 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 上传文件 Request VO") +@Data +public class FileUploadReqVO { + + @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件附件不能为空") + private MultipartFile file; + + @Schema(description = "文件附件", example = "mesyuanma.png") + private String path; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/JobController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/JobController.java new file mode 100644 index 00000000..32df2c38 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/JobController.java @@ -0,0 +1,140 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.job; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.framework.quartz.core.util.CronUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job.JobRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job.JobSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.job.JobDO; +import com.chanko.yunxi.mes.heli.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 org.quartz.SchedulerException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@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); + } + + @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); + } + + @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')") + @OperateLog(type = 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/JobLogController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/JobLogController.java new file mode 100644 index 00000000..99bca20e --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/JobLogController.java @@ -0,0 +1,71 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.job; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.log.JobLogRespVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.job.JobLogDO; +import com.chanko.yunxi.mes.heli.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 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 javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@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')") + @OperateLog(type = 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/job/JobPageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/job/JobPageReqVO.java new file mode 100644 index 00000000..8a75b170 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/job/JobPageReqVO.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job; + +import com.chanko.yunxi.mes.heli.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 +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/job/JobRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/job/JobRespVO.java new file mode 100644 index 00000000..b3c40927 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/job/JobRespVO.java @@ -0,0 +1,59 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.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 javax.validation.constraints.NotNull; +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 = "mes") + @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/job/JobSaveReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/job/JobSaveReqVO.java new file mode 100644 index 00000000..bc4e2075 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/job/JobSaveReqVO.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@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 = "mes") + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java new file mode 100644 index 00000000..1f13dc1d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.log; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 定时任务日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/log/JobLogRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/log/JobLogRespVO.java new file mode 100644 index 00000000..d26c047b --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/job/vo/log/JobLogRespVO.java @@ -0,0 +1,63 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.log; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.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 = "mes") + @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/ApiAccessLogController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/ApiAccessLogController.java new file mode 100644 index 00000000..5a722b99 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/ApiAccessLogController.java @@ -0,0 +1,60 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogRespVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import com.chanko.yunxi.mes.heli.module.infra.service.logger.ApiAccessLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@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')") + @OperateLog(type = 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/ApiErrorLogController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/ApiErrorLogController.java new file mode 100644 index 00000000..507f3269 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/ApiErrorLogController.java @@ -0,0 +1,74 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogRespVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import com.chanko.yunxi.mes.heli.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 org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; +import static com.chanko.yunxi.mes.heli.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')") + @OperateLog(type = 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java new file mode 100644 index 00000000..601034b0 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apiaccesslog; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - API 访问日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java new file mode 100644 index 00000000..55bf816d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java @@ -0,0 +1,82 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apiaccesslog; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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 = "用户 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 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java new file mode 100644 index 00000000..ea800a06 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apierrorlog; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - API 错误日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java new file mode 100644 index 00000000..97b3c5ec --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java @@ -0,0 +1,112 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apierrorlog; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.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 Integer 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 Integer userId; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(com.chanko.yunxi.mes.heli.module.system.enums.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/redis/RedisController.http b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/redis/RedisController.http new file mode 100644 index 00000000..8a0e70fd --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/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: {{adminTenentId}} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/redis/RedisController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/redis/RedisController.java new file mode 100644 index 00000000..193c02a3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/redis/RedisController.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.admin.redis; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.redis.vo.RedisMonitorRespVO; +import com.chanko.yunxi.mes.heli.module.infra.convert.redis.RedisConvert; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.annotation.Resource; +import java.util.Properties; + +import static com.chanko.yunxi.mes.heli.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.info("commandstats")); + assert commandStats != null; // 断言,避免警告 + // 拼接结果返回 + return success(RedisConvert.INSTANCE.build(info, dbSize, commandStats)); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java new file mode 100644 index 00000000..e8ce9532 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/app/file/AppFileController.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/app/file/AppFileController.java new file mode 100644 index 00000000..d4cae782 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/app/file/AppFileController.java @@ -0,0 +1,38 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.app.file; + +import cn.hutool.core.io.IoUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.module.infra.controller.app.file.vo.AppFileUploadReqVO; +import com.chanko.yunxi.mes.heli.module.infra.service.file.FileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; + +import static com.chanko.yunxi.mes.heli.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 = "上传文件") + public CommonResult uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception { + MultipartFile file = uploadReqVO.getFile(); + String path = uploadReqVO.getPath(); + return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/app/file/vo/AppFileUploadReqVO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/app/file/vo/AppFileUploadReqVO.java new file mode 100644 index 00000000..dfd75c5e --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/app/file/vo/AppFileUploadReqVO.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.module.infra.controller.app.file.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 App - 上传文件 Request VO") +@Data +public class AppFileUploadReqVO { + + @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件附件不能为空") + private MultipartFile file; + + @Schema(description = "文件附件", example = "mesyuanma.png") + private String path; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/app/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/app/package-info.java new file mode 100644 index 00000000..bb64b76a --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/app/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.chanko.yunxi.mes.heli.module.infra.controller.app; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/package-info.java new file mode 100644 index 00000000..95bfc2fd --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 mes-ui-admin 前端项目 + * 2. app 包:提供给用户 APP mes-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.chanko.yunxi.mes.heli.module.infra.controller; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/codegen/CodegenConvert.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/codegen/CodegenConvert.java new file mode 100644 index 00000000..881706c8 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/codegen/CodegenConvert.java @@ -0,0 +1,69 @@ +package com.chanko.yunxi.mes.heli.module.infra.convert.codegen; + +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.chanko.yunxi.mes.heli.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 = "keyIdentityFlag", target = "autoIncrement"), + @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/config/ConfigConvert.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/config/ConfigConvert.java new file mode 100644 index 00000000..f1ed3f07 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/config/ConfigConvert.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.infra.convert.config; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo.ConfigRespVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo.ConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/file/FileConfigConvert.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/file/FileConfigConvert.java new file mode 100644 index 00000000..033ade83 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/file/FileConfigConvert.java @@ -0,0 +1,22 @@ +package com.chanko.yunxi.mes.heli.module.infra.convert.file; + +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/package-info.java new file mode 100644 index 00000000..8d09ea51 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package com.chanko.yunxi.mes.heli.module.infra.convert; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/redis/RedisConvert.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/redis/RedisConvert.java new file mode 100644 index 00000000..10a506a7 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/redis/RedisConvert.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.module.infra.convert.redis; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 00000000..09ce3bec --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/codegen/CodegenColumnDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/codegen/CodegenColumnDO.java new file mode 100644 index 00000000..ede3facf --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/codegen/CodegenColumnDO.java @@ -0,0 +1,142 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; +import com.chanko.yunxi.mes.heli.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) +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; + /** + * 是否自增 + * + * 关联 {@link TableField#isKeyIdentityFlag()} + */ + private Boolean autoIncrement; + /** + * 排序 + */ + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/codegen/CodegenTableDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/codegen/CodegenTableDO.java new file mode 100644 index 00000000..19bb267d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/codegen/CodegenTableDO.java @@ -0,0 +1,158 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenFrontTypeEnum; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenSceneEnum; +import com.chanko.yunxi.mes.heli.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) +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/config/ConfigDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/config/ConfigDO.java new file mode 100644 index 00000000..4a7c0b1d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/config/ConfigDO.java @@ -0,0 +1,64 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.config; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.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) +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/db/DataSourceConfigDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/db/DataSourceConfigDO.java new file mode 100644 index 00000000..66eb3d62 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/db/DataSourceConfigDO.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.db; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.type.EncryptTypeHandler; +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 +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo01/Demo01ContactDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo01/Demo01ContactDO.java new file mode 100644 index 00000000..43e9b09a --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo01/Demo01ContactDO.java @@ -0,0 +1,54 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo01; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; + +/** + * 示例联系人 DO + * + * @author 芋道源码 + */ +@TableName("infra_demo01_contact") +@KeySequence("infra_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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo02/Demo02CategoryDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo02/Demo02CategoryDO.java new file mode 100644 index 00000000..4e07bcb9 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo02/Demo02CategoryDO.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo02; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; + +/** + * 示例分类 DO + * + * @author 芋道源码 + */ +@TableName("infra_demo02_category") +@KeySequence("infra_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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java new file mode 100644 index 00000000..03e5dc91 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生课程 DO + * + * @author 芋道源码 + */ +@TableName("infra_demo03_course") +@KeySequence("infra_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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java new file mode 100644 index 00000000..baf4ad49 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生班级 DO + * + * @author 芋道源码 + */ +@TableName("infra_demo03_grade") +@KeySequence("infra_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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo03/Demo03StudentDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo03/Demo03StudentDO.java new file mode 100644 index 00000000..4927391e --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/demo/demo03/Demo03StudentDO.java @@ -0,0 +1,50 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03; + +import lombok.*; +import java.util.*; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; + +/** + * 学生 DO + * + * @author 芋道源码 + */ +@TableName("infra_demo03_student") +@KeySequence("infra_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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/file/FileConfigDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/file/FileConfigDO.java new file mode 100644 index 00000000..b5fc6f92 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/file/FileConfigDO.java @@ -0,0 +1,58 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file; + +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientConfig; +import com.chanko.yunxi.mes.heli.framework.file.core.enums.FileStorageEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +/** + * 文件配置表 + * + * @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 +public class FileConfigDO extends BaseDO { + + /** + * 配置编号,数据库自增 + */ + private Long id; + /** + * 配置名 + */ + private String name; + /** + * 存储器 + * + * 枚举 {@link FileStorageEnum} + */ + private Integer storage; + /** + * 备注 + */ + private String remark; + /** + * 是否为主配置 + * + * 由于我们可以配置多个文件配置,默认情况下,使用主配置进行文件的上传 + */ + private Boolean master; + + /** + * 支付渠道配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private FileClientConfig config; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/file/FileContentDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/file/FileContentDO.java new file mode 100644 index 00000000..cd36122d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/file/FileContentDO.java @@ -0,0 +1,47 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 文件内容表 + * + * 专门用于存储 {@link com.chanko.yunxi.mes.heli.framework.file.core.client.db.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 +public class FileContentDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + @TableId(type = IdType.INPUT) + private String id; + /** + * 配置编号 + * + * 关联 {@link FileConfigDO#getId()} + */ + private Long configId; + /** + * 路径,即文件名 + */ + private String path; + /** + * 文件内容 + */ + private byte[] content; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/file/FileDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/file/FileDO.java new file mode 100644 index 00000000..ffc2b8a9 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/file/FileDO.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +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 +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/job/JobDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/job/JobDO.java new file mode 100644 index 00000000..911d7435 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/job/JobDO.java @@ -0,0 +1,74 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.job; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.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 +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/job/JobLogDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/job/JobLogDO.java new file mode 100644 index 00000000..6fd0bffb --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/job/JobLogDO.java @@ -0,0 +1,82 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.job; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.quartz.core.handler.JobHandler; +import com.chanko.yunxi.mes.heli.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 +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/logger/ApiAccessLogDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/logger/ApiAccessLogDO.java new file mode 100644 index 00000000..389dabce --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/logger/ApiAccessLogDO.java @@ -0,0 +1,109 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.logger; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.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 { + + /** + * 编号 + */ + @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; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 执行相关字段 ========== + + /** + * 开始请求时间 + */ + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/logger/ApiErrorLogDO.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/logger/ApiErrorLogDO.java new file mode 100644 index 00000000..80259bfe --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/dataobject/logger/ApiErrorLogDO.java @@ -0,0 +1,156 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.logger; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.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 { + + /** + * 编号 + */ + @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; + /** + * 处理用户编号 + * + * 关联 com.chanko.yunxi.mes.heli.adminserver.modules.system.dal.dataobject.user.SysUserDO.SysUserDO#getId() + */ + private Long processUserId; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/codegen/CodegenColumnMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/codegen/CodegenColumnMapper.java new file mode 100644 index 00000000..a9ee515d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/codegen/CodegenColumnMapper.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.codegen; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import org.apache.ibatis.annotations.Mapper; + +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::getId)); + } + + default void deleteListByTableId(Long tableId) { + delete(new LambdaQueryWrapperX() + .eq(CodegenColumnDO::getTableId, tableId)); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/codegen/CodegenTableMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/codegen/CodegenTableMapper.java new file mode 100644 index 00000000..abf0eb25 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/codegen/CodegenTableMapper.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.codegen; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import com.chanko.yunxi.mes.heli.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())); + } + + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/config/ConfigMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/config/ConfigMapper.java new file mode 100644 index 00000000..ee22137c --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/config/ConfigMapper.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.config; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import com.chanko.yunxi.mes.heli.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())); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/db/DataSourceConfigMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/db/DataSourceConfigMapper.java new file mode 100644 index 00000000..0be4c7b7 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/db/DataSourceConfigMapper.java @@ -0,0 +1,14 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.db; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.db.DataSourceConfigDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 数据源配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface DataSourceConfigMapper extends BaseMapperX { +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo01/Demo01ContactMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo01/Demo01ContactMapper.java new file mode 100644 index 00000000..115aee15 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo01/Demo01ContactMapper.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo01; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo02/Demo02CategoryMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo02/Demo02CategoryMapper.java new file mode 100644 index 00000000..8ca53dd4 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo02/Demo02CategoryMapper.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo02; + +import java.util.*; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 示例分类 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo03/Demo03CourseMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo03/Demo03CourseMapper.java new file mode 100644 index 00000000..829a2c85 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo03/Demo03CourseMapper.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo03; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 学生课程 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03CourseMapper extends BaseMapperX { + + default PageResult selectPage(PageParam reqVO, Long studentId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(Demo03CourseDO::getStudentId, studentId) + .orderByDesc(Demo03CourseDO::getId)); + } + + default List selectListByStudentId(Long studentId) { + return selectList(Demo03CourseDO::getStudentId, studentId); + } + + default int deleteByStudentId(Long studentId) { + return delete(Demo03CourseDO::getStudentId, studentId); + } + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo03/Demo03GradeMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo03/Demo03GradeMapper.java new file mode 100644 index 00000000..e2a97058 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo03/Demo03GradeMapper.java @@ -0,0 +1,32 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo03; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学生班级 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03GradeMapper 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); + } + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo03/Demo03StudentMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo03/Demo03StudentMapper.java new file mode 100644 index 00000000..3ef03f9c --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/demo/demo03/Demo03StudentMapper.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo03; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import org.apache.ibatis.annotations.Mapper; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo.*; + +/** + * 学生 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface Demo03StudentMapper extends BaseMapperX { + + default PageResult selectPage(Demo03StudentPageReqVO 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileConfigMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileConfigMapper.java new file mode 100644 index 00000000..261c110f --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileConfigMapper.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.file; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileContentDAOImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileContentDAOImpl.java new file mode 100644 index 00000000..116adbf3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileContentDAOImpl.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.file; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.file.core.client.db.DBFileContentFrameworkDAO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file.FileContentDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Optional; + +@Repository +public class FileContentDAOImpl implements DBFileContentFrameworkDAO { + + @Resource + private FileContentMapper fileContentMapper; + + @Override + public void insert(Long configId, String path, byte[] content) { + FileContentDO entity = new FileContentDO().setConfigId(configId) + .setPath(path).setContent(content); + fileContentMapper.insert(entity); + } + + @Override + public void delete(Long configId, String path) { + fileContentMapper.delete(buildQuery(configId, path)); + } + + @Override + public byte[] selectContent(Long configId, String path) { + List list = fileContentMapper.selectList( + buildQuery(configId, path).select(FileContentDO::getContent).orderByDesc(FileContentDO::getId)); + return Optional.ofNullable(CollUtil.getFirst(list)) + .map(FileContentDO::getContent) + .orElse(null); + } + + private LambdaQueryWrapper buildQuery(Long configId, String path) { + return new LambdaQueryWrapper() + .eq(FileContentDO::getConfigId, configId) + .eq(FileContentDO::getPath, path); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileContentMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileContentMapper.java new file mode 100644 index 00000000..ab2e166a --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileContentMapper.java @@ -0,0 +1,9 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.file; + +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file.FileContentDO; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FileContentMapper extends BaseMapper { +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileMapper.java new file mode 100644 index 00000000..e5526af5 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/file/FileMapper.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.file; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/job/JobLogMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/job/JobLogMapper.java new file mode 100644 index 00000000..4a69efdc --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/job/JobLogMapper.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.job; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/job/JobMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/job/JobMapper.java new file mode 100644 index 00000000..859c4097 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/job/JobMapper.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.job; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import com.chanko.yunxi.mes.heli.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()) + ); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/logger/ApiAccessLogMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/logger/ApiAccessLogMapper.java new file mode 100644 index 00000000..16260ca1 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/logger/ApiAccessLogMapper.java @@ -0,0 +1,45 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/logger/ApiErrorLogMapper.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/logger/ApiErrorLogMapper.java new file mode 100644 index 00000000..540354b6 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/dal/mysql/logger/ApiErrorLogMapper.java @@ -0,0 +1,44 @@ +package com.chanko.yunxi.mes.heli.module.infra.dal.mysql.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java new file mode 100644 index 00000000..5d378394 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenColumnListConditionEnum.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenColumnListConditionEnum.java new file mode 100644 index 00000000..32559ea2 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenColumnListConditionEnum.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenFrontTypeEnum.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenFrontTypeEnum.java new file mode 100644 index 00000000..51edb91b --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenFrontTypeEnum.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成的前端类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenFrontTypeEnum { + + VUE2(10), // Vue2 Element UI 标准模版 + VUE3(20), // Vue3 Element Plus 标准模版 + VUE3_SCHEMA(21), // Vue3 Element Plus Schema 模版 + VUE3_VBEN(30), // Vue3 VBEN 模版 + ; + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenSceneEnum.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenSceneEnum.java new file mode 100644 index 00000000..f6a2374f --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenSceneEnum.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenTemplateTypeEnum.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenTemplateTypeEnum.java new file mode 100644 index 00000000..cfada2a1 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/codegen/CodegenTemplateTypeEnum.java @@ -0,0 +1,53 @@ +package com.chanko.yunxi.mes.heli.module.infra.enums.codegen; + +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/config/ConfigTypeEnum.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/config/ConfigTypeEnum.java new file mode 100644 index 00000000..0e163fe5 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/config/ConfigTypeEnum.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/job/JobLogStatusEnum.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/job/JobLogStatusEnum.java new file mode 100644 index 00000000..531dd916 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/job/JobLogStatusEnum.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/job/JobStatusEnum.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/job/JobStatusEnum.java new file mode 100644 index 00000000..eaec4008 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/job/JobStatusEnum.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java new file mode 100644 index 00000000..96099935 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/package-info.java new file mode 100644 index 00000000..7a2a3058 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/enums/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.chanko.yunxi.mes.heli.module.infra.enums; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/codegen/config/CodegenConfiguration.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/codegen/config/CodegenConfiguration.java new file mode 100644 index 00000000..890fa0c0 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/codegen/config/CodegenConfiguration.java @@ -0,0 +1,9 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/codegen/config/CodegenProperties.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/codegen/config/CodegenProperties.java new file mode 100644 index 00000000..b3eea482 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/codegen/config/CodegenProperties.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.infra.framework.codegen.config; + +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenFrontTypeEnum; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Collection; + +@ConfigurationProperties(prefix = "mes.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; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/codegen/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/codegen/package-info.java new file mode 100644 index 00000000..e746dd17 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/codegen/package-info.java @@ -0,0 +1,4 @@ +/** + * 代码生成器 + */ +package com.chanko.yunxi.mes.heli.module.infra.framework.codegen; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/monitor/config/AdminServerConfiguration.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/monitor/config/AdminServerConfiguration.java new file mode 100644 index 00000000..10c2c485 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/monitor/config/AdminServerConfiguration.java @@ -0,0 +1,9 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/monitor/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/monitor/package-info.java new file mode 100644 index 00000000..845da96c --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/monitor/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Spring Boot Admin 实现简单的监控平台 + */ +package com.chanko.yunxi.mes.heli.module.infra.framework.monitor; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md new file mode 100644 index 00000000..eff64340 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/package-info.java new file mode 100644 index 00000000..c1ca8c20 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 infra 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.module.infra.framework; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/security/config/SecurityConfiguration.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/security/config/SecurityConfiguration.java new file mode 100644 index 00000000..6593ae65 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,47 @@ +package com.chanko.yunxi.mes.heli.module.infra.framework.security.config; + +import com.chanko.yunxi.mes.heli.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.ExpressionUrlAuthorizationConfigurer; + +/** + * 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(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // Swagger 接口文档 + registry.antMatchers("/v3/api-docs/**").permitAll() + .antMatchers("/swagger-ui.html").permitAll() + .antMatchers("/swagger-ui/**").permitAll() + .antMatchers("/swagger-resources/**").anonymous() + .antMatchers("/webjars/**").anonymous() + .antMatchers("/*/api-docs").anonymous(); + // Spring Boot Actuator 的安全配置 + registry.antMatchers("/actuator").anonymous() + .antMatchers("/actuator/**").anonymous(); + // Druid 监控 + registry.antMatchers("/druid/**").anonymous(); + // Spring Boot Admin Server 的安全配置 + registry.antMatchers(adminSeverContextPath).anonymous() + .antMatchers(adminSeverContextPath + "/**").anonymous(); + // 文件读取 + registry.antMatchers(buildAdminApi("/infra/file/*/get/**")).permitAll(); + } + + }; + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/security/core/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/security/core/package-info.java new file mode 100644 index 00000000..b4910750 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.chanko.yunxi.mes.heli.module.infra.framework.security.core; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/web/config/InfraWebConfiguration.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/web/config/InfraWebConfiguration.java new file mode 100644 index 00000000..403bf89d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/web/config/InfraWebConfiguration.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.infra.framework.web.config; + +import com.chanko.yunxi.mes.heli.framework.swagger.config.MesSwaggerAutoConfiguration; +import org.springdoc.core.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 MesSwaggerAutoConfiguration.buildGroupedOpenApi("infra"); + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/web/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/web/package-info.java new file mode 100644 index 00000000..cd1a3dc5 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * infra 模块的 web 配置 + */ +package com.chanko.yunxi.mes.heli.module.infra.framework.web; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/job/job/JobLogCleanJob.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/job/job/JobLogCleanJob.java new file mode 100644 index 00000000..5cc8a3e4 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/job/job/JobLogCleanJob.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.infra.job.job; + +import com.chanko.yunxi.mes.heli.framework.quartz.core.handler.JobHandler; +import com.chanko.yunxi.mes.heli.framework.tenant.core.aop.TenantIgnore; +import com.chanko.yunxi.mes.heli.module.infra.service.job.JobLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import javax.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/job/logger/AccessLogCleanJob.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/job/logger/AccessLogCleanJob.java new file mode 100644 index 00000000..0d4b669d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/job/logger/AccessLogCleanJob.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.infra.job.logger; + +import com.chanko.yunxi.mes.heli.framework.quartz.core.handler.JobHandler; +import com.chanko.yunxi.mes.heli.framework.tenant.core.aop.TenantIgnore; +import com.chanko.yunxi.mes.heli.module.infra.service.logger.ApiAccessLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/job/logger/ErrorLogCleanJob.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/job/logger/ErrorLogCleanJob.java new file mode 100644 index 00000000..2e3c68e4 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/job/logger/ErrorLogCleanJob.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.infra.job.logger; + +import com.chanko.yunxi.mes.heli.framework.quartz.core.handler.JobHandler; +import com.chanko.yunxi.mes.heli.framework.tenant.core.aop.TenantIgnore; +import com.chanko.yunxi.mes.heli.module.infra.service.logger.ApiErrorLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/mq/consumer/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/mq/consumer/package-info.java new file mode 100644 index 00000000..11998ed6 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/mq/consumer/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的消费者 + */ +package com.chanko.yunxi.mes.heli.module.infra.mq.consumer; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/mq/message/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/mq/message/package-info.java new file mode 100644 index 00000000..7f799d1e --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/mq/message/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的消息 + */ +package com.chanko.yunxi.mes.heli.module.infra.mq.message; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/mq/producer/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/mq/producer/package-info.java new file mode 100644 index 00000000..6bc24278 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/mq/producer/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的生产者 + */ +package com.chanko.yunxi.mes.heli.module.infra.mq.producer; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/package-info.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/package-info.java new file mode 100644 index 00000000..d3a3173e --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/package-info.java @@ -0,0 +1,9 @@ +/** + * infra 模块,主要提供两块能力: + * 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + * 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + * + * 1. Controller URL:以 /infra/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 infra_ 开头,方便在数据库中区分 + */ +package com.chanko.yunxi.mes.heli.module.infra; diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/CodegenService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/CodegenService.java new file mode 100644 index 00000000..72140845 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/CodegenService.java @@ -0,0 +1,101 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.codegen; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenTableDO; + +import java.util.List; +import java.util.Map; + +/** + * 代码生成 Service 接口 + * + * @author 芋道源码 + */ +public interface CodegenService { + + /** + * 基于数据库的表结构,创建代码生成器的表定义 + * + * @param userId 用户编号 + * @param reqVO 表信息 + * @return 创建的表定义的编号数组 + */ + List createCodegenList(Long userId, CodegenCreateListReqVO reqVO); + + /** + * 更新数据库的表和字段定义 + * + * @param updateReqVO 更新信息 + */ + void updateCodegen(CodegenUpdateReqVO updateReqVO); + + /** + * 基于数据库的表结构,同步数据库的表和字段定义 + * + * @param tableId 表编号 + */ + void syncCodegenFromDB(Long tableId); + + /** + * 删除数据库的表和字段定义 + * + * @param tableId 数据编号 + */ + void deleteCodegen(Long tableId); + + /** + * 获得表定义列表 + * + * @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/CodegenServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/CodegenServiceImpl.java new file mode 100644 index 00000000..53900dfc --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/CodegenServiceImpl.java @@ -0,0 +1,288 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.codegen; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.codegen.CodegenColumnMapper; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.codegen.CodegenTableMapper; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenSceneEnum; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.chanko.yunxi.mes.heli.module.infra.framework.codegen.config.CodegenProperties; +import com.chanko.yunxi.mes.heli.module.infra.service.codegen.inner.CodegenBuilder; +import com.chanko.yunxi.mes.heli.module.infra.service.codegen.inner.CodegenEngine; +import com.chanko.yunxi.mes.heli.module.infra.service.db.DatabaseTableService; +import com.chanko.yunxi.mes.heli.module.system.api.user.AdminUserApi; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.BiPredicate; +import java.util.stream.Collectors; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertMap; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; +import static com.chanko.yunxi.mes.heli.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 AdminUserApi userApi; + + @Resource + private CodegenBuilder codegenBuilder; + @Resource + private CodegenEngine codegenEngine; + + @Resource + private CodegenProperties codegenProperties; + + @Override + @Transactional(rollbackFor = Exception.class) + public List createCodegenList(Long userId, CodegenCreateListReqVO reqVO) { + List ids = new ArrayList<>(reqVO.getTableNames().size()); + // 遍历添加。虽然效率会低一点,但是没必要做成完全批量,因为不会这么大量 + reqVO.getTableNames().forEach(tableName -> ids.add(createCodegen(userId, reqVO.getDataSourceConfigId(), tableName))); + return ids; + } + + private Long createCodegen(Long userId, Long dataSourceConfigId, String tableName) { + // 从数据库中,获得数据库表结构 + TableInfo tableInfo = databaseTableService.getTable(dataSourceConfigId, tableName); + // 导入 + return createCodegen0(userId, dataSourceConfigId, tableInfo); + } + + private Long createCodegen0(Long userId, 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(userApi.getUser(userId).getNickname()); + 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 = tableFields.stream() + .filter(tableField -> codegenColumnDOMap.get(tableField.getColumnName()) != null + && !primaryKeyPredicate.test(tableField, codegenColumnDOMap.get(tableField.getColumnName()))) + .map(TableField::getColumnName) + .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.deleteBatchIds(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 + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/inner/CodegenBuilder.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/inner/CodegenBuilder.java new file mode 100644 index 00000000..ded63134 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/inner/CodegenBuilder.java @@ -0,0 +1,221 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.codegen.inner; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.infra.convert.codegen.CodegenConvert; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenColumnListConditionEnum; +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.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[]{"你猜", "随便", "你说的对"})); + return; + } + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/inner/CodegenEngine.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/inner/CodegenEngine.java new file mode 100644 index 00000000..7c344c59 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/codegen/inner/CodegenEngine.java @@ -0,0 +1,504 @@ +package com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.date.LocalDateTimeUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.ObjectUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.string.StrUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenFrontTypeEnum; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenSceneEnum; +import com.chanko.yunxi.mes.heli.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.chanko.yunxi.mes.heli.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 org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +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 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 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 标准模版 + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/index.vue"), + vueFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("api/api.js"), + vueFilePath("api/${table.moduleName}/${table.businessName}/index.js")) + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/form.vue"), + vueFilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑 + vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) + // Vue3 标准模版 + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑 + vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + // Vue3 Schema 模版 + .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) + .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + // Vue3 vben 模版 + .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) + .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + .build(); + + @Resource + private CodegenProperties codegenProperties; + + /** + * 模板引擎,由 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); + } + + @PostConstruct + @VisibleForTesting + void initGlobalBindingMap() { + // 全局配置 + globalBindingMap.put("basePackage", codegenProperties.getBasePackage()); + globalBindingMap.put("baseFrameworkPackage", codegenProperties.getBasePackage() + + '.' + "framework"); // 用于后续获取测试类的 package 地址 + // 全局 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("OperateLogClassName", OperateLog.class.getName()); + globalBindingMap.put("OperateTypeEnumClassName", OperateTypeEnum.class.getName()); + globalBindingMap.put("BeanUtils", BeanUtils.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 simpleClassName = removePrefix(table.getClassName(), upperFirst(table.getModuleName())); + bindingMap.put("simpleClassName", simpleClassName); + bindingMap.put("simpleClassName_underlineCase", toUnderlineCase(simpleClassName)); // 将 DictType 转换成 dict_type + bindingMap.put("classNameVar", lowerFirst(simpleClassName)); // 将 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); + } + return bindingMap; + } + + private Map getTemplates(Integer frontType) { + Map templates = new LinkedHashMap<>(); + templates.putAll(SERVER_TEMPLATES); + templates.putAll(FRONT_TEMPLATES.row(frontType)); + 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)); + } + 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, "biz", "main"); + } + + private static String javaModuleImplControllerFilePath() { + return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + + "${sceneEnum.prefixClass}${table.className}Controller", "biz", "main"); + } + + private static String javaModuleImplMainFilePath(String path) { + return javaModuleFilePath(path, "biz", "main"); + } + + private static String javaModuleApiMainFilePath(String path) { + return javaModuleFilePath(path, "api", "main"); + } + + private static String javaModuleImplTestFilePath(String path) { + return javaModuleFilePath(path, "biz", "test"); + } + + private static String javaModuleFilePath(String path, String module, String src) { + return "mes-module-${table.moduleName}/" + // 顶级模块 + "mes-module-${table.moduleName}-" + module + "/" + // 子模块 + "src/" + src + "/java/${basePackage}/module/${table.moduleName}/" + path + ".java"; + } + + private static String mapperXmlFilePath() { + return "mes-module-${table.moduleName}/" + // 顶级模块 + "mes-module-${table.moduleName}-biz/" + // 子模块 + "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 "mes-ui-${sceneEnum.basePackage}-vue2/" + // 顶级目录 + "src/" + path; + } + + private static String vue3TemplatePath(String path) { + return "codegen/vue3/" + path + ".vm"; + } + + private static String vue3FilePath(String path) { + return "mes-ui-${sceneEnum.basePackage}-vue3/" + // 顶级目录 + "src/" + path; + } + + private static String vue3SchemaTemplatePath(String path) { + return "codegen/vue3_schema/" + path + ".vm"; + } + + private static String vue3VbenTemplatePath(String path) { + return "codegen/vue3_vben/" + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/config/ConfigService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/config/ConfigService.java new file mode 100644 index 00000000..4a79888e --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/config/ConfigService.java @@ -0,0 +1,63 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.config; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo.ConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.config.ConfigDO; + +import javax.validation.Valid; + +/** + * 参数配置 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 id 配置编号 + * @return 参数配置 + */ + ConfigDO getConfig(Long id); + + /** + * 根据参数键,获得参数配置 + * + * @param key 配置键 + * @return 参数配置 + */ + ConfigDO getConfigByKey(String key); + + /** + * 获得参数配置分页列表 + * + * @param reqVO 分页条件 + * @return 分页列表 + */ + PageResult getConfigPage(@Valid ConfigPageReqVO reqVO); + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/config/ConfigServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/config/ConfigServiceImpl.java new file mode 100644 index 00000000..c52bf602 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/config/ConfigServiceImpl.java @@ -0,0 +1,109 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.config; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.config.vo.ConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.convert.config.ConfigConvert; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.config.ConfigDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.config.ConfigMapper; +import com.chanko.yunxi.mes.heli.module.infra.enums.config.ConfigTypeEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.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 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DataSourceConfigService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DataSourceConfigService.java new file mode 100644 index 00000000..75860186 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DataSourceConfigService.java @@ -0,0 +1,53 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.db; + +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.db.DataSourceConfigDO; + +import javax.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 id 编号 + * @return 数据源配置 + */ + DataSourceConfigDO getDataSourceConfig(Long id); + + /** + * 获得数据源配置列表 + * + * @return 数据源配置列表 + */ + List getDataSourceConfigList(); + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DataSourceConfigServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DataSourceConfigServiceImpl.java new file mode 100644 index 00000000..3344e398 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DataSourceConfigServiceImpl.java @@ -0,0 +1,106 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.db; + +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.util.JdbcUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.db.DataSourceConfigMapper; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_EXISTS; +import static com.chanko.yunxi.mes.heli.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); + } + + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DatabaseTableService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DatabaseTableService.java new file mode 100644 index 00000000..4f357e6f --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DatabaseTableService.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DatabaseTableServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DatabaseTableServiceImpl.java new file mode 100644 index 00000000..1a8623a3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/db/DatabaseTableServiceImpl.java @@ -0,0 +1,69 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.db; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.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 org.springframework.stereotype.Service; + +import javax.annotation.Resource; +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 dataSourceConfig = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(), + config.getPassword()).build(); + StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder(); + if (StrUtil.isNotEmpty(name)) { + strategyConfig.addInclude(name); + } else { + // 移除工作流和定时任务前缀的表名 // TODO 未来做成可配置 + strategyConfig.addExclude("ACT_[\\S\\s]+|QRTZ_[\\S\\s]+|FLW_[\\S\\s]+"); + } + + GlobalConfig globalConfig = new GlobalConfig.Builder().dateType(DateType.TIME_PACK).build(); // 只使用 LocalDateTime 类型,不使用 LocalDate + ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfig, strategyConfig.build(), + null, globalConfig, null); + // 按照名字排序 + List tables = builder.getTableInfoList(); + tables.sort(Comparator.comparing(TableInfo::getName)); + return tables; + } + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo01/Demo01ContactService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo01/Demo01ContactService.java new file mode 100644 index 00000000..e7a9a70e --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo01/Demo01ContactService.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.demo.demo01; + +import javax.validation.*; + +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; + +/** + * 示例联系人 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 id 编号 + * @return 示例联系人 + */ + Demo01ContactDO getDemo01Contact(Long id); + + /** + * 获得示例联系人分页 + * + * @param pageReqVO 分页查询 + * @return 示例联系人分页 + */ + PageResult getDemo01ContactPage(Demo01ContactPageReqVO pageReqVO); + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo01/Demo01ContactServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo01/Demo01ContactServiceImpl.java new file mode 100644 index 00000000..c277e048 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo01/Demo01ContactServiceImpl.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.demo.demo01; + +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO; +import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import org.springframework.validation.annotation.Validated; + +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; + +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo01.Demo01ContactMapper; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants.*; + +/** + * 示例联系人 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); + } + + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo02/Demo02CategoryService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo02/Demo02CategoryService.java new file mode 100644 index 00000000..28def394 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo02/Demo02CategoryService.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.demo.demo02; + +import java.util.*; +import javax.validation.*; + +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; + +/** + * 示例分类 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo02/Demo02CategoryServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo02/Demo02CategoryServiceImpl.java new file mode 100644 index 00000000..fa29d19c --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo02/Demo02CategoryServiceImpl.java @@ -0,0 +1,134 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.demo.demo02; + +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo02.Demo02CategoryMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo03/Demo03StudentService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo03/Demo03StudentService.java new file mode 100644 index 00000000..35627b69 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo03/Demo03StudentService.java @@ -0,0 +1,158 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.demo.demo03; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo.Demo03StudentPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo.Demo03StudentSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 学生 Service 接口 + * + * @author 芋道源码 + */ +public interface Demo03StudentService { + + /** + * 创建学生 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemo03Student(@Valid Demo03StudentSaveReqVO createReqVO); + + /** + * 更新学生 + * + * @param updateReqVO 更新信息 + */ + void updateDemo03Student(@Valid Demo03StudentSaveReqVO updateReqVO); + + /** + * 删除学生 + * + * @param id 编号 + */ + void deleteDemo03Student(Long id); + + /** + * 获得学生 + * + * @param id 编号 + * @return 学生 + */ + Demo03StudentDO getDemo03Student(Long id); + + /** + * 获得学生分页 + * + * @param pageReqVO 分页查询 + * @return 学生分页 + */ + PageResult getDemo03StudentPage(Demo03StudentPageReqVO pageReqVO); + + + // ==================== 子表(学生课程) ==================== + + /** + * 获得学生课程列表 + * + * @param studentId 学生编号 + * @return 学生课程列表 + */ + List getDemo03CourseListByStudentId(Long studentId); + + /** + * 获得学生课程分页 + * + * @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 id 编号 + * @return 学生课程 + */ + Demo03CourseDO getDemo03Course(Long id); + + // ==================== 子表(学生班级) ==================== + + /** + * 获得学生班级 + * + * @param studentId 学生编号 + * @return 学生班级 + */ + Demo03GradeDO getDemo03GradeByStudentId(Long studentId); + + /** + * 获得学生班级分页 + * + * @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 id 编号 + * @return 学生班级 + */ + Demo03GradeDO getDemo03Grade(Long id); + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo03/Demo03StudentServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo03/Demo03StudentServiceImpl.java new file mode 100644 index 00000000..12f8a760 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/demo/demo03/Demo03StudentServiceImpl.java @@ -0,0 +1,217 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.demo.demo03; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo.Demo03StudentPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.demo.demo03.vo.Demo03StudentSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo03.Demo03CourseMapper; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo03.Demo03GradeMapper; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.demo.demo03.Demo03StudentMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants.*; + +/** + * 学生 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class Demo03StudentServiceImpl implements Demo03StudentService { + + @Resource + private Demo03StudentMapper demo03StudentMapper; + @Resource + private Demo03CourseMapper demo03CourseMapper; + @Resource + private Demo03GradeMapper demo03GradeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createDemo03Student(Demo03StudentSaveReqVO createReqVO) { + // 插入 + Demo03StudentDO demo03Student = BeanUtils.toBean(createReqVO, Demo03StudentDO.class); + demo03StudentMapper.insert(demo03Student); + + // 插入子表 + createDemo03CourseList(demo03Student.getId(), createReqVO.getDemo03Courses()); + createDemo03Grade(demo03Student.getId(), createReqVO.getDemo03Grade()); + // 返回 + return demo03Student.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDemo03Student(Demo03StudentSaveReqVO updateReqVO) { + // 校验存在 + validateDemo03StudentExists(updateReqVO.getId()); + // 更新 + Demo03StudentDO updateObj = BeanUtils.toBean(updateReqVO, Demo03StudentDO.class); + demo03StudentMapper.updateById(updateObj); + + // 更新子表 + updateDemo03CourseList(updateReqVO.getId(), updateReqVO.getDemo03Courses()); + updateDemo03Grade(updateReqVO.getId(), updateReqVO.getDemo03Grade()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDemo03Student(Long id) { + // 校验存在 + validateDemo03StudentExists(id); + // 删除 + demo03StudentMapper.deleteById(id); + + // 删除子表 + deleteDemo03CourseByStudentId(id); + deleteDemo03GradeByStudentId(id); + } + + private void validateDemo03StudentExists(Long id) { + if (demo03StudentMapper.selectById(id) == null) { + throw exception(DEMO03_STUDENT_NOT_EXISTS); + } + } + + @Override + public Demo03StudentDO getDemo03Student(Long id) { + return demo03StudentMapper.selectById(id); + } + + @Override + public PageResult getDemo03StudentPage(Demo03StudentPageReqVO pageReqVO) { + return demo03StudentMapper.selectPage(pageReqVO); + } + + // ==================== 子表(学生课程) ==================== + + @Override + public List getDemo03CourseListByStudentId(Long studentId) { + return demo03CourseMapper.selectListByStudentId(studentId); + } + + private void createDemo03CourseList(Long studentId, List list) { + if (list != null) { + list.forEach(o -> o.setStudentId(studentId)); + } + demo03CourseMapper.insertBatch(list); + } + + private void updateDemo03CourseList(Long studentId, List list) { + deleteDemo03CourseByStudentId(studentId); + list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新 + createDemo03CourseList(studentId, list); + } + + private void deleteDemo03CourseByStudentId(Long studentId) { + demo03CourseMapper.deleteByStudentId(studentId); + } + + @Override + public PageResult getDemo03CoursePage(PageParam pageReqVO, Long studentId) { + return demo03CourseMapper.selectPage(pageReqVO, studentId); + } + + @Override + public Long createDemo03Course(Demo03CourseDO demo03Course) { + demo03CourseMapper.insert(demo03Course); + return demo03Course.getId(); + } + + @Override + public void updateDemo03Course(Demo03CourseDO demo03Course) { + demo03CourseMapper.updateById(demo03Course); + } + + @Override + public void deleteDemo03Course(Long id) { + demo03CourseMapper.deleteById(id); + } + + @Override + public Demo03CourseDO getDemo03Course(Long id) { + return demo03CourseMapper.selectById(id); + } + + // ==================== 子表(学生班级) ==================== + + @Override + public Demo03GradeDO getDemo03GradeByStudentId(Long studentId) { + return demo03GradeMapper.selectByStudentId(studentId); + } + + private void createDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) { + if (demo03Grade == null) { + return; + } + demo03Grade.setStudentId(studentId); + demo03GradeMapper.insert(demo03Grade); + } + + private void updateDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) { + if (demo03Grade == null) { + return; + } + demo03Grade.setStudentId(studentId); + demo03Grade.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新 + demo03GradeMapper.insertOrUpdate(demo03Grade); + } + + private void deleteDemo03GradeByStudentId(Long studentId) { + demo03GradeMapper.deleteByStudentId(studentId); + } + + @Override + public PageResult getDemo03GradePage(PageParam pageReqVO, Long studentId) { + return demo03GradeMapper.selectPage(pageReqVO, studentId); + } + + @Override + public Long createDemo03Grade(Demo03GradeDO demo03Grade) { + // 校验是否已经存在 + if (demo03GradeMapper.selectByStudentId(demo03Grade.getStudentId()) != null) { + throw exception(DEMO03_GRADE_EXISTS); + } + demo03GradeMapper.insert(demo03Grade); + return demo03Grade.getId(); + } + + @Override + public void updateDemo03Grade(Demo03GradeDO demo03Grade) { + // 校验存在 + validateDemo03GradeExists(demo03Grade.getId()); + // 更新 + demo03GradeMapper.updateById(demo03Grade); + } + + @Override + public void deleteDemo03Grade(Long id) { + // 校验存在 + validateDemo03GradeExists(id); + // 删除 + demo03GradeMapper.deleteById(id); + } + + @Override + public Demo03GradeDO getDemo03Grade(Long id) { + return demo03GradeMapper.selectById(id); + } + + private void validateDemo03GradeExists(Long id) { + if (demo03GradeMapper.selectById(id) == null) { + throw exception(DEMO03_GRADE_NOT_EXISTS); + } + } + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileConfigService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileConfigService.java new file mode 100644 index 00000000..fa58e49d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileConfigService.java @@ -0,0 +1,86 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.file; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClient; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file.FileConfigDO; + +import javax.validation.Valid; + +/** + * 文件配置 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 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileConfigServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileConfigServiceImpl.java new file mode 100644 index 00000000..ae649634 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileConfigServiceImpl.java @@ -0,0 +1,190 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.file; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.validation.ValidationUtils; +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClient; +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientConfig; +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClientFactory; +import com.chanko.yunxi.mes.heli.framework.file.core.enums.FileStorageEnum; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.convert.file.FileConfigConvert; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file.FileConfigDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.file.FileConfigMapper; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +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 javax.annotation.Resource; +import javax.validation.Validator; +import java.time.Duration; +import java.util.Map; +import java.util.Objects; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; +import static com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER; +import static com.chanko.yunxi.mes.heli.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); + } + + /** + * 清空指定文件配置 + * + * @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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileService.java new file mode 100644 index 00000000..935416a0 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileService.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.file; + +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file.FileDO; + +/** + * 文件 Service 接口 + * + * @author 芋道源码 + */ +public interface FileService { + + /** + * 获得文件分页 + * + * @param pageReqVO 分页查询 + * @return 文件分页 + */ + PageResult getFilePage(FilePageReqVO pageReqVO); + + /** + * 保存文件,并返回文件的访问路径 + * + * @param name 文件名称 + * @param path 文件路径 + * @param content 文件内容 + * @return 文件路径 + */ + String createFile(String name, String path, byte[] content); + + /** + * 删除文件 + * + * @param id 编号 + */ + void deleteFile(Long id) throws Exception; + + /** + * 获得文件内容 + * + * @param configId 配置编号 + * @param path 文件路径 + * @return 文件内容 + */ + byte[] getFileContent(Long configId, String path) throws Exception; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileServiceImpl.java new file mode 100644 index 00000000..9e3c3db1 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/file/FileServiceImpl.java @@ -0,0 +1,98 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.file; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.io.FileUtils; +import com.chanko.yunxi.mes.heli.framework.file.core.client.FileClient; +import com.chanko.yunxi.mes.heli.framework.file.core.utils.FileTypeUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.file.FileDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.file.FileMapper; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; + +/** + * 文件 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class FileServiceImpl implements FileService { + + @Resource + private FileConfigService fileConfigService; + + @Resource + private FileMapper fileMapper; + + @Override + public PageResult getFilePage(FilePageReqVO pageReqVO) { + return fileMapper.selectPage(pageReqVO); + } + + @Override + @SneakyThrows + public String createFile(String name, String path, byte[] content) { + // 计算默认的 path 名 + String type = FileTypeUtils.getMineType(content, name); + if (StrUtil.isEmpty(path)) { + path = FileUtils.generatePath(content, name); + } + // 如果 name 为空,则使用 path 填充 + if (StrUtil.isEmpty(name)) { + name = path; + } + + // 上传到文件存储器 + FileClient client = fileConfigService.getMasterFileClient(); + Assert.notNull(client, "客户端(master) 不能为空"); + String url = client.upload(content, path, type); + + // 保存到数据库 + FileDO file = new FileDO(); + file.setConfigId(client.getId()); + file.setName(name); + file.setPath(path); + file.setUrl(url); + file.setType(type); + file.setSize(content.length); + fileMapper.insert(file); + return url; + } + + @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); + } + + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobLogService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobLogService.java new file mode 100644 index 00000000..fc59b7ec --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobLogService.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.job; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.quartz.core.service.JobLogFrameworkService; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobLogServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobLogServiceImpl.java new file mode 100644 index 00000000..f52411f8 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobLogServiceImpl.java @@ -0,0 +1,80 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.job; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.job.JobLogDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.job.JobLogMapper; +import com.chanko.yunxi.mes.heli.module.infra.enums.job.JobLogStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobService.java new file mode 100644 index 00000000..befb8f8a --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobService.java @@ -0,0 +1,71 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.job; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job.JobSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.job.JobDO; +import org.quartz.SchedulerException; + +import javax.validation.Valid; + +/** + * 定时任务 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; + + /** + * 删除定时任务 + * + * @param id 编号 + */ + void deleteJob(Long id) throws SchedulerException; + + /** + * 获得定时任务 + * + * @param id 编号 + * @return 定时任务 + */ + JobDO getJob(Long id); + + /** + * 获得定时任务分页 + * + * @param pageReqVO 分页查询 + * @return 定时任务分页 + */ + PageResult getJobPage(JobPageReqVO pageReqVO); + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobServiceImpl.java new file mode 100644 index 00000000..01c79235 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/job/JobServiceImpl.java @@ -0,0 +1,161 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.job; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.quartz.core.scheduler.SchedulerManager; +import com.chanko.yunxi.mes.heli.framework.quartz.core.util.CronUtils; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.job.vo.job.JobSaveReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.job.JobDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.job.JobMapper; +import com.chanko.yunxi.mes.heli.module.infra.enums.job.JobStatusEnum; +import org.quartz.SchedulerException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.containsAny; +import static com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants.*; + +/** + * 定时任务 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +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()); + // 校验唯一性 + if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) { + throw exception(JOB_HANDLER_EXISTS); + } + // 插入 + JobDO job = BeanUtils.toBean(createReqVO, JobDO.class); + job.setStatus(JobStatusEnum.INIT.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + + // 添加 Job 到 Quartz 中 + schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), + createReqVO.getRetryCount(), createReqVO.getRetryInterval()); + // 更新 + 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()); + // 校验存在 + JobDO job = validateJobExists(updateReqVO.getId()); + // 只有开启状态,才可以修改.原因是,如果出暂停状态,修改 Quartz Job 时,会导致任务又开始执行 + if (!job.getStatus().equals(JobStatusEnum.NORMAL.getStatus())) { + throw exception(JOB_UPDATE_ONLY_NORMAL_STATUS); + } + // 更新 + JobDO updateObj = BeanUtils.toBean(updateReqVO, JobDO.class); + fillJobMonitorTimeoutEmpty(updateObj); + jobMapper.updateById(updateObj); + + // 更新 Job 到 Quartz 中 + schedulerManager.updateJob(job.getHandlerName(), updateReqVO.getHandlerParam(), updateReqVO.getCronExpression(), + updateReqVO.getRetryCount(), updateReqVO.getRetryInterval()); + } + + @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 deleteJob(Long id) throws SchedulerException { + // 校验存在 + JobDO job = validateJobExists(id); + // 更新 + jobMapper.deleteById(id); + + // 删除 Job 到 Quartz 中 + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiAccessLogService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiAccessLogService.java new file mode 100644 index 00000000..5b665ee4 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiAccessLogService.java @@ -0,0 +1,38 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiAccessLogServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiAccessLogServiceImpl.java new file mode 100644 index 00000000..499e5ac0 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiAccessLogServiceImpl.java @@ -0,0 +1,57 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.logger.ApiAccessLogMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +/** + * 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); + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiErrorLogService.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiErrorLogService.java new file mode 100644 index 00000000..464553ef --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiErrorLogService.java @@ -0,0 +1,47 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiErrorLogServiceImpl.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiErrorLogServiceImpl.java new file mode 100644 index 00000000..b4b99fbf --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/service/logger/ApiErrorLogServiceImpl.java @@ -0,0 +1,76 @@ +package com.chanko.yunxi.mes.heli.module.infra.service.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import com.chanko.yunxi.mes.heli.module.infra.dal.mysql.logger.ApiErrorLogMapper; +import com.chanko.yunxi.mes.heli.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants.*; + +/** + * API 错误日志 Service 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@Service +@Validated +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()); + 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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/websocket/DemoWebSocketMessageListener.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/websocket/DemoWebSocketMessageListener.java new file mode 100644 index 00000000..d9531b5c --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/websocket/DemoWebSocketMessageListener.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.module.infra.websocket; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.websocket.core.listener.WebSocketMessageListener; +import com.chanko.yunxi.mes.heli.framework.websocket.core.sender.WebSocketMessageSender; +import com.chanko.yunxi.mes.heli.framework.websocket.core.util.WebSocketFrameworkUtils; +import com.chanko.yunxi.mes.heli.module.infra.websocket.message.DemoReceiveMessage; +import com.chanko.yunxi.mes.heli.module.infra.websocket.message.DemoSendMessage; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +import javax.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/websocket/message/DemoReceiveMessage.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/websocket/message/DemoReceiveMessage.java new file mode 100644 index 00000000..bed9c81d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/websocket/message/DemoReceiveMessage.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.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/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/websocket/message/DemoSendMessage.java b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/websocket/message/DemoSendMessage.java new file mode 100644 index 00000000..b32a9960 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/java/com/chanko/yunxi/mes/heli/module/infra/websocket/message/DemoSendMessage.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.infra.websocket.message; + +import lombok.Data; + +/** + * 示例:client -> server 发送消息 + * + * @author 芋道源码 + */ +@Data +public class DemoSendMessage { + + /** + * 发送给谁 + * + * 如果为空,说明发送给所有人 + */ + private Long toUserId; + /** + * 内容 + */ + private String text; + +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm new file mode 100644 index 00000000..f58ce0cb --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm @@ -0,0 +1,233 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}; + +import org.springframework.web.bind.annotation.*; +import jakarta.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 jakarta.validation.constraints.*; +import jakarta.validation.*; +import jakarta.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import ${PageParamClassName}; +import ${PageResultClassName}; +import ${CommonResultClassName}; +import ${BeanUtils}; +import static ${CommonResultClassName}.success; + +import ${ExcelUtilsClassName}; + +import ${OperateLogClassName}; +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 ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO) { + return success(${classNameVar}Service.create${simpleClassName}(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新${table.classComment}") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')") +#end + public CommonResult update${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO) { + ${classNameVar}Service.update${simpleClassName}(updateReqVO); + 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); + } + + @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<${sceneEnum.prefixClass}${table.className}RespVO> get${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { + ${table.className}DO ${classNameVar} = ${classNameVar}Service.get${simpleClassName}(id); + return success(BeanUtils.toBean(${classNameVar}, ${sceneEnum.prefixClass}${table.className}RespVO.class)); + } + +#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); + return success(BeanUtils.toBean(pageResult, ${sceneEnum.prefixClass}${table.className}RespVO.class)); + } + +## 特殊:树表专属逻辑(树不需要分页接口) +#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); + return success(BeanUtils.toBean(list, ${sceneEnum.prefixClass}${table.className}RespVO.class)); + } + +#end + @GetMapping("/export-excel") + @Operation(summary = "导出${table.classComment} Excel") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:export')") +#end + @OperateLog(type = 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", "数据", ${table.className}RespVO.class, + BeanUtils.toBean(list, ${table.className}RespVO.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); + } + + @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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/listReqVO.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/listReqVO.vm new file mode 100644 index 00000000..46b6a259 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm new file mode 100644 index 00000000..003bac90 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm @@ -0,0 +1,47 @@ +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 +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm new file mode 100644 index 00000000..54c16671 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm @@ -0,0 +1,54 @@ +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 字段的引入 +import java.util.*; +#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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/saveReqVO.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/saveReqVO.vm new file mode 100644 index 00000000..5e03326c --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/controller/vo/saveReqVO.vm @@ -0,0 +1,65 @@ +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 jakarta.validation.constraints.*; +## 处理 BigDecimal 字段的引入 +import java.util.*; +#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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/do.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/do.vm new file mode 100644 index 00000000..b019d6e1 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/do.vm @@ -0,0 +1,52 @@ +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}; + +/** + * ${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 +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 + private ${column.javaType} ${column.javaField}; +#end +#end + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/do_sub.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/do_sub.vm new file mode 100644 index 00000000..16be55e8 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/do_sub.vm @@ -0,0 +1,49 @@ +#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}; + +/** + * ${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 +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 + private ${column.javaType} ${column.javaField}; +#end +#end + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm new file mode 100644 index 00000000..b98b471f --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm new file mode 100644 index 00000000..290378d3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/mapper_sub.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/mapper_sub.vm new file mode 100644 index 00000000..e5589e99 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/dal/mapper_sub.vm @@ -0,0 +1,51 @@ +#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 倒序 + + } + +## 情况二:非 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}); + } + +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm new file mode 100644 index 00000000..4f8308e4 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm @@ -0,0 +1,22 @@ +// TODO 待办:请将下面的错误码复制到 mes-module-${table.moduleName}-api 模块的 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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/service/service.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/service/service.vm new file mode 100644 index 00000000..828cabdf --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/service/service.vm @@ -0,0 +1,147 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import java.util.*; +import jakarta.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 createReqVO 创建信息 + * @return 编号 + */ + ${primaryColumn.javaType} create${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO); + + /** + * 更新${table.classComment} + * + * @param updateReqVO 更新信息 + */ + void update${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO); + + /** + * 删除${table.classComment} + * + * @param id 编号 + */ + void delete${simpleClassName}(${primaryColumn.javaType} id); + + /** + * 获得${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); + + /** + * 获得${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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm new file mode 100644 index 00000000..4d707092 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm @@ -0,0 +1,350 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import org.springframework.stereotype.Service; +import jakarta.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 ${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}(${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO) { +## 特殊:树表专属逻辑 +#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, createReqVO.get${TreeParentJavaField}()); + // 校验${treeNameColumn.columnComment}的唯一性 + validate${simpleClassName}${TreeNameJavaField}Unique(null, createReqVO.get${TreeParentJavaField}(), createReqVO.get${TreeNameJavaField}()); + +#end + // 插入 + ${table.className}DO ${classNameVar} = BeanUtils.toBean(createReqVO, ${table.className}DO.class); + ${classNameVar}Mapper.insert(${classNameVar}); +## 特殊:主子表专属逻辑(非 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(), createReqVO.get${subSimpleClassNames.get($index)}s()); + #else + create${subSimpleClassName}(${classNameVar}.getId(), createReqVO.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}(${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO) { + // 校验存在 + validate${simpleClassName}Exists(updateReqVO.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}(updateReqVO.getId(), updateReqVO.get${TreeParentJavaField}()); + // 校验${treeNameColumn.columnComment}的唯一性 + validate${simpleClassName}${TreeNameJavaField}Unique(updateReqVO.getId(), updateReqVO.get${TreeParentJavaField}(), updateReqVO.get${TreeNameJavaField}()); + +#end + // 更新 + ${table.className}DO updateObj = BeanUtils.toBean(updateReqVO, ${table.className}DO.class); + ${classNameVar}Mapper.updateById(updateObj); +## 特殊:主子表专属逻辑(非 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(updateReqVO.getId(), updateReqVO.get${subSimpleClassNames.get($index)}s()); + #else + update${subSimpleClassName}(updateReqVO.getId(), updateReqVO.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 + } + + 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 + ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar}); + return ${subClassNameVar}.getId(); + } + + @Override + public void update${subSimpleClassName}(${subTable.className}DO ${subClassNameVar}) { + // 校验存在 + validate${subSimpleClassName}Exists(${subClassNameVar}.getId()); + // 更新 + ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar}); + } + + @Override + public void delete${subSimpleClassName}(${subPrimaryColumn.javaType} id) { + // 校验存在 + validate${subSimpleClassName}Exists(id); + // 删除 + ${subClassNameVars.get($index)}Mapper.deleteById(id); + } + + @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})); + ${subClassNameVars.get($index)}Mapper.insertBatch(list); + } + + private void update${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) { + delete${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}); + list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新 + create${subSimpleClassName}List(${subJoinColumn.javaField}, list); + } + + #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}); + ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下: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}); + } + +#end +} \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm new file mode 100644 index 00000000..f7294506 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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 jakarta.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 jakarta.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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/sql/h2.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/sql/h2.vm new file mode 100644 index 00000000..e7fb70e8 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/sql/h2.vm @@ -0,0 +1,37 @@ +-- 将该建表 SQL 语句,添加到 mes-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} == 'tenantId') + "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 语句,添加到 mes-module-${table.moduleName}-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "${table.tableName}"; \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/sql/sql.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/sql/sql.vm new file mode 100644 index 00000000..41b107db --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm new file mode 100644 index 00000000..bfe2dc00 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm @@ -0,0 +1,147 @@ +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' + }) +} + +// 获得${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' + }) +} +## 特殊:主子表专属逻辑 TODO @puhui999:下面方法的【空格】不太对 +#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' + }) + } + + // 获得${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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm new file mode 100644 index 00000000..99aa91af --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm new file mode 100644 index 00000000..ca266be9 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm new file mode 100644 index 00000000..48a404a3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm new file mode 100644 index 00000000..589736b6 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm @@ -0,0 +1,165 @@ +#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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm new file mode 100644 index 00000000..90b8e415 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/form.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/form.vue.vm new file mode 100644 index 00000000..634d05d3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/form.vue.vm @@ -0,0 +1,320 @@ + + + diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm new file mode 100644 index 00000000..e2cf95be --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm @@ -0,0 +1,340 @@ + + + diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm new file mode 100644 index 00000000..c4b0b433 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm @@ -0,0 +1,111 @@ +import request from '@/config/axios' +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +export interface ${simpleClassName}VO { +#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}: number +#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime") + ${column.javaField}: Date +#else + ${column.javaField}: ${column.javaType.toLowerCase()} +#end +#end +#end +} + +#if ( $table.templateType != 2 ) +// 查询${table.classComment}分页 +export const get${simpleClassName}Page = async (params) => { + return await request.get({ url: `${baseURL}/page`, params }) +} +#else +// 查询${table.classComment}列表 +export const get${simpleClassName}List = async (params) => { + return await request.get({ url: `${baseURL}/list`, params }) +} +#end + +// 查询${table.classComment}详情 +export const get${simpleClassName} = async (id: number) => { + return await request.get({ url: `${baseURL}/get?id=` + id }) +} + +// 新增${table.classComment} +export const create${simpleClassName} = async (data: ${simpleClassName}VO) => { + return await request.post({ url: `${baseURL}/create`, data }) +} + +// 修改${table.classComment} +export const update${simpleClassName} = async (data: ${simpleClassName}VO) => { + return await request.put({ url: `${baseURL}/update`, data }) +} + +// 删除${table.classComment} +export const delete${simpleClassName} = async (id: number) => { + return await request.delete({ url: `${baseURL}/delete?id=` + id }) +} + +// 导出${table.classComment} Excel +export const 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}分页 +export const get${subSimpleClassName}Page = async (params) => { + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/page`, params }) +} +## 情况二:非 MASTER_ERP 时,需要列表查询子表 +#else + #if ( $subTable.subJoinMany ) + +// 获得${subTable.classComment}列表 +export const 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} +export const 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} +export const create${subSimpleClassName} = async (data) => { + return await request.post({ url: `${baseURL}/${subSimpleClassName_strikeCase}/create`, data }) +} + +// 修改${subTable.classComment} +export const update${subSimpleClassName} = async (data) => { + return await request.put({ url: `${baseURL}/${subSimpleClassName_strikeCase}/update`, data }) +} + +// 删除${subTable.classComment} +export const delete${subSimpleClassName} = async (id: number) => { + return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id }) +} + +// 获得${subTable.classComment} +export const get${subSimpleClassName} = async (id: number) => { + return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id }) +} +#end +#end \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm new file mode 100644 index 00000000..ed318875 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm @@ -0,0 +1,205 @@ +#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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm new file mode 100644 index 00000000..d8542c3d --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm new file mode 100644 index 00000000..90df7981 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm @@ -0,0 +1,362 @@ +#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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm new file mode 100644 index 00000000..5ad208b3 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm @@ -0,0 +1,181 @@ +#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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm new file mode 100644 index 00000000..3fe64889 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm new file mode 100644 index 00000000..1c155362 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm @@ -0,0 +1,298 @@ + + \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm new file mode 100644 index 00000000..092b54ce --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm @@ -0,0 +1,373 @@ + + + \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm new file mode 100644 index 00000000..48cd5422 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm @@ -0,0 +1,46 @@ +import request from '@/config/axios' +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +export interface ${simpleClassName}VO { + #foreach ($column in $columns) + #if ($column.createOperation || $column.updateOperation) + #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") + ${column.javaField}: number + #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime") + ${column.javaField}: Date + #else + ${column.javaField}: ${column.javaType.toLowerCase()} + #end + #end + #end +} + +// 查询${table.classComment}列表 +export const get${simpleClassName}Page = async (params) => { + return await request.get({ url: '${baseURL}/page', params }) +} + +// 查询${table.classComment}详情 +export const get${simpleClassName} = async (id: number) => { + return await request.get({ url: '${baseURL}/get?id=' + id }) +} + +// 新增${table.classComment} +export const create${simpleClassName} = async (data: ${simpleClassName}VO) => { + return await request.post({ url: '${baseURL}/create', data }) +} + +// 修改${table.classComment} +export const update${simpleClassName} = async (data: ${simpleClassName}VO) => { + return await request.put({ url: '${baseURL}/update', data }) +} + +// 删除${table.classComment} +export const delete${simpleClassName} = async (id: number) => { + return await request.delete({ url: '${baseURL}/delete?id=' + id }) +} + +// 导出${table.classComment} Excel +export const export${simpleClassName}Api = async (params) => { + return await request.download({ url: '${baseURL}/export-excel', params }) +} diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm new file mode 100644 index 00000000..ff4fa810 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm @@ -0,0 +1,124 @@ +import type { CrudSchema } from '@/hooks/web/useCrudSchemas' +import { dateFormatter } from '@/utils/formatTime' + +// 表单校验 +export const rules = reactive({ +#foreach ($column in $columns) +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 +#set($comment=$column.columnComment) + $column.javaField: [required], +#end +#end +}) + +// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/ +const crudSchemas = reactive([ +#foreach($column in $columns) +#if ($column.listOperation || $column.listOperationResult || $column.createOperation || $column.updateOperation) +#set ($dictType = $column.dictType) +#set ($javaField = $column.javaField) +#set ($javaType = $column.javaType) + { + label: '${column.columnComment}', + field: '${column.javaField}', +## ========= 字典部分 ========= + #if ("" != $dictType)## 有数据字典 + dictType: DICT_TYPE.$dictType.toUpperCase(), + #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") + dictClass: 'number', + #elseif ($javaType == "String") + dictClass: 'string', + #elseif ($javaType == "Boolean") + dictClass: 'boolean', + #end + #end +## ========= Table 表格部分 ========= + #if (!$column.listOperationResult) + isTable: false, + #else + #if ($column.htmlType == "datetime") + formatter: dateFormatter, + #end + #end +## ========= Search 表格部分 ========= + #if ($column.listOperation) + isSearch: true, + #if ($column.htmlType == "datetime") + search: { + component: 'DatePicker', + componentProps: { + valueFormat: 'YYYY-MM-DD HH:mm:ss', + type: 'daterange', + defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')] + } + }, + #end + #end +## ========= Form 表单部分 ========= + #if ((!$column.createOperation && !$column.updateOperation) || $column.primaryKey) + isForm: false, + #else + #if($column.htmlType == "imageUpload")## 图片上传 + form: { + component: 'UploadImg' + }, + #elseif($column.htmlType == "fileUpload")## 文件上传 + form: { + component: 'UploadFile' + }, + #elseif($column.htmlType == "editor")## 文本编辑器 + form: { + component: 'Editor', + componentProps: { + valueHtml: '', + height: 200 + } + }, + #elseif($column.htmlType == "select")## 下拉框 + form: { + component: 'SelectV2' + }, + #elseif($column.htmlType == "checkbox")## 多选框 + form: { + component: 'Checkbox' + }, + #elseif($column.htmlType == "radio")## 单选框 + form: { + component: 'Radio' + }, + #elseif($column.htmlType == "datetime")## 时间框 + form: { + component: 'DatePicker', + componentProps: { + type: 'datetime', + valueFormat: 'x' + } + }, + #elseif($column.htmlType == "textarea")## 文本框 + form: { + component: 'Input', + componentProps: { + type: 'textarea', + rows: 4 + }, + colProps: { + span: 24 + } + }, + #elseif(${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer")## 文本框 + form: { + component: 'InputNumber', + value: 0 + }, + #end + #end + }, +#end +#end + { + label: '操作', + field: 'action', + isForm: false + } +]) +export const { allSchemas } = useCrudSchemas(crudSchemas) diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm new file mode 100644 index 00000000..52f20a2f --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm @@ -0,0 +1,65 @@ + + diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm new file mode 100644 index 00000000..6e8f1403 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm @@ -0,0 +1,85 @@ + + diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/api/api.ts.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/api/api.ts.vm new file mode 100644 index 00000000..b7f26510 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm new file mode 100644 index 00000000..92d3b2d7 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm @@ -0,0 +1,236 @@ +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 ($javaField = $column.javaField) + #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + #set ($comment=$column.columnComment) + { + 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()), + #else## 未设置 dictType 数据字典的情况 + options: [], + #end + }, + #elseif ($column.htmlType == "radio") + component: 'Radio', + componentProps: { + #if ("" != $dictType)## 设置了 dictType 数据字典的情况 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()), + #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 ($javaField = $column.javaField) + #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + #set ($comment = $column.columnComment) +#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(), 'number'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "checkbox")## 多选框 + component: 'Checkbox', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "radio")## 单选框 + component: 'RadioButtonGroup', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + #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 ($javaField = $column.javaField) +#set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#set ($comment = $column.columnComment) + #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(), 'number'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "checkbox")## 多选框 + component: 'Checkbox', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + #else##没数据字典 + options:[], + #end + }, + #elseif($column.htmlType == "radio")## 单选框 + component: 'RadioButtonGroup', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number'), + #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/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/views/form.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/views/form.vue.vm new file mode 100644 index 00000000..18043651 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/views/form.vue.vm @@ -0,0 +1,58 @@ + + \ No newline at end of file diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/views/index.vue.vm b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/views/index.vue.vm new file mode 100644 index 00000000..84ec4bf5 --- /dev/null +++ b/mes-module-infra/mes-module-infra-biz/src/main/resources/codegen/vue3_vben/views/index.vue.vm @@ -0,0 +1,91 @@ + + diff --git a/mes-module-infra/mes-module-infra-biz/src/main/resources/file/erweima.jpg b/mes-module-infra/mes-module-infra-biz/src/main/resources/file/erweima.jpg new file mode 100644 index 00000000..1447283c Binary files /dev/null and b/mes-module-infra/mes-module-infra-biz/src/main/resources/file/erweima.jpg differ diff --git a/mes-module-infra/pom.xml b/mes-module-infra/pom.xml new file mode 100644 index 00000000..9d358f90 --- /dev/null +++ b/mes-module-infra/pom.xml @@ -0,0 +1,25 @@ + + + + com.chanko.yunxi + mes + ${revision} + + 4.0.0 + + mes-module-infra-api + mes-module-infra-biz + + mes-module-infra + pom + + ${project.artifactId} + + infra 模块,主要提供两块能力: + 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + + + diff --git a/mes-module-system/mes-module-system-api/pom.xml b/mes-module-system/mes-module-system-api/pom.xml new file mode 100644 index 00000000..e5938c99 --- /dev/null +++ b/mes-module-system/mes-module-system-api/pom.xml @@ -0,0 +1,34 @@ + + + + com.chanko.yunxi + mes-module-system + ${revision} + + 4.0.0 + mes-module-system-api + jar + + ${project.artifactId} + + system 模块 API,暴露给其它模块调用 + + + + + com.chanko.yunxi + mes-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + + diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/DeptApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/DeptApi.java new file mode 100644 index 00000000..cc9bc0fd --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/DeptApi.java @@ -0,0 +1,53 @@ +package com.chanko.yunxi.mes.heli.module.system.api.dept; + +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.module.system.api.dept.dto.DeptRespDTO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 部门 API 接口 + * + * @author 芋道源码 + */ +public interface DeptApi { + + /** + * 获得部门信息 + * + * @param id 部门编号 + * @return 部门信息 + */ + DeptRespDTO getDept(Long id); + + /** + * 获得部门信息数组 + * + * @param ids 部门编号数组 + * @return 部门信息数组 + */ + List getDeptList(Collection ids); + + /** + * 校验部门们是否有效。如下情况,视为无效: + * 1. 部门编号不存在 + * 2. 部门被禁用 + * + * @param ids 角色编号数组 + */ + void validateDeptList(Collection ids); + + /** + * 获得指定编号的部门 Map + * + * @param ids 部门编号数组 + * @return 部门 Map + */ + default Map getDeptMap(Collection ids) { + List list = getDeptList(ids); + return CollectionUtils.convertMap(list, DeptRespDTO::getId); + } + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/PostApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/PostApi.java new file mode 100644 index 00000000..5bc08b0d --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/PostApi.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.api.dept; + +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.module.system.api.dept.dto.PostRespDTO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 岗位 API 接口 + * + * @author 芋道源码 + */ +public interface PostApi { + + /** + * 校验岗位们是否有效。如下情况,视为无效: + * 1. 岗位编号不存在 + * 2. 岗位被禁用 + * + * @param ids 岗位编号数组 + */ + void validPostList(Collection ids); + + List getPostList(Collection ids); + + default Map getPostMap(Collection ids) { + List list = getPostList(ids); + return CollectionUtils.convertMap(list, PostRespDTO::getId); + } + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/dto/DeptRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/dto/DeptRespDTO.java new file mode 100644 index 00000000..a1f8a2bd --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/dto/DeptRespDTO.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.system.api.dept.dto; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +/** + * 部门 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DeptRespDTO { + + /** + * 部门编号 + */ + private Long id; + /** + * 部门名称 + */ + private String name; + /** + * 父部门编号 + */ + private Long parentId; + /** + * 负责人的用户编号 + */ + private Long leaderUserId; + /** + * 部门状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/dto/PostRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/dto/PostRespDTO.java new file mode 100644 index 00000000..04a4f9a8 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/dto/PostRespDTO.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.system.api.dept.dto; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +/** + * 岗位 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PostRespDTO { + + /** + * 岗位序号 + */ + private Long id; + /** + * 岗位名称 + */ + private String name; + /** + * 岗位编码 + */ + private String code; + /** + * 岗位排序 + */ + private Integer sort; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dict/DictDataApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dict/DictDataApi.java new file mode 100644 index 00000000..7f801770 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dict/DictDataApi.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.system.api.dict; + +import com.chanko.yunxi.mes.heli.module.system.api.dict.dto.DictDataRespDTO; + +import java.util.Collection; + +/** + * 字典数据 API 接口 + * + * @author 芋道源码 + */ +public interface DictDataApi { + + /** + * 校验字典数据们是否有效。如下情况,视为无效: + * 1. 字典数据不存在 + * 2. 字典数据被禁用 + * + * @param dictType 字典类型 + * @param values 字典数据值的数组 + */ + void validateDictDataList(String dictType, Collection values); + + /** + * 获得指定的字典数据,从缓存中 + * + * @param type 字典类型 + * @param value 字典数据值 + * @return 字典数据 + */ + DictDataRespDTO getDictData(String type, String value); + + /** + * 解析获得指定的字典数据,从缓存中 + * + * @param type 字典类型 + * @param label 字典数据标签 + * @return 字典数据 + */ + DictDataRespDTO parseDictData(String type, String label); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dict/dto/DictDataRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dict/dto/DictDataRespDTO.java new file mode 100644 index 00000000..81bd2f44 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dict/dto/DictDataRespDTO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.api.dict.dto; + +import com.chanko.yunxi.mes.heli.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/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/ErrorCodeApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/ErrorCodeApi.java new file mode 100644 index 00000000..88323b39 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/ErrorCodeApi.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.module.system.api.errorcode; + +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeRespDTO; + +import javax.validation.Valid; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 错误码 Api 接口 + * + * @author 芋道源码 + */ +public interface ErrorCodeApi { + + /** + * 自动创建错误码 + * + * @param autoGenerateDTOs 错误码信息 + */ + void autoGenerateErrorCodeList(@Valid List autoGenerateDTOs); + + /** + * 增量获得错误码数组 + * + * 如果 minUpdateTime 为空时,则获取所有错误码 + * + * @param applicationName 应用名 + * @param minUpdateTime 最小更新时间 + * @return 错误码数组 + */ + List getErrorCodeList(String applicationName, LocalDateTime minUpdateTime); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/dto/ErrorCodeAutoGenerateReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/dto/ErrorCodeAutoGenerateReqDTO.java new file mode 100644 index 00000000..7ec856a6 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/dto/ErrorCodeAutoGenerateReqDTO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 错误码自动生成 DTO + * + * @author dylan + */ +@Data +@Accessors(chain = true) +public class ErrorCodeAutoGenerateReqDTO { + + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + /** + * 错误码编码 + */ + @NotNull(message = "错误码编码不能为空") + private Integer code; + /** + * 错误码错误提示 + */ + @NotEmpty(message = "错误码错误提示不能为空") + private String message; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/dto/ErrorCodeRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/dto/ErrorCodeRespDTO.java new file mode 100644 index 00000000..a449cee8 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/dto/ErrorCodeRespDTO.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 错误码的 Response DTO + * + * @author 芋道源码 + */ +@Data +public class ErrorCodeRespDTO { + + /** + * 错误码编码 + */ + private Integer code; + /** + * 错误码错误提示 + */ + private String message; + /** + * 更新时间 + */ + private LocalDateTime updateTime; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/LoginLogApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/LoginLogApi.java new file mode 100644 index 00000000..c4a55344 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/LoginLogApi.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.api.logger; + +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.LoginLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * 登录日志的 API 接口 + * + * @author 芋道源码 + */ +public interface LoginLogApi { + + /** + * 创建登录日志 + * + * @param reqDTO 日志信息 + */ + void createLoginLog(@Valid LoginLogCreateReqDTO reqDTO); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/OperateLogApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/OperateLogApi.java new file mode 100644 index 00000000..20a7d8a6 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/OperateLogApi.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.api.logger; + +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.OperateLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * 操作日志 API 接口 + * + * @author 芋道源码 + */ +public interface OperateLogApi { + + /** + * 创建操作日志 + * + * @param createReqDTO 请求 + */ + void createOperateLog(@Valid OperateLogCreateReqDTO createReqDTO); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/dto/LoginLogCreateReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/dto/LoginLogCreateReqDTO.java new file mode 100644 index 00000000..89497b8a --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/dto/LoginLogCreateReqDTO.java @@ -0,0 +1,62 @@ +package com.chanko.yunxi.mes.heli.module.system.api.logger.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 登录日志创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class LoginLogCreateReqDTO { + + /** + * 日志类型 + */ + @NotNull(message = "日志类型不能为空") + private Integer logType; + /** + * 链路追踪编号 + */ + private String traceId; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + /** + * 用户账号 + */ + @NotBlank(message = "用户账号不能为空") + @Size(max = 30, message = "用户账号长度不能超过30个字符") + private String username; + + /** + * 登录结果 + */ + @NotNull(message = "登录结果不能为空") + private Integer result; + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + /** + * 浏览器 UserAgent + * + * 允许空,原因:Job 过期登出时,是无法传递 UserAgent 的 + */ + private String userAgent; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/dto/OperateLogCreateReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/dto/OperateLogCreateReqDTO.java new file mode 100644 index 00000000..989b45f1 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/dto/OperateLogCreateReqDTO.java @@ -0,0 +1,123 @@ +package com.chanko.yunxi.mes.heli.module.system.api.logger.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 操作日志创建 Request DTO + */ +@Data +public class OperateLogCreateReqDTO { + + /** + * 链路追踪编号 + */ + private String traceId; + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 操作模块 + */ + @NotEmpty(message = "操作模块不能为空") + private String module; + + /** + * 操作名 + */ + @NotEmpty(message = "操作名") + private String name; + + /** + * 操作分类 + */ + @NotNull(message = "操作分类不能为空") + private Integer type; + + /** + * 操作明细 + */ + private String content; + + /** + * 拓展字段 + */ + private Map exts; + + /** + * 请求方法名 + */ + @NotEmpty(message = "请求方法名不能为空") + private String requestMethod; + + /** + * 请求地址 + */ + @NotEmpty(message = "请求地址不能为空") + private String requestUrl; + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + /** + * 浏览器 UserAgent + */ + @NotEmpty(message = "浏览器 UserAgent 不能为空") + private String userAgent; + + /** + * Java 方法名 + */ + @NotEmpty(message = "Java 方法名不能为空") + private String javaMethod; + + /** + * Java 方法的参数 + */ + private String javaMethodArgs; + + /** + * 开始时间 + */ + @NotNull(message = "开始时间不能为空") + private LocalDateTime startTime; + + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + + /** + * 结果码 + */ + @NotNull(message = "结果码不能为空") + private Integer resultCode; + + /** + * 结果提示 + */ + private String resultMsg; + + /** + * 结果数据 + */ + private String resultData; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/mail/MailSendApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/mail/MailSendApi.java new file mode 100644 index 00000000..f71985cc --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/mail/MailSendApi.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.api.mail; + +import com.chanko.yunxi.mes.heli.module.system.api.mail.dto.MailSendSingleToUserReqDTO; + +import javax.validation.Valid; + +/** + * 邮箱发送 API 接口 + * + * @author 芋道源码 + */ +public interface MailSendApi { + + /** + * 发送单条邮箱给 Admin 用户 + * + * 在 mail 为空时,使用 userId 加载对应 Admin 的邮箱 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleMailToAdmin(@Valid MailSendSingleToUserReqDTO reqDTO); + + /** + * 发送单条邮箱给 Member 用户 + * + * 在 mail 为空时,使用 userId 加载对应 Member 的邮箱 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleMailToMember(@Valid MailSendSingleToUserReqDTO reqDTO); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java new file mode 100644 index 00000000..37792313 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.system.api.mail.dto; + +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; +import java.util.Map; + +/** + * 邮件发送 Request DTO + * + * @author wangjingqi + */ +@Data +public class MailSendSingleToUserReqDTO { + + /** + * 用户编号 + */ + private Long userId; + /** + * 邮箱 + */ + @Email + private String mail; + + /** + * 邮件模板编号 + */ + @NotNull(message = "邮件模板编号不能为空") + private String templateCode; + /** + * 邮件模板参数 + */ + private Map templateParams; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/NotifyMessageSendApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/NotifyMessageSendApi.java new file mode 100644 index 00000000..17aae1d8 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/NotifyMessageSendApi.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.api.notify; + +import com.chanko.yunxi.mes.heli.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; + +import javax.validation.Valid; + +/** + * 站内信发送 API 接口 + * + * @author xrcoder + */ +public interface NotifyMessageSendApi { + + /** + * 发送单条站内信给 Admin 用户 + * + * @param reqDTO 发送请求 + * @return 发送消息 ID + */ + Long sendSingleMessageToAdmin(@Valid NotifySendSingleToUserReqDTO reqDTO); + + /** + * 发送单条站内信给 Member 用户 + * + * @param reqDTO 发送请求 + * @return 发送消息 ID + */ + Long sendSingleMessageToMember(@Valid NotifySendSingleToUserReqDTO reqDTO); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/dto/NotifySendSingleToUserReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/dto/NotifySendSingleToUserReqDTO.java new file mode 100644 index 00000000..498d34dd --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/dto/NotifySendSingleToUserReqDTO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.api.notify.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +/** + * 站内信发送给 Admin 或者 Member 用户 + * + * @author xrcoder + */ +@Data +public class NotifySendSingleToUserReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + + /** + * 站内信模板编号 + */ + @NotEmpty(message = "站内信模板编号不能为空") + private String templateCode; + + /** + * 站内信模板参数 + */ + private Map templateParams; +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/dto/NotifyTemplateReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/dto/NotifyTemplateReqDTO.java new file mode 100644 index 00000000..5f76ab95 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/dto/NotifyTemplateReqDTO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.api.notify.dto; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Data +public class NotifyTemplateReqDTO { + + @NotEmpty(message = "模版名称不能为空") + private String name; + + @NotNull(message = "模版编码不能为空") + private String code; + + @NotNull(message = "模版类型不能为空") + private Integer type; + + @NotEmpty(message = "发送人名称不能为空") + private String nickname; + + @NotEmpty(message = "模版内容不能为空") + private String content; + + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + + private String remark; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/OAuth2TokenApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/OAuth2TokenApi.java new file mode 100644 index 00000000..86c9ebe0 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/OAuth2TokenApi.java @@ -0,0 +1,49 @@ +package com.chanko.yunxi.mes.heli.module.system.api.oauth2; + +import com.chanko.yunxi.mes.heli.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import com.chanko.yunxi.mes.heli.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; + +import javax.validation.Valid; + +/** + * OAuth2.0 Token API 接口 + * + * @author 芋道源码 + */ +public interface OAuth2TokenApi { + + /** + * 创建访问令牌 + * + * @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/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java new file mode 100644 index 00000000..f0876203 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.api.oauth2.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * OAuth2.0 访问令牌的校验 Response DTO + * + * @author 芋道源码 + */ +@Data +public class OAuth2AccessTokenCheckRespDTO implements Serializable { + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围的数组 + */ + private List scopes; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java new file mode 100644 index 00000000..61002ef1 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.api.oauth2.dto; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import lombok.Data; + +import javax.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/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/dto/OAuth2AccessTokenRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/dto/OAuth2AccessTokenRespDTO.java new file mode 100644 index 00000000..d7c66322 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/dto/OAuth2AccessTokenRespDTO.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.system.api.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/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/package-info.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/package-info.java new file mode 100644 index 00000000..96c0d90f --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/package-info.java @@ -0,0 +1,4 @@ +/** + * System API 包,定义暴露给其它模块的 API + */ +package com.chanko.yunxi.mes.heli.module.system.api; diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/PermissionApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/PermissionApi.java new file mode 100644 index 00000000..3bf40fd5 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/PermissionApi.java @@ -0,0 +1,49 @@ +package com.chanko.yunxi.mes.heli.module.system.api.permission; + +import com.chanko.yunxi.mes.heli.module.system.api.permission.dto.DeptDataPermissionRespDTO; + +import java.util.Collection; +import java.util.Set; + +/** + * 权限 API 接口 + * + * @author 芋道源码 + */ +public interface PermissionApi { + + /** + * 获得拥有多个角色的用户编号集合 + * + * @param roleIds 角色编号集合 + * @return 用户编号集合 + */ + Set getUserRoleIdListByRoleIds(Collection roleIds); + + /** + * 判断是否有权限,任一一个即可 + * + * @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/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/RoleApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/RoleApi.java new file mode 100644 index 00000000..5417de4f --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/RoleApi.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.api.permission; + +import java.util.Collection; + +/** + * 角色 API 接口 + * + * @author 芋道源码 + */ +public interface RoleApi { + + /** + * 校验角色们是否有效。如下情况,视为无效: + * 1. 角色编号不存在 + * 2. 角色被禁用 + * + * @param ids 角色编号数组 + */ + void validRoleList(Collection ids); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/dto/DeptDataPermissionRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/dto/DeptDataPermissionRespDTO.java new file mode 100644 index 00000000..b601f4df --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/dto/DeptDataPermissionRespDTO.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.module.system.api.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/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sensitiveword/SensitiveWordApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sensitiveword/SensitiveWordApi.java new file mode 100644 index 00000000..7a8dd384 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sensitiveword/SensitiveWordApi.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sensitiveword; + +import java.util.List; + +/** + * 敏感词 API 接口 + * + * @author 永不言败 + */ +public interface SensitiveWordApi { + + /** + * 获得文本所包含的不合法的敏感词数组 + * + * @param text 文本 + * @param tags 标签数组 + * @return 不合法的敏感词数组 + */ + List validateText(String text, List tags); + + /** + * 判断文本是否包含敏感词 + * + * @param text 文本 + * @param tags 表述数组 + * @return 是否包含 + */ + boolean isTextValid(String text, List tags); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsCodeApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsCodeApi.java new file mode 100644 index 00000000..d66d3ea2 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsCodeApi.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sms; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeUseReqDTO; + +import javax.validation.Valid; + +/** + * 短信验证码 API 接口 + * + * @author 芋道源码 + */ +public interface SmsCodeApi { + + /** + * 创建短信验证码,并进行发送 + * + * @param reqDTO 发送请求 + */ + void sendSmsCode(@Valid SmsCodeSendReqDTO reqDTO); + + /** + * 验证短信验证码,并进行使用 + * 如果正确,则将验证码标记成已使用 + * 如果错误,则抛出 {@link ServiceException} 异常 + * + * @param reqDTO 使用请求 + */ + void useSmsCode(@Valid SmsCodeUseReqDTO reqDTO); + + /** + * 检查验证码是否有效 + * + * @param reqDTO 校验请求 + */ + void validateSmsCode(@Valid SmsCodeValidateReqDTO reqDTO); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsSendApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsSendApi.java new file mode 100644 index 00000000..c11c402c --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsSendApi.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sms; + +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; + +import javax.validation.Valid; + +/** + * 短信发送 API 接口 + * + * @author 芋道源码 + */ +public interface SmsSendApi { + + /** + * 发送单条短信给 Admin 用户 + * + * 在 mobile 为空时,使用 userId 加载对应 Admin 的手机号 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleSmsToAdmin(@Valid SmsSendSingleToUserReqDTO reqDTO); + + /** + * 发送单条短信给 Member 用户 + * + * 在 mobile 为空时,使用 userId 加载对应 Member 的手机号 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleSmsToMember(@Valid SmsSendSingleToUserReqDTO reqDTO); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java new file mode 100644 index 00000000..0bb920b1 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code; + +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.Mobile; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsSceneEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信验证码的发送 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SmsCodeSendReqDTO { + + /** + * 手机号 + */ + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + /** + * 发送场景 + */ + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + /** + * 发送 IP + */ + @NotEmpty(message = "发送 IP 不能为空") + private String createIp; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java new file mode 100644 index 00000000..7466dd4c --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code; + +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.Mobile; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsSceneEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信验证码的使用 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SmsCodeUseReqDTO { + + /** + * 手机号 + */ + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + /** + * 发送场景 + */ + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + /** + * 验证码 + */ + @NotEmpty(message = "验证码") + private String code; + /** + * 使用 IP + */ + @NotEmpty(message = "使用 IP 不能为空") + private String usedIp; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java new file mode 100644 index 00000000..c49013ce --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code; + +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.Mobile; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsSceneEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信验证码的校验 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SmsCodeValidateReqDTO { + + /** + * 手机号 + */ + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + /** + * 发送场景 + */ + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + /** + * 验证码 + */ + @NotEmpty(message = "验证码") + private String code; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java new file mode 100644 index 00000000..b4ff0f1d --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sms.dto.send; + +import com.chanko.yunxi.mes.heli.framework.common.validation.Mobile; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.Map; + +/** + * 短信发送给 Admin 或者 Member 用户 + * + * @author 芋道源码 + */ +@Data +public class SmsSendSingleToUserReqDTO { + + /** + * 用户编号 + */ + private Long userId; + /** + * 手机号 + */ + @Mobile + private String mobile; + /** + * 短信模板编号 + */ + @NotEmpty(message = "短信模板编号不能为空") + private String templateCode; + /** + * 短信模板参数 + */ + private Map templateParams; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialClientApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialClientApi.java new file mode 100644 index 00000000..7cd2a503 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialClientApi.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.system.api.social; + +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; + +/** + * 社交应用的 API 接口 + * + * @author 芋道源码 + */ +public interface SocialClientApi { + + /** + * 获得社交平台的授权 URL + * + * @param socialType 社交平台的类型 {@link SocialTypeEnum} + * @param userType 用户类型 + * @param redirectUri 重定向 URL + * @return 社交平台的授权 URL + */ + String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri); + + /** + * 创建微信公众号 JS SDK 初始化所需的签名 + * + * @param userType 用户类型 + * @param url 访问的 URL 地址 + * @return 签名 + */ + SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url); + + /** + * 获得微信小程序的手机信息 + * + * @param userType 用户类型 + * @param phoneCode 手机授权码 + * @return 手机信息 + */ + SocialWxPhoneNumberInfoRespDTO getWxMaPhoneNumberInfo(Integer userType, String phoneCode); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialUserApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialUserApi.java new file mode 100644 index 00000000..080667de --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialUserApi.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.module.system.api.social; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserBindReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserRespDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserUnbindReqDTO; + +import javax.validation.Valid; + +/** + * 社交用户的 API 接口 + * + * @author 芋道源码 + */ +public interface SocialUserApi { + + /** + * 绑定社交用户 + * + * @param reqDTO 绑定信息 + * @return 社交用户 openid + */ + String bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); + + /** + * 取消绑定社交用户 + * + * @param reqDTO 解绑 + */ + void unbindSocialUser(@Valid SocialUserUnbindReqDTO reqDTO); + + /** + * 获得社交用户 + * + * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 + * + * @param userType 用户类型 + * @param socialType 社交平台的类型 + * @param code 授权码 + * @param state state + * @return 社交用户 + */ + SocialUserRespDTO getSocialUser(Integer userType, Integer socialType, + String code, String state); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialUserBindReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialUserBindReqDTO.java new file mode 100644 index 00000000..5804ff47 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialUserBindReqDTO.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.module.system.api.social.dto; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 取消绑定社交用户 Request DTO + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserBindReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 社交平台的类型 + */ + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer socialType; + /** + * 授权码 + */ + @NotEmpty(message = "授权码不能为空") + private String code; + /** + * state + */ + @NotNull(message = "state 不能为空") + private String state; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialUserRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialUserRespDTO.java new file mode 100644 index 00000000..6a7bfb9f --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialUserRespDTO.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.system.api.social.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 社交用户 Response DTO + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserRespDTO { + + /** + * 社交用户 openid + */ + private String openid; + + /** + * 关联的用户编号 + */ + private Long userId; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialUserUnbindReqDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialUserUnbindReqDTO.java new file mode 100644 index 00000000..ee6644bf --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialUserUnbindReqDTO.java @@ -0,0 +1,44 @@ +package com.chanko.yunxi.mes.heli.module.system.api.social.dto; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 社交绑定 Request DTO,使用 code 授权码 + * + * @author 芋道源码 + */ +@Data +public class SocialUserUnbindReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 社交平台的类型 + */ + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer socialType; + + /** + * 社交平台的 openid + */ + @NotEmpty(message = "社交平台的 openid 不能为空") + private String openid; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java new file mode 100644 index 00000000..44341f45 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialWxJsapiSignatureRespDTO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.api.social.dto; + +import lombok.Data; + +/** + * 微信公众号 JSAPI 签名 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SocialWxJsapiSignatureRespDTO { + + /** + * 微信公众号的 appId + */ + private String appId; + /** + * 匿名串 + */ + private String nonceStr; + /** + * 时间戳 + */ + private Long timestamp; + /** + * URL + */ + private String url; + /** + * 签名 + */ + private String signature; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialWxPhoneNumberInfoRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialWxPhoneNumberInfoRespDTO.java new file mode 100644 index 00000000..3b90d035 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/dto/SocialWxPhoneNumberInfoRespDTO.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.system.api.social.dto; + +import lombok.Data; + +/** + * 微信小程序的手机信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SocialWxPhoneNumberInfoRespDTO { + + /** + * 用户绑定的手机号(国外手机号会有区号) + */ + private String phoneNumber; + + /** + * 没有区号的手机号 + */ + private String purePhoneNumber; + /** + * 区号 + */ + private String countryCode; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/tenant/TenantApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/tenant/TenantApi.java new file mode 100644 index 00000000..713c9907 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/tenant/TenantApi.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.system.api.tenant; + +import java.util.List; + +/** + * 多租户的 API 接口 + * + * @author 芋道源码 + */ +public interface TenantApi { + + /** + * 获得所有租户 + * + * @return 租户编号数组 + */ + List getTenantIdList(); + + /** + * 校验租户是否合法 + * + * @param id 租户编号 + */ + void validateTenant(Long id); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/user/AdminUserApi.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/user/AdminUserApi.java new file mode 100644 index 00000000..cb95a19a --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/user/AdminUserApi.java @@ -0,0 +1,79 @@ +package com.chanko.yunxi.mes.heli.module.system.api.user; + +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.module.system.api.user.dto.AdminUserRespDTO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Admin 用户 API 接口 + * + * @author 芋道源码 + */ +public interface AdminUserApi { + + /** + * 通过用户 ID 查询用户 + * + * @param id 用户ID + * @return 用户对象信息 + */ + AdminUserRespDTO getUser(Long id); + + // TODO @puhui999:这里返回 List 方法名可以改成 getUserListBySubordinate + /** + * 通过用户 ID 查询用户下属 + * + * @param id 用户编号 + * @return 用户下属用户编号列表 + */ + Set getSubordinateIds(Long id); + + /** + * 通过用户 ID 查询用户们 + * + * @param ids 用户 ID 们 + * @return 用户对象信息 + */ + List getUserList(Collection ids); + + /** + * 获得指定部门的用户数组 + * + * @param deptIds 部门数组 + * @return 用户数组 + */ + List getUserListByDeptIds(Collection deptIds); + + /** + * 获得指定岗位的用户数组 + * + * @param postIds 岗位数组 + * @return 用户数组 + */ + List getUserListByPostIds(Collection postIds); + + /** + * 获得用户 Map + * + * @param ids 用户编号数组 + * @return 用户 Map + */ + default Map getUserMap(Collection ids) { + List users = getUserList(ids); + return CollectionUtils.convertMap(users, AdminUserRespDTO::getId); + } + + /** + * 校验用户们是否有效。如下情况,视为无效: + * 1. 用户编号不存在 + * 2. 用户被禁用 + * + * @param ids 用户编号数组 + */ + void validateUserList(Collection ids); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/user/dto/AdminUserRespDTO.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/user/dto/AdminUserRespDTO.java new file mode 100644 index 00000000..502e8046 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/user/dto/AdminUserRespDTO.java @@ -0,0 +1,44 @@ +package com.chanko.yunxi.mes.heli.module.system.api.user.dto; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +import java.util.Set; + +/** + * Admin 用户 Response DTO + * + * @author 芋道源码 + */ +@Data +public class AdminUserRespDTO { + + /** + * 用户ID + */ + private Long id; + /** + * 用户昵称 + */ + private String nickname; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + /** + * 部门ID + */ + private Long deptId; + /** + * 岗位编号数组 + */ + private Set postIds; + /** + * 手机号码 + */ + private String mobile; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/DictTypeConstants.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/DictTypeConstants.java new file mode 100644 index 00000000..fea84ad7 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/DictTypeConstants.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.module.system.enums; + +/** + * System 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String USER_TYPE = "user_type"; // 用户类型 + String COMMON_STATUS = "common_status"; // 系统状态 + + // ========== SYSTEM 模块 ========== + + String USER_SEX = "system_user_sex"; // 用户性别 + + String OPERATE_TYPE = "system_operate_type"; // 操作类型 + + String LOGIN_TYPE = "system_login_type"; // 登录日志的类型 + String LOGIN_RESULT = "system_login_result"; // 登录结果 + + String ERROR_CODE_TYPE = "system_error_code_type"; // 错误码的类型枚举 + + String SMS_CHANNEL_CODE = "system_sms_channel_code"; // 短信渠道编码 + String SMS_TEMPLATE_TYPE = "system_sms_template_type"; // 短信模板类型 + String SMS_SEND_STATUS = "system_sms_send_status"; // 短信发送状态 + String SMS_RECEIVE_STATUS = "system_sms_receive_status"; // 短信接收状态 + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/ErrorCodeConstants.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/ErrorCodeConstants.java new file mode 100644 index 00000000..d5d09056 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/ErrorCodeConstants.java @@ -0,0 +1,174 @@ +package com.chanko.yunxi.mes.heli.module.system.enums; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ErrorCode; + +/** + * System 错误码枚举类 + * + * system 系统,使用 1-002-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== AUTH 模块 1-002-000-000 ========== + ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1_002_000_000, "登录失败,账号密码不正确"); + ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1_002_000_001, "登录失败,账号被禁用"); + ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1_002_000_004, "验证码不正确,原因:{}"); + ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1_002_000_005, "未绑定账号,需要进行绑定"); + ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1_002_000_006, "Token 已经过期"); + ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1_002_000_007, "手机号不存在"); + + // ========== 菜单模块 1-002-001-000 ========== + ErrorCode MENU_NAME_DUPLICATE = new ErrorCode(1_002_001_000, "已经存在该名字的菜单"); + ErrorCode MENU_PARENT_NOT_EXISTS = new ErrorCode(1_002_001_001, "父菜单不存在"); + ErrorCode MENU_PARENT_ERROR = new ErrorCode(1_002_001_002, "不能设置自己为父菜单"); + ErrorCode MENU_NOT_EXISTS = new ErrorCode(1_002_001_003, "菜单不存在"); + ErrorCode MENU_EXISTS_CHILDREN = new ErrorCode(1_002_001_004, "存在子菜单,无法删除"); + ErrorCode MENU_PARENT_NOT_DIR_OR_MENU = new ErrorCode(1_002_001_005, "父菜单的类型必须是目录或者菜单"); + + // ========== 角色模块 1-002-002-000 ========== + ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在"); + ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1_002_002_001, "已经存在名为【{}】的角色"); + ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1_002_002_002, "已经存在编码为【{}】的角色"); + ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能操作类型为系统内置的角色"); + ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用"); + ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "编码【{}】不能使用"); + + // ========== 用户模块 1-002-003-000 ========== + ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在"); + ErrorCode USER_MOBILE_EXISTS = new ErrorCode(1_002_003_001, "手机号已经存在"); + ErrorCode USER_EMAIL_EXISTS = new ErrorCode(1_002_003_002, "邮箱已经存在"); + ErrorCode USER_NOT_EXISTS = new ErrorCode(1_002_003_003, "用户不存在"); + ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_002_003_004, "导入用户数据不能为空!"); + ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1_002_003_005, "用户密码校验失败"); + ErrorCode USER_IS_DISABLE = new ErrorCode(1_002_003_006, "名字为【{}】的用户已被禁用"); + ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过租户最大租户配额({})!"); + + // ========== 部门模块 1-002-004-000 ========== + ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门"); + ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001,"父级部门不存在"); + ErrorCode DEPT_NOT_FOUND = new ErrorCode(1_002_004_002, "当前部门不存在"); + ErrorCode DEPT_EXITS_CHILDREN = new ErrorCode(1_002_004_003, "存在子部门,无法删除"); + ErrorCode DEPT_PARENT_ERROR = new ErrorCode(1_002_004_004, "不能设置自己为父部门"); + ErrorCode DEPT_EXISTS_USER = new ErrorCode(1_002_004_005, "部门中存在员工,无法删除"); + ErrorCode DEPT_NOT_ENABLE = new ErrorCode(1_002_004_006, "部门({})不处于开启状态,不允许选择"); + ErrorCode DEPT_PARENT_IS_CHILD = new ErrorCode(1_002_004_007, "不能设置自己的子部门为父部门"); + + // ========== 岗位模块 1-002-005-000 ========== + ErrorCode POST_NOT_FOUND = new ErrorCode(1_002_005_000, "当前岗位不存在"); + ErrorCode POST_NOT_ENABLE = new ErrorCode(1_002_005_001, "岗位({}) 不处于开启状态,不允许选择"); + ErrorCode POST_NAME_DUPLICATE = new ErrorCode(1_002_005_002, "已经存在该名字的岗位"); + ErrorCode POST_CODE_DUPLICATE = new ErrorCode(1_002_005_003, "已经存在该标识的岗位"); + + // ========== 字典类型 1-002-006-000 ========== + ErrorCode DICT_TYPE_NOT_EXISTS = new ErrorCode(1_002_006_001, "当前字典类型不存在"); + ErrorCode DICT_TYPE_NOT_ENABLE = new ErrorCode(1_002_006_002, "字典类型不处于开启状态,不允许选择"); + ErrorCode DICT_TYPE_NAME_DUPLICATE = new ErrorCode(1_002_006_003, "已经存在该名字的字典类型"); + ErrorCode DICT_TYPE_TYPE_DUPLICATE = new ErrorCode(1_002_006_004, "已经存在该类型的字典类型"); + ErrorCode DICT_TYPE_HAS_CHILDREN = new ErrorCode(1_002_006_005, "无法删除,该字典类型还有字典数据"); + + // ========== 字典数据 1-002-007-000 ========== + ErrorCode DICT_DATA_NOT_EXISTS = new ErrorCode(1_002_007_001, "当前字典数据不存在"); + ErrorCode DICT_DATA_NOT_ENABLE = new ErrorCode(1_002_007_002, "字典数据({})不处于开启状态,不允许选择"); + ErrorCode DICT_DATA_VALUE_DUPLICATE = new ErrorCode(1_002_007_003, "已经存在该值的字典数据"); + + // ========== 通知公告 1-002-008-000 ========== + ErrorCode NOTICE_NOT_FOUND = new ErrorCode(1_002_008_001, "当前通知公告不存在"); + + // ========== 短信渠道 1-002-011-000 ========== + ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1_002_011_000, "短信渠道不存在"); + ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1_002_011_001, "短信渠道不处于开启状态,不允许选择"); + ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1_002_011_002, "无法删除,该短信渠道还有短信模板"); + + // ========== 短信模板 1-002-012-000 ========== + ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_012_000, "短信模板不存在"); + ErrorCode SMS_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1_002_012_001, "已经存在编码为【{}】的短信模板"); + ErrorCode SMS_TEMPLATE_API_ERROR = new ErrorCode(1_002_012_002, "短信 API 模板调用失败,原因是:{}"); + ErrorCode SMS_TEMPLATE_API_AUDIT_CHECKING = new ErrorCode(1_002_012_003, "短信 API 模版无法使用,原因:审批中"); + ErrorCode SMS_TEMPLATE_API_AUDIT_FAIL = new ErrorCode(1_002_012_004, "短信 API 模版无法使用,原因:审批不通过,{}"); + ErrorCode SMS_TEMPLATE_API_NOT_FOUND = new ErrorCode(1_002_012_005, "短信 API 模版无法使用,原因:模版不存在"); + + // ========== 短信发送 1-002-013-000 ========== + ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1_002_013_000, "手机号不存在"); + ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_013_001, "模板参数({})缺失"); + ErrorCode SMS_SEND_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_013_002, "短信模板不存在"); + + // ========== 短信验证码 1-002-014-000 ========== + ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在"); + ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期"); + ErrorCode SMS_CODE_USED = new ErrorCode(1_002_014_002, "验证码已使用"); + ErrorCode SMS_CODE_NOT_CORRECT = new ErrorCode(1_002_014_003, "验证码不正确"); + ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1_002_014_004, "超过每日短信发送数量"); + ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频率"); + ErrorCode SMS_CODE_IS_EXISTS = new ErrorCode(1_002_014_006, "手机号已被使用"); + ErrorCode SMS_CODE_IS_UNUSED = new ErrorCode(1_002_014_007, "验证码未被使用"); + + // ========== 租户信息 1-002-015-000 ========== + ErrorCode TENANT_NOT_EXISTS = new ErrorCode(1_002_015_000, "租户不存在"); + ErrorCode TENANT_DISABLE = new ErrorCode(1_002_015_001, "名字为【{}】的租户已被禁用"); + ErrorCode TENANT_EXPIRE = new ErrorCode(1_002_015_002, "名字为【{}】的租户已过期"); + ErrorCode TENANT_CAN_NOT_UPDATE_SYSTEM = new ErrorCode(1_002_015_003, "系统租户不能进行修改、删除等操作!"); + ErrorCode TENANT_NAME_DUPLICATE = new ErrorCode(1_002_015_004, "名字为【{}】的租户已存在"); + ErrorCode TENANT_WEBSITE_DUPLICATE = new ErrorCode(1_002_015_005, "域名为【{}】的租户已存在"); + + // ========== 租户套餐 1-002-016-000 ========== + ErrorCode TENANT_PACKAGE_NOT_EXISTS = new ErrorCode(1_002_016_000, "租户套餐不存在"); + ErrorCode TENANT_PACKAGE_USED = new ErrorCode(1_002_016_001, "租户正在使用该套餐,请给租户重新设置套餐后再尝试删除"); + ErrorCode TENANT_PACKAGE_DISABLE = new ErrorCode(1_002_016_002, "名字为【{}】的租户套餐已被禁用"); + + // ========== 错误码模块 1-002-017-000 ========== + ErrorCode ERROR_CODE_NOT_EXISTS = new ErrorCode(1_002_017_000, "错误码不存在"); + ErrorCode ERROR_CODE_DUPLICATE = new ErrorCode(1_002_017_001, "已经存在编码为【{}】的错误码"); + + // ========== 社交用户 1-002-018-000 ========== + ErrorCode SOCIAL_USER_AUTH_FAILURE = new ErrorCode(1_002_018_000, "社交授权失败,原因是:{}"); + ErrorCode SOCIAL_USER_NOT_FOUND = new ErrorCode(1_002_018_001, "社交授权失败,找不到对应的用户"); + + ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR = new ErrorCode(1_002_018_200, "获得手机号失败"); + ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_201, "社交客户端不存在"); + ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_201, "社交客户端已存在配置"); + + // ========== 系统敏感词 1-002-019-000 ========= + ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1_002_019_000, "系统敏感词在所有标签中都不存在"); + ErrorCode SENSITIVE_WORD_EXISTS = new ErrorCode(1_002_019_001, "系统敏感词已在标签中存在"); + + // ========== OAuth2 客户端 1-002-020-000 ========= + ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在"); + ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1_002_020_001, "OAuth2 客户端编号已存在"); + ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1_002_020_002, "OAuth2 客户端已禁用"); + ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1_002_020_003, "不支持该授权类型"); + ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1_002_020_004, "授权范围过大"); + ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1_002_020_005, "无效 redirect_uri: {}"); + ErrorCode OAUTH2_CLIENT_CLIENT_SECRET_ERROR = new ErrorCode(1_002_020_006, "无效 client_secret: {}"); + + // ========== OAuth2 授权 1-002-021-000 ========= + ErrorCode OAUTH2_GRANT_CLIENT_ID_MISMATCH = new ErrorCode(1_002_021_000, "client_id 不匹配"); + ErrorCode OAUTH2_GRANT_REDIRECT_URI_MISMATCH = new ErrorCode(1_002_021_001, "redirect_uri 不匹配"); + ErrorCode OAUTH2_GRANT_STATE_MISMATCH = new ErrorCode(1_002_021_002, "state 不匹配"); + ErrorCode OAUTH2_GRANT_CODE_NOT_EXISTS = new ErrorCode(1_002_021_003, "code 不存在"); + + // ========== OAuth2 授权 1-002-022-000 ========= + ErrorCode OAUTH2_CODE_NOT_EXISTS = new ErrorCode(1_002_022_000, "code 不存在"); + ErrorCode OAUTH2_CODE_EXPIRE = new ErrorCode(1_002_022_001, "code 已过期"); + + // ========== 邮箱账号 1-002-023-000 ========== + ErrorCode MAIL_ACCOUNT_NOT_EXISTS = new ErrorCode(1_002_023_000, "邮箱账号不存在"); + ErrorCode MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS = new ErrorCode(1_002_023_001, "无法删除,该邮箱账号还有邮件模板"); + + // ========== 邮件模版 1-002-024-000 ========== + ErrorCode MAIL_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_024_000, "邮件模版不存在"); + ErrorCode MAIL_TEMPLATE_CODE_EXISTS = new ErrorCode(1_002_024_001, "邮件模版 code({}) 已存在"); + + // ========== 邮件发送 1-002-025-000 ========== + ErrorCode MAIL_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_025_000, "模板参数({})缺失"); + ErrorCode MAIL_SEND_MAIL_NOT_EXISTS = new ErrorCode(1_002_025_001, "邮箱不存在"); + + // ========== 站内信模版 1-002-026-000 ========== + ErrorCode NOTIFY_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_026_000, "站内信模版不存在"); + ErrorCode NOTIFY_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1_002_026_001, "已经存在编码为【{}】的站内信模板"); + + // ========== 站内信模版 1-002-027-000 ========== + + // ========== 站内信发送 1-002-028-000 ========== + ErrorCode NOTIFY_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_028_000, "模板参数({})缺失"); + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/common/SexEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/common/SexEnum.java new file mode 100644 index 00000000..0c3e67a8 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/common/SexEnum.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 性别的枚举值 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SexEnum { + + /** 男 */ + MALE(1), + /** 女 */ + FEMALE(2), + /* 未知 */ + UNKNOWN(3); + + /** + * 性别 + */ + private final Integer sex; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/errorcode/ErrorCodeTypeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/errorcode/ErrorCodeTypeEnum.java new file mode 100644 index 00000000..f0ffa248 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/errorcode/ErrorCodeTypeEnum.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.errorcode; + +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 错误码的类型枚举 + * + * @author dylan + */ +@AllArgsConstructor +@Getter +public enum ErrorCodeTypeEnum implements IntArrayValuable { + + /** + * 自动生成 + */ + AUTO_GENERATION(1), + /** + * 手动编辑 + */ + MANUAL_OPERATION(2); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ErrorCodeTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/logger/LoginLogTypeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/logger/LoginLogTypeEnum.java new file mode 100644 index 00000000..0a2fefc5 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/logger/LoginLogTypeEnum.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 登录日志的类型枚举 + */ +@Getter +@AllArgsConstructor +public enum LoginLogTypeEnum { + + LOGIN_USERNAME(100), // 使用账号登录 + LOGIN_SOCIAL(101), // 使用社交登录 + LOGIN_MOBILE(103), // 使用手机登陆 + LOGIN_SMS(104), // 使用短信登陆 + + LOGOUT_SELF(200), // 自己主动登出 + LOGOUT_DELETE(202), // 强制退出 + ; + + /** + * 日志类型 + */ + private final Integer type; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/logger/LoginResultEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/logger/LoginResultEnum.java new file mode 100644 index 00000000..ffcd20ae --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/logger/LoginResultEnum.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 登录结果的枚举类 + */ +@Getter +@AllArgsConstructor +public enum LoginResultEnum { + + SUCCESS(0), // 成功 + BAD_CREDENTIALS(10), // 账号或密码不正确 + USER_DISABLED(20), // 用户被禁用 + CAPTCHA_NOT_FOUND(30), // 图片验证码不存在 + CAPTCHA_CODE_ERROR(31), // 图片验证码不正确 + + ; + + /** + * 结果 + */ + private final Integer result; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/mail/MailSendStatusEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/mail/MailSendStatusEnum.java new file mode 100644 index 00000000..f633322c --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/mail/MailSendStatusEnum.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.mail; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 邮件的发送状态枚举 + * + * @author wangjingyi + * @since 2022/4/10 13:39 + */ +@Getter +@AllArgsConstructor +public enum MailSendStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 发送成功 + FAILURE(20), // 发送失败 + IGNORE(30), // 忽略,即不发送 + ; + + private final int status; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/notice/NoticeTypeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/notice/NoticeTypeEnum.java new file mode 100644 index 00000000..da71acdd --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/notice/NoticeTypeEnum.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.notice; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 通知类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum NoticeTypeEnum { + + NOTICE(1), + ANNOUNCEMENT(2); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/notify/NotifyTemplateTypeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/notify/NotifyTemplateTypeEnum.java new file mode 100644 index 00000000..60c435e6 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/notify/NotifyTemplateTypeEnum.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.notify; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 通知模板类型枚举 + * + * @author HUIHUI + */ +@Getter +@AllArgsConstructor +public enum NotifyTemplateTypeEnum { + + /** + * 系统消息 + */ + SYSTEM_MESSAGE(2), + /** + * 通知消息 + */ + NOTIFICATION_MESSAGE(1); + + private final Integer type; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/oauth2/OAuth2ClientConstants.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/oauth2/OAuth2ClientConstants.java new file mode 100644 index 00000000..6ce4c10c --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/oauth2/OAuth2ClientConstants.java @@ -0,0 +1,12 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.oauth2; + +/** + * OAuth2.0 客户端的通用枚举 + * + * @author 芋道源码 + */ +public interface OAuth2ClientConstants { + + String CLIENT_ID_DEFAULT = "default"; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/oauth2/OAuth2GrantTypeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/oauth2/OAuth2GrantTypeEnum.java new file mode 100644 index 00000000..8496d7f2 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/oauth2/OAuth2GrantTypeEnum.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.oauth2; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * OAuth2 授权类型(模式)的枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum OAuth2GrantTypeEnum { + + PASSWORD("password"), // 密码模式 + AUTHORIZATION_CODE("authorization_code"), // 授权码模式 + IMPLICIT("implicit"), // 简化模式 + CLIENT_CREDENTIALS("client_credentials"), // 客户端模式 + REFRESH_TOKEN("refresh_token"), // 刷新模式 + ; + + private final String grantType; + + public static OAuth2GrantTypeEnum getByGranType(String grantType) { + return ArrayUtil.firstMatch(o -> o.getGrantType().equals(grantType), values()); + } + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/DataScopeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/DataScopeEnum.java new file mode 100644 index 00000000..2421d1bd --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/DataScopeEnum.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.permission; + +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 数据范围枚举类 + * + * 用于实现数据级别的权限 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum DataScopeEnum implements IntArrayValuable { + + ALL(1), // 全部数据权限 + + DEPT_CUSTOM(2), // 指定部门数据权限 + DEPT_ONLY(3), // 部门数据权限 + DEPT_AND_CHILD(4), // 部门及以下数据权限 + + SELF(5); // 仅本人数据权限 + + /** + * 范围 + */ + private final Integer scope; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DataScopeEnum::getScope).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/MenuTypeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/MenuTypeEnum.java new file mode 100644 index 00000000..1556b2a0 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/MenuTypeEnum.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 菜单类型枚举类 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum MenuTypeEnum { + + DIR(1), // 目录 + MENU(2), // 菜单 + BUTTON(3) // 按钮 + ; + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/RoleCodeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/RoleCodeEnum.java new file mode 100644 index 00000000..831390b4 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/RoleCodeEnum.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.permission; + +import com.chanko.yunxi.mes.heli.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 角色标识枚举 + */ +@Getter +@AllArgsConstructor +public enum RoleCodeEnum { + + SUPER_ADMIN("super_admin", "超级管理员"), + TENANT_ADMIN("tenant_admin", "租户管理员"), + ; + + /** + * 角色编码 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + public static boolean isSuperAdmin(String code) { + return ObjectUtils.equalsAny(code, SUPER_ADMIN.getCode()); + } + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/RoleTypeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/RoleTypeEnum.java new file mode 100644 index 00000000..0c599471 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/permission/RoleTypeEnum.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum RoleTypeEnum { + + /** + * 内置角色 + */ + SYSTEM(1), + /** + * 自定义角色 + */ + CUSTOM(2); + + private final Integer type; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsReceiveStatusEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsReceiveStatusEnum.java new file mode 100644 index 00000000..1d17c78d --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsReceiveStatusEnum.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的接收状态枚举 + * + * @author 芋道源码 + * @date 2021/2/1 13:39 + */ +@Getter +@AllArgsConstructor +public enum SmsReceiveStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 接收成功 + FAILURE(20), // 接收失败 + ; + + private final int status; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsSceneEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsSceneEnum.java new file mode 100644 index 00000000..3d744389 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsSceneEnum.java @@ -0,0 +1,51 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.sms; + +import cn.hutool.core.util.ArrayUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 用户短信验证码发送场景的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SmsSceneEnum implements IntArrayValuable { + + MEMBER_LOGIN(1, "user-sms-login", "会员用户 - 手机号登陆"), + MEMBER_UPDATE_MOBILE(2, "user-update-mobile", "会员用户 - 修改手机"), + MEMBER_UPDATE_PASSWORD(3, "user-update-mobile", "会员用户 - 修改密码"), + MEMBER_RESET_PASSWORD(4, "user-reset-password", "会员用户 - 忘记密码"), + + ADMIN_MEMBER_LOGIN(21, "admin-sms-login", "后台用户 - 手机号登录"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SmsSceneEnum::getScene).toArray(); + + /** + * 验证场景的编号 + */ + private final Integer scene; + /** + * 模版编码 + */ + private final String templateCode; + /** + * 描述 + */ + private final String description; + + @Override + public int[] array() { + return ARRAYS; + } + + public static SmsSceneEnum getCodeByScene(Integer scene) { + return ArrayUtil.firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene), + values()); + } + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsSendStatusEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsSendStatusEnum.java new file mode 100644 index 00000000..896db045 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsSendStatusEnum.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的发送状态枚举 + * + * @author zzf + * @date 2021/2/1 13:39 + */ +@Getter +@AllArgsConstructor +public enum SmsSendStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 发送成功 + FAILURE(20), // 发送失败 + IGNORE(30), // 忽略,即不发送 + ; + + private final int status; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsTemplateTypeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsTemplateTypeEnum.java new file mode 100644 index 00000000..2b8dd8cc --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/sms/SmsTemplateTypeEnum.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的模板类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SmsTemplateTypeEnum { + + VERIFICATION_CODE(1), // 验证码 + NOTICE(2), // 通知 + PROMOTION(3), // 营销 + ; + + /** + * 类型 + */ + private final int type; + +} diff --git a/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/social/SocialTypeEnum.java b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/social/SocialTypeEnum.java new file mode 100644 index 00000000..fa9289b5 --- /dev/null +++ b/mes-module-system/mes-module-system-api/src/main/java/com/chanko/yunxi/mes/heli/module/system/enums/social/SocialTypeEnum.java @@ -0,0 +1,78 @@ +package com.chanko.yunxi.mes.heli.module.system.enums.social; + +import cn.hutool.core.util.ArrayUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 社交平台的类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SocialTypeEnum implements IntArrayValuable { + + /** + * Gitee + * + * @see 接入文档 + */ + GITEE(10, "GITEE"), + /** + * 钉钉 + * + * @see 接入文档 + */ + DINGTALK(20, "DINGTALK"), + + /** + * 企业微信 + * + * @see 接入文档 + */ + WECHAT_ENTERPRISE(30, "WECHAT_ENTERPRISE"), + /** + * 微信公众平台 - 移动端 H5 + * + * @see 接入文档 + */ + WECHAT_MP(31, "WECHAT_MP"), + /** + * 微信开放平台 - 网站应用 PC 端扫码授权登录 + * + * @see 接入文档 + */ + WECHAT_OPEN(32, "WECHAT_OPEN"), + /** + * 微信小程序 + * + * @see 接入文档 + */ + WECHAT_MINI_APP(34, "WECHAT_MINI_APP"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SocialTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型的标识 + */ + private final String source; + + @Override + public int[] array() { + return ARRAYS; + } + + public static SocialTypeEnum valueOfType(Integer type) { + return ArrayUtil.firstMatch(o -> o.getType().equals(type), values()); + } + +} diff --git a/mes-module-system/mes-module-system-biz/pom.xml b/mes-module-system/mes-module-system-biz/pom.xml new file mode 100644 index 00000000..e4e85d7f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/pom.xml @@ -0,0 +1,125 @@ + + + + com.chanko.yunxi + mes-module-system + ${revision} + + 4.0.0 + mes-module-system-biz + jar + + ${project.artifactId} + + system 模块下,我们放通用业务,支撑上层的核心业务。 + 例如说:用户、部门、权限、数据字典等等 + + + + + com.chanko.yunxi + mes-module-system-api + ${revision} + + + com.chanko.yunxi + mes-module-infra-api + ${revision} + + + + + com.chanko.yunxi + mes-spring-boot-starter-biz-operatelog + + + com.chanko.yunxi + mes-spring-boot-starter-biz-sms + + + com.chanko.yunxi + mes-spring-boot-starter-biz-dict + + + com.chanko.yunxi + mes-spring-boot-starter-biz-data-permission + + + com.chanko.yunxi + mes-spring-boot-starter-biz-tenant + + + com.chanko.yunxi + mes-spring-boot-starter-biz-ip + + + + + com.chanko.yunxi + mes-spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.chanko.yunxi + mes-spring-boot-starter-mybatis + + + + com.chanko.yunxi + mes-spring-boot-starter-redis + + + + + com.chanko.yunxi + mes-spring-boot-starter-job + + + + + com.chanko.yunxi + mes-spring-boot-starter-mq + + + + + com.chanko.yunxi + mes-spring-boot-starter-excel + + + + com.chanko.yunxi + mes-spring-boot-starter-captcha + + + + org.springframework.boot + spring-boot-starter-mail + + + + + com.xingyuv + spring-boot-starter-justauth + + + + com.github.binarywang + wx-java-mp-spring-boot-starter + + + com.github.binarywang + wx-java-miniapp-spring-boot-starter + + + + + diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/DeptApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/DeptApiImpl.java new file mode 100644 index 00000000..607bb41b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/DeptApiImpl.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.system.api.dept; + +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.api.dept.dto.DeptRespDTO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.service.dept.DeptService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * 部门 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class DeptApiImpl implements DeptApi { + + @Resource + private DeptService deptService; + + @Override + public DeptRespDTO getDept(Long id) { + DeptDO dept = deptService.getDept(id); + return BeanUtils.toBean(dept, DeptRespDTO.class); + } + + @Override + public List getDeptList(Collection ids) { + List depts = deptService.getDeptList(ids); + return BeanUtils.toBean(depts, DeptRespDTO.class); + } + + @Override + public void validateDeptList(Collection ids) { + deptService.validateDeptList(ids); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/PostApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/PostApiImpl.java new file mode 100644 index 00000000..4c13e897 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dept/PostApiImpl.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.module.system.api.dept; + +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.api.dept.dto.PostRespDTO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.PostDO; +import com.chanko.yunxi.mes.heli.module.system.service.dept.PostService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * 岗位 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PostApiImpl implements PostApi { + + @Resource + private PostService postService; + + @Override + public void validPostList(Collection ids) { + postService.validatePostList(ids); + } + + @Override + public List getPostList(Collection ids) { + List list = postService.getPostList(ids); + return BeanUtils.toBean(list, PostRespDTO.class); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dict/DictDataApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dict/DictDataApiImpl.java new file mode 100644 index 00000000..78ab90fa --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/dict/DictDataApiImpl.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.api.dict; + +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.api.dict.dto.DictDataRespDTO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictDataDO; +import com.chanko.yunxi.mes.heli.module.system.service.dict.DictDataService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; + +/** + * 字典数据 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class DictDataApiImpl implements DictDataApi { + + @Resource + private DictDataService dictDataService; + + @Override + public void validateDictDataList(String dictType, Collection values) { + dictDataService.validateDictDataList(dictType, values); + } + + @Override + public DictDataRespDTO getDictData(String dictType, String value) { + DictDataDO dictData = dictDataService.getDictData(dictType, value); + return BeanUtils.toBean(dictData, DictDataRespDTO.class); + } + + @Override + public DictDataRespDTO parseDictData(String dictType, String label) { + DictDataDO dictData = dictDataService.parseDictData(dictType, label); + return BeanUtils.toBean(dictData, DictDataRespDTO.class); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/ErrorCodeApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/ErrorCodeApiImpl.java new file mode 100644 index 00000000..83042989 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/errorcode/ErrorCodeApiImpl.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.api.errorcode; + +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import com.chanko.yunxi.mes.heli.module.system.service.errorcode.ErrorCodeService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 错误码 Api 实现类 + * + * @author 芋道源码 + */ +@Service +public class ErrorCodeApiImpl implements ErrorCodeApi { + + @Resource + private ErrorCodeService errorCodeService; + + @Override + public void autoGenerateErrorCodeList(List autoGenerateDTOs) { + errorCodeService.autoGenerateErrorCodes(autoGenerateDTOs); + } + + @Override + public List getErrorCodeList(String applicationName, LocalDateTime minUpdateTime) { + return errorCodeService.getErrorCodeList(applicationName, minUpdateTime); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/LoginLogApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/LoginLogApiImpl.java new file mode 100644 index 00000000..c21b394d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/LoginLogApiImpl.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.system.api.logger; + +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.service.logger.LoginLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 登录日志的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class LoginLogApiImpl implements LoginLogApi { + + @Resource + private LoginLogService loginLogService; + + @Override + public void createLoginLog(LoginLogCreateReqDTO reqDTO) { + loginLogService.createLoginLog(reqDTO); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/OperateLogApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/OperateLogApiImpl.java new file mode 100644 index 00000000..ff0dfeaa --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/logger/OperateLogApiImpl.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.system.api.logger; + +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.OperateLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.service.logger.OperateLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 操作日志 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class OperateLogApiImpl implements OperateLogApi { + + @Resource + private OperateLogService operateLogService; + + @Override + public void createOperateLog(OperateLogCreateReqDTO createReqDTO) { + operateLogService.createOperateLog(createReqDTO); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/mail/MailSendApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/mail/MailSendApiImpl.java new file mode 100644 index 00000000..61cd2e53 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/mail/MailSendApiImpl.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.api.mail; + +import com.chanko.yunxi.mes.heli.module.system.api.mail.dto.MailSendSingleToUserReqDTO; +import com.chanko.yunxi.mes.heli.module.system.service.mail.MailSendService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 邮件发送 API 实现类 + * + * @author wangjingyi + */ +@Service +@Validated +public class MailSendApiImpl implements MailSendApi { + + @Resource + private MailSendService mailSendService; + + @Override + public Long sendSingleMailToAdmin(MailSendSingleToUserReqDTO reqDTO) { + return mailSendService.sendSingleMailToAdmin(reqDTO.getMail(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + + @Override + public Long sendSingleMailToMember(MailSendSingleToUserReqDTO reqDTO) { + return mailSendService.sendSingleMailToMember(reqDTO.getMail(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/NotifyMessageSendApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/NotifyMessageSendApiImpl.java new file mode 100644 index 00000000..aaf94c34 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/notify/NotifyMessageSendApiImpl.java @@ -0,0 +1,32 @@ +package com.chanko.yunxi.mes.heli.module.system.api.notify; + +import com.chanko.yunxi.mes.heli.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import com.chanko.yunxi.mes.heli.module.system.service.notify.NotifySendService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 站内信发送 API 实现类 + * + * @author xrcoder + */ +@Service +public class NotifyMessageSendApiImpl implements NotifyMessageSendApi { + + @Resource + private NotifySendService notifySendService; + + @Override + public Long sendSingleMessageToAdmin(NotifySendSingleToUserReqDTO reqDTO) { + return notifySendService.sendSingleNotifyToAdmin(reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + + @Override + public Long sendSingleMessageToMember(NotifySendSingleToUserReqDTO reqDTO) { + return notifySendService.sendSingleNotifyToMember(reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/OAuth2TokenApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/OAuth2TokenApiImpl.java new file mode 100644 index 00000000..8501598c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/oauth2/OAuth2TokenApiImpl.java @@ -0,0 +1,49 @@ +package com.chanko.yunxi.mes.heli.module.system.api.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import com.chanko.yunxi.mes.heli.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.chanko.yunxi.mes.heli.module.system.service.oauth2.OAuth2TokenService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * OAuth2.0 Token API 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2TokenApiImpl implements OAuth2TokenApi { + + @Resource + private OAuth2TokenService oauth2TokenService; + + @Override + public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken( + reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId(), reqDTO.getScopes()); + return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenRespDTO.class); + } + + @Override + public OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.checkAccessToken(accessToken); + return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenCheckRespDTO.class); + } + + @Override + public OAuth2AccessTokenRespDTO removeAccessToken(String accessToken) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(accessToken); + return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenRespDTO.class); + } + + @Override + public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); + return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenRespDTO.class); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/PermissionApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/PermissionApiImpl.java new file mode 100644 index 00000000..9970c0f4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/PermissionApiImpl.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.system.api.permission; + +import com.chanko.yunxi.mes.heli.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import com.chanko.yunxi.mes.heli.module.system.service.permission.PermissionService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Set; + +/** + * 权限 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PermissionApiImpl implements PermissionApi { + + @Resource + private PermissionService permissionService; + + @Override + public Set getUserRoleIdListByRoleIds(Collection roleIds) { + return permissionService.getUserRoleIdListByRoleId(roleIds); + } + + @Override + public boolean hasAnyPermissions(Long userId, String... permissions) { + return permissionService.hasAnyPermissions(userId, permissions); + } + + @Override + public boolean hasAnyRoles(Long userId, String... roles) { + return permissionService.hasAnyRoles(userId, roles); + } + + @Override + public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { + return permissionService.getDeptDataPermission(userId); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/RoleApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/RoleApiImpl.java new file mode 100644 index 00000000..ea44e3b0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/permission/RoleApiImpl.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.system.api.permission; + +import com.chanko.yunxi.mes.heli.module.system.service.permission.RoleService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; + +/** + * 角色 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class RoleApiImpl implements RoleApi { + + @Resource + private RoleService roleService; + + @Override + public void validRoleList(Collection ids) { + roleService.validateRoleList(ids); + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sensitiveword/SensitiveWordApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sensitiveword/SensitiveWordApiImpl.java new file mode 100644 index 00000000..b5e674ab --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sensitiveword/SensitiveWordApiImpl.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sensitiveword; + +import com.chanko.yunxi.mes.heli.module.system.service.sensitiveword.SensitiveWordService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 敏感词 API 实现类 + * + * @author 永不言败 + */ +@Service +public class SensitiveWordApiImpl implements SensitiveWordApi { + + @Resource + private SensitiveWordService sensitiveWordService; + + @Override + public List validateText(String text, List tags) { + return sensitiveWordService.validateText(text, tags); + } + + @Override + public boolean isTextValid(String text, List tags) { + return sensitiveWordService.isTextValid(text, tags); + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsCodeApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsCodeApiImpl.java new file mode 100644 index 00000000..5537eea7 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsCodeApiImpl.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sms; + +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import com.chanko.yunxi.mes.heli.module.system.service.sms.SmsCodeService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 短信验证码 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SmsCodeApiImpl implements SmsCodeApi { + + @Resource + private SmsCodeService smsCodeService; + + @Override + public void sendSmsCode(SmsCodeSendReqDTO reqDTO) { + smsCodeService.sendSmsCode(reqDTO); + } + + @Override + public void useSmsCode(SmsCodeUseReqDTO reqDTO) { + smsCodeService.useSmsCode(reqDTO); + } + + @Override + public void validateSmsCode(SmsCodeValidateReqDTO reqDTO) { + smsCodeService.validateSmsCode(reqDTO); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsSendApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsSendApiImpl.java new file mode 100644 index 00000000..83175e11 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/sms/SmsSendApiImpl.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.api.sms; + +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; +import com.chanko.yunxi.mes.heli.module.system.service.sms.SmsSendService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 短信发送 API 接口 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SmsSendApiImpl implements SmsSendApi { + + @Resource + private SmsSendService smsSendService; + + @Override + public Long sendSingleSmsToAdmin(SmsSendSingleToUserReqDTO reqDTO) { + return smsSendService.sendSingleSmsToAdmin(reqDTO.getMobile(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + + @Override + public Long sendSingleSmsToMember(SmsSendSingleToUserReqDTO reqDTO) { + return smsSendService.sendSingleSmsToMember(reqDTO.getMobile(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialClientApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialClientApiImpl.java new file mode 100644 index 00000000..48dba0b6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialClientApiImpl.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.module.system.api.social; + +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO; +import com.chanko.yunxi.mes.heli.module.system.service.social.SocialClientService; +import me.chanjar.weixin.common.bean.WxJsapiSignature; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 社交应用的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SocialClientApiImpl implements SocialClientApi { + + @Resource + private SocialClientService socialClientService; + + @Override + public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) { + return socialClientService.getAuthorizeUrl(socialType, userType, redirectUri); + } + + @Override + public SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url) { + WxJsapiSignature signature = socialClientService.createWxMpJsapiSignature(userType, url); + return BeanUtils.toBean(signature, SocialWxJsapiSignatureRespDTO.class); + } + + @Override + public SocialWxPhoneNumberInfoRespDTO getWxMaPhoneNumberInfo(Integer userType, String phoneCode) { + WxMaPhoneNumberInfo info = socialClientService.getWxMaPhoneNumberInfo(userType, phoneCode); + return BeanUtils.toBean(info, SocialWxPhoneNumberInfoRespDTO.class); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialUserApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialUserApiImpl.java new file mode 100644 index 00000000..15e1e3ee --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/social/SocialUserApiImpl.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.api.social; + +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserBindReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserRespDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserUnbindReqDTO; +import com.chanko.yunxi.mes.heli.module.system.service.social.SocialUserService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 社交用户的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SocialUserApiImpl implements SocialUserApi { + + @Resource + private SocialUserService socialUserService; + + @Override + public String bindSocialUser(SocialUserBindReqDTO reqDTO) { + return socialUserService.bindSocialUser(reqDTO); + } + + @Override + public void unbindSocialUser(SocialUserUnbindReqDTO reqDTO) { + socialUserService.unbindSocialUser(reqDTO.getUserId(), reqDTO.getUserType(), + reqDTO.getSocialType(), reqDTO.getOpenid()); + } + + @Override + public SocialUserRespDTO getSocialUser(Integer userType, Integer socialType, String code, String state) { + return socialUserService.getSocialUser(userType, socialType, code, state); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/tenant/TenantApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/tenant/TenantApiImpl.java new file mode 100644 index 00000000..783d7b28 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/tenant/TenantApiImpl.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.api.tenant; + +import com.chanko.yunxi.mes.heli.module.system.service.tenant.TenantService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 多租户的 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class TenantApiImpl implements TenantApi { + + @Resource + private TenantService tenantService; + + @Override + public List getTenantIdList() { + return tenantService.getTenantIdList(); + } + + @Override + public void validateTenant(Long id) { + tenantService.validTenant(id); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/user/AdminUserApiImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/user/AdminUserApiImpl.java new file mode 100644 index 00000000..29ef7217 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/api/user/AdminUserApiImpl.java @@ -0,0 +1,80 @@ +package com.chanko.yunxi.mes.heli.module.system.api.user; + +import cn.hutool.core.util.ObjUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.api.user.dto.AdminUserRespDTO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.service.dept.DeptService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * Admin 用户 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class AdminUserApiImpl implements AdminUserApi { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + + @Override + public AdminUserRespDTO getUser(Long id) { + AdminUserDO user = userService.getUser(id); + return BeanUtils.toBean(user, AdminUserRespDTO.class); + } + + @Override + public Set getSubordinateIds(Long id) { + AdminUserDO user = userService.getUser(id); + if (user == null) { + return null; + } + + Set subordinateIds = null; // 下属用户编号 + DeptDO dept = deptService.getDept(user.getDeptId()); + // TODO @puhui999:需要递归查询到子部门;并且要排除到自己噢。 + // TODO @puhui999:保持 if return 原则,这里其实要判断不等于,则返回 null;最好返回 空集合,上面也是 + if (ObjUtil.equal(dept.getLeaderUserId(), id)) { // 校验是否是该部门的负责人 + List users = userService.getUserListByDeptIds(Collections.singletonList(dept.getId())); + subordinateIds = convertSet(users, AdminUserDO::getId); + } + return subordinateIds; + } + + @Override + public List getUserList(Collection ids) { + List users = userService.getUserList(ids); + return BeanUtils.toBean(users, AdminUserRespDTO.class); + } + + @Override + public List getUserListByDeptIds(Collection deptIds) { + List users = userService.getUserListByDeptIds(deptIds); + return BeanUtils.toBean(users, AdminUserRespDTO.class); + } + + @Override + public List getUserListByPostIds(Collection postIds) { + List users = userService.getUserListByPostIds(postIds); + return BeanUtils.toBean(users, AdminUserRespDTO.class); + } + + @Override + public void validateUserList(Collection ids) { + userService.validateUserList(ids); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/AuthController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/AuthController.http new file mode 100644 index 00000000..00ae2ba2 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/AuthController.http @@ -0,0 +1,33 @@ +### 请求 /login 接口 => 成功 +POST {{baseUrl}}/system/auth/login +Content-Type: application/json +tenant-id: {{adminTenentId}} +tag: Yunai.local + +{ + "username": "admin", + "password": "admin123", + "uuid": "3acd87a09a4f48fb9118333780e94883", + "code": "1024" +} + +### 请求 /login 接口 => 成功(无验证码) +POST {{baseUrl}}/system/auth/login +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "username": "admin", + "password": "admin123" +} + +### 请求 /get-permission-info 接口 => 成功 +GET {{baseUrl}}/system/auth/get-permission-info +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /list-menus 接口 => 成功 +GET {{baseUrl}}/system/list-menus +Authorization: Bearer {{token}} +#Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a +tenant-id: {{adminTenentId}} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/AuthController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/AuthController.java new file mode 100644 index 00000000..c218cf15 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/AuthController.java @@ -0,0 +1,164 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.auth; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.framework.security.config.SecurityProperties; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo.*; +import com.chanko.yunxi.mes.heli.module.system.convert.auth.AuthConvert; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.enums.logger.LoginLogTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.service.auth.AdminAuthService; +import com.chanko.yunxi.mes.heli.module.system.service.permission.MenuService; +import com.chanko.yunxi.mes.heli.module.system.service.permission.PermissionService; +import com.chanko.yunxi.mes.heli.module.system.service.permission.RoleService; +import com.chanko.yunxi.mes.heli.module.system.service.social.SocialClientService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +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 lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; +import static com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 认证") +@RestController +@RequestMapping("/system/auth") +@Validated +@Slf4j +public class AuthController { + + @Resource + private AdminAuthService authService; + @Resource + private AdminUserService userService; + @Resource + private RoleService roleService; + @Resource + private MenuService menuService; + @Resource + private PermissionService permissionService; + @Resource + private SocialClientService socialClientService; + + @Resource + private SecurityProperties securityProperties; + + @PostMapping("/login") + @PermitAll + @Operation(summary = "使用账号密码登录") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult login(@RequestBody @Valid AuthLoginReqVO reqVO) { + return success(authService.login(reqVO)); + } + + @PostMapping("/logout") + @PermitAll + @Operation(summary = "登出系统") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult logout(HttpServletRequest request) { + String token = SecurityFrameworkUtils.obtainAuthorization(request, + securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); + if (StrUtil.isNotBlank(token)) { + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); + } + return success(true); + } + + @PostMapping("/refresh-token") + @PermitAll + @Operation(summary = "刷新令牌") + @Parameter(name = "refreshToken", description = "刷新令牌", required = true) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return success(authService.refreshToken(refreshToken)); + } + + @GetMapping("/get-permission-info") + @Operation(summary = "获取登录用户的权限信息") + public CommonResult getPermissionInfo() { + // 1.1 获得用户信息 + AdminUserDO user = userService.getUser(getLoginUserId()); + if (user == null) { + return null; + } + + // 1.2 获得角色列表 + Set roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId()); + if (CollUtil.isEmpty(roleIds)) { + return success(AuthConvert.INSTANCE.convert(user, Collections.emptyList(), Collections.emptyList())); + } + List roles = roleService.getRoleList(roleIds); + roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色 + + // 1.3 获得菜单列表 + Set menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId)); + List menuList = menuService.getMenuList(menuIds); + menuList.removeIf(menu -> !CommonStatusEnum.ENABLE.getStatus().equals(menu.getStatus())); // 移除禁用的菜单 + + // 2. 拼接结果返回 + return success(AuthConvert.INSTANCE.convert(user, roles, menuList)); + } + + // ========== 短信登录相关 ========== + + @PostMapping("/sms-login") + @PermitAll + @Operation(summary = "使用短信验证码登录") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { + return success(authService.smsLogin(reqVO)); + } + + @PostMapping("/send-sms-code") + @PermitAll + @Operation(summary = "发送手机验证码") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult sendLoginSmsCode(@RequestBody @Valid AuthSmsSendReqVO reqVO) { + authService.sendSmsCode(reqVO); + return success(true); + } + + // ========== 社交登录相关 ========== + + @GetMapping("/social-auth-redirect") + @PermitAll + @Operation(summary = "社交授权的跳转") + @Parameters({ + @Parameter(name = "type", description = "社交类型", required = true), + @Parameter(name = "redirectUri", description = "回调路径") + }) + public CommonResult socialLogin(@RequestParam("type") Integer type, + @RequestParam("redirectUri") String redirectUri) { + return success(socialClientService.getAuthorizeUrl( + type, UserTypeEnum.ADMIN.getValue(), redirectUri)); + } + + @PostMapping("/social-login") + @PermitAll + @Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult socialQuickLogin(@RequestBody @Valid AuthSocialLoginReqVO reqVO) { + return success(authService.socialLogin(reqVO)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthLoginReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthLoginReqVO.java new file mode 100644 index 00000000..de2215b8 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthLoginReqVO.java @@ -0,0 +1,69 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@Schema(description = "管理后台 - 账号密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthLoginReqVO { + + @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mesyuanma") + @NotEmpty(message = "登录账号不能为空") + @Length(min = 4, max = 16, message = "账号长度为 4-16 位") + @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + // ========== 图片验证码相关 ========== + + @Schema(description = "验证码,验证码开启时,需要传递", requiredMode = Schema.RequiredMode.REQUIRED, + example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==") + @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) + private String captchaVerification; + + // ========== 绑定社交登录时,需要传递如下参数 ========== + + @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + private Integer socialType; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String socialCode; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + private String socialState; + + /** + * 开启验证码的 Group + */ + public interface CodeEnableGroup {} + + @AssertTrue(message = "授权码不能为空") + public boolean isSocialCodeValid() { + return socialType == null || StrUtil.isNotEmpty(socialCode); + } + + @AssertTrue(message = "授权 state 不能为空") + public boolean isSocialState() { + return socialType == null || StrUtil.isNotEmpty(socialState); + } + +} \ No newline at end of file diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthLoginRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthLoginRespVO.java new file mode 100644 index 00000000..b655484e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthLoginRespVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 登录 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthLoginRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "happy") + private String accessToken; + + @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") + private String refreshToken; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime expiresTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthMenuRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthMenuRespVO.java new file mode 100644 index 00000000..cfa3f36c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthMenuRespVO.java @@ -0,0 +1,53 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 登录用户的菜单信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthMenuRespVO { + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private Long id; + + @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") + private String path; + + @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") + private String component; + + @Schema(description = "组件名", example = "SystemUser") + private String componentName; + + @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") + private String icon; + + @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean visible; + + @Schema(description = "是否缓存", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean keepAlive; + + @Schema(description = "是否总是显示", example = "false") + private Boolean alwaysShow; + + /** + * 子路由 + */ + private List children; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java new file mode 100644 index 00000000..51f21109 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java @@ -0,0 +1,93 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Set; + +@Schema(description = "管理后台 - 登录用户的权限信息 Response VO,额外包括用户信息和角色列表") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthPermissionInfoRespVO { + + @Schema(description = "用户信息", requiredMode = Schema.RequiredMode.REQUIRED) + private UserVO user; + + @Schema(description = "角色标识数组", requiredMode = Schema.RequiredMode.REQUIRED) + private Set roles; + + @Schema(description = "操作权限数组", requiredMode = Schema.RequiredMode.REQUIRED) + private Set permissions; + + @Schema(description = "菜单树", requiredMode = Schema.RequiredMode.REQUIRED) + private List menus; + + @Schema(description = "用户信息 VO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class UserVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String nickname; + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.jpg") + private String avatar; + + } + + @Schema(description = "管理后台 - 登录用户的菜单信息 Response VO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class MenuVO { + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private Long id; + + @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") + private String path; + + @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") + private String component; + + @Schema(description = "组件名", example = "SystemUser") + private String componentName; + + @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") + private String icon; + + @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean visible; + + @Schema(description = "是否缓存", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean keepAlive; + + @Schema(description = "是否总是显示", example = "false") + private Boolean alwaysShow; + + /** + * 子路由 + */ + private List children; + + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java new file mode 100644 index 00000000..b6c50df9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo; + +import com.chanko.yunxi.mes.heli.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 短信验证码的登录 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSmsLoginReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mesyuanma") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "短信验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "验证码不能为空") + private String code; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java new file mode 100644 index 00000000..1e4e54af --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java @@ -0,0 +1,32 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo; + +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.Mobile; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsSceneEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 发送手机验证码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSmsSendReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mesyuanma") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "短信场景", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java new file mode 100644 index 00000000..13e79a5d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo; + +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 社交绑定登录 Request VO,使用 code 授权码 + 账号密码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSocialLoginReqVO { + + @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/captcha/CaptchaController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/captcha/CaptchaController.java new file mode 100644 index 00000000..a8025995 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/captcha/CaptchaController.java @@ -0,0 +1,56 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.captcha; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.xingyuv.captcha.model.common.ResponseModel; +import com.xingyuv.captcha.model.vo.CaptchaVO; +import com.xingyuv.captcha.service.CaptchaService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; + +@Tag(name = "管理后台 - 验证码") +@RestController("adminCaptchaController") +@RequestMapping("/system/captcha") +public class CaptchaController { + + @Resource + private CaptchaService captchaService; + + @PostMapping({"/get"}) + @Operation(summary = "获得验证码") + @PermitAll + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) { + assert request.getRemoteHost() != null; + data.setBrowserInfo(getRemoteId(request)); + return captchaService.get(data); + } + + @PostMapping("/check") + @Operation(summary = "校验验证码") + @PermitAll + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) { + data.setBrowserInfo(getRemoteId(request)); + return captchaService.check(data); + } + + public static String getRemoteId(HttpServletRequest request) { + String ip = ServletUtils.getClientIP(request); + String ua = request.getHeader("user-agent"); + if (StrUtil.isNotBlank(ip)) { + return ip + ua; + } + return request.getRemoteAddr() + ua; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/DeptController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/DeptController.java new file mode 100644 index 00000000..6f0a1a48 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/DeptController.java @@ -0,0 +1,84 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.service.dept.DeptService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 部门") +@RestController +@RequestMapping("/system/dept") +@Validated +public class DeptController { + + @Resource + private DeptService deptService; + + @PostMapping("create") + @Operation(summary = "创建部门") + @PreAuthorize("@ss.hasPermission('system:dept:create')") + public CommonResult createDept(@Valid @RequestBody DeptSaveReqVO createReqVO) { + Long deptId = deptService.createDept(createReqVO); + return success(deptId); + } + + @PutMapping("update") + @Operation(summary = "更新部门") + @PreAuthorize("@ss.hasPermission('system:dept:update')") + public CommonResult updateDept(@Valid @RequestBody DeptSaveReqVO updateReqVO) { + deptService.updateDept(updateReqVO); + return success(true); + } + + @DeleteMapping("delete") + @Operation(summary = "删除部门") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dept:delete')") + public CommonResult deleteDept(@RequestParam("id") Long id) { + deptService.deleteDept(id); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获取部门列表") + @PreAuthorize("@ss.hasPermission('system:dept:query')") + public CommonResult> getDeptList(DeptListReqVO reqVO) { + List list = deptService.getDeptList(reqVO); + return success(BeanUtils.toBean(list, DeptRespVO.class)); + } + + @GetMapping(value = {"/list-all-simple", "/simple-list"}) + @Operation(summary = "获取部门精简信息列表", description = "只包含被开启的部门,主要用于前端的下拉选项") + public CommonResult> getSimpleDeptList() { + List list = deptService.getDeptList( + new DeptListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus())); + return success(BeanUtils.toBean(list, DeptSimpleRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获得部门信息") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dept:query')") + public CommonResult getDept(@RequestParam("id") Long id) { + DeptDO dept = deptService.getDept(id); + return success(BeanUtils.toBean(dept, DeptRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/PostController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/PostController.java new file mode 100644 index 00000000..3e4d27bb --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/PostController.java @@ -0,0 +1,106 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.PostDO; +import com.chanko.yunxi.mes.heli.module.system.service.dept.PostService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 岗位") +@RestController +@RequestMapping("/system/post") +@Validated +public class PostController { + + @Resource + private PostService postService; + + @PostMapping("/create") + @Operation(summary = "创建岗位") + @PreAuthorize("@ss.hasPermission('system:post:create')") + public CommonResult createPost(@Valid @RequestBody PostSaveReqVO createReqVO) { + Long postId = postService.createPost(createReqVO); + return success(postId); + } + + @PutMapping("/update") + @Operation(summary = "修改岗位") + @PreAuthorize("@ss.hasPermission('system:post:update')") + public CommonResult updatePost(@Valid @RequestBody PostSaveReqVO updateReqVO) { + postService.updatePost(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除岗位") + @PreAuthorize("@ss.hasPermission('system:post:delete')") + public CommonResult deletePost(@RequestParam("id") Long id) { + postService.deletePost(id); + return success(true); + } + + @GetMapping(value = "/get") + @Operation(summary = "获得岗位信息") + @Parameter(name = "id", description = "岗位编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:post:query')") + public CommonResult getPost(@RequestParam("id") Long id) { + PostDO post = postService.getPost(id); + return success(BeanUtils.toBean(post, PostRespVO.class)); + } + + @GetMapping(value = {"/list-all-simple", "simple-list"}) + @Operation(summary = "获取岗位全列表", description = "只包含被开启的岗位,主要用于前端的下拉选项") + public CommonResult> getSimplePostList() { + // 获得岗位列表,只要开启状态的 + List list = postService.getPostList(null, Collections.singleton(CommonStatusEnum.ENABLE.getStatus())); + // 排序后,返回给前端 + list.sort(Comparator.comparing(PostDO::getSort)); + return success(BeanUtils.toBean(list, PostSimpleRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得岗位分页列表") + @PreAuthorize("@ss.hasPermission('system:post:query')") + public CommonResult> getPostPage(@Validated PostPageReqVO pageReqVO) { + PageResult pageResult = postService.getPostPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, PostRespVO.class)); + } + + @GetMapping("/export") + @Operation(summary = "岗位管理") + @PreAuthorize("@ss.hasPermission('system:post:export')") + @OperateLog(type = EXPORT) + public void export(HttpServletResponse response, @Validated PostPageReqVO reqVO) throws IOException { + reqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = postService.getPostPage(reqVO).getList(); + // 输出 + ExcelUtils.write(response, "岗位数据.xls", "岗位列表", PostRespVO.class, + BeanUtils.toBean(list, PostRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java new file mode 100644 index 00000000..d2fbd7d6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 部门列表 Request VO") +@Data +public class DeptListReqVO { + + @Schema(description = "部门名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptRespVO.java new file mode 100644 index 00000000..1f705bd4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptRespVO.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 部门信息 Response VO") +@Data +public class DeptRespVO { + + @Schema(description = "部门编号", example = "1024") + private Long id; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "父部门 ID", example = "1024") + private Long parentId; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer sort; + + @Schema(description = "负责人的用户编号", example = "2048") + private Long leaderUserId; + + @Schema(description = "联系电话", example = "15601691000") + private String phone; + + @Schema(description = "邮箱", example = "mes@iocoder.cn") + private String email; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java new file mode 100644 index 00000000..beafc03b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java @@ -0,0 +1,49 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 部门创建/修改 Request VO") +@Data +public class DeptSaveReqVO { + + @Schema(description = "部门编号", example = "1024") + private Long id; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotBlank(message = "部门名称不能为空") + @Size(max = 30, message = "部门名称长度不能超过 30 个字符") + private String name; + + @Schema(description = "父部门 ID", example = "1024") + private Long parentId; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "负责人的用户编号", example = "2048") + private Long leaderUserId; + + @Schema(description = "联系电话", example = "15601691000") + @Size(max = 11, message = "联系电话长度不能超过11个字符") + private String phone; + + @Schema(description = "邮箱", example = "mes@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java new file mode 100644 index 00000000..d05c7cf9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 部门精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeptSimpleRespVO { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "父部门 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostPageReqVO.java new file mode 100644 index 00000000..80aa0696 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostPageReqVO.java @@ -0,0 +1,22 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 岗位分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostPageReqVO extends PageParam { + + @Schema(description = "岗位编码,模糊匹配", example = "mes") + private String code; + + @Schema(description = "岗位名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostRespVO.java new file mode 100644 index 00000000..ef0f87c1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostRespVO.java @@ -0,0 +1,45 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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 PostRespVO { + + @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 = "mes") + @ExcelProperty("岗位编码") + private String code; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("岗位排序") + private Integer sort; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "备注", example = "快乐的备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java new file mode 100644 index 00000000..8c6001a9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostSaveReqVO.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 岗位创建/修改 Request VO") +@Data +public class PostSaveReqVO { + + @Schema(description = "岗位编号", example = "1024") + private Long id; + + @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小土豆") + @NotBlank(message = "岗位名称不能为空") + @Size(max = 50, message = "岗位名称长度不能超过 50 个字符") + private String name; + + @Schema(description = "岗位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @NotBlank(message = "岗位编码不能为空") + @Size(max = 64, message = "岗位编码长度不能超过64个字符") + private String code; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "备注", example = "快乐的备注") + private String remark; + +} \ No newline at end of file diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java new file mode 100644 index 00000000..0ff30904 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java @@ -0,0 +1,19 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post; + +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 岗位信息的精简 Response VO") +@Data +public class PostSimpleRespVO { + + @Schema(description = "岗位序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("岗位序号") + private Long id; + + @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小土豆") + @ExcelProperty("岗位名称") + private String name; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/DictDataController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/DictDataController.http new file mode 100644 index 00000000..f5243150 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/DictDataController.http @@ -0,0 +1,4 @@ +### 请求 /menu/list 接口 => 成功 +GET {{baseUrl}}/system/dict-data/list-all-simple +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/DictDataController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/DictDataController.java new file mode 100644 index 00000000..debe5a94 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/DictDataController.java @@ -0,0 +1,104 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data.DictDataRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data.DictDataSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictDataDO; +import com.chanko.yunxi.mes.heli.module.system.service.dict.DictDataService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 字典数据") +@RestController +@RequestMapping("/system/dict-data") +@Validated +public class DictDataController { + + @Resource + private DictDataService dictDataService; + + @PostMapping("/create") + @Operation(summary = "新增字典数据") + @PreAuthorize("@ss.hasPermission('system:dict:create')") + public CommonResult createDictData(@Valid @RequestBody DictDataSaveReqVO createReqVO) { + Long dictDataId = dictDataService.createDictData(createReqVO); + return success(dictDataId); + } + + @PutMapping("/update") + @Operation(summary = "修改字典数据") + @PreAuthorize("@ss.hasPermission('system:dict:update')") + public CommonResult updateDictData(@Valid @RequestBody DictDataSaveReqVO updateReqVO) { + dictDataService.updateDictData(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除字典数据") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dict:delete')") + public CommonResult deleteDictData(Long id) { + dictDataService.deleteDictData(id); + return success(true); + } + + @GetMapping(value = {"/list-all-simple", "simple-list"}) + @Operation(summary = "获得全部字典数据列表", description = "一般用于管理后台缓存字典数据在本地") + // 无需添加权限认证,因为前端全局都需要 + public CommonResult> getSimpleDictDataList() { + List list = dictDataService.getDictDataList( + CommonStatusEnum.ENABLE.getStatus(), null); + return success(BeanUtils.toBean(list, DictDataSimpleRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "/获得字典类型的分页列表") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult> getDictTypePage(@Valid DictDataPageReqVO pageReqVO) { + PageResult pageResult = dictDataService.getDictDataPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, DictDataRespVO.class)); + } + + @GetMapping(value = "/get") + @Operation(summary = "/查询字典数据详细") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult getDictData(@RequestParam("id") Long id) { + DictDataDO dictData = dictDataService.getDictData(id); + return success(BeanUtils.toBean(dictData, DictDataRespVO.class)); + } + + @GetMapping("/export") + @Operation(summary = "导出字典数据") + @PreAuthorize("@ss.hasPermission('system:dict:export')") + @OperateLog(type = EXPORT) + public void export(HttpServletResponse response, @Valid DictDataPageReqVO exportReqVO) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = dictDataService.getDictDataPage(exportReqVO).getList(); + // 输出 + ExcelUtils.write(response, "字典数据.xls", "数据", DictDataRespVO.class, + BeanUtils.toBean(list, DictDataRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/DictTypeController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/DictTypeController.java new file mode 100644 index 00000000..9b1690c6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/DictTypeController.java @@ -0,0 +1,102 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type.DictTypeRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type.DictTypeSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictTypeDO; +import com.chanko.yunxi.mes.heli.module.system.service.dict.DictTypeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 字典类型") +@RestController +@RequestMapping("/system/dict-type") +@Validated +public class DictTypeController { + + @Resource + private DictTypeService dictTypeService; + + @PostMapping("/create") + @Operation(summary = "创建字典类型") + @PreAuthorize("@ss.hasPermission('system:dict:create')") + public CommonResult createDictType(@Valid @RequestBody DictTypeSaveReqVO createReqVO) { + Long dictTypeId = dictTypeService.createDictType(createReqVO); + return success(dictTypeId); + } + + @PutMapping("/update") + @Operation(summary = "修改字典类型") + @PreAuthorize("@ss.hasPermission('system:dict:update')") + public CommonResult updateDictType(@Valid @RequestBody DictTypeSaveReqVO updateReqVO) { + dictTypeService.updateDictType(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除字典类型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dict:delete')") + public CommonResult deleteDictType(Long id) { + dictTypeService.deleteDictType(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得字典类型的分页列表") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult> pageDictTypes(@Valid DictTypePageReqVO pageReqVO) { + PageResult pageResult = dictTypeService.getDictTypePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, DictTypeRespVO.class)); + } + + @Operation(summary = "/查询字典类型详细") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @GetMapping(value = "/get") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult getDictType(@RequestParam("id") Long id) { + DictTypeDO dictType = dictTypeService.getDictType(id); + return success(BeanUtils.toBean(dictType, DictTypeRespVO.class)); + } + + @GetMapping(value = {"/list-all-simple", "simple-list"}) + @Operation(summary = "获得全部字典类型列表", description = "包括开启 + 禁用的字典类型,主要用于前端的下拉选项") + // 无需添加权限认证,因为前端全局都需要 + public CommonResult> getSimpleDictTypeList() { + List list = dictTypeService.getDictTypeList(); + return success(BeanUtils.toBean(list, DictTypeSimpleRespVO.class)); + } + + @Operation(summary = "导出数据类型") + @GetMapping("/export") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + @OperateLog(type = EXPORT) + public void export(HttpServletResponse response, @Valid DictTypePageReqVO exportReqVO) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = dictTypeService.getDictTypePage(exportReqVO).getList(); + // 导出 + ExcelUtils.write(response, "字典类型.xls", "数据", DictTypeRespVO.class, + BeanUtils.toBean(list, DictTypeRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java new file mode 100644 index 00000000..93a537cb --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 字典类型分页列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataPageReqVO extends PageParam { + + @Schema(description = "字典标签", example = "芋道") + @Size(max = 100, message = "字典标签长度不能超过100个字符") + private String label; + + @Schema(description = "字典类型,模糊匹配", example = "sys_common_sex") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String dictType; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataRespVO.java new file mode 100644 index 00000000..e1d6d7e6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataRespVO.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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 DictDataRespVO { + + @Schema(description = "字典数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("字典编码") + private Long id; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("字典排序") + private Integer sort; + + @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @ExcelProperty("字典标签") + private String label; + + @Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "iocoder") + @ExcelProperty("字典键值") + private String value; + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") + @ExcelProperty("字典类型") + private String dictType; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") + private String colorType; + + @Schema(description = "css 样式", example = "btn-visible") + private String cssClass; + + @Schema(description = "备注", example = "我是一个角色") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java new file mode 100644 index 00000000..aae21a04 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataSaveReqVO.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 字典数据创建/修改 Request VO") +@Data +public class DictDataSaveReqVO { + + @Schema(description = "字典数据编号", example = "1024") + private Long id; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotBlank(message = "字典标签不能为空") + @Size(max = 100, message = "字典标签长度不能超过100个字符") + private String label; + + @Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "iocoder") + @NotBlank(message = "字典键值不能为空") + @Size(max = 100, message = "字典键值长度不能超过100个字符") + private String value; + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") + @NotBlank(message = "字典类型不能为空") + @Size(max = 100, message = "字典类型长度不能超过100个字符") + private String dictType; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + + @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") + private String colorType; + + @Schema(description = "css 样式", example = "btn-visible") + private String cssClass; + + @Schema(description = "备注", example = "我是一个角色") + private String remark; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java new file mode 100644 index 00000000..8625cb31 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 数据字典精简 Response VO") +@Data +public class DictDataSimpleRespVO { + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "gender") + private String dictType; + + @Schema(description = "字典键值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String value; + + @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "男") + private String label; + + @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") + private String colorType; + + @Schema(description = "css 样式", example = "btn-visible") + private String cssClass; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java new file mode 100644 index 00000000..fcb37eb6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 字典类型分页列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictTypePageReqVO extends PageParam { + + @Schema(description = "字典类型名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "字典类型,模糊匹配", example = "sys_common_sex") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String type; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java new file mode 100644 index 00000000..f8c270be --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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 DictTypeRespVO { + + @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 = "sys_common_sex") + @ExcelProperty("字典类型") + private String type; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "备注", example = "快乐的备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypeSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypeSaveReqVO.java new file mode 100644 index 00000000..37b05189 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypeSaveReqVO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 字典类型创建/修改 Request VO") +@Data +public class DictTypeSaveReqVO { + + @Schema(description = "字典类型编号", example = "1024") + private Long id; + + @Schema(description = "字典名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "性别") + @NotBlank(message = "字典名称不能为空") + @Size(max = 100, message = "字典类型名称长度不能超过100个字符") + private String name; + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") + @NotNull(message = "字典类型不能为空") + @Size(max = 100, message = "字典类型类型长度不能超过 100 个字符") + private String type; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "快乐的备注") + private String remark; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java new file mode 100644 index 00000000..7ad87ea0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java @@ -0,0 +1,19 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 字典类型精简信息 Response VO") +@Data +public class DictTypeSimpleRespVO { + + @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 = "sys_common_sex") + private String type; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/ErrorCodeController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/ErrorCodeController.http new file mode 100644 index 00000000..06b87231 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/ErrorCodeController.http @@ -0,0 +1,13 @@ +### 创建错误码 +POST {{baseUrl}}/inra/error-code/create +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "code": 200, + "message": "成功", + "group": "test", + "type": 1 +} + diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/ErrorCodeController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/ErrorCodeController.java new file mode 100644 index 00000000..658ed60f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/ErrorCodeController.java @@ -0,0 +1,91 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode.vo.*; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import com.chanko.yunxi.mes.heli.module.system.service.errorcode.ErrorCodeService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 错误码") +@RestController +@RequestMapping("/system/error-code") +@Validated +public class ErrorCodeController { + + @Resource + private ErrorCodeService errorCodeService; + + @PostMapping("/create") + @Operation(summary = "创建错误码") + @PreAuthorize("@ss.hasPermission('system:error-code:create')") + public CommonResult createErrorCode(@Valid @RequestBody ErrorCodeSaveReqVO createReqVO) { + return success(errorCodeService.createErrorCode(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新错误码") + @PreAuthorize("@ss.hasPermission('system:error-code:update')") + public CommonResult updateErrorCode(@Valid @RequestBody ErrorCodeSaveReqVO updateReqVO) { + errorCodeService.updateErrorCode(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除错误码") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:error-code:delete')") + public CommonResult deleteErrorCode(@RequestParam("id") Long id) { + errorCodeService.deleteErrorCode(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得错误码") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:error-code:query')") + public CommonResult getErrorCode(@RequestParam("id") Long id) { + ErrorCodeDO errorCode = errorCodeService.getErrorCode(id); + return success(BeanUtils.toBean(errorCode, ErrorCodeRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得错误码分页") + @PreAuthorize("@ss.hasPermission('system:error-code:query')") + public CommonResult> getErrorCodePage(@Valid ErrorCodePageReqVO pageVO) { + PageResult pageResult = errorCodeService.getErrorCodePage(pageVO); + return success(BeanUtils.toBean(pageResult, ErrorCodeRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出错误码 Excel") + @PreAuthorize("@ss.hasPermission('system:error-code:export')") + @OperateLog(type = EXPORT) + public void exportErrorCodeExcel(@Valid ErrorCodePageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = errorCodeService.getErrorCodePage(exportReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "错误码.xls", "数据", ErrorCodeRespVO.class, + BeanUtils.toBean(list, ErrorCodeRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java new file mode 100644 index 00000000..cc922f87 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode.vo; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 错误码分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodePageReqVO extends PageParam { + + @Schema(description = "错误码类型,参见 ErrorCodeTypeEnum 枚举类", example = "1") + private Integer type; + + @Schema(description = "应用名", example = "dashboard") + private String applicationName; + + @Schema(description = "错误码编码", example = "1234") + private Integer code; + + @Schema(description = "错误码错误提示", example = "帅气") + private String message; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java new file mode 100644 index 00000000..c74a7ac1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode.vo; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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 javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 错误码 Response VO") +@Data +@ExcelIgnoreUnannotated +public class ErrorCodeRespVO { + + @Schema(description = "错误码编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("错误码编号") + private Long id; + + @Schema(description = "错误码类型,参见 ErrorCodeTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "错误码类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.ERROR_CODE_TYPE) + private Integer type; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard") + @ExcelProperty("应用名") + private String applicationName; + + @Schema(description = "错误码编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234") + @ExcelProperty("错误码编码") + private Integer code; + + @Schema(description = "错误码错误提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "帅气") + @ExcelProperty("错误码错误提示") + private String message; + + @Schema(description = "备注", example = "哈哈哈") + @ExcelProperty("备注") + private String memo; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/vo/ErrorCodeSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/vo/ErrorCodeSaveReqVO.java new file mode 100644 index 00000000..99f74113 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/errorcode/vo/ErrorCodeSaveReqVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 错误码创建/修改 Request VO") +@Data +public class ErrorCodeSaveReqVO { + + @Schema(description = "错误码编号", example = "1024") + private Long id; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard") + @NotNull(message = "应用名不能为空") + private String applicationName; + + @Schema(description = "错误码编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234") + @NotNull(message = "错误码编码不能为空") + private Integer code; + + @Schema(description = "错误码错误提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "帅气") + @NotNull(message = "错误码错误提示不能为空") + private String message; + + @Schema(description = "备注", example = "哈哈哈") + private String memo; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/ip/AreaController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/ip/AreaController.http new file mode 100644 index 00000000..f1b893d0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/ip/AreaController.http @@ -0,0 +1,5 @@ +### 获得地区树 +GET {{baseUrl}}/system/area/tree +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/ip/AreaController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/ip/AreaController.java new file mode 100644 index 00000000..8e5da8e4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/ip/AreaController.java @@ -0,0 +1,50 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.ip; + +import cn.hutool.core.lang.Assert; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.ip.core.Area; +import com.chanko.yunxi.mes.heli.framework.ip.core.utils.AreaUtils; +import com.chanko.yunxi.mes.heli.framework.ip.core.utils.IPUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.ip.vo.AreaNodeRespVO; +import com.chanko.yunxi.mes.heli.module.system.convert.ip.AreaConvert; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +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.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 地区") +@RestController +@RequestMapping("/system/area") +@Validated +public class AreaController { + + @GetMapping("/tree") + @Operation(summary = "获得地区树") + public CommonResult> getAreaTree() { + Area area = AreaUtils.getArea(Area.ID_CHINA); + Assert.notNull(area, "获取不到中国"); + return success(AreaConvert.INSTANCE.convertList(area.getChildren())); + } + + @GetMapping("/get-by-ip") + @Operation(summary = "获得 IP 对应的地区名") + @Parameter(name = "ip", description = "IP", required = true) + public CommonResult getAreaByIp(@RequestParam("ip") String ip) { + // 获得城市 + Area area = IPUtils.getArea(ip); + if (area == null) { + return success("未知"); + } + // 格式化返回 + return success(AreaUtils.format(area.getId())); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/ip/vo/AreaNodeRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/ip/vo/AreaNodeRespVO.java new file mode 100644 index 00000000..2c90fd73 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/ip/vo/AreaNodeRespVO.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.ip.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 地区节点 Response VO") +@Data +public class AreaNodeRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") + private Integer id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "北京") + private String name; + + /** + * 子节点 + */ + private List children; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/LoginLogController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/LoginLogController.java new file mode 100644 index 00000000..3decbadf --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/LoginLogController.java @@ -0,0 +1,59 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.loginlog.LoginLogRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.LoginLogDO; +import com.chanko.yunxi.mes.heli.module.system.service.logger.LoginLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 登录日志") +@RestController +@RequestMapping("/system/login-log") +@Validated +public class LoginLogController { + + @Resource + private LoginLogService loginLogService; + + @GetMapping("/page") + @Operation(summary = "获得登录日志分页列表") + @PreAuthorize("@ss.hasPermission('system:login-log:query')") + public CommonResult> getLoginLogPage(@Valid LoginLogPageReqVO pageReqVO) { + PageResult pageResult = loginLogService.getLoginLogPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, LoginLogRespVO.class)); + } + + @GetMapping("/export") + @Operation(summary = "导出登录日志 Excel") + @PreAuthorize("@ss.hasPermission('system:login-log:export')") + @OperateLog(type = EXPORT) + public void exportLoginLog(HttpServletResponse response, @Valid LoginLogPageReqVO exportReqVO) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = loginLogService.getLoginLogPage(exportReqVO).getList(); + // 输出 + ExcelUtils.write(response, "登录日志.xls", "数据列表", LoginLogRespVO.class, + BeanUtils.toBean(list, LoginLogRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/OperateLogController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/OperateLogController.http new file mode 100644 index 00000000..f667482d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/OperateLogController.http @@ -0,0 +1,4 @@ +### 请求 /system/operate-log/demo 接口 => 成功 +GET {{baseUrl}}/system/operate-log/demo +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/OperateLogController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/OperateLogController.java new file mode 100644 index 00000000..4ef32087 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/OperateLogController.java @@ -0,0 +1,71 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO; +import com.chanko.yunxi.mes.heli.module.system.convert.logger.OperateLogConvert; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.OperateLogDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.service.logger.OperateLogService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertList; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 操作日志") +@RestController +@RequestMapping("/system/operate-log") +@Validated +public class OperateLogController { + + @Resource + private OperateLogService operateLogService; + @Resource + private AdminUserService userService; + + @GetMapping("/page") + @Operation(summary = "查看操作日志分页列表") + @PreAuthorize("@ss.hasPermission('system:operate-log:query')") + public CommonResult> pageOperateLog(@Valid OperateLogPageReqVO pageReqVO) { + PageResult pageResult = operateLogService.getOperateLogPage(pageReqVO); + // 获得拼接需要的数据 + Map userMap = userService.getUserMap( + convertList(pageResult.getList(), OperateLogDO::getUserId)); + return success(new PageResult<>(OperateLogConvert.INSTANCE.convertList(pageResult.getList(), userMap), + pageResult.getTotal())); + } + + @Operation(summary = "导出操作日志") + @GetMapping("/export") + @PreAuthorize("@ss.hasPermission('system:operate-log:export')") + @OperateLog(type = EXPORT) + public void exportOperateLog(HttpServletResponse response, @Valid OperateLogPageReqVO exportReqVO) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = operateLogService.getOperateLogPage(exportReqVO).getList(); + // 输出 + Map userMap = userService.getUserMap( + convertList(list, OperateLogDO::getUserId)); + ExcelUtils.write(response, "操作日志.xls", "数据列表", OperateLogRespVO.class, + OperateLogConvert.INSTANCE.convertList(list, userMap)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java new file mode 100644 index 00000000..fb32461f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.loginlog; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 登录日志分页列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class LoginLogPageReqVO extends PageParam { + + @Schema(description = "用户 IP,模拟匹配", example = "127.0.0.1") + private String userIp; + + @Schema(description = "用户账号,模拟匹配", example = "芋道") + private String username; + + @Schema(description = "操作状态", example = "true") + private Boolean status; + + @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/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java new file mode 100644 index 00000000..75286fba --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java @@ -0,0 +1,57 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.loginlog; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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 LoginLogRespVO { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("日志主键") + private Long id; + + @Schema(description = "日志类型,参见 LoginLogTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "日志类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.LOGIN_TYPE) + private Integer logType; + + @Schema(description = "用户编号", example = "666") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer userType; + + @Schema(description = "链路追踪编号", example = "89aca178-a370-411c-ae02-3f0d672be4ab") + private String traceId; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @ExcelProperty("用户账号") + private String username; + + @Schema(description = "登录结果,参见 LoginResultEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "登录结果", converter = DictConvert.class) + @DictFormat(DictTypeConstants.LOGIN_RESULT) + private Integer result; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @ExcelProperty("登录 IP") + private String userIp; + + @Schema(description = "浏览器 UserAgent", example = "Mozilla/5.0") + @ExcelProperty("浏览器 UA") + private String userAgent; + + @Schema(description = "登录时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("登录时间") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java new file mode 100644 index 00000000..d12689c9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java @@ -0,0 +1,32 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.operatelog; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 操作日志分页列表 Request VO") +@Data +public class OperateLogPageReqVO extends PageParam { + + @Schema(description = "操作模块,模拟匹配", example = "订单") + private String module; + + @Schema(description = "用户昵称,模拟匹配", example = "芋道") + private String userNickname; + + @Schema(description = "操作分类,参见 OperateLogTypeEnum 枚举类", example = "1") + private Integer type; + + @Schema(description = "操作状态", example = "true") + private Boolean success; + + @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[] startTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java new file mode 100644 index 00000000..84aa5823 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java @@ -0,0 +1,90 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.operatelog; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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 javax.validation.constraints.NotEmpty; +import java.time.LocalDateTime; +import java.util.Map; + +@Schema(description = "管理后台 - 操作日志 Response VO") +@Data +@ExcelIgnoreUnannotated +public class OperateLogRespVO { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("日志编号") + private Long id; + + @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "89aca178-a370-411c-ae02-3f0d672be4ab") + private String traceId; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @ExcelProperty("操作人") + private String userNickname; + + @Schema(description = "操作模块", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单") + @ExcelProperty("操作模块") + private String module; + + @Schema(description = "操作名", requiredMode = Schema.RequiredMode.REQUIRED, example = "创建订单") + @ExcelProperty("操作名") + private String name; + + @Schema(description = "操作分类,参见 OperateLogTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "操作类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.OPERATE_TYPE) + private Integer type; + + @Schema(description = "操作明细", example = "修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。") + private String content; + + @Schema(description = "拓展字段", example = "{'orderId': 1}") + private Map exts; + + @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET") + @NotEmpty(message = "请求方法名不能为空") + private String requestMethod; + + @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xxx/yyy") + private String requestUrl; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + private String userIp; + + @Schema(description = "浏览器 UserAgent", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0") + private String userAgent; + + @Schema(description = "Java 方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "com.chanko.yunxi.mes.heli.adminserver.UserController.save(...)") + private String javaMethod; + + @Schema(description = "Java 方法的参数") + private String javaMethodArgs; + + @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("操作日志") + private LocalDateTime startTime; + + @Schema(description = "执行时长,单位:毫秒", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("执行时长") + private Integer duration; + + @Schema(description = "结果码", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty(value = "结果码") + private Integer resultCode; + + @Schema(description = "结果提示") + private String resultMsg; + + @Schema(description = "结果数据") + private String resultData; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailAccountController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailAccountController.java new file mode 100644 index 00000000..a9c2e68e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailAccountController.java @@ -0,0 +1,81 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail; + + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account.MailAccountRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account.MailAccountSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account.MailAccountSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailAccountDO; +import com.chanko.yunxi.mes.heli.module.system.service.mail.MailAccountService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 邮箱账号") +@RestController +@RequestMapping("/system/mail-account") +public class MailAccountController { + + @Resource + private MailAccountService mailAccountService; + + @PostMapping("/create") + @Operation(summary = "创建邮箱账号") + @PreAuthorize("@ss.hasPermission('system:mail-account:create')") + public CommonResult createMailAccount(@Valid @RequestBody MailAccountSaveReqVO createReqVO) { + return success(mailAccountService.createMailAccount(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改邮箱账号") + @PreAuthorize("@ss.hasPermission('system:mail-account:update')") + public CommonResult updateMailAccount(@Valid @RequestBody MailAccountSaveReqVO updateReqVO) { + mailAccountService.updateMailAccount(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除邮箱账号") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:mail-account:delete')") + public CommonResult deleteMailAccount(@RequestParam Long id) { + mailAccountService.deleteMailAccount(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得邮箱账号") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:mail-account:get')") + public CommonResult getMailAccount(@RequestParam("id") Long id) { + MailAccountDO account = mailAccountService.getMailAccount(id); + return success(BeanUtils.toBean(account, MailAccountRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得邮箱账号分页") + @PreAuthorize("@ss.hasPermission('system:mail-account:query')") + public CommonResult> getMailAccountPage(@Valid MailAccountPageReqVO pageReqVO) { + PageResult pageResult = mailAccountService.getMailAccountPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, MailAccountRespVO.class)); + } + + @GetMapping({"/list-all-simple", "simple-list"}) + @Operation(summary = "获得邮箱账号精简列表") + public CommonResult> getSimpleMailAccountList() { + List list = mailAccountService.getMailAccountList(); + return success(BeanUtils.toBean(list, MailAccountSimpleRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailLogController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailLogController.java new file mode 100644 index 00000000..ec57cfa5 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailLogController.java @@ -0,0 +1,49 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.log.MailLogRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailLogDO; +import com.chanko.yunxi.mes.heli.module.system.service.mail.MailLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 邮件日志") +@RestController +@RequestMapping("/system/mail-log") +public class MailLogController { + + @Resource + private MailLogService mailLogService; + + @GetMapping("/page") + @Operation(summary = "获得邮箱日志分页") + @PreAuthorize("@ss.hasPermission('system:mail-log:query')") + public CommonResult> getMailLogPage(@Valid MailLogPageReqVO pageVO) { + PageResult pageResult = mailLogService.getMailLogPage(pageVO); + return success(BeanUtils.toBean(pageResult, MailLogRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获得邮箱日志") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:mail-log:query')") + public CommonResult getMailTemplate(@RequestParam("id") Long id) { + MailLogDO log = mailLogService.getMailLog(id); + return success(BeanUtils.toBean(log, MailLogRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailTemplateController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailTemplateController.http new file mode 100644 index 00000000..f3c47f51 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailTemplateController.http @@ -0,0 +1,14 @@ +### 请求 /system/mail-template/send-mail 接口 => 成功 +POST {{baseUrl}}/system/mail-template/send-mail +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "templateCode": "test_01", + "mail": "7685413@qq.com", + "templateParams": { + "key01": "value01", + "key02": "value02" + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailTemplateController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailTemplateController.java new file mode 100644 index 00000000..4e343758 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/MailTemplateController.java @@ -0,0 +1,89 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template.*; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.service.mail.MailSendService; +import com.chanko.yunxi.mes.heli.module.system.service.mail.MailTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 邮件模版") +@RestController +@RequestMapping("/system/mail-template") +public class MailTemplateController { + + @Resource + private MailTemplateService mailTempleService; + @Resource + private MailSendService mailSendService; + + @PostMapping("/create") + @Operation(summary = "创建邮件模版") + @PreAuthorize("@ss.hasPermission('system:mail-template:create')") + public CommonResult createMailTemplate(@Valid @RequestBody MailTemplateSaveReqVO createReqVO){ + return success(mailTempleService.createMailTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改邮件模版") + @PreAuthorize("@ss.hasPermission('system:mail-template:update')") + public CommonResult updateMailTemplate(@Valid @RequestBody MailTemplateSaveReqVO updateReqVO){ + mailTempleService.updateMailTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除邮件模版") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:mail-template:delete')") + public CommonResult deleteMailTemplate(@RequestParam("id") Long id) { + mailTempleService.deleteMailTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得邮件模版") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:mail-template:get')") + public CommonResult getMailTemplate(@RequestParam("id") Long id) { + MailTemplateDO template = mailTempleService.getMailTemplate(id); + return success(BeanUtils.toBean(template, MailTemplateRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得邮件模版分页") + @PreAuthorize("@ss.hasPermission('system:mail-template:query')") + public CommonResult> getMailTemplatePage(@Valid MailTemplatePageReqVO pageReqVO) { + PageResult pageResult = mailTempleService.getMailTemplatePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, MailTemplateRespVO.class)); + } + + @GetMapping({"/list-all-simple", "simple-list"}) + @Operation(summary = "获得邮件模版精简列表") + public CommonResult> getSimpleTemplateList() { + List list = mailTempleService.getMailTemplateList(); + return success(BeanUtils.toBean(list, MailTemplateSimpleRespVO.class)); + } + + @PostMapping("/send-mail") + @Operation(summary = "发送短信") + @PreAuthorize("@ss.hasPermission('system:mail-template:send-mail')") + public CommonResult sendMail(@Valid @RequestBody MailTemplateSendReqVO sendReqVO) { + return success(mailSendService.sendSingleMailToAdmin(sendReqVO.getMail(), getLoginUserId(), + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountPageReqVO.java new file mode 100644 index 00000000..8807730a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountPageReqVO.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account; + +import com.chanko.yunxi.mes.heli.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 +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailAccountPageReqVO extends PageParam { + + @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "mesyuanma@123.com") + private String mail; + + @Schema(description = "用户名" , requiredMode = Schema.RequiredMode.REQUIRED , example = "mes") + private String username; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountRespVO.java new file mode 100644 index 00000000..68de6a7e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountRespVO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 邮箱账号 Response VO") +@Data +public class MailAccountRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "mesyuanma@123.com") + private String mail; + + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + private String password; + + @Schema(description = "SMTP 服务器域名", requiredMode = Schema.RequiredMode.REQUIRED, example = "www.iocoder.cn") + private String host; + + @Schema(description = "SMTP 服务器端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") + private Integer port; + + @Schema(description = "是否开启 ssl", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean sslEnable; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountSaveReqVO.java new file mode 100644 index 00000000..a2be9dac --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountSaveReqVO.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 邮箱账号创建/修改 Request VO") +@Data +public class MailAccountSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "mesyuanma@123.com") + @NotNull(message = "邮箱不能为空") + @Email(message = "必须是 Email 格式") + private String mail; + + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @NotNull(message = "用户名不能为空") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotNull(message = "密码必填") + private String password; + + @Schema(description = "SMTP 服务器域名", requiredMode = Schema.RequiredMode.REQUIRED, example = "www.iocoder.cn") + @NotNull(message = "SMTP 服务器域名不能为空") + private String host; + + @Schema(description = "SMTP 服务器端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") + @NotNull(message = "SMTP 服务器端口不能为空") + private Integer port; + + @Schema(description = "是否开启 ssl", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否开启 ssl 必填") + private Boolean sslEnable; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountSimpleRespVO.java new file mode 100644 index 00000000..e08f29bd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/account/MailAccountSimpleRespVO.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 邮箱账号的精简 Response VO") +@Data +public class MailAccountSimpleRespVO { + + @Schema(description = "邮箱编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "768541388@qq.com") + private String mail; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/log/MailLogPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/log/MailLogPageReqVO.java new file mode 100644 index 00000000..15a99b08 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/log/MailLogPageReqVO.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.log; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 邮箱日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailLogPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "30883") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") + private Integer userType; + + @Schema(description = "接收邮箱地址,模糊匹配", example = "76854@qq.com") + private String toMail; + + @Schema(description = "邮箱账号编号", example = "18107") + private Long accountId; + + @Schema(description = "模板编号", example = "5678") + private Long templateId; + + @Schema(description = "发送状态,参见 MailSendStatusEnum 枚举", example = "1") + private Integer sendStatus; + + @Schema(description = "发送时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] sendTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/log/MailLogRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/log/MailLogRespVO.java new file mode 100644 index 00000000..a062040b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/log/MailLogRespVO.java @@ -0,0 +1,67 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 邮件日志 Response VO") +@Data +public class MailLogRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31020") + private Long id; + + @Schema(description = "用户编号", example = "30883") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") + private Byte userType; + + @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "76854@qq.com") + private String toMail; + + @Schema(description = "邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18107") + private Long accountId; + + @Schema(description = "发送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "85757@qq.com") + private String fromMail; + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5678") + private Long templateId; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + private String templateCode; + + @Schema(description = "模版发送人名称", example = "李四") + private String templateNickname; + + @Schema(description = "邮件标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试标题") + private String templateTitle; + + @Schema(description = "邮件内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试内容") + private String templateContent; + + @Schema(description = "邮件参数", requiredMode = Schema.RequiredMode.REQUIRED) + private Map templateParams; + + @Schema(description = "发送状态,参见 MailSendStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Byte sendStatus; + + @Schema(description = "发送时间") + private LocalDateTime sendTime; + + @Schema(description = "发送返回的消息 ID", example = "28568") + private String sendMessageId; + + @Schema(description = "发送异常") + private String sendException; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplatePageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplatePageReqVO.java new file mode 100644 index 00000000..5cf0849b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplatePageReqVO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 邮件模版分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailTemplatePageReqVO extends PageParam { + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", example = "1") + private Integer status; + + @Schema(description = "标识,模糊匹配", example = "code_1024") + private String code; + + @Schema(description = "名称,模糊匹配", example = "芋头") + private String name; + + @Schema(description = "账号编号", example = "2048") + private Long accountId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateRespVO.java new file mode 100644 index 00000000..8046ea5e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateRespVO.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 邮件末班 Response VO") +@Data +public class MailTemplateRespVO { + + @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 = "test") + private String code; + + @Schema(description = "发送的邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountId; + + @Schema(description = "发送人名称", example = "芋头") + private String nickname; + + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "注册成功") + private String title; + + @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,注册成功啦") + private String content; + + @Schema(description = "参数数组", example = "name,code") + private List params; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "备注", example = "奥特曼") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateSaveReqVO.java new file mode 100644 index 00000000..7866ec97 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateSaveReqVO.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 邮件模版创建/修改 Request VO") +@Data +public class MailTemplateSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试名字") + @NotNull(message = "名称不能为空") + private String name; + + @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "test") + @NotNull(message = "模版编号不能为空") + private String code; + + @Schema(description = "发送的邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "发送的邮箱账号编号不能为空") + private Long accountId; + + @Schema(description = "发送人名称", example = "芋头") + private String nickname; + + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "注册成功") + @NotEmpty(message = "标题不能为空") + private String title; + + @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,注册成功啦") + @NotEmpty(message = "内容不能为空") + private String content; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "奥特曼") + private String remark; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java new file mode 100644 index 00000000..e94da6fe --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 邮件发送 Req VO") +@Data +public class MailTemplateSendReqVO { + + @Schema(description = "接收邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "7685413@qq.com") + @NotEmpty(message = "接收邮箱不能为空") + private String mail; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String templateCode; + + @Schema(description = "模板参数") + private Map templateParams; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java new file mode 100644 index 00000000..792214e3 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 邮件模版的精简 Response VO") +@Data +public class MailTemplateSimpleRespVO { + + @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "模版名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "哒哒哒") + private String name; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/NoticeController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/NoticeController.java new file mode 100644 index 00000000..3eceafb6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/NoticeController.java @@ -0,0 +1,92 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notice; + +import cn.hutool.core.lang.Assert; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.infra.api.websocket.WebSocketSenderApi; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo.NoticePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo.NoticeRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo.NoticeSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notice.NoticeDO; +import com.chanko.yunxi.mes.heli.module.system.service.notice.NoticeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 通知公告") +@RestController +@RequestMapping("/system/notice") +@Validated +public class NoticeController { + + @Resource + private NoticeService noticeService; + + @Resource + private WebSocketSenderApi webSocketSenderApi; + + @PostMapping("/create") + @Operation(summary = "创建通知公告") + @PreAuthorize("@ss.hasPermission('system:notice:create')") + public CommonResult createNotice(@Valid @RequestBody NoticeSaveReqVO createReqVO) { + Long noticeId = noticeService.createNotice(createReqVO); + return success(noticeId); + } + + @PutMapping("/update") + @Operation(summary = "修改通知公告") + @PreAuthorize("@ss.hasPermission('system:notice:update')") + public CommonResult updateNotice(@Valid @RequestBody NoticeSaveReqVO updateReqVO) { + noticeService.updateNotice(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除通知公告") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:notice:delete')") + public CommonResult deleteNotice(@RequestParam("id") Long id) { + noticeService.deleteNotice(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获取通知公告列表") + @PreAuthorize("@ss.hasPermission('system:notice:query')") + public CommonResult> getNoticePage(@Validated NoticePageReqVO pageReqVO) { + PageResult pageResult = noticeService.getNoticePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, NoticeRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获得通知公告") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:notice:query')") + public CommonResult getNotice(@RequestParam("id") Long id) { + NoticeDO notice = noticeService.getNotice(id); + return success(BeanUtils.toBean(notice, NoticeRespVO.class)); + } + + @PostMapping("/push") + @Operation(summary = "推送通知公告", description = "只发送给 websocket 连接在线的用户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:notice:update')") + public CommonResult push(@RequestParam("id") Long id) { + NoticeDO notice = noticeService.getNotice(id); + Assert.notNull(notice, "公告不能为空"); + // 通过 websocket 推送给在线的用户 + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), "notice-push", notice); + return success(true); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/vo/NoticePageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/vo/NoticePageReqVO.java new file mode 100644 index 00000000..da066380 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/vo/NoticePageReqVO.java @@ -0,0 +1,19 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 通知公告分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticePageReqVO extends PageParam { + + @Schema(description = "通知公告名称,模糊匹配", example = "芋道") + private String title; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/vo/NoticeRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/vo/NoticeRespVO.java new file mode 100644 index 00000000..701ad914 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/vo/NoticeRespVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 通知公告信息 Response VO") +@Data +public class NoticeRespVO { + + @Schema(description = "通知公告序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公告标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") + private String title; + + @Schema(description = "公告类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") + private Integer type; + + @Schema(description = "公告内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "半生编码") + private String content; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/vo/NoticeSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/vo/NoticeSaveReqVO.java new file mode 100644 index 00000000..b423de71 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notice/vo/NoticeSaveReqVO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 通知公告创建/修改 Request VO") +@Data +public class NoticeSaveReqVO { + + @Schema(description = "岗位公告编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "岗位公告编号不能为空") + private Long id; + + @Schema(description = "公告标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") + @NotBlank(message = "公告标题不能为空") + @Size(max = 50, message = "公告标题不能超过50个字符") + private String title; + + @Schema(description = "公告类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") + @NotNull(message = "公告类型不能为空") + private Integer type; + + @Schema(description = "公告内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "半生编码") + private String content; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/NotifyMessageController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/NotifyMessageController.java new file mode 100644 index 00000000..8cbfffe5 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/NotifyMessageController.java @@ -0,0 +1,96 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notify; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message.NotifyMessageRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyMessageDO; +import com.chanko.yunxi.mes.heli.module.system.service.notify.NotifyMessageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 我的站内信") +@RestController +@RequestMapping("/system/notify-message") +@Validated +public class NotifyMessageController { + + @Resource + private NotifyMessageService notifyMessageService; + + // ========== 管理所有的站内信 ========== + + @GetMapping("/get") + @Operation(summary = "获得站内信") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:notify-message:query')") + public CommonResult getNotifyMessage(@RequestParam("id") Long id) { + NotifyMessageDO message = notifyMessageService.getNotifyMessage(id); + return success(BeanUtils.toBean(message, NotifyMessageRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得站内信分页") + @PreAuthorize("@ss.hasPermission('system:notify-message:query')") + public CommonResult> getNotifyMessagePage(@Valid NotifyMessagePageReqVO pageVO) { + PageResult pageResult = notifyMessageService.getNotifyMessagePage(pageVO); + return success(BeanUtils.toBean(pageResult, NotifyMessageRespVO.class)); + } + + // ========== 查看自己的站内信 ========== + + @GetMapping("/my-page") + @Operation(summary = "获得我的站内信分页") + public CommonResult> getMyMyNotifyMessagePage(@Valid NotifyMessageMyPageReqVO pageVO) { + PageResult pageResult = notifyMessageService.getMyMyNotifyMessagePage(pageVO, + getLoginUserId(), UserTypeEnum.ADMIN.getValue()); + return success(BeanUtils.toBean(pageResult, NotifyMessageRespVO.class)); + } + + @PutMapping("/update-read") + @Operation(summary = "标记站内信为已读") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + public CommonResult updateNotifyMessageRead(@RequestParam("ids") List ids) { + notifyMessageService.updateNotifyMessageRead(ids, getLoginUserId(), UserTypeEnum.ADMIN.getValue()); + return success(Boolean.TRUE); + } + + @PutMapping("/update-all-read") + @Operation(summary = "标记所有站内信为已读") + public CommonResult updateAllNotifyMessageRead() { + notifyMessageService.updateAllNotifyMessageRead(getLoginUserId(), UserTypeEnum.ADMIN.getValue()); + return success(Boolean.TRUE); + } + + @GetMapping("/get-unread-list") + @Operation(summary = "获取当前用户的最新站内信列表,默认 10 条") + @Parameter(name = "size", description = "10") + public CommonResult> getUnreadNotifyMessageList( + @RequestParam(name = "size", defaultValue = "10") Integer size) { + List list = notifyMessageService.getUnreadNotifyMessageList( + getLoginUserId(), UserTypeEnum.ADMIN.getValue(), size); + return success(BeanUtils.toBean(list, NotifyMessageRespVO.class)); + } + + @GetMapping("/get-unread-count") + @Operation(summary = "获得当前用户的未读站内信数量") + public CommonResult getUnreadNotifyMessageCount() { + return success(notifyMessageService.getUnreadNotifyMessageCount( + getLoginUserId(), UserTypeEnum.ADMIN.getValue())); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/NotifyTemplateController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/NotifyTemplateController.java new file mode 100644 index 00000000..6837cc10 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/NotifyTemplateController.java @@ -0,0 +1,88 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notify; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template.*; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.service.notify.NotifySendService; +import com.chanko.yunxi.mes.heli.module.system.service.notify.NotifyTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 站内信模版") +@RestController +@RequestMapping("/system/notify-template") +@Validated +public class NotifyTemplateController { + + @Resource + private NotifyTemplateService notifyTemplateService; + + @Resource + private NotifySendService notifySendService; + + @PostMapping("/create") + @Operation(summary = "创建站内信模版") + @PreAuthorize("@ss.hasPermission('system:notify-template:create')") + public CommonResult createNotifyTemplate(@Valid @RequestBody NotifyTemplateSaveReqVO createReqVO) { + return success(notifyTemplateService.createNotifyTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新站内信模版") + @PreAuthorize("@ss.hasPermission('system:notify-template:update')") + public CommonResult updateNotifyTemplate(@Valid @RequestBody NotifyTemplateSaveReqVO updateReqVO) { + notifyTemplateService.updateNotifyTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除站内信模版") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:notify-template:delete')") + public CommonResult deleteNotifyTemplate(@RequestParam("id") Long id) { + notifyTemplateService.deleteNotifyTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得站内信模版") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:notify-template:query')") + public CommonResult getNotifyTemplate(@RequestParam("id") Long id) { + NotifyTemplateDO template = notifyTemplateService.getNotifyTemplate(id); + return success(BeanUtils.toBean(template, NotifyTemplateRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得站内信模版分页") + @PreAuthorize("@ss.hasPermission('system:notify-template:query')") + public CommonResult> getNotifyTemplatePage(@Valid NotifyTemplatePageReqVO pageVO) { + PageResult pageResult = notifyTemplateService.getNotifyTemplatePage(pageVO); + return success(BeanUtils.toBean(pageResult, NotifyTemplateRespVO.class)); + } + + @PostMapping("/send-notify") + @Operation(summary = "发送站内信") + @PreAuthorize("@ss.hasPermission('system:notify-template:send-notify')") + public CommonResult sendNotify(@Valid @RequestBody NotifyTemplateSendReqVO sendReqVO) { + if (UserTypeEnum.MEMBER.getValue().equals(sendReqVO.getUserType())) { + return success(notifySendService.sendSingleNotifyToMember(sendReqVO.getUserId(), + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); + } else { + return success(notifySendService.sendSingleNotifyToAdmin(sendReqVO.getUserId(), + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); + } + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/message/NotifyMessageMyPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/message/NotifyMessageMyPageReqVO.java new file mode 100644 index 00000000..cc301153 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/message/NotifyMessageMyPageReqVO.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 站内信分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyMessageMyPageReqVO extends PageParam { + + @Schema(description = "是否已读", example = "true") + private Boolean readStatus; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/message/NotifyMessagePageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/message/NotifyMessagePageReqVO.java new file mode 100644 index 00000000..c809181f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/message/NotifyMessagePageReqVO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 站内信分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyMessagePageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "25025") + private Long userId; + + @Schema(description = "用户类型", example = "1") + private Integer userType; + + @Schema(description = "模板编码", example = "test_01") + private String templateCode; + + @Schema(description = "模版类型", example = "2") + private Integer templateType; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/message/NotifyMessageRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/message/NotifyMessageRespVO.java new file mode 100644 index 00000000..accedc58 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/message/NotifyMessageRespVO.java @@ -0,0 +1,49 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +@Schema(description = "管理后台 - 站内信 Response VO") +@Data +public class NotifyMessageRespVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25025") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Byte userType; + + @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13013") + private Long templateId; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + private String templateCode; + + @Schema(description = "模版发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String templateNickname; + + @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试内容") + private String templateContent; + + @Schema(description = "模版类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer templateType; + + @Schema(description = "模版参数", requiredMode = Schema.RequiredMode.REQUIRED) + private Map templateParams; + + @Schema(description = "是否已读", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean readStatus; + + @Schema(description = "阅读时间") + private LocalDateTime readTime; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplatePageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplatePageReqVO.java new file mode 100644 index 00000000..cd8dc19b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplatePageReqVO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 站内信模版分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyTemplatePageReqVO extends PageParam { + + @Schema(description = "模版编码", example = "test_01") + private String code; + + @Schema(description = "模版名称", example = "我是名称") + private String name; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java new file mode 100644 index 00000000..7e74830d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template; + +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 NotifyTemplateRespVO { + + @Schema(description = "ID", 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 = "SEND_TEST") + private String code; + + @Schema(description = "模版类型,对应 system_notify_template_type 字典", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + private String nickname; + + @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是模版内容") + private String content; + + @Schema(description = "参数数组", example = "name,code") + private List params; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "备注", example = "我是备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplateSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplateSaveReqVO.java new file mode 100644 index 00000000..92c6f0df --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplateSaveReqVO.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 站内信模版创建/修改 Request VO") +@Data +public class NotifyTemplateSaveReqVO { + + @Schema(description = "ID", example = "1024") + private Long id; + + @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试模版") + @NotEmpty(message = "模版名称不能为空") + private String name; + + @Schema(description = "模版编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "SEND_TEST") + @NotNull(message = "模版编码不能为空") + private String code; + + @Schema(description = "模版类型,对应 system_notify_template_type 字典", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模版类型不能为空") + private Integer type; + + @Schema(description = "发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + @NotEmpty(message = "发送人名称不能为空") + private String nickname; + + @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是模版内容") + @NotEmpty(message = "模版内容不能为空") + private String content; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + + @Schema(description = "备注", example = "我是备注") + private String remark; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplateSendReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplateSendReqVO.java new file mode 100644 index 00000000..327daa2f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/notify/vo/template/NotifyTemplateSendReqVO.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 站内信模板的发送 Request VO") +@Data +public class NotifyTemplateSendReqVO { + + @Schema(description = "用户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "01") + @NotNull(message = "用户id不能为空") + private Long userId; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "01") + @NotEmpty(message = "模板编码不能为空") + private String templateCode; + + @Schema(description = "模板参数") + private Map templateParams; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2ClientController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2ClientController.http new file mode 100644 index 00000000..dcf60a6c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2ClientController.http @@ -0,0 +1,23 @@ +### 请求 /login 接口 => 成功 +POST {{baseUrl}}/system/oauth2-client/create +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "id": "1", + "secret": "admin123", + "name": "芋道源码", + "logo": "https://www.iocoder.cn/images/favicon.ico", + "description": "我是描述", + "status": 0, + "accessTokenValiditySeconds": 180, + "refreshTokenValiditySeconds": 8640, + "redirectUris": ["https://www.iocoder.cn"], + "autoApprove": true, + "authorizedGrantTypes": ["password"], + "scopes": ["user_info"], + "authorities": ["system:user:query"], + "resource_ids": ["1024"], + "additionalInformation": "{}" +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2ClientController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2ClientController.java new file mode 100644 index 00000000..98c2735e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2ClientController.java @@ -0,0 +1,73 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client.OAuth2ClientRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client.OAuth2ClientSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.chanko.yunxi.mes.heli.module.system.service.oauth2.OAuth2ClientService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - OAuth2 客户端") +@RestController +@RequestMapping("/system/oauth2-client") +@Validated +public class OAuth2ClientController { + + @Resource + private OAuth2ClientService oAuth2ClientService; + + @PostMapping("/create") + @Operation(summary = "创建 OAuth2 客户端") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:create')") + public CommonResult createOAuth2Client(@Valid @RequestBody OAuth2ClientSaveReqVO createReqVO) { + return success(oAuth2ClientService.createOAuth2Client(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新 OAuth2 客户端") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:update')") + public CommonResult updateOAuth2Client(@Valid @RequestBody OAuth2ClientSaveReqVO updateReqVO) { + oAuth2ClientService.updateOAuth2Client(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除 OAuth2 客户端") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:oauth2-client:delete')") + public CommonResult deleteOAuth2Client(@RequestParam("id") Long id) { + oAuth2ClientService.deleteOAuth2Client(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得 OAuth2 客户端") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") + public CommonResult getOAuth2Client(@RequestParam("id") Long id) { + OAuth2ClientDO client = oAuth2ClientService.getOAuth2Client(id); + return success(BeanUtils.toBean(client, OAuth2ClientRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得 OAuth2 客户端分页") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") + public CommonResult> getOAuth2ClientPage(@Valid OAuth2ClientPageReqVO pageVO) { + PageResult pageResult = oAuth2ClientService.getOAuth2ClientPage(pageVO); + return success(BeanUtils.toBean(pageResult, OAuth2ClientRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2OpenController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2OpenController.http new file mode 100644 index 00000000..725a5d4f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2OpenController.http @@ -0,0 +1,54 @@ +### 请求 /system/oauth2/authorize 接口 => 成功 +GET {{baseUrl}}/system/oauth2/authorize?clientId=default +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /system/oauth2/authorize + token 接口 => 成功 +POST {{baseUrl}}/system/oauth2/authorize +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true + +### 请求 /system/oauth2/authorize + code 接口 => 成功 +POST {{baseUrl}}/system/oauth2/authorize +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=false + +### 请求 /system/oauth2/token + code 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07a174588a97157eabef2f93a + +### 请求 /system/oauth2/token + password 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=password&username=admin&password=admin123&scope=user.read + +### 请求 /system/oauth2/token + refresh_token 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=refresh_token&refresh_token=00895465d6994f72a9d926ceeed0f588 + +### 请求 /system/oauth2/token + DELETE 接口 => 成功 +DELETE {{baseUrl}}/system/oauth2/token?token=ca8a188f464441d6949c51493a2b7596 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +### 请求 /system/oauth2/check-token 接口 => 成功 +POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2OpenController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2OpenController.java new file mode 100644 index 00000000..fde7d92f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2OpenController.java @@ -0,0 +1,302 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.http.HttpUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; +import com.chanko.yunxi.mes.heli.module.system.convert.oauth2.OAuth2OpenConvert; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.chanko.yunxi.mes.heli.module.system.enums.oauth2.OAuth2GrantTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.service.oauth2.OAuth2ApproveService; +import com.chanko.yunxi.mes.heli.module.system.service.oauth2.OAuth2ClientService; +import com.chanko.yunxi.mes.heli.module.system.service.oauth2.OAuth2GrantService; +import com.chanko.yunxi.mes.heli.module.system.service.oauth2.OAuth2TokenService; +import com.chanko.yunxi.mes.heli.module.system.util.oauth2.OAuth2Utils; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertList; +import static com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 提供给外部应用调用为主 + * + * 一般来说,管理后台的 /system-api/* 是不直接提供给外部应用使用,主要是外部应用能够访问的数据与接口是有限的,而管理后台的 RBAC 无法很好的控制。 + * 参考大量的开放平台,都是独立的一套 OpenAPI,对应到【本系统】就是在 Controller 下新建 open 包,实现 /open-api/* 接口,然后通过 scope 进行控制。 + * 另外,一个公司如果有多个管理后台,它们 client_id 产生的 access token 相互之间是无法互通的,即无法访问它们系统的 API 接口,直到两个 client_id 产生信任授权。 + * + * 考虑到【本系统】暂时不想做的过于复杂,默认只有获取到 access token 之后,可以访问【本系统】管理后台的 /system-api/* 所有接口,除非手动添加 scope 控制。 + * scope 的使用示例,可见 {@link OAuth2UserController} 类 + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - OAuth2.0 授权") +@RestController +@RequestMapping("/system/oauth2") +@Validated +@Slf4j +public class OAuth2OpenController { + + @Resource + private OAuth2GrantService oauth2GrantService; + @Resource + private OAuth2ClientService oauth2ClientService; + @Resource + private OAuth2ApproveService oauth2ApproveService; + @Resource + private OAuth2TokenService oauth2TokenService; + + /** + * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法 + * + * 授权码 authorization_code 模式时:code + redirectUri + state 参数 + * 密码 password 模式时:username + password + scope 参数 + * 刷新 refresh_token 模式时:refreshToken 参数 + * 客户端 client_credentials 模式:scope 参数 + * 简化 implicit 模式时:不支持 + * + * 注意,默认需要传递 client_id + client_secret 参数 + */ + @PostMapping("/token") + @PermitAll + @Operation(summary = "获得访问令牌", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") + @Parameters({ + @Parameter(name = "grant_type", required = true, description = "授权类型", example = "code"), + @Parameter(name = "code", description = "授权范围", example = "userinfo.read"), + @Parameter(name = "redirect_uri", description = "重定向 URI", example = "https://www.iocoder.cn"), + @Parameter(name = "state", description = "状态", example = "1"), + @Parameter(name = "username", example = "tudou"), + @Parameter(name = "password", example = "cai"), // 多个使用空格分隔 + @Parameter(name = "scope", example = "user_info"), + @Parameter(name = "refresh_token", example = "123424233"), + }) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult postAccessToken(HttpServletRequest request, + @RequestParam("grant_type") String grantType, + @RequestParam(value = "code", required = false) String code, // 授权码模式 + @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式 + @RequestParam(value = "state", required = false) String state, // 授权码模式 + @RequestParam(value = "username", required = false) String username, // 密码模式 + @RequestParam(value = "password", required = false) String password, // 密码模式 + @RequestParam(value = "scope", required = false) String scope, // 密码模式 + @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式 + List scopes = OAuth2Utils.buildScopes(scope); + // 1.1 校验授权类型 + OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); + if (grantTypeEnum == null) { + throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType)); + } + if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) { + throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式"); + } + + // 1.2 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + grantType, scopes, redirectUri); + + // 2. 根据授权模式,获取访问令牌 + OAuth2AccessTokenDO accessTokenDO; + switch (grantTypeEnum) { + case AUTHORIZATION_CODE: + accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state); + break; + case PASSWORD: + accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes); + break; + case CLIENT_CREDENTIALS: + accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes); + break; + case REFRESH_TOKEN: + accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId()); + break; + default: + throw new IllegalArgumentException("未知授权类型:" + grantType); + } + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO)); + } + + @DeleteMapping("/token") + @PermitAll + @Operation(summary = "删除访问令牌") + @Parameter(name = "token", required = true, description = "访问令牌", example = "biu") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult revokeToken(HttpServletRequest request, + @RequestParam("token") String token) { + // 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + null, null, null); + + // 删除访问令牌 + return success(oauth2GrantService.revokeToken(client.getClientId(), token)); + } + + /** + * 对应 Spring Security OAuth 的 CheckTokenEndpoint 类的 checkToken 方法 + */ + @PostMapping("/check-token") + @PermitAll + @Operation(summary = "校验访问令牌") + @Parameter(name = "token", required = true, description = "访问令牌", example = "biu") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult checkToken(HttpServletRequest request, + @RequestParam("token") String token) { + // 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + null, null, null); + + // 校验令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.checkAccessToken(token); + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + return success(OAuth2OpenConvert.INSTANCE.convert2(accessTokenDO)); + } + + /** + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 authorize 方法 + */ + @GetMapping("/authorize") + @Operation(summary = "获得授权信息", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") + @Parameter(name = "clientId", required = true, description = "客户端编号", example = "tudou") + public CommonResult authorize(@RequestParam("clientId") String clientId) { + // 0. 校验用户已经登录。通过 Spring Security 实现 + + // 1. 获得 Client 客户端的信息 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId); + // 2. 获得用户已经授权的信息 + List approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId); + // 拼接返回 + return success(OAuth2OpenConvert.INSTANCE.convert(client, approves)); + } + + /** + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法 + * + * 场景一:【自动授权 autoApprove = true】 + * 刚进入 sso.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权 + * 场景二:【手动授权 autoApprove = false】 + * 在 sso.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false + * + * 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理 + */ + @PostMapping("/authorize") + @Operation(summary = "申请授权", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【提交】调用") + @Parameters({ + @Parameter(name = "response_type", required = true, description = "响应类型", example = "code"), + @Parameter(name = "client_id", required = true, description = "客户端编号", example = "tudou"), + @Parameter(name = "scope", description = "授权范围", example = "userinfo.read"), // 使用 Map 格式,Spring MVC 暂时不支持这么接收参数 + @Parameter(name = "redirect_uri", required = true, description = "重定向 URI", example = "https://www.iocoder.cn"), + @Parameter(name = "auto_approve", required = true, description = "用户是否接受", example = "true"), + @Parameter(name = "state", example = "1") + }) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult approveOrDeny(@RequestParam("response_type") String responseType, + @RequestParam("client_id") String clientId, + @RequestParam(value = "scope", required = false) String scope, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam(value = "auto_approve") Boolean autoApprove, + @RequestParam(value = "state", required = false) String state) { + @SuppressWarnings("unchecked") + Map scopes = JsonUtils.parseObject(scope, Map.class); + scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap()); + // 0. 校验用户已经登录。通过 Spring Security 实现 + + // 1.1 校验 responseType 是否满足 code 或者 token 值 + OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); + // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null, + grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); + + // 2.1 假设 approved 为 null,说明是场景一 + if (Boolean.TRUE.equals(autoApprove)) { + // 如果无法自动授权通过,则返回空 url,前端不进行跳转 + if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), getUserType(), clientId, scopes.keySet())) { + return success(null); + } + } else { // 2.2 假设 approved 非 null,说明是场景二 + // 如果计算后不通过,则跳转一个错误链接 + if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), getUserType(), clientId, scopes)) { + return success(OAuth2Utils.buildUnsuccessfulRedirect(redirectUri, responseType, state, + "access_denied", "User denied access")); + } + } + + // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 + List approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue); + if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { + return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); + } + // 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向 + return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); + } + + private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) { + if (StrUtil.equals(responseType, "code")) { + return OAuth2GrantTypeEnum.AUTHORIZATION_CODE; + } + if (StrUtil.equalsAny(responseType, "token")) { + return OAuth2GrantTypeEnum.IMPLICIT; + } + throw exception0(BAD_REQUEST.getCode(), "response_type 参数值只允许 code 和 token"); + } + + private String getImplicitGrantRedirect(Long userId, OAuth2ClientDO client, + List scopes, String redirectUri, String state) { + // 1. 创建 access token 访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2GrantService.grantImplicit(userId, getUserType(), client.getClientId(), scopes); + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + // 2. 拼接重定向的 URL + // noinspection unchecked + return OAuth2Utils.buildImplicitRedirectUri(redirectUri, accessTokenDO.getAccessToken(), state, accessTokenDO.getExpiresTime(), + scopes, JsonUtils.parseObject(client.getAdditionalInformation(), Map.class)); + } + + private String getAuthorizationCodeRedirect(Long userId, OAuth2ClientDO client, + List scopes, String redirectUri, String state) { + // 1. 创建 code 授权码 + String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId, getUserType(), client.getClientId(), scopes, + redirectUri, state); + // 2. 拼接重定向的 URL + return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state); + } + + private Integer getUserType() { + return UserTypeEnum.ADMIN.getValue(); + } + + private String[] obtainBasicAuthorization(HttpServletRequest request) { + String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request); + if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) { + throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递"); + } + return clientIdAndSecret; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2TokenController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2TokenController.java new file mode 100644 index 00000000..674d424b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2TokenController.java @@ -0,0 +1,50 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.chanko.yunxi.mes.heli.module.system.enums.logger.LoginLogTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.service.auth.AdminAuthService; +import com.chanko.yunxi.mes.heli.module.system.service.oauth2.OAuth2TokenService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - OAuth2.0 令牌") +@RestController +@RequestMapping("/system/oauth2-token") +public class OAuth2TokenController { + + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private AdminAuthService authService; + + @GetMapping("/page") + @Operation(summary = "获得访问令牌分页", description = "只返回有效期内的") + @PreAuthorize("@ss.hasPermission('system:oauth2-token:page')") + public CommonResult> getAccessTokenPage(@Valid OAuth2AccessTokenPageReqVO reqVO) { + PageResult pageResult = oauth2TokenService.getAccessTokenPage(reqVO); + return success(BeanUtils.toBean(pageResult, OAuth2AccessTokenRespVO.class)); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除访问令牌") + @Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou") + @PreAuthorize("@ss.hasPermission('system:oauth2-token:delete')") + public CommonResult deleteAccessToken(@RequestParam("accessToken") String accessToken) { + authService.logout(accessToken, LoginLogTypeEnum.LOGOUT_DELETE.getType()); + return success(true); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2UserController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2UserController.http new file mode 100644 index 00000000..13c8545b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2UserController.http @@ -0,0 +1,14 @@ +### 请求 /system/oauth2/user/get 接口 => 成功 +GET {{baseUrl}}/system/oauth2/user/get +Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d +tenant-id: {{adminTenentId}} + +### 请求 /system/oauth2/user/update 接口 => 成功 +PUT {{baseUrl}}/system/oauth2/user/update +Content-Type: application/json +Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d +tenant-id: {{adminTenentId}} + +{ + "nickname": "芋道源码" +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2UserController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2UserController.java new file mode 100644 index 00000000..e7394654 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/OAuth2UserController.java @@ -0,0 +1,81 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.user.OAuth2UserInfoRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.user.OAuth2UserUpdateReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.PostDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.service.dept.DeptService; +import com.chanko.yunxi.mes.heli.module.system.service.dept.PostService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 提供给外部应用调用为主 + * + * 1. 在 getUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.read')") 注解,声明需要满足 scope = user.read + * 2. 在 updateUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.write')") 注解,声明需要满足 scope = user.write + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - OAuth2.0 用户") +@RestController +@RequestMapping("/system/oauth2/user") +@Validated +@Slf4j +public class OAuth2UserController { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + @Resource + private PostService postService; + + @GetMapping("/get") + @Operation(summary = "获得用户基本信息") + @PreAuthorize("@ss.hasScope('user.read')") // + public CommonResult getUserInfo() { + // 获得用户基本信息 + AdminUserDO user = userService.getUser(getLoginUserId()); + OAuth2UserInfoRespVO resp = BeanUtils.toBean(user, OAuth2UserInfoRespVO.class); + // 获得部门信息 + if (user.getDeptId() != null) { + DeptDO dept = deptService.getDept(user.getDeptId()); + resp.setDept(BeanUtils.toBean(dept, OAuth2UserInfoRespVO.Dept.class)); + } + // 获得岗位信息 + if (CollUtil.isNotEmpty(user.getPostIds())) { + List posts = postService.getPostList(user.getPostIds()); + resp.setPosts(BeanUtils.toBean(posts, OAuth2UserInfoRespVO.Post.class)); + } + return success(resp); + } + + @PutMapping("/update") + @Operation(summary = "更新用户基本信息") + @PreAuthorize("@ss.hasScope('user.write')") + public CommonResult updateUserInfo(@Valid @RequestBody OAuth2UserUpdateReqVO reqVO) { + // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。 + // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做 + userService.updateUserProfile(getLoginUserId(), BeanUtils.toBean(reqVO, UserProfileUpdateReqVO.class)); + return success(true); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java new file mode 100644 index 00000000..8b6d762c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java @@ -0,0 +1,19 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; + +@Schema(description = "管理后台 - OAuth2 客户端分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientPageReqVO extends PageParam { + + @Schema(description = "应用名,模糊匹配", example = "土豆") + private String name; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", example = "1") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java new file mode 100644 index 00000000..0e8e1ee3 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java @@ -0,0 +1,64 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - OAuth2 客户端 Response VO") +@Data +public class OAuth2ClientRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") + private String clientId; + + @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "fan") + private String secret; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + private String name; + + @Schema(description = "应用图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + private String logo; + + @Schema(description = "应用描述", example = "我是一个应用") + private String description; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "访问令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640") + private Integer accessTokenValiditySeconds; + + @Schema(description = "刷新令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640000") + private Integer refreshTokenValiditySeconds; + + @Schema(description = "可重定向的 URI 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn") + private List redirectUris; + + @Schema(description = "授权类型,参见 OAuth2GrantTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "password") + private List authorizedGrantTypes; + + @Schema(description = "授权范围", example = "user_info") + private List scopes; + + @Schema(description = "自动通过的授权范围", example = "user_info") + private List autoApproveScopes; + + @Schema(description = "权限", example = "system:user:query") + private List authorities; + + @Schema(description = "资源", example = "1024") + private List resourceIds; + + @Schema(description = "附加信息", example = "{yunai: true}") + private String additionalInformation; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/client/OAuth2ClientSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/client/OAuth2ClientSaveReqVO.java new file mode 100644 index 00000000..f6176752 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/client/OAuth2ClientSaveReqVO.java @@ -0,0 +1,81 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - OAuth2 客户端创建/修改 Request VO") +@Data +public class OAuth2ClientSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") + @NotNull(message = "客户端编号不能为空") + private String clientId; + + @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "fan") + @NotNull(message = "客户端密钥不能为空") + private String secret; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + @NotNull(message = "应用名不能为空") + private String name; + + @Schema(description = "应用图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + @NotNull(message = "应用图标不能为空") + @URL(message = "应用图标的地址不正确") + private String logo; + + @Schema(description = "应用描述", example = "我是一个应用") + private String description; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "访问令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640") + @NotNull(message = "访问令牌的有效期不能为空") + private Integer accessTokenValiditySeconds; + + @Schema(description = "刷新令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640000") + @NotNull(message = "刷新令牌的有效期不能为空") + private Integer refreshTokenValiditySeconds; + + @Schema(description = "可重定向的 URI 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn") + @NotNull(message = "可重定向的 URI 地址不能为空") + private List<@NotEmpty(message = "重定向的 URI 不能为空") @URL(message = "重定向的 URI 格式不正确") String> redirectUris; + + @Schema(description = "授权类型,参见 OAuth2GrantTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "password") + @NotNull(message = "授权类型不能为空") + private List authorizedGrantTypes; + + @Schema(description = "授权范围", example = "user_info") + private List scopes; + + @Schema(description = "自动通过的授权范围", example = "user_info") + private List autoApproveScopes; + + @Schema(description = "权限", example = "system:user:query") + private List authorities; + + @Schema(description = "资源", example = "1024") + private List resourceIds; + + @Schema(description = "附加信息", example = "{yunai: true}") + private String additionalInformation; + + @AssertTrue(message = "附加信息必须是 JSON 格式") + public boolean isAdditionalInformationJson() { + return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java new file mode 100644 index 00000000..3ebcf80b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.open; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 【开放接口】访问令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenAccessTokenRespVO { + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") + @JsonProperty("access_token") + private String accessToken; + + @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") + @JsonProperty("refresh_token") + private String refreshToken; + + @Schema(description = "令牌类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "bearer") + @JsonProperty("token_type") + private String tokenType; + + @Schema(description = "过期时间,单位:秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "42430") + @JsonProperty("expires_in") + private Long expiresIn; + + @Schema(description = "授权范围,如果多个授权范围,使用空格分隔", example = "user_info") + private String scope; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java new file mode 100644 index 00000000..53d35b32 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java @@ -0,0 +1,38 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.open; + +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 授权页的信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenAuthorizeInfoRespVO { + + /** + * 客户端 + */ + private Client client; + + @Schema(description = "scope 的选中信息,使用 List 保证有序性,Key 是 scope,Value 为是否选中", requiredMode = Schema.RequiredMode.REQUIRED) + private List> scopes; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Client { + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + private String name; + + @Schema(description = "应用图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + private String logo; + + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java new file mode 100644 index 00000000..8bc090e7 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.open; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 【开放接口】校验令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenCheckTokenRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + @JsonProperty("user_id") + private Long userId; + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @JsonProperty("user_type") + private Integer userType; + @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @JsonProperty("tenant_id") + private Long tenantId; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "car") + @JsonProperty("client_id") + private String clientId; + @Schema(description = "授权范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_info") + private List scopes; + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") + @JsonProperty("access_token") + private String accessToken; + + @Schema(description = "过期时间,时间戳 / 1000,即单位:秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1593092157") + private Long exp; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java new file mode 100644 index 00000000..8135fbcd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java @@ -0,0 +1,22 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.token; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 访问令牌分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2AccessTokenPageReqVO extends PageParam { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer userType; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private String clientId; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java new file mode 100644 index 00000000..3abd485c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.token; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 访问令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2AccessTokenRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") + private String accessToken; + + @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") + private String refreshToken; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer userType; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private String clientId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime expiresTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java new file mode 100644 index 00000000..73f3e2d8 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java @@ -0,0 +1,70 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - OAuth2 获得用户基本信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2UserInfoRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String username; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String nickname; + + @Schema(description = "用户邮箱", example = "mes@iocoder.cn") + private String email; + @Schema(description = "手机号码", example = "15601691300") + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + private Integer sex; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + + /** + * 所在部门 + */ + private Dept dept; + + /** + * 所属岗位数组 + */ + private List posts; + + @Schema(description = "部门") + @Data + public static class Dept { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") + private String name; + + } + + @Schema(description = "岗位") + @Data + public static class Post { + + @Schema(description = "岗位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "开发") + private String name; + + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java new file mode 100644 index 00000000..ad9b17fd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - OAuth2 更新用户基本信息 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2UserUpdateReqVO { + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @Size(max = 30, message = "用户昵称长度不能超过 30 个字符") + private String nickname; + + @Schema(description = "用户邮箱", example = "mes@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @Schema(description = "手机号码", example = "15601691300") + @Length(min = 11, max = 11, message = "手机号长度必须 11 位") + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + private Integer sex; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/MenuController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/MenuController.http new file mode 100644 index 00000000..a90d8b8a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/MenuController.http @@ -0,0 +1,4 @@ +### 请求 /menu/list 接口 => 成功 +GET {{baseUrl}}/system/menu/list +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/MenuController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/MenuController.java new file mode 100644 index 00000000..85e6aff9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/MenuController.java @@ -0,0 +1,87 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu.MenuRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu.MenuSaveVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu.MenuSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO; +import com.chanko.yunxi.mes.heli.module.system.service.permission.MenuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 菜单") +@RestController +@RequestMapping("/system/menu") +@Validated +public class MenuController { + + @Resource + private MenuService menuService; + + @PostMapping("/create") + @Operation(summary = "创建菜单") + @PreAuthorize("@ss.hasPermission('system:menu:create')") + public CommonResult createMenu(@Valid @RequestBody MenuSaveVO createReqVO) { + Long menuId = menuService.createMenu(createReqVO); + return success(menuId); + } + + @PutMapping("/update") + @Operation(summary = "修改菜单") + @PreAuthorize("@ss.hasPermission('system:menu:update')") + public CommonResult updateMenu(@Valid @RequestBody MenuSaveVO updateReqVO) { + menuService.updateMenu(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除菜单") + @Parameter(name = "id", description = "角色编号", required= true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:menu:delete')") + public CommonResult deleteMenu(@RequestParam("id") Long id) { + menuService.deleteMenu(id); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获取菜单列表", description = "用于【菜单管理】界面") + @PreAuthorize("@ss.hasPermission('system:menu:query')") + public CommonResult> getMenuList(MenuListReqVO reqVO) { + List list = menuService.getMenuList(reqVO); + list.sort(Comparator.comparing(MenuDO::getSort)); + return success(BeanUtils.toBean(list, MenuRespVO.class)); + } + + @GetMapping({"/list-all-simple", "simple-list"}) + @Operation(summary = "获取菜单精简信息列表", description = "只包含被开启的菜单,用于【角色分配菜单】功能的选项。" + + "在多租户的场景下,会只返回租户所在套餐有的菜单") + public CommonResult> getSimpleMenuList() { + List list = menuService.getMenuListByTenant( + new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus())); + list.sort(Comparator.comparing(MenuDO::getSort)); + return success(BeanUtils.toBean(list, MenuSimpleRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获取菜单信息") + @PreAuthorize("@ss.hasPermission('system:menu:query')") + public CommonResult getMenu(Long id) { + MenuDO menu = menuService.getMenu(id); + return success(BeanUtils.toBean(menu, MenuRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/PermissionController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/PermissionController.java new file mode 100644 index 00000000..7d4f891a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/PermissionController.java @@ -0,0 +1,82 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleDataScopeReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleMenuReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.permission.PermissionAssignUserRoleReqVO; +import com.chanko.yunxi.mes.heli.module.system.service.permission.PermissionService; +import com.chanko.yunxi.mes.heli.module.system.service.tenant.TenantService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Set; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +/** + * 权限 Controller,提供赋予用户、角色的权限的 API 接口 + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - 权限") +@RestController +@RequestMapping("/system/permission") +public class PermissionController { + + @Resource + private PermissionService permissionService; + @Resource + private TenantService tenantService; + + @Operation(summary = "获得角色拥有的菜单编号") + @Parameter(name = "roleId", description = "角色编号", required = true) + @GetMapping("/list-role-menus") + @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')") + public CommonResult> getRoleMenuList(Long roleId) { + return success(permissionService.getRoleMenuListByRoleId(roleId)); + } + + @PostMapping("/assign-role-menu") + @Operation(summary = "赋予角色菜单") + @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')") + public CommonResult assignRoleMenu(@Validated @RequestBody PermissionAssignRoleMenuReqVO reqVO) { + // 开启多租户的情况下,需要过滤掉未开通的菜单 + tenantService.handleTenantMenu(menuIds -> reqVO.getMenuIds().removeIf(menuId -> !CollUtil.contains(menuIds, menuId))); + + // 执行菜单的分配 + permissionService.assignRoleMenu(reqVO.getRoleId(), reqVO.getMenuIds()); + return success(true); + } + + @PostMapping("/assign-role-data-scope") + @Operation(summary = "赋予角色数据权限") + @PreAuthorize("@ss.hasPermission('system:permission:assign-role-data-scope')") + public CommonResult assignRoleDataScope(@Valid @RequestBody PermissionAssignRoleDataScopeReqVO reqVO) { + permissionService.assignRoleDataScope(reqVO.getRoleId(), reqVO.getDataScope(), reqVO.getDataScopeDeptIds()); + return success(true); + } + + @Operation(summary = "获得管理员拥有的角色编号列表") + @Parameter(name = "userId", description = "用户编号", required = true) + @GetMapping("/list-user-roles") + @PreAuthorize("@ss.hasPermission('system:permission:assign-user-role')") + public CommonResult> listAdminRoles(@RequestParam("userId") Long userId) { + return success(permissionService.getUserRoleIdListByUserId(userId)); + } + + @Operation(summary = "赋予用户角色") + @PostMapping("/assign-user-role") + @PreAuthorize("@ss.hasPermission('system:permission:assign-user-role')") + public CommonResult assignUserRole(@Validated @RequestBody PermissionAssignUserRoleReqVO reqVO) { + permissionService.assignUserRole(reqVO.getUserId(), reqVO.getRoleIds()); + return success(true); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/RoleController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/RoleController.http new file mode 100644 index 00000000..c68b86b7 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/RoleController.http @@ -0,0 +1,42 @@ +### /role/create 成功 +POST {{baseUrl}}/system/role/create +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "name": "测试角色", + "code": "test", + "sort": 0 +} + +### /role/update 成功 +POST {{baseUrl}}/system/role/update +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "id": 100, + "name": "测试角色", + "code": "test", + "sort": 10 +} +### /resource/delete 成功 +POST {{baseUrl}}/system/role/delete +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +roleId=14 + +### /role/get 成功 +GET {{baseUrl}}/system/role/get?id=100 +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### /role/page 成功 +GET {{baseUrl}}/system/role/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/RoleController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/RoleController.java new file mode 100644 index 00000000..660254a4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/RoleController.java @@ -0,0 +1,108 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role.*; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; +import com.chanko.yunxi.mes.heli.module.system.service.permission.RoleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; +import static java.util.Collections.singleton; + +@Tag(name = "管理后台 - 角色") +@RestController +@RequestMapping("/system/role") +@Validated +public class RoleController { + + @Resource + private RoleService roleService; + + @PostMapping("/create") + @Operation(summary = "创建角色") + @PreAuthorize("@ss.hasPermission('system:role:create')") + public CommonResult createRole(@Valid @RequestBody RoleSaveReqVO createReqVO) { + return success(roleService.createRole(createReqVO, null)); + } + + @PutMapping("/update") + @Operation(summary = "修改角色") + @PreAuthorize("@ss.hasPermission('system:role:update')") + public CommonResult updateRole(@Valid @RequestBody RoleSaveReqVO updateReqVO) { + roleService.updateRole(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "修改角色状态") + @PreAuthorize("@ss.hasPermission('system:role:update')") + public CommonResult updateRoleStatus(@Valid @RequestBody RoleUpdateStatusReqVO reqVO) { + roleService.updateRoleStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除角色") + @Parameter(name = "id", description = "角色编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:role:delete')") + public CommonResult deleteRole(@RequestParam("id") Long id) { + roleService.deleteRole(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得角色信息") + @PreAuthorize("@ss.hasPermission('system:role:query')") + public CommonResult getRole(@RequestParam("id") Long id) { + RoleDO role = roleService.getRole(id); + return success(BeanUtils.toBean(role, RoleRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得角色分页") + @PreAuthorize("@ss.hasPermission('system:role:query')") + public CommonResult> getRolePage(RolePageReqVO pageReqVO) { + PageResult pageResult = roleService.getRolePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, RoleRespVO.class)); + } + + @GetMapping({"/list-all-simple", "/simple-list"}) + @Operation(summary = "获取角色精简信息列表", description = "只包含被开启的角色,主要用于前端的下拉选项") + public CommonResult> getSimpleRoleList() { + List list = roleService.getRoleListByStatus(singleton(CommonStatusEnum.ENABLE.getStatus())); + list.sort(Comparator.comparing(RoleDO::getSort)); + return success(BeanUtils.toBean(list, RoleSimpleRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出角色 Excel") + @OperateLog(type = EXPORT) + @PreAuthorize("@ss.hasPermission('system:role:export')") + public void export(HttpServletResponse response, @Validated RolePageReqVO exportReqVO) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = roleService.getRolePage(exportReqVO).getList(); + // 输出 + ExcelUtils.write(response, "角色数据.xls", "数据", RoleRespVO.class, + BeanUtils.toBean(list, RoleRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java new file mode 100644 index 00000000..e3510b5d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 菜单列表 Request VO") +@Data +public class MenuListReqVO { + + @Schema(description = "菜单名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuRespVO.java new file mode 100644 index 00000000..db8f86ff --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuRespVO.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 菜单信息 Response VO") +@Data +public class MenuRespVO { + + @Schema(description = "菜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotBlank(message = "菜单名称不能为空") + @Size(max = 50, message = "菜单名称长度不能超过50个字符") + private String name; + + @Schema(description = "权限标识,仅菜单类型为按钮时,才需要传递", example = "sys:menu:add") + @Size(max = 100) + private String permission; + + @Schema(description = "类型,参见 MenuTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "菜单类型不能为空") + private Integer type; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "父菜单 ID 不能为空") + private Long parentId; + + @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") + @Size(max = 200, message = "路由地址不能超过200个字符") + private String path; + + @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") + private String icon; + + @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") + @Size(max = 200, message = "组件路径不能超过255个字符") + private String component; + + @Schema(description = "组件名", example = "SystemUser") + private String componentName; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "是否可见", example = "false") + private Boolean visible; + + @Schema(description = "是否缓存", example = "false") + private Boolean keepAlive; + + @Schema(description = "是否总是显示", example = "false") + private Boolean alwaysShow; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java new file mode 100644 index 00000000..4f48449a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuSaveVO.java @@ -0,0 +1,65 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 菜单创建/修改 Request VO") +@Data +public class MenuSaveVO { + + @Schema(description = "菜单编号", example = "1024") + private Long id; + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotBlank(message = "菜单名称不能为空") + @Size(max = 50, message = "菜单名称长度不能超过50个字符") + private String name; + + @Schema(description = "权限标识,仅菜单类型为按钮时,才需要传递", example = "sys:menu:add") + @Size(max = 100) + private String permission; + + @Schema(description = "类型,参见 MenuTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "菜单类型不能为空") + private Integer type; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "父菜单 ID 不能为空") + private Long parentId; + + @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") + @Size(max = 200, message = "路由地址不能超过200个字符") + private String path; + + @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") + private String icon; + + @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") + @Size(max = 200, message = "组件路径不能超过255个字符") + private String component; + + @Schema(description = "组件名", example = "SystemUser") + private String componentName; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "是否可见", example = "false") + private Boolean visible; + + @Schema(description = "是否缓存", example = "false") + private Boolean keepAlive; + + @Schema(description = "是否总是显示", example = "false") + private Boolean alwaysShow; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java new file mode 100644 index 00000000..d1e5636e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 菜单精简信息 Response VO") +@Data +public class MenuSimpleRespVO { + + @Schema(description = "菜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + + @Schema(description = "类型,参见 MenuTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java new file mode 100644 index 00000000..23a01b5c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.permission; + +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.DataScopeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Set; + +@Schema(description = "管理后台 - 赋予角色数据权限 Request VO") +@Data +public class PermissionAssignRoleDataScopeReqVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "角色编号不能为空") + private Long roleId; + + @Schema(description = "数据范围,参见 DataScopeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数据范围不能为空") + @InEnum(value = DataScopeEnum.class, message = "数据范围必须是 {value}") + private Integer dataScope; + + @Schema(description = "部门编号列表,只有范围类型为 DEPT_CUSTOM 时,该字段才需要", example = "1,3,5") + private Set dataScopeDeptIds = Collections.emptySet(); // 兜底 + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java new file mode 100644 index 00000000..ca902f63 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.permission; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Set; + +@Schema(description = "管理后台 - 赋予角色菜单 Request VO") +@Data +public class PermissionAssignRoleMenuReqVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "角色编号不能为空") + private Long roleId; + + @Schema(description = "菜单编号列表", example = "1,3,5") + private Set menuIds = Collections.emptySet(); // 兜底 + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java new file mode 100644 index 00000000..d8a1f619 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.permission; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Set; + +@Schema(description = "管理后台 - 赋予用户角色 Request VO") +@Data +public class PermissionAssignUserRoleReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "角色编号列表", example = "1,3,5") + private Set roleIds = Collections.emptySet(); // 兜底 + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RolePageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RolePageReqVO.java new file mode 100644 index 00000000..5464eb7e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RolePageReqVO.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 角色分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class RolePageReqVO extends PageParam { + + @Schema(description = "角色名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "角色标识,模糊匹配", example = "mes") + private String code; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @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/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleRespVO.java new file mode 100644 index 00000000..9a58d97c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleRespVO.java @@ -0,0 +1,57 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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 javax.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import java.util.Set; + +@Schema(description = "管理后台 - 角色信息 Response VO") +@Data +@ExcelIgnoreUnannotated +public class RoleRespVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("角色序号") + private Long id; + + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "管理员") + @ExcelProperty("角色名称") + private String name; + + @NotBlank(message = "角色标志不能为空") + @ExcelProperty("角色标志") + private String code; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("角色排序") + private Integer sort; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "角色状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "角色类型,参见 RoleTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "备注", example = "我是一个角色") + private String remark; + + @Schema(description = "数据范围,参见 DataScopeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("数据范围") + private Integer dataScope; + + @Schema(description = "数据范围(指定部门数组)", example = "1") + private Set dataScopeDeptIds; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java new file mode 100644 index 00000000..bbc9fa55 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleSaveReqVO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 角色创建 Request VO") +@Data +public class RoleSaveReqVO { + + @Schema(description = "角色编号", example = "1") + private Long id; + + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "管理员") + @NotBlank(message = "角色名称不能为空") + @Size(max = 30, message = "角色名称长度不能超过30个字符") + private String name; + + @NotBlank(message = "角色标志不能为空") + @Size(max = 100, message = "角色标志长度不能超过100个字符") + @Schema(description = "角色编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ADMIN") + private String code; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "备注", example = "我是一个角色") + private String remark; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java new file mode 100644 index 00000000..494e9ff9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java @@ -0,0 +1,18 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 角色精简信息 Response VO") +@Data +public class RoleSimpleRespVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleUpdateStatusReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleUpdateStatusReqVO.java new file mode 100644 index 00000000..bbef6567 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/permission/vo/role/RoleUpdateStatusReqVO.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 角色更新状态 Request VO") +@Data +public class RoleUpdateStatusReqVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "角色编号不能为空") + private Long id; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/SensitiveWordController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/SensitiveWordController.http new file mode 100644 index 00000000..cd97d2de --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/SensitiveWordController.http @@ -0,0 +1,4 @@ +### 请求 /system/sensitive-word/validate-text 接口 => 成功 +GET {{baseUrl}}/system/sensitive-word/validate-text?text=XXX&tags=短信&tags=蔬菜 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/SensitiveWordController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/SensitiveWordController.java new file mode 100644 index 00000000..dd84ff06 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/SensitiveWordController.java @@ -0,0 +1,108 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo.SensitiveWordRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo.SensitiveWordSaveVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import com.chanko.yunxi.mes.heli.module.system.service.sensitiveword.SensitiveWordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 敏感词") +@RestController +@RequestMapping("/system/sensitive-word") +@Validated +public class SensitiveWordController { + + @Resource + private SensitiveWordService sensitiveWordService; + + @PostMapping("/create") + @Operation(summary = "创建敏感词") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:create')") + public CommonResult createSensitiveWord(@Valid @RequestBody SensitiveWordSaveVO createReqVO) { + return success(sensitiveWordService.createSensitiveWord(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新敏感词") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:update')") + public CommonResult updateSensitiveWord(@Valid @RequestBody SensitiveWordSaveVO updateReqVO) { + sensitiveWordService.updateSensitiveWord(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除敏感词") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:sensitive-word:delete')") + public CommonResult deleteSensitiveWord(@RequestParam("id") Long id) { + sensitiveWordService.deleteSensitiveWord(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得敏感词") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')") + public CommonResult getSensitiveWord(@RequestParam("id") Long id) { + SensitiveWordDO sensitiveWord = sensitiveWordService.getSensitiveWord(id); + return success(BeanUtils.toBean(sensitiveWord, SensitiveWordRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得敏感词分页") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')") + public CommonResult> getSensitiveWordPage(@Valid SensitiveWordPageReqVO pageVO) { + PageResult pageResult = sensitiveWordService.getSensitiveWordPage(pageVO); + return success(BeanUtils.toBean(pageResult, SensitiveWordRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出敏感词 Excel") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:export')") + @OperateLog(type = EXPORT) + public void exportSensitiveWordExcel(@Valid SensitiveWordPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = sensitiveWordService.getSensitiveWordPage(exportReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "敏感词.xls", "数据", SensitiveWordRespVO.class, + BeanUtils.toBean(list, SensitiveWordRespVO.class)); + } + + @GetMapping("/get-tags") + @Operation(summary = "获取所有敏感词的标签数组") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')") + public CommonResult> getSensitiveWordTagSet() { + return success(sensitiveWordService.getSensitiveWordTagSet()); + } + + @GetMapping("/validate-text") + @Operation(summary = "获得文本所包含的不合法的敏感词数组") + public CommonResult> validateText(@RequestParam("text") String text, + @RequestParam(value = "tags", required = false) List tags) { + return success(sensitiveWordService.validateText(text, tags)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java new file mode 100644 index 00000000..9d1a1a89 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 敏感词分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SensitiveWordPageReqVO extends PageParam { + + @Schema(description = "敏感词", example = "敏感词") + private String name; + + @Schema(description = "标签", example = "短信,评论") + private String tag; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java new file mode 100644 index 00000000..526602ae --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java @@ -0,0 +1,45 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.JsonConvert; +import com.chanko.yunxi.mes.heli.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 敏感词 Response VO") +@Data +public class SensitiveWordRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词") + @ExcelProperty("敏感词") + private String name; + + @Schema(description = "标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "短信,评论") + @ExcelProperty(value = "标签", converter = JsonConvert.class) + private List tags; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "描述", example = "污言秽语") + @ExcelProperty("描述") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/vo/SensitiveWordSaveVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/vo/SensitiveWordSaveVO.java new file mode 100644 index 00000000..d7dfd348 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sensitiveword/vo/SensitiveWordSaveVO.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 敏感词创建/修改 Request VO") +@Data +public class SensitiveWordSaveVO { + + @Schema(description = "编号", example = "1") + private Long id; + + @Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词") + @NotNull(message = "敏感词不能为空") + private String name; + + @Schema(description = "标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "短信,评论") + @NotNull(message = "标签不能为空") + private List tags; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "描述", example = "污言秽语") + private String description; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsCallbackController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsCallbackController.java new file mode 100644 index 00000000..716621b4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsCallbackController.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.framework.sms.core.enums.SmsChannelEnum; +import com.chanko.yunxi.mes.heli.module.system.service.sms.SmsSendService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 短信回调") +@RestController +@RequestMapping("/system/sms/callback") +public class SmsCallbackController { + + @Resource + private SmsSendService smsSendService; + + @PostMapping("/aliyun") + @PermitAll + @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档") + @OperateLog(enable = false) + public CommonResult receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { + String text = ServletUtils.getBody(request); + smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); + return success(true); + } + + @PostMapping("/tencent") + @PermitAll + @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档") + @OperateLog(enable = false) + public CommonResult receiveTencentSmsStatus(HttpServletRequest request) throws Throwable { + String text = ServletUtils.getBody(request); + smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text); + return success(true); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsChannelController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsChannelController.java new file mode 100644 index 00000000..17770bd7 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsChannelController.java @@ -0,0 +1,82 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel.SmsChannelRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel.SmsChannelSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsChannelDO; +import com.chanko.yunxi.mes.heli.module.system.service.sms.SmsChannelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 短信渠道") +@RestController +@RequestMapping("system/sms-channel") +public class SmsChannelController { + + @Resource + private SmsChannelService smsChannelService; + + @PostMapping("/create") + @Operation(summary = "创建短信渠道") + @PreAuthorize("@ss.hasPermission('system:sms-channel:create')") + public CommonResult createSmsChannel(@Valid @RequestBody SmsChannelSaveReqVO createReqVO) { + return success(smsChannelService.createSmsChannel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新短信渠道") + @PreAuthorize("@ss.hasPermission('system:sms-channel:update')") + public CommonResult updateSmsChannel(@Valid @RequestBody SmsChannelSaveReqVO updateReqVO) { + smsChannelService.updateSmsChannel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除短信渠道") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:sms-channel:delete')") + public CommonResult deleteSmsChannel(@RequestParam("id") Long id) { + smsChannelService.deleteSmsChannel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得短信渠道") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") + public CommonResult getSmsChannel(@RequestParam("id") Long id) { + SmsChannelDO channel = smsChannelService.getSmsChannel(id); + return success(BeanUtils.toBean(channel, SmsChannelRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得短信渠道分页") + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") + public CommonResult> getSmsChannelPage(@Valid SmsChannelPageReqVO pageVO) { + PageResult pageResult = smsChannelService.getSmsChannelPage(pageVO); + return success(BeanUtils.toBean(pageResult, SmsChannelRespVO.class)); + } + + @GetMapping({"/list-all-simple", "/simple-list"}) + @Operation(summary = "获得短信渠道精简列表", description = "包含被禁用的短信渠道") + public CommonResult> getSimpleSmsChannelList() { + List list = smsChannelService.getSmsChannelList(); + list.sort(Comparator.comparing(SmsChannelDO::getId)); + return success(BeanUtils.toBean(list, SmsChannelSimpleRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsLogController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsLogController.java new file mode 100644 index 00000000..7b4cb165 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsLogController.java @@ -0,0 +1,60 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.log.SmsLogRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsLogDO; +import com.chanko.yunxi.mes.heli.module.system.service.sms.SmsLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 短信日志") +@RestController +@RequestMapping("/system/sms-log") +@Validated +public class SmsLogController { + + @Resource + private SmsLogService smsLogService; + + @GetMapping("/page") + @Operation(summary = "获得短信日志分页") + @PreAuthorize("@ss.hasPermission('system:sms-log:query')") + public CommonResult> getSmsLogPage(@Valid SmsLogPageReqVO pageReqVO) { + PageResult pageResult = smsLogService.getSmsLogPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, SmsLogRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出短信日志 Excel") + @PreAuthorize("@ss.hasPermission('system:sms-log:export')") + @OperateLog(type = EXPORT) + public void exportSmsLogExcel(@Valid SmsLogPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = smsLogService.getSmsLogPage(exportReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "短信日志.xls", "数据", SmsLogRespVO.class, + BeanUtils.toBean(list, SmsLogRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsTemplateController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsTemplateController.http new file mode 100644 index 00000000..ee24e928 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsTemplateController.http @@ -0,0 +1,14 @@ +### 请求 /system/sms-template/send-sms 接口 => 成功 +POST {{baseUrl}}/system/sms-template/send-sms +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "templateCode": "test_01", + "mobile": "15601691390", + "templateParams": { + "operation": "value01", + "code": "value02" + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsTemplateController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsTemplateController.java new file mode 100644 index 00000000..81feff34 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/SmsTemplateController.java @@ -0,0 +1,100 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template.*; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.service.sms.SmsTemplateService; +import com.chanko.yunxi.mes.heli.module.system.service.sms.SmsSendService; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 短信模板") +@RestController +@RequestMapping("/system/sms-template") +public class SmsTemplateController { + + @Resource + private SmsTemplateService smsTemplateService; + @Resource + private SmsSendService smsSendService; + + @PostMapping("/create") + @Operation(summary = "创建短信模板") + @PreAuthorize("@ss.hasPermission('system:sms-template:create')") + public CommonResult createSmsTemplate(@Valid @RequestBody SmsTemplateSaveReqVO createReqVO) { + return success(smsTemplateService.createSmsTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新短信模板") + @PreAuthorize("@ss.hasPermission('system:sms-template:update')") + public CommonResult updateSmsTemplate(@Valid @RequestBody SmsTemplateSaveReqVO updateReqVO) { + smsTemplateService.updateSmsTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除短信模板") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:sms-template:delete')") + public CommonResult deleteSmsTemplate(@RequestParam("id") Long id) { + smsTemplateService.deleteSmsTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得短信模板") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:sms-template:query')") + public CommonResult getSmsTemplate(@RequestParam("id") Long id) { + SmsTemplateDO template = smsTemplateService.getSmsTemplate(id); + return success(BeanUtils.toBean(template, SmsTemplateRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得短信模板分页") + @PreAuthorize("@ss.hasPermission('system:sms-template:query')") + public CommonResult> getSmsTemplatePage(@Valid SmsTemplatePageReqVO pageVO) { + PageResult pageResult = smsTemplateService.getSmsTemplatePage(pageVO); + return success(BeanUtils.toBean(pageResult, SmsTemplateRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出短信模板 Excel") + @PreAuthorize("@ss.hasPermission('system:sms-template:export')") + @OperateLog(type = EXPORT) + public void exportSmsTemplateExcel(@Valid SmsTemplatePageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = smsTemplateService.getSmsTemplatePage(exportReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "短信模板.xls", "数据", SmsTemplateRespVO.class, + BeanUtils.toBean(list, SmsTemplateRespVO.class)); + } + + @PostMapping("/send-sms") + @Operation(summary = "发送短信") + @PreAuthorize("@ss.hasPermission('system:sms-template:send-sms')") + public CommonResult sendSms(@Valid @RequestBody SmsTemplateSendReqVO sendReqVO) { + return success(smsSendService.sendSingleSmsToAdmin(sendReqVO.getMobile(), null, + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java new file mode 100644 index 00000000..1d425dda --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 短信渠道分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelPageReqVO extends PageParam { + + @Schema(description = "任务状态", example = "1") + private Integer status; + + @Schema(description = "短信签名,模糊匹配", example = "芋道源码") + private String signature; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java new file mode 100644 index 00000000..8a9f366b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java @@ -0,0 +1,45 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 短信渠道 Response VO") +@Data +public class SmsChannelRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + @NotNull(message = "短信签名不能为空") + private String signature; + + @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") + private String code; + + @Schema(description = "启用状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "启用状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "好吃!") + private String remark; + + @Schema(description = "短信 API 的账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @NotNull(message = "短信 API 的账号不能为空") + private String apiKey; + + @Schema(description = "短信 API 的密钥", example = "yuanma") + private String apiSecret; + + @Schema(description = "短信发送回调 URL", example = "https://www.iocoder.cn") + @URL(message = "回调 URL 格式不正确") + private String callbackUrl; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelSaveReqVO.java new file mode 100644 index 00000000..7b4136d5 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelSaveReqVO.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 短信渠道创建/修改 Request VO") +@Data +public class SmsChannelSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + @NotNull(message = "短信签名不能为空") + private String signature; + + @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") + @NotNull(message = "渠道编码不能为空") + private String code; + + @Schema(description = "启用状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "启用状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "好吃!") + private String remark; + + @Schema(description = "短信 API 的账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @NotNull(message = "短信 API 的账号不能为空") + private String apiKey; + + @Schema(description = "短信 API 的密钥", example = "yuanma") + private String apiSecret; + + @Schema(description = "短信发送回调 URL", example = "http://www.iocoder.cn") + @URL(message = "回调 URL 格式不正确") + private String callbackUrl; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java new file mode 100644 index 00000000..416b2841 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java @@ -0,0 +1,19 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 短信渠道精简 Response VO") +@Data +public class SmsChannelSimpleRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String signature; + + @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") + private String code; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java new file mode 100644 index 00000000..f0dce2ce --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java @@ -0,0 +1,43 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.log; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 短信日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsLogPageReqVO extends PageParam { + + @Schema(description = "短信渠道编号", example = "10") + private Long channelId; + + @Schema(description = "模板编号", example = "20") + private Long templateId; + + @Schema(description = "手机号", example = "15601691300") + private String mobile; + + @Schema(description = "发送状态,参见 SmsSendStatusEnum 枚举类", example = "1") + private Integer sendStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "发送时间") + private LocalDateTime[] sendTime; + + @Schema(description = "接收状态,参见 SmsReceiveStatusEnum 枚举类", example = "0") + private Integer receiveStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "接收时间") + private LocalDateTime[] receiveTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java new file mode 100644 index 00000000..1ac6b9d2 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java @@ -0,0 +1,116 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.log; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.JsonConvert; +import com.chanko.yunxi.mes.heli.module.system.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; +import java.util.Map; + +@Schema(description = "管理后台 - 短信日志 Response VO") +@Data +@ExcelIgnoreUnannotated +public class SmsLogRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "短信渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @ExcelProperty("短信渠道编号") + private Long channelId; + + @Schema(description = "短信渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ALIYUN") + @ExcelProperty("短信渠道编码") + private String channelCode; + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + @ExcelProperty("模板编号") + private Long templateId; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test-01") + @ExcelProperty("模板编码") + private String templateCode; + + @Schema(description = "短信类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "短信类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_TEMPLATE_TYPE) + private Integer templateType; + + @Schema(description = "短信内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你的验证码是 1024") + @ExcelProperty("短信内容") + private String templateContent; + + @Schema(description = "短信参数", requiredMode = Schema.RequiredMode.REQUIRED, example = "name,code") + @ExcelProperty(value = "短信参数", converter = JsonConvert.class) + private Map templateParams; + + @Schema(description = "短信 API 的模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "SMS_207945135") + @ExcelProperty("短信 API 的模板编号") + private String apiTemplateId; + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @ExcelProperty("手机号") + private String mobile; + + @Schema(description = "用户编号", example = "10") + @ExcelProperty("用户编号") + private Long userId; + + @Schema(description = "用户类型", example = "1") + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_TYPE) + private Integer userType; + + @Schema(description = "发送状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "发送状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_SEND_STATUS) + private Integer sendStatus; + + @Schema(description = "发送时间") + @ExcelProperty("发送时间") + private LocalDateTime sendTime; + + @Schema(description = "短信 API 发送结果的编码", example = "SUCCESS") + @ExcelProperty("短信 API 发送结果的编码") + private String apiSendCode; + + @Schema(description = "短信 API 发送失败的提示", example = "成功") + @ExcelProperty("短信 API 发送失败的提示") + private String apiSendMsg; + + @Schema(description = "短信 API 发送返回的唯一请求 ID", example = "3837C6D3-B96F-428C-BBB2-86135D4B5B99") + @ExcelProperty("短信 API 发送返回的唯一请求 ID") + private String apiRequestId; + + @Schema(description = "短信 API 发送返回的序号", example = "62923244790") + @ExcelProperty("短信 API 发送返回的序号") + private String apiSerialNo; + + @Schema(description = "接收状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @ExcelProperty(value = "接收状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_RECEIVE_STATUS) + private Integer receiveStatus; + + @Schema(description = "接收时间") + @ExcelProperty("接收时间") + private LocalDateTime receiveTime; + + @Schema(description = "API 接收结果的编码", example = "DELIVRD") + @ExcelProperty("API 接收结果的编码") + private String apiReceiveCode; + + @Schema(description = "API 接收结果的说明", example = "用户接收成功") + @ExcelProperty("API 接收结果的说明") + private String apiReceiveMsg; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java new file mode 100644 index 00000000..f12bad32 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 短信模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplatePageReqVO extends PageParam { + + @Schema(description = "短信签名", example = "1") + private Integer type; + + @Schema(description = "开启状态", example = "1") + private Integer status; + + @Schema(description = "模板编码,模糊匹配", example = "test_01") + private String code; + + @Schema(description = "模板内容,模糊匹配", example = "你好,{name}。你长的太{like}啦!") + private String content; + + @Schema(description = "短信 API 的模板编号,模糊匹配", example = "4383920") + private String apiTemplateId; + + @Schema(description = "短信渠道编号", example = "10") + private Long channelId; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java new file mode 100644 index 00000000..e3f1151d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java @@ -0,0 +1,69 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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; +import java.util.List; + +@Schema(description = "管理后台 - 短信模板 Response VO") +@Data +@ExcelIgnoreUnannotated +public class SmsTemplateRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "短信类型,参见 SmsTemplateTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "短信签名", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_TEMPLATE_TYPE) + private Integer type; + + @Schema(description = "开启状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "开启状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + @ExcelProperty("模板编码") + private String code; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @ExcelProperty("模板名称") + private String name; + + @Schema(description = "模板内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,{name}。你长的太{like}啦!") + @ExcelProperty("模板内容") + private String content; + + @Schema(description = "参数数组", example = "name,code") + private List params; + + @Schema(description = "备注", example = "哈哈哈") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "短信 API 的模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4383920") + @ExcelProperty("短信 API 的模板编号") + private String apiTemplateId; + + @Schema(description = "短信渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @ExcelProperty("短信渠道编号") + private Long channelId; + + @Schema(description = "短信渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ALIYUN") + @ExcelProperty(value = "短信渠道编码", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_CHANNEL_CODE) + private String channelCode; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplateSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplateSaveReqVO.java new file mode 100644 index 00000000..fc51e141 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplateSaveReqVO.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 短信模板创建/修改 Request VO") +@Data +public class SmsTemplateSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "短信类型,参见 SmsTemplateTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "短信类型不能为空") + private Integer type; + + @Schema(description = "开启状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "开启状态不能为空") + private Integer status; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String code; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @NotNull(message = "模板名称不能为空") + private String name; + + @Schema(description = "模板内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,{name}。你长的太{like}啦!") + @NotNull(message = "模板内容不能为空") + private String content; + + @Schema(description = "备注", example = "哈哈哈") + private String remark; + + @Schema(description = "短信 API 的模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4383920") + @NotNull(message = "短信 API 的模板编号不能为空") + private String apiTemplateId; + + @Schema(description = "短信渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "短信渠道编号不能为空") + private Long channelId; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java new file mode 100644 index 00000000..42d495fd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 短信模板的发送 Request VO") +@Data +public class SmsTemplateSendReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @NotNull(message = "手机号不能为空") + private String mobile; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String templateCode; + + @Schema(description = "模板参数") + private Map templateParams; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/SocialClientController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/SocialClientController.java new file mode 100644 index 00000000..8e0e45bd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/SocialClientController.java @@ -0,0 +1,73 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.socail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client.SocialClientRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialClientDO; +import com.chanko.yunxi.mes.heli.module.system.service.social.SocialClientService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 社交客户端") +@RestController +@RequestMapping("/system/social-client") +@Validated +public class SocialClientController { + + @Resource + private SocialClientService socialClientService; + + @PostMapping("/create") + @Operation(summary = "创建社交客户端") + @PreAuthorize("@ss.hasPermission('system:social-client:create')") + public CommonResult createSocialClient(@Valid @RequestBody SocialClientSaveReqVO createReqVO) { + return success(socialClientService.createSocialClient(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新社交客户端") + @PreAuthorize("@ss.hasPermission('system:social-client:update')") + public CommonResult updateSocialClient(@Valid @RequestBody SocialClientSaveReqVO updateReqVO) { + socialClientService.updateSocialClient(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除社交客户端") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:social-client:delete')") + public CommonResult deleteSocialClient(@RequestParam("id") Long id) { + socialClientService.deleteSocialClient(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得社交客户端") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:social-client:query')") + public CommonResult getSocialClient(@RequestParam("id") Long id) { + SocialClientDO client = socialClientService.getSocialClient(id); + return success(BeanUtils.toBean(client, SocialClientRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得社交客户端分页") + @PreAuthorize("@ss.hasPermission('system:social-client:query')") + public CommonResult> getSocialClientPage(@Valid SocialClientPageReqVO pageVO) { + PageResult pageResult = socialClientService.getSocialClientPage(pageVO); + return success(BeanUtils.toBean(pageResult, SocialClientRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/SocialUserController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/SocialUserController.java new file mode 100644 index 00000000..e7a700c1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/SocialUserController.java @@ -0,0 +1,70 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.socail; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user.SocialUserBindReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user.SocialUserUnbindReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user.SocialUserRespVO; +import com.chanko.yunxi.mes.heli.module.system.convert.social.SocialUserConvert; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialUserDO; +import com.chanko.yunxi.mes.heli.module.system.service.social.SocialUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 社交用户") +@RestController +@RequestMapping("/system/social-user") +@Validated +public class SocialUserController { + + @Resource + private SocialUserService socialUserService; + + @PostMapping("/bind") + @Operation(summary = "社交绑定,使用 code 授权码") + public CommonResult socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) { + socialUserService.bindSocialUser(SocialUserConvert.INSTANCE.convert( + getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO)); + return CommonResult.success(true); + } + + @DeleteMapping("/unbind") + @Operation(summary = "取消社交绑定") + public CommonResult socialUnbind(@RequestBody SocialUserUnbindReqVO reqVO) { + socialUserService.unbindSocialUser(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO.getType(), reqVO.getOpenid()); + return CommonResult.success(true); + } + + // ==================== 社交用户 CRUD ==================== + + @GetMapping("/get") + @Operation(summary = "获得社交用户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:social-user:query')") + public CommonResult getSocialUser(@RequestParam("id") Long id) { + SocialUserDO socialUser = socialUserService.getSocialUser(id); + return success(BeanUtils.toBean(socialUser, SocialUserRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得社交用户分页") + @PreAuthorize("@ss.hasPermission('system:social-user:query')") + public CommonResult> getSocialUserPage(@Valid SocialUserPageReqVO pageVO) { + PageResult pageResult = socialUserService.getSocialUserPage(pageVO); + return success(BeanUtils.toBean(pageResult, SocialUserRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/client/SocialClientPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/client/SocialClientPageReqVO.java new file mode 100644 index 00000000..8da5ec59 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/client/SocialClientPageReqVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client; + +import com.chanko.yunxi.mes.heli.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 +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SocialClientPageReqVO extends PageParam { + + @Schema(description = "应用名", example = "mes商城") + private String name; + + @Schema(description = "社交平台的类型", example = "31") + private Integer socialType; + + @Schema(description = "用户类型", example = "2") + private Integer userType; + + @Schema(description = "客户端编号", example = "145442115") + private String clientId; + + @Schema(description = "状态", example = "1") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/client/SocialClientRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/client/SocialClientRespVO.java new file mode 100644 index 00000000..4979ffd6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/client/SocialClientRespVO.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 社交客户端 Response VO") +@Data +public class SocialClientRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "27162") + private Long id; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes商城") + private String name; + + @Schema(description = "社交平台的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "31") + private Integer socialType; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer userType; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "wwd411c69a39ad2e54") + private String clientId; + + @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "peter") + private String clientSecret; + + @Schema(description = "授权方的网页应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000045") + private String agentId; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/client/SocialClientSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/client/SocialClientSaveReqVO.java new file mode 100644 index 00000000..0386eb7d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/client/SocialClientSaveReqVO.java @@ -0,0 +1,61 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import java.util.Objects; + +@Schema(description = "管理后台 - 社交客户端创建/修改 Request VO") +@Data +public class SocialClientSaveReqVO { + + @Schema(description = "编号", example = "27162") + private Long id; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes商城") + @NotNull(message = "应用名不能为空") + private String name; + + @Schema(description = "社交平台的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "31") + @NotNull(message = "社交平台的类型不能为空") + @InEnum(SocialTypeEnum.class) + private Integer socialType; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "用户类型不能为空") + @InEnum(UserTypeEnum.class) + private Integer userType; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "wwd411c69a39ad2e54") + @NotNull(message = "客户端编号不能为空") + private String clientId; + + @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "peter") + @NotNull(message = "客户端密钥不能为空") + private String clientSecret; + + @Schema(description = "授权方的网页应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000045") + private String agentId; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @AssertTrue(message = "agentId 不能为空") + @JsonIgnore + public boolean isAgentIdValid() { + // 如果是企业微信,必须填写 agentId 属性 + return !Objects.equals(socialType, SocialTypeEnum.WECHAT_ENTERPRISE.getType()) + || !StrUtil.isEmpty(agentId); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserBindReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserBindReqVO.java new file mode 100644 index 00000000..97ad2d1a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserBindReqVO.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user; + +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 社交绑定 Request VO,使用 code 授权码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SocialUserBindReqVO { + + @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserPageReqVO.java new file mode 100644 index 00000000..b3bdf953 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserPageReqVO.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 社交用户分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SocialUserPageReqVO extends PageParam { + + @Schema(description = "社交平台的类型", example = "30") + private Integer type; + + @Schema(description = "用户昵称", example = "李四") + private String nickname; + + @Schema(description = "社交 openid", example = "oz-Jdt0kd_jdhUxJHQdBJMlOFN7w") + private String openid; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserRespVO.java new file mode 100644 index 00000000..6d21f2e9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserRespVO.java @@ -0,0 +1,48 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 社交用户 Response VO") +@Data +public class SocialUserRespVO { + + @Schema(description = "主键(自增策略)", requiredMode = Schema.RequiredMode.REQUIRED, example = "14569") + private Long id; + + @Schema(description = "社交平台的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") + private Integer type; + + @Schema(description = "社交 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private String openid; + + @Schema(description = "社交 token", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private String token; + + @Schema(description = "原始 Token 数据,一般是 JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private String rawTokenInfo; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String nickname; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + + @Schema(description = "原始用户数据,一般是 JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") + private String rawUserInfo; + + @Schema(description = "最后一次的认证 code", requiredMode = Schema.RequiredMode.REQUIRED, example = "666666") + private String code; + + @Schema(description = "最后一次的认证 state", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + private String state; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserUnbindReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserUnbindReqVO.java new file mode 100644 index 00000000..884eac89 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/socail/vo/user/SocialUserUnbindReqVO.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user; + +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 取消社交绑定 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SocialUserUnbindReqVO { + + @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + @NotEmpty(message = "社交用户的 openid 不能为空") + private String openid; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/TenantController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/TenantController.http new file mode 100644 index 00000000..a4d51738 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/TenantController.http @@ -0,0 +1,21 @@ +### 获取租户编号 /admin-api/system/get-id-by-name +GET {{baseUrl}}/system/tenant/get-id-by-name?name=芋道源码 + +### 创建租户 /admin-api/system/tenant/create +POST {{baseUrl}}/system/tenant/create +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "name": "芋道", + "contactName": "芋艿", + "contactMobile": "15601691300", + "status": 0, + "domain": "https://www.iocoder.cn", + "packageId": 110, + "expireTime": 1699545600000, + "accountCount": 20, + "username": "admin", + "password": "123321" +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/TenantController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/TenantController.java new file mode 100644 index 00000000..15e1bad6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/TenantController.java @@ -0,0 +1,111 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantDO; +import com.chanko.yunxi.mes.heli.module.system.service.tenant.TenantService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 租户") +@RestController +@RequestMapping("/system/tenant") +public class TenantController { + + @Resource + private TenantService tenantService; + + @GetMapping("/get-id-by-name") + @PermitAll + @Operation(summary = "使用租户名,获得租户编号", description = "登录界面,根据用户的租户名,获得租户编号") + @Parameter(name = "name", description = "租户名", required = true, example = "1024") + public CommonResult getTenantIdByName(@RequestParam("name") String name) { + TenantDO tenant = tenantService.getTenantByName(name); + return success(tenant != null ? tenant.getId() : null); + } + + @GetMapping("/get-by-website") + @PermitAll + @Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息") + @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") + public CommonResult getTenantByWebsite(@RequestParam("website") String website) { + TenantDO tenant = tenantService.getTenantByWebsite(website); + return success(BeanUtils.toBean(tenant, TenantSimpleRespVO.class)); + } + + @PostMapping("/create") + @Operation(summary = "创建租户") + @PreAuthorize("@ss.hasPermission('system:tenant:create')") + public CommonResult createTenant(@Valid @RequestBody TenantSaveReqVO createReqVO) { + return success(tenantService.createTenant(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新租户") + @PreAuthorize("@ss.hasPermission('system:tenant:update')") + public CommonResult updateTenant(@Valid @RequestBody TenantSaveReqVO updateReqVO) { + tenantService.updateTenant(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除租户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:tenant:delete')") + public CommonResult deleteTenant(@RequestParam("id") Long id) { + tenantService.deleteTenant(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得租户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:tenant:query')") + public CommonResult getTenant(@RequestParam("id") Long id) { + TenantDO tenant = tenantService.getTenant(id); + return success(BeanUtils.toBean(tenant, TenantRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得租户分页") + @PreAuthorize("@ss.hasPermission('system:tenant:query')") + public CommonResult> getTenantPage(@Valid TenantPageReqVO pageVO) { + PageResult pageResult = tenantService.getTenantPage(pageVO); + return success(BeanUtils.toBean(pageResult, TenantRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出租户 Excel") + @PreAuthorize("@ss.hasPermission('system:tenant:export')") + @OperateLog(type = EXPORT) + public void exportTenantExcel(@Valid TenantPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = tenantService.getTenantPage(exportReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "租户.xls", "数据", TenantRespVO.class, + BeanUtils.toBean(list, TenantRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/TenantPackageController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/TenantPackageController.java new file mode 100644 index 00000000..fc396c8f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/TenantPackageController.java @@ -0,0 +1,80 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages.*; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantPackageDO; +import com.chanko.yunxi.mes.heli.module.system.service.tenant.TenantPackageService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 租户套餐") +@RestController +@RequestMapping("/system/tenant-package") +@Validated +public class TenantPackageController { + + @Resource + private TenantPackageService tenantPackageService; + + @PostMapping("/create") + @Operation(summary = "创建租户套餐") + @PreAuthorize("@ss.hasPermission('system:tenant-package:create')") + public CommonResult createTenantPackage(@Valid @RequestBody TenantPackageSaveReqVO createReqVO) { + return success(tenantPackageService.createTenantPackage(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新租户套餐") + @PreAuthorize("@ss.hasPermission('system:tenant-package:update')") + public CommonResult updateTenantPackage(@Valid @RequestBody TenantPackageSaveReqVO updateReqVO) { + tenantPackageService.updateTenantPackage(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除租户套餐") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:tenant-package:delete')") + public CommonResult deleteTenantPackage(@RequestParam("id") Long id) { + tenantPackageService.deleteTenantPackage(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得租户套餐") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:tenant-package:query')") + public CommonResult getTenantPackage(@RequestParam("id") Long id) { + TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(id); + return success(BeanUtils.toBean(tenantPackage, TenantPackageRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得租户套餐分页") + @PreAuthorize("@ss.hasPermission('system:tenant-package:query')") + public CommonResult> getTenantPackagePage(@Valid TenantPackagePageReqVO pageVO) { + PageResult pageResult = tenantPackageService.getTenantPackagePage(pageVO); + return success(BeanUtils.toBean(pageResult, TenantPackageRespVO.class)); + } + + @GetMapping({"/get-simple-list", "simple-list"}) + @Operation(summary = "获取租户套餐精简信息列表", description = "只包含被开启的租户套餐,主要用于前端的下拉选项") + public CommonResult> getTenantPackageList() { + List list = tenantPackageService.getTenantPackageListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(BeanUtils.toBean(list, TenantPackageSimpleRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java new file mode 100644 index 00000000..f8cc9ecd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java @@ -0,0 +1,32 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 租户套餐分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPackagePageReqVO extends PageParam { + + @Schema(description = "套餐名", example = "VIP") + private String name; + + @Schema(description = "状态", example = "1") + private Integer status; + + @Schema(description = "备注", example = "好") + private String remark; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java new file mode 100644 index 00000000..811fdc65 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages; + +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 TenantPackageRespVO { + + @Schema(description = "套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") + private String name; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "备注", example = "好") + private String remark; + + @Schema(description = "关联的菜单编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Set menuIds; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackageSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackageSaveReqVO.java new file mode 100644 index 00000000..ead952af --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackageSaveReqVO.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Set; + +@Schema(description = "管理后台 - 租户套餐创建/修改 Request VO") +@Data +public class TenantPackageSaveReqVO { + + @Schema(description = "套餐编号", example = "1024") + private Long id; + + @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") + @NotEmpty(message = "套餐名不能为空") + private String name; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + + @Schema(description = "备注", example = "好") + private String remark; + + @Schema(description = "关联的菜单编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "关联的菜单编号不能为空") + private Set menuIds; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java new file mode 100644 index 00000000..c44101df --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 租户套餐精简 Response VO") +@Data +public class TenantPackageSimpleRespVO { + + @Schema(description = "套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "套餐编号不能为空") + private Long id; + + @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") + @NotNull(message = "套餐名不能为空") + private String name; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java new file mode 100644 index 00000000..3ed6d417 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant; + +import com.chanko.yunxi.mes.heli.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 com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 租户分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPageReqVO extends PageParam { + + @Schema(description = "租户名", example = "芋道") + private String name; + + @Schema(description = "联系人", example = "芋艿") + private String contactName; + + @Schema(description = "联系手机", example = "15601691300") + private String contactMobile; + + @Schema(description = "租户状态(0正常 1停用)", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java new file mode 100644 index 00000000..d7a0d572 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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 TenantRespVO { + + @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 = "芋艿") + @ExcelProperty("联系人") + private String contactName; + + @Schema(description = "联系手机", example = "15601691300") + @ExcelProperty("联系手机") + private String contactMobile; + + @Schema(description = "租户状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "绑定域名", example = "https://www.iocoder.cn") + private String website; + + @Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long packageId; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime expireTime; + + @Schema(description = "账号数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer accountCount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantSaveReqVO.java new file mode 100644 index 00000000..ac15ef08 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantSaveReqVO.java @@ -0,0 +1,70 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant; + +import cn.hutool.core.util.ObjectUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 租户创建/修改 Request VO") +@Data +public class TenantSaveReqVO { + + @Schema(description = "租户编号", example = "1024") + private Long id; + + @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "租户名不能为空") + private String name; + + @Schema(description = "联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotNull(message = "联系人不能为空") + private String contactName; + + @Schema(description = "联系手机", example = "15601691300") + private String contactMobile; + + @Schema(description = "租户状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "租户状态") + private Integer status; + + @Schema(description = "绑定域名", example = "https://www.iocoder.cn") + private String website; + + @Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "租户套餐编号不能为空") + private Long packageId; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "过期时间不能为空") + private LocalDateTime expireTime; + + @Schema(description = "账号数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账号数量不能为空") + private Integer accountCount; + + // ========== 仅【创建】时,需要传递的字段 ========== + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") + @Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + @AssertTrue(message = "用户账号、密码不能为空") + @JsonIgnore + public boolean isUsernameValid() { + return id != null // 修改时,不需要传递 + || (ObjectUtil.isAllNotEmpty(username, password)); // 新增时,必须都传递 username、password + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java new file mode 100644 index 00000000..4acd78d7 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java @@ -0,0 +1,16 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 租户精简 Response VO") +@Data +public class TenantSimpleRespVO { + + @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserController.http new file mode 100644 index 00000000..6d9cea80 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserController.http @@ -0,0 +1,4 @@ +### 请求 /system/user/page 接口 => 没有权限 +GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserController.java new file mode 100644 index 00000000..e067eb91 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserController.java @@ -0,0 +1,169 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.excel.core.util.ExcelUtils; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.annotations.OperateLog; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.*; +import com.chanko.yunxi.mes.heli.module.system.convert.user.UserConvert; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.enums.common.SexEnum; +import com.chanko.yunxi.mes.heli.module.system.service.dept.DeptService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +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 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 javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertList; +import static com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 用户") +@RestController +@RequestMapping("/system/user") +@Validated +public class UserController { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + + @PostMapping("/create") + @Operation(summary = "新增用户") + @PreAuthorize("@ss.hasPermission('system:user:create')") + public CommonResult createUser(@Valid @RequestBody UserSaveReqVO reqVO) { + Long id = userService.createUser(reqVO); + return success(id); + } + + @PutMapping("update") + @Operation(summary = "修改用户") + @PreAuthorize("@ss.hasPermission('system:user:update')") + public CommonResult updateUser(@Valid @RequestBody UserSaveReqVO reqVO) { + userService.updateUser(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:user:delete')") + public CommonResult deleteUser(@RequestParam("id") Long id) { + userService.deleteUser(id); + return success(true); + } + + @PutMapping("/update-password") + @Operation(summary = "重置用户密码") + @PreAuthorize("@ss.hasPermission('system:user:update-password')") + public CommonResult updateUserPassword(@Valid @RequestBody UserUpdatePasswordReqVO reqVO) { + userService.updateUserPassword(reqVO.getId(), reqVO.getPassword()); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "修改用户状态") + @PreAuthorize("@ss.hasPermission('system:user:update')") + public CommonResult updateUserStatus(@Valid @RequestBody UserUpdateStatusReqVO reqVO) { + userService.updateUserStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得用户分页列表") + @PreAuthorize("@ss.hasPermission('system:user:list')") + public CommonResult> getUserPage(@Valid UserPageReqVO pageReqVO) { + // 获得用户分页列表 + PageResult pageResult = userService.getUserPage(pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); + } + // 拼接数据 + Map deptMap = deptService.getDeptMap( + convertList(pageResult.getList(), AdminUserDO::getDeptId)); + return success(new PageResult<>(UserConvert.INSTANCE.convertList(pageResult.getList(), deptMap), + pageResult.getTotal())); + } + + @GetMapping({"/list-all-simple", "/simple-list"}) + @Operation(summary = "获取用户精简信息列表", description = "只包含被开启的用户,主要用于前端的下拉选项") + public CommonResult> getSimpleUserList() { + List list = userService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus()); + // 拼接数据 + Map deptMap = deptService.getDeptMap( + convertList(list, AdminUserDO::getDeptId)); + return success(UserConvert.INSTANCE.convertSimpleList(list, deptMap)); + } + + @GetMapping("/get") + @Operation(summary = "获得用户详情") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:user:query')") + public CommonResult getUser(@RequestParam("id") Long id) { + AdminUserDO user = userService.getUser(id); + // 拼接数据 + DeptDO dept = deptService.getDept(user.getDeptId()); + return success(UserConvert.INSTANCE.convert(user, dept)); + } + + @GetMapping("/export") + @Operation(summary = "导出用户") + @PreAuthorize("@ss.hasPermission('system:user:export')") + @OperateLog(type = EXPORT) + public void exportUserList(@Validated UserPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = userService.getUserPage(exportReqVO).getList(); + // 输出 Excel + Map deptMap = deptService.getDeptMap( + convertList(list, AdminUserDO::getDeptId)); + ExcelUtils.write(response, "用户数据.xls", "数据", UserRespVO.class, + UserConvert.INSTANCE.convertList(list, deptMap)); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得导入用户模板") + public void importTemplate(HttpServletResponse response) throws IOException { + // 手动创建导出 demo + List list = Arrays.asList( + UserImportExcelVO.builder().username("yunai").deptId(1L).email("yunai@iocoder.cn").mobile("15601691300") + .nickname("芋道").status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.MALE.getSex()).build(), + UserImportExcelVO.builder().username("yuanma").deptId(2L).email("yuanma@iocoder.cn").mobile("15601701300") + .nickname("源码").status(CommonStatusEnum.DISABLE.getStatus()).sex(SexEnum.FEMALE.getSex()).build() + ); + // 输出 + ExcelUtils.write(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, list); + } + + @PostMapping("/import") + @Operation(summary = "导入用户") + @Parameters({ + @Parameter(name = "file", description = "Excel 文件", required = true), + @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true") + }) + @PreAuthorize("@ss.hasPermission('system:user:import')") + public CommonResult importExcel(@RequestParam("file") MultipartFile file, + @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception { + List list = ExcelUtils.read(file, UserImportExcelVO.class); + return success(userService.importUserList(list, updateSupport)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserProfileController.http b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserProfileController.http new file mode 100644 index 00000000..f06037b3 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserProfileController.http @@ -0,0 +1,4 @@ +### 请求 /system/user/profile/get 接口 => 没有权限 +GET {{baseUrl}}/system/user/profile/get +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserProfileController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserProfileController.java new file mode 100644 index 00000000..1cac9758 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/UserProfileController.java @@ -0,0 +1,100 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.annotation.DataPermission; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile.UserProfileRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.chanko.yunxi.mes.heli.module.system.convert.user.UserConvert; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.PostDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialUserDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.service.dept.DeptService; +import com.chanko.yunxi.mes.heli.module.system.service.dept.PostService; +import com.chanko.yunxi.mes.heli.module.system.service.permission.PermissionService; +import com.chanko.yunxi.mes.heli.module.system.service.permission.RoleService; +import com.chanko.yunxi.mes.heli.module.system.service.social.SocialUserService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; +import static com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY; + +@Tag(name = "管理后台 - 用户个人中心") +@RestController +@RequestMapping("/system/user/profile") +@Validated +@Slf4j +public class UserProfileController { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + @Resource + private PostService postService; + @Resource + private PermissionService permissionService; + @Resource + private RoleService roleService; + @Resource + private SocialUserService socialService; + + @GetMapping("/get") + @Operation(summary = "获得登录用户信息") + @DataPermission(enable = false) // 关闭数据权限,避免只查看自己时,查询不到部门。 + public CommonResult getUserProfile() { + // 获得用户基本信息 + AdminUserDO user = userService.getUser(getLoginUserId()); + // 获得用户角色 + List userRoles = roleService.getRoleListFromCache(permissionService.getUserRoleIdListByUserId(user.getId())); + // 获得部门信息 + DeptDO dept = user.getDeptId() != null ? deptService.getDept(user.getDeptId()) : null; + // 获得岗位信息 + List posts = CollUtil.isNotEmpty(user.getPostIds()) ? postService.getPostList(user.getPostIds()) : null; + // 获得社交用户信息 + List socialUsers = socialService.getSocialUserList(user.getId(), UserTypeEnum.ADMIN.getValue()); + return success(UserConvert.INSTANCE.convert(user, userRoles, dept, posts, socialUsers)); + } + + @PutMapping("/update") + @Operation(summary = "修改用户个人信息") + public CommonResult updateUserProfile(@Valid @RequestBody UserProfileUpdateReqVO reqVO) { + userService.updateUserProfile(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/update-password") + @Operation(summary = "修改用户个人密码") + public CommonResult updateUserProfilePassword(@Valid @RequestBody UserProfileUpdatePasswordReqVO reqVO) { + userService.updateUserPassword(getLoginUserId(), reqVO); + return success(true); + } + + @RequestMapping(value = "/update-avatar", + method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题 + @Operation(summary = "上传用户个人头像") + public CommonResult updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception { + if (file.isEmpty()) { + throw exception(FILE_IS_EMPTY); + } + String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream()); + return success(avatar); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java new file mode 100644 index 00000000..1046883b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java @@ -0,0 +1,75 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile; + +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role.RoleSimpleRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Schema(description = "管理后台 - 用户个人中心信息 Response VO") +public class UserProfileRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + private String username; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String nickname; + + @Schema(description = "用户邮箱", example = "mes@iocoder.cn") + private String email; + + @Schema(description = "手机号码", example = "15601691300") + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + private Integer sex; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + + @Schema(description = "最后登录 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.1") + private String loginIp; + + @Schema(description = "最后登录时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime loginDate; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + + /** + * 所属角色 + */ + private List roles; + /** + * 所在部门 + */ + private DeptSimpleRespVO dept; + /** + * 所属岗位数组 + */ + private List posts; + /** + * 社交用户数组 + */ + private List socialUsers; + + @Schema(description = "社交用户") + @Data + public static class SocialUser { + + @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer type; + + @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + private String openid; + + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java new file mode 100644 index 00000000..c018e674 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 用户个人中心更新密码 Request VO") +@Data +public class UserProfileUpdatePasswordReqVO { + + @Schema(description = "旧密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotEmpty(message = "旧密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String oldPassword; + + @Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "654321") + @NotEmpty(message = "新密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String newPassword; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java new file mode 100644 index 00000000..3d2b65b2 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Size; + + +@Schema(description = "管理后台 - 用户个人信息更新 Request VO") +@Data +public class UserProfileUpdateReqVO { + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @Size(max = 30, message = "用户昵称长度不能超过 30 个字符") + private String nickname; + + @Schema(description = "用户邮箱", example = "mes@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @Schema(description = "手机号码", example = "15601691300") + @Length(min = 11, max = 11, message = "手机号长度必须 11 位") + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + private Integer sex; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserImportExcelVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserImportExcelVO.java new file mode 100644 index 00000000..64fd9df3 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserImportExcelVO.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 用户 Excel 导入 VO + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题 +public class UserImportExcelVO { + + @ExcelProperty("登录名称") + private String username; + + @ExcelProperty("用户名称") + private String nickname; + + @ExcelProperty("部门编号") + private Long deptId; + + @ExcelProperty("用户邮箱") + private String email; + + @ExcelProperty("手机号码") + private String mobile; + + @ExcelProperty(value = "用户性别", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_SEX) + private Integer sex; + + @ExcelProperty(value = "账号状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserImportRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserImportRespVO.java new file mode 100644 index 00000000..c68f9eb0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserImportRespVO.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - 用户导入 Response VO") +@Data +@Builder +public class UserImportRespVO { + + @Schema(description = "创建成功的用户名数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List createUsernames; + + @Schema(description = "更新成功的用户名数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List updateUsernames; + + @Schema(description = "导入失败的用户集合,key 为用户名,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureUsernames; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserPageReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserPageReqVO.java new file mode 100644 index 00000000..de395c47 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserPageReqVO.java @@ -0,0 +1,38 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 用户分页 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class UserPageReqVO extends PageParam { + + @Schema(description = "用户账号,模糊匹配", example = "mes") + private String username; + + @Schema(description = "手机号码,模糊匹配", example = "mes") + private String mobile; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @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; + + @Schema(description = "部门编号,同时筛选子部门", example = "1024") + private Long deptId; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserRespVO.java new file mode 100644 index 00000000..af51d4da --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserRespVO.java @@ -0,0 +1,75 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user; + +import com.chanko.yunxi.mes.heli.framework.excel.core.annotations.DictFormat; +import com.chanko.yunxi.mes.heli.framework.excel.core.convert.DictConvert; +import com.chanko.yunxi.mes.heli.module.system.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; +import java.util.Set; + +@Schema(description = "管理后台 - 用户信息 Response VO") +@Data +@ExcelIgnoreUnannotated +public class UserRespVO{ + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("用户编号") + private Long id; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @ExcelProperty("用户名称") + private String username; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @ExcelProperty("用户昵称") + private String nickname; + + @Schema(description = "备注", example = "我是一个用户") + private String remark; + + @Schema(description = "部门ID", example = "我是一个用户") + private Long deptId; + @Schema(description = "部门名称", example = "IT 部") + @ExcelProperty("部门名称") + private String deptName; + + @Schema(description = "岗位编号数组", example = "1") + private Set postIds; + + @Schema(description = "用户邮箱", example = "mes@iocoder.cn") + @ExcelProperty("用户邮箱") + private String email; + + @Schema(description = "手机号码", example = "15601691300") + @ExcelProperty("手机号码") + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + @ExcelProperty(value = "用户性别", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_SEX) + private Integer sex; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "帐号状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "最后登录 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.1") + @ExcelProperty("最后登录IP") + private String loginIp; + + @Schema(description = "最后登录时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + @ExcelProperty("最后登录时间") + private LocalDateTime loginDate; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserSaveReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserSaveReqVO.java new file mode 100644 index 00000000..0c47bd52 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserSaveReqVO.java @@ -0,0 +1,67 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user; + +import cn.hutool.core.util.ObjectUtil; +import com.chanko.yunxi.mes.heli.framework.common.validation.Mobile; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.*; +import java.util.Set; + +@Schema(description = "管理后台 - 用户创建/修改 Request VO") +@Data +public class UserSaveReqVO { + + @Schema(description = "用户编号", example = "1024") + private Long id; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "mes") + @NotBlank(message = "用户账号不能为空") + @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") + @Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") + private String username; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @Size(max = 30, message = "用户昵称长度不能超过30个字符") + private String nickname; + + @Schema(description = "备注", example = "我是一个用户") + private String remark; + + @Schema(description = "部门ID", example = "我是一个用户") + private Long deptId; + + @Schema(description = "岗位编号数组", example = "1") + private Set postIds; + + @Schema(description = "用户邮箱", example = "mes@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @Schema(description = "手机号码", example = "15601691300") + @Mobile + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + private Integer sex; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + + // ========== 仅【创建】时,需要传递的字段 ========== + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + @AssertTrue(message = "密码不能为空") + @JsonIgnore + public boolean isPasswordValid() { + return id != null // 修改时,不需要传递 + || (ObjectUtil.isAllNotEmpty(password)); // 新增时,必须都传递 password + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java new file mode 100644 index 00000000..3298c04d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 用户精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserSimpleRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String nickname; + + @Schema(description = "部门ID", example = "我是一个用户") + private Long deptId; + @Schema(description = "部门名称", example = "IT 部") + private String deptName; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java new file mode 100644 index 00000000..cbf5e427 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 用户更新密码 Request VO") +@Data +public class UserUpdatePasswordReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long id; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java new file mode 100644 index 00000000..464f891d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 用户更新状态 Request VO") +@Data +public class UserUpdateStatusReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "角色编号不能为空") + private Long id; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/dict/AppDictDataController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/dict/AppDictDataController.java new file mode 100644 index 00000000..39b256b1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/dict/AppDictDataController.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.app.dict; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.app.dict.vo.AppDictDataRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictDataDO; +import com.chanko.yunxi.mes.heli.module.system.service.dict.DictDataService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +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 javax.annotation.Resource; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 字典数据") +@RestController +@RequestMapping("/system/dict-data") +@Validated +public class AppDictDataController { + + @Resource + private DictDataService dictDataService; + + @GetMapping("/type") + @Operation(summary = "根据字典类型查询字典数据信息") + @Parameter(name = "type", description = "字典类型", required = true, example = "common_status") + public CommonResult> getDictDataListByType(@RequestParam("type") String type) { + List list = dictDataService.getDictDataList( + CommonStatusEnum.ENABLE.getStatus(), type); + return success(BeanUtils.toBean(list, AppDictDataRespVO.class)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/dict/vo/AppDictDataRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/dict/vo/AppDictDataRespVO.java new file mode 100644 index 00000000..b6d4dd4c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/dict/vo/AppDictDataRespVO.java @@ -0,0 +1,29 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.app.dict.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Schema(description = "用户 App - 字典数据信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AppDictDataRespVO { + + @Schema(description = "字典数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String label; + + @Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "iocoder") + private String value; + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") + private String dictType; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/ip/AppAreaController.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/ip/AppAreaController.java new file mode 100644 index 00000000..5e1836a1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/ip/AppAreaController.java @@ -0,0 +1,34 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.app.ip; + +import cn.hutool.core.lang.Assert; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.ip.core.Area; +import com.chanko.yunxi.mes.heli.framework.ip.core.utils.AreaUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.app.ip.vo.AppAreaNodeRespVO; +import com.chanko.yunxi.mes.heli.module.system.convert.ip.AreaConvert; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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.List; + +import static com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 地区") +@RestController +@RequestMapping("/system/area") +@Validated +public class AppAreaController { + + @GetMapping("/tree") + @Operation(summary = "获得地区树") + public CommonResult> getAreaTree() { + Area area = AreaUtils.getArea(Area.ID_CHINA); + Assert.notNull(area, "获取不到中国"); + return success(AreaConvert.INSTANCE.convertList3(area.getChildren())); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/ip/vo/AppAreaNodeRespVO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/ip/vo/AppAreaNodeRespVO.java new file mode 100644 index 00000000..1416eb6b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/app/ip/vo/AppAreaNodeRespVO.java @@ -0,0 +1,23 @@ +package com.chanko.yunxi.mes.heli.module.system.controller.app.ip.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 地区节点 Response VO") +@Data +public class AppAreaNodeRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") + private Integer id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "北京") + private String name; + + /** + * 子节点 + */ + private List children; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/package-info.java new file mode 100644 index 00000000..d0324b1c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 mes-ui-admin 前端项目 + * 2. app 包:提供给用户 APP mes-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.chanko.yunxi.mes.heli.module.system.controller; diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/auth/AuthConvert.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/auth/AuthConvert.java new file mode 100644 index 00000000..30ac8c95 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/auth/AuthConvert.java @@ -0,0 +1,87 @@ +package com.chanko.yunxi.mes.heli.module.system.convert.auth; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserBindReqDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo.*; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.MenuTypeEnum; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import org.slf4j.LoggerFactory; + +import java.util.*; + +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.filterList; +import static com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO.ID_ROOT; + +@Mapper +public interface AuthConvert { + + AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class); + + AuthLoginRespVO convert(OAuth2AccessTokenDO bean); + + default AuthPermissionInfoRespVO convert(AdminUserDO user, List roleList, List menuList) { + return AuthPermissionInfoRespVO.builder() + .user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).build()) + .roles(convertSet(roleList, RoleDO::getCode)) + // 权限标识信息 + .permissions(convertSet(menuList, MenuDO::getPermission)) + // 菜单树 + .menus(buildMenuTree(menuList)) + .build(); + } + + AuthPermissionInfoRespVO.MenuVO convertTreeNode(MenuDO menu); + + /** + * 将菜单列表,构建成菜单树 + * + * @param menuList 菜单列表 + * @return 菜单树 + */ + default List buildMenuTree(List menuList) { + if (CollUtil.isEmpty(menuList)) { + return Collections.emptyList(); + } + // 移除按钮 + menuList.removeIf(menu -> menu.getType().equals(MenuTypeEnum.BUTTON.getType())); + // 排序,保证菜单的有序性 + menuList.sort(Comparator.comparing(MenuDO::getSort)); + + // 构建菜单树 + // 使用 LinkedHashMap 的原因,是为了排序 。实际也可以用 Stream API ,就是太丑了。 + Map treeNodeMap = new LinkedHashMap<>(); + menuList.forEach(menu -> treeNodeMap.put(menu.getId(), AuthConvert.INSTANCE.convertTreeNode(menu))); + // 处理父子关系 + treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(ID_ROOT)).forEach(childNode -> { + // 获得父节点 + AuthPermissionInfoRespVO.MenuVO parentNode = treeNodeMap.get(childNode.getParentId()); + if (parentNode == null) { + LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]", + childNode.getId(), childNode.getParentId()); + return; + } + // 将自己添加到父节点中 + if (parentNode.getChildren() == null) { + parentNode.setChildren(new ArrayList<>()); + } + parentNode.getChildren().add(childNode); + }); + // 获得到所有的根节点 + return filterList(treeNodeMap.values(), node -> ID_ROOT.equals(node.getParentId())); + } + + SocialUserBindReqDTO convert(Long userId, Integer userType, AuthSocialLoginReqVO reqVO); + + SmsCodeSendReqDTO convert(AuthSmsSendReqVO reqVO); + + SmsCodeUseReqDTO convert(AuthSmsLoginReqVO reqVO, Integer scene, String usedIp); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/ip/AreaConvert.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/ip/AreaConvert.java new file mode 100644 index 00000000..8d3120ec --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/ip/AreaConvert.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.module.system.convert.ip; + +import com.chanko.yunxi.mes.heli.framework.ip.core.Area; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.ip.vo.AreaNodeRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.app.ip.vo.AppAreaNodeRespVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface AreaConvert { + + AreaConvert INSTANCE = Mappers.getMapper(AreaConvert.class); + + List convertList(List list); + + List convertList3(List list); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/logger/OperateLogConvert.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/logger/OperateLogConvert.java new file mode 100644 index 00000000..cd414679 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/logger/OperateLogConvert.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.system.convert.logger; + +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.MapUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.OperateLogDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface OperateLogConvert { + + OperateLogConvert INSTANCE = Mappers.getMapper(OperateLogConvert.class); + + default List convertList(List list, Map userMap) { + return CollectionUtils.convertList(list, log -> { + OperateLogRespVO logVO = BeanUtils.toBean(log, OperateLogRespVO.class); + MapUtils.findAndThen(userMap, log.getUserId(), user -> logVO.setUserNickname(user.getNickname())); + return logVO; + }); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/mail/MailAccountConvert.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/mail/MailAccountConvert.java new file mode 100644 index 00000000..ca357ac6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/mail/MailAccountConvert.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.convert.mail; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.mail.MailAccount; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailAccountDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface MailAccountConvert { + + MailAccountConvert INSTANCE = Mappers.getMapper(MailAccountConvert.class); + + default MailAccount convert(MailAccountDO account, String nickname) { + String from = StrUtil.isNotEmpty(nickname) ? nickname + " <" + account.getMail() + ">" : account.getMail(); + return new MailAccount().setFrom(from).setAuth(true) + .setUser(account.getUsername()).setPass(account.getPassword()) + .setHost(account.getHost()).setPort(account.getPort()).setSslEnable(account.getSslEnable()); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/oauth2/OAuth2OpenConvert.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/oauth2/OAuth2OpenConvert.java new file mode 100644 index 00000000..b3b7038d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/oauth2/OAuth2OpenConvert.java @@ -0,0 +1,56 @@ +package com.chanko.yunxi.mes.heli.module.system.convert.oauth2; + +import cn.hutool.core.date.LocalDateTimeUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.chanko.yunxi.mes.heli.module.system.util.oauth2.OAuth2Utils; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Mapper +public interface OAuth2OpenConvert { + + OAuth2OpenConvert INSTANCE = Mappers.getMapper(OAuth2OpenConvert.class); + + default OAuth2OpenAccessTokenRespVO convert(OAuth2AccessTokenDO bean) { + OAuth2OpenAccessTokenRespVO respVO = BeanUtils.toBean(bean, OAuth2OpenAccessTokenRespVO.class); + respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); + respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime())); + respVO.setScope(OAuth2Utils.buildScopeStr(bean.getScopes())); + return respVO; + } + + default OAuth2OpenCheckTokenRespVO convert2(OAuth2AccessTokenDO bean) { + OAuth2OpenCheckTokenRespVO respVO = BeanUtils.toBean(bean, OAuth2OpenCheckTokenRespVO.class); + respVO.setExp(LocalDateTimeUtil.toEpochMilli(bean.getExpiresTime()) / 1000L); + respVO.setUserType(UserTypeEnum.ADMIN.getValue()); + return respVO; + } + + default OAuth2OpenAuthorizeInfoRespVO convert(OAuth2ClientDO client, List approves) { + // 构建 scopes + List> scopes = new ArrayList<>(client.getScopes().size()); + Map approveMap = CollectionUtils.convertMap(approves, OAuth2ApproveDO::getScope); + client.getScopes().forEach(scope -> { + OAuth2ApproveDO approve = approveMap.get(scope); + scopes.add(new KeyValue<>(scope, approve != null ? approve.getApproved() : false)); + }); + // 拼接返回 + return new OAuth2OpenAuthorizeInfoRespVO( + new OAuth2OpenAuthorizeInfoRespVO.Client(client.getName(), client.getLogo()), scopes); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/package-info.java new file mode 100644 index 00000000..0578b165 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package com.chanko.yunxi.mes.heli.module.system.convert; diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/social/SocialUserConvert.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/social/SocialUserConvert.java new file mode 100644 index 00000000..92a35e17 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/social/SocialUserConvert.java @@ -0,0 +1,17 @@ +package com.chanko.yunxi.mes.heli.module.system.convert.social; + +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserBindReqDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user.SocialUserBindReqVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SocialUserConvert { + + SocialUserConvert INSTANCE = Mappers.getMapper(SocialUserConvert.class); + + @Mapping(source = "reqVO.type", target = "socialType") + SocialUserBindReqDTO convert(Long userId, Integer userType, SocialUserBindReqVO reqVO); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/tenant/TenantConvert.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/tenant/TenantConvert.java new file mode 100644 index 00000000..5ef19898 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/tenant/TenantConvert.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.system.convert.tenant; + +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.UserSaveReqVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 租户 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface TenantConvert { + + TenantConvert INSTANCE = Mappers.getMapper(TenantConvert.class); + + default UserSaveReqVO convert02(TenantSaveReqVO bean) { + UserSaveReqVO reqVO = new UserSaveReqVO(); + reqVO.setUsername(bean.getUsername()); + reqVO.setPassword(bean.getPassword()); + reqVO.setNickname(bean.getContactName()).setMobile(bean.getContactMobile()); + return reqVO; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/user/UserConvert.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/user/UserConvert.java new file mode 100644 index 00000000..53ecb7a9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/user/UserConvert.java @@ -0,0 +1,58 @@ +package com.chanko.yunxi.mes.heli.module.system.convert.user; + +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.MapUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role.RoleSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile.UserProfileRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.UserRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.UserSimpleRespVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.PostDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialUserDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface UserConvert { + + UserConvert INSTANCE = Mappers.getMapper(UserConvert.class); + + default List convertList(List list, Map deptMap) { + return CollectionUtils.convertList(list, user -> convert(user, deptMap.get(user.getDeptId()))); + } + + default UserRespVO convert(AdminUserDO user, DeptDO dept) { + UserRespVO userVO = BeanUtils.toBean(user, UserRespVO.class); + if (dept != null) { + userVO.setDeptName(dept.getName()); + } + return userVO; + } + + default List convertSimpleList(List list, Map deptMap) { + return CollectionUtils.convertList(list, user -> { + UserSimpleRespVO userVO = BeanUtils.toBean(user, UserSimpleRespVO.class); + MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> userVO.setDeptName(dept.getName())); + return userVO; + }); + } + + default UserProfileRespVO convert(AdminUserDO user, List userRoles, + DeptDO dept, List posts, List socialUsers) { + UserProfileRespVO userVO = BeanUtils.toBean(user, UserProfileRespVO.class); + userVO.setRoles(BeanUtils.toBean(userRoles, RoleSimpleRespVO.class)); + userVO.setDept(BeanUtils.toBean(dept, DeptSimpleRespVO.class)); + userVO.setPosts(BeanUtils.toBean(posts, PostSimpleRespVO.class)); + userVO.setSocialUsers(BeanUtils.toBean(socialUsers, UserProfileRespVO.SocialUser.class)); + return userVO; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 00000000..09ce3bec --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dept/DeptDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dept/DeptDO.java new file mode 100644 index 00000000..e974254b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dept/DeptDO.java @@ -0,0 +1,66 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.tenant.core.db.TenantBaseDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 部门表 + * + * @author ruoyi + * @author 芋道源码 + */ +@TableName("system_dept") +@KeySequence("system_dept_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class DeptDO extends TenantBaseDO { + + public static final Long PARENT_ID_ROOT = 0L; + + /** + * 部门ID + */ + @TableId + private Long id; + /** + * 部门名称 + */ + private String name; + /** + * 父部门ID + * + * 关联 {@link #id} + */ + private Long parentId; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 负责人 + * + * 关联 {@link AdminUserDO#getId()} + */ + private Long leaderUserId; + /** + * 联系电话 + */ + private String phone; + /** + * 邮箱 + */ + private String email; + /** + * 部门状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dept/PostDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dept/PostDO.java new file mode 100644 index 00000000..a4318d04 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dept/PostDO.java @@ -0,0 +1,50 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.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.Data; +import lombok.EqualsAndHashCode; + +/** + * 岗位表 + * + * @author ruoyi + */ +@TableName("system_post") +@KeySequence("system_post_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class PostDO extends BaseDO { + + /** + * 岗位序号 + */ + @TableId + private Long id; + /** + * 岗位名称 + */ + private String name; + /** + * 岗位编码 + */ + private String code; + /** + * 岗位排序 + */ + private Integer sort; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dept/UserPostDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dept/UserPostDO.java new file mode 100644 index 00000000..dd2854ed --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dept/UserPostDO.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户和岗位关联 + * + * @author ruoyi + */ +@TableName("system_user_post") +@KeySequence("system_user_post_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class UserPostDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 用户 ID + * + * 关联 {@link AdminUserDO#getId()} + */ + private Long userId; + /** + * 角色 ID + * + * 关联 {@link PostDO#getId()} + */ + private Long postId; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dict/DictDataDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dict/DictDataDO.java new file mode 100644 index 00000000..2f07286a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dict/DictDataDO.java @@ -0,0 +1,65 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 字典数据表 + * + * @author ruoyi + */ +@TableName("system_dict_data") +@KeySequence("system_dict_data_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataDO extends BaseDO { + + /** + * 字典数据编号 + */ + @TableId + private Long id; + /** + * 字典排序 + */ + private Integer sort; + /** + * 字典标签 + */ + private String label; + /** + * 字典值 + */ + private String value; + /** + * 字典类型 + * + * 冗余 {@link DictDataDO#getDictType()} + */ + private String dictType; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 颜色类型 + * + * 对应到 element-ui 为 default、primary、success、info、warning、danger + */ + private String colorType; + /** + * css 样式 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String cssClass; + /** + * 备注 + */ + private String remark; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dict/DictTypeDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dict/DictTypeDO.java new file mode 100644 index 00000000..f43331ce --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/dict/DictTypeDO.java @@ -0,0 +1,57 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.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 lombok.*; + +import java.time.LocalDateTime; + +/** + * 字典类型表 + * + * @author ruoyi + */ +@TableName("system_dict_type") +@KeySequence("system_dict_type_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DictTypeDO extends BaseDO { + + /** + * 字典主键 + */ + @TableId + private Long id; + /** + * 字典名称 + */ + private String name; + /** + * 字典类型 + */ + private String type; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + + /** + * 删除时间 + */ + private LocalDateTime deletedTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/errorcode/ErrorCodeDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/errorcode/ErrorCodeDO.java new file mode 100644 index 00000000..544624f0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/errorcode/ErrorCodeDO.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.errorcode; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.errorcode.ErrorCodeTypeEnum; +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(value = "system_error_code") +@KeySequence("system_error_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodeDO extends BaseDO { + + /** + * 错误码编号,自增 + */ + @TableId + private Long id; + /** + * 错误码类型 + * + * 枚举 {@link ErrorCodeTypeEnum} + */ + private Integer type; + /** + * 应用名 + */ + private String applicationName; + /** + * 错误码编码 + */ + private Integer code; + /** + * 错误码错误提示 + */ + private String message; + /** + * 错误码备注 + */ + private String memo; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/logger/LoginLogDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/logger/LoginLogDO.java new file mode 100644 index 00000000..afe05bf8 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/logger/LoginLogDO.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.logger.LoginLogTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.logger.LoginResultEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 登录日志表 + * + * 注意,包括登录和登出两种行为 + * + * @author 芋道源码 + */ +@TableName("system_login_log") +@KeySequence("system_login_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class LoginLogDO extends BaseDO { + + /** + * 日志主键 + */ + private Long id; + /** + * 日志类型 + * + * 枚举 {@link LoginLogTypeEnum} + */ + private Integer logType; + /** + * 链路追踪编号 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 用户账号 + * + * 冗余,因为账号可以变更 + */ + private String username; + /** + * 登录结果 + * + * 枚举 {@link LoginResultEnum} + */ + private Integer result; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/logger/OperateLogDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/logger/OperateLogDO.java new file mode 100644 index 00000000..920701d0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/logger/OperateLogDO.java @@ -0,0 +1,144 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.operatelog.core.enums.OperateTypeEnum; +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.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 操作日志表 + * + * @author 芋道源码 + */ +@TableName(value = "system_operate_log", autoResultMap = true) +@KeySequence("system_operate_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OperateLogDO extends BaseDO { + + /** + * {@link #javaMethodArgs} 的最大长度 + */ + public static final Integer JAVA_METHOD_ARGS_MAX_LENGTH = 8000; + + /** + * {@link #resultData} 的最大长度 + */ + public static final Integer RESULT_MAX_LENGTH = 4000; + + /** + * 日志主键 + */ + @TableId + private Long id; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性 + */ + private Long userId; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 操作模块 + */ + private String module; + /** + * 操作名 + */ + private String name; + /** + * 操作分类 + * + * 枚举 {@link OperateTypeEnum} + */ + private Integer type; + /** + * 操作内容,记录整个操作的明细 + * 例如说,修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。 + */ + private String content; + /** + * 拓展字段,有些复杂的业务,需要记录一些字段 + * 例如说,记录订单编号,则可以添加 key 为 "orderId",value 为订单编号 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map exts; + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 请求地址 + */ + private String requestUrl; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + /** + * Java 方法名 + */ + private String javaMethod; + /** + * Java 方法的参数 + * + * 实际格式为 Map + * 不使用 @TableField(typeHandler = FastjsonTypeHandler.class) 注解的原因是,数据库存储有长度限制,会进行裁剪,会导致 JSON 反序列化失败 + * 其中,key 为参数名,value 为参数值 + */ + private String javaMethodArgs; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + /** + * 结果数据 + * + * 如果是对象,则使用 JSON 格式化 + */ + private String resultData; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/mail/MailAccountDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/mail/MailAccountDO.java new file mode 100644 index 00000000..a22403c4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/mail/MailAccountDO.java @@ -0,0 +1,53 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 邮箱账号 DO + * + * 用途:配置发送邮箱的账号 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@TableName(value = "system_mail_account", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class MailAccountDO extends BaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 邮箱 + */ + private String mail; + + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * SMTP 服务器域名 + */ + private String host; + /** + * SMTP 服务器端口 + */ + private Integer port; + /** + * 是否开启 SSL + */ + private Boolean sslEnable; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/mail/MailLogDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/mail/MailLogDO.java new file mode 100644 index 00000000..94a2fe22 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/mail/MailLogDO.java @@ -0,0 +1,121 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.mail.MailSendStatusEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 邮箱日志 DO + * 记录每一次邮件的发送 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@TableName(value = "system_mail_log", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MailLogDO extends BaseDO implements Serializable { + + /** + * 日志编号,自增 + */ + private Long id; + + /** + * 用户编码 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 接收邮箱地址 + */ + private String toMail; + + /** + * 邮箱账号编号 + * + * 关联 {@link MailAccountDO#getId()} + */ + private Long accountId; + /** + * 发送邮箱地址 + * + * 冗余 {@link MailAccountDO#getMail()} + */ + private String fromMail; + + // ========= 模板相关字段 ========= + /** + * 模版编号 + * + * 关联 {@link MailTemplateDO#getId()} + */ + private Long templateId; + /** + * 模版编码 + * + * 冗余 {@link MailTemplateDO#getCode()} + */ + private String templateCode; + /** + * 模版发送人名称 + * + * 冗余 {@link MailTemplateDO#getNickname()} + */ + private String templateNickname; + /** + * 模版标题 + */ + private String templateTitle; + /** + * 模版内容 + * + * 基于 {@link MailTemplateDO#getContent()} 格式化后的内容 + */ + private String templateContent; + /** + * 模版参数 + * + * 基于 {@link MailTemplateDO#getParams()} 输入后的参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map templateParams; + + // ========= 发送相关字段 ========= + /** + * 发送状态 + * + * 枚举 {@link MailSendStatusEnum} + */ + private Integer sendStatus; + /** + * 发送时间 + */ + private LocalDateTime sendTime; + /** + * 发送返回的消息 ID + */ + private String sendMessageId; + /** + * 发送异常 + */ + private String sendException; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/mail/MailTemplateDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/mail/MailTemplateDO.java new file mode 100644 index 00000000..04d28d62 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/mail/MailTemplateDO.java @@ -0,0 +1,71 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 邮件模版 DO + * + * @author wangjingyi + * @since 2022-03-21 + */ +@TableName(value = "system_mail_template", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class MailTemplateDO extends BaseDO { + + /** + * 主键 + */ + private Long id; + /** + * 模版名称 + */ + private String name; + /** + * 模版编号 + */ + private String code; + /** + * 发送的邮箱账号编号 + * + * 关联 {@link MailAccountDO#getId()} + */ + private Long accountId; + + /** + * 发送人名称 + */ + private String nickname; + /** + * 标题 + */ + private String title; + /** + * 内容 + */ + private String content; + /** + * 参数数组(自动根据内容生成) + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List params; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/notice/NoticeDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/notice/NoticeDO.java new file mode 100644 index 00000000..67754f5d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/notice/NoticeDO.java @@ -0,0 +1,47 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notice; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.notice.NoticeTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 通知公告表 + * + * @author ruoyi + */ +@TableName("system_notice") +@KeySequence("system_notice_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeDO extends BaseDO { + + /** + * 公告ID + */ + private Long id; + /** + * 公告标题 + */ + private String title; + /** + * 公告类型 + * + * 枚举 {@link NoticeTypeEnum} + */ + private Integer type; + /** + * 公告内容 + */ + private String content; + /** + * 公告状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/notify/NotifyMessageDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/notify/NotifyMessageDO.java new file mode 100644 index 00000000..eb72e027 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/notify/NotifyMessageDO.java @@ -0,0 +1,101 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailTemplateDO; +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.*; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Map; + +/** + * 站内信 DO + * + * @author xrcoder + */ +@TableName(value = "system_notify_message", autoResultMap = true) +@KeySequence("system_notify_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NotifyMessageDO extends BaseDO { + + /** + * 站内信编号,自增 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 字段、或者 AdminUserDO 的 id 字段 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + // ========= 模板相关字段 ========= + + /** + * 模版编号 + * + * 关联 {@link NotifyTemplateDO#getId()} + */ + private Long templateId; + /** + * 模版编码 + * + * 关联 {@link NotifyTemplateDO#getCode()} + */ + private String templateCode; + /** + * 模版类型 + * + * 冗余 {@link NotifyTemplateDO#getType()} + */ + private Integer templateType; + /** + * 模版发送人名称 + * + * 冗余 {@link NotifyTemplateDO#getNickname()} + */ + private String templateNickname; + /** + * 模版内容 + * + * 基于 {@link NotifyTemplateDO#getContent()} 格式化后的内容 + */ + private String templateContent; + /** + * 模版参数 + * + * 基于 {@link NotifyTemplateDO#getParams()} 输入后的参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map templateParams; + + // ========= 读取相关字段 ========= + + /** + * 是否已读 + */ + private Boolean readStatus; + /** + * 阅读时间 + */ + private LocalDateTime readTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/notify/NotifyTemplateDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/notify/NotifyTemplateDO.java new file mode 100644 index 00000000..2142b837 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/notify/NotifyTemplateDO.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.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.*; + +import java.util.List; + +/** + * 站内信模版 DO + * + * @author xrcoder + */ +@TableName(value = "system_notify_template", autoResultMap = true) +@KeySequence("system_notify_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NotifyTemplateDO extends BaseDO { + + /** + * ID + */ + @TableId + private Long id; + /** + * 模版名称 + */ + private String name; + /** + * 模版编码 + */ + private String code; + /** + * 模版类型 + * + * 对应 system_notify_template_type 字典 + */ + private Integer type; + /** + * 发送人名称 + */ + private String nickname; + /** + * 模版内容 + */ + private String content; + /** + * 参数数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List params; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java new file mode 100644 index 00000000..bdf7e7ec --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java @@ -0,0 +1,69 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.tenant.core.db.TenantBaseDO; +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.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * OAuth2 访问令牌 DO + * + * 如下字段,暂时未使用,暂时不支持: + * user_name、authentication(用户信息) + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_access_token", autoResultMap = true) +@KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2AccessTokenDO extends TenantBaseDO { + + /** + * 编号,数据库递增 + */ + @TableId + private Long id; + /** + * 访问令牌 + */ + private String accessToken; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private String clientId; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java new file mode 100644 index 00000000..83f34e9f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java @@ -0,0 +1,63 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.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.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * OAuth2 批准 DO + * + * 用户在 sso.vue 界面时,记录接受的 scope 列表 + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_approve", autoResultMap = true) +@KeySequence("system_oauth2_approve_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2ApproveDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private String clientId; + /** + * 授权范围 + */ + private String scope; + /** + * 是否接受 + * + * true - 接受 + * false - 拒绝 + */ + private Boolean approved; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java new file mode 100644 index 00000000..75079da5 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java @@ -0,0 +1,107 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.oauth2.OAuth2GrantTypeEnum; +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.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * OAuth2 客户端 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_client", autoResultMap = true) +@KeySequence("system_oauth2_client_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2ClientDO extends BaseDO { + + /** + * 编号,数据库自增 + * + * 由于 SQL Server 在存储 String 主键有点问题,所以暂时使用 Long 类型 + */ + @TableId + private Long id; + /** + * 客户端编号 + */ + private String clientId; + /** + * 客户端密钥 + */ + private String secret; + /** + * 应用名 + */ + private String name; + /** + * 应用图标 + */ + private String logo; + /** + * 应用描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 访问令牌的有效期 + */ + private Integer accessTokenValiditySeconds; + /** + * 刷新令牌的有效期 + */ + private Integer refreshTokenValiditySeconds; + /** + * 可重定向的 URI 地址 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List redirectUris; + /** + * 授权类型(模式) + * + * 枚举 {@link OAuth2GrantTypeEnum} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List authorizedGrantTypes; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 自动授权的 Scope + * + * code 授权时,如果 scope 在这个范围内,则自动通过 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List autoApproveScopes; + /** + * 权限 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List authorities; + /** + * 资源 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List resourceIds; + /** + * 附加信息,JSON 格式 + */ + private String additionalInformation; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java new file mode 100644 index 00000000..c4d2c785 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java @@ -0,0 +1,68 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * OAuth2 授权码 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_code", autoResultMap = true) +@KeySequence("system_oauth2_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2CodeDO extends BaseDO { + + /** + * 编号,数据库递增 + */ + private Long id; + /** + * 授权码 + */ + private String code; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getClientId()} + */ + private String clientId; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 重定向地址 + */ + private String redirectUri; + /** + * 状态 + */ + private String state; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java new file mode 100644 index 00000000..4f1691a4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java @@ -0,0 +1,63 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * OAuth2 刷新令牌 + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_refresh_token", autoResultMap = true) +// 由于 Oracle 的 SEQ 的名字长度有限制,所以就先用 system_oauth2_access_token_seq 吧,反正也没啥问题 +@KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class OAuth2RefreshTokenDO extends BaseDO { + + /** + * 编号,数据库字典 + */ + private Long id; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private String clientId; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/MenuDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/MenuDO.java new file mode 100644 index 00000000..de0dff23 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/MenuDO.java @@ -0,0 +1,107 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.MenuTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 菜单 DO + * + * @author ruoyi + */ +@TableName("system_menu") +@KeySequence("system_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class MenuDO extends BaseDO { + + /** + * 菜单编号 - 根节点 + */ + public static final Long ID_ROOT = 0L; + + /** + * 菜单编号 + */ + @TableId + private Long id; + /** + * 菜单名称 + */ + private String name; + /** + * 权限标识 + * + * 一般格式为:${系统}:${模块}:${操作} + * 例如说:system:admin:add,即 system 服务的添加管理员。 + * + * 当我们把该 MenuDO 赋予给角色后,意味着该角色有该资源: + * - 对于后端,配合 @PreAuthorize 注解,配置 API 接口需要该权限,从而对 API 接口进行权限控制。 + * - 对于前端,配合前端标签,配置按钮是否展示,避免用户没有该权限时,结果可以看到该操作。 + */ + private String permission; + /** + * 菜单类型 + * + * 枚举 {@link MenuTypeEnum} + */ + private Integer type; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 父菜单ID + */ + private Long parentId; + /** + * 路由地址 + * + * 如果 path 为 http(s) 时,则它是外链 + */ + private String path; + /** + * 菜单图标 + */ + private String icon; + /** + * 组件路径 + */ + private String component; + /** + * 组件名 + */ + private String componentName; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 是否可见 + * + * 只有菜单、目录使用 + * 当设置为 true 时,该菜单不会展示在侧边栏,但是路由还是存在。例如说,一些独立的编辑页面 /edit/1024 等等 + */ + private Boolean visible; + /** + * 是否缓存 + * + * 只有菜单、目录使用,否使用 Vue 路由的 keep-alive 特性 + * 注意:如果开启缓存,则必须填写 {@link #componentName} 属性,否则无法缓存 + */ + private Boolean keepAlive; + /** + * 是否总是显示 + * + * 如果为 false 时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单 + */ + private Boolean alwaysShow; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/RoleDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/RoleDO.java new file mode 100644 index 00000000..8f3b7505 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/RoleDO.java @@ -0,0 +1,78 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.type.JsonLongSetTypeHandler; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.DataScopeEnum; +import com.chanko.yunxi.mes.heli.framework.tenant.core.db.TenantBaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.RoleTypeEnum; +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 lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Set; + +/** + * 角色 DO + * + * @author ruoyi + */ +@TableName(value = "system_role", autoResultMap = true) +@KeySequence("system_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleDO extends TenantBaseDO { + + /** + * 角色ID + */ + @TableId + private Long id; + /** + * 角色名称 + */ + private String name; + /** + * 角色标识 + * + * 枚举 + */ + private String code; + /** + * 角色排序 + */ + private Integer sort; + /** + * 角色状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 角色类型 + * + * 枚举 {@link RoleTypeEnum} + */ + private Integer type; + /** + * 备注 + */ + private String remark; + + /** + * 数据范围 + * + * 枚举 {@link DataScopeEnum} + */ + private Integer dataScope; + /** + * 数据范围(指定部门数组) + * + * 适用于 {@link #dataScope} 的值为 {@link DataScopeEnum#DEPT_CUSTOM} 时 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set dataScopeDeptIds; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/RoleMenuDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/RoleMenuDO.java new file mode 100644 index 00000000..e0834f14 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/RoleMenuDO.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission; + +import com.chanko.yunxi.mes.heli.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 角色和菜单关联 + * + * @author ruoyi + */ +@TableName("system_role_menu") +@KeySequence("system_role_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleMenuDO extends TenantBaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 角色ID + */ + private Long roleId; + /** + * 菜单ID + */ + private Long menuId; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/UserRoleDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/UserRoleDO.java new file mode 100644 index 00000000..773d481d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/permission/UserRoleDO.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission; + +import com.chanko.yunxi.mes.heli.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.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户和角色关联 + * + * @author ruoyi + */ +@TableName("system_user_role") +@KeySequence("system_user_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class UserRoleDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 用户 ID + */ + private Long userId; + /** + * 角色 ID + */ + private Long roleId; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java new file mode 100644 index 00000000..94196bd1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java @@ -0,0 +1,58 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sensitiveword; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.type.StringListTypeHandler; +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 lombok.*; + +import java.util.List; + +/** + * 敏感词 DO + * + * @author 永不言败 + */ +@TableName(value = "system_sensitive_word", autoResultMap = true) +@KeySequence("system_sensitive_word_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SensitiveWordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 敏感词 + */ + private String name; + /** + * 描述 + */ + private String description; + /** + * 标签数组 + * + * 用于实现不同的业务场景下,需要使用不同标签的敏感词。 + * 例如说,tag 有短信、论坛两种,敏感词 "推广" 在短信下是敏感词,在论坛下不是敏感词。 + * 此时,我们会存储一条敏感词记录,它的 name 为"推广",tag 为短信。 + */ + @TableField(typeHandler = StringListTypeHandler.class) + private List tags; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsChannelDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsChannelDO.java new file mode 100644 index 00000000..2b867cc9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsChannelDO.java @@ -0,0 +1,62 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.sms.core.enums.SmsChannelEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 短信渠道 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "system_sms_channel", autoResultMap = true) +@KeySequence("system_sms_channel_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelDO extends BaseDO { + + /** + * 渠道编号 + */ + private Long id; + /** + * 短信签名 + */ + private String signature; + /** + * 渠道编码 + * + * 枚举 {@link SmsChannelEnum} + */ + private String code; + /** + * 启用状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 短信 API 的账号 + */ + private String apiKey; + /** + * 短信 API 的密钥 + */ + private String apiSecret; + /** + * 短信发送回调 URL + */ + private String callbackUrl; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsCodeDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsCodeDO.java new file mode 100644 index 00000000..95fbf2f8 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsCodeDO.java @@ -0,0 +1,65 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 手机验证码 DO + * + * idx_mobile 索引:基于 {@link #mobile} 字段 + * + * @author 芋道源码 + */ +@TableName("system_sms_code") +@KeySequence("system_sms_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SmsCodeDO extends BaseDO { + + /** + * 编号 + */ + private Long id; + /** + * 手机号 + */ + private String mobile; + /** + * 验证码 + */ + private String code; + /** + * 发送场景 + * + * 枚举 {@link SmsCodeDO} + */ + private Integer scene; + /** + * 创建 IP + */ + private String createIp; + /** + * 今日发送的第几条 + */ + private Integer todayIndex; + /** + * 是否使用 + */ + private Boolean used; + /** + * 使用时间 + */ + private LocalDateTime usedTime; + /** + * 使用 IP + */ + private String usedIp; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsLogDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsLogDO.java new file mode 100644 index 00000000..d3fbd2cd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsLogDO.java @@ -0,0 +1,161 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsReceiveStatusEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsSendStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 短信日志 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "system_sms_log", autoResultMap = true) +@KeySequence("system_sms_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SmsLogDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + // ========= 渠道相关字段 ========= + + /** + * 短信渠道编号 + * + * 关联 {@link SmsChannelDO#getId()} + */ + private Long channelId; + /** + * 短信渠道编码 + * + * 冗余 {@link SmsChannelDO#getCode()} + */ + private String channelCode; + + // ========= 模板相关字段 ========= + + /** + * 模板编号 + * + * 关联 {@link SmsTemplateDO#getId()} + */ + private Long templateId; + /** + * 模板编码 + * + * 冗余 {@link SmsTemplateDO#getCode()} + */ + private String templateCode; + /** + * 短信类型 + * + * 冗余 {@link SmsTemplateDO#getType()} + */ + private Integer templateType; + /** + * 基于 {@link SmsTemplateDO#getContent()} 格式化后的内容 + */ + private String templateContent; + /** + * 基于 {@link SmsTemplateDO#getParams()} 输入后的参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map templateParams; + /** + * 短信 API 的模板编号 + * + * 冗余 {@link SmsTemplateDO#getApiTemplateId()} + */ + private String apiTemplateId; + + // ========= 手机相关字段 ========= + + /** + * 手机号 + */ + private String mobile; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + // ========= 发送相关字段 ========= + + /** + * 发送状态 + * + * 枚举 {@link SmsSendStatusEnum} + */ + private Integer sendStatus; + /** + * 发送时间 + */ + private LocalDateTime sendTime; + /** + * 短信 API 发送结果的编码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiSendCode; + /** + * 短信 API 发送失败的提示 + */ + private String apiSendMsg; + /** + * 短信 API 发送返回的唯一请求 ID + * + * 用于和短信 API 进行定位于排错 + */ + private String apiRequestId; + /** + * 短信 API 发送返回的序号 + * + * 用于和短信 API 平台的发送记录关联 + */ + private String apiSerialNo; + + // ========= 接收相关字段 ========= + + /** + * 接收状态 + * + * 枚举 {@link SmsReceiveStatusEnum} + */ + private Integer receiveStatus; + /** + * 接收时间 + */ + private LocalDateTime receiveTime; + /** + * 短信 API 接收结果的编码 + */ + private String apiReceiveCode; + /** + * 短信 API 接收结果的提示 + */ + private String apiReceiveMsg; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsTemplateDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsTemplateDO.java new file mode 100644 index 00000000..6b15afe8 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/sms/SmsTemplateDO.java @@ -0,0 +1,91 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms; + +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsTemplateTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +/** + * 短信模板 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "system_sms_template", autoResultMap = true) +@KeySequence("system_sms_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplateDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + // ========= 模板相关字段 ========= + + /** + * 短信类型 + * + * 枚举 {@link SmsTemplateTypeEnum} + */ + private Integer type; + /** + * 启用状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 模板编码,保证唯一 + */ + private String code; + /** + * 模板名称 + */ + private String name; + /** + * 模板内容 + * + * 内容的参数,使用 {} 包括,例如说 {name} + */ + private String content; + /** + * 参数数组(自动根据内容生成) + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List params; + /** + * 备注 + */ + private String remark; + /** + * 短信 API 的模板编号 + */ + private String apiTemplateId; + + // ========= 渠道相关字段 ========= + + /** + * 短信渠道编号 + * + * 关联 {@link SmsChannelDO#getId()} + */ + private Long channelId; + /** + * 短信渠道编码 + * + * 冗余 {@link SmsChannelDO#getCode()} + */ + private String channelCode; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/social/SocialClientDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/social/SocialClientDO.java new file mode 100644 index 00000000..2a2123da --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/social/SocialClientDO.java @@ -0,0 +1,76 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.tenant.core.db.TenantBaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.xingyuv.jushauth.config.AuthConfig; +import lombok.*; + +/** + * 社交客户端 DO + * + * 对应 {@link AuthConfig} 配置,满足不同租户,有自己的客户端配置,实现社交(三方)登录 + * + * @author 芋道源码 + */ +@TableName(value = "system_social_client", autoResultMap = true) +@KeySequence("system_social_client_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialClientDO extends TenantBaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + /** + * 应用名 + */ + private String name; + /** + * 社交类型 + * + * 枚举 {@link SocialTypeEnum} + */ + private Integer socialType; + /** + * 用户类型 + * + * 目的:不同用户类型,对应不同的小程序,需要自己的配置 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + /** + * 客户端 id + */ + private String clientId; + /** + * 客户端 Secret + */ + private String clientSecret; + + /** + * 代理编号 + * + * 目前只有部分“社交类型”在使用: + * 1. 企业微信:对应授权方的网页应用 ID + */ + private String agentId; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/social/SocialUserBindDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/social/SocialUserBindDO.java new file mode 100644 index 00000000..4ee66977 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/social/SocialUserBindDO.java @@ -0,0 +1,56 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social; + +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.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.*; + +/** + * 社交用户的绑定 + * 即 {@link SocialUserDO} 与 UserDO 的关联表 + * + * @author 芋道源码 + */ +@TableName(value = "system_social_user_bind", autoResultMap = true) +@KeySequence("system_social_user_bind_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserBindDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 关联的用户编号 + * + * 关联 UserDO 的编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + /** + * 社交平台的用户编号 + * + * 关联 {@link SocialUserDO#getId()} + */ + private Long socialUserId; + /** + * 社交平台的类型 + * + * 冗余 {@link SocialUserDO#getType()} + */ + private Integer socialType; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/social/SocialUserDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/social/SocialUserDO.java new file mode 100644 index 00000000..835e4f3e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/social/SocialUserDO.java @@ -0,0 +1,73 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 社交(三方)用户 + * + * @author weir + */ +@TableName(value = "system_social_user", autoResultMap = true) +@KeySequence("system_social_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 社交平台的类型 + * + * 枚举 {@link SocialTypeEnum} + */ + private Integer type; + + /** + * 社交 openid + */ + private String openid; + /** + * 社交 token + */ + private String token; + /** + * 原始 Token 数据,一般是 JSON 格式 + */ + private String rawTokenInfo; + + /** + * 用户昵称 + */ + private String nickname; + /** + * 用户头像 + */ + private String avatar; + /** + * 原始用户数据,一般是 JSON 格式 + */ + private String rawUserInfo; + + /** + * 最后一次的认证 code + */ + private String code; + /** + * 最后一次的认证 state + */ + private String state; + +} + + diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/tenant/TenantDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/tenant/TenantDO.java new file mode 100644 index 00000000..415325bf --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/tenant/TenantDO.java @@ -0,0 +1,80 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 租户 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_tenant", autoResultMap = true) +@KeySequence("system_tenant_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TenantDO extends BaseDO { + + /** + * 套餐编号 - 系统 + */ + public static final Long PACKAGE_ID_SYSTEM = 0L; + + /** + * 租户编号,自增 + */ + private Long id; + /** + * 租户名,唯一 + */ + private String name; + /** + * 联系人的用户编号 + * + * 关联 {@link AdminUserDO#getId()} + */ + private Long contactUserId; + /** + * 联系人 + */ + private String contactName; + /** + * 联系手机 + */ + private String contactMobile; + /** + * 租户状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 绑定域名 + */ + private String website; + /** + * 租户套餐编号 + * + * 关联 {@link TenantPackageDO#getId()} + * 特殊逻辑:系统内置租户,不使用套餐,暂时使用 {@link #PACKAGE_ID_SYSTEM} 标识 + */ + private Long packageId; + /** + * 过期时间 + */ + private LocalDateTime expireTime; + /** + * 账号数量 + */ + private Integer accountCount; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/tenant/TenantPackageDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/tenant/TenantPackageDO.java new file mode 100644 index 00000000..d7448f5d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/tenant/TenantPackageDO.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.type.JsonLongSetTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Set; + +/** + * 租户套餐 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_tenant_package", autoResultMap = true) +@KeySequence("system_tenant_package_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TenantPackageDO extends BaseDO { + + /** + * 套餐编号,自增 + */ + private Long id; + /** + * 套餐名,唯一 + */ + private String name; + /** + * 租户套餐状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 关联的菜单编号 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set menuIds; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/user/AdminUserDO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/user/AdminUserDO.java new file mode 100644 index 00000000..c391c247 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/dataobject/user/AdminUserDO.java @@ -0,0 +1,96 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.type.JsonLongSetTypeHandler; +import com.chanko.yunxi.mes.heli.framework.tenant.core.db.TenantBaseDO; +import com.chanko.yunxi.mes.heli.module.system.enums.common.SexEnum; +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 lombok.*; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.time.LocalDateTime; +import java.util.Set; + +/** + * 管理后台的用户 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_users", autoResultMap = true) // 由于 SQL Server 的 system_user 是关键字,所以使用 system_users +@KeySequence("system_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AdminUserDO extends TenantBaseDO { + + /** + * 用户ID + */ + @TableId + private Long id; + /** + * 用户账号 + */ + private String username; + /** + * 加密后的密码 + * + * 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐 + */ + private String password; + /** + * 用户昵称 + */ + private String nickname; + /** + * 备注 + */ + private String remark; + /** + * 部门 ID + */ + private Long deptId; + /** + * 岗位编号数组 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set postIds; + /** + * 用户邮箱 + */ + private String email; + /** + * 手机号码 + */ + private String mobile; + /** + * 用户性别 + * + * 枚举类 {@link SexEnum} + */ + private Integer sex; + /** + * 用户头像 + */ + private String avatar; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 最后登录IP + */ + private String loginIp; + /** + * 最后登录时间 + */ + private LocalDateTime loginDate; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dept/DeptMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dept/DeptMapper.java new file mode 100644 index 00000000..3f7c9581 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dept/DeptMapper.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.dept; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DeptMapper extends BaseMapperX { + + default List selectList(DeptListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DeptDO::getName, reqVO.getName()) + .eqIfPresent(DeptDO::getStatus, reqVO.getStatus())); + } + + default DeptDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(DeptDO::getParentId, parentId, DeptDO::getName, name); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(DeptDO::getParentId, parentId); + } + + default List selectListByParentId(Collection parentIds) { + return selectList(DeptDO::getParentId, parentIds); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dept/PostMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dept/PostMapper.java new file mode 100644 index 00000000..2f52af5b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dept/PostMapper.java @@ -0,0 +1,38 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.dept; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.PostDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PostMapper extends BaseMapperX { + + default List selectList(Collection ids, Collection statuses) { + return selectList(new LambdaQueryWrapperX() + .inIfPresent(PostDO::getId, ids) + .inIfPresent(PostDO::getStatus, statuses)); + } + + default PageResult selectPage(PostPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(PostDO::getCode, reqVO.getCode()) + .likeIfPresent(PostDO::getName, reqVO.getName()) + .eqIfPresent(PostDO::getStatus, reqVO.getStatus()) + .orderByDesc(PostDO::getId)); + } + + default PostDO selectByName(String name) { + return selectOne(PostDO::getName, name); + } + + default PostDO selectByCode(String code) { + return selectOne(PostDO::getCode, code); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dept/UserPostMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dept/UserPostMapper.java new file mode 100644 index 00000000..457f3ebf --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dept/UserPostMapper.java @@ -0,0 +1,32 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.dept; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.UserPostDO; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface UserPostMapper extends BaseMapperX { + + default List selectListByUserId(Long userId) { + return selectList(UserPostDO::getUserId, userId); + } + + default void deleteByUserIdAndPostId(Long userId, Collection postIds) { + delete(new LambdaQueryWrapperX() + .eq(UserPostDO::getUserId, userId) + .in(UserPostDO::getPostId, postIds)); + } + + default List selectListByPostIds(Collection postIds) { + return selectList(UserPostDO::getPostId, postIds); + } + + default void deleteByUserId(Long userId) { + delete(Wrappers.lambdaUpdate(UserPostDO.class).eq(UserPostDO::getUserId, userId)); + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dict/DictDataMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dict/DictDataMapper.java new file mode 100644 index 00000000..c649abe5 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dict/DictDataMapper.java @@ -0,0 +1,49 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.dict; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictDataDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DictDataMapper extends BaseMapperX { + + default DictDataDO selectByDictTypeAndValue(String dictType, String value) { + return selectOne(DictDataDO::getDictType, dictType, DictDataDO::getValue, value); + } + + default DictDataDO selectByDictTypeAndLabel(String dictType, String label) { + return selectOne(DictDataDO::getDictType, dictType, DictDataDO::getLabel, label); + } + + default List selectByDictTypeAndValues(String dictType, Collection values) { + return selectList(new LambdaQueryWrapper().eq(DictDataDO::getDictType, dictType) + .in(DictDataDO::getValue, values)); + } + + default long selectCountByDictType(String dictType) { + return selectCount(DictDataDO::getDictType, dictType); + } + + default PageResult selectPage(DictDataPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DictDataDO::getLabel, reqVO.getLabel()) + .eqIfPresent(DictDataDO::getDictType, reqVO.getDictType()) + .eqIfPresent(DictDataDO::getStatus, reqVO.getStatus()) + .orderByDesc(Arrays.asList(DictDataDO::getDictType, DictDataDO::getSort))); + } + + default List selectListByStatusAndDictType(Integer status, String dictType) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(DictDataDO::getStatus, status) + .eqIfPresent(DictDataDO::getDictType, dictType)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dict/DictTypeMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dict/DictTypeMapper.java new file mode 100644 index 00000000..7def506e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/dict/DictTypeMapper.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.dict; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictTypeDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +import java.time.LocalDateTime; + +@Mapper +public interface DictTypeMapper extends BaseMapperX { + + default PageResult selectPage(DictTypePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DictTypeDO::getName, reqVO.getName()) + .likeIfPresent(DictTypeDO::getType, reqVO.getType()) + .eqIfPresent(DictTypeDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DictTypeDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(DictTypeDO::getId)); + } + + default DictTypeDO selectByType(String type) { + return selectOne(DictTypeDO::getType, type); + } + + default DictTypeDO selectByName(String name) { + return selectOne(DictTypeDO::getName, name); + } + + @Update("UPDATE system_dict_type SET deleted = 1, deleted_time = #{deletedTime} WHERE id = #{id}") + void updateToDelete(@Param("id") Long id, @Param("deletedTime") LocalDateTime deletedTime); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/errorcode/ErrorCodeMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/errorcode/ErrorCodeMapper.java new file mode 100644 index 00000000..e0e3a6ee --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/errorcode/ErrorCodeMapper.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.errorcode; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface ErrorCodeMapper extends BaseMapperX { + + default PageResult selectPage(ErrorCodePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(ErrorCodeDO::getType, reqVO.getType()) + .likeIfPresent(ErrorCodeDO::getApplicationName, reqVO.getApplicationName()) + .eqIfPresent(ErrorCodeDO::getCode, reqVO.getCode()) + .likeIfPresent(ErrorCodeDO::getMessage, reqVO.getMessage()) + .betweenIfPresent(ErrorCodeDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(ErrorCodeDO::getCode)); + } + + default List selectListByCodes(Collection codes) { + return selectList(ErrorCodeDO::getCode, codes); + } + + default ErrorCodeDO selectByCode(Integer code) { + return selectOne(ErrorCodeDO::getCode, code); + } + + default List selectListByApplicationNameAndUpdateTimeGt(String applicationName, LocalDateTime minUpdateTime) { + return selectList(new LambdaQueryWrapperX().eq(ErrorCodeDO::getApplicationName, applicationName) + .gtIfPresent(ErrorCodeDO::getUpdateTime, minUpdateTime)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/logger/LoginLogMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/logger/LoginLogMapper.java new file mode 100644 index 00000000..e58e7720 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/logger/LoginLogMapper.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.LoginLogDO; +import com.chanko.yunxi.mes.heli.module.system.enums.logger.LoginResultEnum; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface LoginLogMapper extends BaseMapperX { + + default PageResult selectPage(LoginLogPageReqVO reqVO) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(LoginLogDO::getUserIp, reqVO.getUserIp()) + .likeIfPresent(LoginLogDO::getUsername, reqVO.getUsername()) + .betweenIfPresent(LoginLogDO::getCreateTime, reqVO.getCreateTime()); + if (Boolean.TRUE.equals(reqVO.getStatus())) { + query.eq(LoginLogDO::getResult, LoginResultEnum.SUCCESS.getResult()); + } else if (Boolean.FALSE.equals(reqVO.getStatus())) { + query.gt(LoginLogDO::getResult, LoginResultEnum.SUCCESS.getResult()); + } + query.orderByDesc(LoginLogDO::getId); // 降序 + return selectPage(reqVO, query); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/logger/OperateLogMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/logger/OperateLogMapper.java new file mode 100644 index 00000000..f450cddb --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/logger/OperateLogMapper.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.logger; + +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.OperateLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; + +@Mapper +public interface OperateLogMapper extends BaseMapperX { + + default PageResult selectPage(OperateLogPageReqVO reqVO, Collection userIds) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(OperateLogDO::getModule, reqVO.getModule()) + .inIfPresent(OperateLogDO::getUserId, userIds) + .eqIfPresent(OperateLogDO::getType, reqVO.getType()) + .betweenIfPresent(OperateLogDO::getStartTime, reqVO.getStartTime()); + if (Boolean.TRUE.equals(reqVO.getSuccess())) { + query.eq(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } else if (Boolean.FALSE.equals(reqVO.getSuccess())) { + query.gt(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + query.orderByDesc(OperateLogDO::getId); // 降序 + return selectPage(reqVO, query); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/mail/MailAccountMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/mail/MailAccountMapper.java new file mode 100644 index 00000000..88b2b6bc --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/mail/MailAccountMapper.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.QueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailAccountDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MailAccountMapper extends BaseMapperX { + + default PageResult selectPage(MailAccountPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(MailAccountDO::getMail, pageReqVO.getMail()) + .likeIfPresent(MailAccountDO::getUsername , pageReqVO.getUsername())); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/mail/MailLogMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/mail/MailLogMapper.java new file mode 100644 index 00000000..4444cdbd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/mail/MailLogMapper.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailLogDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MailLogMapper extends BaseMapperX { + + default PageResult selectPage(MailLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MailLogDO::getUserId, reqVO.getUserId()) + .eqIfPresent(MailLogDO::getUserType, reqVO.getUserType()) + .likeIfPresent(MailLogDO::getToMail, reqVO.getToMail()) + .eqIfPresent(MailLogDO::getAccountId, reqVO.getAccountId()) + .eqIfPresent(MailLogDO::getTemplateId, reqVO.getTemplateId()) + .eqIfPresent(MailLogDO::getSendStatus, reqVO.getSendStatus()) + .betweenIfPresent(MailLogDO::getSendTime, reqVO.getSendTime()) + .orderByDesc(MailLogDO::getId)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/mail/MailTemplateMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/mail/MailTemplateMapper.java new file mode 100644 index 00000000..b0e5ee5f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/mail/MailTemplateMapper.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.QueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsTemplateDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; + +@Mapper +public interface MailTemplateMapper extends BaseMapperX { + + default PageResult selectPage(MailTemplatePageReqVO pageReqVO){ + return selectPage(pageReqVO , new LambdaQueryWrapperX() + .eqIfPresent(MailTemplateDO::getStatus, pageReqVO.getStatus()) + .likeIfPresent(MailTemplateDO::getCode, pageReqVO.getCode()) + .likeIfPresent(MailTemplateDO::getName, pageReqVO.getName()) + .eqIfPresent(MailTemplateDO::getAccountId, pageReqVO.getAccountId()) + .betweenIfPresent(MailTemplateDO::getCreateTime, pageReqVO.getCreateTime())); + } + + default Long selectCountByAccountId(Long accountId) { + return selectCount(MailTemplateDO::getAccountId, accountId); + } + + default MailTemplateDO selectByCode(String code) { + return selectOne(MailTemplateDO::getCode, code); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/notice/NoticeMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/notice/NoticeMapper.java new file mode 100644 index 00000000..ddbf3102 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/notice/NoticeMapper.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.notice; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo.NoticePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notice.NoticeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface NoticeMapper extends BaseMapperX { + + default PageResult selectPage(NoticePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(NoticeDO::getTitle, reqVO.getTitle()) + .eqIfPresent(NoticeDO::getStatus, reqVO.getStatus()) + .orderByDesc(NoticeDO::getId)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/notify/NotifyMessageMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/notify/NotifyMessageMapper.java new file mode 100644 index 00000000..95c1133b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/notify/NotifyMessageMapper.java @@ -0,0 +1,70 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.notify; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.QueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyMessageDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface NotifyMessageMapper extends BaseMapperX { + + default PageResult selectPage(NotifyMessagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(NotifyMessageDO::getUserId, reqVO.getUserId()) + .eqIfPresent(NotifyMessageDO::getUserType, reqVO.getUserType()) + .likeIfPresent(NotifyMessageDO::getTemplateCode, reqVO.getTemplateCode()) + .eqIfPresent(NotifyMessageDO::getTemplateType, reqVO.getTemplateType()) + .betweenIfPresent(NotifyMessageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(NotifyMessageDO::getId)); + } + + default PageResult selectPage(NotifyMessageMyPageReqVO reqVO, Long userId, Integer userType) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(NotifyMessageDO::getReadStatus, reqVO.getReadStatus()) + .betweenIfPresent(NotifyMessageDO::getCreateTime, reqVO.getCreateTime()) + .eq(NotifyMessageDO::getUserId, userId) + .eq(NotifyMessageDO::getUserType, userType) + .orderByDesc(NotifyMessageDO::getId)); + } + + default int updateListRead(Collection ids, Long userId, Integer userType) { + return update(new NotifyMessageDO().setReadStatus(true).setReadTime(LocalDateTime.now()), + new LambdaQueryWrapperX() + .in(NotifyMessageDO::getId, ids) + .eq(NotifyMessageDO::getUserId, userId) + .eq(NotifyMessageDO::getUserType, userType) + .eq(NotifyMessageDO::getReadStatus, false)); + } + + default int updateListRead(Long userId, Integer userType) { + return update(new NotifyMessageDO().setReadStatus(true).setReadTime(LocalDateTime.now()), + new LambdaQueryWrapperX() + .eq(NotifyMessageDO::getUserId, userId) + .eq(NotifyMessageDO::getUserType, userType) + .eq(NotifyMessageDO::getReadStatus, false)); + } + + default List selectUnreadListByUserIdAndUserType(Long userId, Integer userType, Integer size) { + return selectList(new QueryWrapperX() // 由于要使用 limitN 语句,所以只能用 QueryWrapperX + .eq("user_id", userId) + .eq("user_type", userType) + .eq("read_status", false) + .orderByDesc("id").limitN(size)); + } + + default Long selectUnreadCountByUserIdAndUserType(Long userId, Integer userType) { + return selectCount(new LambdaQueryWrapperX() + .eq(NotifyMessageDO::getReadStatus, false) + .eq(NotifyMessageDO::getUserId, userId) + .eq(NotifyMessageDO::getUserType, userType)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/notify/NotifyTemplateMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/notify/NotifyTemplateMapper.java new file mode 100644 index 00000000..ad236fc7 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/notify/NotifyTemplateMapper.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.notify; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface NotifyTemplateMapper extends BaseMapperX { + + default NotifyTemplateDO selectByCode(String code) { + return selectOne(NotifyTemplateDO::getCode, code); + } + + default PageResult selectPage(NotifyTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(NotifyTemplateDO::getCode, reqVO.getCode()) + .likeIfPresent(NotifyTemplateDO::getName, reqVO.getName()) + .eqIfPresent(NotifyTemplateDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(NotifyTemplateDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(NotifyTemplateDO::getId)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java new file mode 100644 index 00000000..a956171f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface OAuth2AccessTokenMapper extends BaseMapperX { + + default OAuth2AccessTokenDO selectByAccessToken(String accessToken) { + return selectOne(OAuth2AccessTokenDO::getAccessToken, accessToken); + } + + default List selectListByRefreshToken(String refreshToken) { + return selectList(OAuth2AccessTokenDO::getRefreshToken, refreshToken); + } + + default PageResult selectPage(OAuth2AccessTokenPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(OAuth2AccessTokenDO::getUserId, reqVO.getUserId()) + .eqIfPresent(OAuth2AccessTokenDO::getUserType, reqVO.getUserType()) + .likeIfPresent(OAuth2AccessTokenDO::getClientId, reqVO.getClientId()) + .gt(OAuth2AccessTokenDO::getExpiresTime, LocalDateTime.now()) + .orderByDesc(OAuth2AccessTokenDO::getId)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java new file mode 100644 index 00000000..00def0cf --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface OAuth2ApproveMapper extends BaseMapperX { + + default int update(OAuth2ApproveDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(OAuth2ApproveDO::getUserId, updateObj.getUserId()) + .eq(OAuth2ApproveDO::getUserType, updateObj.getUserType()) + .eq(OAuth2ApproveDO::getClientId, updateObj.getClientId()) + .eq(OAuth2ApproveDO::getScope, updateObj.getScope())); + } + + default List selectListByUserIdAndUserTypeAndClientId(Long userId, Integer userType, String clientId) { + return selectList(new LambdaQueryWrapperX() + .eq(OAuth2ApproveDO::getUserId, userId) + .eq(OAuth2ApproveDO::getUserType, userType) + .eq(OAuth2ApproveDO::getClientId, clientId)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java new file mode 100644 index 00000000..30d44539 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import org.apache.ibatis.annotations.Mapper; + + +/** + * OAuth2 客户端 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface OAuth2ClientMapper extends BaseMapperX { + + default PageResult selectPage(OAuth2ClientPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(OAuth2ClientDO::getName, reqVO.getName()) + .eqIfPresent(OAuth2ClientDO::getStatus, reqVO.getStatus()) + .orderByDesc(OAuth2ClientDO::getId)); + } + + default OAuth2ClientDO selectByClientId(String clientId) { + return selectOne(OAuth2ClientDO::getClientId, clientId); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java new file mode 100644 index 00000000..c1f38c97 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java @@ -0,0 +1,14 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OAuth2CodeMapper extends BaseMapperX { + + default OAuth2CodeDO selectByCode(String code) { + return selectOne(OAuth2CodeDO::getCode, code); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java new file mode 100644 index 00000000..ec4c6281 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java @@ -0,0 +1,20 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OAuth2RefreshTokenMapper extends BaseMapperX { + + default int deleteByRefreshToken(String refreshToken) { + return delete(new LambdaQueryWrapperX() + .eq(OAuth2RefreshTokenDO::getRefreshToken, refreshToken)); + } + + default OAuth2RefreshTokenDO selectByRefreshToken(String refreshToken) { + return selectOne(OAuth2RefreshTokenDO::getRefreshToken, refreshToken); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/package-info.java new file mode 100644 index 00000000..0c5582a6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/package-info.java @@ -0,0 +1,9 @@ +/** + * DAL = Data Access Layer 数据访问层 + * 1. data object:数据对象 + * 2. redis:Redis 的 CRUD 操作 + * 3. mysql:MySQL 的 CRUD 操作 + * + * 其中,MySQL 的表以 system_ 作为前缀 + */ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql; diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/MenuMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/MenuMapper.java new file mode 100644 index 00000000..305b82a0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/MenuMapper.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.permission; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MenuMapper extends BaseMapperX { + + default MenuDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(MenuDO::getParentId, parentId, MenuDO::getName, name); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(MenuDO::getParentId, parentId); + } + + default List selectList(MenuListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(MenuDO::getName, reqVO.getName()) + .eqIfPresent(MenuDO::getStatus, reqVO.getStatus())); + } + + default List selectListByPermission(String permission) { + return selectList(MenuDO::getPermission, permission); + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/RoleMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/RoleMapper.java new file mode 100644 index 00000000..b2db28d9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/RoleMapper.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.permission; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.dataobject.BaseDO; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface RoleMapper extends BaseMapperX { + + default PageResult selectPage(RolePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(RoleDO::getName, reqVO.getName()) + .likeIfPresent(RoleDO::getCode, reqVO.getCode()) + .eqIfPresent(RoleDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(RoleDO::getId)); + } + + default RoleDO selectByName(String name) { + return selectOne(RoleDO::getName, name); + } + + default RoleDO selectByCode(String code) { + return selectOne(RoleDO::getCode, code); + } + + default List selectListByStatus(@Nullable Collection statuses) { + return selectList(RoleDO::getStatus, statuses); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/RoleMenuMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/RoleMenuMapper.java new file mode 100644 index 00000000..5dfd034b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/RoleMenuMapper.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.permission; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleMenuDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface RoleMenuMapper extends BaseMapperX { + + default List selectListByRoleId(Long roleId) { + return selectList(RoleMenuDO::getRoleId, roleId); + } + + default List selectListByRoleId(Collection roleIds) { + return selectList(RoleMenuDO::getRoleId, roleIds); + } + + default List selectListByMenuId(Long menuId) { + return selectList(RoleMenuDO::getMenuId, menuId); + } + + default void deleteListByRoleIdAndMenuIds(Long roleId, Collection menuIds) { + delete(new LambdaQueryWrapper() + .eq(RoleMenuDO::getRoleId, roleId) + .in(RoleMenuDO::getMenuId, menuIds)); + } + + default void deleteListByMenuId(Long menuId) { + delete(new LambdaQueryWrapper().eq(RoleMenuDO::getMenuId, menuId)); + } + + default void deleteListByRoleId(Long roleId) { + delete(new LambdaQueryWrapper().eq(RoleMenuDO::getRoleId, roleId)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/UserRoleMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/UserRoleMapper.java new file mode 100644 index 00000000..28f11885 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/permission/UserRoleMapper.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.permission; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.UserRoleDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface UserRoleMapper extends BaseMapperX { + + default List selectListByUserId(Long userId) { + return selectList(UserRoleDO::getUserId, userId); + } + + default void deleteListByUserIdAndRoleIdIds(Long userId, Collection roleIds) { + delete(new LambdaQueryWrapper() + .eq(UserRoleDO::getUserId, userId) + .in(UserRoleDO::getRoleId, roleIds)); + } + + default void deleteListByUserId(Long userId) { + delete(new LambdaQueryWrapper().eq(UserRoleDO::getUserId, userId)); + } + + default void deleteListByRoleId(Long roleId) { + delete(new LambdaQueryWrapper().eq(UserRoleDO::getRoleId, roleId)); + } + + default List selectListByRoleIds(Collection roleIds) { + return selectList(UserRoleDO::getRoleId, roleIds); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java new file mode 100644 index 00000000..ab0d99fb --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.sensitiveword; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; + +/** + * 敏感词 Mapper + * + * @author 永不言败 + */ +@Mapper +public interface SensitiveWordMapper extends BaseMapperX { + + default PageResult selectPage(SensitiveWordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SensitiveWordDO::getName, reqVO.getName()) + .likeIfPresent(SensitiveWordDO::getTags, reqVO.getTag()) + .eqIfPresent(SensitiveWordDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(SensitiveWordDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SensitiveWordDO::getId)); + } + default SensitiveWordDO selectByName(String name) { + return selectOne(SensitiveWordDO::getName, name); + } + + @Select("SELECT COUNT(*) FROM system_sensitive_word WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(LocalDateTime maxTime); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsChannelMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsChannelMapper.java new file mode 100644 index 00000000..6849dc05 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsChannelMapper.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsChannelDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmsChannelMapper extends BaseMapperX { + + default PageResult selectPage(SmsChannelPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SmsChannelDO::getSignature, reqVO.getSignature()) + .eqIfPresent(SmsChannelDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(SmsChannelDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SmsChannelDO::getId)); + } + + default SmsChannelDO selectByCode(String code) { + return selectOne(SmsChannelDO::getCode, code); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsCodeMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsCodeMapper.java new file mode 100644 index 00000000..2e10cf52 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsCodeMapper.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.sms; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.QueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsCodeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmsCodeMapper extends BaseMapperX { + + /** + * 获得手机号的最后一个手机验证码 + * + * @param mobile 手机号 + * @param scene 发送场景,选填 + * @param code 验证码 选填 + * @return 手机验证码 + */ + default SmsCodeDO selectLastByMobile(String mobile, String code, Integer scene) { + return selectOne(new QueryWrapperX() + .eq("mobile", mobile) + .eqIfPresent("scene", scene) + .eqIfPresent("code", code) + .orderByDesc("id") + .limitN(1)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsLogMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsLogMapper.java new file mode 100644 index 00000000..14920b67 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsLogMapper.java @@ -0,0 +1,25 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsLogDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmsLogMapper extends BaseMapperX { + + default PageResult selectPage(SmsLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(SmsLogDO::getChannelId, reqVO.getChannelId()) + .eqIfPresent(SmsLogDO::getTemplateId, reqVO.getTemplateId()) + .likeIfPresent(SmsLogDO::getMobile, reqVO.getMobile()) + .eqIfPresent(SmsLogDO::getSendStatus, reqVO.getSendStatus()) + .betweenIfPresent(SmsLogDO::getSendTime, reqVO.getSendTime()) + .eqIfPresent(SmsLogDO::getReceiveStatus, reqVO.getReceiveStatus()) + .betweenIfPresent(SmsLogDO::getReceiveTime, reqVO.getReceiveTime()) + .orderByDesc(SmsLogDO::getId)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsTemplateMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsTemplateMapper.java new file mode 100644 index 00000000..9c2090e3 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/sms/SmsTemplateMapper.java @@ -0,0 +1,33 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmsTemplateMapper extends BaseMapperX { + + default SmsTemplateDO selectByCode(String code) { + return selectOne(SmsTemplateDO::getCode, code); + } + + default PageResult selectPage(SmsTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(SmsTemplateDO::getType, reqVO.getType()) + .eqIfPresent(SmsTemplateDO::getStatus, reqVO.getStatus()) + .likeIfPresent(SmsTemplateDO::getCode, reqVO.getCode()) + .likeIfPresent(SmsTemplateDO::getContent, reqVO.getContent()) + .likeIfPresent(SmsTemplateDO::getApiTemplateId, reqVO.getApiTemplateId()) + .eqIfPresent(SmsTemplateDO::getChannelId, reqVO.getChannelId()) + .betweenIfPresent(SmsTemplateDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SmsTemplateDO::getId)); + } + + default Long selectCountByChannelId(Long channelId) { + return selectCount(SmsTemplateDO::getChannelId, channelId); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/social/SocialClientMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/social/SocialClientMapper.java new file mode 100644 index 00000000..49587a8d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/social/SocialClientMapper.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.social; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialClientDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SocialClientMapper extends BaseMapperX { + + default SocialClientDO selectBySocialTypeAndUserType(Integer socialType, Integer userType) { + return selectOne(SocialClientDO::getSocialType, socialType, + SocialClientDO::getUserType, userType); + } + + default PageResult selectPage(SocialClientPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SocialClientDO::getName, reqVO.getName()) + .eqIfPresent(SocialClientDO::getSocialType, reqVO.getSocialType()) + .eqIfPresent(SocialClientDO::getUserType, reqVO.getUserType()) + .likeIfPresent(SocialClientDO::getClientId, reqVO.getClientId()) + .eqIfPresent(SocialClientDO::getStatus, reqVO.getStatus()) + .orderByDesc(SocialClientDO::getId)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/social/SocialUserBindMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/social/SocialUserBindMapper.java new file mode 100644 index 00000000..06be8ae6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/social/SocialUserBindMapper.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.social; + +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialUserBindDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SocialUserBindMapper extends BaseMapperX { + + default void deleteByUserTypeAndUserIdAndSocialType(Integer userType, Long userId, Integer socialType) { + delete(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getUserId, userId) + .eq(SocialUserBindDO::getSocialType, socialType)); + } + + default void deleteByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { + delete(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getSocialUserId, socialUserId)); + } + + default SocialUserBindDO selectByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { + return selectOne(SocialUserBindDO::getUserType, userType, + SocialUserBindDO::getSocialUserId, socialUserId); + } + + default List selectListByUserIdAndUserType(Long userId, Integer userType) { + return selectList(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserId, userId) + .eq(SocialUserBindDO::getUserType, userType)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/social/SocialUserMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/social/SocialUserMapper.java new file mode 100644 index 00000000..47a6d274 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/social/SocialUserMapper.java @@ -0,0 +1,36 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.social; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialUserDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SocialUserMapper extends BaseMapperX { + + default SocialUserDO selectByTypeAndCodeAnState(Integer type, String code, String state) { + return selectOne(new LambdaQueryWrapper() + .eq(SocialUserDO::getType, type) + .eq(SocialUserDO::getCode, code) + .eq(SocialUserDO::getState, state)); + } + + default SocialUserDO selectByTypeAndOpenid(Integer type, String openid) { + return selectOne(new LambdaQueryWrapper() + .eq(SocialUserDO::getType, type) + .eq(SocialUserDO::getOpenid, openid)); + } + + default PageResult selectPage(SocialUserPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(SocialUserDO::getType, reqVO.getType()) + .likeIfPresent(SocialUserDO::getNickname, reqVO.getNickname()) + .likeIfPresent(SocialUserDO::getOpenid, reqVO.getOpenid()) + .betweenIfPresent(SocialUserDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SocialUserDO::getId)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/tenant/TenantMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/tenant/TenantMapper.java new file mode 100644 index 00000000..5ddd10bd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/tenant/TenantMapper.java @@ -0,0 +1,46 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.tenant; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 租户 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface TenantMapper extends BaseMapperX { + + default PageResult selectPage(TenantPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TenantDO::getName, reqVO.getName()) + .likeIfPresent(TenantDO::getContactName, reqVO.getContactName()) + .likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile()) + .eqIfPresent(TenantDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(TenantDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TenantDO::getId)); + } + + default TenantDO selectByName(String name) { + return selectOne(TenantDO::getName, name); + } + + default TenantDO selectByWebsite(String website) { + return selectOne(TenantDO::getWebsite, website); + } + + default Long selectCountByPackageId(Long packageId) { + return selectCount(TenantDO::getPackageId, packageId); + } + + default List selectListByPackageId(Long packageId) { + return selectList(TenantDO::getPackageId, packageId); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/tenant/TenantPackageMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/tenant/TenantPackageMapper.java new file mode 100644 index 00000000..966451f5 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/tenant/TenantPackageMapper.java @@ -0,0 +1,32 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.tenant; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantPackageDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 租户套餐 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface TenantPackageMapper extends BaseMapperX { + + default PageResult selectPage(TenantPackagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TenantPackageDO::getName, reqVO.getName()) + .eqIfPresent(TenantPackageDO::getStatus, reqVO.getStatus()) + .likeIfPresent(TenantPackageDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(TenantPackageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TenantPackageDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(TenantPackageDO::getStatus, status); + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/user/AdminUserMapper.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/user/AdminUserMapper.java new file mode 100644 index 00000000..d2767bc8 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/mysql/user/AdminUserMapper.java @@ -0,0 +1,50 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.mysql.user; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.mapper.BaseMapperX; +import com.chanko.yunxi.mes.heli.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.UserPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface AdminUserMapper extends BaseMapperX { + + default AdminUserDO selectByUsername(String username) { + return selectOne(AdminUserDO::getUsername, username); + } + + default AdminUserDO selectByEmail(String email) { + return selectOne(AdminUserDO::getEmail, email); + } + + default AdminUserDO selectByMobile(String mobile) { + return selectOne(AdminUserDO::getMobile, mobile); + } + + default PageResult selectPage(UserPageReqVO reqVO, Collection deptIds) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()) + .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()) + .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime()) + .inIfPresent(AdminUserDO::getDeptId, deptIds) + .orderByDesc(AdminUserDO::getId)); + } + + default List selectListByNickname(String nickname) { + return selectList(new LambdaQueryWrapperX().like(AdminUserDO::getNickname, nickname)); + } + + default List selectListByStatus(Integer status) { + return selectList(AdminUserDO::getStatus, status); + } + + default List selectListByDeptIds(Collection deptIds) { + return selectList(AdminUserDO::getDeptId, deptIds); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/redis/RedisKeyConstants.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/redis/RedisKeyConstants.java new file mode 100644 index 00000000..e655a7b0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/redis/RedisKeyConstants.java @@ -0,0 +1,101 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.redis; + +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; + +/** + * System Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 指定部门的所有子部门编号数组的缓存 + *

+ * KEY 格式:dept_children_ids:{id} + * VALUE 数据类型:String 子部门编号集合 + */ + String DEPT_CHILDREN_ID_LIST = "dept_children_ids"; + + /** + * 角色的缓存 + *

+ * KEY 格式:role:{id} + * VALUE 数据类型:String 角色信息 + */ + String ROLE = "role"; + + /** + * 用户拥有的角色编号的缓存 + *

+ * KEY 格式:user_role_ids:{userId} + * VALUE 数据类型:String 角色编号集合 + */ + String USER_ROLE_ID_LIST = "user_role_ids"; + + /** + * 拥有指定菜单的角色编号的缓存 + *

+ * KEY 格式:user_role_ids:{menuId} + * VALUE 数据类型:String 角色编号集合 + */ + String MENU_ROLE_ID_LIST = "menu_role_ids"; + + /** + * 拥有权限对应的菜单编号数组的缓存 + *

+ * KEY 格式:permission_menu_ids:{permission} + * VALUE 数据类型:String 菜单编号数组 + */ + String PERMISSION_MENU_ID_LIST = "permission_menu_ids"; + + /** + * OAuth2 客户端的缓存 + *

+ * KEY 格式:user:{id} + * VALUE 数据类型:String 客户端信息 + */ + String OAUTH_CLIENT = "oauth_client"; + + /** + * 访问令牌的缓存 + *

+ * KEY 格式:oauth2_access_token:{token} + * VALUE 数据类型:String 访问令牌信息 {@link OAuth2AccessTokenDO} + *

+ * 由于动态过期时间,使用 RedisTemplate 操作 + */ + String OAUTH2_ACCESS_TOKEN = "oauth2_access_token:%s"; + + /** + * 站内信模版的缓存 + *

+ * KEY 格式:notify_template:{code} + * VALUE 数据格式:String 模版信息 + */ + String NOTIFY_TEMPLATE = "notify_template"; + + /** + * 邮件账号的缓存 + *

+ * KEY 格式:sms_template:{id} + * VALUE 数据格式:String 账号信息 + */ + String MAIL_ACCOUNT = "mail_account"; + + /** + * 邮件模版的缓存 + *

+ * KEY 格式:mail_template:{code} + * VALUE 数据格式:String 模版信息 + */ + String MAIL_TEMPLATE = "mail_template"; + + /** + * 短信模版的缓存 + *

+ * KEY 格式:sms_template:{id} + * VALUE 数据格式:String 模版信息 + */ + String SMS_TEMPLATE = "sms_template"; +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java new file mode 100644 index 00000000..fa91fcfd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java @@ -0,0 +1,59 @@ +package com.chanko.yunxi.mes.heli.module.system.dal.redis.oauth2; + +import cn.hutool.core.date.LocalDateTimeUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN; + +/** + * {@link OAuth2AccessTokenDO} 的 RedisDAO + * + * @author 芋道源码 + */ +@Repository +public class OAuth2AccessTokenRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public OAuth2AccessTokenDO get(String accessToken) { + String redisKey = formatKey(accessToken); + return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class); + } + + public void set(OAuth2AccessTokenDO accessTokenDO) { + String redisKey = formatKey(accessTokenDO.getAccessToken()); + // 清理多余字段,避免缓存 + accessTokenDO.setUpdater(null).setUpdateTime(null).setCreateTime(null).setCreator(null).setDeleted(null); + long time = LocalDateTimeUtil.between(LocalDateTime.now(), accessTokenDO.getExpiresTime(), ChronoUnit.SECONDS); + if (time > 0) { + stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(accessTokenDO), time, TimeUnit.SECONDS); + } + } + + public void delete(String accessToken) { + String redisKey = formatKey(accessToken); + stringRedisTemplate.delete(redisKey); + } + + public void deleteList(Collection accessTokens) { + List redisKeys = CollectionUtils.convertList(accessTokens, OAuth2AccessTokenRedisDAO::formatKey); + stringRedisTemplate.delete(redisKeys); + } + + private static String formatKey(String accessToken) { + return String.format(OAUTH2_ACCESS_TOKEN, accessToken); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/datapermission/config/DataPermissionConfiguration.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/datapermission/config/DataPermissionConfiguration.java new file mode 100644 index 00000000..ddbe30f1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/datapermission/config/DataPermissionConfiguration.java @@ -0,0 +1,28 @@ +package com.chanko.yunxi.mes.heli.module.system.framework.datapermission.config; + +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * system 模块的数据权限 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class DataPermissionConfiguration { + + @Bean + public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { + return rule -> { + // dept + rule.addDeptColumn(AdminUserDO.class); + rule.addDeptColumn(DeptDO.class, "id"); + // user + rule.addUserColumn(AdminUserDO.class, "id"); + }; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/datapermission/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/datapermission/package-info.java new file mode 100644 index 00000000..a76bd9c6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/datapermission/package-info.java @@ -0,0 +1,4 @@ +/** + * system 模块的数据权限配置 + */ +package com.chanko.yunxi.mes.heli.module.system.framework.datapermission; diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/package-info.java new file mode 100644 index 00000000..869f73aa --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 system 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.chanko.yunxi.mes.heli.module.system.framework; diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/sms/SmsCodeConfiguration.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/sms/SmsCodeConfiguration.java new file mode 100644 index 00000000..5dcd3e5d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/sms/SmsCodeConfiguration.java @@ -0,0 +1,9 @@ +package com.chanko.yunxi.mes.heli.module.system.framework.sms; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SmsCodeProperties.class) +public class SmsCodeConfiguration { +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/sms/SmsCodeProperties.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/sms/SmsCodeProperties.java new file mode 100644 index 00000000..9ec4570b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/sms/SmsCodeProperties.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.system.framework.sms; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.time.Duration; + +@ConfigurationProperties(prefix = "mes.sms-code") +@Validated +@Data +public class SmsCodeProperties { + + /** + * 过期时间 + */ + @NotNull(message = "过期时间不能为空") + private Duration expireTimes; + /** + * 短信发送频率 + */ + @NotNull(message = "短信发送频率不能为空") + private Duration sendFrequency; + /** + * 每日发送最大数量 + */ + @NotNull(message = "每日发送最大数量不能为空") + private Integer sendMaximumQuantityPerDay; + /** + * 验证码最小值 + */ + @NotNull(message = "验证码最小值不能为空") + private Integer beginCode; + /** + * 验证码最大值 + */ + @NotNull(message = "验证码最大值不能为空") + private Integer endCode; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/web/config/SystemWebConfiguration.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/web/config/SystemWebConfiguration.java new file mode 100644 index 00000000..46d636bf --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/web/config/SystemWebConfiguration.java @@ -0,0 +1,24 @@ +package com.chanko.yunxi.mes.heli.module.system.framework.web.config; + +import com.chanko.yunxi.mes.heli.framework.swagger.config.MesSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * system 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class SystemWebConfiguration { + + /** + * system 模块的 API 分组 + */ + @Bean + public GroupedOpenApi systemGroupedOpenApi() { + return MesSwaggerAutoConfiguration.buildGroupedOpenApi("system"); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/web/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/web/package-info.java new file mode 100644 index 00000000..0c428850 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * system 模块的 web 配置 + */ +package com.chanko.yunxi.mes.heli.module.system.framework.web; diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/job/DemoJob.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/job/DemoJob.java new file mode 100644 index 00000000..e894358f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/job/DemoJob.java @@ -0,0 +1,27 @@ +package com.chanko.yunxi.mes.heli.module.system.job; + +import com.chanko.yunxi.mes.heli.framework.quartz.core.handler.JobHandler; +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.chanko.yunxi.mes.heli.framework.tenant.core.job.TenantJob; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.user.AdminUserMapper; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +@Component +public class DemoJob implements JobHandler { + + @Resource + private AdminUserMapper adminUserMapper; + + @Override + @TenantJob // 标记多租户 + public String execute(String param) { + System.out.println("当前租户:" + TenantContextHolder.getTenantId()); + List users = adminUserMapper.selectList(); + return "用户数量:" + users.size(); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/job/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/job/package-info.java new file mode 100644 index 00000000..86368f13 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/job/package-info.java @@ -0,0 +1 @@ +package com.chanko.yunxi.mes.heli.module.system.job; diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/consumer/mail/MailSendConsumer.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/consumer/mail/MailSendConsumer.java new file mode 100644 index 00000000..63e69e34 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/consumer/mail/MailSendConsumer.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.mq.consumer.mail; + +import com.chanko.yunxi.mes.heli.module.system.mq.message.mail.MailSendMessage; +import com.chanko.yunxi.mes.heli.module.system.service.mail.MailSendService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link MailSendMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class MailSendConsumer { + + @Resource + private MailSendService mailSendService; + + @EventListener + @Async // Spring Event 默认在 Producer 发送的线程,通过 @Async 实现异步 + public void onMessage(MailSendMessage message) { + log.info("[onMessage][消息内容({})]", message); + mailSendService.doSendMail(message); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/consumer/sms/SmsSendConsumer.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/consumer/sms/SmsSendConsumer.java new file mode 100644 index 00000000..6fba8aeb --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/consumer/sms/SmsSendConsumer.java @@ -0,0 +1,31 @@ +package com.chanko.yunxi.mes.heli.module.system.mq.consumer.sms; + +import com.chanko.yunxi.mes.heli.module.system.mq.message.sms.SmsSendMessage; +import com.chanko.yunxi.mes.heli.module.system.service.sms.SmsSendService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SmsSendMessage} 的消费者 + * + * @author zzf + */ +@Component +@Slf4j +public class SmsSendConsumer { + + @Resource + private SmsSendService smsSendService; + + @EventListener + @Async // Spring Event 默认在 Producer 发送的线程,通过 @Async 实现异步 + public void onMessage(SmsSendMessage message) { + log.info("[onMessage][消息内容({})]", message); + smsSendService.doSendSms(message); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/message/mail/MailSendMessage.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/message/mail/MailSendMessage.java new file mode 100644 index 00000000..09efa813 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/message/mail/MailSendMessage.java @@ -0,0 +1,47 @@ +package com.chanko.yunxi.mes.heli.module.system.mq.message.mail; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 邮箱发送消息 + * + * @author 芋道源码 + */ +@Data +public class MailSendMessage { + + /** + * 邮件日志编号 + */ + @NotNull(message = "邮件日志编号不能为空") + private Long logId; + /** + * 接收邮件地址 + */ + @NotNull(message = "接收邮件地址不能为空") + private String mail; + /** + * 邮件账号编号 + */ + @NotNull(message = "邮件账号编号不能为空") + private Long accountId; + + /** + * 邮件发件人 + */ + private String nickname; + /** + * 邮件标题 + */ + @NotEmpty(message = "邮件标题不能为空") + private String title; + /** + * 邮件内容 + */ + @NotEmpty(message = "邮件内容不能为空") + private String content; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/message/sms/SmsSendMessage.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/message/sms/SmsSendMessage.java new file mode 100644 index 00000000..6fa495c1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/message/sms/SmsSendMessage.java @@ -0,0 +1,42 @@ +package com.chanko.yunxi.mes.heli.module.system.mq.message.sms; + +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 短信发送消息 + * + * @author 芋道源码 + */ +@Data +public class SmsSendMessage { + + /** + * 短信日志编号 + */ + @NotNull(message = "短信日志编号不能为空") + private Long logId; + /** + * 手机号 + */ + @NotNull(message = "手机号不能为空") + private String mobile; + /** + * 短信渠道编号 + */ + @NotNull(message = "短信渠道编号不能为空") + private Long channelId; + /** + * 短信 API 的模板编号 + */ + @NotNull(message = "短信 API 的模板编号不能为空") + private String apiTemplateId; + /** + * 短信模板参数 + */ + private List> templateParams; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/producer/mail/MailProducer.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/producer/mail/MailProducer.java new file mode 100644 index 00000000..a421bcff --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/producer/mail/MailProducer.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.system.mq.producer.mail; + +import com.chanko.yunxi.mes.heli.module.system.mq.message.mail.MailSendMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Mail 邮件相关消息的 Producer + * + * @author wangjingyi + * @since 2021/4/19 13:33 + */ +@Slf4j +@Component +public class MailProducer { + + @Resource + private ApplicationContext applicationContext; + + /** + * 发送 {@link MailSendMessage} 消息 + * + * @param sendLogId 发送日志编码 + * @param mail 接收邮件地址 + * @param accountId 邮件账号编号 + * @param nickname 邮件发件人 + * @param title 邮件标题 + * @param content 邮件内容 + */ + public void sendMailSendMessage(Long sendLogId, String mail, Long accountId, + String nickname, String title, String content) { + MailSendMessage message = new MailSendMessage() + .setLogId(sendLogId).setMail(mail).setAccountId(accountId) + .setNickname(nickname).setTitle(title).setContent(content); + applicationContext.publishEvent(message); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/producer/sms/SmsProducer.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/producer/sms/SmsProducer.java new file mode 100644 index 00000000..d702e577 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/mq/producer/sms/SmsProducer.java @@ -0,0 +1,41 @@ +package com.chanko.yunxi.mes.heli.module.system.mq.producer.sms; + +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import com.chanko.yunxi.mes.heli.module.system.mq.message.sms.SmsSendMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +/** + * Sms 短信相关消息的 Producer + * + * @author zzf + * @since 2021/3/9 16:35 + */ +@Slf4j +@Component +public class SmsProducer { + + @Resource + private ApplicationContext applicationContext; + + /** + * 发送 {@link SmsSendMessage} 消息 + * + * @param logId 短信日志编号 + * @param mobile 手机号 + * @param channelId 渠道编号 + * @param apiTemplateId 短信模板编号 + * @param templateParams 短信模板参数 + */ + public void sendSmsSendMessage(Long logId, String mobile, + Long channelId, String apiTemplateId, List> templateParams) { + SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile); + message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams); + applicationContext.publishEvent(message); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/package-info.java new file mode 100644 index 00000000..2aabc486 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/package-info.java @@ -0,0 +1,8 @@ +/** + * system 模块下,我们放通用业务,支撑上层的核心业务。 + * 例如说:用户、部门、权限、数据字典等等 + * + * 1. Controller URL:以 /system/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 system_ 开头,方便在数据库中区分 + */ +package com.chanko.yunxi.mes.heli.module.system; diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/auth/AdminAuthService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/auth/AdminAuthService.java new file mode 100644 index 00000000..e0fdbd15 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/auth/AdminAuthService.java @@ -0,0 +1,73 @@ +package com.chanko.yunxi.mes.heli.module.system.service.auth; + +import com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo.*; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; + +import javax.validation.Valid; + +/** + * 管理后台的认证 Service 接口 + * + * 提供用户的登录、登出的能力 + * + * @author 芋道源码 + */ +public interface AdminAuthService { + + /** + * 验证账号 + 密码。如果通过,则返回用户 + * + * @param username 账号 + * @param password 密码 + * @return 用户 + */ + AdminUserDO authenticate(String username, String password); + + /** + * 账号登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO login(@Valid AuthLoginReqVO reqVO); + + /** + * 基于 token 退出登录 + * + * @param token token + * @param logType 登出类型 + */ + void logout(String token, Integer logType); + + /** + * 短信验证码发送 + * + * @param reqVO 发送请求 + */ + void sendSmsCode(AuthSmsSendReqVO reqVO); + + /** + * 短信登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) ; + + /** + * 社交快捷登录,使用 code 授权码 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO socialLogin(@Valid AuthSocialLoginReqVO reqVO); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 登录结果 + */ + AuthLoginRespVO refreshToken(String refreshToken); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/auth/AdminAuthServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/auth/AdminAuthServiceImpl.java new file mode 100644 index 00000000..b1dff5da --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/auth/AdminAuthServiceImpl.java @@ -0,0 +1,250 @@ +package com.chanko.yunxi.mes.heli.module.system.service.auth; + +import cn.hutool.core.util.ObjectUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.common.util.monitor.TracerUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.validation.ValidationUtils; +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.SmsCodeApi; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserBindReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserRespDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.auth.vo.*; +import com.chanko.yunxi.mes.heli.module.system.convert.auth.AuthConvert; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.enums.logger.LoginLogTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.logger.LoginResultEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.oauth2.OAuth2ClientConstants; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsSceneEnum; +import com.chanko.yunxi.mes.heli.module.system.service.logger.LoginLogService; +import com.chanko.yunxi.mes.heli.module.system.service.member.MemberService; +import com.chanko.yunxi.mes.heli.module.system.service.oauth2.OAuth2TokenService; +import com.chanko.yunxi.mes.heli.module.system.service.social.SocialUserService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import com.google.common.annotations.VisibleForTesting; +import com.xingyuv.captcha.model.common.ResponseModel; +import com.xingyuv.captcha.model.vo.CaptchaVO; +import com.xingyuv.captcha.service.CaptchaService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.Objects; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * Auth Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class AdminAuthServiceImpl implements AdminAuthService { + + @Resource + private AdminUserService userService; + @Resource + private LoginLogService loginLogService; + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private SocialUserService socialUserService; + @Resource + private MemberService memberService; + @Resource + private Validator validator; + @Resource + private CaptchaService captchaService; + @Resource + private SmsCodeApi smsCodeApi; + + /** + * 验证码的开关,默认为 true + */ + @Value("${mes.captcha.enable:true}") + private Boolean captchaEnable; + + @Override + public AdminUserDO authenticate(String username, String password) { + final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; + // 校验账号是否存在 + AdminUserDO user = userService.getUserByUsername(username); + if (user == null) { + createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + if (!userService.isPasswordMatch(password, user.getPassword())) { + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + // 校验是否禁用 + if (CommonStatusEnum.isDisable(user.getStatus())) { + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + return user; + } + + @Override + public AuthLoginRespVO login(AuthLoginReqVO reqVO) { + // 校验验证码 + validateCaptcha(reqVO); + + // 使用账号密码,进行登录 + AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); + + // 如果 socialType 非空,说明需要绑定社交用户 + if (reqVO.getSocialType() != null) { + socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), + reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); + } + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); + } + + @Override + public void sendSmsCode(AuthSmsSendReqVO reqVO) { + // 登录场景,验证是否存在 + if (userService.getUserByMobile(reqVO.getMobile()) == null) { + throw exception(AUTH_MOBILE_NOT_EXISTS); + } + // 发送验证码 + smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); + } + + @Override + public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) { + // 校验验证码 + smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())); + + // 获得用户信息 + AdminUserDO user = userService.getUserByMobile(reqVO.getMobile()); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); + } + + private void createLoginLog(Long userId, String username, + LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { + // 插入登录日志 + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(logTypeEnum.getType()); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(getUserType().getValue()); + reqDTO.setUsername(username); + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(ServletUtils.getClientIP()); + reqDTO.setResult(loginResult.getResult()); + loginLogService.createLoginLog(reqDTO); + // 更新最后登录时间 + if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { + userService.updateUserLogin(userId, ServletUtils.getClientIP()); + } + } + + @Override + public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) { + // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 + SocialUserRespDTO socialUser = socialUserService.getSocialUser(UserTypeEnum.ADMIN.getValue(), reqVO.getType(), + reqVO.getCode(), reqVO.getState()); + if (socialUser == null) { + throw exception(AUTH_THIRD_LOGIN_NOT_BIND); + } + + // 获得用户 + AdminUserDO user = userService.getUser(socialUser.getUserId()); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); + } + + @VisibleForTesting + void validateCaptcha(AuthLoginReqVO reqVO) { + // 如果验证码关闭,则不进行校验 + if (!captchaEnable) { + return; + } + // 校验验证码 + ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class); + CaptchaVO captchaVO = new CaptchaVO(); + captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification()); + ResponseModel response = captchaService.verification(captchaVO); + // 验证不通过 + if (!response.isSuccess()) { + // 创建登录失败日志(验证码不正确) + createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR); + throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg()); + } + } + + private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { + // 插入登陆日志 + createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); + // 创建访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), + OAuth2ClientConstants.CLIENT_ID_DEFAULT, null); + // 构建返回结果 + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + + @Override + public AuthLoginRespVO refreshToken(String refreshToken) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + + @Override + public void logout(String token, Integer logType) { + // 删除访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token); + if (accessTokenDO == null) { + return; + } + // 删除成功,则记录登出日志 + createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType); + } + + private void createLogoutLog(Long userId, Integer userType, Integer logType) { + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(logType); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(userType); + if (ObjectUtil.equal(getUserType().getValue(), userType)) { + reqDTO.setUsername(getUsername(userId)); + } else { + reqDTO.setUsername(memberService.getMemberUserMobile(userId)); + } + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(ServletUtils.getClientIP()); + reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); + loginLogService.createLoginLog(reqDTO); + } + + private String getUsername(Long userId) { + if (userId == null) { + return null; + } + AdminUserDO user = userService.getUser(userId); + return user != null ? user.getUsername() : null; + } + + private UserTypeEnum getUserType() { + return UserTypeEnum.ADMIN; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/DeptService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/DeptService.java new file mode 100644 index 00000000..0156fcce --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/DeptService.java @@ -0,0 +1,102 @@ +package com.chanko.yunxi.mes.heli.module.system.service.dept; + +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 部门 Service 接口 + * + * @author 芋道源码 + */ +public interface DeptService { + + /** + * 创建部门 + * + * @param createReqVO 部门信息 + * @return 部门编号 + */ + Long createDept(DeptSaveReqVO createReqVO); + + /** + * 更新部门 + * + * @param updateReqVO 部门信息 + */ + void updateDept(DeptSaveReqVO updateReqVO); + + /** + * 删除部门 + * + * @param id 部门编号 + */ + void deleteDept(Long id); + + /** + * 获得部门信息 + * + * @param id 部门编号 + * @return 部门信息 + */ + DeptDO getDept(Long id); + + /** + * 获得部门信息数组 + * + * @param ids 部门编号数组 + * @return 部门信息数组 + */ + List getDeptList(Collection ids); + + /** + * 筛选部门列表 + * + * @param reqVO 筛选条件请求 VO + * @return 部门列表 + */ + List getDeptList(DeptListReqVO reqVO); + + /** + * 获得指定编号的部门 Map + * + * @param ids 部门编号数组 + * @return 部门 Map + */ + default Map getDeptMap(Collection ids) { + List list = getDeptList(ids); + return CollectionUtils.convertMap(list, DeptDO::getId); + } + + /** + * 获得指定部门的所有子部门 + * + * @param id 部门编号 + * @return 子部门列表 + */ + List getChildDeptList(Long id); + + /** + * 获得所有子部门,从缓存中 + * + * @param id 父部门编号 + * @return 子部门列表 + */ + Set getChildDeptIdListFromCache(Long id); + + /** + * 校验部门们是否有效。如下情况,视为无效: + * 1. 部门编号不存在 + * 2. 部门被禁用 + * + * @param ids 角色编号数组 + */ + void validateDeptList(Collection ids); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/DeptServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/DeptServiceImpl.java new file mode 100644 index 00000000..821335c5 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/DeptServiceImpl.java @@ -0,0 +1,218 @@ +package com.chanko.yunxi.mes.heli.module.system.service.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.annotation.DataPermission; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.dept.DeptMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.*; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 部门 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class DeptServiceImpl implements DeptService { + + @Resource + private DeptMapper deptMapper; + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 + public Long createDept(DeptSaveReqVO createReqVO) { + if (createReqVO.getParentId() == null) { + createReqVO.setParentId(DeptDO.PARENT_ID_ROOT); + } + // 校验父部门的有效性 + validateParentDept(null, createReqVO.getParentId()); + // 校验部门名的唯一性 + validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); + + // 插入部门 + DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class); + deptMapper.insert(dept); + return dept.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 + public void updateDept(DeptSaveReqVO updateReqVO) { + if (updateReqVO.getParentId() == null) { + updateReqVO.setParentId(DeptDO.PARENT_ID_ROOT); + } + // 校验自己存在 + validateDeptExists(updateReqVO.getId()); + // 校验父部门的有效性 + validateParentDept(updateReqVO.getId(), updateReqVO.getParentId()); + // 校验部门名的唯一性 + validateDeptNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName()); + + // 更新部门 + DeptDO updateObj = BeanUtils.toBean(updateReqVO, DeptDO.class); + deptMapper.updateById(updateObj); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 + public void deleteDept(Long id) { + // 校验是否存在 + validateDeptExists(id); + // 校验是否有子部门 + if (deptMapper.selectCountByParentId(id) > 0) { + throw exception(DEPT_EXITS_CHILDREN); + } + // 删除部门 + deptMapper.deleteById(id); + } + + @VisibleForTesting + void validateDeptExists(Long id) { + if (id == null) { + return; + } + DeptDO dept = deptMapper.selectById(id); + if (dept == null) { + throw exception(DEPT_NOT_FOUND); + } + } + + @VisibleForTesting + void validateParentDept(Long id, Long parentId) { + if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) { + return; + } + // 1. 不能设置自己为父部门 + if (Objects.equals(id, parentId)) { + throw exception(DEPT_PARENT_ERROR); + } + // 2. 父部门不存在 + DeptDO parentDept = deptMapper.selectById(parentId); + if (parentDept == null) { + throw exception(DEPT_PARENT_NOT_EXITS); + } + // 3. 递归校验父部门,如果父部门是自己的子部门,则报错,避免形成环路 + if (id == null) { // id 为空,说明新增,不需要考虑环路 + return; + } + for (int i = 0; i < Short.MAX_VALUE; i++) { + // 3.1 校验环路 + parentId = parentDept.getParentId(); + if (Objects.equals(id, parentId)) { + throw exception(DEPT_PARENT_IS_CHILD); + } + // 3.2 继续递归下一级父部门 + if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) { + break; + } + parentDept = deptMapper.selectById(parentId); + if (parentDept == null) { + break; + } + } + } + + @VisibleForTesting + void validateDeptNameUnique(Long id, Long parentId, String name) { + DeptDO dept = deptMapper.selectByParentIdAndName(parentId, name); + if (dept == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的部门 + if (id == null) { + throw exception(DEPT_NAME_DUPLICATE); + } + if (ObjectUtil.notEqual(dept.getId(), id)) { + throw exception(DEPT_NAME_DUPLICATE); + } + } + + @Override + public DeptDO getDept(Long id) { + return deptMapper.selectById(id); + } + + @Override + public List getDeptList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return deptMapper.selectBatchIds(ids); + } + + @Override + public List getDeptList(DeptListReqVO reqVO) { + List list = deptMapper.selectList(reqVO); + list.sort(Comparator.comparing(DeptDO::getSort)); + return list; + } + + @Override + public List getChildDeptList(Long id) { + List children = new LinkedList<>(); + // 遍历每一层 + Collection parentIds = Collections.singleton(id); + for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环 + // 查询当前层,所有的子部门 + List depts = deptMapper.selectListByParentId(parentIds); + // 1. 如果没有子部门,则结束遍历 + if (CollUtil.isEmpty(depts)) { + break; + } + // 2. 如果有子部门,继续遍历 + children.addAll(depts); + parentIds = convertSet(depts, DeptDO::getId); + } + return children; + } + + @Override + @DataPermission(enable = false) // 禁用数据权限,避免建立不正确的缓存 + @Cacheable(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, key = "#id") + public Set getChildDeptIdListFromCache(Long id) { + List children = getChildDeptList(id); + return convertSet(children, DeptDO::getId); + } + + @Override + public void validateDeptList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得科室信息 + Map deptMap = getDeptMap(ids); + // 校验 + ids.forEach(id -> { + DeptDO dept = deptMap.get(id); + if (dept == null) { + throw exception(DEPT_NOT_FOUND); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus())) { + throw exception(DEPT_NOT_ENABLE, dept.getName()); + } + }); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/PostService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/PostService.java new file mode 100644 index 00000000..7e13c22c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/PostService.java @@ -0,0 +1,84 @@ +package com.chanko.yunxi.mes.heli.module.system.service.dept; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.PostDO; +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.List; + +/** + * 岗位 Service 接口 + * + * @author 芋道源码 + */ +public interface PostService { + + /** + * 创建岗位 + * + * @param createReqVO 岗位信息 + * @return 岗位编号 + */ + Long createPost(PostSaveReqVO createReqVO); + + /** + * 更新岗位 + * + * @param updateReqVO 岗位信息 + */ + void updatePost(PostSaveReqVO updateReqVO); + + /** + * 删除岗位信息 + * + * @param id 岗位编号 + */ + void deletePost(Long id); + + /** + * 获得岗位列表 + * + * @param ids 岗位编号数组 + * @return 部门列表 + */ + List getPostList(@Nullable Collection ids); + + /** + * 获得符合条件的岗位列表 + * + * @param ids 岗位编号数组。如果为空,不进行筛选 + * @param statuses 状态数组。如果为空,不进行筛选 + * @return 部门列表 + */ + List getPostList(@Nullable Collection ids, + @Nullable Collection statuses); + + /** + * 获得岗位分页列表 + * + * @param reqVO 分页条件 + * @return 部门分页列表 + */ + PageResult getPostPage(PostPageReqVO reqVO); + + /** + * 获得岗位信息 + * + * @param id 岗位编号 + * @return 岗位信息 + */ + PostDO getPost(Long id); + + /** + * 校验岗位们是否有效。如下情况,视为无效: + * 1. 岗位编号不存在 + * 2. 岗位被禁用 + * + * @param ids 岗位编号数组 + */ + void validatePostList(Collection ids); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/PostServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/PostServiceImpl.java new file mode 100644 index 00000000..11074bc0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dept/PostServiceImpl.java @@ -0,0 +1,153 @@ +package com.chanko.yunxi.mes.heli.module.system.service.dept; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dept.vo.post.PostSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.PostDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.dept.PostMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertMap; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 岗位 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PostServiceImpl implements PostService { + + @Resource + private PostMapper postMapper; + + @Override + public Long createPost(PostSaveReqVO createReqVO) { + // 校验正确性 + validatePostForCreateOrUpdate(null, createReqVO.getName(), createReqVO.getCode()); + + // 插入岗位 + PostDO post = BeanUtils.toBean(createReqVO, PostDO.class); + postMapper.insert(post); + return post.getId(); + } + + @Override + public void updatePost(PostSaveReqVO updateReqVO) { + // 校验正确性 + validatePostForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getName(), updateReqVO.getCode()); + + // 更新岗位 + PostDO updateObj = BeanUtils.toBean(updateReqVO, PostDO.class); + postMapper.updateById(updateObj); + } + + @Override + public void deletePost(Long id) { + // 校验是否存在 + validatePostExists(id); + // 删除部门 + postMapper.deleteById(id); + } + + private void validatePostForCreateOrUpdate(Long id, String name, String code) { + // 校验自己存在 + validatePostExists(id); + // 校验岗位名的唯一性 + validatePostNameUnique(id, name); + // 校验岗位编码的唯一性 + validatePostCodeUnique(id, code); + } + + private void validatePostNameUnique(Long id, String name) { + PostDO post = postMapper.selectByName(name); + if (post == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的岗位 + if (id == null) { + throw exception(POST_NAME_DUPLICATE); + } + if (!post.getId().equals(id)) { + throw exception(POST_NAME_DUPLICATE); + } + } + + private void validatePostCodeUnique(Long id, String code) { + PostDO post = postMapper.selectByCode(code); + if (post == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的岗位 + if (id == null) { + throw exception(POST_CODE_DUPLICATE); + } + if (!post.getId().equals(id)) { + throw exception(POST_CODE_DUPLICATE); + } + } + + private void validatePostExists(Long id) { + if (id == null) { + return; + } + if (postMapper.selectById(id) == null) { + throw exception(POST_NOT_FOUND); + } + } + + @Override + public List getPostList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return postMapper.selectBatchIds(ids); + } + + @Override + public List getPostList(Collection ids, Collection statuses) { + return postMapper.selectList(ids, statuses); + } + + @Override + public PageResult getPostPage(PostPageReqVO reqVO) { + return postMapper.selectPage(reqVO); + } + + @Override + public PostDO getPost(Long id) { + return postMapper.selectById(id); + } + + @Override + public void validatePostList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得岗位信息 + List posts = postMapper.selectBatchIds(ids); + Map postMap = convertMap(posts, PostDO::getId); + // 校验 + ids.forEach(id -> { + PostDO post = postMap.get(id); + if (post == null) { + throw exception(POST_NOT_FOUND); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(post.getStatus())) { + throw exception(POST_NOT_ENABLE, post.getName()); + } + }); + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictDataService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictDataService.java new file mode 100644 index 00000000..a38bb268 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictDataService.java @@ -0,0 +1,102 @@ +package com.chanko.yunxi.mes.heli.module.system.service.dict; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictDataDO; +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.List; + +/** + * 字典数据 Service 接口 + * + * @author ruoyi + */ +public interface DictDataService { + + /** + * 创建字典数据 + * + * @param createReqVO 字典数据信息 + * @return 字典数据编号 + */ + Long createDictData(DictDataSaveReqVO createReqVO); + + /** + * 更新字典数据 + * + * @param updateReqVO 字典数据信息 + */ + void updateDictData(DictDataSaveReqVO updateReqVO); + + /** + * 删除字典数据 + * + * @param id 字典数据编号 + */ + void deleteDictData(Long id); + + /** + * 获得字典数据列表 + * + * @param status 状态 + * @param dictType 字典类型 + * @return 字典数据全列表 + */ + List getDictDataList(@Nullable Integer status, @Nullable String dictType); + + /** + * 获得字典数据分页列表 + * + * @param pageReqVO 分页请求 + * @return 字典数据分页列表 + */ + PageResult getDictDataPage(DictDataPageReqVO pageReqVO); + + /** + * 获得字典数据详情 + * + * @param id 字典数据编号 + * @return 字典数据 + */ + DictDataDO getDictData(Long id); + + /** + * 获得指定字典类型的数据数量 + * + * @param dictType 字典类型 + * @return 数据数量 + */ + long getDictDataCountByDictType(String dictType); + + /** + * 校验字典数据们是否有效。如下情况,视为无效: + * 1. 字典数据不存在 + * 2. 字典数据被禁用 + * + * @param dictType 字典类型 + * @param values 字典数据值的数组 + */ + void validateDictDataList(String dictType, Collection values); + + /** + * 获得指定的字典数据 + * + * @param dictType 字典类型 + * @param value 字典数据值 + * @return 字典数据 + */ + DictDataDO getDictData(String dictType, String value); + + /** + * 解析获得指定的字典数据,从缓存中 + * + * @param dictType 字典类型 + * @param label 字典数据标签 + * @return 字典数据 + */ + DictDataDO parseDictData(String dictType, String label); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictDataServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictDataServiceImpl.java new file mode 100644 index 00000000..806e295a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictDataServiceImpl.java @@ -0,0 +1,172 @@ +package com.chanko.yunxi.mes.heli.module.system.service.dict; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictDataDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictTypeDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.dict.DictDataMapper; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 字典数据 Service 实现类 + * + * @author ruoyi + */ +@Service +@Slf4j +public class DictDataServiceImpl implements DictDataService { + + /** + * 排序 dictType > sort + */ + private static final Comparator COMPARATOR_TYPE_AND_SORT = Comparator + .comparing(DictDataDO::getDictType) + .thenComparingInt(DictDataDO::getSort); + + @Resource + private DictTypeService dictTypeService; + + @Resource + private DictDataMapper dictDataMapper; + + @Override + public List getDictDataList(Integer status, String dictType) { + List list = dictDataMapper.selectListByStatusAndDictType(status, dictType); + list.sort(COMPARATOR_TYPE_AND_SORT); + return list; + } + + @Override + public PageResult getDictDataPage(DictDataPageReqVO pageReqVO) { + return dictDataMapper.selectPage(pageReqVO); + } + + @Override + public DictDataDO getDictData(Long id) { + return dictDataMapper.selectById(id); + } + + @Override + public Long createDictData(DictDataSaveReqVO createReqVO) { + // 校验字典类型有效 + validateDictTypeExists(createReqVO.getDictType()); + // 校验字典数据的值的唯一性 + validateDictDataValueUnique(null, createReqVO.getDictType(), createReqVO.getValue()); + + // 插入字典类型 + DictDataDO dictData = BeanUtils.toBean(createReqVO, DictDataDO.class); + dictDataMapper.insert(dictData); + return dictData.getId(); + } + + @Override + public void updateDictData(DictDataSaveReqVO updateReqVO) { + // 校验自己存在 + validateDictDataExists(updateReqVO.getId()); + // 校验字典类型有效 + validateDictTypeExists(updateReqVO.getDictType()); + // 校验字典数据的值的唯一性 + validateDictDataValueUnique(updateReqVO.getId(), updateReqVO.getDictType(), updateReqVO.getValue()); + + // 更新字典类型 + DictDataDO updateObj = BeanUtils.toBean(updateReqVO, DictDataDO.class); + dictDataMapper.updateById(updateObj); + } + + @Override + public void deleteDictData(Long id) { + // 校验是否存在 + validateDictDataExists(id); + + // 删除字典数据 + dictDataMapper.deleteById(id); + } + + @Override + public long getDictDataCountByDictType(String dictType) { + return dictDataMapper.selectCountByDictType(dictType); + } + + @VisibleForTesting + public void validateDictDataValueUnique(Long id, String dictType, String value) { + DictDataDO dictData = dictDataMapper.selectByDictTypeAndValue(dictType, value); + if (dictData == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典数据 + if (id == null) { + throw exception(DICT_DATA_VALUE_DUPLICATE); + } + if (!dictData.getId().equals(id)) { + throw exception(DICT_DATA_VALUE_DUPLICATE); + } + } + + @VisibleForTesting + public void validateDictDataExists(Long id) { + if (id == null) { + return; + } + DictDataDO dictData = dictDataMapper.selectById(id); + if (dictData == null) { + throw exception(DICT_DATA_NOT_EXISTS); + } + } + + @VisibleForTesting + public void validateDictTypeExists(String type) { + DictTypeDO dictType = dictTypeService.getDictType(type); + if (dictType == null) { + throw exception(DICT_TYPE_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(dictType.getStatus())) { + throw exception(DICT_TYPE_NOT_ENABLE); + } + } + + @Override + public void validateDictDataList(String dictType, Collection values) { + if (CollUtil.isEmpty(values)) { + return; + } + Map dictDataMap = CollectionUtils.convertMap( + dictDataMapper.selectByDictTypeAndValues(dictType, values), DictDataDO::getValue); + // 校验 + values.forEach(value -> { + DictDataDO dictData = dictDataMap.get(value); + if (dictData == null) { + throw exception(DICT_DATA_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(dictData.getStatus())) { + throw exception(DICT_DATA_NOT_ENABLE, dictData.getLabel()); + } + }); + } + + @Override + public DictDataDO getDictData(String dictType, String value) { + return dictDataMapper.selectByDictTypeAndValue(dictType, value); + } + + @Override + public DictDataDO parseDictData(String dictType, String label) { + return dictDataMapper.selectByDictTypeAndLabel(dictType, label); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictTypeService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictTypeService.java new file mode 100644 index 00000000..2c51b023 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictTypeService.java @@ -0,0 +1,70 @@ +package com.chanko.yunxi.mes.heli.module.system.service.dict; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictTypeDO; + +import java.util.List; + +/** + * 字典类型 Service 接口 + * + * @author 芋道源码 + */ +public interface DictTypeService { + + /** + * 创建字典类型 + * + * @param createReqVO 字典类型信息 + * @return 字典类型编号 + */ + Long createDictType(DictTypeSaveReqVO createReqVO); + + /** + * 更新字典类型 + * + * @param updateReqVO 字典类型信息 + */ + void updateDictType(DictTypeSaveReqVO updateReqVO); + + /** + * 删除字典类型 + * + * @param id 字典类型编号 + */ + void deleteDictType(Long id); + + /** + * 获得字典类型分页列表 + * + * @param pageReqVO 分页请求 + * @return 字典类型分页列表 + */ + PageResult getDictTypePage(DictTypePageReqVO pageReqVO); + + /** + * 获得字典类型详情 + * + * @param id 字典类型编号 + * @return 字典类型 + */ + DictTypeDO getDictType(Long id); + + /** + * 获得字典类型详情 + * + * @param type 字典类型 + * @return 字典类型详情 + */ + DictTypeDO getDictType(String type); + + /** + * 获得全部字典类型列表 + * + * @return 字典类型列表 + */ + List getDictTypeList(); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictTypeServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictTypeServiceImpl.java new file mode 100644 index 00000000..1f36e6a2 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/dict/DictTypeServiceImpl.java @@ -0,0 +1,140 @@ +package com.chanko.yunxi.mes.heli.module.system.service.dict; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.date.LocalDateTimeUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dict.DictTypeDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.dict.DictTypeMapper; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 字典类型 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class DictTypeServiceImpl implements DictTypeService { + + @Resource + private DictDataService dictDataService; + + @Resource + private DictTypeMapper dictTypeMapper; + + @Override + public PageResult getDictTypePage(DictTypePageReqVO pageReqVO) { + return dictTypeMapper.selectPage(pageReqVO); + } + + @Override + public DictTypeDO getDictType(Long id) { + return dictTypeMapper.selectById(id); + } + + @Override + public DictTypeDO getDictType(String type) { + return dictTypeMapper.selectByType(type); + } + + @Override + public Long createDictType(DictTypeSaveReqVO createReqVO) { + // 校验字典类型的名字的唯一性 + validateDictTypeNameUnique(null, createReqVO.getName()); + // 校验字典类型的类型的唯一性 + validateDictTypeUnique(null, createReqVO.getType()); + + // 插入字典类型 + DictTypeDO dictType = BeanUtils.toBean(createReqVO, DictTypeDO.class); + dictType.setDeletedTime(LocalDateTimeUtils.EMPTY); // 唯一索引,避免 null 值 + dictTypeMapper.insert(dictType); + return dictType.getId(); + } + + @Override + public void updateDictType(DictTypeSaveReqVO updateReqVO) { + // 校验自己存在 + validateDictTypeExists(updateReqVO.getId()); + // 校验字典类型的名字的唯一性 + validateDictTypeNameUnique(updateReqVO.getId(), updateReqVO.getName()); + // 校验字典类型的类型的唯一性 + validateDictTypeUnique(updateReqVO.getId(), updateReqVO.getType()); + + // 更新字典类型 + DictTypeDO updateObj = BeanUtils.toBean(updateReqVO, DictTypeDO.class); + dictTypeMapper.updateById(updateObj); + } + + @Override + public void deleteDictType(Long id) { + // 校验是否存在 + DictTypeDO dictType = validateDictTypeExists(id); + // 校验是否有字典数据 + if (dictDataService.getDictDataCountByDictType(dictType.getType()) > 0) { + throw exception(DICT_TYPE_HAS_CHILDREN); + } + // 删除字典类型 + dictTypeMapper.updateToDelete(id, LocalDateTime.now()); + } + + @Override + public List getDictTypeList() { + return dictTypeMapper.selectList(); + } + + @VisibleForTesting + void validateDictTypeNameUnique(Long id, String name) { + DictTypeDO dictType = dictTypeMapper.selectByName(name); + if (dictType == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(DICT_TYPE_NAME_DUPLICATE); + } + if (!dictType.getId().equals(id)) { + throw exception(DICT_TYPE_NAME_DUPLICATE); + } + } + + @VisibleForTesting + void validateDictTypeUnique(Long id, String type) { + if (StrUtil.isEmpty(type)) { + return; + } + DictTypeDO dictType = dictTypeMapper.selectByType(type); + if (dictType == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(DICT_TYPE_TYPE_DUPLICATE); + } + if (!dictType.getId().equals(id)) { + throw exception(DICT_TYPE_TYPE_DUPLICATE); + } + } + + @VisibleForTesting + DictTypeDO validateDictTypeExists(Long id) { + if (id == null) { + return null; + } + DictTypeDO dictType = dictTypeMapper.selectById(id); + if (dictType == null) { + throw exception(DICT_TYPE_NOT_EXISTS); + } + return dictType; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/errorcode/ErrorCodeService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/errorcode/ErrorCodeService.java new file mode 100644 index 00000000..de7c8bfd --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/errorcode/ErrorCodeService.java @@ -0,0 +1,77 @@ +package com.chanko.yunxi.mes.heli.module.system.service.errorcode; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode.vo.ErrorCodeSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.errorcode.ErrorCodeDO; + +import javax.validation.Valid; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 错误码 Service 接口 + * + * @author 芋道源码 + */ +public interface ErrorCodeService { + + /** + * 自动创建错误码 + * + * @param autoGenerateDTOs 错误码信息 + */ + void autoGenerateErrorCodes(@Valid List autoGenerateDTOs); + + /** + * 增量获得错误码数组 + * + * 如果 minUpdateTime 为空时,则获取所有错误码 + * + * @param applicationName 应用名 + * @param minUpdateTime 最小更新时间 + * @return 错误码数组 + */ + List getErrorCodeList(String applicationName, LocalDateTime minUpdateTime); + + /** + * 创建错误码 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createErrorCode(@Valid ErrorCodeSaveReqVO createReqVO); + + /** + * 更新错误码 + * + * @param updateReqVO 更新信息 + */ + void updateErrorCode(@Valid ErrorCodeSaveReqVO updateReqVO); + + /** + * 删除错误码 + * + * @param id 编号 + */ + void deleteErrorCode(Long id); + + /** + * 获得错误码 + * + * @param id 编号 + * @return 错误码 + */ + ErrorCodeDO getErrorCode(Long id); + + /** + * 获得错误码分页 + * + * @param pageReqVO 分页查询 + * @return 错误码分页 + */ + PageResult getErrorCodePage(ErrorCodePageReqVO pageReqVO); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/errorcode/ErrorCodeServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/errorcode/ErrorCodeServiceImpl.java new file mode 100644 index 00000000..a19c93ba --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/errorcode/ErrorCodeServiceImpl.java @@ -0,0 +1,167 @@ +package com.chanko.yunxi.mes.heli.module.system.service.errorcode; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.errorcode.vo.ErrorCodeSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.errorcode.ErrorCodeMapper; +import com.chanko.yunxi.mes.heli.module.system.enums.errorcode.ErrorCodeTypeEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertMap; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.ERROR_CODE_DUPLICATE; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.ERROR_CODE_NOT_EXISTS; + +/** + * 错误码 Service 实现类 + * + * @author dlyan + */ +@Service +@Validated +@Slf4j +public class ErrorCodeServiceImpl implements ErrorCodeService { + + @Resource + private ErrorCodeMapper errorCodeMapper; + + @Override + public Long createErrorCode(ErrorCodeSaveReqVO createReqVO) { + // 校验 code 重复 + validateCodeDuplicate(createReqVO.getCode(), null); + + // 插入 + ErrorCodeDO errorCode = BeanUtils.toBean(createReqVO, ErrorCodeDO.class) + .setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType()); + errorCodeMapper.insert(errorCode); + // 返回 + return errorCode.getId(); + } + + @Override + public void updateErrorCode(ErrorCodeSaveReqVO updateReqVO) { + // 校验存在 + validateErrorCodeExists(updateReqVO.getId()); + // 校验 code 重复 + validateCodeDuplicate(updateReqVO.getCode(), updateReqVO.getId()); + + // 更新 + ErrorCodeDO updateObj = BeanUtils.toBean(updateReqVO, ErrorCodeDO.class) + .setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType()); + errorCodeMapper.updateById(updateObj); + } + + @Override + public void deleteErrorCode(Long id) { + // 校验存在 + validateErrorCodeExists(id); + // 删除 + errorCodeMapper.deleteById(id); + } + + /** + * 校验错误码的唯一字段是否重复 + * + * 是否存在相同编码的错误码 + * + * @param code 错误码编码 + * @param id 错误码编号 + */ + @VisibleForTesting + public void validateCodeDuplicate(Integer code, Long id) { + ErrorCodeDO errorCodeDO = errorCodeMapper.selectByCode(code); + if (errorCodeDO == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的错误码 + if (id == null) { + throw exception(ERROR_CODE_DUPLICATE); + } + if (!errorCodeDO.getId().equals(id)) { + throw exception(ERROR_CODE_DUPLICATE); + } + } + + @VisibleForTesting + void validateErrorCodeExists(Long id) { + if (errorCodeMapper.selectById(id) == null) { + throw exception(ERROR_CODE_NOT_EXISTS); + } + } + + @Override + public ErrorCodeDO getErrorCode(Long id) { + return errorCodeMapper.selectById(id); + } + + @Override + public PageResult getErrorCodePage(ErrorCodePageReqVO pageReqVO) { + return errorCodeMapper.selectPage(pageReqVO); + } + + @Override + @Transactional + public void autoGenerateErrorCodes(List autoGenerateDTOs) { + if (CollUtil.isEmpty(autoGenerateDTOs)) { + return; + } + // 获得错误码 + List errorCodeDOs = errorCodeMapper.selectListByCodes( + convertSet(autoGenerateDTOs, ErrorCodeAutoGenerateReqDTO::getCode)); + Map errorCodeDOMap = convertMap(errorCodeDOs, ErrorCodeDO::getCode); + + // 遍历 autoGenerateBOs 数组,逐个插入或更新。考虑到每次量级不大,就不走批量了 + autoGenerateDTOs.forEach(autoGenerateDTO -> { + ErrorCodeDO errorCode = errorCodeDOMap.get(autoGenerateDTO.getCode()); + // 不存在,则进行新增 + if (errorCode == null) { + errorCode = BeanUtils.toBean(autoGenerateDTO, ErrorCodeDO.class) + .setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()); + errorCodeMapper.insert(errorCode); + return; + } + // 存在,则进行更新。更新有三个前置条件: + // 条件 1. 只更新自动生成的错误码,即 Type 为 ErrorCodeTypeEnum.AUTO_GENERATION + if (!ErrorCodeTypeEnum.AUTO_GENERATION.getType().equals(errorCode.getType())) { + return; + } + // 条件 2. 分组 applicationName 必须匹配,避免存在错误码冲突的情况 + if (!autoGenerateDTO.getApplicationName().equals(errorCode.getApplicationName())) { + log.error("[autoGenerateErrorCodes][自动创建({}/{}) 错误码失败,数据库中已经存在({}/{})]", + autoGenerateDTO.getCode(), autoGenerateDTO.getApplicationName(), + errorCode.getCode(), errorCode.getApplicationName()); + return; + } + // 条件 3. 错误提示语存在差异 + if (autoGenerateDTO.getMessage().equals(errorCode.getMessage())) { + return; + } + // 最终匹配,进行更新 + errorCodeMapper.updateById(new ErrorCodeDO().setId(errorCode.getId()).setMessage(autoGenerateDTO.getMessage())); + }); + } + + @Override + public List getErrorCodeList(String applicationName, LocalDateTime minUpdateTime) { + List list = errorCodeMapper.selectListByApplicationNameAndUpdateTimeGt( + applicationName, minUpdateTime); + return BeanUtils.toBean(list, ErrorCodeRespDTO.class); + } + +} + diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/LoginLogService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/LoginLogService.java new file mode 100644 index 00000000..e7916507 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/LoginLogService.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.service.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.LoginLogDO; + +import javax.validation.Valid; + +/** + * 登录日志 Service 接口 + */ +public interface LoginLogService { + + /** + * 获得登录日志分页 + * + * @param pageReqVO 分页条件 + * @return 登录日志分页 + */ + PageResult getLoginLogPage(LoginLogPageReqVO pageReqVO); + + /** + * 创建登录日志 + * + * @param reqDTO 日志信息 + */ + void createLoginLog(@Valid LoginLogCreateReqDTO reqDTO); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/LoginLogServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/LoginLogServiceImpl.java new file mode 100644 index 00000000..455bab29 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/LoginLogServiceImpl.java @@ -0,0 +1,35 @@ +package com.chanko.yunxi.mes.heli.module.system.service.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.LoginLogDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.logger.LoginLogMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 登录日志 Service 实现 + */ +@Service +@Validated +public class LoginLogServiceImpl implements LoginLogService { + + @Resource + private LoginLogMapper loginLogMapper; + + @Override + public PageResult getLoginLogPage(LoginLogPageReqVO pageReqVO) { + return loginLogMapper.selectPage(pageReqVO); + } + + @Override + public void createLoginLog(LoginLogCreateReqDTO reqDTO) { + LoginLogDO loginLog = BeanUtils.toBean(reqDTO, LoginLogDO.class); + loginLogMapper.insert(loginLog); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/OperateLogService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/OperateLogService.java new file mode 100644 index 00000000..e49407f0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/OperateLogService.java @@ -0,0 +1,30 @@ +package com.chanko.yunxi.mes.heli.module.system.service.logger; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.OperateLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.OperateLogDO; + +/** + * 操作日志 Service 接口 + * + * @author 芋道源码 + */ +public interface OperateLogService { + + /** + * 记录操作日志 + * + * @param createReqDTO 操作日志请求 + */ + void createOperateLog(OperateLogCreateReqDTO createReqDTO); + + /** + * 获得操作日志分页列表 + * + * @param pageReqVO 分页条件 + * @return 操作日志分页列表 + */ + PageResult getOperateLogPage(OperateLogPageReqVO pageReqVO); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/OperateLogServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/OperateLogServiceImpl.java new file mode 100644 index 00000000..bcc323f5 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/logger/OperateLogServiceImpl.java @@ -0,0 +1,63 @@ +package com.chanko.yunxi.mes.heli.module.system.service.logger; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.string.StrUtils; +import com.chanko.yunxi.mes.heli.module.system.api.logger.dto.OperateLogCreateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.OperateLogDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.logger.OperateLogMapper; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; + +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; +import static com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.OperateLogDO.JAVA_METHOD_ARGS_MAX_LENGTH; +import static com.chanko.yunxi.mes.heli.module.system.dal.dataobject.logger.OperateLogDO.RESULT_MAX_LENGTH; + +/** + * 操作日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class OperateLogServiceImpl implements OperateLogService { + + @Resource + private OperateLogMapper operateLogMapper; + + @Resource + private AdminUserService userService; + + @Override + public void createOperateLog(OperateLogCreateReqDTO createReqDTO) { + OperateLogDO log = BeanUtils.toBean(createReqDTO, OperateLogDO.class); + log.setJavaMethodArgs(StrUtils.maxLength(log.getJavaMethodArgs(), JAVA_METHOD_ARGS_MAX_LENGTH)); + log.setResultData(StrUtils.maxLength(log.getResultData(), RESULT_MAX_LENGTH)); + operateLogMapper.insert(log); + } + + @Override + public PageResult getOperateLogPage(OperateLogPageReqVO pageReqVO) { + // 处理基于用户昵称的查询 + Collection userIds = null; + if (StrUtil.isNotEmpty(pageReqVO.getUserNickname())) { + userIds = convertSet(userService.getUserListByNickname(pageReqVO.getUserNickname()), AdminUserDO::getId); + if (CollUtil.isEmpty(userIds)) { + return PageResult.empty(); + } + } + // 查询分页 + return operateLogMapper.selectPage(pageReqVO, userIds); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailAccountService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailAccountService.java new file mode 100644 index 00000000..54411a24 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailAccountService.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.module.system.service.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account.MailAccountSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailAccountDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 邮箱账号 Service 接口 + * + * @author wangjingyi + * @since 2022-03-21 + */ +public interface MailAccountService { + + /** + * 创建邮箱账号 + * + * @param createReqVO 邮箱账号信息 + * @return 编号 + */ + Long createMailAccount(@Valid MailAccountSaveReqVO createReqVO); + + /** + * 修改邮箱账号 + * + * @param updateReqVO 邮箱账号信息 + */ + void updateMailAccount(@Valid MailAccountSaveReqVO updateReqVO); + + /** + * 删除邮箱账号 + * + * @param id 编号 + */ + void deleteMailAccount(Long id); + + /** + * 获取邮箱账号信息 + * + * @param id 编号 + * @return 邮箱账号信息 + */ + MailAccountDO getMailAccount(Long id); + + /** + * 从缓存中获取邮箱账号 + * + * @param id 编号 + * @return 邮箱账号 + */ + MailAccountDO getMailAccountFromCache(Long id); + + /** + * 获取邮箱账号分页信息 + * + * @param pageReqVO 邮箱账号分页参数 + * @return 邮箱账号分页信息 + */ + PageResult getMailAccountPage(MailAccountPageReqVO pageReqVO); + + /** + * 获取邮箱数组信息 + * + * @return 邮箱账号信息数组 + */ + List getMailAccountList(); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailAccountServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailAccountServiceImpl.java new file mode 100644 index 00000000..b49a0a55 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailAccountServiceImpl.java @@ -0,0 +1,99 @@ +package com.chanko.yunxi.mes.heli.module.system.service.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.account.MailAccountSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailAccountDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.mail.MailAccountMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_NOT_EXISTS; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS; + +/** + * 邮箱账号 Service 实现类 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@Service +@Validated +@Slf4j +public class MailAccountServiceImpl implements MailAccountService { + + @Resource + private MailAccountMapper mailAccountMapper; + + @Resource + private MailTemplateService mailTemplateService; + + @Override + public Long createMailAccount(MailAccountSaveReqVO createReqVO) { + MailAccountDO account = BeanUtils.toBean(createReqVO, MailAccountDO.class); + mailAccountMapper.insert(account); + return account.getId(); + } + + @Override + @CacheEvict(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#updateReqVO.id") + public void updateMailAccount(MailAccountSaveReqVO updateReqVO) { + // 校验是否存在 + validateMailAccountExists(updateReqVO.getId()); + + // 更新 + MailAccountDO updateObj = BeanUtils.toBean(updateReqVO, MailAccountDO.class); + mailAccountMapper.updateById(updateObj); + } + + @Override + @CacheEvict(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#id") + public void deleteMailAccount(Long id) { + // 校验是否存在账号 + validateMailAccountExists(id); + // 校验是否存在关联模版 + if (mailTemplateService.getMailTemplateCountByAccountId(id) > 0) { + throw exception(MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS); + } + + // 删除 + mailAccountMapper.deleteById(id); + } + + private void validateMailAccountExists(Long id) { + if (mailAccountMapper.selectById(id) == null) { + throw exception(MAIL_ACCOUNT_NOT_EXISTS); + } + } + + @Override + public MailAccountDO getMailAccount(Long id) { + return mailAccountMapper.selectById(id); + } + + @Override + @Cacheable(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#id", unless = "#result == null") + public MailAccountDO getMailAccountFromCache(Long id) { + return getMailAccount(id); + } + + @Override + public PageResult getMailAccountPage(MailAccountPageReqVO pageReqVO) { + return mailAccountMapper.selectPage(pageReqVO); + } + + @Override + public List getMailAccountList() { + return mailAccountMapper.selectList(); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailLogService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailLogService.java new file mode 100644 index 00000000..02985e00 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailLogService.java @@ -0,0 +1,61 @@ +package com.chanko.yunxi.mes.heli.module.system.service.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailAccountDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailLogDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailTemplateDO; + +import java.util.Map; + +/** + * 邮件日志 Service 接口 + * + * @author wangjingyi + * @since 2022-03-21 + */ +public interface MailLogService { + + /** + * 邮件日志分页 + * + * @param pageVO 分页参数 + * @return 分页结果 + */ + PageResult getMailLogPage(MailLogPageReqVO pageVO); + + /** + * 获得指定编号的邮件日志 + * + * @param id 日志编号 + * @return 邮件日志 + */ + MailLogDO getMailLog(Long id); + + /** + * 创建邮件日志 + * + * @param userId 用户编码 + * @param userType 用户类型 + * @param toMail 收件人邮件 + * @param account 邮件账号信息 + * @param template 模版信息 + * @param templateContent 模版内容 + * @param templateParams 模版参数 + * @param isSend 是否发送成功 + * @return 日志编号 + */ + Long createMailLog(Long userId, Integer userType, String toMail, + MailAccountDO account, MailTemplateDO template , + String templateContent, Map templateParams, Boolean isSend); + + /** + * 更新邮件发送结果 + * + * @param logId 日志编号 + * @param messageId 发送后的消息编号 + * @param exception 发送异常 + */ + void updateMailSendResult(Long logId, String messageId, Exception exception); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailLogServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailLogServiceImpl.java new file mode 100644 index 00000000..49f2c83f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailLogServiceImpl.java @@ -0,0 +1,78 @@ +package com.chanko.yunxi.mes.heli.module.system.service.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailAccountDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailLogDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.mail.MailLogMapper; +import com.chanko.yunxi.mes.heli.module.system.enums.mail.MailSendStatusEnum; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Objects; + +import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; + +/** + * 邮件日志 Service 实现类 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@Service +@Validated +public class MailLogServiceImpl implements MailLogService { + + @Resource + private MailLogMapper mailLogMapper; + + @Override + public PageResult getMailLogPage(MailLogPageReqVO pageVO) { + return mailLogMapper.selectPage(pageVO); + } + + @Override + public MailLogDO getMailLog(Long id) { + return mailLogMapper.selectById(id); + } + + @Override + public Long createMailLog(Long userId, Integer userType, String toMail, + MailAccountDO account, MailTemplateDO template, + String templateContent, Map templateParams, Boolean isSend) { + MailLogDO.MailLogDOBuilder logDOBuilder = MailLogDO.builder(); + // 根据是否要发送,设置状态 + logDOBuilder.sendStatus(Objects.equals(isSend, true) ? MailSendStatusEnum.INIT.getStatus() + : MailSendStatusEnum.IGNORE.getStatus()) + // 用户信息 + .userId(userId).userType(userType).toMail(toMail) + .accountId(account.getId()).fromMail(account.getMail()) + // 模板相关字段 + .templateId(template.getId()).templateCode(template.getCode()).templateNickname(template.getNickname()) + .templateTitle(template.getTitle()).templateContent(templateContent).templateParams(templateParams); + + // 插入数据库 + MailLogDO logDO = logDOBuilder.build(); + mailLogMapper.insert(logDO); + return logDO.getId(); + } + + @Override + public void updateMailSendResult(Long logId, String messageId, Exception exception) { + // 1. 成功 + if (exception == null) { + mailLogMapper.updateById(new MailLogDO().setId(logId).setSendTime(LocalDateTime.now()) + .setSendStatus(MailSendStatusEnum.SUCCESS.getStatus()).setSendMessageId(messageId)); + return; + } + // 2. 失败 + mailLogMapper.updateById(new MailLogDO().setId(logId).setSendTime(LocalDateTime.now()) + .setSendStatus(MailSendStatusEnum.FAILURE.getStatus()).setSendException(getRootCauseMessage(exception))); + + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailSendService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailSendService.java new file mode 100644 index 00000000..de91f2dc --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailSendService.java @@ -0,0 +1,60 @@ +package com.chanko.yunxi.mes.heli.module.system.service.mail; + +import com.chanko.yunxi.mes.heli.module.system.mq.message.mail.MailSendMessage; + +import java.util.Map; + +/** + * 邮件发送 Service 接口 + * + * @author wangjingyi + * @since 2022-03-21 + */ +public interface MailSendService { + + /** + * 发送单条邮件给管理后台的用户 + * + * @param mail 邮箱 + * @param userId 用户编码 + * @param templateCode 邮件模版编码 + * @param templateParams 邮件模版参数 + * @return 发送日志编号 + */ + Long sendSingleMailToAdmin(String mail, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条邮件给用户 APP 的用户 + * + * @param mail 邮箱 + * @param userId 用户编码 + * @param templateCode 邮件模版编码 + * @param templateParams 邮件模版参数 + * @return 发送日志编号 + */ + Long sendSingleMailToMember(String mail, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条邮件给用户 + * + * @param mail 邮箱 + * @param userId 用户编码 + * @param userType 用户类型 + * @param templateCode 邮件模版编码 + * @param templateParams 邮件模版参数 + * @return 发送日志编号 + */ + Long sendSingleMail(String mail, Long userId, Integer userType, + String templateCode, Map templateParams); + + /** + * 执行真正的邮件发送 + * 注意,该方法仅仅提供给 MQ Consumer 使用 + * + * @param message 邮件 + */ + void doSendMail(MailSendMessage message); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailSendServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailSendServiceImpl.java new file mode 100644 index 00000000..f5c58be9 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailSendServiceImpl.java @@ -0,0 +1,167 @@ +package com.chanko.yunxi.mes.heli.module.system.service.mail; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.mail.MailAccount; +import cn.hutool.extra.mail.MailUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.convert.mail.MailAccountConvert; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailAccountDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.mq.message.mail.MailSendMessage; +import com.chanko.yunxi.mes.heli.module.system.mq.producer.mail.MailProducer; +import com.chanko.yunxi.mes.heli.module.system.service.member.MemberService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Map; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 邮箱发送 Service 实现类 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@Service +@Validated +@Slf4j +public class MailSendServiceImpl implements MailSendService { + + @Resource + private AdminUserService adminUserService; + @Resource + private MemberService memberService; + + @Resource + private MailAccountService mailAccountService; + @Resource + private MailTemplateService mailTemplateService; + + @Resource + private MailLogService mailLogService; + @Resource + private MailProducer mailProducer; + + @Override + public Long sendSingleMailToAdmin(String mail, Long userId, + String templateCode, Map templateParams) { + // 如果 mail 为空,则加载用户编号对应的邮箱 + if (StrUtil.isEmpty(mail)) { + AdminUserDO user = adminUserService.getUser(userId); + if (user != null) { + mail = user.getEmail(); + } + } + // 执行发送 + return sendSingleMail(mail, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleMailToMember(String mail, Long userId, + String templateCode, Map templateParams) { + // 如果 mail 为空,则加载用户编号对应的邮箱 + if (StrUtil.isEmpty(mail)) { + mail = memberService.getMemberUserEmail(userId); + } + // 执行发送 + return sendSingleMail(mail, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleMail(String mail, Long userId, Integer userType, + String templateCode, Map templateParams) { + // 校验邮箱模版是否合法 + MailTemplateDO template = validateMailTemplate(templateCode); + // 校验邮箱账号是否合法 + MailAccountDO account = validateMailAccount(template.getAccountId()); + + // 校验邮箱是否存在 + mail = validateMail(mail); + validateTemplateParams(template, templateParams); + + // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 + Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); + String title = mailTemplateService.formatMailTemplateContent(template.getTitle(), templateParams); + String content = mailTemplateService.formatMailTemplateContent(template.getContent(), templateParams); + Long sendLogId = mailLogService.createMailLog(userId, userType, mail, + account, template, content, templateParams, isSend); + // 发送 MQ 消息,异步执行发送短信 + if (isSend) { + mailProducer.sendMailSendMessage(sendLogId, mail, account.getId(), + template.getNickname(), title, content); + } + return sendLogId; + } + + @Override + public void doSendMail(MailSendMessage message) { + // 1. 创建发送账号 + MailAccountDO account = validateMailAccount(message.getAccountId()); + MailAccount mailAccount = MailAccountConvert.INSTANCE.convert(account, message.getNickname()); + // 2. 发送邮件 + try { + String messageId = MailUtil.send(mailAccount, message.getMail(), + message.getTitle(), message.getContent(),true); + // 3. 更新结果(成功) + mailLogService.updateMailSendResult(message.getLogId(), messageId, null); + } catch (Exception e) { + // 3. 更新结果(异常) + mailLogService.updateMailSendResult(message.getLogId(), null, e); + } + } + + @VisibleForTesting + MailTemplateDO validateMailTemplate(String templateCode) { + // 获得邮件模板。考虑到效率,从缓存中获取 + MailTemplateDO template = mailTemplateService.getMailTemplateByCodeFromCache(templateCode); + // 邮件模板不存在 + if (template == null) { + throw exception(MAIL_TEMPLATE_NOT_EXISTS); + } + return template; + } + + @VisibleForTesting + MailAccountDO validateMailAccount(Long accountId) { + // 获得邮箱账号。考虑到效率,从缓存中获取 + MailAccountDO account = mailAccountService.getMailAccountFromCache(accountId); + // 邮箱账号不存在 + if (account == null) { + throw exception(MAIL_ACCOUNT_NOT_EXISTS); + } + return account; + } + + @VisibleForTesting + String validateMail(String mail) { + if (StrUtil.isEmpty(mail)) { + throw exception(MAIL_SEND_MAIL_NOT_EXISTS); + } + return mail; + } + + /** + * 校验邮件参数是否确实 + * + * @param template 邮箱模板 + * @param templateParams 参数列表 + */ + @VisibleForTesting + void validateTemplateParams(MailTemplateDO template, Map templateParams) { + template.getParams().forEach(key -> { + Object value = templateParams.get(key); + if (value == null) { + throw exception(MAIL_SEND_TEMPLATE_PARAM_MISS, key); + } + }); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailTemplateService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailTemplateService.java new file mode 100644 index 00000000..97e46433 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailTemplateService.java @@ -0,0 +1,90 @@ +package com.chanko.yunxi.mes.heli.module.system.service.mail; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template.MailTemplateSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailTemplateDO; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +/** + * 邮件模版 Service 接口 + * + * @author wangjingyi + * @since 2022-03-21 + */ +public interface MailTemplateService { + + /** + * 邮件模版创建 + * + * @param createReqVO 邮件信息 + * @return 编号 + */ + Long createMailTemplate(@Valid MailTemplateSaveReqVO createReqVO); + + /** + * 邮件模版修改 + * + * @param updateReqVO 邮件信息 + */ + void updateMailTemplate(@Valid MailTemplateSaveReqVO updateReqVO); + + /** + * 邮件模版删除 + * + * @param id 编号 + */ + void deleteMailTemplate(Long id); + + /** + * 获取邮件模版 + * + * @param id 编号 + * @return 邮件模版 + */ + MailTemplateDO getMailTemplate(Long id); + + /** + * 获取邮件模版分页 + * + * @param pageReqVO 模版信息 + * @return 邮件模版分页信息 + */ + PageResult getMailTemplatePage(MailTemplatePageReqVO pageReqVO); + + /** + * 获取邮件模板数组 + * + * @return 模版数组 + */ + List getMailTemplateList(); + + /** + * 从缓存中获取邮件模版 + * + * @param code 模板编码 + * @return 邮件模板 + */ + MailTemplateDO getMailTemplateByCodeFromCache(String code); + + /** + * 邮件模版内容合成 + * + * @param content 邮件模版 + * @param params 合成参数 + * @return 格式化后的内容 + */ + String formatMailTemplateContent(String content, Map params); + + /** + * 获得指定邮件账号下的邮件模板数量 + * + * @param accountId 账号编号 + * @return 数量 + */ + long getMailTemplateCountByAccountId(Long accountId); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailTemplateServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailTemplateServiceImpl.java new file mode 100644 index 00000000..3133a664 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/mail/MailTemplateServiceImpl.java @@ -0,0 +1,138 @@ +package com.chanko.yunxi.mes.heli.module.system.service.mail; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.mail.vo.template.MailTemplateSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.mail.MailTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.mail.MailTemplateMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_CODE_EXISTS; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_NOT_EXISTS; + +/** + * 邮箱模版 Service 实现类 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@Service +@Validated +@Slf4j +public class MailTemplateServiceImpl implements MailTemplateService { + + /** + * 正则表达式,匹配 {} 中的变量 + */ + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); + + @Resource + private MailTemplateMapper mailTemplateMapper; + + @Override + public Long createMailTemplate(MailTemplateSaveReqVO createReqVO) { + // 校验 code 是否唯一 + validateCodeUnique(null, createReqVO.getCode()); + + // 插入 + MailTemplateDO template = BeanUtils.toBean(createReqVO, MailTemplateDO.class) + .setParams(parseTemplateContentParams(createReqVO.getContent())); + mailTemplateMapper.insert(template); + return template.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理 + public void updateMailTemplate(@Valid MailTemplateSaveReqVO updateReqVO) { + // 校验是否存在 + validateMailTemplateExists(updateReqVO.getId()); + // 校验 code 是否唯一 + validateCodeUnique(updateReqVO.getId(),updateReqVO.getCode()); + + // 更新 + MailTemplateDO updateObj = BeanUtils.toBean(updateReqVO, MailTemplateDO.class) + .setParams(parseTemplateContentParams(updateReqVO.getContent())); + mailTemplateMapper.updateById(updateObj); + } + + @VisibleForTesting + void validateCodeUnique(Long id, String code) { + MailTemplateDO template = mailTemplateMapper.selectByCode(code); + if (template == null) { + return; + } + // 存在 template 记录的情况下 + if (id == null // 新增时,说明重复 + || ObjUtil.notEqual(id, template.getId())) { // 更新时,如果 id 不一致,说明重复 + throw exception(MAIL_TEMPLATE_CODE_EXISTS); + } + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理 + public void deleteMailTemplate(Long id) { + // 校验是否存在 + validateMailTemplateExists(id); + + // 删除 + mailTemplateMapper.deleteById(id); + } + + private void validateMailTemplateExists(Long id) { + if (mailTemplateMapper.selectById(id) == null) { + throw exception(MAIL_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public MailTemplateDO getMailTemplate(Long id) {return mailTemplateMapper.selectById(id);} + + @Override + @Cacheable(value = RedisKeyConstants.MAIL_TEMPLATE, key = "#code", unless = "#result == null") + public MailTemplateDO getMailTemplateByCodeFromCache(String code) { + return mailTemplateMapper.selectByCode(code); + } + + @Override + public PageResult getMailTemplatePage(MailTemplatePageReqVO pageReqVO) { + return mailTemplateMapper.selectPage(pageReqVO); + } + + @Override + public List getMailTemplateList() {return mailTemplateMapper.selectList();} + + @Override + public String formatMailTemplateContent(String content, Map params) { + return StrUtil.format(content, params); + } + + @VisibleForTesting + public List parseTemplateContentParams(String content) { + return ReUtil.findAllGroup1(PATTERN_PARAMS, content); + } + + @Override + public long getMailTemplateCountByAccountId(Long accountId) { + return mailTemplateMapper.selectCountByAccountId(accountId); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/member/MemberService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/member/MemberService.java new file mode 100644 index 00000000..39cbe5c3 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/member/MemberService.java @@ -0,0 +1,26 @@ +package com.chanko.yunxi.mes.heli.module.system.service.member; + +/** + * Member Service 接口 + * + * @author 芋道源码 + */ +public interface MemberService { + + /** + * 获得会员用户的手机号码 + * + * @param id 会员用户编号 + * @return 手机号码 + */ + String getMemberUserMobile(Long id); + + /** + * 获得会员用户的邮箱 + * + * @param id 会员用户编号 + * @return 邮箱 + */ + String getMemberUserEmail(Long id); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/member/MemberServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/member/MemberServiceImpl.java new file mode 100644 index 00000000..79f60096 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/member/MemberServiceImpl.java @@ -0,0 +1,54 @@ +package com.chanko.yunxi.mes.heli.module.system.service.member; + +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * Member Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class MemberServiceImpl implements MemberService { + + @Value("${mes.info.base-package}") + private String basePackage; + + private volatile Object memberUserApi; + + @Override + public String getMemberUserMobile(Long id) { + Object user = getMemberUser(id); + if (user == null) { + return null; + } + return ReflectUtil.invoke(user, "getMobile"); + } + + @Override + public String getMemberUserEmail(Long id) { + Object user = getMemberUser(id); + if (user == null) { + return null; + } + return ReflectUtil.invoke(user, "getEmail"); + } + + private Object getMemberUser(Long id) { + if (id == null) { + return null; + } + return ReflectUtil.invoke(getMemberUserApi(), "getUser", id); + } + + private Object getMemberUserApi() { + if (memberUserApi == null) { + memberUserApi = SpringUtil.getBean(ClassUtil.loadClass(String.format("%s.module.member.api.user.MemberUserApi", basePackage))); + } + return memberUserApi; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/member/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/member/package-info.java new file mode 100644 index 00000000..583931a3 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/member/package-info.java @@ -0,0 +1,4 @@ +/** + * mes-module-member 模块的适配,解除 mes-module-system 对它们的依赖 + */ +package com.chanko.yunxi.mes.heli.module.system.service.member; diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notice/NoticeService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notice/NoticeService.java new file mode 100644 index 00000000..db54dcf4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notice/NoticeService.java @@ -0,0 +1,51 @@ +package com.chanko.yunxi.mes.heli.module.system.service.notice; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo.NoticePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo.NoticeSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notice.NoticeDO; + +/** + * 通知公告 Service 接口 + */ +public interface NoticeService { + + /** + * 创建通知公告 + * + * @param createReqVO 通知公告 + * @return 编号 + */ + Long createNotice(NoticeSaveReqVO createReqVO); + + /** + * 更新通知公告 + * + * @param reqVO 通知公告 + */ + void updateNotice(NoticeSaveReqVO reqVO); + + /** + * 删除通知公告 + * + * @param id 编号 + */ + void deleteNotice(Long id); + + /** + * 获得通知公告分页列表 + * + * @param reqVO 分页条件 + * @return 部门分页列表 + */ + PageResult getNoticePage(NoticePageReqVO reqVO); + + /** + * 获得通知公告 + * + * @param id 编号 + * @return 通知公告 + */ + NoticeDO getNotice(Long id); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notice/NoticeServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notice/NoticeServiceImpl.java new file mode 100644 index 00000000..a51666f4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notice/NoticeServiceImpl.java @@ -0,0 +1,73 @@ +package com.chanko.yunxi.mes.heli.module.system.service.notice; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo.NoticePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notice.vo.NoticeSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notice.NoticeDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.notice.NoticeMapper; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND; + +/** + * 通知公告 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class NoticeServiceImpl implements NoticeService { + + @Resource + private NoticeMapper noticeMapper; + + @Override + public Long createNotice(NoticeSaveReqVO createReqVO) { + NoticeDO notice = BeanUtils.toBean(createReqVO, NoticeDO.class); + noticeMapper.insert(notice); + return notice.getId(); + } + + @Override + public void updateNotice(NoticeSaveReqVO updateReqVO) { + // 校验是否存在 + validateNoticeExists(updateReqVO.getId()); + // 更新通知公告 + NoticeDO updateObj = BeanUtils.toBean(updateReqVO, NoticeDO.class); + noticeMapper.updateById(updateObj); + } + + @Override + public void deleteNotice(Long id) { + // 校验是否存在 + validateNoticeExists(id); + // 删除通知公告 + noticeMapper.deleteById(id); + } + + @Override + public PageResult getNoticePage(NoticePageReqVO reqVO) { + return noticeMapper.selectPage(reqVO); + } + + @Override + public NoticeDO getNotice(Long id) { + return noticeMapper.selectById(id); + } + + @VisibleForTesting + public void validateNoticeExists(Long id) { + if (id == null) { + return; + } + NoticeDO notice = noticeMapper.selectById(id); + if (notice == null) { + throw exception(NOTICE_NOT_FOUND); + } + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyMessageService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyMessageService.java new file mode 100644 index 00000000..81eaaab6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyMessageService.java @@ -0,0 +1,97 @@ +package com.chanko.yunxi.mes.heli.module.system.service.notify; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyMessageDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyTemplateDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 站内信 Service 接口 + * + * @author xrcoder + */ +public interface NotifyMessageService { + + /** + * 创建站内信 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param template 模版信息 + * @param templateContent 模版内容 + * @param templateParams 模版参数 + * @return 站内信编号 + */ + Long createNotifyMessage(Long userId, Integer userType, + NotifyTemplateDO template, String templateContent, Map templateParams); + + /** + * 获得站内信分页 + * + * @param pageReqVO 分页查询 + * @return 站内信分页 + */ + PageResult getNotifyMessagePage(NotifyMessagePageReqVO pageReqVO); + + /** + * 获得【我的】站内信分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @param userType 用户类型 + * @return 站内信分页 + */ + PageResult getMyMyNotifyMessagePage(NotifyMessageMyPageReqVO pageReqVO, Long userId, Integer userType); + + /** + * 获得站内信 + * + * @param id 编号 + * @return 站内信 + */ + NotifyMessageDO getNotifyMessage(Long id); + + /** + * 获得【我的】未读站内信列表 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param size 数量 + * @return 站内信列表 + */ + List getUnreadNotifyMessageList(Long userId, Integer userType, Integer size); + + /** + * 统计用户未读站内信条数 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 返回未读站内信条数 + */ + Long getUnreadNotifyMessageCount(Long userId, Integer userType); + + /** + * 标记站内信为已读 + * + * @param ids 站内信编号集合 + * @param userId 用户编号 + * @param userType 用户类型 + * @return 更新到的条数 + */ + int updateNotifyMessageRead(Collection ids, Long userId, Integer userType); + + /** + * 标记所有站内信为已读 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 更新到的条数 + */ + int updateAllNotifyMessageRead(Long userId, Integer userType); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyMessageServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyMessageServiceImpl.java new file mode 100644 index 00000000..5b12bd3e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyMessageServiceImpl.java @@ -0,0 +1,75 @@ +package com.chanko.yunxi.mes.heli.module.system.service.notify; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyMessageDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.notify.NotifyMessageMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 站内信 Service 实现类 + * + * @author xrcoder + */ +@Service +@Validated +public class NotifyMessageServiceImpl implements NotifyMessageService { + + @Resource + private NotifyMessageMapper notifyMessageMapper; + + @Override + public Long createNotifyMessage(Long userId, Integer userType, + NotifyTemplateDO template, String templateContent, Map templateParams) { + NotifyMessageDO message = new NotifyMessageDO().setUserId(userId).setUserType(userType) + .setTemplateId(template.getId()).setTemplateCode(template.getCode()) + .setTemplateType(template.getType()).setTemplateNickname(template.getNickname()) + .setTemplateContent(templateContent).setTemplateParams(templateParams).setReadStatus(false); + notifyMessageMapper.insert(message); + return message.getId(); + } + + @Override + public PageResult getNotifyMessagePage(NotifyMessagePageReqVO pageReqVO) { + return notifyMessageMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getMyMyNotifyMessagePage(NotifyMessageMyPageReqVO pageReqVO, Long userId, Integer userType) { + return notifyMessageMapper.selectPage(pageReqVO, userId, userType); + } + + @Override + public NotifyMessageDO getNotifyMessage(Long id) { + return notifyMessageMapper.selectById(id); + } + + @Override + public List getUnreadNotifyMessageList(Long userId, Integer userType, Integer size) { + return notifyMessageMapper.selectUnreadListByUserIdAndUserType(userId, userType, size); + } + + @Override + public Long getUnreadNotifyMessageCount(Long userId, Integer userType) { + return notifyMessageMapper.selectUnreadCountByUserIdAndUserType(userId, userType); + } + + @Override + public int updateNotifyMessageRead(Collection ids, Long userId, Integer userType) { + return notifyMessageMapper.updateListRead(ids, userId, userType); + } + + @Override + public int updateAllNotifyMessageRead(Long userId, Integer userType) { + return notifyMessageMapper.updateListRead(userId, userType); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifySendService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifySendService.java new file mode 100644 index 00000000..b6bf1a97 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifySendService.java @@ -0,0 +1,55 @@ +package com.chanko.yunxi.mes.heli.module.system.service.notify; + +import java.util.List; +import java.util.Map; + +/** + * 站内信发送 Service 接口 + * + * @author xrcoder + */ +public interface NotifySendService { + + /** + * 发送单条站内信给管理后台的用户 + * + * 在 mobile 为空时,使用 userId 加载对应管理员的手机号 + * + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleNotifyToAdmin(Long userId, + String templateCode, Map templateParams); + /** + * 发送单条站内信给用户 APP 的用户 + * + * 在 mobile 为空时,使用 userId 加载对应会员的手机号 + * + * @param userId 用户编号 + * @param templateCode 站内信模板编号 + * @param templateParams 站内信模板参数 + * @return 发送日志编号 + */ + Long sendSingleNotifyToMember(Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条站内信给用户 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param templateCode 站内信模板编号 + * @param templateParams 站内信模板参数 + * @return 发送日志编号 + */ + Long sendSingleNotify( Long userId, Integer userType, + String templateCode, Map templateParams); + + default void sendBatchNotify(List mobiles, List userIds, Integer userType, + String templateCode, Map templateParams) { + throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!"); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifySendServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifySendServiceImpl.java new file mode 100644 index 00000000..c568be5b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifySendServiceImpl.java @@ -0,0 +1,86 @@ +package com.chanko.yunxi.mes.heli.module.system.service.notify; + +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Map; +import java.util.Objects; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 站内信发送 Service 实现类 + * + * @author xrcoder + */ +@Service +@Validated +@Slf4j +public class NotifySendServiceImpl implements NotifySendService { + + @Resource + private NotifyTemplateService notifyTemplateService; + + @Resource + private NotifyMessageService notifyMessageService; + + @Override + public Long sendSingleNotifyToAdmin(Long userId, String templateCode, Map templateParams) { + return sendSingleNotify(userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleNotifyToMember(Long userId, String templateCode, Map templateParams) { + return sendSingleNotify(userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleNotify(Long userId, Integer userType, String templateCode, Map templateParams) { + // 校验模版 + NotifyTemplateDO template = validateNotifyTemplate(templateCode); + if (Objects.equals(template.getStatus(), CommonStatusEnum.DISABLE.getStatus())) { + log.info("[sendSingleNotify][模版({})已经关闭,无法给用户({}/{})发送]", templateCode, userId, userType); + return null; + } + // 校验参数 + validateTemplateParams(template, templateParams); + + // 发送站内信 + String content = notifyTemplateService.formatNotifyTemplateContent(template.getContent(), templateParams); + return notifyMessageService.createNotifyMessage(userId, userType, template, content, templateParams); + } + + @VisibleForTesting + public NotifyTemplateDO validateNotifyTemplate(String templateCode) { + // 获得站内信模板。考虑到效率,从缓存中获取 + NotifyTemplateDO template = notifyTemplateService.getNotifyTemplateByCodeFromCache(templateCode); + // 站内信模板不存在 + if (template == null) { + throw exception(NOTICE_NOT_FOUND); + } + return template; + } + + /** + * 校验站内信模版参数是否确实 + * + * @param template 邮箱模板 + * @param templateParams 参数列表 + */ + @VisibleForTesting + public void validateTemplateParams(NotifyTemplateDO template, Map templateParams) { + template.getParams().forEach(key -> { + Object value = templateParams.get(key); + if (value == null) { + throw exception(NOTIFY_SEND_TEMPLATE_PARAM_MISS, key); + } + }); + } +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyTemplateService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyTemplateService.java new file mode 100644 index 00000000..a662fbff --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyTemplateService.java @@ -0,0 +1,73 @@ +package com.chanko.yunxi.mes.heli.module.system.service.notify; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template.NotifyTemplateSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyTemplateDO; + +import javax.validation.Valid; +import java.util.Map; + +/** + * 站内信模版 Service 接口 + * + * @author xrcoder + */ +public interface NotifyTemplateService { + + /** + * 创建站内信模版 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createNotifyTemplate(@Valid NotifyTemplateSaveReqVO createReqVO); + + /** + * 更新站内信模版 + * + * @param updateReqVO 更新信息 + */ + void updateNotifyTemplate(@Valid NotifyTemplateSaveReqVO updateReqVO); + + /** + * 删除站内信模版 + * + * @param id 编号 + */ + void deleteNotifyTemplate(Long id); + + /** + * 获得站内信模版 + * + * @param id 编号 + * @return 站内信模版 + */ + NotifyTemplateDO getNotifyTemplate(Long id); + + /** + * 获得站内信模板,从缓存中 + * + * @param code 模板编码 + * @return 站内信模板 + */ + NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code); + + /** + * 获得站内信模版分页 + * + * @param pageReqVO 分页查询 + * @return 站内信模版分页 + */ + PageResult getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO); + + /** + * 格式化站内信内容 + * + * @param content 站内信模板的内容 + * @param params 站内信内容的参数 + * @return 格式化后的内容 + */ + String formatNotifyTemplateContent(String content, Map params); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyTemplateServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyTemplateServiceImpl.java new file mode 100644 index 00000000..809d985a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/notify/NotifyTemplateServiceImpl.java @@ -0,0 +1,138 @@ +package com.chanko.yunxi.mes.heli.module.system.service.notify; + +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.notify.vo.template.NotifyTemplateSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.notify.NotifyTemplateMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_CODE_DUPLICATE; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_NOT_EXISTS; + +/** + * 站内信模版 Service 实现类 + * + * @author xrcoder + */ +@Service +@Validated +@Slf4j +public class NotifyTemplateServiceImpl implements NotifyTemplateService { + + /** + * 正则表达式,匹配 {} 中的变量 + */ + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); + + @Resource + private NotifyTemplateMapper notifyTemplateMapper; + + @Override + public Long createNotifyTemplate(NotifyTemplateSaveReqVO createReqVO) { + // 校验站内信编码是否重复 + validateNotifyTemplateCodeDuplicate(null, createReqVO.getCode()); + + // 插入 + NotifyTemplateDO notifyTemplate = BeanUtils.toBean(createReqVO, NotifyTemplateDO.class); + notifyTemplate.setParams(parseTemplateContentParams(notifyTemplate.getContent())); + notifyTemplateMapper.insert(notifyTemplate); + return notifyTemplate.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理 + public void updateNotifyTemplate(NotifyTemplateSaveReqVO updateReqVO) { + // 校验存在 + validateNotifyTemplateExists(updateReqVO.getId()); + // 校验站内信编码是否重复 + validateNotifyTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode()); + + // 更新 + NotifyTemplateDO updateObj = BeanUtils.toBean(updateReqVO, NotifyTemplateDO.class); + updateObj.setParams(parseTemplateContentParams(updateObj.getContent())); + notifyTemplateMapper.updateById(updateObj); + } + + @VisibleForTesting + public List parseTemplateContentParams(String content) { + return ReUtil.findAllGroup1(PATTERN_PARAMS, content); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理 + public void deleteNotifyTemplate(Long id) { + // 校验存在 + validateNotifyTemplateExists(id); + // 删除 + notifyTemplateMapper.deleteById(id); + } + + private void validateNotifyTemplateExists(Long id) { + if (notifyTemplateMapper.selectById(id) == null) { + throw exception(NOTIFY_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public NotifyTemplateDO getNotifyTemplate(Long id) { + return notifyTemplateMapper.selectById(id); + } + + @Override + @Cacheable(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, key = "#code", + unless = "#result == null") + public NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code) { + return notifyTemplateMapper.selectByCode(code); + } + + @Override + public PageResult getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO) { + return notifyTemplateMapper.selectPage(pageReqVO); + } + + @VisibleForTesting + void validateNotifyTemplateCodeDuplicate(Long id, String code) { + NotifyTemplateDO template = notifyTemplateMapper.selectByCode(code); + if (template == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code); + } + if (!template.getId().equals(id)) { + throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code); + } + } + + /** + * 格式化站内信内容 + * + * @param content 站内信模板的内容 + * @param params 站内信内容的参数 + * @return 格式化后的内容 + */ + @Override + public String formatNotifyTemplateContent(String content, Map params) { + return StrUtil.format(content, params); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ApproveService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ApproveService.java new file mode 100644 index 00000000..aa1bd721 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ApproveService.java @@ -0,0 +1,52 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * OAuth2 批准 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 ApprovalStoreUserApprovalHandler 的功能,记录用户针对指定客户端的授权,减少手动确定。 + * + * @author 芋道源码 + */ +public interface OAuth2ApproveService { + + /** + * 获得指定用户,针对指定客户端的指定授权,是否通过 + * + * 参考 ApprovalStoreUserApprovalHandler 的 checkForPreApproval 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param requestedScopes 授权范围 + * @return 是否授权通过 + */ + boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection requestedScopes); + + /** + * 在用户发起批准时,基于 scopes 的选项,计算最终是否通过 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param requestedScopes 授权范围 + * @return 是否授权通过 + */ + boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map requestedScopes); + + /** + * 获得用户的批准列表,排除已过期的 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @return 是否授权通过 + */ + List getApproveList(Long userId, Integer userType, String clientId); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ApproveServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ApproveServiceImpl.java new file mode 100644 index 00000000..b95bf522 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ApproveServiceImpl.java @@ -0,0 +1,103 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2.OAuth2ApproveMapper; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.*; + +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * OAuth2 批准 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class OAuth2ApproveServiceImpl implements OAuth2ApproveService { + + /** + * 批准的过期时间,默认 30 天 + */ + private static final Integer TIMEOUT = 30 * 24 * 60 * 60; // 单位:秒 + + @Resource + private OAuth2ClientService oauth2ClientService; + + @Resource + private OAuth2ApproveMapper oauth2ApproveMapper; + + @Override + @Transactional + public boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection requestedScopes) { + // 第一步,基于 Client 的自动授权计算,如果 scopes 都在自动授权中,则返回 true 通过 + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + Assert.notNull(clientDO, "客户端不能为空"); // 防御性编程 + if (CollUtil.containsAll(clientDO.getAutoApproveScopes(), requestedScopes)) { + // gh-877 - if all scopes are auto approved, approvals still need to be added to the approval store. + LocalDateTime expireTime = LocalDateTime.now().plusSeconds(TIMEOUT); + for (String scope : requestedScopes) { + saveApprove(userId, userType, clientId, scope, true, expireTime); + } + return true; + } + + // 第二步,算上用户已经批准的授权。如果 scopes 都包含,则返回 true + List approveDOs = getApproveList(userId, userType, clientId); + Set scopes = convertSet(approveDOs, OAuth2ApproveDO::getScope, + OAuth2ApproveDO::getApproved); // 只保留未过期的 + 同意的 + return CollUtil.containsAll(scopes, requestedScopes); + } + + @Override + @Transactional + public boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map requestedScopes) { + // 如果 requestedScopes 为空,说明没有要求,则返回 true 通过 + if (CollUtil.isEmpty(requestedScopes)) { + return true; + } + + // 更新批准的信息 + boolean success = false; // 需要至少有一个同意 + LocalDateTime expireTime = LocalDateTime.now().plusSeconds(TIMEOUT); + for (Map.Entry entry : requestedScopes.entrySet()) { + if (entry.getValue()) { + success = true; + } + saveApprove(userId, userType, clientId, entry.getKey(), entry.getValue(), expireTime); + } + return success; + } + + @Override + public List getApproveList(Long userId, Integer userType, String clientId) { + List approveDOs = oauth2ApproveMapper.selectListByUserIdAndUserTypeAndClientId( + userId, userType, clientId); + approveDOs.removeIf(o -> DateUtils.isExpired(o.getExpiresTime())); + return approveDOs; + } + + @VisibleForTesting + void saveApprove(Long userId, Integer userType, String clientId, + String scope, Boolean approved, LocalDateTime expireTime) { + // 先更新 + OAuth2ApproveDO approveDO = new OAuth2ApproveDO().setUserId(userId).setUserType(userType) + .setClientId(clientId).setScope(scope).setApproved(approved).setExpiresTime(expireTime); + if (oauth2ApproveMapper.update(approveDO) == 1) { + return; + } + // 失败,则说明不存在,进行更新 + oauth2ApproveMapper.insert(approveDO); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ClientService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ClientService.java new file mode 100644 index 00000000..bb813da1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ClientService.java @@ -0,0 +1,90 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client.OAuth2ClientSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ClientDO; + +import javax.validation.Valid; +import java.util.Collection; + +/** + * OAuth2.0 Client Service 接口 + * + * 从功能上,和 JdbcClientDetailsService 的功能,提供客户端的操作 + * + * @author 芋道源码 + */ +public interface OAuth2ClientService { + + /** + * 创建 OAuth2 客户端 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createOAuth2Client(@Valid OAuth2ClientSaveReqVO createReqVO); + + /** + * 更新 OAuth2 客户端 + * + * @param updateReqVO 更新信息 + */ + void updateOAuth2Client(@Valid OAuth2ClientSaveReqVO updateReqVO); + + /** + * 删除 OAuth2 客户端 + * + * @param id 编号 + */ + void deleteOAuth2Client(Long id); + + /** + * 获得 OAuth2 客户端 + * + * @param id 编号 + * @return OAuth2 客户端 + */ + OAuth2ClientDO getOAuth2Client(Long id); + + /** + * 获得 OAuth2 客户端,从缓存中 + * + * @param clientId 客户端编号 + * @return OAuth2 客户端 + */ + OAuth2ClientDO getOAuth2ClientFromCache(String clientId); + + /** + * 获得 OAuth2 客户端分页 + * + * @param pageReqVO 分页查询 + * @return OAuth2 客户端分页 + */ + PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO); + + /** + * 从缓存中,校验客户端是否合法 + * + * @return 客户端 + */ + default OAuth2ClientDO validOAuthClientFromCache(String clientId) { + return validOAuthClientFromCache(clientId, null, null, null, null); + } + + /** + * 从缓存中,校验客户端是否合法 + * + * 非空时,进行校验 + * + * @param clientId 客户端编号 + * @param clientSecret 客户端密钥 + * @param authorizedGrantType 授权方式 + * @param scopes 授权范围 + * @param redirectUri 重定向地址 + * @return 客户端 + */ + OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, String authorizedGrantType, + Collection scopes, String redirectUri); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ClientServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ClientServiceImpl.java new file mode 100644 index 00000000..d407929e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2ClientServiceImpl.java @@ -0,0 +1,153 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.string.StrUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.client.OAuth2ClientSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2.OAuth2ClientMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * OAuth2.0 Client Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class OAuth2ClientServiceImpl implements OAuth2ClientService { + + @Resource + private OAuth2ClientMapper oauth2ClientMapper; + + @Override + public Long createOAuth2Client(OAuth2ClientSaveReqVO createReqVO) { + validateClientIdExists(null, createReqVO.getClientId()); + // 插入 + OAuth2ClientDO client = BeanUtils.toBean(createReqVO, OAuth2ClientDO.class); + oauth2ClientMapper.insert(client); + return client.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT, + allEntries = true) // allEntries 清空所有缓存,因为可能修改到 clientId 字段,不好清理 + public void updateOAuth2Client(OAuth2ClientSaveReqVO updateReqVO) { + // 校验存在 + validateOAuth2ClientExists(updateReqVO.getId()); + // 校验 Client 未被占用 + validateClientIdExists(updateReqVO.getId(), updateReqVO.getClientId()); + + // 更新 + OAuth2ClientDO updateObj = BeanUtils.toBean(updateReqVO, OAuth2ClientDO.class); + oauth2ClientMapper.updateById(updateObj); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT, + allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 key,不好清理 + public void deleteOAuth2Client(Long id) { + // 校验存在 + validateOAuth2ClientExists(id); + // 删除 + oauth2ClientMapper.deleteById(id); + } + + private void validateOAuth2ClientExists(Long id) { + if (oauth2ClientMapper.selectById(id) == null) { + throw exception(OAUTH2_CLIENT_NOT_EXISTS); + } + } + + @VisibleForTesting + void validateClientIdExists(Long id, String clientId) { + OAuth2ClientDO client = oauth2ClientMapper.selectByClientId(clientId); + if (client == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的客户端 + if (id == null) { + throw exception(OAUTH2_CLIENT_EXISTS); + } + if (!client.getId().equals(id)) { + throw exception(OAUTH2_CLIENT_EXISTS); + } + } + + @Override + public OAuth2ClientDO getOAuth2Client(Long id) { + return oauth2ClientMapper.selectById(id); + } + + @Override + @Cacheable(cacheNames = RedisKeyConstants.OAUTH_CLIENT, key = "#clientId", + unless = "#result == null") + public OAuth2ClientDO getOAuth2ClientFromCache(String clientId) { + return oauth2ClientMapper.selectByClientId(clientId); + } + + @Override + public PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO) { + return oauth2ClientMapper.selectPage(pageReqVO); + } + + @Override + public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, String authorizedGrantType, + Collection scopes, String redirectUri) { + // 校验客户端存在、且开启 + OAuth2ClientDO client = getSelf().getOAuth2ClientFromCache(clientId); + if (client == null) { + throw exception(OAUTH2_CLIENT_NOT_EXISTS); + } + if (CommonStatusEnum.isDisable(client.getStatus())) { + throw exception(OAUTH2_CLIENT_DISABLE); + } + + // 校验客户端密钥 + if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getSecret(), clientSecret)) { + throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR); + } + // 校验授权方式 + if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) { + throw exception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); + } + // 校验授权范围 + if (CollUtil.isNotEmpty(scopes) && !CollUtil.containsAll(client.getScopes(), scopes)) { + throw exception(OAUTH2_CLIENT_SCOPE_OVER); + } + // 校验回调地址 + if (StrUtil.isNotEmpty(redirectUri) && !StrUtils.startWithAny(redirectUri, client.getRedirectUris())) { + throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, redirectUri); + } + return client; + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private OAuth2ClientServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2CodeService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2CodeService.java new file mode 100644 index 00000000..2ad00462 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2CodeService.java @@ -0,0 +1,39 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2CodeDO; + +import java.util.List; + +/** + * OAuth2.0 授权码 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 JdbcAuthorizationCodeServices 的功能,提供授权码的操作 + * + * @author 芋道源码 + */ +public interface OAuth2CodeService { + + /** + * 创建授权码 + * + * 参考 JdbcAuthorizationCodeServices 的 createAuthorizationCode 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 授权码的信息 + */ + OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, + List scopes, String redirectUri, String state); + + /** + * 使用授权码 + * + * @param code 授权码 + */ + OAuth2CodeDO consumeAuthorizationCode(String code); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2CodeServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2CodeServiceImpl.java new file mode 100644 index 00000000..22e01bbf --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2CodeServiceImpl.java @@ -0,0 +1,64 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import cn.hutool.core.util.IdUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2.OAuth2CodeMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_EXPIRE; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_NOT_EXISTS; + +/** + * OAuth2.0 授权码 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class OAuth2CodeServiceImpl implements OAuth2CodeService { + + /** + * 授权码的过期时间,默认 5 分钟 + */ + private static final Integer TIMEOUT = 5 * 60; + + @Resource + private OAuth2CodeMapper oauth2CodeMapper; + + @Override + public OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, + List scopes, String redirectUri, String state) { + OAuth2CodeDO codeDO = new OAuth2CodeDO().setCode(generateCode()) + .setUserId(userId).setUserType(userType) + .setClientId(clientId).setScopes(scopes) + .setExpiresTime(LocalDateTime.now().plusSeconds(TIMEOUT)) + .setRedirectUri(redirectUri).setState(state); + oauth2CodeMapper.insert(codeDO); + return codeDO; + } + + @Override + public OAuth2CodeDO consumeAuthorizationCode(String code) { + OAuth2CodeDO codeDO = oauth2CodeMapper.selectByCode(code); + if (codeDO == null) { + throw exception(OAUTH2_CODE_NOT_EXISTS); + } + if (DateUtils.isExpired(codeDO.getExpiresTime())) { + throw exception(OAUTH2_CODE_EXPIRE); + } + oauth2CodeMapper.deleteById(codeDO.getId()); + return codeDO; + } + + private static String generateCode() { + return IdUtil.fastSimpleUUID(); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2GrantService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2GrantService.java new file mode 100644 index 00000000..85fe6b02 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2GrantService.java @@ -0,0 +1,113 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; + +import java.util.List; + +/** + * OAuth2 授予 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 TokenGranter 的功能,提供访问令牌、刷新令牌的操作 + * + * 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。 + * + * 问题:为什么自身也作为一个第三方应用,也走这套流程呢? + * 回复:当然可以这么做,采用 password 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。 + * + * @author 芋道源码 + */ +public interface OAuth2GrantService { + + /** + * 简化模式 + * + * 对应 Spring Security OAuth2 的 ImplicitTokenGranter 功能 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, + String clientId, List scopes); + + /** + * 授权码模式,第一阶段,获得 code 授权码 + * + * 对应 Spring Security OAuth2 的 AuthorizationEndpoint 的 generateCode 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 授权码 + */ + String grantAuthorizationCodeForCode(Long userId, Integer userType, + String clientId, List scopes, + String redirectUri, String state); + + /** + * 授权码模式,第二阶段,获得 accessToken 访问令牌 + * + * 对应 Spring Security OAuth2 的 AuthorizationCodeTokenGranter 功能 + * + * @param clientId 客户端编号 + * @param code 授权码 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, + String redirectUri, String state); + + /** + * 密码模式 + * + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 + * + * @param username 账号 + * @param password 密码 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantPassword(String username, String password, + String clientId, List scopes); + + /** + * 刷新模式 + * + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId); + + /** + * 客户端模式 + * + * 对应 Spring Security OAuth2 的 ClientCredentialsTokenGranter 功能 + * + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantClientCredentials(String clientId, List scopes); + + /** + * 移除访问令牌 + * + * 对应 Spring Security OAuth2 的 ConsumerTokenServices 的 revokeToken 方法 + * + * @param accessToken 访问令牌 + * @param clientId 客户端编号 + * @return 是否移除到 + */ + boolean revokeToken(String clientId, String accessToken); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2GrantServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2GrantServiceImpl.java new file mode 100644 index 00000000..f66bc70b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2GrantServiceImpl.java @@ -0,0 +1,104 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants; +import com.chanko.yunxi.mes.heli.module.system.service.auth.AdminAuthService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * OAuth2 授予 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2GrantServiceImpl implements OAuth2GrantService { + + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private OAuth2CodeService oauth2CodeService; + @Resource + private AdminAuthService adminAuthService; + + @Override + public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, + String clientId, List scopes) { + return oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); + } + + @Override + public String grantAuthorizationCodeForCode(Long userId, Integer userType, + String clientId, List scopes, + String redirectUri, String state) { + return oauth2CodeService.createAuthorizationCode(userId, userType, clientId, scopes, + redirectUri, state).getCode(); + } + + @Override + public OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, + String redirectUri, String state) { + OAuth2CodeDO codeDO = oauth2CodeService.consumeAuthorizationCode(code); + Assert.notNull(codeDO, "授权码不能为空"); // 防御性编程 + // 校验 clientId 是否匹配 + if (!StrUtil.equals(clientId, codeDO.getClientId())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH); + } + // 校验 redirectUri 是否匹配 + if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH); + } + // 校验 state 是否匹配 + state = StrUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串 + if (!StrUtil.equals(state, codeDO.getState())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH); + } + + // 创建访问令牌 + return oauth2TokenService.createAccessToken(codeDO.getUserId(), codeDO.getUserType(), + codeDO.getClientId(), codeDO.getScopes()); + } + + @Override + public OAuth2AccessTokenDO grantPassword(String username, String password, String clientId, List scopes) { + // 使用账号 + 密码进行登录 + AdminUserDO user = adminAuthService.authenticate(username, password); + Assert.notNull(user, "用户不能为空!"); // 防御性编程 + + // 创建访问令牌 + return oauth2TokenService.createAccessToken(user.getId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes); + } + + @Override + public OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId) { + return oauth2TokenService.refreshAccessToken(refreshToken, clientId); + } + + @Override + public OAuth2AccessTokenDO grantClientCredentials(String clientId, List scopes) { + // TODO 芋艿:项目中使用 OAuth2 解决的是三方应用的授权,内部的 SSO 等问题,所以暂时不考虑 client_credentials 这个场景 + throw new UnsupportedOperationException("暂时不支持 client_credentials 授权模式"); + } + + @Override + public boolean revokeToken(String clientId, String accessToken) { + // 先查询,保证 clientId 时匹配的 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.getAccessToken(accessToken); + if (accessTokenDO == null || ObjectUtil.notEqual(clientId, accessTokenDO.getClientId())) { + return false; + } + // 再删除 + return oauth2TokenService.removeAccessToken(accessToken) != null; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2TokenService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2TokenService.java new file mode 100644 index 00000000..2eff1094 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2TokenService.java @@ -0,0 +1,80 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; + +import java.util.List; + +/** + * OAuth2.0 Token Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 DefaultTokenServices + JdbcTokenStore 的功能,提供访问令牌、刷新令牌的操作 + * + * @author 芋道源码 + */ +public interface OAuth2TokenService { + + /** + * 创建访问令牌 + * 注意:该流程中,会包含创建刷新令牌的创建 + * + * 参考 DefaultTokenServices 的 createAccessToken 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes); + + /** + * 刷新访问令牌 + * + * 参考 DefaultTokenServices 的 refreshAccessToken 方法 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId); + + /** + * 获得访问令牌 + * + * 参考 DefaultTokenServices 的 getAccessToken 方法 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO getAccessToken(String accessToken); + + /** + * 校验访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO checkAccessToken(String accessToken); + + /** + * 移除访问令牌 + * 注意:该流程中,会移除相关的刷新令牌 + * + * 参考 DefaultTokenServices 的 revokeToken 方法 + * + * @param accessToken 刷新令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO removeAccessToken(String accessToken); + + /** + * 获得访问令牌分页 + * + * @param reqVO 请求 + * @return 访问令牌分页 + */ + PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2TokenServiceImpl.java new file mode 100644 index 00000000..258cd229 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/oauth2/OAuth2TokenServiceImpl.java @@ -0,0 +1,166 @@ +package com.chanko.yunxi.mes.heli.module.system.service.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils; +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Calendar; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * OAuth2.0 Token Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2TokenServiceImpl implements OAuth2TokenService { + + @Resource + private OAuth2AccessTokenMapper oauth2AccessTokenMapper; + @Resource + private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper; + + @Resource + private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO; + + @Resource + private OAuth2ClientService oauth2ClientService; + + @Override + @Transactional + public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes) { + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + // 创建刷新令牌 + OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes); + // 创建访问令牌 + return createOAuth2AccessToken(refreshTokenDO, clientDO); + } + + @Override + public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { + // 查询访问令牌 + OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); + if (refreshTokenDO == null) { + throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌"); + } + + // 校验 Client 匹配 + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) { + throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确"); + } + + // 移除相关的访问令牌 + List accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); + if (CollUtil.isNotEmpty(accessTokenDOs)) { + oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); + oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); + } + + // 已过期的情况下,删除刷新令牌 + if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) { + oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId()); + throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期"); + } + + // 创建访问令牌 + return createOAuth2AccessToken(refreshTokenDO, clientDO); + } + + @Override + public OAuth2AccessTokenDO getAccessToken(String accessToken) { + // 优先从 Redis 中获取 + OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken); + if (accessTokenDO != null) { + return accessTokenDO; + } + + // 获取不到,从 MySQL 中获取 + accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); + // 如果在 MySQL 存在,则往 Redis 中写入 + if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) { + oauth2AccessTokenRedisDAO.set(accessTokenDO); + } + return accessTokenDO; + } + + @Override + public OAuth2AccessTokenDO checkAccessToken(String accessToken) { + OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken); + if (accessTokenDO == null) { + throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在"); + } + if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) { + throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期"); + } + return accessTokenDO; + } + + @Override + public OAuth2AccessTokenDO removeAccessToken(String accessToken) { + // 删除访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); + if (accessTokenDO == null) { + return null; + } + oauth2AccessTokenMapper.deleteById(accessTokenDO.getId()); + oauth2AccessTokenRedisDAO.delete(accessToken); + // 删除刷新令牌 + oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken()); + return accessTokenDO; + } + + @Override + public PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO) { + return oauth2AccessTokenMapper.selectPage(reqVO); + } + + private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { + OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) + .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()) + .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes()) + .setRefreshToken(refreshTokenDO.getRefreshToken()) + .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds())); + accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号 + oauth2AccessTokenMapper.insert(accessTokenDO); + // 记录到 Redis 中 + oauth2AccessTokenRedisDAO.set(accessTokenDO); + return accessTokenDO; + } + + private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List scopes) { + OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) + .setUserId(userId).setUserType(userType) + .setClientId(clientDO.getClientId()).setScopes(scopes) + .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds())); + oauth2RefreshTokenMapper.insert(refreshToken); + return refreshToken; + } + + private static String generateAccessToken() { + return IdUtil.fastSimpleUUID(); + } + + private static String generateRefreshToken() { + return IdUtil.fastSimpleUUID(); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/MenuService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/MenuService.java new file mode 100644 index 00000000..d3d09abc --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/MenuService.java @@ -0,0 +1,87 @@ +package com.chanko.yunxi.mes.heli.module.system.service.permission; + +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu.MenuSaveVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO; + +import java.util.Collection; +import java.util.List; + +/** + * 菜单 Service 接口 + * + * @author 芋道源码 + */ +public interface MenuService { + + /** + * 创建菜单 + * + * @param createReqVO 菜单信息 + * @return 创建出来的菜单编号 + */ + Long createMenu(MenuSaveVO createReqVO); + + /** + * 更新菜单 + * + * @param updateReqVO 菜单信息 + */ + void updateMenu(MenuSaveVO updateReqVO); + + /** + * 删除菜单 + * + * @param id 菜单编号 + */ + void deleteMenu(Long id); + + /** + * 获得所有菜单列表 + * + * @return 菜单列表 + */ + List getMenuList(); + + /** + * 基于租户,筛选菜单列表 + * 注意,如果是系统租户,返回的还是全菜单 + * + * @param reqVO 筛选条件请求 VO + * @return 菜单列表 + */ + List getMenuListByTenant(MenuListReqVO reqVO); + + /** + * 筛选菜单列表 + * + * @param reqVO 筛选条件请求 VO + * @return 菜单列表 + */ + List getMenuList(MenuListReqVO reqVO); + + /** + * 获得权限对应的菜单编号数组 + * + * @param permission 权限标识 + * @return 数组 + */ + List getMenuIdListByPermissionFromCache(String permission); + + /** + * 获得菜单 + * + * @param id 菜单编号 + * @return 菜单 + */ + MenuDO getMenu(Long id); + + /** + * 获得菜单数组 + * + * @param ids 菜单编号数组 + * @return 菜单数组 + */ + List getMenuList(Collection ids); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/MenuServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/MenuServiceImpl.java new file mode 100644 index 00000000..13de7db8 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/MenuServiceImpl.java @@ -0,0 +1,208 @@ +package com.chanko.yunxi.mes.heli.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu.MenuSaveVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.permission.MenuMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.MenuTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.service.tenant.TenantService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertList; +import static com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO.ID_ROOT; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 菜单 Service 实现 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class MenuServiceImpl implements MenuService { + + @Resource + private MenuMapper menuMapper; + @Resource + private PermissionService permissionService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private TenantService tenantService; + + @Override + @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#createReqVO.permission", + condition = "#createReqVO.permission != null") + public Long createMenu(MenuSaveVO createReqVO) { + // 校验父菜单存在 + validateParentMenu(createReqVO.getParentId(), null); + // 校验菜单(自己) + validateMenu(createReqVO.getParentId(), createReqVO.getName(), null); + + // 插入数据库 + MenuDO menu = BeanUtils.toBean(createReqVO, MenuDO.class); + initMenuProperty(menu); + menuMapper.insert(menu); + // 返回 + return menu.getId(); + } + + @Override + @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为 permission 如果变更,涉及到新老两个 permission。直接清理,简单有效 + public void updateMenu(MenuSaveVO updateReqVO) { + // 校验更新的菜单是否存在 + if (menuMapper.selectById(updateReqVO.getId()) == null) { + throw exception(MENU_NOT_EXISTS); + } + // 校验父菜单存在 + validateParentMenu(updateReqVO.getParentId(), updateReqVO.getId()); + // 校验菜单(自己) + validateMenu(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId()); + + // 更新到数据库 + MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class); + initMenuProperty(updateObj); + menuMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为此时不知道 id 对应的 permission 是多少。直接清理,简单有效 + public void deleteMenu(Long id) { + // 校验是否还有子菜单 + if (menuMapper.selectCountByParentId(id) > 0) { + throw exception(MENU_EXISTS_CHILDREN); + } + // 校验删除的菜单是否存在 + if (menuMapper.selectById(id) == null) { + throw exception(MENU_NOT_EXISTS); + } + // 标记删除 + menuMapper.deleteById(id); + // 删除授予给角色的权限 + permissionService.processMenuDeleted(id); + } + + @Override + public List getMenuList() { + return menuMapper.selectList(); + } + + @Override + public List getMenuListByTenant(MenuListReqVO reqVO) { + List menus = getMenuList(reqVO); + // 开启多租户的情况下,需要过滤掉未开通的菜单 + tenantService.handleTenantMenu(menuIds -> menus.removeIf(menu -> !CollUtil.contains(menuIds, menu.getId()))); + return menus; + } + + @Override + public List getMenuList(MenuListReqVO reqVO) { + return menuMapper.selectList(reqVO); + } + + @Override + @Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#permission") + public List getMenuIdListByPermissionFromCache(String permission) { + List menus = menuMapper.selectListByPermission(permission); + return convertList(menus, MenuDO::getId); + } + + @Override + public MenuDO getMenu(Long id) { + return menuMapper.selectById(id); + } + + @Override + public List getMenuList(Collection ids) { + return menuMapper.selectBatchIds(ids); + } + + /** + * 校验父菜单是否合法 + *

+ * 1. 不能设置自己为父菜单 + * 2. 父菜单不存在 + * 3. 父菜单必须是 {@link MenuTypeEnum#MENU} 菜单类型 + * + * @param parentId 父菜单编号 + * @param childId 当前菜单编号 + */ + @VisibleForTesting + void validateParentMenu(Long parentId, Long childId) { + if (parentId == null || ID_ROOT.equals(parentId)) { + return; + } + // 不能设置自己为父菜单 + if (parentId.equals(childId)) { + throw exception(MENU_PARENT_ERROR); + } + MenuDO menu = menuMapper.selectById(parentId); + // 父菜单不存在 + if (menu == null) { + throw exception(MENU_PARENT_NOT_EXISTS); + } + // 父菜单必须是目录或者菜单类型 + if (!MenuTypeEnum.DIR.getType().equals(menu.getType()) + && !MenuTypeEnum.MENU.getType().equals(menu.getType())) { + throw exception(MENU_PARENT_NOT_DIR_OR_MENU); + } + } + + /** + * 校验菜单是否合法 + *

+ * 1. 校验相同父菜单编号下,是否存在相同的菜单名 + * + * @param name 菜单名字 + * @param parentId 父菜单编号 + * @param id 菜单编号 + */ + @VisibleForTesting + void validateMenu(Long parentId, String name, Long id) { + MenuDO menu = menuMapper.selectByParentIdAndName(parentId, name); + if (menu == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的菜单 + if (id == null) { + throw exception(MENU_NAME_DUPLICATE); + } + if (!menu.getId().equals(id)) { + throw exception(MENU_NAME_DUPLICATE); + } + } + + /** + * 初始化菜单的通用属性。 + *

+ * 例如说,只有目录或者菜单类型的菜单,才设置 icon + * + * @param menu 菜单 + */ + private void initMenuProperty(MenuDO menu) { + // 菜单为按钮类型时,无需 component、icon、path 属性,进行置空 + if (MenuTypeEnum.BUTTON.getType().equals(menu.getType())) { + menu.setComponent(""); + menu.setComponentName(""); + menu.setIcon(""); + menu.setPath(""); + } + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/PermissionService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/PermissionService.java new file mode 100644 index 00000000..10ebb9f7 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/PermissionService.java @@ -0,0 +1,146 @@ +package com.chanko.yunxi.mes.heli.module.system.service.permission; + +import com.chanko.yunxi.mes.heli.module.system.api.permission.dto.DeptDataPermissionRespDTO; + +import java.util.Collection; +import java.util.Set; + +import static java.util.Collections.singleton; + +/** + * 权限 Service 接口 + *

+ * 提供用户-角色、角色-菜单、角色-部门的关联权限处理 + * + * @author 芋道源码 + */ +public interface PermissionService { + + /** + * 判断是否有权限,任一一个即可 + * + * @param userId 用户编号 + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(Long userId, String... permissions); + + /** + * 判断是否有角色,任一一个即可 + * + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(Long userId, String... roles); + + // ========== 角色-菜单的相关方法 ========== + + /** + * 设置角色菜单 + * + * @param roleId 角色编号 + * @param menuIds 菜单编号集合 + */ + void assignRoleMenu(Long roleId, Set menuIds); + + /** + * 处理角色删除时,删除关联授权数据 + * + * @param roleId 角色编号 + */ + void processRoleDeleted(Long roleId); + + /** + * 处理菜单删除时,删除关联授权数据 + * + * @param menuId 菜单编号 + */ + void processMenuDeleted(Long menuId); + + /** + * 获得角色拥有的菜单编号集合 + * + * @param roleId 角色编号 + * @return 菜单编号集合 + */ + default Set getRoleMenuListByRoleId(Long roleId) { + return getRoleMenuListByRoleId(singleton(roleId)); + } + + /** + * 获得角色们拥有的菜单编号集合 + * + * @param roleIds 角色编号数组 + * @return 菜单编号集合 + */ + Set getRoleMenuListByRoleId(Collection roleIds); + + /** + * 获得拥有指定菜单的角色编号数组,从缓存中获取 + * + * @param menuId 菜单编号 + * @return 角色编号数组 + */ + Set getMenuRoleIdListByMenuIdFromCache(Long menuId); + + // ========== 用户-角色的相关方法 ========== + + /** + * 设置用户角色 + * + * @param userId 角色编号 + * @param roleIds 角色编号集合 + */ + void assignUserRole(Long userId, Set roleIds); + + /** + * 处理用户删除时,删除关联授权数据 + * + * @param userId 用户编号 + */ + void processUserDeleted(Long userId); + + /** + * 获得拥有多个角色的用户编号集合 + * + * @param roleIds 角色编号集合 + * @return 用户编号集合 + */ + Set getUserRoleIdListByRoleId(Collection roleIds); + + /** + * 获得用户拥有的角色编号集合 + * + * @param userId 用户编号 + * @return 角色编号集合 + */ + Set getUserRoleIdListByUserId(Long userId); + + /** + * 获得用户拥有的角色编号集合,从缓存中获取 + * + * @param userId 用户编号 + * @return 角色编号集合 + */ + Set getUserRoleIdListByUserIdFromCache(Long userId); + + // ========== 用户-部门的相关方法 ========== + + /** + * 设置角色的数据权限 + * + * @param roleId 角色编号 + * @param dataScope 数据范围 + * @param dataScopeDeptIds 部门编号数组 + */ + void assignRoleDataScope(Long roleId, Integer dataScope, Set dataScopeDeptIds); + + /** + * 获得登陆用户的部门数据权限 + * + * @param userId 用户编号 + * @return 部门数据权限 + */ + DeptDataPermissionRespDTO getDeptDataPermission(Long userId); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/PermissionServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/PermissionServiceImpl.java new file mode 100644 index 00000000..86060ca1 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/PermissionServiceImpl.java @@ -0,0 +1,337 @@ +package com.chanko.yunxi.mes.heli.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.annotation.DataPermission; +import com.chanko.yunxi.mes.heli.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleMenuDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.UserRoleDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.permission.RoleMenuMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.permission.UserRoleMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.DataScopeEnum; +import com.chanko.yunxi.mes.heli.module.system.service.dept.DeptService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Suppliers; +import com.google.common.collect.Sets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Supplier; + +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; +import static com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 权限 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class PermissionServiceImpl implements PermissionService { + + @Resource + private RoleMenuMapper roleMenuMapper; + @Resource + private UserRoleMapper userRoleMapper; + + @Resource + private RoleService roleService; + @Resource + private MenuService menuService; + @Resource + private DeptService deptService; + @Resource + private AdminUserService userService; + + @Override + public boolean hasAnyPermissions(Long userId, String... permissions) { + // 如果为空,说明已经有权限 + if (ArrayUtil.isEmpty(permissions)) { + return true; + } + + // 获得当前登录的角色。如果为空,说明没有权限 + List roles = getEnableUserRoleListByUserIdFromCache(userId); + if (CollUtil.isEmpty(roles)) { + return false; + } + + // 情况一:遍历判断每个权限,如果有一满足,说明有权限 + for (String permission : permissions) { + if (hasAnyPermission(roles, permission)) { + return true; + } + } + + // 情况二:如果是超管,也说明有权限 + return roleService.hasAnySuperAdmin(convertSet(roles, RoleDO::getId)); + } + + /** + * 判断指定角色,是否拥有该 permission 权限 + * + * @param roles 指定角色数组 + * @param permission 权限标识 + * @return 是否拥有 + */ + private boolean hasAnyPermission(List roles, String permission) { + List menuIds = menuService.getMenuIdListByPermissionFromCache(permission); + // 采用严格模式,如果权限找不到对应的 Menu 的话,也认为没有权限 + if (CollUtil.isEmpty(menuIds)) { + return false; + } + + // 判断是否有权限 + Set roleIds = convertSet(roles, RoleDO::getId); + for (Long menuId : menuIds) { + // 获得拥有该菜单的角色编号集合 + Set menuRoleIds = getSelf().getMenuRoleIdListByMenuIdFromCache(menuId); + // 如果有交集,说明有权限 + if (CollUtil.containsAny(menuRoleIds, roleIds)) { + return true; + } + } + return false; + } + + @Override + public boolean hasAnyRoles(Long userId, String... roles) { + // 如果为空,说明已经有权限 + if (ArrayUtil.isEmpty(roles)) { + return true; + } + + // 获得当前登录的角色。如果为空,说明没有权限 + List roleList = getEnableUserRoleListByUserIdFromCache(userId); + if (CollUtil.isEmpty(roleList)) { + return false; + } + + // 判断是否有角色 + Set userRoles = convertSet(roleList, RoleDO::getCode); + return CollUtil.containsAny(userRoles, Sets.newHashSet(roles)); + } + + // ========== 角色-菜单的相关方法 ========== + + @Override + @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 + @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快 + public void assignRoleMenu(Long roleId, Set menuIds) { + // 获得角色拥有菜单编号 + Set dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId); + // 计算新增和删除的菜单编号 + Set menuIdList = CollUtil.emptyIfNull(menuIds); + Collection createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds); + Collection deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList); + // 执行新增和删除。对于已经授权的菜单,不用做任何处理 + if (CollUtil.isNotEmpty(createMenuIds)) { + roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> { + RoleMenuDO entity = new RoleMenuDO(); + entity.setRoleId(roleId); + entity.setMenuId(menuId); + return entity; + })); + } + if (CollUtil.isNotEmpty(deleteMenuIds)) { + roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + @Caching(evict = { + @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, + allEntries = true), // allEntries 清空所有缓存,此处无法方便获得 roleId 对应的 menu 缓存们 + @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,此处无法方便获得 roleId 对应的 user 缓存们 + }) + public void processRoleDeleted(Long roleId) { + // 标记删除 UserRole + userRoleMapper.deleteListByRoleId(roleId); + // 标记删除 RoleMenu + roleMenuMapper.deleteListByRoleId(roleId); + } + + @Override + @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId") + public void processMenuDeleted(Long menuId) { + roleMenuMapper.deleteListByMenuId(menuId); + } + + @Override + public Set getRoleMenuListByRoleId(Collection roleIds) { + if (CollUtil.isEmpty(roleIds)) { + return Collections.emptySet(); + } + + // 如果是管理员的情况下,获取全部菜单编号 + if (roleService.hasAnySuperAdmin(roleIds)) { + return convertSet(menuService.getMenuList(), MenuDO::getId); + } + // 如果是非管理员的情况下,获得拥有的菜单编号 + return convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId); + } + + @Override + @Cacheable(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId") + public Set getMenuRoleIdListByMenuIdFromCache(Long menuId) { + return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId); + } + + // ========== 用户-角色的相关方法 ========== + + @Override + @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 + @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") + public void assignUserRole(Long userId, Set roleIds) { + // 获得角色拥有角色编号 + Set dbRoleIds = convertSet(userRoleMapper.selectListByUserId(userId), + UserRoleDO::getRoleId); + // 计算新增和删除的角色编号 + Set roleIdList = CollUtil.emptyIfNull(roleIds); + Collection createRoleIds = CollUtil.subtract(roleIdList, dbRoleIds); + Collection deleteMenuIds = CollUtil.subtract(dbRoleIds, roleIdList); + // 执行新增和删除。对于已经授权的角色,不用做任何处理 + if (!CollectionUtil.isEmpty(createRoleIds)) { + userRoleMapper.insertBatch(CollectionUtils.convertList(createRoleIds, roleId -> { + UserRoleDO entity = new UserRoleDO(); + entity.setUserId(userId); + entity.setRoleId(roleId); + return entity; + })); + } + if (!CollectionUtil.isEmpty(deleteMenuIds)) { + userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteMenuIds); + } + } + + @Override + @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") + public void processUserDeleted(Long userId) { + userRoleMapper.deleteListByUserId(userId); + } + + @Override + public Set getUserRoleIdListByUserId(Long userId) { + return convertSet(userRoleMapper.selectListByUserId(userId), UserRoleDO::getRoleId); + } + + @Override + @Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") + public Set getUserRoleIdListByUserIdFromCache(Long userId) { + return getUserRoleIdListByUserId(userId); + } + + @Override + public Set getUserRoleIdListByRoleId(Collection roleIds) { + return convertSet(userRoleMapper.selectListByRoleIds(roleIds), UserRoleDO::getUserId); + } + + /** + * 获得用户拥有的角色,并且这些角色是开启状态的 + * + * @param userId 用户编号 + * @return 用户拥有的角色 + */ + @VisibleForTesting + List getEnableUserRoleListByUserIdFromCache(Long userId) { + // 获得用户拥有的角色编号 + Set roleIds = getSelf().getUserRoleIdListByUserIdFromCache(userId); + // 获得角色数组,并移除被禁用的 + List roles = roleService.getRoleListFromCache(roleIds); + roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); + return roles; + } + + // ========== 用户-部门的相关方法 ========== + + @Override + public void assignRoleDataScope(Long roleId, Integer dataScope, Set dataScopeDeptIds) { + roleService.updateRoleDataScope(roleId, dataScope, dataScopeDeptIds); + } + + @Override + @DataPermission(enable = false) // 关闭数据权限,不然就会出现递归获取数据权限的问题 + public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { + // 获得用户的角色 + List roles = getEnableUserRoleListByUserIdFromCache(userId); + + // 如果角色为空,则只能查看自己 + DeptDataPermissionRespDTO result = new DeptDataPermissionRespDTO(); + if (CollUtil.isEmpty(roles)) { + result.setSelf(true); + return result; + } + + // 获得用户的部门编号的缓存,通过 Guava 的 Suppliers 惰性求值,即有且仅有第一次发起 DB 的查询 + Supplier userDeptId = Suppliers.memoize(() -> userService.getUser(userId).getDeptId()); + // 遍历每个角色,计算 + for (RoleDO role : roles) { + // 为空时,跳过 + if (role.getDataScope() == null) { + continue; + } + // 情况一,ALL + if (Objects.equals(role.getDataScope(), DataScopeEnum.ALL.getScope())) { + result.setAll(true); + continue; + } + // 情况二,DEPT_CUSTOM + if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_CUSTOM.getScope())) { + CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); + // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。 + // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉 + CollUtil.addAll(result.getDeptIds(), userDeptId.get()); + continue; + } + // 情况三,DEPT_ONLY + if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) { + CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get()); + continue; + } + // 情况四,DEPT_DEPT_AND_CHILD + if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { + CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(userDeptId.get())); + // 添加本身部门编号 + CollUtil.addAll(result.getDeptIds(), userDeptId.get()); + continue; + } + // 情况五,SELF + if (Objects.equals(role.getDataScope(), DataScopeEnum.SELF.getScope())) { + result.setSelf(true); + continue; + } + // 未知情况,error log 即可 + log.error("[getDeptDataPermission][LoginUser({}) role({}) 无法处理]", userId, toJsonString(result)); + } + return result; + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PermissionServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/RoleService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/RoleService.java new file mode 100644 index 00000000..75d17a7f --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/RoleService.java @@ -0,0 +1,132 @@ +package com.chanko.yunxi.mes.heli.module.system.service.permission; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * 角色 Service 接口 + * + * @author 芋道源码 + */ +public interface RoleService { + + /** + * 创建角色 + * + * @param createReqVO 创建角色信息 + * @param type 角色类型 + * @return 角色编号 + */ + Long createRole(@Valid RoleSaveReqVO createReqVO, Integer type); + + /** + * 更新角色 + * + * @param updateReqVO 更新角色信息 + */ + void updateRole(@Valid RoleSaveReqVO updateReqVO); + + /** + * 删除角色 + * + * @param id 角色编号 + */ + void deleteRole(Long id); + + /** + * 更新角色状态 + * + * @param id 角色编号 + * @param status 状态 + */ + void updateRoleStatus(Long id, Integer status); + + /** + * 设置角色的数据权限 + * + * @param id 角色编号 + * @param dataScope 数据范围 + * @param dataScopeDeptIds 部门编号数组 + */ + void updateRoleDataScope(Long id, Integer dataScope, Set dataScopeDeptIds); + + /** + * 获得角色 + * + * @param id 角色编号 + * @return 角色 + */ + RoleDO getRole(Long id); + + /** + * 获得角色,从缓存中 + * + * @param id 角色编号 + * @return 角色 + */ + RoleDO getRoleFromCache(Long id); + + /** + * 获得角色列表 + * + * @param ids 角色编号数组 + * @return 角色列表 + */ + List getRoleList(Collection ids); + + /** + * 获得角色数组,从缓存中 + * + * @param ids 角色编号数组 + * @return 角色数组 + */ + List getRoleListFromCache(Collection ids); + + /** + * 获得角色列表 + * + * @param statuses 筛选的状态 + * @return 角色列表 + */ + List getRoleListByStatus(Collection statuses); + + /** + * 获得所有角色列表 + * + * @return 角色列表 + */ + List getRoleList(); + + /** + * 获得角色分页 + * + * @param reqVO 角色分页查询 + * @return 角色分页结果 + */ + PageResult getRolePage(RolePageReqVO reqVO); + + /** + * 判断角色编号数组中,是否有管理员 + * + * @param ids 角色编号数组 + * @return 是否有管理员 + */ + boolean hasAnySuperAdmin(Collection ids); + + /** + * 校验角色们是否有效。如下情况,视为无效: + * 1. 角色编号不存在 + * 2. 角色被禁用 + * + * @param ids 角色编号数组 + */ + void validateRoleList(Collection ids); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/RoleServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/RoleServiceImpl.java new file mode 100644 index 00000000..c0e98fce --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/permission/RoleServiceImpl.java @@ -0,0 +1,250 @@ +package com.chanko.yunxi.mes.heli.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.permission.RoleMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.DataScopeEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.RoleCodeEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.RoleTypeEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.util.*; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertMap; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 角色 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class RoleServiceImpl implements RoleService { + + @Resource + private PermissionService permissionService; + + @Resource + private RoleMapper roleMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createRole(RoleSaveReqVO createReqVO, Integer type) { + // 校验角色 + validateRoleDuplicate(createReqVO.getName(), createReqVO.getCode(), null); + // 插入到数据库 + RoleDO role = BeanUtils.toBean(createReqVO, RoleDO.class); + role.setType(ObjectUtil.defaultIfNull(type, RoleTypeEnum.CUSTOM.getType())); + role.setStatus(CommonStatusEnum.ENABLE.getStatus()); + role.setDataScope(DataScopeEnum.ALL.getScope()); // 默认可查看所有数据。原因是,可能一些项目不需要项目权限 + roleMapper.insert(role); + // 返回 + return role.getId(); + } + + @Override + @CacheEvict(value = RedisKeyConstants.ROLE, key = "#updateReqVO.id") + public void updateRole(RoleSaveReqVO updateReqVO) { + // 校验是否可以更新 + validateRoleForUpdate(updateReqVO.getId()); + // 校验角色的唯一字段是否重复 + validateRoleDuplicate(updateReqVO.getName(), updateReqVO.getCode(), updateReqVO.getId()); + + // 更新到数据库 + RoleDO updateObj = BeanUtils.toBean(updateReqVO, RoleDO.class); + roleMapper.updateById(updateObj); + } + + @Override + @CacheEvict(value = RedisKeyConstants.ROLE, key = "#id") + public void updateRoleStatus(Long id, Integer status) { + // 校验是否可以更新 + validateRoleForUpdate(id); + + // 更新状态 + RoleDO updateObj = new RoleDO().setId(id).setStatus(status); + roleMapper.updateById(updateObj); + } + + @Override + @CacheEvict(value = RedisKeyConstants.ROLE, key = "#id") + public void updateRoleDataScope(Long id, Integer dataScope, Set dataScopeDeptIds) { + // 校验是否可以更新 + validateRoleForUpdate(id); + + // 更新数据范围 + RoleDO updateObject = new RoleDO(); + updateObject.setId(id); + updateObject.setDataScope(dataScope); + updateObject.setDataScopeDeptIds(dataScopeDeptIds); + roleMapper.updateById(updateObject); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheEvict(value = RedisKeyConstants.ROLE, key = "#id") + public void deleteRole(Long id) { + // 校验是否可以更新 + validateRoleForUpdate(id); + // 标记删除 + roleMapper.deleteById(id); + // 删除相关数据 + permissionService.processRoleDeleted(id); + } + + /** + * 校验角色的唯一字段是否重复 + * + * 1. 是否存在相同名字的角色 + * 2. 是否存在相同编码的角色 + * + * @param name 角色名字 + * @param code 角色额编码 + * @param id 角色编号 + */ + @VisibleForTesting + void validateRoleDuplicate(String name, String code, Long id) { + // 0. 超级管理员,不允许创建 + if (RoleCodeEnum.isSuperAdmin(code)) { + throw exception(ROLE_ADMIN_CODE_ERROR, code); + } + // 1. 该 name 名字被其它角色所使用 + RoleDO role = roleMapper.selectByName(name); + if (role != null && !role.getId().equals(id)) { + throw exception(ROLE_NAME_DUPLICATE, name); + } + // 2. 是否存在相同编码的角色 + if (!StringUtils.hasText(code)) { + return; + } + // 该 code 编码被其它角色所使用 + role = roleMapper.selectByCode(code); + if (role != null && !role.getId().equals(id)) { + throw exception(ROLE_CODE_DUPLICATE, code); + } + } + + /** + * 校验角色是否可以被更新 + * + * @param id 角色编号 + */ + @VisibleForTesting + void validateRoleForUpdate(Long id) { + RoleDO roleDO = roleMapper.selectById(id); + if (roleDO == null) { + throw exception(ROLE_NOT_EXISTS); + } + // 内置角色,不允许删除 + if (RoleTypeEnum.SYSTEM.getType().equals(roleDO.getType())) { + throw exception(ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE); + } + } + + @Override + public RoleDO getRole(Long id) { + return roleMapper.selectById(id); + } + + @Override + @Cacheable(value = RedisKeyConstants.ROLE, key = "#id", + unless = "#result == null") + public RoleDO getRoleFromCache(Long id) { + return roleMapper.selectById(id); + } + + + @Override + public List getRoleListByStatus(Collection statuses) { + return roleMapper.selectListByStatus(statuses); + } + + @Override + public List getRoleList() { + return roleMapper.selectList(); + } + + @Override + public List getRoleList(Collection ids) { + if (CollectionUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return roleMapper.selectBatchIds(ids); + } + + @Override + public List getRoleListFromCache(Collection ids) { + if (CollectionUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + // 这里采用 for 循环从缓存中获取,主要考虑 Spring CacheManager 无法批量操作的问题 + RoleServiceImpl self = getSelf(); + return CollectionUtils.convertList(ids, self::getRoleFromCache); + } + + @Override + public PageResult getRolePage(RolePageReqVO reqVO) { + return roleMapper.selectPage(reqVO); + } + + @Override + public boolean hasAnySuperAdmin(Collection ids) { + if (CollectionUtil.isEmpty(ids)) { + return false; + } + RoleServiceImpl self = getSelf(); + return ids.stream().anyMatch(id -> { + RoleDO role = self.getRoleFromCache(id); + return role != null && RoleCodeEnum.isSuperAdmin(role.getCode()); + }); + } + + @Override + public void validateRoleList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得角色信息 + List roles = roleMapper.selectBatchIds(ids); + Map roleMap = convertMap(roles, RoleDO::getId); + // 校验 + ids.forEach(id -> { + RoleDO role = roleMap.get(id); + if (role == null) { + throw exception(ROLE_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())) { + throw exception(ROLE_IS_DISABLE, role.getName()); + } + }); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private RoleServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sensitiveword/SensitiveWordService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sensitiveword/SensitiveWordService.java new file mode 100644 index 00000000..c0ea0b2b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sensitiveword/SensitiveWordService.java @@ -0,0 +1,89 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sensitiveword; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo.SensitiveWordSaveVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; + +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +/** + * 敏感词 Service 接口 + * + * @author 永不言败 + */ +public interface SensitiveWordService { + + /** + * 创建敏感词 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSensitiveWord(@Valid SensitiveWordSaveVO createReqVO); + + /** + * 更新敏感词 + * + * @param updateReqVO 更新信息 + */ + void updateSensitiveWord(@Valid SensitiveWordSaveVO updateReqVO); + + /** + * 删除敏感词 + * + * @param id 编号 + */ + void deleteSensitiveWord(Long id); + + /** + * 获得敏感词 + * + * @param id 编号 + * @return 敏感词 + */ + SensitiveWordDO getSensitiveWord(Long id); + + /** + * 获得敏感词列表 + * + * @return 敏感词列表 + */ + List getSensitiveWordList(); + + /** + * 获得敏感词分页 + * + * @param pageReqVO 分页查询 + * @return 敏感词分页 + */ + PageResult getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO); + + /** + * 获得所有敏感词的标签数组 + * + * @return 标签数组 + */ + Set getSensitiveWordTagSet(); + + /** + * 获得文本所包含的不合法的敏感词数组 + * + * @param text 文本 + * @param tags 标签数组 + * @return 不合法的敏感词数组 + */ + List validateText(String text, List tags); + + /** + * 判断文本是否包含敏感词 + * + * @param text 文本 + * @param tags 标签数组 + * @return 是否包含敏感词 + */ + boolean isTextValid(String text, List tags); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sensitiveword/SensitiveWordServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sensitiveword/SensitiveWordServiceImpl.java new file mode 100644 index 00000000..dd8a263b --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sensitiveword/SensitiveWordServiceImpl.java @@ -0,0 +1,262 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sensitiveword; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sensitiveword.vo.SensitiveWordSaveVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.sensitiveword.SensitiveWordMapper; +import com.chanko.yunxi.mes.heli.module.system.util.collection.SimpleTrie; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.filterList; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.getMaxValue; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_EXISTS; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS; + +/** + * 敏感词 Service 实现类 + * + * @author 永不言败 + */ +@Service +@Slf4j +@Validated +public class SensitiveWordServiceImpl implements SensitiveWordService { + + /** + * 是否开启敏感词功能 + */ + public static Boolean ENABLED = false; + + /** + * 敏感词列表缓存 + */ + @Getter + private volatile List sensitiveWordCache = Collections.emptyList(); + /** + * 敏感词标签缓存 + * key:敏感词编号 {@link SensitiveWordDO#getId()} + *

+ * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + private volatile Set sensitiveWordTagsCache = Collections.emptySet(); + + @Resource + private SensitiveWordMapper sensitiveWordMapper; + + /** + * 默认的敏感词的字典树,包含所有敏感词 + */ + @Getter + private volatile SimpleTrie defaultSensitiveWordTrie = new SimpleTrie(Collections.emptySet()); + /** + * 标签与敏感词的字段数的映射 + */ + @Getter + private volatile Map tagSensitiveWordTries = Collections.emptyMap(); + + /** + * 初始化缓存 + */ + @PostConstruct + public void initLocalCache() { + if (!ENABLED) { + return; + } + + // 第一步:查询数据 + List sensitiveWords = sensitiveWordMapper.selectList(); + log.info("[initLocalCache][缓存敏感词,数量为:{}]", sensitiveWords.size()); + + // 第二步:构建缓存 + // 写入 sensitiveWordTagsCache 缓存 + Set tags = new HashSet<>(); + sensitiveWords.forEach(word -> tags.addAll(word.getTags())); + sensitiveWordTagsCache = tags; + sensitiveWordCache = sensitiveWords; + // 写入 defaultSensitiveWordTrie、tagSensitiveWordTries 缓存 + initSensitiveWordTrie(sensitiveWords); + } + + private void initSensitiveWordTrie(List wordDOs) { + // 过滤禁用的敏感词 + wordDOs = filterList(wordDOs, word -> word.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())); + + // 初始化默认的 defaultSensitiveWordTrie + this.defaultSensitiveWordTrie = new SimpleTrie(CollectionUtils.convertList(wordDOs, SensitiveWordDO::getName)); + + // 初始化 tagSensitiveWordTries + Multimap tagWords = HashMultimap.create(); + for (SensitiveWordDO word : wordDOs) { + if (CollUtil.isEmpty(word.getTags())) { + continue; + } + word.getTags().forEach(tag -> tagWords.put(tag, word.getName())); + } + // 添加到 tagSensitiveWordTries 中 + Map tagSensitiveWordTries = new HashMap<>(); + tagWords.asMap().forEach((tag, words) -> tagSensitiveWordTries.put(tag, new SimpleTrie(words))); + this.tagSensitiveWordTries = tagSensitiveWordTries; + } + + /** + * 通过定时任务轮询,刷新缓存 + * + * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新 + */ + @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) + public void refreshLocalCache() { + // 情况一:如果缓存里没有数据,则直接刷新缓存 + if (CollUtil.isEmpty(sensitiveWordCache)) { + initLocalCache(); + return; + } + + // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存 + LocalDateTime maxTime = getMaxValue(sensitiveWordCache, SensitiveWordDO::getUpdateTime); + if (sensitiveWordMapper.selectCountByUpdateTimeGt(maxTime) > 0) { + initLocalCache(); + } + } + + @Override + public Long createSensitiveWord(SensitiveWordSaveVO createReqVO) { + // 校验唯一性 + validateSensitiveWordNameUnique(null, createReqVO.getName()); + + // 插入 + SensitiveWordDO sensitiveWord = BeanUtils.toBean(createReqVO, SensitiveWordDO.class); + sensitiveWordMapper.insert(sensitiveWord); + + // 刷新缓存 + initLocalCache(); + return sensitiveWord.getId(); + } + + @Override + public void updateSensitiveWord(SensitiveWordSaveVO updateReqVO) { + // 校验唯一性 + validateSensitiveWordExists(updateReqVO.getId()); + validateSensitiveWordNameUnique(updateReqVO.getId(), updateReqVO.getName()); + + // 更新 + SensitiveWordDO updateObj = BeanUtils.toBean(updateReqVO, SensitiveWordDO.class); + sensitiveWordMapper.updateById(updateObj); + + // 刷新缓存 + initLocalCache(); + } + + @Override + public void deleteSensitiveWord(Long id) { + // 校验存在 + validateSensitiveWordExists(id); + // 删除 + sensitiveWordMapper.deleteById(id); + + // 刷新缓存 + initLocalCache(); + } + + private void validateSensitiveWordNameUnique(Long id, String name) { + SensitiveWordDO word = sensitiveWordMapper.selectByName(name); + if (word == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的敏感词 + if (id == null) { + throw exception(SENSITIVE_WORD_EXISTS); + } + if (!word.getId().equals(id)) { + throw exception(SENSITIVE_WORD_EXISTS); + } + } + + private void validateSensitiveWordExists(Long id) { + if (sensitiveWordMapper.selectById(id) == null) { + throw exception(SENSITIVE_WORD_NOT_EXISTS); + } + } + + @Override + public SensitiveWordDO getSensitiveWord(Long id) { + return sensitiveWordMapper.selectById(id); + } + + @Override + public List getSensitiveWordList() { + return sensitiveWordMapper.selectList(); + } + + @Override + public PageResult getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO) { + return sensitiveWordMapper.selectPage(pageReqVO); + } + + @Override + public Set getSensitiveWordTagSet() { + return sensitiveWordTagsCache; + } + + @Override + public List validateText(String text, List tags) { + Assert.isTrue(ENABLED, "敏感词功能未开启,请将 ENABLED 设置为 true"); + + // 无标签时,默认所有 + if (CollUtil.isEmpty(tags)) { + return defaultSensitiveWordTrie.validate(text); + } + // 有标签的情况 + Set result = new HashSet<>(); + tags.forEach(tag -> { + SimpleTrie trie = tagSensitiveWordTries.get(tag); + if (trie == null) { + return; + } + result.addAll(trie.validate(text)); + }); + return new ArrayList<>(result); + } + + @Override + public boolean isTextValid(String text, List tags) { + Assert.isTrue(ENABLED, "敏感词功能未开启,请将 ENABLED 设置为 true"); + + // 无标签时,默认所有 + if (CollUtil.isEmpty(tags)) { + return defaultSensitiveWordTrie.isValid(text); + } + // 有标签的情况 + for (String tag : tags) { + SimpleTrie trie = tagSensitiveWordTries.get(tag); + if (trie == null) { + continue; + } + // 如果有一个标签不合法,则返回 false 不合法 + if (!trie.isValid(text)) { + return false; + } + } + return true; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsChannelService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsChannelService.java new file mode 100644 index 00000000..a3e8a285 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsChannelService.java @@ -0,0 +1,81 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.SmsClient; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsChannelDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 短信渠道 Service 接口 + * + * @author zzf + * @since 2021/1/25 9:24 + */ +public interface SmsChannelService { + + /** + * 创建短信渠道 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSmsChannel(@Valid SmsChannelSaveReqVO createReqVO); + + /** + * 更新短信渠道 + * + * @param updateReqVO 更新信息 + */ + void updateSmsChannel(@Valid SmsChannelSaveReqVO updateReqVO); + + /** + * 删除短信渠道 + * + * @param id 编号 + */ + void deleteSmsChannel(Long id); + + /** + * 获得短信渠道 + * + * @param id 编号 + * @return 短信渠道 + */ + SmsChannelDO getSmsChannel(Long id); + + /** + * 获得所有短信渠道列表 + * + * @return 短信渠道列表 + */ + List getSmsChannelList(); + + /** + * 获得短信渠道分页 + * + * @param pageReqVO 分页查询 + * @return 短信渠道分页 + */ + PageResult getSmsChannelPage(SmsChannelPageReqVO pageReqVO); + + /** + * 获得短信客户端 + * + * @param id 编号 + * @return 短信客户端 + */ + SmsClient getSmsClient(Long id); + + /** + * 获得短信客户端 + * + * @param code 编码 + * @return 短信客户端 + */ + SmsClient getSmsClient(String code); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsChannelServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsChannelServiceImpl.java new file mode 100644 index 00000000..35a485c4 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsChannelServiceImpl.java @@ -0,0 +1,166 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.SmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.SmsClientFactory; +import com.chanko.yunxi.mes.heli.framework.sms.core.property.SmsChannelProperties; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsChannelDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.sms.SmsChannelMapper; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; + +/** + * 短信渠道 Service 实现类 + * + * @author zzf + */ +@Service +@Slf4j +public class SmsChannelServiceImpl implements SmsChannelService { + + /** + * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory + */ + @Getter + private final LoadingCache idClientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L), + new CacheLoader() { + + @Override + public SmsClient load(Long id) { + // 查询,然后尝试刷新 + SmsChannelDO channel = smsChannelMapper.selectById(id); + if (channel != null) { + SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); + smsClientFactory.createOrUpdateSmsClient(properties); + } + return smsClientFactory.getSmsClient(id); + } + + }); + + /** + * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory + */ + @Getter + private final LoadingCache codeClientCache = buildAsyncReloadingCache(Duration.ofSeconds(60L), + new CacheLoader() { + + @Override + public SmsClient load(String code) { + // 查询,然后尝试刷新 + SmsChannelDO channel = smsChannelMapper.selectByCode(code); + if (channel != null) { + SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); + smsClientFactory.createOrUpdateSmsClient(properties); + } + return smsClientFactory.getSmsClient(code); + } + + }); + + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SmsChannelMapper smsChannelMapper; + + @Resource + private SmsTemplateService smsTemplateService; + + @Override + public Long createSmsChannel(SmsChannelSaveReqVO createReqVO) { + SmsChannelDO channel = BeanUtils.toBean(createReqVO, SmsChannelDO.class); + smsChannelMapper.insert(channel); + return channel.getId(); + } + + @Override + public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) { + // 校验存在 + SmsChannelDO channel = validateSmsChannelExists(updateReqVO.getId()); + // 更新 + SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class); + smsChannelMapper.updateById(updateObj); + + // 清空缓存 + clearCache(updateReqVO.getId(), channel.getCode()); + } + + @Override + public void deleteSmsChannel(Long id) { + // 校验存在 + SmsChannelDO channel = validateSmsChannelExists(id); + // 校验是否有在使用该账号的模版 + if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) { + throw exception(SMS_CHANNEL_HAS_CHILDREN); + } + // 删除 + smsChannelMapper.deleteById(id); + + // 清空缓存 + clearCache(id, channel.getCode()); + } + + /** + * 清空指定渠道编号的缓存 + * + * @param id 渠道编号 + * @param code 渠道编码 + */ + private void clearCache(Long id, String code) { + idClientCache.invalidate(id); + if (StrUtil.isNotEmpty(code)) { + codeClientCache.invalidate(code); + } + } + + private SmsChannelDO validateSmsChannelExists(Long id) { + SmsChannelDO channel = smsChannelMapper.selectById(id); + if (channel == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + return channel; + } + + @Override + public SmsChannelDO getSmsChannel(Long id) { + return smsChannelMapper.selectById(id); + } + + @Override + public List getSmsChannelList() { + return smsChannelMapper.selectList(); + } + + @Override + public PageResult getSmsChannelPage(SmsChannelPageReqVO pageReqVO) { + return smsChannelMapper.selectPage(pageReqVO); + } + + @Override + public SmsClient getSmsClient(Long id) { + return idClientCache.getUnchecked(id); + } + + @Override + public SmsClient getSmsClient(String code) { + return codeClientCache.getUnchecked(code); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsCodeService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsCodeService.java new file mode 100644 index 00000000..97d05693 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsCodeService.java @@ -0,0 +1,40 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeUseReqDTO; + +import javax.validation.Valid; + +/** + * 短信验证码 Service 接口 + * + * @author 芋道源码 + */ +public interface SmsCodeService { + + /** + * 创建短信验证码,并进行发送 + * + * @param reqDTO 发送请求 + */ + void sendSmsCode(@Valid SmsCodeSendReqDTO reqDTO); + + /** + * 验证短信验证码,并进行使用 + * 如果正确,则将验证码标记成已使用 + * 如果错误,则抛出 {@link ServiceException} 异常 + * + * @param reqDTO 使用请求 + */ + void useSmsCode(@Valid SmsCodeUseReqDTO reqDTO); + + /** + * 检查验证码是否有效 + * + * @param reqDTO 校验请求 + */ + void validateSmsCode(@Valid SmsCodeValidateReqDTO reqDTO); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsCodeServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsCodeServiceImpl.java new file mode 100644 index 00000000..9c2094a8 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsCodeServiceImpl.java @@ -0,0 +1,111 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsCodeDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.sms.SmsCodeMapper; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsSceneEnum; +import com.chanko.yunxi.mes.heli.module.system.framework.sms.SmsCodeProperties; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.randomInt; +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils.isToday; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 短信验证码 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SmsCodeServiceImpl implements SmsCodeService { + + @Resource + private SmsCodeProperties smsCodeProperties; + + @Resource + private SmsCodeMapper smsCodeMapper; + + @Resource + private SmsSendService smsSendService; + + @Override + public void sendSmsCode(SmsCodeSendReqDTO reqDTO) { + SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene()); + Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene()); + // 创建验证码 + String code = createSmsCode(reqDTO.getMobile(), reqDTO.getScene(), reqDTO.getCreateIp()); + // 发送验证码 + smsSendService.sendSingleSms(reqDTO.getMobile(), null, null, + sceneEnum.getTemplateCode(), MapUtil.of("code", code)); + } + + private String createSmsCode(String mobile, Integer scene, String ip) { + // 校验是否可以发送验证码,不用筛选场景 + SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null, null); + if (lastSmsCode != null) { + if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis() + < smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁 + throw exception(SMS_CODE_SEND_TOO_FAST); + } + if (isToday(lastSmsCode.getCreateTime()) && // 必须是今天,才能计算超过当天的上限 + lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。 + throw exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY); + } + // TODO 芋艿:提升,每个 IP 每天可发送数量 + // TODO 芋艿:提升,每个 IP 每小时可发送数量 + } + + // 创建验证码记录 + String code = String.valueOf(randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1)); + SmsCodeDO newSmsCode = SmsCodeDO.builder().mobile(mobile).code(code).scene(scene) + .todayIndex(lastSmsCode != null && isToday(lastSmsCode.getCreateTime()) ? lastSmsCode.getTodayIndex() + 1 : 1) + .createIp(ip).used(false).build(); + smsCodeMapper.insert(newSmsCode); + return code; + } + + @Override + public void useSmsCode(SmsCodeUseReqDTO reqDTO) { + // 检测验证码是否有效 + SmsCodeDO lastSmsCode = validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene()); + // 使用验证码 + smsCodeMapper.updateById(SmsCodeDO.builder().id(lastSmsCode.getId()) + .used(true).usedTime(LocalDateTime.now()).usedIp(reqDTO.getUsedIp()).build()); + } + + @Override + public void validateSmsCode(SmsCodeValidateReqDTO reqDTO) { + validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene()); + } + + private SmsCodeDO validateSmsCode0(String mobile, String code, Integer scene) { + // 校验验证码 + SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, code, scene); + // 若验证码不存在,抛出异常 + if (lastSmsCode == null) { + throw exception(SMS_CODE_NOT_FOUND); + } + // 超过时间 + if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis() + >= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期 + throw exception(SMS_CODE_EXPIRED); + } + // 判断验证码是否已被使用 + if (Boolean.TRUE.equals(lastSmsCode.getUsed())) { + throw exception(SMS_CODE_USED); + } + return lastSmsCode; + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsLogService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsLogService.java new file mode 100644 index 00000000..1bd5eeca --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsLogService.java @@ -0,0 +1,68 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsLogDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsTemplateDO; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 短信日志 Service 接口 + * + * @author zzf + * @date 13:48 2021/3/2 + */ +public interface SmsLogService { + + /** + * 创建短信日志 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param isSend 是否发送 + * @param template 短信模板 + * @param templateContent 短信内容 + * @param templateParams 短信参数 + * @return 发送日志编号 + */ + Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, + SmsTemplateDO template, String templateContent, Map templateParams); + + /** + * 更新日志的发送结果 + * + * @param id 日志编号 + * @param success 发送是否成功 + * @param apiSendCode 短信 API 发送结果的编码 + * @param apiSendMsg 短信 API 发送失败的提示 + * @param apiRequestId 短信 API 发送返回的唯一请求 ID + * @param apiSerialNo 短信 API 发送返回的序号 + */ + void updateSmsSendResult(Long id, Boolean success, + String apiSendCode, String apiSendMsg, + String apiRequestId, String apiSerialNo); + + /** + * 更新日志的接收结果 + * + * @param id 日志编号 + * @param success 是否接收成功 + * @param receiveTime 用户接收时间 + * @param apiReceiveCode API 接收结果的编码 + * @param apiReceiveMsg API 接收结果的说明 + */ + void updateSmsReceiveResult(Long id, Boolean success, + LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg); + + /** + * 获得短信日志分页 + * + * @param pageReqVO 分页查询 + * @return 短信日志分页 + */ + PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsLogServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsLogServiceImpl.java new file mode 100644 index 00000000..abfecaa0 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsLogServiceImpl.java @@ -0,0 +1,79 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsLogDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.sms.SmsLogMapper; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsReceiveStatusEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.sms.SmsSendStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Objects; + +/** + * 短信日志 Service 实现类 + * + * @author zzf + */ +@Slf4j +@Service +public class SmsLogServiceImpl implements SmsLogService { + + @Resource + private SmsLogMapper smsLogMapper; + + @Override + public Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, + SmsTemplateDO template, String templateContent, Map templateParams) { + SmsLogDO.SmsLogDOBuilder logBuilder = SmsLogDO.builder(); + // 根据是否要发送,设置状态 + logBuilder.sendStatus(Objects.equals(isSend, true) ? SmsSendStatusEnum.INIT.getStatus() + : SmsSendStatusEnum.IGNORE.getStatus()); + // 设置手机相关字段 + logBuilder.mobile(mobile).userId(userId).userType(userType); + // 设置模板相关字段 + logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType()); + logBuilder.templateContent(templateContent).templateParams(templateParams) + .apiTemplateId(template.getApiTemplateId()); + // 设置渠道相关字段 + logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode()); + // 设置接收相关字段 + logBuilder.receiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + + // 插入数据库 + SmsLogDO logDO = logBuilder.build(); + smsLogMapper.insert(logDO); + return logDO.getId(); + } + + @Override + public void updateSmsSendResult(Long id, Boolean success, + String apiSendCode, String apiSendMsg, + String apiRequestId, String apiSerialNo) { + SmsSendStatusEnum sendStatus = success ? SmsSendStatusEnum.SUCCESS : SmsSendStatusEnum.FAILURE; + smsLogMapper.updateById(SmsLogDO.builder().id(id) + .sendStatus(sendStatus.getStatus()).sendTime(LocalDateTime.now()) + .apiSendCode(apiSendCode).apiSendMsg(apiSendMsg) + .apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).build()); + } + + @Override + public void updateSmsReceiveResult(Long id, Boolean success, LocalDateTime receiveTime, + String apiReceiveCode, String apiReceiveMsg) { + SmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? + SmsReceiveStatusEnum.SUCCESS : SmsReceiveStatusEnum.FAILURE; + smsLogMapper.updateById(SmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()) + .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); + } + + @Override + public PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO) { + return smsLogMapper.selectPage(pageReqVO); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsSendService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsSendService.java new file mode 100644 index 00000000..76ae1f36 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsSendService.java @@ -0,0 +1,78 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import com.chanko.yunxi.mes.heli.module.system.mq.message.sms.SmsSendMessage; + +import java.util.List; +import java.util.Map; + +/** + * 短信发送 Service 接口 + * + * @author 芋道源码 + */ +public interface SmsSendService { + + /** + * 发送单条短信给管理后台的用户 + * + * 在 mobile 为空时,使用 userId 加载对应管理员的手机号 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSmsToAdmin(String mobile, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条短信给用户 APP 的用户 + * + * 在 mobile 为空时,使用 userId 加载对应会员的手机号 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSmsToMember(String mobile, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条短信给用户 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSms(String mobile, Long userId, Integer userType, + String templateCode, Map templateParams); + + default void sendBatchSms(List mobiles, List userIds, Integer userType, + String templateCode, Map templateParams) { + throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!"); + } + + /** + * 执行真正的短信发送 + * 注意,该方法仅仅提供给 MQ Consumer 使用 + * + * @param message 短信 + */ + void doSendSms(SmsSendMessage message); + + /** + * 接收短信的接收结果 + * + * @param channelCode 渠道编码 + * @param text 结果内容 + * @throws Throwable 处理失败时,抛出异常 + */ + void receiveSmsStatus(String channelCode, String text) throws Throwable; + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsSendServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsSendServiceImpl.java new file mode 100644 index 00000000..26900253 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsSendServiceImpl.java @@ -0,0 +1,191 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.core.KeyValue; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.enums.UserTypeEnum; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.annotation.DataPermission; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.SmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsSendRespDTO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsChannelDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.mq.message.sms.SmsSendMessage; +import com.chanko.yunxi.mes.heli.module.system.mq.producer.sms.SmsProducer; +import com.chanko.yunxi.mes.heli.module.system.service.member.MemberService; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 短信发送 Service 发送的实现 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class SmsSendServiceImpl implements SmsSendService { + + @Resource + private AdminUserService adminUserService; + @Resource + private MemberService memberService; + @Resource + private SmsChannelService smsChannelService; + @Resource + private SmsTemplateService smsTemplateService; + @Resource + private SmsLogService smsLogService; + + @Resource + private SmsProducer smsProducer; + + @Override + @DataPermission(enable = false) // 发送短信时,无需考虑数据权限 + public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map templateParams) { + // 如果 mobile 为空,则加载用户编号对应的手机号 + if (StrUtil.isEmpty(mobile)) { + AdminUserDO user = adminUserService.getUser(userId); + if (user != null) { + mobile = user.getMobile(); + } + } + // 执行发送 + return sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map templateParams) { + // 如果 mobile 为空,则加载用户编号对应的手机号 + if (StrUtil.isEmpty(mobile)) { + mobile = memberService.getMemberUserMobile(userId); + } + // 执行发送 + return sendSingleSms(mobile, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleSms(String mobile, Long userId, Integer userType, + String templateCode, Map templateParams) { + // 校验短信模板是否合法 + SmsTemplateDO template = validateSmsTemplate(templateCode); + // 校验短信渠道是否合法 + SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId()); + + // 校验手机号码是否存在 + mobile = validateMobile(mobile); + // 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志 + List> newTemplateParams = buildTemplateParams(template, templateParams); + + // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 + Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()) + && CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus()); + String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams); + Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams); + + // 发送 MQ 消息,异步执行发送短信 + if (isSend) { + smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(), + template.getApiTemplateId(), newTemplateParams); + } + return sendLogId; + } + + @VisibleForTesting + SmsChannelDO validateSmsChannel(Long channelId) { + // 获得短信模板。考虑到效率,从缓存中获取 + SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); + // 短信模板不存在 + if (channelDO == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + return channelDO; + } + + @VisibleForTesting + SmsTemplateDO validateSmsTemplate(String templateCode) { + // 获得短信模板。考虑到效率,从缓存中获取 + SmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode); + // 短信模板不存在 + if (template == null) { + throw exception(SMS_SEND_TEMPLATE_NOT_EXISTS); + } + return template; + } + + /** + * 将参数模板,处理成有序的 KeyValue 数组 + *

+ * 原因是,部分短信平台并不是使用 key 作为参数,而是数组下标,例如说 腾讯云 + * + * @param template 短信模板 + * @param templateParams 原始参数 + * @return 处理后的参数 + */ + @VisibleForTesting + List> buildTemplateParams(SmsTemplateDO template, Map templateParams) { + return template.getParams().stream().map(key -> { + Object value = templateParams.get(key); + if (value == null) { + throw exception(SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, key); + } + return new KeyValue<>(key, value); + }).collect(Collectors.toList()); + } + + @VisibleForTesting + public String validateMobile(String mobile) { + if (StrUtil.isEmpty(mobile)) { + throw exception(SMS_SEND_MOBILE_NOT_EXISTS); + } + return mobile; + } + + @Override + public void doSendSms(SmsSendMessage message) { + // 获得渠道对应的 SmsClient 客户端 + SmsClient smsClient = smsChannelService.getSmsClient(message.getChannelId()); + Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId()); + // 发送短信 + try { + SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(), + message.getApiTemplateId(), message.getTemplateParams()); + smsLogService.updateSmsSendResult(message.getLogId(), sendResponse.getSuccess(), + sendResponse.getApiCode(), sendResponse.getApiMsg(), + sendResponse.getApiRequestId(), sendResponse.getSerialNo()); + } catch (Throwable ex) { + log.error("[doSendSms][发送短信异常,日志编号({})]", message.getLogId(), ex); + smsLogService.updateSmsSendResult(message.getLogId(), false, + "EXCEPTION", ExceptionUtil.getRootCauseMessage(ex), null, null); + } + } + + @Override + public void receiveSmsStatus(String channelCode, String text) throws Throwable { + // 获得渠道对应的 SmsClient 客户端 + SmsClient smsClient = smsChannelService.getSmsClient(channelCode); + Assert.notNull(smsClient, "短信客户端({}) 不存在", channelCode); + // 解析内容 + List receiveResults = smsClient.parseSmsReceiveStatus(text); + if (CollUtil.isEmpty(receiveResults)) { + return; + } + // 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新 + receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), + result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg())); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsTemplateService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsTemplateService.java new file mode 100644 index 00000000..2ec8bcc6 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsTemplateService.java @@ -0,0 +1,82 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsTemplateDO; + +import javax.validation.Valid; +import java.util.Map; + +/** + * 短信模板 Service 接口 + * + * @author zzf + * @since 2021/1/25 9:24 + */ +public interface SmsTemplateService { + + /** + * 创建短信模板 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSmsTemplate(@Valid SmsTemplateSaveReqVO createReqVO); + + /** + * 更新短信模板 + * + * @param updateReqVO 更新信息 + */ + void updateSmsTemplate(@Valid SmsTemplateSaveReqVO updateReqVO); + + /** + * 删除短信模板 + * + * @param id 编号 + */ + void deleteSmsTemplate(Long id); + + /** + * 获得短信模板 + * + * @param id 编号 + * @return 短信模板 + */ + SmsTemplateDO getSmsTemplate(Long id); + + /** + * 获得短信模板,从缓存中 + * + * @param code 模板编码 + * @return 短信模板 + */ + SmsTemplateDO getSmsTemplateByCodeFromCache(String code); + + /** + * 获得短信模板分页 + * + * @param pageReqVO 分页查询 + * @return 短信模板分页 + */ + PageResult getSmsTemplatePage(SmsTemplatePageReqVO pageReqVO); + + /** + * 获得指定短信渠道下的短信模板数量 + * + * @param channelId 短信渠道编号 + * @return 数量 + */ + Long getSmsTemplateCountByChannelId(Long channelId); + + /** + * 格式化短信内容 + * + * @param content 短信模板的内容 + * @param params 内容的参数 + * @return 格式化后的内容 + */ + String formatSmsTemplateContent(String content, Map params); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsTemplateServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsTemplateServiceImpl.java new file mode 100644 index 00000000..c6b2813a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/sms/SmsTemplateServiceImpl.java @@ -0,0 +1,199 @@ +package com.chanko.yunxi.mes.heli.module.system.service.sms; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.SmsClient; +import com.chanko.yunxi.mes.heli.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.chanko.yunxi.mes.heli.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsChannelDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.sms.SmsTemplateMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.redis.RedisKeyConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 短信模板 Service 实现类 + * + * @author zzf + * @since 2021/1/25 9:25 + */ +@Service +@Slf4j +public class SmsTemplateServiceImpl implements SmsTemplateService { + + /** + * 正则表达式,匹配 {} 中的变量 + */ + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); + + @Resource + private SmsTemplateMapper smsTemplateMapper; + + @Resource + private SmsChannelService smsChannelService; + + @Override + public Long createSmsTemplate(SmsTemplateSaveReqVO createReqVO) { + // 校验短信渠道 + SmsChannelDO channelDO = validateSmsChannel(createReqVO.getChannelId()); + // 校验短信编码是否重复 + validateSmsTemplateCodeDuplicate(null, createReqVO.getCode()); + // 校验短信模板 + validateApiTemplate(createReqVO.getChannelId(), createReqVO.getApiTemplateId()); + + // 插入 + SmsTemplateDO template = BeanUtils.toBean(createReqVO, SmsTemplateDO.class); + template.setParams(parseTemplateContentParams(template.getContent())); + template.setChannelCode(channelDO.getCode()); + smsTemplateMapper.insert(template); + // 返回 + return template.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.SMS_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理 + public void updateSmsTemplate(SmsTemplateSaveReqVO updateReqVO) { + // 校验存在 + validateSmsTemplateExists(updateReqVO.getId()); + // 校验短信渠道 + SmsChannelDO channelDO = validateSmsChannel(updateReqVO.getChannelId()); + // 校验短信编码是否重复 + validateSmsTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode()); + // 校验短信模板 + validateApiTemplate(updateReqVO.getChannelId(), updateReqVO.getApiTemplateId()); + + // 更新 + SmsTemplateDO updateObj = BeanUtils.toBean(updateReqVO, SmsTemplateDO.class); + updateObj.setParams(parseTemplateContentParams(updateObj.getContent())); + updateObj.setChannelCode(channelDO.getCode()); + smsTemplateMapper.updateById(updateObj); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.SMS_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理 + public void deleteSmsTemplate(Long id) { + // 校验存在 + validateSmsTemplateExists(id); + // 更新 + smsTemplateMapper.deleteById(id); + } + + private void validateSmsTemplateExists(Long id) { + if (smsTemplateMapper.selectById(id) == null) { + throw exception(SMS_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public SmsTemplateDO getSmsTemplate(Long id) { + return smsTemplateMapper.selectById(id); + } + + @Override + @Cacheable(cacheNames = RedisKeyConstants.SMS_TEMPLATE, key = "#code", + unless = "#result == null") + public SmsTemplateDO getSmsTemplateByCodeFromCache(String code) { + return smsTemplateMapper.selectByCode(code); + } + + @Override + public PageResult getSmsTemplatePage(SmsTemplatePageReqVO pageReqVO) { + return smsTemplateMapper.selectPage(pageReqVO); + } + + @Override + public Long getSmsTemplateCountByChannelId(Long channelId) { + return smsTemplateMapper.selectCountByChannelId(channelId); + } + + @VisibleForTesting + public SmsChannelDO validateSmsChannel(Long channelId) { + SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); + if (channelDO == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + if (CommonStatusEnum.isDisable(channelDO.getStatus())) { + throw exception(SMS_CHANNEL_DISABLE); + } + return channelDO; + } + + @VisibleForTesting + public void validateSmsTemplateCodeDuplicate(Long id, String code) { + SmsTemplateDO template = smsTemplateMapper.selectByCode(code); + if (template == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); + } + if (!template.getId().equals(id)) { + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); + } + } + + /** + * 校验 API 短信平台的模板是否有效 + * + * @param channelId 渠道编号 + * @param apiTemplateId API 模板编号 + */ + @VisibleForTesting + void validateApiTemplate(Long channelId, String apiTemplateId) { + // 获得短信模板 + SmsClient smsClient = smsChannelService.getSmsClient(channelId); + Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", channelId)); + SmsTemplateRespDTO template; + try { + template = smsClient.getSmsTemplate(apiTemplateId); + } catch (Throwable ex) { + throw exception(SMS_TEMPLATE_API_ERROR, ExceptionUtil.getRootCauseMessage(ex)); + } + // 校验短信模版 + if (template == null) { + throw exception(SMS_TEMPLATE_API_NOT_FOUND); + } + if (Objects.equals(template.getAuditStatus(), SmsTemplateAuditStatusEnum.CHECKING.getStatus())) { + throw exception(SMS_TEMPLATE_API_AUDIT_CHECKING); + } + if (Objects.equals(template.getAuditStatus(), SmsTemplateAuditStatusEnum.FAIL.getStatus())) { + throw exception(SMS_TEMPLATE_API_AUDIT_FAIL, template.getAuditReason()); + } + Assert.equals(template.getAuditStatus(), SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + String.format("短信模板(%s) 审核状态(%d) 不正确", apiTemplateId, template.getAuditStatus())); + } + + @Override + public String formatSmsTemplateContent(String content, Map params) { + return StrUtil.format(content, params); + } + + @VisibleForTesting + List parseTemplateContentParams(String content) { + return ReUtil.findAllGroup1(PATTERN_PARAMS, content); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialClientService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialClientService.java new file mode 100644 index 00000000..723e8f35 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialClientService.java @@ -0,0 +1,104 @@ +package com.chanko.yunxi.mes.heli.module.system.service.social; + +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialClientDO; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import com.xingyuv.jushauth.model.AuthUser; +import me.chanjar.weixin.common.bean.WxJsapiSignature; + +import javax.validation.Valid; + +/** + * 社交应用 Service 接口 + * + * @author 芋道源码 + */ +public interface SocialClientService { + + /** + * 获得社交平台的授权 URL + * + * @param socialType 社交平台的类型 {@link SocialTypeEnum} + * @param userType 用户类型 + * @param redirectUri 重定向 URL + * @return 社交平台的授权 URL + */ + String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri); + + /** + * 请求社交平台,获得授权的用户 + * + * @param socialType 社交平台的类型 + * @param userType 用户类型 + * @param code 授权码 + * @param state 授权 state + * @return 授权的用户 + */ + AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state); + + // =================== 微信公众号独有 =================== + + /** + * 创建微信公众号的 JS SDK 初始化所需的签名 + * + * @param userType 用户类型 + * @param url 访问的 URL 地址 + * @return 签名 + */ + WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url); + + // =================== 微信小程序独有 =================== + + /** + * 获得微信小程序的手机信息 + * + * @param userType 用户类型 + * @param phoneCode 手机授权码 + * @return 手机信息 + */ + WxMaPhoneNumberInfo getWxMaPhoneNumberInfo(Integer userType, String phoneCode); + + // =================== 客户端管理 =================== + + /** + * 创建社交客户端 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSocialClient(@Valid SocialClientSaveReqVO createReqVO); + + /** + * 更新社交客户端 + * + * @param updateReqVO 更新信息 + */ + void updateSocialClient(@Valid SocialClientSaveReqVO updateReqVO); + + /** + * 删除社交客户端 + * + * @param id 编号 + */ + void deleteSocialClient(Long id); + + /** + * 获得社交客户端 + * + * @param id 编号 + * @return 社交客户端 + */ + SocialClientDO getSocialClient(Long id); + + /** + * 获得社交客户端分页 + * + * @param pageReqVO 分页查询 + * @return 社交客户端分页 + */ + PageResult getSocialClientPage(SocialClientPageReqVO pageReqVO); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialClientServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialClientServiceImpl.java new file mode 100644 index 00000000..b529f455 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialClientServiceImpl.java @@ -0,0 +1,339 @@ +package com.chanko.yunxi.mes.heli.module.system.service.social; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ReflectUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.cache.CacheUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.http.HttpUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialClientDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.social.SocialClientMapper; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.xingyuv.jushauth.config.AuthConfig; +import com.xingyuv.jushauth.model.AuthCallback; +import com.xingyuv.jushauth.model.AuthResponse; +import com.xingyuv.jushauth.model.AuthUser; +import com.xingyuv.jushauth.request.AuthRequest; +import com.xingyuv.jushauth.utils.AuthStateUtils; +import com.xingyuv.justauth.AuthRequestFactory; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.WxJsapiSignature; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.Objects; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils.toJsonString; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 社交应用 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class SocialClientServiceImpl implements SocialClientService { + + @Resource + private AuthRequestFactory authRequestFactory; + + @Resource + private WxMpService wxMpService; + @Resource + private WxMpProperties wxMpProperties; + @Resource + private StringRedisTemplate stringRedisTemplate; // WxMpService 需要使用到,所以在 Service 注入了它 + /** + * 缓存 WxMpService 对象 + * + * key:使用微信公众号的 appId + secret 拼接,即 {@link SocialClientDO} 的 clientId 和 clientSecret 属性。 + * 为什么 key 使用这种格式?因为 {@link SocialClientDO} 在管理后台可以变更,通过这个 key 存储它的单例。 + * + * 为什么要做 WxMpService 缓存?因为 WxMpService 构建成本比较大,所以尽量保证它是单例。 + */ + private final LoadingCache wxMpServiceCache = CacheUtils.buildAsyncReloadingCache( + Duration.ofSeconds(10L), + new CacheLoader() { + + @Override + public WxMpService load(String key) { + String[] keys = key.split(":"); + return buildWxMpService(keys[0], keys[1]); + } + + }); + + @Resource + private WxMaService wxMaService; + @Resource + private WxMaProperties wxMaProperties; + /** + * 缓存 WxMaService 对象 + * + * 说明同 {@link #wxMpServiceCache} 变量 + */ + private final LoadingCache wxMaServiceCache = CacheUtils.buildAsyncReloadingCache( + Duration.ofSeconds(10L), + new CacheLoader() { + + @Override + public WxMaService load(String key) { + String[] keys = key.split(":"); + return buildWxMaService(keys[0], keys[1]); + } + + }); + + @Resource + private SocialClientMapper socialClientMapper; + + @Override + public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) { + // 获得对应的 AuthRequest 实现 + AuthRequest authRequest = buildAuthRequest(socialType, userType); + // 生成跳转地址 + String authorizeUri = authRequest.authorize(AuthStateUtils.createState()); + return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri); + } + + @Override + public AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state) { + // 构建请求 + AuthRequest authRequest = buildAuthRequest(socialType, userType); + AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build(); + // 执行请求 + AuthResponse authResponse = authRequest.login(authCallback); + log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", socialType, + toJsonString(authCallback), toJsonString(authResponse)); + if (!authResponse.ok()) { + throw exception(SOCIAL_USER_AUTH_FAILURE, authResponse.getMsg()); + } + return (AuthUser) authResponse.getData(); + } + + /** + * 构建 AuthRequest 对象,支持多租户配置 + * + * @param socialType 社交类型 + * @param userType 用户类型 + * @return AuthRequest 对象 + */ + @VisibleForTesting + AuthRequest buildAuthRequest(Integer socialType, Integer userType) { + // 1. 先查找默认的配置项,从 application-*.yaml 中读取 + AuthRequest request = authRequestFactory.get(SocialTypeEnum.valueOfType(socialType).getSource()); + Assert.notNull(request, String.format("社交平台(%d) 不存在", socialType)); + // 2. 查询 DB 的配置项,如果存在则进行覆盖 + SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(socialType, userType); + if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + // 2.1 构造新的 AuthConfig 对象 + AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(request, "config"); + AuthConfig newAuthConfig = ReflectUtil.newInstance(authConfig.getClass()); + BeanUtil.copyProperties(authConfig, newAuthConfig); + // 2.2 修改对应的 clientId + clientSecret 密钥 + newAuthConfig.setClientId(client.getClientId()); + newAuthConfig.setClientSecret(client.getClientSecret()); + if (client.getAgentId() != null) { // 如果有 agentId 则修改 agentId + newAuthConfig.setAgentId(client.getAgentId()); + } + // 2.3 设置会 request 里,进行后续使用 + ReflectUtil.setFieldValue(request, "config", newAuthConfig); + } + return request; + } + + // =================== 微信公众号独有 =================== + + @Override + @SneakyThrows + public WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url) { + WxMpService service = getWxMpService(userType); + return service.createJsapiSignature(url); + } + + /** + * 获得 clientId + clientSecret 对应的 WxMpService 对象 + * + * @param userType 用户类型 + * @return WxMpService 对象 + */ + @VisibleForTesting + WxMpService getWxMpService(Integer userType) { + // 第一步,查询 DB 的配置项,获得对应的 WxMpService 对象 + SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( + SocialTypeEnum.WECHAT_MP.getType(), userType); + if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + return wxMpServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret()); + } + // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMpService 对象 + return wxMpService; + } + + /** + * 创建 clientId + clientSecret 对应的 WxMpService 对象 + * + * @param clientId 微信公众号 appId + * @param clientSecret 微信公众号 secret + * @return WxMpService 对象 + */ + public WxMpService buildWxMpService(String clientId, String clientSecret) { + // 第一步,创建 WxMpRedisConfigImpl 对象 + WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl( + new RedisTemplateWxRedisOps(stringRedisTemplate), + wxMpProperties.getConfigStorage().getKeyPrefix()); + configStorage.setAppId(clientId); + configStorage.setSecret(clientSecret); + + // 第二步,创建 WxMpService 对象 + WxMpService service = new WxMpServiceImpl(); + service.setWxMpConfigStorage(configStorage); + return service; + } + + // =================== 微信小程序独有 =================== + + @Override + public WxMaPhoneNumberInfo getWxMaPhoneNumberInfo(Integer userType, String phoneCode) { + WxMaService service = getWxMaService(userType); + try { + return service.getUserService().getPhoneNoInfo(phoneCode); + } catch (WxErrorException e) { + log.error("[getPhoneNoInfo][userType({}) phoneCode({}) 获得手机号失败]", userType, phoneCode, e); + throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR); + } + } + + /** + * 获得 clientId + clientSecret 对应的 WxMpService 对象 + * + * @param userType 用户类型 + * @return WxMpService 对象 + */ + @VisibleForTesting + WxMaService getWxMaService(Integer userType) { + // 第一步,查询 DB 的配置项,获得对应的 WxMaService 对象 + SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( + SocialTypeEnum.WECHAT_MINI_APP.getType(), userType); + if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + return wxMaServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret()); + } + // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMaService 对象 + return wxMaService; + } + + /** + * 创建 clientId + clientSecret 对应的 WxMaService 对象 + * + * @param clientId 微信小程序 appId + * @param clientSecret 微信小程序 secret + * @return WxMaService 对象 + */ + private WxMaService buildWxMaService(String clientId, String clientSecret) { + // 第一步,创建 WxMaRedisBetterConfigImpl 对象 + WxMaRedisBetterConfigImpl configStorage = new WxMaRedisBetterConfigImpl( + new RedisTemplateWxRedisOps(stringRedisTemplate), + wxMaProperties.getConfigStorage().getKeyPrefix()); + configStorage.setAppid(clientId); + configStorage.setSecret(clientSecret); + + // 第二步,创建 WxMpService 对象 + WxMaService service = new WxMaServiceImpl(); + service.setWxMaConfig(configStorage); + return service; + } + + // =================== 客户端管理 =================== + + @Override + public Long createSocialClient(SocialClientSaveReqVO createReqVO) { + // 校验重复 + validateSocialClientUnique(null, createReqVO.getUserType(), createReqVO.getSocialType()); + + // 插入 + SocialClientDO client = BeanUtils.toBean(createReqVO, SocialClientDO.class); + socialClientMapper.insert(client); + return client.getId(); + } + + @Override + public void updateSocialClient(SocialClientSaveReqVO updateReqVO) { + // 校验存在 + validateSocialClientExists(updateReqVO.getId()); + // 校验重复 + validateSocialClientUnique(updateReqVO.getId(), updateReqVO.getUserType(), updateReqVO.getSocialType()); + + // 更新 + SocialClientDO updateObj = BeanUtils.toBean(updateReqVO, SocialClientDO.class); + socialClientMapper.updateById(updateObj); + } + + @Override + public void deleteSocialClient(Long id) { + // 校验存在 + validateSocialClientExists(id); + // 删除 + socialClientMapper.deleteById(id); + } + + private void validateSocialClientExists(Long id) { + if (socialClientMapper.selectById(id) == null) { + throw exception(SOCIAL_CLIENT_NOT_EXISTS); + } + } + + /** + * 校验社交应用是否重复,需要保证 userType + socialType 唯一 + * + * 原因是,不同端(userType)选择某个社交登录(socialType)时,需要通过 {@link #buildAuthRequest(Integer, Integer)} 构建对应的请求 + * + * @param id 编号 + * @param userType 用户类型 + * @param socialType 社交类型 + */ + private void validateSocialClientUnique(Long id, Integer userType, Integer socialType) { + SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( + socialType, userType); + if (client == null) { + return; + } + if (id == null // 新增时,说明重复 + || ObjUtil.notEqual(id, client.getId())) { // 更新时,如果 id 不一致,说明重复 + throw exception(SOCIAL_CLIENT_UNIQUE); + } + } + + @Override + public SocialClientDO getSocialClient(Long id) { + return socialClientMapper.selectById(id); + } + + @Override + public PageResult getSocialClientPage(SocialClientPageReqVO pageReqVO) { + return socialClientMapper.selectPage(pageReqVO); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialUserService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialUserService.java new file mode 100644 index 00000000..821f3639 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialUserService.java @@ -0,0 +1,79 @@ +package com.chanko.yunxi.mes.heli.module.system.service.social; + +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserBindReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserRespDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialUserDO; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; + +import javax.validation.Valid; +import java.util.List; + +/** + * 社交用户 Service 接口,例如说社交平台的授权登录 + * + * @author 芋道源码 + */ +public interface SocialUserService { + + /** + * 获得指定用户的社交用户列表 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 社交用户列表 + */ + List getSocialUserList(Long userId, Integer userType); + + /** + * 绑定社交用户 + * + * @param reqDTO 绑定信息 + * @return 社交用户 openid + */ + String bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); + + /** + * 取消绑定社交用户 + * + * @param userId 用户编号 + * @param userType 全局用户类型 + * @param socialType 社交平台的类型 {@link SocialTypeEnum} + * @param openid 社交平台的 openid + */ + void unbindSocialUser(Long userId, Integer userType, Integer socialType, String openid); + + /** + * 获得社交用户 + * + * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 + * + * @param userType 用户类型 + * @param socialType 社交平台的类型 + * @param code 授权码 + * @param state state + * @return 社交用户 + */ + SocialUserRespDTO getSocialUser(Integer userType, Integer socialType, String code, String state); + + // ==================== 社交用户 CRUD ==================== + + /** + * 获得社交用户 + * + * @param id 编号 + * @return 社交用户 + */ + SocialUserDO getSocialUser(Long id); + + /** + * 获得社交用户分页 + * + * @param pageReqVO 分页查询 + * @return 社交用户分页 + */ + PageResult getSocialUserPage(SocialUserPageReqVO pageReqVO); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialUserServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialUserServiceImpl.java new file mode 100644 index 00000000..1278cfb3 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/social/SocialUserServiceImpl.java @@ -0,0 +1,162 @@ +package com.chanko.yunxi.mes.heli.module.system.service.social; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserBindReqDTO; +import com.chanko.yunxi.mes.heli.module.system.api.social.dto.SocialUserRespDTO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialUserBindDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.social.SocialUserDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.social.SocialUserBindMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.social.SocialUserMapper; +import com.chanko.yunxi.mes.heli.module.system.enums.social.SocialTypeEnum; +import com.xingyuv.jushauth.model.AuthUser; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; +import static com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils.toJsonString; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.AUTH_THIRD_LOGIN_NOT_BIND; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.SOCIAL_USER_NOT_FOUND; + +/** + * 社交用户 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class SocialUserServiceImpl implements SocialUserService { + + @Resource + private SocialUserBindMapper socialUserBindMapper; + @Resource + private SocialUserMapper socialUserMapper; + + @Resource + private SocialClientService socialClientService; + + @Override + public List getSocialUserList(Long userId, Integer userType) { + // 获得绑定 + List socialUserBinds = socialUserBindMapper.selectListByUserIdAndUserType(userId, userType); + if (CollUtil.isEmpty(socialUserBinds)) { + return Collections.emptyList(); + } + // 获得社交用户 + return socialUserMapper.selectBatchIds(convertSet(socialUserBinds, SocialUserBindDO::getSocialUserId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String bindSocialUser(SocialUserBindReqDTO reqDTO) { + // 获得社交用户 + SocialUserDO socialUser = authSocialUser(reqDTO.getSocialType(), reqDTO.getUserType(), + reqDTO.getCode(), reqDTO.getState()); + Assert.notNull(socialUser, "社交用户不能为空"); + + // 社交用户可能之前绑定过别的用户,需要进行解绑 + socialUserBindMapper.deleteByUserTypeAndSocialUserId(reqDTO.getUserType(), socialUser.getId()); + + // 用户可能之前已经绑定过该社交类型,需要进行解绑 + socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(reqDTO.getUserType(), reqDTO.getUserId(), + socialUser.getType()); + + // 绑定当前登录的社交用户 + SocialUserBindDO socialUserBind = SocialUserBindDO.builder() + .userId(reqDTO.getUserId()).userType(reqDTO.getUserType()) + .socialUserId(socialUser.getId()).socialType(socialUser.getType()).build(); + socialUserBindMapper.insert(socialUserBind); + return socialUser.getOpenid(); + } + + @Override + public void unbindSocialUser(Long userId, Integer userType, Integer socialType, String openid) { + // 获得 openid 对应的 SocialUserDO 社交用户 + SocialUserDO socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, openid); + if (socialUser == null) { + throw exception(SOCIAL_USER_NOT_FOUND); + } + + // 获得对应的社交绑定关系 + socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(userType, userId, socialUser.getType()); + } + + @Override + public SocialUserRespDTO getSocialUser(Integer userType, Integer socialType, String code, String state) { + // 获得社交用户 + SocialUserDO socialUser = authSocialUser(socialType, userType, code, state); + Assert.notNull(socialUser, "社交用户不能为空"); + + // 如果未绑定的社交用户,则无法自动登录,进行报错 + SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndSocialUserId(userType, + socialUser.getId()); + if (socialUserBind == null) { + throw exception(AUTH_THIRD_LOGIN_NOT_BIND); + } + return new SocialUserRespDTO(socialUser.getOpenid(), socialUserBind.getUserId()); + } + + /** + * 授权获得对应的社交用户 + * 如果授权失败,则会抛出 {@link ServiceException} 异常 + * + * @param socialType 社交平台的类型 {@link SocialTypeEnum} + * @param userType 用户类型 + * @param code 授权码 + * @param state state + * @return 授权用户 + */ + @NotNull + public SocialUserDO authSocialUser(Integer socialType, Integer userType, String code, String state) { + // 优先从 DB 中获取,因为 code 有且可以使用一次。 + // 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次 + SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(socialType, code, state); + if (socialUser != null) { + return socialUser; + } + + // 请求获取 + AuthUser authUser = socialClientService.getAuthUser(socialType, userType, code, state); + Assert.notNull(authUser, "三方用户不能为空"); + + // 保存到 DB 中 + socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, authUser.getUuid()); + if (socialUser == null) { + socialUser = new SocialUserDO(); + } + socialUser.setType(socialType).setCode(code).setState(state) // 需要保存 code + state 字段,保证后续可查询 + .setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken()))) + .setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo())); + if (socialUser.getId() == null) { + socialUserMapper.insert(socialUser); + } else { + socialUserMapper.updateById(socialUser); + } + return socialUser; + } + + // ==================== 社交用户 CRUD ==================== + + @Override + public SocialUserDO getSocialUser(Long id) { + return socialUserMapper.selectById(id); + } + + @Override + public PageResult getSocialUserPage(SocialUserPageReqVO pageReqVO) { + return socialUserMapper.selectPage(pageReqVO); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantPackageService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantPackageService.java new file mode 100644 index 00000000..0cb5e22c --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantPackageService.java @@ -0,0 +1,72 @@ +package com.chanko.yunxi.mes.heli.module.system.service.tenant; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages.TenantPackageSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantPackageDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 租户套餐 Service 接口 + * + * @author 芋道源码 + */ +public interface TenantPackageService { + + /** + * 创建租户套餐 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTenantPackage(@Valid TenantPackageSaveReqVO createReqVO); + + /** + * 更新租户套餐 + * + * @param updateReqVO 更新信息 + */ + void updateTenantPackage(@Valid TenantPackageSaveReqVO updateReqVO); + + /** + * 删除租户套餐 + * + * @param id 编号 + */ + void deleteTenantPackage(Long id); + + /** + * 获得租户套餐 + * + * @param id 编号 + * @return 租户套餐 + */ + TenantPackageDO getTenantPackage(Long id); + + /** + * 获得租户套餐分页 + * + * @param pageReqVO 分页查询 + * @return 租户套餐分页 + */ + PageResult getTenantPackagePage(TenantPackagePageReqVO pageReqVO); + + /** + * 校验租户套餐 + * + * @param id 编号 + * @return 租户套餐 + */ + TenantPackageDO validTenantPackage(Long id); + + /** + * 获得指定状态的租户套餐列表 + * + * @param status 状态 + * @return 租户套餐 + */ + List getTenantPackageListByStatus(Integer status); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantPackageServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantPackageServiceImpl.java new file mode 100644 index 00000000..fcbb387e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantPackageServiceImpl.java @@ -0,0 +1,114 @@ +package com.chanko.yunxi.mes.heli.module.system.service.tenant; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.packages.TenantPackageSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantPackageDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.tenant.TenantPackageMapper; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 租户套餐 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class TenantPackageServiceImpl implements TenantPackageService { + + @Resource + private TenantPackageMapper tenantPackageMapper; + + @Resource + @Lazy // 避免循环依赖的报错 + private TenantService tenantService; + + @Override + public Long createTenantPackage(TenantPackageSaveReqVO createReqVO) { + // 插入 + TenantPackageDO tenantPackage = BeanUtils.toBean(createReqVO, TenantPackageDO.class); + tenantPackageMapper.insert(tenantPackage); + // 返回 + return tenantPackage.getId(); + } + + @Override + @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 + public void updateTenantPackage(TenantPackageSaveReqVO updateReqVO) { + // 校验存在 + TenantPackageDO tenantPackage = validateTenantPackageExists(updateReqVO.getId()); + // 更新 + TenantPackageDO updateObj = BeanUtils.toBean(updateReqVO, TenantPackageDO.class); + tenantPackageMapper.updateById(updateObj); + // 如果菜单发生变化,则修改每个租户的菜单 + if (!CollUtil.isEqualList(tenantPackage.getMenuIds(), updateReqVO.getMenuIds())) { + List tenants = tenantService.getTenantListByPackageId(tenantPackage.getId()); + tenants.forEach(tenant -> tenantService.updateTenantRoleMenu(tenant.getId(), updateReqVO.getMenuIds())); + } + } + + @Override + public void deleteTenantPackage(Long id) { + // 校验存在 + validateTenantPackageExists(id); + // 校验正在使用 + validateTenantUsed(id); + // 删除 + tenantPackageMapper.deleteById(id); + } + + private TenantPackageDO validateTenantPackageExists(Long id) { + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); + if (tenantPackage == null) { + throw exception(TENANT_PACKAGE_NOT_EXISTS); + } + return tenantPackage; + } + + private void validateTenantUsed(Long id) { + if (tenantService.getTenantCountByPackageId(id) > 0) { + throw exception(TENANT_PACKAGE_USED); + } + } + + @Override + public TenantPackageDO getTenantPackage(Long id) { + return tenantPackageMapper.selectById(id); + } + + @Override + public PageResult getTenantPackagePage(TenantPackagePageReqVO pageReqVO) { + return tenantPackageMapper.selectPage(pageReqVO); + } + + @Override + public TenantPackageDO validTenantPackage(Long id) { + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); + if (tenantPackage == null) { + throw exception(TENANT_PACKAGE_NOT_EXISTS); + } + if (tenantPackage.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { + throw exception(TENANT_PACKAGE_DISABLE, tenantPackage.getName()); + } + return tenantPackage; + } + + @Override + public List getTenantPackageListByStatus(Integer status) { + return tenantPackageMapper.selectListByStatus(status); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantService.java new file mode 100644 index 00000000..0c19d34e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantService.java @@ -0,0 +1,130 @@ +package com.chanko.yunxi.mes.heli.module.system.service.tenant; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantDO; +import com.chanko.yunxi.mes.heli.module.system.service.tenant.handler.TenantInfoHandler; +import com.chanko.yunxi.mes.heli.module.system.service.tenant.handler.TenantMenuHandler; + +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +/** + * 租户 Service 接口 + * + * @author 芋道源码 + */ +public interface TenantService { + + /** + * 创建租户 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTenant(@Valid TenantSaveReqVO createReqVO); + + /** + * 更新租户 + * + * @param updateReqVO 更新信息 + */ + void updateTenant(@Valid TenantSaveReqVO updateReqVO); + + /** + * 更新租户的角色菜单 + * + * @param tenantId 租户编号 + * @param menuIds 菜单编号数组 + */ + void updateTenantRoleMenu(Long tenantId, Set menuIds); + + /** + * 删除租户 + * + * @param id 编号 + */ + void deleteTenant(Long id); + + /** + * 获得租户 + * + * @param id 编号 + * @return 租户 + */ + TenantDO getTenant(Long id); + + /** + * 获得租户分页 + * + * @param pageReqVO 分页查询 + * @return 租户分页 + */ + PageResult getTenantPage(TenantPageReqVO pageReqVO); + + /** + * 获得名字对应的租户 + * + * @param name 租户名 + * @return 租户 + */ + TenantDO getTenantByName(String name); + + /** + * 获得域名对应的租户 + * + * @param website 域名 + * @return 租户 + */ + TenantDO getTenantByWebsite(String website); + + /** + * 获得使用指定套餐的租户数量 + * + * @param packageId 租户套餐编号 + * @return 租户数量 + */ + Long getTenantCountByPackageId(Long packageId); + + /** + * 获得使用指定套餐的租户数组 + * + * @param packageId 租户套餐编号 + * @return 租户数组 + */ + List getTenantListByPackageId(Long packageId); + + /** + * 进行租户的信息处理逻辑 + * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 + * + * @param handler 处理器 + */ + void handleTenantInfo(TenantInfoHandler handler); + + /** + * 进行租户的菜单处理逻辑 + * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 + * + * @param handler 处理器 + */ + void handleTenantMenu(TenantMenuHandler handler); + + /** + * 获得所有租户 + * + * @return 租户编号数组 + */ + List getTenantIdList(); + + /** + * 校验租户是否合法 + * + * @param id 租户编号 + */ + void validTenant(Long id); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantServiceImpl.java new file mode 100644 index 00000000..7caa860e --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/TenantServiceImpl.java @@ -0,0 +1,306 @@ +package com.chanko.yunxi.mes.heli.module.system.service.tenant; + +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 com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.date.DateUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.tenant.config.TenantProperties; +import com.chanko.yunxi.mes.heli.framework.tenant.core.context.TenantContextHolder; +import com.chanko.yunxi.mes.heli.framework.tenant.core.util.TenantUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.convert.tenant.TenantConvert; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.MenuDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.permission.RoleDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantPackageDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.tenant.TenantMapper; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.RoleCodeEnum; +import com.chanko.yunxi.mes.heli.module.system.enums.permission.RoleTypeEnum; +import com.chanko.yunxi.mes.heli.module.system.service.permission.MenuService; +import com.chanko.yunxi.mes.heli.module.system.service.permission.PermissionService; +import com.chanko.yunxi.mes.heli.module.system.service.permission.RoleService; +import com.chanko.yunxi.mes.heli.module.system.service.tenant.handler.TenantInfoHandler; +import com.chanko.yunxi.mes.heli.module.system.service.tenant.handler.TenantMenuHandler; +import com.chanko.yunxi.mes.heli.module.system.service.user.AdminUserService; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singleton; + +/** + * 租户 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class TenantServiceImpl implements TenantService { + + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired(required = false) // 由于 mes.tenant.enable 配置项,可以关闭多租户的功能,所以这里只能不强制注入 + private TenantProperties tenantProperties; + + @Resource + private TenantMapper tenantMapper; + + @Resource + private TenantPackageService tenantPackageService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private AdminUserService userService; + @Resource + private RoleService roleService; + @Resource + private MenuService menuService; + @Resource + private PermissionService permissionService; + + @Override + public List getTenantIdList() { + List tenants = tenantMapper.selectList(); + return CollectionUtils.convertList(tenants, TenantDO::getId); + } + + @Override + public void validTenant(Long id) { + TenantDO tenant = getTenant(id); + if (tenant == null) { + throw exception(TENANT_NOT_EXISTS); + } + if (tenant.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { + throw exception(TENANT_DISABLE, tenant.getName()); + } + if (DateUtils.isExpired(tenant.getExpireTime())) { + throw exception(TENANT_EXPIRE, tenant.getName()); + } + } + + @Override + @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 + public Long createTenant(TenantSaveReqVO createReqVO) { + // 校验租户名称是否重复 + validTenantNameDuplicate(createReqVO.getName(), null); + // 校验租户域名是否重复 + validTenantWebsiteDuplicate(createReqVO.getWebsite(), null); + // 校验套餐被禁用 + TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId()); + + // 创建租户 + TenantDO tenant = BeanUtils.toBean(createReqVO, TenantDO.class); + tenantMapper.insert(tenant); + // 创建租户的管理员 + TenantUtils.execute(tenant.getId(), () -> { + // 创建角色 + Long roleId = createRole(tenantPackage); + // 创建用户,并分配角色 + Long userId = createUser(roleId, createReqVO); + // 修改租户的管理员 + tenantMapper.updateById(new TenantDO().setId(tenant.getId()).setContactUserId(userId)); + }); + return tenant.getId(); + } + + private Long createUser(Long roleId, TenantSaveReqVO createReqVO) { + // 创建用户 + Long userId = userService.createUser(TenantConvert.INSTANCE.convert02(createReqVO)); + // 分配角色 + permissionService.assignUserRole(userId, singleton(roleId)); + return userId; + } + + private Long createRole(TenantPackageDO tenantPackage) { + // 创建角色 + RoleSaveReqVO reqVO = new RoleSaveReqVO(); + reqVO.setName(RoleCodeEnum.TENANT_ADMIN.getName()).setCode(RoleCodeEnum.TENANT_ADMIN.getCode()) + .setSort(0).setRemark("系统自动生成"); + Long roleId = roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType()); + // 分配权限 + permissionService.assignRoleMenu(roleId, tenantPackage.getMenuIds()); + return roleId; + } + + @Override + @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 + public void updateTenant(TenantSaveReqVO updateReqVO) { + // 校验存在 + TenantDO tenant = validateUpdateTenant(updateReqVO.getId()); + // 校验租户名称是否重复 + validTenantNameDuplicate(updateReqVO.getName(), updateReqVO.getId()); + // 校验租户域名是否重复 + validTenantWebsiteDuplicate(updateReqVO.getWebsite(), updateReqVO.getId()); + // 校验套餐被禁用 + TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId()); + + // 更新租户 + TenantDO updateObj = BeanUtils.toBean(updateReqVO, TenantDO.class); + tenantMapper.updateById(updateObj); + // 如果套餐发生变化,则修改其角色的权限 + if (ObjectUtil.notEqual(tenant.getPackageId(), updateReqVO.getPackageId())) { + updateTenantRoleMenu(tenant.getId(), tenantPackage.getMenuIds()); + } + } + + private void validTenantNameDuplicate(String name, Long id) { + TenantDO tenant = tenantMapper.selectByName(name); + if (tenant == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同名字的租户 + if (id == null) { + throw exception(TENANT_NAME_DUPLICATE, name); + } + if (!tenant.getId().equals(id)) { + throw exception(TENANT_NAME_DUPLICATE, name); + } + } + + private void validTenantWebsiteDuplicate(String website, Long id) { + if (StrUtil.isEmpty(website)) { + return; + } + TenantDO tenant = tenantMapper.selectByWebsite(website); + if (tenant == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同名字的租户 + if (id == null) { + throw exception(TENANT_WEBSITE_DUPLICATE, website); + } + if (!tenant.getId().equals(id)) { + throw exception(TENANT_WEBSITE_DUPLICATE, website); + } + } + + @Override + @DSTransactional + public void updateTenantRoleMenu(Long tenantId, Set menuIds) { + TenantUtils.execute(tenantId, () -> { + // 获得所有角色 + List roles = roleService.getRoleList(); + roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配", + role.getId(), role.getTenantId(), tenantId)); // 兜底校验 + // 重新分配每个角色的权限 + roles.forEach(role -> { + // 如果是租户管理员,重新分配其权限为租户套餐的权限 + if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) { + permissionService.assignRoleMenu(role.getId(), menuIds); + log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds); + return; + } + // 如果是其他角色,则去掉超过套餐的权限 + Set roleMenuIds = permissionService.getRoleMenuListByRoleId(role.getId()); + roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds); + permissionService.assignRoleMenu(role.getId(), roleMenuIds); + log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds); + }); + }); + } + + @Override + public void deleteTenant(Long id) { + // 校验存在 + validateUpdateTenant(id); + // 删除 + tenantMapper.deleteById(id); + } + + private TenantDO validateUpdateTenant(Long id) { + TenantDO tenant = tenantMapper.selectById(id); + if (tenant == null) { + throw exception(TENANT_NOT_EXISTS); + } + // 内置租户,不允许删除 + if (isSystemTenant(tenant)) { + throw exception(TENANT_CAN_NOT_UPDATE_SYSTEM); + } + return tenant; + } + + @Override + public TenantDO getTenant(Long id) { + return tenantMapper.selectById(id); + } + + @Override + public PageResult getTenantPage(TenantPageReqVO pageReqVO) { + return tenantMapper.selectPage(pageReqVO); + } + + @Override + public TenantDO getTenantByName(String name) { + return tenantMapper.selectByName(name); + } + + @Override + public TenantDO getTenantByWebsite(String website) { + return tenantMapper.selectByWebsite(website); + } + + @Override + public Long getTenantCountByPackageId(Long packageId) { + return tenantMapper.selectCountByPackageId(packageId); + } + + @Override + public List getTenantListByPackageId(Long packageId) { + return tenantMapper.selectListByPackageId(packageId); + } + + @Override + public void handleTenantInfo(TenantInfoHandler handler) { + // 如果禁用,则不执行逻辑 + if (isTenantDisable()) { + return; + } + // 获得租户 + TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); + // 执行处理器 + handler.handle(tenant); + } + + @Override + public void handleTenantMenu(TenantMenuHandler handler) { + // 如果禁用,则不执行逻辑 + if (isTenantDisable()) { + return; + } + // 获得租户,然后获得菜单 + TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); + Set menuIds; + if (isSystemTenant(tenant)) { // 系统租户,菜单是全量的 + menuIds = CollectionUtils.convertSet(menuService.getMenuList(), MenuDO::getId); + } else { + menuIds = tenantPackageService.getTenantPackage(tenant.getPackageId()).getMenuIds(); + } + // 执行处理器 + handler.handle(menuIds); + } + + private static boolean isSystemTenant(TenantDO tenant) { + return Objects.equals(tenant.getPackageId(), TenantDO.PACKAGE_ID_SYSTEM); + } + + private boolean isTenantDisable() { + return tenantProperties == null || Boolean.FALSE.equals(tenantProperties.getEnable()); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/handler/TenantInfoHandler.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/handler/TenantInfoHandler.java new file mode 100644 index 00000000..d047f508 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/handler/TenantInfoHandler.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.service.tenant.handler; + +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.tenant.TenantDO; + +/** + * 租户信息处理 + * 目的:尽量减少租户逻辑耦合到系统中 + * + * @author 芋道源码 + */ +public interface TenantInfoHandler { + + /** + * 基于传入的租户信息,进行相关逻辑的执行 + * 例如说,创建用户时,超过最大账户配额 + * + * @param tenant 租户信息 + */ + void handle(TenantDO tenant); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/handler/TenantMenuHandler.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/handler/TenantMenuHandler.java new file mode 100644 index 00000000..39538506 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/tenant/handler/TenantMenuHandler.java @@ -0,0 +1,21 @@ +package com.chanko.yunxi.mes.heli.module.system.service.tenant.handler; + +import java.util.Set; + +/** + * 租户菜单处理 + * 目的:尽量减少租户逻辑耦合到系统中 + * + * @author 芋道源码 + */ +public interface TenantMenuHandler { + + /** + * 基于传入的租户菜单【全】列表,进行相关逻辑的执行 + * 例如说,返回可分配菜单的时候,可以移除多余的 + * + * @param menuIds 菜单列表 + */ + void handle(Set menuIds); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/user/AdminUserService.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/user/AdminUserService.java new file mode 100644 index 00000000..beb9867d --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/user/AdminUserService.java @@ -0,0 +1,204 @@ +package com.chanko.yunxi.mes.heli.module.system.service.user; + +import cn.hutool.core.collection.CollUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.*; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; + +import javax.validation.Valid; +import java.io.InputStream; +import java.util.*; + +/** + * 后台用户 Service 接口 + * + * @author 芋道源码 + */ +public interface AdminUserService { + + /** + * 创建用户 + * + * @param createReqVO 用户信息 + * @return 用户编号 + */ + Long createUser(@Valid UserSaveReqVO createReqVO); + + /** + * 修改用户 + * + * @param updateReqVO 用户信息 + */ + void updateUser(@Valid UserSaveReqVO updateReqVO); + + /** + * 更新用户的最后登陆信息 + * + * @param id 用户编号 + * @param loginIp 登陆 IP + */ + void updateUserLogin(Long id, String loginIp); + + /** + * 修改用户个人信息 + * + * @param id 用户编号 + * @param reqVO 用户个人信息 + */ + void updateUserProfile(Long id, @Valid UserProfileUpdateReqVO reqVO); + + /** + * 修改用户个人密码 + * + * @param id 用户编号 + * @param reqVO 更新用户个人密码 + */ + void updateUserPassword(Long id, @Valid UserProfileUpdatePasswordReqVO reqVO); + + /** + * 更新用户头像 + * + * @param id 用户 id + * @param avatarFile 头像文件 + */ + String updateUserAvatar(Long id, InputStream avatarFile) throws Exception; + + /** + * 修改密码 + * + * @param id 用户编号 + * @param password 密码 + */ + void updateUserPassword(Long id, String password); + + /** + * 修改状态 + * + * @param id 用户编号 + * @param status 状态 + */ + void updateUserStatus(Long id, Integer status); + + /** + * 删除用户 + * + * @param id 用户编号 + */ + void deleteUser(Long id); + + /** + * 通过用户名查询用户 + * + * @param username 用户名 + * @return 用户对象信息 + */ + AdminUserDO getUserByUsername(String username); + + /** + * 通过手机号获取用户 + * + * @param mobile 手机号 + * @return 用户对象信息 + */ + AdminUserDO getUserByMobile(String mobile); + + /** + * 获得用户分页列表 + * + * @param reqVO 分页条件 + * @return 分页列表 + */ + PageResult getUserPage(UserPageReqVO reqVO); + + /** + * 通过用户 ID 查询用户 + * + * @param id 用户ID + * @return 用户对象信息 + */ + AdminUserDO getUser(Long id); + + /** + * 获得指定部门的用户数组 + * + * @param deptIds 部门数组 + * @return 用户数组 + */ + List getUserListByDeptIds(Collection deptIds); + + /** + * 获得指定岗位的用户数组 + * + * @param postIds 岗位数组 + * @return 用户数组 + */ + List getUserListByPostIds(Collection postIds); + + /** + * 获得用户列表 + * + * @param ids 用户编号数组 + * @return 用户列表 + */ + List getUserList(Collection ids); + + /** + * 校验用户们是否有效。如下情况,视为无效: + * 1. 用户编号不存在 + * 2. 用户被禁用 + * + * @param ids 用户编号数组 + */ + void validateUserList(Collection ids); + + /** + * 获得用户 Map + * + * @param ids 用户编号数组 + * @return 用户 Map + */ + default Map getUserMap(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return new HashMap<>(); + } + return CollectionUtils.convertMap(getUserList(ids), AdminUserDO::getId); + } + + /** + * 获得用户列表,基于昵称模糊匹配 + * + * @param nickname 昵称 + * @return 用户列表 + */ + List getUserListByNickname(String nickname); + + /** + * 批量导入用户 + * + * @param importUsers 导入用户列表 + * @param isUpdateSupport 是否支持更新 + * @return 导入结果 + */ + UserImportRespVO importUserList(List importUsers, boolean isUpdateSupport); + + /** + * 获得指定状态的用户们 + * + * @param status 状态 + * @return 用户们 + */ + List getUserListByStatus(Integer status); + + /** + * 判断密码是否匹配 + * + * @param rawPassword 未加密的密码 + * @param encodedPassword 加密后的密码 + * @return 是否匹配 + */ + boolean isPasswordMatch(String rawPassword, String encodedPassword); + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/user/AdminUserServiceImpl.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/user/AdminUserServiceImpl.java new file mode 100644 index 00000000..29c034de --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/service/user/AdminUserServiceImpl.java @@ -0,0 +1,455 @@ +package com.chanko.yunxi.mes.heli.module.system.service.user; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.enums.CommonStatusEnum; +import com.chanko.yunxi.mes.heli.framework.common.exception.ServiceException; +import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils; +import com.chanko.yunxi.mes.heli.framework.common.util.object.BeanUtils; +import com.chanko.yunxi.mes.heli.framework.datapermission.core.util.DataPermissionUtils; +import com.chanko.yunxi.mes.heli.module.infra.api.file.FileApi; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.UserImportExcelVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.UserImportRespVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.UserPageReqVO; +import com.chanko.yunxi.mes.heli.module.system.controller.admin.user.vo.user.UserSaveReqVO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.DeptDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.dept.UserPostDO; +import com.chanko.yunxi.mes.heli.module.system.dal.dataobject.user.AdminUserDO; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.dept.UserPostMapper; +import com.chanko.yunxi.mes.heli.module.system.dal.mysql.user.AdminUserMapper; +import com.chanko.yunxi.mes.heli.module.system.service.dept.DeptService; +import com.chanko.yunxi.mes.heli.module.system.service.dept.PostService; +import com.chanko.yunxi.mes.heli.module.system.service.permission.PermissionService; +import com.chanko.yunxi.mes.heli.module.system.service.tenant.TenantService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.io.InputStream; +import java.time.LocalDateTime; +import java.util.*; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertList; +import static com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils.convertSet; +import static com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants.*; + +/** + * 后台用户 Service 实现类 + * + * @author 芋道源码 + */ +@Service("adminUserService") +@Slf4j +public class AdminUserServiceImpl implements AdminUserService { + + @Value("${sys.user.init-password:mesyuanma}") + private String userInitPassword; + + @Resource + private AdminUserMapper userMapper; + + @Resource + private DeptService deptService; + @Resource + private PostService postService; + @Resource + private PermissionService permissionService; + @Resource + private PasswordEncoder passwordEncoder; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private TenantService tenantService; + + @Resource + private UserPostMapper userPostMapper; + + @Resource + private FileApi fileApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createUser(UserSaveReqVO createReqVO) { + // 校验账户配合 + tenantService.handleTenantInfo(tenant -> { + long count = userMapper.selectCount(); + if (count >= tenant.getAccountCount()) { + throw exception(USER_COUNT_MAX, tenant.getAccountCount()); + } + }); + // 校验正确性 + validateUserForCreateOrUpdate(null, createReqVO.getUsername(), + createReqVO.getMobile(), createReqVO.getEmail(), createReqVO.getDeptId(), createReqVO.getPostIds()); + // 插入用户 + AdminUserDO user = BeanUtils.toBean(createReqVO, AdminUserDO.class); + user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 + user.setPassword(encodePassword(createReqVO.getPassword())); // 加密密码 + userMapper.insert(user); + // 插入关联岗位 + if (CollectionUtil.isNotEmpty(user.getPostIds())) { + userPostMapper.insertBatch(convertList(user.getPostIds(), + postId -> new UserPostDO().setUserId(user.getId()).setPostId(postId))); + } + return user.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUser(UserSaveReqVO updateReqVO) { + updateReqVO.setPassword(null); // 特殊:此处不更新密码 + // 校验正确性 + validateUserForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getUsername(), + updateReqVO.getMobile(), updateReqVO.getEmail(), updateReqVO.getDeptId(), updateReqVO.getPostIds()); + // 更新用户 + AdminUserDO updateObj = BeanUtils.toBean(updateReqVO, AdminUserDO.class); + userMapper.updateById(updateObj); + // 更新岗位 + updateUserPost(updateReqVO, updateObj); + } + + private void updateUserPost(UserSaveReqVO reqVO, AdminUserDO updateObj) { + Long userId = reqVO.getId(); + Set dbPostIds = convertSet(userPostMapper.selectListByUserId(userId), UserPostDO::getPostId); + // 计算新增和删除的岗位编号 + Set postIds = CollUtil.emptyIfNull(updateObj.getPostIds()); + Collection createPostIds = CollUtil.subtract(postIds, dbPostIds); + Collection deletePostIds = CollUtil.subtract(dbPostIds, postIds); + // 执行新增和删除。对于已经授权的菜单,不用做任何处理 + if (!CollectionUtil.isEmpty(createPostIds)) { + userPostMapper.insertBatch(convertList(createPostIds, + postId -> new UserPostDO().setUserId(userId).setPostId(postId))); + } + if (!CollectionUtil.isEmpty(deletePostIds)) { + userPostMapper.deleteByUserIdAndPostId(userId, deletePostIds); + } + } + + @Override + public void updateUserLogin(Long id, String loginIp) { + userMapper.updateById(new AdminUserDO().setId(id).setLoginIp(loginIp).setLoginDate(LocalDateTime.now())); + } + + @Override + public void updateUserProfile(Long id, UserProfileUpdateReqVO reqVO) { + // 校验正确性 + validateUserExists(id); + validateEmailUnique(id, reqVO.getEmail()); + validateMobileUnique(id, reqVO.getMobile()); + // 执行更新 + userMapper.updateById(BeanUtils.toBean(reqVO, AdminUserDO.class).setId(id)); + } + + @Override + public void updateUserPassword(Long id, UserProfileUpdatePasswordReqVO reqVO) { + // 校验旧密码密码 + validateOldPassword(id, reqVO.getOldPassword()); + // 执行更新 + AdminUserDO updateObj = new AdminUserDO().setId(id); + updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码 + userMapper.updateById(updateObj); + } + + @Override + public String updateUserAvatar(Long id, InputStream avatarFile) { + validateUserExists(id); + // 存储文件 + String avatar = fileApi.createFile(IoUtil.readBytes(avatarFile)); + // 更新路径 + AdminUserDO sysUserDO = new AdminUserDO(); + sysUserDO.setId(id); + sysUserDO.setAvatar(avatar); + userMapper.updateById(sysUserDO); + return avatar; + } + + @Override + public void updateUserPassword(Long id, String password) { + // 校验用户存在 + validateUserExists(id); + // 更新密码 + AdminUserDO updateObj = new AdminUserDO(); + updateObj.setId(id); + updateObj.setPassword(encodePassword(password)); // 加密密码 + userMapper.updateById(updateObj); + } + + @Override + public void updateUserStatus(Long id, Integer status) { + // 校验用户存在 + validateUserExists(id); + // 更新状态 + AdminUserDO updateObj = new AdminUserDO(); + updateObj.setId(id); + updateObj.setStatus(status); + userMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteUser(Long id) { + // 校验用户存在 + validateUserExists(id); + // 删除用户 + userMapper.deleteById(id); + // 删除用户关联数据 + permissionService.processUserDeleted(id); + // 删除用户岗位 + userPostMapper.deleteByUserId(id); + } + + @Override + public AdminUserDO getUserByUsername(String username) { + return userMapper.selectByUsername(username); + } + + @Override + public AdminUserDO getUserByMobile(String mobile) { + return userMapper.selectByMobile(mobile); + } + + @Override + public PageResult getUserPage(UserPageReqVO reqVO) { + return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId())); + } + + @Override + public AdminUserDO getUser(Long id) { + return userMapper.selectById(id); + } + + @Override + public List getUserListByDeptIds(Collection deptIds) { + if (CollUtil.isEmpty(deptIds)) { + return Collections.emptyList(); + } + return userMapper.selectListByDeptIds(deptIds); + } + + @Override + public List getUserListByPostIds(Collection postIds) { + if (CollUtil.isEmpty(postIds)) { + return Collections.emptyList(); + } + Set userIds = convertSet(userPostMapper.selectListByPostIds(postIds), UserPostDO::getUserId); + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + return userMapper.selectBatchIds(userIds); + } + + @Override + public List getUserList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return userMapper.selectBatchIds(ids); + } + + @Override + public void validateUserList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得岗位信息 + List users = userMapper.selectBatchIds(ids); + Map userMap = CollectionUtils.convertMap(users, AdminUserDO::getId); + // 校验 + ids.forEach(id -> { + AdminUserDO user = userMap.get(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus())) { + throw exception(USER_IS_DISABLE, user.getNickname()); + } + }); + } + + @Override + public List getUserListByNickname(String nickname) { + return userMapper.selectListByNickname(nickname); + } + + /** + * 获得部门条件:查询指定部门的子部门编号们,包括自身 + * @param deptId 部门编号 + * @return 部门编号集合 + */ + private Set getDeptCondition(Long deptId) { + if (deptId == null) { + return Collections.emptySet(); + } + Set deptIds = convertSet(deptService.getChildDeptList(deptId), DeptDO::getId); + deptIds.add(deptId); // 包括自身 + return deptIds; + } + + private void validateUserForCreateOrUpdate(Long id, String username, String mobile, String email, + Long deptId, Set postIds) { + // 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确 + DataPermissionUtils.executeIgnore(() -> { + // 校验用户存在 + validateUserExists(id); + // 校验用户名唯一 + validateUsernameUnique(id, username); + // 校验手机号唯一 + validateMobileUnique(id, mobile); + // 校验邮箱唯一 + validateEmailUnique(id, email); + // 校验部门处于开启状态 + deptService.validateDeptList(CollectionUtils.singleton(deptId)); + // 校验岗位处于开启状态 + postService.validatePostList(postIds); + }); + } + + @VisibleForTesting + void validateUserExists(Long id) { + if (id == null) { + return; + } + AdminUserDO user = userMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + } + + @VisibleForTesting + void validateUsernameUnique(Long id, String username) { + if (StrUtil.isBlank(username)) { + return; + } + AdminUserDO user = userMapper.selectByUsername(username); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_USERNAME_EXISTS); + } + if (!user.getId().equals(id)) { + throw exception(USER_USERNAME_EXISTS); + } + } + + @VisibleForTesting + void validateEmailUnique(Long id, String email) { + if (StrUtil.isBlank(email)) { + return; + } + AdminUserDO user = userMapper.selectByEmail(email); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_EMAIL_EXISTS); + } + if (!user.getId().equals(id)) { + throw exception(USER_EMAIL_EXISTS); + } + } + + @VisibleForTesting + void validateMobileUnique(Long id, String mobile) { + if (StrUtil.isBlank(mobile)) { + return; + } + AdminUserDO user = userMapper.selectByMobile(mobile); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_MOBILE_EXISTS); + } + if (!user.getId().equals(id)) { + throw exception(USER_MOBILE_EXISTS); + } + } + + /** + * 校验旧密码 + * @param id 用户 id + * @param oldPassword 旧密码 + */ + @VisibleForTesting + void validateOldPassword(Long id, String oldPassword) { + AdminUserDO user = userMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + if (!isPasswordMatch(oldPassword, user.getPassword())) { + throw exception(USER_PASSWORD_FAILED); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 + public UserImportRespVO importUserList(List importUsers, boolean isUpdateSupport) { + if (CollUtil.isEmpty(importUsers)) { + throw exception(USER_IMPORT_LIST_IS_EMPTY); + } + UserImportRespVO respVO = UserImportRespVO.builder().createUsernames(new ArrayList<>()) + .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build(); + importUsers.forEach(importUser -> { + // 校验,判断是否有不符合的原因 + try { + validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(), + importUser.getDeptId(), null); + } catch (ServiceException ex) { + respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage()); + return; + } + // 判断如果不存在,在进行插入 + AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername()); + if (existUser == null) { + userMapper.insert(BeanUtils.toBean(importUser, AdminUserDO.class) + .setPassword(encodePassword(userInitPassword)).setPostIds(new HashSet<>())); // 设置默认密码及空岗位编号数组 + respVO.getCreateUsernames().add(importUser.getUsername()); + return; + } + // 如果存在,判断是否允许更新 + if (!isUpdateSupport) { + respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg()); + return; + } + AdminUserDO updateUser = BeanUtils.toBean(importUser, AdminUserDO.class); + updateUser.setId(existUser.getId()); + userMapper.updateById(updateUser); + respVO.getUpdateUsernames().add(importUser.getUsername()); + }); + return respVO; + } + + @Override + public List getUserListByStatus(Integer status) { + return userMapper.selectListByStatus(status); + } + + @Override + public boolean isPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 对密码进行加密 + * + * @param password 密码 + * @return 加密后的密码 + */ + private String encodePassword(String password) { + return passwordEncoder.encode(password); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/util/collection/SimpleTrie.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/util/collection/SimpleTrie.java new file mode 100644 index 00000000..a173c19a --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/util/collection/SimpleTrie.java @@ -0,0 +1,152 @@ +package com.chanko.yunxi.mes.heli.module.system.util.collection; + +import cn.hutool.core.collection.CollUtil; + +import java.util.*; + +/** + * 基于前缀树,实现敏感词的校验 + *

+ * 相比 Apache Common 提供的 PatriciaTrie 来说,性能可能会更加好一些。 + * + * @author 芋道源码 + */ +@SuppressWarnings("unchecked") +public class SimpleTrie { + + /** + * 一个敏感词结束后对应的 key + */ + private static final Character CHARACTER_END = '\0'; + + /** + * 使用敏感词,构建的前缀树 + */ + private final Map children; + + /** + * 基于字符串,构建前缀树 + * + * @param strs 字符串数组 + */ + public SimpleTrie(Collection strs) { + // 排序,优先使用较短的前缀 + strs = CollUtil.sort(strs, String::compareTo); + // 构建树 + children = new HashMap<>(); + for (String str : strs) { + Map child = children; + // 遍历每个字符 + for (Character c : str.toCharArray()) { + // 如果已经到达结束,就没必要在添加更长的敏感词。 + // 例如说,有两个敏感词是:吃饭啊、吃饭。输入一句话是 “我要吃饭啊”,则只要匹配到 “吃饭” 这个敏感词即可。 + if (child.containsKey(CHARACTER_END)) { + break; + } + if (!child.containsKey(c)) { + child.put(c, new HashMap<>()); + } + child = (Map) child.get(c); + } + // 结束 + child.put(CHARACTER_END, null); + } + } + + /** + * 验证文本是否合法,即不包含敏感词 + * + * @param text 文本 + * @return 是否 true-合法 false-不合法 + */ + public boolean isValid(String text) { + // 遍历 text,使用每一个 [i, n) 段的字符串,使用 children 前缀树匹配,是否包含敏感词 + for (int i = 0; i < text.length(); i++) { + Map child = (Map) children.get(text.charAt(i)); + if (child == null) { + continue; + } + boolean ok = recursion(text, i + 1, child); + if (!ok) { + return false; + } + } + return true; + } + + /** + * 验证文本从指定位置开始,是否不包含某个敏感词 + * + * @param text 文本 + * @param index 开始位置 + * @param child 节点(当前遍历到的) + * @return 是否不包含 true-不包含 false-包含 + */ + private boolean recursion(String text, int index, Map child) { + if (child.containsKey(CHARACTER_END)) { + return false; + } + if (index == text.length()) { + return true; + } + child = (Map) child.get(text.charAt(index)); + return child == null || !child.containsKey(CHARACTER_END) && recursion(text, ++index, child); + } + + /** + * 获得文本所包含的不合法的敏感词 + * + * 注意,才当即最短匹配原则。例如说:当敏感词存在 “煞笔”,“煞笔二货 ”时,只会返回 “煞笔”。 + * + * @param text 文本 + * @return 匹配的敏感词 + */ + public List validate(String text) { + Set results = new HashSet<>(); + for (int i = 0; i < text.length(); i++) { + Character c = text.charAt(i); + Map child = (Map) children.get(c); + if (child == null) { + continue; + } + StringBuilder result = new StringBuilder().append(c); + boolean ok = recursionWithResult(text, i + 1, child, result); + if (!ok) { + results.add(result.toString()); + } + } + return new ArrayList<>(results); + } + + /** + * 返回文本从 index 开始的敏感词,并使用 StringBuilder 参数进行返回 + * + * 逻辑和 {@link #recursion(String, int, Map)} 是一致,只是多了 result 返回结果 + * + * @param text 文本 + * @param index 开始未知 + * @param child 节点(当前遍历到的) + * @param result 返回敏感词 + * @return 是否有敏感词 + */ + @SuppressWarnings("unchecked") + private static boolean recursionWithResult(String text, int index, Map child, StringBuilder result) { + if (child.containsKey(CHARACTER_END)) { + return false; + } + if (index == text.length()) { + return true; + } + Character c = text.charAt(index); + child = (Map) child.get(c); + if (child == null) { + return true; + } + if (child.containsKey(CHARACTER_END)) { + result.append(c); + return false; + } + return recursionWithResult(text, ++index, child, result.append(c)); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/util/oauth2/OAuth2Utils.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/util/oauth2/OAuth2Utils.java new file mode 100644 index 00000000..96fd1c32 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/util/oauth2/OAuth2Utils.java @@ -0,0 +1,103 @@ +package com.chanko.yunxi.mes.heli.module.system.util.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.http.HttpUtils; +import com.chanko.yunxi.mes.heli.framework.security.core.util.SecurityFrameworkUtils; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; + +/** + * OAuth2 相关的工具类 + * + * @author 芋道源码 + */ +public class OAuth2Utils { + + /** + * 构建授权码模式下,重定向的 URI + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 getSuccessfulRedirect 方法 + * + * @param redirectUri 重定向 URI + * @param authorizationCode 授权码 + * @param state 状态 + * @return 授权码模式下的重定向 URI + */ + public static String buildAuthorizationCodeRedirectUri(String redirectUri, String authorizationCode, String state) { + Map query = new LinkedHashMap<>(); + query.put("code", authorizationCode); + if (state != null) { + query.put("state", state); + } + return HttpUtils.append(redirectUri, query, null, false); + } + + /** + * 构建简化模式下,重定向的 URI + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 appendAccessToken 方法 + * + * @param redirectUri 重定向 URI + * @param accessToken 访问令牌 + * @param state 状态 + * @param expireTime 过期时间 + * @param scopes 授权范围 + * @param additionalInformation 附加信息 + * @return 简化授权模式下的重定向 URI + */ + public static String buildImplicitRedirectUri(String redirectUri, String accessToken, String state, LocalDateTime expireTime, + Collection scopes, Map additionalInformation) { + Map vars = new LinkedHashMap(); + Map keys = new HashMap(); + vars.put("access_token", accessToken); + vars.put("token_type", SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); + if (state != null) { + vars.put("state", state); + } + if (expireTime != null) { + vars.put("expires_in", getExpiresIn(expireTime)); + } + if (CollUtil.isNotEmpty(scopes)) { + vars.put("scope", buildScopeStr(scopes)); + } + if (CollUtil.isNotEmpty(additionalInformation)) { + for (String key : additionalInformation.keySet()) { + Object value = additionalInformation.get(key); + if (value != null) { + keys.put("extra_" + key, key); + vars.put("extra_" + key, value); + } + } + } + // Do not include the refresh token (even if there is one) + return HttpUtils.append(redirectUri, vars, keys, true); + } + + public static String buildUnsuccessfulRedirect(String redirectUri, String responseType, String state, + String error, String description) { + Map query = new LinkedHashMap(); + query.put("error", error); + query.put("error_description", description); + if (state != null) { + query.put("state", state); + } + return HttpUtils.append(redirectUri, query, null, !responseType.contains("code")); + } + + public static long getExpiresIn(LocalDateTime expireTime) { + return LocalDateTimeUtil.between(LocalDateTime.now(), expireTime, ChronoUnit.SECONDS); + } + + public static String buildScopeStr(Collection scopes) { + return CollUtil.join(scopes, " "); + } + + public static List buildScopes(String scope) { + return StrUtil.split(scope, ' '); + } + +} diff --git a/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/util/package-info.java b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/util/package-info.java new file mode 100644 index 00000000..5d2b3c73 --- /dev/null +++ b/mes-module-system/mes-module-system-biz/src/main/java/com/chanko/yunxi/mes/heli/module/system/util/package-info.java @@ -0,0 +1,4 @@ +/** + * 每个模块的 util 包,放专属当前模块的 Utils 工具类 + */ +package com.chanko.yunxi.mes.heli.module.system.util; diff --git a/mes-module-system/pom.xml b/mes-module-system/pom.xml new file mode 100644 index 00000000..2976da78 --- /dev/null +++ b/mes-module-system/pom.xml @@ -0,0 +1,24 @@ + + + + com.chanko.yunxi + mes + ${revision} + + 4.0.0 + + mes-module-system-api + mes-module-system-biz + + mes-module-system + pom + + ${project.artifactId} + + system 模块下,我们放通用业务,支撑上层的核心业务。 + 例如说:用户、部门、权限、数据字典等等 + + + diff --git a/mes-server/Dockerfile b/mes-server/Dockerfile new file mode 100644 index 00000000..7a101870 --- /dev/null +++ b/mes-server/Dockerfile @@ -0,0 +1,23 @@ +## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性 +## 感谢复旦核博士的建议!灰子哥,牛皮! +FROM eclipse-temurin:8-jre + +## 创建目录,并使用它作为工作目录 +RUN mkdir -p /mes-server +WORKDIR /mes-server +## 将后端项目的 Jar 文件,复制到镜像中 +COPY ./target/mes-server.jar app.jar + +## 设置 TZ 时区 +ENV TZ=Asia/Shanghai +## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖 +ENV JAVA_OPTS="-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom" + +## 应用参数 +ENV ARGS="" + +## 暴露后端项目的 48080 端口 +EXPOSE 48080 + +## 启动后端项目 +CMD java ${JAVA_OPTS} -jar app.jar $ARGS diff --git a/mes-server/pom.xml b/mes-server/pom.xml new file mode 100644 index 00000000..5b247fa0 --- /dev/null +++ b/mes-server/pom.xml @@ -0,0 +1,140 @@ + + + + com.chanko.yunxi + mes + ${revision} + + 4.0.0 + + mes-server + jar + + ${project.artifactId} + + 后端 Server 的主项目,通过引入需要 mes-module-xxx 的依赖, + 从而实现提供 RESTful API 给 mes-ui-admin、mes-ui-user 等前端项目。 + 本质上来说,它就是个空壳(容器)! + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.chanko.yunxi + mes-module-system-biz + ${revision} + + + com.chanko.yunxi + mes-module-infra-biz + ${revision} + + + com.chanko.yunxi + mes-spring-boot-starter-biz-error-code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + com.chanko.yunxi + mes-spring-boot-starter-banner + + + + + com.chanko.yunxi + mes-spring-boot-starter-protection + + + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + + diff --git a/mes-server/src/main/java/com/chanko/yunxi/mes/heli/server/MesServerApplication.java b/mes-server/src/main/java/com/chanko/yunxi/mes/heli/server/MesServerApplication.java new file mode 100644 index 00000000..81ff0e0b --- /dev/null +++ b/mes-server/src/main/java/com/chanko/yunxi/mes/heli/server/MesServerApplication.java @@ -0,0 +1,37 @@ +package com.chanko.yunxi.mes.heli.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 项目的启动类 + * + * 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + * + * @author 芋道源码 + */ +@SuppressWarnings("SpringComponentScan") // 忽略 IDEA 无法识别 ${mes.info.base-package} +@SpringBootApplication(scanBasePackages = {"${mes.info.base-package}.server", "${mes.info.base-package}.module"}) +public class MesServerApplication { + + public static void main(String[] args) { + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + try { + SpringApplication.run(MesServerApplication.class, args); + }catch (Exception e){ + e.printStackTrace(); + } +// new SpringApplicationBuilder(MesServerApplication.class) +// .applicationStartup(new BufferingApplicationStartup(20480)) +// .run(args); + + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + } + +} diff --git a/mes-server/src/main/java/com/chanko/yunxi/mes/heli/server/controller/DefaultController.java b/mes-server/src/main/java/com/chanko/yunxi/mes/heli/server/controller/DefaultController.java new file mode 100644 index 00000000..0dd57d5c --- /dev/null +++ b/mes-server/src/main/java/com/chanko/yunxi/mes/heli/server/controller/DefaultController.java @@ -0,0 +1,50 @@ +package com.chanko.yunxi.mes.heli.server.controller; + +import com.chanko.yunxi.mes.heli.framework.common.pojo.CommonResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.chanko.yunxi.mes.heli.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; + +/** + * 默认 Controller,解决部分 module 未开启时的 404 提示。 + * 例如说,/bpm/** 路径,工作流 + * + * @author 芋道源码 + */ +@RestController +public class DefaultController { + + @RequestMapping("/admin-api/bpm/**") + public CommonResult bpm404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[工作流模块 mes-module-bpm - 已禁用][参考 https://doc.iocoder.cn/bpm/ 开启]"); + } + + @RequestMapping("/admin-api/mp/**") + public CommonResult mp404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[微信公众号 mes-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + } + + @RequestMapping(value = {"/admin-api/product/**", // 商品中心 + "/admin-api/trade/**", // 交易中心 + "/admin-api/promotion/**"}) // 营销中心 + public CommonResult mall404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[商城系统 mes-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + } + + @RequestMapping(value = {"/admin-api/report/**"}) + public CommonResult report404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[报表模块 mes-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]"); + } + + @RequestMapping(value = {"/admin-api/pay/**"}) + public CommonResult pay404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[支付模块 mes-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + } + +} diff --git a/mes-server/src/main/resources/application-dev.yaml b/mes-server/src/main/resources/application-dev.yaml new file mode 100644 index 00000000..8588ee69 --- /dev/null +++ b/mes-server/src/main/resources/application-dev.yaml @@ -0,0 +1,214 @@ +server: + port: 8080 + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: ruoyi + url: jdbc:mysql://localhost:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=CTT&allowPublicKeyRetrieval=true + driver-class-name: com.mysql.jdbc.Driver + username: root + password: imysql + # slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 + # name: ruoyi-vue-pro + # url: jdbc:mysql://localhost:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&serverTimezone=CTT&allowPublicKeyRetrieval=true + # driver-class-name: com.mysql.jdbc.Driver + # username: root + # password: 3WLiVUBEwTbvAfsh + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 222.71.165.188 # 地址 + port: 6379 # 端口 + database: 3 # 数据库索引 + password: 'qweasd,.123' # 密码,建议生产环境开启 + +--- #################### 定时任务相关配置 #################### + +# Quartz 配置项,对应 QuartzProperties 配置类 +spring: + quartz: + auto-startup: true # 测试环境,需要开启 Job + scheduler-name: schedulerName # Scheduler 名字。默认为 schedulerName + job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 + wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true + properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档 + org: + quartz: + # Scheduler 相关配置 + scheduler: + instanceName: schedulerName + instanceId: AUTO # 自动生成 instance ID + # JobStore 相关配置 + jobStore: + # JobStore 实现类。可见博客:https://blog.csdn.net/weixin_42458219/article/details/122247162 + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + isClustered: true # 是集群模式 + clusterCheckinInterval: 15000 # 集群检查频率,单位:毫秒。默认为 15000,即 15 秒 + misfireThreshold: 60000 # misfire 阀值,单位:毫秒。 + # 线程池相关配置 + threadPool: + threadCount: 25 # 线程池大小。默认为 10 。 + threadPriority: 5 # 线程优先级 + class: org.quartz.simpl.SimpleThreadPool # 线程池类型 + jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置 + initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。 + +--- #################### 消息队列相关 #################### + +# rocketmq 配置项,对应 RocketMQProperties 配置类 +rocketmq: + name-server: 127.0.0.1:9876 # RocketMQ Namesrv + +spring: + # RabbitMQ 配置项,对应 RabbitProperties 配置类 + rabbitmq: + host: 127.0.0.1 # RabbitMQ 服务的地址 + port: 5672 # RabbitMQ 服务的端口 + username: guest # RabbitMQ 服务的账号 + password: guest # RabbitMQ 服务的密码 + # Kafka 配置项,对应 KafkaProperties 配置类 + kafka: + bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 + +# Resilience4j 配置项 +resilience4j: + ratelimiter: + instances: + backendA: + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s + register-health-indicator: true # 是否注册到健康监测 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址 + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + # Spring Boot Admin Server 服务端的相关配置 + context-path: /admin # 配置 Spring + +# 日志文件配置 +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + +--- #################### 微信公众号相关配置 #################### +wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 + mp: + # 公众号配置(必填) + app-id: wx041349c6f39b268b + secret: 5abee519483bc9f8cb37ce280e814bd0 + # 存储配置,解决 AccessToken 的跨节点的共享 + config-storage: + type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 + key-prefix: wx # Redis Key 的前缀 + http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 + miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档 + appid: wx63c280fe3248a3e7 + secret: 6f270509224a7ae1296bbf1c8cb97aed + config-storage: + type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 + key-prefix: wa # Redis Key 的前缀 + http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +mes: + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + pay: + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 + demo: true # 开启演示模式 + tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc + +justauth: + enabled: true + type: + DINGTALK: # 钉钉 + client-id: dingvrnreaje3yqvzhxg + client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI + ignore-check-redirect-uri: true + WECHAT_ENTERPRISE: # 企业微信 + client-id: wwd411c69a39ad2e54 + client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw + agent-id: 1000004 + ignore-check-redirect-uri: true + cache: + type: REDIS + prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE:: + timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟 +wx: + mp: + useRedis: false + defaultContent: \u60A8\u597D\uFF0C\u6709\u4EC0\u4E48\u95EE\u9898\uFF1F + redisConfig: + host: 127.0.0.1 + port: 6379 + password: diff --git a/mes-server/src/main/resources/application-local.yaml b/mes-server/src/main/resources/application-local.yaml new file mode 100644 index 00000000..f303a937 --- /dev/null +++ b/mes-server/src/main/resources/application-local.yaml @@ -0,0 +1,214 @@ +server: + port: 8080 + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: ruoyi + url: jdbc:mysql://localhost:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + driver-class-name: com.mysql.jdbc.Driver + username: root + password: imysql + # slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 + # name: ruoyi-vue-pro + # url: jdbc:mysql://localhost:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&serverTimezone=CTT&allowPublicKeyRetrieval=true + # driver-class-name: com.mysql.jdbc.Driver + # username: root + # password: 3WLiVUBEwTbvAfsh + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 222.71.165.188 # 地址 + port: 6379 # 端口 + database: 3 # 数据库索引 + password: 'qweasd,.123' # 密码,建议生产环境开启 + +--- #################### 定时任务相关配置 #################### + +# Quartz 配置项,对应 QuartzProperties 配置类 +spring: + quartz: + auto-startup: true # 测试环境,需要开启 Job + scheduler-name: schedulerName # Scheduler 名字。默认为 schedulerName + job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 + wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true + properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档 + org: + quartz: + # Scheduler 相关配置 + scheduler: + instanceName: schedulerName + instanceId: AUTO # 自动生成 instance ID + # JobStore 相关配置 + jobStore: + # JobStore 实现类。可见博客:https://blog.csdn.net/weixin_42458219/article/details/122247162 + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + isClustered: true # 是集群模式 + clusterCheckinInterval: 15000 # 集群检查频率,单位:毫秒。默认为 15000,即 15 秒 + misfireThreshold: 60000 # misfire 阀值,单位:毫秒。 + # 线程池相关配置 + threadPool: + threadCount: 25 # 线程池大小。默认为 10 。 + threadPriority: 5 # 线程优先级 + class: org.quartz.simpl.SimpleThreadPool # 线程池类型 + jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置 + initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。 + +--- #################### 消息队列相关 #################### + +# rocketmq 配置项,对应 RocketMQProperties 配置类 +rocketmq: + name-server: 127.0.0.1:9876 # RocketMQ Namesrv + +spring: + # RabbitMQ 配置项,对应 RabbitProperties 配置类 + rabbitmq: + host: 127.0.0.1 # RabbitMQ 服务的地址 + port: 5672 # RabbitMQ 服务的端口 + username: guest # RabbitMQ 服务的账号 + password: guest # RabbitMQ 服务的密码 + # Kafka 配置项,对应 KafkaProperties 配置类 + kafka: + bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 + +# Resilience4j 配置项 +resilience4j: + ratelimiter: + instances: + backendA: + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s + register-health-indicator: true # 是否注册到健康监测 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址 + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + # Spring Boot Admin Server 服务端的相关配置 + context-path: /admin # 配置 Spring + +# 日志文件配置 +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + +--- #################### 微信公众号相关配置 #################### +wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 + mp: + # 公众号配置(必填) + app-id: wx041349c6f39b268b + secret: 5abee519483bc9f8cb37ce280e814bd0 + # 存储配置,解决 AccessToken 的跨节点的共享 + config-storage: + type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 + key-prefix: wx # Redis Key 的前缀 + http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 + miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档 + appid: wx63c280fe3248a3e7 + secret: 6f270509224a7ae1296bbf1c8cb97aed + config-storage: + type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 + key-prefix: wa # Redis Key 的前缀 + http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +mes: + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + pay: + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 + demo: true # 开启演示模式 + tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc + +justauth: + enabled: true + type: + DINGTALK: # 钉钉 + client-id: dingvrnreaje3yqvzhxg + client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI + ignore-check-redirect-uri: true + WECHAT_ENTERPRISE: # 企业微信 + client-id: wwd411c69a39ad2e54 + client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw + agent-id: 1000004 + ignore-check-redirect-uri: true + cache: + type: REDIS + prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE:: + timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟 +wx: + mp: + useRedis: false + defaultContent: \u60A8\u597D\uFF0C\u6709\u4EC0\u4E48\u95EE\u9898\uFF1F + redisConfig: + host: 127.0.0.1 + port: 6379 + password: diff --git a/mes-server/src/main/resources/application.yaml b/mes-server/src/main/resources/application.yaml new file mode 100644 index 00000000..73fcd877 --- /dev/null +++ b/mes-server/src/main/resources/application.yaml @@ -0,0 +1,271 @@ +spring: + application: + name: mes-server + + profiles: + active: local + + main: + allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 + + # Servlet 配置 + servlet: + # 文件上传相关配置项 + multipart: + max-file-size: 16MB # 单个文件大小 + max-request-size: 32MB # 设置总上传的文件大小 + mvc: + pathmatch: + matching-strategy: ANT_PATH_MATCHER # 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题,参见 SpringFoxHandlerProviderBeanPostProcessor 类 +# throw-exception-if-no-handler-found: true # 404 错误时抛出异常,方便统一处理 +# static-path-pattern: /static/** # 静态资源路径; 注意:如果不配置,则 throw-exception-if-no-handler-found 不生效!!! TODO 芋艿:不能配置,会导致 swagger 不生效 + + # Jackson 配置项 + jackson: + serialization: + write-dates-as-timestamps: true # 设置 Date 的格式,使用时间戳 + write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 + write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 + fail-on-empty-beans: false # 允许序列化无属性的 Bean + + # Cache 配置项 + cache: + type: REDIS + redis: + time-to-live: 1h # 设置过期时间为 1 小时 + +--- #################### 接口文档配置 #################### + +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui + default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档 + +knife4j: + enable: true + setting: + language: zh_cn + +# 工作流 Flowable 配置 +flowable: + # 1. false: 默认值,Flowable 启动时,对比数据库表中保存的版本,如果不匹配。将抛出异常 + # 2. true: 启动时会对数据库中所有表进行更新操作,如果表存在,不做处理,反之,自动创建表 + # 3. create_drop: 启动时自动创建表,关闭时自动删除表 + # 4. drop_create: 启动时,删除旧表,再创建新表 + database-schema-update: true # 设置为 false,可通过 https://github.com/flowable/flowable-sql 初始化 + db-history-used: true # flowable6 默认 true 生成信息表,无需手动设置 + check-process-definitions: false # 设置为 false,禁用 /resources/processes 自动部署 BPMN XML 流程 + history-level: full # full:保存历史数据的最高级别,可保存全部流程相关细节,包括流程流转各节点参数 + +# MyBatis Plus 的配置项 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + global-config: + db-config: + id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 +# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 +# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 +# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + banner: false # 关闭控制台的 Banner 打印 + type-aliases-package: ${mes.info.base-package}.module.*.dal.dataobject + encryptor: + password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成 + +mybatis-plus-join: + banner: false # 关闭控制台的 Banner 打印 + +# Spring Data Redis 配置 +spring: + data: + redis: + repositories: + enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度 +--- #################### 验证码相关配置 #################### + +aj: + captcha: + jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径 + pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径 + cache-type: redis # 缓存 local/redis... + cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存 + timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行 + type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选 + water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode + interference-options: 0 # 滑动干扰项(0/1/2) + req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false + req-get-lock-limit: 5 # 验证失败 5 次,get接口锁定 + req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔 + req-get-minute-limit: 30 # get 接口一分钟内请求数限制 + req-check-minute-limit: 60 # check 接口一分钟内请求数限制 + req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制 + +--- #################### 消息队列相关 #################### + +# rocketmq 配置项,对应 RocketMQProperties 配置类 +rocketmq: + # Producer 配置项 + producer: + group: ${spring.application.name}_PRODUCER # 生产者分组 + +spring: + # Kafka 配置项,对应 KafkaProperties 配置类 + kafka: + # Kafka Producer 配置项 + producer: + acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。 + retries: 3 # 发送失败时,重试发送的次数 + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化 + # Kafka Consumer 配置项 + consumer: + auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解 + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: '*' + # Kafka Consumer Listener 监听器配置 + listener: + missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错 + +--- #################### 芋道相关配置 #################### + +mes: + info: + version: 1.0.0 + base-package: com.chanko.yunxi.mes.heli + web: + admin-ui: + url: http://localhost:8080 # Admin 管理后台 UI 的地址 + security: + permit-all_urls: + - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 + websocket: + enable: true # websocket的开关 + path: /infra/ws # 路径 + sender-type: local # 消息发送的类型,可选值为 local、redis、rocketmq、kafka、rabbitmq + sender-rocketmq: + topic: ${spring.application.name}-websocket # 消息发送的 RocketMQ Topic + consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 RocketMQ Consumer Group + sender-rabbitmq: + exchange: ${spring.application.name}-websocket-exchange # 消息发送的 RabbitMQ Exchange + queue: ${spring.application.name}-websocket-queue # 消息发送的 RabbitMQ Queue + sender-kafka: + topic: ${spring.application.name}-websocket # 消息发送的 Kafka Topic + consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 Kafka Consumer Group + swagger: + title: 云息 + description: 提供管理后台、用户 App 的所有功能 + version: ${mes.info.version} + url: ${mes.web.admin-ui.url} + email: xingyu4j@vip.qq.com + license: MIT + license-url: https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/LICENSE + captcha: + enable: true # 验证码的开关,默认为 true + codegen: + base-package: ${mes.info.base-package} + db-schemas: ${spring.datasource.dynamic.datasource.master.name} + front-type: 10 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类 + error-code: # 错误码相关配置项 + constants-class-list: + - com.chanko.yunxi.mes.heli.module.bpm.enums.ErrorCodeConstants + - com.chanko.yunxi.mes.heli.module.infra.enums.ErrorCodeConstants + - com.chanko.yunxi.mes.heli.module.member.enums.ErrorCodeConstants + - com.chanko.yunxi.mes.heli.module.pay.enums.ErrorCodeConstants + - com.chanko.yunxi.mes.heli.module.system.enums.ErrorCodeConstants + - com.chanko.yunxi.mes.heli.module.mp.enums.ErrorCodeConstants + tenant: # 多租户相关配置项 + enable: true + ignore-urls: + - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号 + - /admin-api/system/tenant/get-by-website # 基于域名获取租户,不许带租户编号 + - /admin-api/system/captcha/get # 获取图片验证码,和租户无关 + - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 + - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 + - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号 + - /jmreport/* # 积木报表,无法携带租户编号 + - /ureport/* # UReport 报表,无法携带租户编号 + - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号 + ignore-tables: + - system_tenant + - system_tenant_package + - system_dict_data + - system_dict_type + - system_error_code + - system_menu + - system_sms_channel + - system_sms_template + - system_sms_log + - system_sensitive_word + - system_oauth2_client + - system_mail_account + - system_mail_template + - system_mail_log + - system_notify_template + - infra_codegen_column + - infra_codegen_table + - infra_test_demo + - infra_config + - infra_file_config + - infra_file + - infra_file_content + - infra_job + - infra_job_log + - infra_job_log + - infra_data_source_config + - jimu_dict + - jimu_dict_item + - jimu_report + - jimu_report_data_source + - jimu_report_db + - jimu_report_db_field + - jimu_report_db_param + - jimu_report_link + - jimu_report_map + - jimu_report_share + - report_ureport_data + - rep_demo_dxtj + - rep_demo_employee + - rep_demo_gongsi + - rep_demo_jianpiao + - tmp_report_data_1 + - tmp_report_data_income + sms-code: # 短信验证码相关的配置项 + expire-times: 10m + send-frequency: 1m + send-maximum-quantity-per-day: 10 + begin-code: 9999 # 这里配置 9999 的原因是,测试方便。 + end-code: 9999 # 这里配置 9999 的原因是,测试方便。 + trade: + order: + app-id: 1 # 商户编号 + pay-expire-time: 2h # 支付的过期时间 + receive-expire-time: 14d # 收货的过期时间 + comment-expire-time: 7d # 评论的过期时间 + express: + client: kd_niao + kd-niao: + api-key: cb022f1e-48f1-4c4a-a723-9001ac9676b8 + business-id: 1809751 + kd100: + key: pLXUGAwK5305 + customer: E77DF18BE109F454A5CD319E44BF5177 + +debug: false + +# 积木报表配置 +jeecg: + jmreport: + saas-mode: tenant + +# UReport 配置 +ureport: + provider: + database: + disabled: false diff --git a/mes-server/src/main/resources/logback-spring.xml b/mes-server/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..71295c0f --- /dev/null +++ b/mes-server/src/main/resources/logback-spring.xml @@ -0,0 +1,76 @@ + + + + + + + + + +       + + + ${PATTERN_DEFAULT} + + + + + + + + + + ${PATTERN_DEFAULT} + + + + ${LOG_FILE} + + + ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} + + ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} + + ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} + + ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} + + ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30} + + + + + + 0 + + 256 + + + + + + + + ${PATTERN_DEFAULT} + + + + + + + + + + + + + + + + + + + + + + diff --git a/mes-server/src/test/java/com/chanko/yunxi/mes/heli/ProjectReactor.java b/mes-server/src/test/java/com/chanko/yunxi/mes/heli/ProjectReactor.java new file mode 100644 index 00000000..1267193a --- /dev/null +++ b/mes-server/src/test/java/com/chanko/yunxi/mes/heli/ProjectReactor.java @@ -0,0 +1,146 @@ +package com.chanko.yunxi.mes.heli; + +import cn.hutool.core.io.FileTypeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import com.chanko.yunxi.mes.heli.framework.common.util.collection.SetUtils; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +import static java.io.File.separator; + +/** + * 项目修改器,一键替换 Maven 的 groupId、artifactId,项目的 package 等 + *

+ * 通过修改 groupIdNew、artifactIdNew、projectBaseDirNew 三个变量 + * + * @author 芋道源码 + */ +@Slf4j +public class ProjectReactor { + + private static final String GROUP_ID = "com.chanko.yunxi"; + private static final String ARTIFACT_ID = "mes"; + private static final String PACKAGE_NAME = "com.chanko.yunxi.mes.heli"; + private static final String TITLE = "合立MES系统"; + + /** + * 白名单文件,不进行重写,避免出问题 + */ + private static final Set WHITE_FILE_TYPES = SetUtils.asSet("gif", "jpg", "svg", "png", // 图片 + "eot", "woff2", "ttf", "woff"); // 字体 + + public static void main(String[] args) { + long start = System.currentTimeMillis(); + String projectBaseDir = getProjectBaseDir(); + log.info("[main][原项目路劲改地址 ({})]", projectBaseDir); + + // ========== 配置,需要你手动修改 ========== + String groupIdNew = "com.chanko.yunxi"; + String artifactIdNew = "mes"; + String packageNameNew = "com.chanko.yunxi.mes.heli"; + String titleNew = "合立MES系统"; + String projectBaseDirNew = projectBaseDir + "-new"; // 一键改名后,“新”项目所在的目录 + log.info("[main][检测新项目目录 ({})是否存在]", projectBaseDirNew); + if (FileUtil.exist(projectBaseDirNew)) { + log.error("[main][新项目目录检测 ({})已存在,请更改新的目录!程序退出]", projectBaseDirNew); + return; + } + // 如果新目录中存在 PACKAGE_NAME,ARTIFACT_ID 等关键字,路径会被替换,导致生成的文件不在预期目录 + if (StrUtil.containsAny(projectBaseDirNew, PACKAGE_NAME, ARTIFACT_ID, StrUtil.upperFirst(ARTIFACT_ID))) { + log.error("[main][新项目目录 `projectBaseDirNew` 检测 ({}) 存在冲突名称「{}」或者「{}」,请更改新的目录!程序退出]", + projectBaseDirNew, PACKAGE_NAME, ARTIFACT_ID); + return; + } + log.info("[main][完成新项目目录检测,新项目路径地址 ({})]", projectBaseDirNew); + // 获得需要复制的文件 + log.info("[main][开始获得需要重写的文件,预计需要 10-20 秒]"); + Collection files = listFiles(projectBaseDir); + log.info("[main][需要重写的文件数量:{},预计需要 15-30 秒]", files.size()); + // 写入文件 + files.forEach(file -> { + // 如果是白名单的文件类型,不进行重写,直接拷贝 + String fileType = getFileType(file); + if (WHITE_FILE_TYPES.contains(fileType)) { + copyFile(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); + return; + } + // 如果非白名单的文件类型,重写内容,在生成文件 + String content = replaceFileContent(file, groupIdNew, artifactIdNew, packageNameNew, titleNew); + writeFile(file, content, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); + }); + log.info("[main][重写完成]共耗时:{} 秒", (System.currentTimeMillis() - start) / 1000); + } + + private static String getProjectBaseDir() { + String baseDir = System.getProperty("user.dir"); + if (StrUtil.isEmpty(baseDir)) { + throw new NullPointerException("项目基础路径不存在"); + } + return baseDir; + } + + private static Collection listFiles(String projectBaseDir) { + Collection files = FileUtil.loopFiles(projectBaseDir); + // 移除 IDEA、Git 自身的文件、Node 编译出来的文件 + files = files.stream() + .filter(file -> !file.getPath().contains(separator + "target" + separator) + && !file.getPath().contains(separator + "node_modules" + separator) + && !file.getPath().contains(separator + ".idea" + separator) + && !file.getPath().contains(separator + ".git" + separator) + && !file.getPath().contains(separator + "dist" + separator) + && !file.getPath().contains(".iml") + && !file.getPath().contains(".html.gz")) + .collect(Collectors.toList()); + return files; + } + + private static String replaceFileContent(File file, String groupIdNew, + String artifactIdNew, String packageNameNew, + String titleNew) { + String content = FileUtil.readString(file, StandardCharsets.UTF_8); + // 如果是白名单的文件类型,不进行重写 + String fileType = getFileType(file); + if (WHITE_FILE_TYPES.contains(fileType)) { + return content; + } + // 执行文件内容都重写 + return content.replaceAll(GROUP_ID, groupIdNew) + .replaceAll(PACKAGE_NAME, packageNameNew) + .replaceAll(ARTIFACT_ID, artifactIdNew) // 必须放在最后替换,因为 ARTIFACT_ID 太短! + .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew)) + .replaceAll(TITLE, titleNew); + } + + private static void writeFile(File file, String fileContent, String projectBaseDir, + String projectBaseDirNew, String packageNameNew, String artifactIdNew) { + String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); + FileUtil.writeUtf8String(fileContent, newPath); + } + + private static void copyFile(File file, String projectBaseDir, + String projectBaseDirNew, String packageNameNew, String artifactIdNew) { + String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); + FileUtil.copyFile(file, new File(newPath)); + } + + private static String buildNewFilePath(File file, String projectBaseDir, + String projectBaseDirNew, String packageNameNew, String artifactIdNew) { + return file.getPath().replace(projectBaseDir, projectBaseDirNew) // 新目录 + .replace(PACKAGE_NAME.replaceAll("\\.", Matcher.quoteReplacement(separator)), + packageNameNew.replaceAll("\\.", Matcher.quoteReplacement(separator))) + .replace(ARTIFACT_ID, artifactIdNew) // + .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew)); + } + + private static String getFileType(File file) { + return file.length() > 0 ? FileTypeUtil.getType(file) : ""; + } + +} diff --git a/mes-ui/mes-ui-admin-vue3/.editorconfig b/mes-ui/mes-ui-admin-vue3/.editorconfig new file mode 100644 index 00000000..79a12ffa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.editorconfig @@ -0,0 +1,12 @@ +root = true +[*.{js,ts,vue}] +charset = utf-8 # 设置文件字符集为 utf-8 +end_of_line = lf # 控制换行类型(lf | cr | crlf) +insert_final_newline = true # 始终在文件末尾插入一个新行 +indent_style = space # 缩进风格(tab | space) +indent_size = 2 # 缩进大小 +max_line_length = 100 # 最大行长度 + +[*.md] # 仅 md 文件适用以下规则 +max_line_length = off # 关闭最大行长度限制 +trim_trailing_whitespace = false # 关闭末尾空格修剪 diff --git a/mes-ui/mes-ui-admin-vue3/.env b/mes-ui/mes-ui-admin-vue3/.env new file mode 100644 index 00000000..fac89790 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.env @@ -0,0 +1,17 @@ +# 标题 +VITE_APP_TITLE=合立MES系统 + +# 项目本地运行端口号 +VITE_PORT=80 + +# open 运行 npm run dev 时自动打开浏览器 +VITE_OPEN=true + +# 租户开关 +VITE_APP_TENANT_ENABLE=true + +# 验证码的开关 +VITE_APP_CAPTCHA_ENABLE=true + +# 百度统计 +VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc diff --git a/mes-ui/mes-ui-admin-vue3/.env.base b/mes-ui/mes-ui-admin-vue3/.env.base new file mode 100644 index 00000000..2cf98761 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.env.base @@ -0,0 +1,19 @@ +# 本地开发环境 +NODE_ENV=development + +VITE_DEV=true + +# 请求路径 +VITE_BASE_URL='http://127.0.0.1:8080' + +# 上传路径 +VITE_UPLOAD_URL='http://127.0.0.1:8080/admin-api/infra/file/upload' + +# 接口前缀 +VITE_API_BASEPATH=/dev-api + +# 接口地址 +VITE_API_URL=/admin-api + +# 打包路径 +VITE_BASE_PATH=/ diff --git a/mes-ui/mes-ui-admin-vue3/.env.dev b/mes-ui/mes-ui-admin-vue3/.env.dev new file mode 100644 index 00000000..dea8f8d6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.env.dev @@ -0,0 +1,31 @@ +# 开发环境 +NODE_ENV=development + +VITE_DEV=false + +# 请求路径 +VITE_BASE_URL='http://localhost:8080' + +# 上传路径 +VITE_UPLOAD_URL='http://localhost:8080/admin-api/infra/file/upload' + +# 接口前缀 +VITE_API_BASEPATH=/dev-api + +# 接口地址 +VITE_API_URL=/admin-api + +# 打包路径 +VITE_BASE_PATH=/ + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=false + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 输出路径 +VITE_OUT_DIR=dist diff --git a/mes-ui/mes-ui-admin-vue3/.env.front b/mes-ui/mes-ui-admin-vue3/.env.front new file mode 100644 index 00000000..3aa737c5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.env.front @@ -0,0 +1,34 @@ +# 本地开发环境 +NODE_ENV=development + +VITE_DEV=true + +# 请求路径 +VITE_BASE_URL='http://api-dashboard.mes.iocoder.cn' + +# 上传路径 +VITE_UPLOAD_URL='http://api-dashboard.mes.iocoder.cn/admin-api/infra/file/upload' + +# 接口前缀 +VITE_API_BASEPATH=/dev-api + +# 接口地址 +VITE_API_URL=/admin-api + +# 打包路径 +VITE_BASE_PATH=/ + +# 项目本地运行端口号, 与.vscode/launch.json配合 +VITE_PORT=80 + +# 是否删除debugger +VITE_DROP_DEBUGGER=false + +# 是否删除console.log +VITE_DROP_CONSOLE=false + +# 是否sourcemap +VITE_SOURCEMAP=true + +# 验证码的开关 +VITE_APP_CAPTCHA_ENABLE=false diff --git a/mes-ui/mes-ui-admin-vue3/.env.pro b/mes-ui/mes-ui-admin-vue3/.env.pro new file mode 100644 index 00000000..8348e02e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.env.pro @@ -0,0 +1,31 @@ +# 生产环境 +NODE_ENV=production + +VITE_DEV=false + +# 请求路径 +VITE_BASE_URL='http://localhost:48080' + +# 上传路径 +VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' + +# 接口前缀 +VITE_API_BASEPATH= + +# 接口地址 +VITE_API_URL=/admin-api + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 打包路径 +VITE_BASE_PATH=/ + +# 输出路径 +VITE_OUT_DIR=dist-pro diff --git a/mes-ui/mes-ui-admin-vue3/.env.stage b/mes-ui/mes-ui-admin-vue3/.env.stage new file mode 100644 index 00000000..7806c166 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.env.stage @@ -0,0 +1,31 @@ +# 生产环境 +NODE_ENV=production + +VITE_DEV=false + +# 请求路径 +VITE_BASE_URL='http://api-dashboard.mes.iocoder.cn' + +# 上传路径 +VITE_UPLOAD_URL='http://api-dashboard.mes.iocoder.cn/admin-api/infra/file/upload' + +# 接口前缀 +VITE_API_BASEPATH= + +# 接口地址 +VITE_API_URL=/admin-api + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 打包路径 +VITE_BASE_PATH='http://static-vue3.mes.iocoder.cn/' + +# 输出路径 +VITE_OUT_DIR=dist-stage diff --git a/mes-ui/mes-ui-admin-vue3/.env.static b/mes-ui/mes-ui-admin-vue3/.env.static new file mode 100644 index 00000000..034a7f4d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.env.static @@ -0,0 +1,31 @@ +# 开发环境 +NODE_ENV=production + +VITE_DEV=false + +# 请求路径 +VITE_BASE_URL='http://localhost:48080' + +# 上传路径 +VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' + +# 接口前缀 +VITE_API_BASEPATH= + +# 接口地址 +VITE_API_URL=/admin-api + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 打包路径 +VITE_BASE_PATH=/admin-ui-vue3/ + +# 输出路径 +VITE_OUT_DIR=dist-dev diff --git a/mes-ui/mes-ui-admin-vue3/.eslintignore b/mes-ui/mes-ui-admin-vue3/.eslintignore new file mode 100644 index 00000000..1e85c0fb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.eslintignore @@ -0,0 +1,8 @@ +/build/ +/config/ +/dist/ +/*.js +/test/unit/coverage/ +/node_modules/* +/dist* +/src/main.ts diff --git a/mes-ui/mes-ui-admin-vue3/.eslintrc-auto-import.json b/mes-ui/mes-ui-admin-vue3/.eslintrc-auto-import.json new file mode 100644 index 00000000..024c96a3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.eslintrc-auto-import.json @@ -0,0 +1,259 @@ +{ + "globals": { + "EffectScope": true, + "ElMessage": true, + "ElMessageBox": true, + "ElTag": true, + "asyncComputed": true, + "autoResetRef": true, + "computed": true, + "computedAsync": true, + "computedEager": true, + "computedInject": true, + "computedWithControl": true, + "controlledComputed": true, + "controlledRef": true, + "createApp": true, + "createEventHook": true, + "createGlobalState": true, + "createInjectionState": true, + "createReactiveFn": true, + "createSharedComposable": true, + "createUnrefFn": true, + "customRef": true, + "debouncedRef": true, + "debouncedWatch": true, + "defineAsyncComponent": true, + "defineComponent": true, + "eagerComputed": true, + "effectScope": true, + "extendRef": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "ignorableWatch": true, + "inject": true, + "isDefined": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "makeDestructurable": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onClickOutside": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onKeyStroke": true, + "onLongPress": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onStartTyping": true, + "onUnmounted": true, + "onUpdated": true, + "pausableWatch": true, + "provide": true, + "reactify": true, + "reactifyObject": true, + "reactive": true, + "reactiveComputed": true, + "reactiveOmit": true, + "reactivePick": true, + "readonly": true, + "ref": true, + "refAutoReset": true, + "refDebounced": true, + "refDefault": true, + "refThrottled": true, + "refWithControl": true, + "resolveComponent": true, + "resolveRef": true, + "resolveUnref": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "syncRef": true, + "syncRefs": true, + "templateRef": true, + "throttledRef": true, + "throttledWatch": true, + "toRaw": true, + "toReactive": true, + "toRef": true, + "toRefs": true, + "triggerRef": true, + "tryOnBeforeMount": true, + "tryOnBeforeUnmount": true, + "tryOnMounted": true, + "tryOnScopeDispose": true, + "tryOnUnmounted": true, + "unref": true, + "unrefElement": true, + "until": true, + "useActiveElement": true, + "useArrayEvery": true, + "useArrayFilter": true, + "useArrayFind": true, + "useArrayFindIndex": true, + "useArrayJoin": true, + "useArrayMap": true, + "useArrayReduce": true, + "useArraySome": true, + "useAsyncQueue": true, + "useAsyncState": true, + "useAttrs": true, + "useBase64": true, + "useBattery": true, + "useBluetooth": true, + "useBreakpoints": true, + "useBroadcastChannel": true, + "useBrowserLocation": true, + "useCached": true, + "useClipboard": true, + "useColorMode": true, + "useConfirmDialog": true, + "useCounter": true, + "useCssModule": true, + "useCssVar": true, + "useCssVars": true, + "useCurrentElement": true, + "useCycleList": true, + "useDark": true, + "useDateFormat": true, + "useDebounce": true, + "useDebounceFn": true, + "useDebouncedRefHistory": true, + "useDeviceMotion": true, + "useDeviceOrientation": true, + "useDevicePixelRatio": true, + "useDevicesList": true, + "useDisplayMedia": true, + "useDocumentVisibility": true, + "useDraggable": true, + "useDropZone": true, + "useElementBounding": true, + "useElementByPoint": true, + "useElementHover": true, + "useElementSize": true, + "useElementVisibility": true, + "useEventBus": true, + "useEventListener": true, + "useEventSource": true, + "useEyeDropper": true, + "useFavicon": true, + "useFetch": true, + "useFileDialog": true, + "useFileSystemAccess": true, + "useFocus": true, + "useFocusWithin": true, + "useFps": true, + "useFullscreen": true, + "useGamepad": true, + "useGeolocation": true, + "useIdle": true, + "useImage": true, + "useInfiniteScroll": true, + "useIntersectionObserver": true, + "useInterval": true, + "useIntervalFn": true, + "useKeyModifier": true, + "useLastChanged": true, + "useLocalStorage": true, + "useMagicKeys": true, + "useManualRefHistory": true, + "useMediaControls": true, + "useMediaQuery": true, + "useMemoize": true, + "useMemory": true, + "useMounted": true, + "useMouse": true, + "useMouseInElement": true, + "useMousePressed": true, + "useMutationObserver": true, + "useNavigatorLanguage": true, + "useNetwork": true, + "useNow": true, + "useObjectUrl": true, + "useOffsetPagination": true, + "useOnline": true, + "usePageLeave": true, + "useParallax": true, + "usePermission": true, + "usePointer": true, + "usePointerSwipe": true, + "usePreferredColorScheme": true, + "usePreferredDark": true, + "usePreferredLanguages": true, + "useRafFn": true, + "useRefHistory": true, + "useResizeObserver": true, + "useRoute": true, + "useRouter": true, + "useScreenOrientation": true, + "useScreenSafeArea": true, + "useScriptTag": true, + "useScroll": true, + "useScrollLock": true, + "useSessionStorage": true, + "useShare": true, + "useSlots": true, + "useSpeechRecognition": true, + "useSpeechSynthesis": true, + "useStepper": true, + "useStorage": true, + "useStorageAsync": true, + "useStyleTag": true, + "useSupported": true, + "useSwipe": true, + "useTemplateRefsList": true, + "useTextDirection": true, + "useTextSelection": true, + "useTextareaAutosize": true, + "useThrottle": true, + "useThrottleFn": true, + "useThrottledRefHistory": true, + "useTimeAgo": true, + "useTimeout": true, + "useTimeoutFn": true, + "useTimeoutPoll": true, + "useTimestamp": true, + "useTitle": true, + "useToggle": true, + "useTransition": true, + "useUrlSearchParams": true, + "useUserMedia": true, + "useVModel": true, + "useVModels": true, + "useVibrate": true, + "useVirtualList": true, + "useWakeLock": true, + "useWebNotification": true, + "useWebSocket": true, + "useWebWorker": true, + "useWebWorkerFn": true, + "useWindowFocus": true, + "useWindowScroll": true, + "useWindowSize": true, + "watch": true, + "watchArray": true, + "watchAtMost": true, + "watchDebounced": true, + "watchEffect": true, + "watchIgnorable": true, + "watchOnce": true, + "watchPausable": true, + "watchPostEffect": true, + "watchSyncEffect": true, + "watchThrottled": true, + "watchTriggerable": true, + "watchWithFilter": true, + "whenever": true + } +} diff --git a/mes-ui/mes-ui-admin-vue3/.eslintrc.js b/mes-ui/mes-ui-admin-vue3/.eslintrc.js new file mode 100644 index 00000000..70c91784 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.eslintrc.js @@ -0,0 +1,73 @@ +// @ts-check +const { defineConfig } = require('eslint-define-config') +module.exports = defineConfig({ + root: true, + env: { + browser: true, + node: true, + es6: true + }, + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + ecmaVersion: 2020, + sourceType: 'module', + jsxPragma: 'React', + ecmaFeatures: { + jsx: true + } + }, + extends: [ + 'plugin:vue/vue3-recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + 'plugin:prettier/recommended', + '@unocss' + ], + rules: { + 'vue/no-setup-props-destructure': 'off', + 'vue/script-setup-uses-vars': 'error', + 'vue/no-reserved-component-names': 'off', + '@typescript-eslint/ban-ts-ignore': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-empty-function': 'off', + 'vue/custom-event-name-casing': 'off', + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'no-unused-vars': 'off', + 'space-before-function-paren': 'off', + + 'vue/attributes-order': 'off', + 'vue/one-component-per-file': 'off', + 'vue/html-closing-bracket-newline': 'off', + 'vue/max-attributes-per-line': 'off', + 'vue/multiline-html-element-content-newline': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/attribute-hyphenation': 'off', + 'vue/require-default-prop': 'off', + 'vue/require-explicit-emits': 'off', + 'vue/require-toggle-inside-transition': 'off', + 'vue/html-self-closing': [ + 'error', + { + html: { + void: 'always', + normal: 'never', + component: 'always' + }, + svg: 'always', + math: 'always' + } + ], + 'vue/multi-word-component-names': 'off', + 'vue/no-v-html': 'off', + 'prettier/prettier': 'off' // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件 + } +}) diff --git a/mes-ui/mes-ui-admin-vue3/.gitignore b/mes-ui/mes-ui-admin-vue3/.gitignore new file mode 100644 index 00000000..0f033cc4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.gitignore @@ -0,0 +1,11 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +/dist* +*-lock.* +pnpm-debug +auto-*.d.ts +.idea +.history diff --git a/mes-ui/mes-ui-admin-vue3/.image/Java监控.jpg b/mes-ui/mes-ui-admin-vue3/.image/Java监控.jpg new file mode 100644 index 00000000..6ad522ab Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/Java监控.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/MySQL.jpg b/mes-ui/mes-ui-admin-vue3/.image/MySQL.jpg new file mode 100644 index 00000000..64a1940c Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/MySQL.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/OA请假-列表.jpg b/mes-ui/mes-ui-admin-vue3/.image/OA请假-列表.jpg new file mode 100644 index 00000000..787bb73f Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/OA请假-列表.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/OA请假-发起.jpg b/mes-ui/mes-ui-admin-vue3/.image/OA请假-发起.jpg new file mode 100644 index 00000000..1a7342d7 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/OA请假-发起.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/OA请假-详情.jpg b/mes-ui/mes-ui-admin-vue3/.image/OA请假-详情.jpg new file mode 100644 index 00000000..a83e7c14 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/OA请假-详情.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/Redis.jpg b/mes-ui/mes-ui-admin-vue3/.image/Redis.jpg new file mode 100644 index 00000000..95693526 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/Redis.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/01.png b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/01.png new file mode 100644 index 00000000..0f65d99e Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/01.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/02.png b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/02.png new file mode 100644 index 00000000..05ec781c Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/02.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/03.png b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/03.png new file mode 100644 index 00000000..f400c688 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/03.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/04.png b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/04.png new file mode 100644 index 00000000..d5d5ea07 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/04.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/05.png b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/05.png new file mode 100644 index 00000000..1de6d8ae Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/05.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/06.png b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/06.png new file mode 100644 index 00000000..400ae90b Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/06.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/07.png b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/07.png new file mode 100644 index 00000000..2ed8c0ff Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/07.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/08.png b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/08.png new file mode 100644 index 00000000..090e64a6 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/08.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/09.png b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/09.png new file mode 100644 index 00000000..f2032c8a Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/admin-uniapp/09.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/common/mall-feature.png b/mes-ui/mes-ui-admin-vue3/.image/common/mall-feature.png new file mode 100644 index 00000000..cca05c0e Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/common/mall-feature.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/common/mall-preview.png b/mes-ui/mes-ui-admin-vue3/.image/common/mall-preview.png new file mode 100644 index 00000000..f939214b Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/common/mall-preview.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/common/mes-cloud-architecture.png b/mes-ui/mes-ui-admin-vue3/.image/common/mes-cloud-architecture.png new file mode 100644 index 00000000..59416d80 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/common/mes-cloud-architecture.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/common/mes-roadmap.png b/mes-ui/mes-ui-admin-vue3/.image/common/mes-roadmap.png new file mode 100644 index 00000000..f4becc98 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/common/mes-roadmap.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/common/project-vs.png b/mes-ui/mes-ui-admin-vue3/.image/common/project-vs.png new file mode 100644 index 00000000..561e092f Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/common/project-vs.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/common/ruoyi-vue-pro-architecture.png b/mes-ui/mes-ui-admin-vue3/.image/common/ruoyi-vue-pro-architecture.png new file mode 100644 index 00000000..7bd7d59a Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/common/ruoyi-vue-pro-architecture.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/common/ruoyi-vue-pro-biz.png b/mes-ui/mes-ui-admin-vue3/.image/common/ruoyi-vue-pro-biz.png new file mode 100644 index 00000000..24a385ab Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/common/ruoyi-vue-pro-biz.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/个人中心.jpg b/mes-ui/mes-ui-admin-vue3/.image/个人中心.jpg new file mode 100644 index 00000000..ce57f6e1 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/个人中心.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/代码生成.jpg b/mes-ui/mes-ui-admin-vue3/.image/代码生成.jpg new file mode 100644 index 00000000..751603ef Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/代码生成.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/令牌管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/令牌管理.jpg new file mode 100644 index 00000000..04abf4d2 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/令牌管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/任务列表-审批.jpg b/mes-ui/mes-ui-admin-vue3/.image/任务列表-审批.jpg new file mode 100644 index 00000000..cba312a0 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/任务列表-审批.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/任务列表-已办.jpg b/mes-ui/mes-ui-admin-vue3/.image/任务列表-已办.jpg new file mode 100644 index 00000000..7a8d0fb1 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/任务列表-已办.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/任务列表-待办.jpg b/mes-ui/mes-ui-admin-vue3/.image/任务列表-待办.jpg new file mode 100644 index 00000000..a90323fb Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/任务列表-待办.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/任务日志.jpg b/mes-ui/mes-ui-admin-vue3/.image/任务日志.jpg new file mode 100644 index 00000000..599e50a9 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/任务日志.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/商户信息.jpg b/mes-ui/mes-ui-admin-vue3/.image/商户信息.jpg new file mode 100644 index 00000000..483eace1 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/商户信息.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/在线用户.jpg b/mes-ui/mes-ui-admin-vue3/.image/在线用户.jpg new file mode 100644 index 00000000..b183009b Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/在线用户.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/大屏设计器-列表.jpg b/mes-ui/mes-ui-admin-vue3/.image/大屏设计器-列表.jpg new file mode 100644 index 00000000..9a45c3bd Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/大屏设计器-列表.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/大屏设计器-编辑.jpg b/mes-ui/mes-ui-admin-vue3/.image/大屏设计器-编辑.jpg new file mode 100644 index 00000000..63298a0c Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/大屏设计器-编辑.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/大屏设计器-预览.jpg b/mes-ui/mes-ui-admin-vue3/.image/大屏设计器-预览.jpg new file mode 100644 index 00000000..501d9ea2 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/大屏设计器-预览.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/字典数据.jpg b/mes-ui/mes-ui-admin-vue3/.image/字典数据.jpg new file mode 100644 index 00000000..8298c893 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/字典数据.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/字典类型.jpg b/mes-ui/mes-ui-admin-vue3/.image/字典类型.jpg new file mode 100644 index 00000000..6613392f Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/字典类型.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/定时任务.jpg b/mes-ui/mes-ui-admin-vue3/.image/定时任务.jpg new file mode 100644 index 00000000..d5bbd851 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/定时任务.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/岗位管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/岗位管理.jpg new file mode 100644 index 00000000..42b64d2c Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/岗位管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/应用信息-列表.jpg b/mes-ui/mes-ui-admin-vue3/.image/应用信息-列表.jpg new file mode 100644 index 00000000..da419a24 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/应用信息-列表.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/应用信息-编辑.jpg b/mes-ui/mes-ui-admin-vue3/.image/应用信息-编辑.jpg new file mode 100644 index 00000000..913cfbc8 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/应用信息-编辑.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/应用管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/应用管理.jpg new file mode 100644 index 00000000..6e7789fc Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/应用管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/我的流程-列表.jpg b/mes-ui/mes-ui-admin-vue3/.image/我的流程-列表.jpg new file mode 100644 index 00000000..223d17af Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/我的流程-列表.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/我的流程-发起.jpg b/mes-ui/mes-ui-admin-vue3/.image/我的流程-发起.jpg new file mode 100644 index 00000000..7a833062 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/我的流程-发起.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/我的流程-详情.jpg b/mes-ui/mes-ui-admin-vue3/.image/我的流程-详情.jpg new file mode 100644 index 00000000..6a015418 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/我的流程-详情.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/报表设计器-图形报表.jpg b/mes-ui/mes-ui-admin-vue3/.image/报表设计器-图形报表.jpg new file mode 100644 index 00000000..681b3185 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/报表设计器-图形报表.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/报表设计器-打印设计.jpg b/mes-ui/mes-ui-admin-vue3/.image/报表设计器-打印设计.jpg new file mode 100644 index 00000000..bb86da64 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/报表设计器-打印设计.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/报表设计器-数据报表.jpg b/mes-ui/mes-ui-admin-vue3/.image/报表设计器-数据报表.jpg new file mode 100644 index 00000000..9ca5b9b6 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/报表设计器-数据报表.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/操作日志.jpg b/mes-ui/mes-ui-admin-vue3/.image/操作日志.jpg new file mode 100644 index 00000000..4a0611a3 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/操作日志.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/支付订单.jpg b/mes-ui/mes-ui-admin-vue3/.image/支付订单.jpg new file mode 100644 index 00000000..0a56dd74 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/支付订单.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/敏感词.jpg b/mes-ui/mes-ui-admin-vue3/.image/敏感词.jpg new file mode 100644 index 00000000..92a53974 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/敏感词.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/数据库文档.jpg b/mes-ui/mes-ui-admin-vue3/.image/数据库文档.jpg new file mode 100644 index 00000000..a4339d96 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/数据库文档.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/文件管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/文件管理.jpg new file mode 100644 index 00000000..054b19f1 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/文件管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/文件管理2.jpg b/mes-ui/mes-ui-admin-vue3/.image/文件管理2.jpg new file mode 100644 index 00000000..b12e5c3c Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/文件管理2.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/文件配置.jpg b/mes-ui/mes-ui-admin-vue3/.image/文件配置.jpg new file mode 100644 index 00000000..e618049a Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/文件配置.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/日志中心.jpg b/mes-ui/mes-ui-admin-vue3/.image/日志中心.jpg new file mode 100644 index 00000000..27c1c6cb Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/日志中心.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/流程模型-列表.jpg b/mes-ui/mes-ui-admin-vue3/.image/流程模型-列表.jpg new file mode 100644 index 00000000..ffdc5840 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/流程模型-列表.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/流程模型-定义.jpg b/mes-ui/mes-ui-admin-vue3/.image/流程模型-定义.jpg new file mode 100644 index 00000000..18b316c7 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/流程模型-定义.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/流程模型-设计.jpg b/mes-ui/mes-ui-admin-vue3/.image/流程模型-设计.jpg new file mode 100644 index 00000000..96149690 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/流程模型-设计.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/流程表单.jpg b/mes-ui/mes-ui-admin-vue3/.image/流程表单.jpg new file mode 100644 index 00000000..60669c14 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/流程表单.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/生成效果.jpg b/mes-ui/mes-ui-admin-vue3/.image/生成效果.jpg new file mode 100644 index 00000000..98ff2cca Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/生成效果.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/用户分组.jpg b/mes-ui/mes-ui-admin-vue3/.image/用户分组.jpg new file mode 100644 index 00000000..39af1cd1 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/用户分组.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/用户管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/用户管理.jpg new file mode 100644 index 00000000..844604a6 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/用户管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/登录.jpg b/mes-ui/mes-ui-admin-vue3/.image/登录.jpg new file mode 100644 index 00000000..b782b988 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/登录.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/登录日志.jpg b/mes-ui/mes-ui-admin-vue3/.image/登录日志.jpg new file mode 100644 index 00000000..25662d97 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/登录日志.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/短信日志.jpg b/mes-ui/mes-ui-admin-vue3/.image/短信日志.jpg new file mode 100644 index 00000000..ada8e56d Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/短信日志.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/短信模板.jpg b/mes-ui/mes-ui-admin-vue3/.image/短信模板.jpg new file mode 100644 index 00000000..09381cc5 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/短信模板.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/短信渠道.jpg b/mes-ui/mes-ui-admin-vue3/.image/短信渠道.jpg new file mode 100644 index 00000000..df3a5c39 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/短信渠道.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/租户套餐.png b/mes-ui/mes-ui-admin-vue3/.image/租户套餐.png new file mode 100644 index 00000000..96631679 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/租户套餐.png differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/租户管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/租户管理.jpg new file mode 100644 index 00000000..647416a9 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/租户管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/系统接口.jpg b/mes-ui/mes-ui-admin-vue3/.image/系统接口.jpg new file mode 100644 index 00000000..6d39d421 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/系统接口.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/菜单管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/菜单管理.jpg new file mode 100644 index 00000000..ad3b7979 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/菜单管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/表单构建.jpg b/mes-ui/mes-ui-admin-vue3/.image/表单构建.jpg new file mode 100644 index 00000000..81f03746 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/表单构建.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/角色管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/角色管理.jpg new file mode 100644 index 00000000..eed776e8 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/角色管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/访问日志.jpg b/mes-ui/mes-ui-admin-vue3/.image/访问日志.jpg new file mode 100644 index 00000000..ef301aad Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/访问日志.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/退款订单.jpg b/mes-ui/mes-ui-admin-vue3/.image/退款订单.jpg new file mode 100644 index 00000000..2c6c6c9e Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/退款订单.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/通知公告.jpg b/mes-ui/mes-ui-admin-vue3/.image/通知公告.jpg new file mode 100644 index 00000000..97bb42fe Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/通知公告.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/部门管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/部门管理.jpg new file mode 100644 index 00000000..6eab2330 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/部门管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/配置管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/配置管理.jpg new file mode 100644 index 00000000..0abaec93 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/配置管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/链路追踪.jpg b/mes-ui/mes-ui-admin-vue3/.image/链路追踪.jpg new file mode 100644 index 00000000..12f7aa8e Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/链路追踪.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/错误日志.jpg b/mes-ui/mes-ui-admin-vue3/.image/错误日志.jpg new file mode 100644 index 00000000..eb615ea3 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/错误日志.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/错误码管理.jpg b/mes-ui/mes-ui-admin-vue3/.image/错误码管理.jpg new file mode 100644 index 00000000..ea91dde1 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/错误码管理.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.image/首页.jpg b/mes-ui/mes-ui-admin-vue3/.image/首页.jpg new file mode 100644 index 00000000..10a7fde7 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/.image/首页.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/.prettierignore b/mes-ui/mes-ui-admin-vue3/.prettierignore new file mode 100644 index 00000000..f68ea869 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.prettierignore @@ -0,0 +1,11 @@ +/node_modules/** +/dist/ +/dist* +/public/* +/docs/* +/vite.config.ts +/src/types/env.d.ts +/src/types/auto-components.d.ts +/src/types/auto-imports.d.ts +/docs/**/* +CHANGELOG diff --git a/mes-ui/mes-ui-admin-vue3/.stylelintignore b/mes-ui/mes-ui-admin-vue3/.stylelintignore new file mode 100644 index 00000000..aa605b49 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.stylelintignore @@ -0,0 +1,6 @@ +/dist/* +/public/* +public/* +/dist* +/src/types/env.d.ts +/docs/**/* diff --git a/mes-ui/mes-ui-admin-vue3/.vscode/extensions.json b/mes-ui/mes-ui-admin-vue3/.vscode/extensions.json new file mode 100644 index 00000000..5d7e57f3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.vscode/extensions.json @@ -0,0 +1,19 @@ +{ + "recommendations": [ + "christian-kohler.path-intellisense", + "vscode-icons-team.vscode-icons", + "davidanson.vscode-markdownlint", + "stylelint.vscode-stylelint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "mrmlnc.vscode-less", + "lokalise.i18n-ally", + "redhat.vscode-yaml", + "csstools.postcss", + "mikestead.dotenv", + "eamodio.gitlens", + "antfu.iconify", + "antfu.unocss", + "Vue.volar" + ] +} diff --git a/mes-ui/mes-ui-admin-vue3/.vscode/launch.json b/mes-ui/mes-ui-admin-vue3/.vscode/launch.json new file mode 100644 index 00000000..f43edc03 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "msedge", + "request": "launch", + "name": "Launch Edge against localhost", + "url": "http://localhost", + "webRoot": "${workspaceFolder}/src", + "sourceMaps": true + } + ] +} diff --git a/mes-ui/mes-ui-admin-vue3/.vscode/settings.json b/mes-ui/mes-ui-admin-vue3/.vscode/settings.json new file mode 100644 index 00000000..0779608e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/.vscode/settings.json @@ -0,0 +1,144 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "npm.packageManager": "pnpm", + "editor.tabSize": 2, + "prettier.printWidth": 100, // 超过最大值换行 + "editor.defaultFormatter": "esbenp.prettier-vscode", + "files.eol": "\n", + "search.exclude": { + "**/node_modules": true, + "**/*.log": true, + "**/*.log*": true, + "**/bower_components": true, + "**/dist": true, + "**/elehukouben": true, + "**/.git": true, + "**/.gitignore": true, + "**/.svn": true, + "**/.DS_Store": true, + "**/.idea": true, + "**/.vscode": false, + "**/yarn.lock": true, + "**/tmp": true, + "out": true, + "dist": true, + "node_modules": true, + "CHANGELOG.md": true, + "examples": true, + "res": true, + "screenshots": true, + "yarn-error.log": true, + "**/.yarn": true + }, + "files.exclude": { + "**/.cache": true, + "**/.editorconfig": true, + "**/.eslintcache": true, + "**/bower_components": true, + "**/.idea": true, + "**/tmp": true, + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/.vscode/**": true, + "**/node_modules/**": true, + "**/tmp/**": true, + "**/bower_components/**": true, + "**/dist/**": true, + "**/yarn.lock": true + }, + "stylelint.enable": true, + "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"], + "path-intellisense.mappings": { + "@/": "${workspaceRoot}/src" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[css]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[less]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[scss]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "[vue]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "i18n-ally.localesPaths": ["src/locales"], + "i18n-ally.keystyle": "nested", + "i18n-ally.sortKeys": true, + "i18n-ally.namespace": false, + "i18n-ally.enabledParsers": ["ts"], + "i18n-ally.sourceLanguage": "en", + "i18n-ally.displayLanguage": "zh-CN", + "i18n-ally.enabledFrameworks": ["vue", "react"], + "cSpell.words": [ + "brotli", + "browserslist", + "codemirror", + "commitlint", + "cropperjs", + "echart", + "echarts", + "esnext", + "esno", + "iconify", + "INTLIFY", + "lintstagedrc", + "logicflow", + "nprogress", + "pinia", + "pnpm", + "qrcode", + "sider", + "sortablejs", + "stylelint", + "svgs", + "unocss", + "unplugin", + "unref", + "videojs", + "VITE", + "vitejs", + "vueuse", + "wangeditor", + "xingyu", + "mes", + "zxcvbn" + ], + // 控制相关文件嵌套展示 + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.expand": false, + "explorer.fileNesting.patterns": { + "*.ts": "$(capture).test.ts, $(capture).test.tsx", + "*.tsx": "$(capture).test.ts, $(capture).test.tsx", + "*.env": "$(capture).env.*", + "package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.eslintrc-auto-import.json,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore" + }, + "terminal.integrated.scrollback": 10000, + "nuxt.isNuxtApp": false +} diff --git a/mes-ui/mes-ui-admin-vue3/LICENSE b/mes-ui/mes-ui-admin-vue3/LICENSE new file mode 100644 index 00000000..9861118a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present Archer + +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/mes-ui/mes-ui-admin-vue3/README.md b/mes-ui/mes-ui-admin-vue3/README.md new file mode 100644 index 00000000..3f51f459 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/README.md @@ -0,0 +1 @@ +基于 Vue3 + element-plus 实现的管理后台。 diff --git a/mes-ui/mes-ui-admin-vue3/build/vite/index.ts b/mes-ui/mes-ui-admin-vue3/build/vite/index.ts new file mode 100644 index 00000000..585759f5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/build/vite/index.ts @@ -0,0 +1,100 @@ +import { resolve } from 'path' +import Vue from '@vitejs/plugin-vue' +import VueJsx from '@vitejs/plugin-vue-jsx' +import progress from 'vite-plugin-progress' +import EslintPlugin from 'vite-plugin-eslint' +import PurgeIcons from 'vite-plugin-purge-icons' +import { ViteEjsPlugin } from 'vite-plugin-ejs' +// @ts-ignore +import ElementPlus from 'unplugin-element-plus/vite' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import viteCompression from 'vite-plugin-compression' +import topLevelAwait from 'vite-plugin-top-level-await' +import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' +import UnoCSS from 'unocss/vite' + +export function createVitePlugins() { + const root = process.cwd() + + // 路径查找 + function pathResolve(dir: string) { + return resolve(root, '.', dir) + } + + return [ + Vue(), + VueJsx(), + UnoCSS(), + progress(), + PurgeIcons(), + ElementPlus({}), + AutoImport({ + include: [ + /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx + /\.vue$/, + /\.vue\?vue/, // .vue + /\.md$/ // .md + ], + imports: [ + 'vue', + 'vue-router', + // 可额外添加需要 autoImport 的组件 + { + '@/hooks/web/useI18n': ['useI18n'], + '@/hooks/web/useMessage': ['useMessage'], + '@/hooks/web/useTable': ['useTable'], + '@/hooks/web/useCrudSchemas': ['useCrudSchemas'], + '@/utils/formRules': ['required'], + '@/utils/dict': ['DICT_TYPE'] + } + ], + dts: 'src/types/auto-imports.d.ts', + resolvers: [ElementPlusResolver()], + eslintrc: { + enabled: false, // Default `false` + filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json` + globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable') + } + }), + Components({ + // 生成自定义 `auto-components.d.ts` 全局声明 + dts: 'src/types/auto-components.d.ts', + // 自定义组件的解析器 + resolvers: [ElementPlusResolver()], + globs: ["src/components/**/**.{vue, md}", '!src/components/DiyEditor/components/mobile/**'] + }), + EslintPlugin({ + cache: false, + include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件 + }), + VueI18nPlugin({ + runtimeOnly: true, + compositionOnly: true, + include: [resolve(__dirname, 'src/locales/**')] + }), + createSvgIconsPlugin({ + iconDirs: [pathResolve('src/assets/svgs')], + symbolId: 'icon-[dir]-[name]', + svgoOptions: true + }), + viteCompression({ + verbose: true, // 是否在控制台输出压缩结果 + disable: false, // 是否禁用 + threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b + algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw'] + ext: '.gz', // 生成的压缩包后缀 + deleteOriginFile: false //压缩后是否删除源文件 + }), + ViteEjsPlugin(), + topLevelAwait({ + // https://juejin.cn/post/7152191742513512485 + // The export name of top-level await promise for each chunk module + promiseExportName: '__tla', + // The function to generate import names of top-level await promise in each chunk module + promiseImportName: (i) => `__tla_${i}` + }) + ] +} diff --git a/mes-ui/mes-ui-admin-vue3/build/vite/optimize.ts b/mes-ui/mes-ui-admin-vue3/build/vite/optimize.ts new file mode 100644 index 00000000..3dda50b0 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/build/vite/optimize.ts @@ -0,0 +1,112 @@ +const include = [ + 'qs', + 'url', + 'vue', + 'sass', + 'mitt', + 'axios', + 'pinia', + 'dayjs', + 'qrcode', + 'unocss', + 'vue-router', + 'vue-types', + 'vue-i18n', + 'crypto-js', + 'cropperjs', + 'lodash-es', + 'nprogress', + 'web-storage-cache', + '@iconify/iconify', + '@vueuse/core', + '@zxcvbn-ts/core', + 'echarts/core', + 'echarts/charts', + 'echarts/components', + 'echarts/renderers', + 'echarts-wordcloud', + '@wangeditor/editor', + '@wangeditor/editor-for-vue', + 'element-plus', + 'element-plus/es', + 'element-plus/es/locale/lang/zh-cn', + 'element-plus/es/locale/lang/en', + 'element-plus/es/components/avatar/style/css', + 'element-plus/es/components/space/style/css', + 'element-plus/es/components/backtop/style/css', + 'element-plus/es/components/form/style/css', + 'element-plus/es/components/radio-group/style/css', + 'element-plus/es/components/radio/style/css', + 'element-plus/es/components/checkbox/style/css', + 'element-plus/es/components/checkbox-group/style/css', + 'element-plus/es/components/switch/style/css', + 'element-plus/es/components/time-picker/style/css', + 'element-plus/es/components/date-picker/style/css', + 'element-plus/es/components/descriptions/style/css', + 'element-plus/es/components/descriptions-item/style/css', + 'element-plus/es/components/link/style/css', + 'element-plus/es/components/tooltip/style/css', + 'element-plus/es/components/drawer/style/css', + 'element-plus/es/components/dialog/style/css', + 'element-plus/es/components/checkbox-button/style/css', + 'element-plus/es/components/option-group/style/css', + 'element-plus/es/components/radio-button/style/css', + 'element-plus/es/components/cascader/style/css', + 'element-plus/es/components/color-picker/style/css', + 'element-plus/es/components/input-number/style/css', + 'element-plus/es/components/rate/style/css', + 'element-plus/es/components/select-v2/style/css', + 'element-plus/es/components/tree-select/style/css', + 'element-plus/es/components/slider/style/css', + 'element-plus/es/components/time-select/style/css', + 'element-plus/es/components/autocomplete/style/css', + 'element-plus/es/components/image-viewer/style/css', + 'element-plus/es/components/upload/style/css', + 'element-plus/es/components/col/style/css', + 'element-plus/es/components/form-item/style/css', + 'element-plus/es/components/alert/style/css', + 'element-plus/es/components/breadcrumb/style/css', + 'element-plus/es/components/select/style/css', + 'element-plus/es/components/input/style/css', + 'element-plus/es/components/breadcrumb-item/style/css', + 'element-plus/es/components/tag/style/css', + 'element-plus/es/components/pagination/style/css', + 'element-plus/es/components/table/style/css', + 'element-plus/es/components/table-v2/style/css', + 'element-plus/es/components/table-column/style/css', + 'element-plus/es/components/card/style/css', + 'element-plus/es/components/row/style/css', + 'element-plus/es/components/button/style/css', + 'element-plus/es/components/menu/style/css', + 'element-plus/es/components/sub-menu/style/css', + 'element-plus/es/components/menu-item/style/css', + 'element-plus/es/components/option/style/css', + 'element-plus/es/components/dropdown/style/css', + 'element-plus/es/components/dropdown-menu/style/css', + 'element-plus/es/components/dropdown-item/style/css', + 'element-plus/es/components/skeleton/style/css', + 'element-plus/es/components/skeleton/style/css', + 'element-plus/es/components/backtop/style/css', + 'element-plus/es/components/menu/style/css', + 'element-plus/es/components/sub-menu/style/css', + 'element-plus/es/components/menu-item/style/css', + 'element-plus/es/components/dropdown/style/css', + 'element-plus/es/components/tree/style/css', + 'element-plus/es/components/dropdown-menu/style/css', + 'element-plus/es/components/dropdown-item/style/css', + 'element-plus/es/components/badge/style/css', + 'element-plus/es/components/breadcrumb/style/css', + 'element-plus/es/components/breadcrumb-item/style/css', + 'element-plus/es/components/image/style/css', + 'element-plus/es/components/collapse-transition/style/css', + 'element-plus/es/components/timeline/style/css', + 'element-plus/es/components/timeline-item/style/css', + 'element-plus/es/components/collapse/style/css', + 'element-plus/es/components/collapse-item/style/css', + 'element-plus/es/components/button-group/style/css', + 'element-plus/es/components/text/style/css' +] + +const exclude = ['@iconify/json'] + +export { include, exclude } diff --git a/mes-ui/mes-ui-admin-vue3/index.html b/mes-ui/mes-ui-admin-vue3/index.html new file mode 100644 index 00000000..f9e01911 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/index.html @@ -0,0 +1,151 @@ + + + + + + + + + + %VITE_APP_TITLE% + + +

+ +
+
+
+ +
%VITE_APP_TITLE%
+
+
+
+
+
+
+
+
+ + + diff --git a/mes-ui/mes-ui-admin-vue3/package.json b/mes-ui/mes-ui-admin-vue3/package.json new file mode 100644 index 00000000..5fd2e6e6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/package.json @@ -0,0 +1,146 @@ +{ + "name": "mes-ui-admin-vue3", + "version": "1.8.3-snapshot", + "description": "基于vue3、vite4、element-plus、typesScript", + "author": "xingyu", + "private": false, + "scripts": { + "i": "pnpm install", + "dev": "vite --mode base", + "front": "vite --mode front", + "ts:check": "vue-tsc --noEmit", + "build:pro": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode pro", + "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev", + "build:base": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode base", + "build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage", + "build:static": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode static", + "build:front": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode front", + "serve:pro": "vite preview --mode pro", + "serve:dev": "vite preview --mode dev", + "preview": "pnpm build:base && vite preview", + "clean": "npx rimraf node_modules", + "clean:cache": "npx rimraf node_modules/.cache", + "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src", + "lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"", + "lint:style": "stylelint --fix \"./src/**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", + "lint:lint-staged": "lint-staged -c " + }, + "dependencies": { + "@element-plus/icons-vue": "^2.1.0", + "@form-create/designer": "^3.1.3", + "@form-create/element-ui": "^3.1.24", + "@iconify/iconify": "^3.1.1", + "@videojs-player/vue": "^1.0.0", + "@vueuse/core": "^10.6.1", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^5.1.10", + "@zxcvbn-ts/core": "^3.0.4", + "animate.css": "^4.1.1", + "axios": "^1.6.1", + "benz-amr-recorder": "^1.1.5", + "bpmn-js-token-simulation": "^0.10.0", + "camunda-bpmn-moddle": "^7.0.1", + "cropperjs": "^1.6.1", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.10", + "diagram-js": "^12.8.0", + "driver.js": "^1.3.1", + "echarts": "^5.4.3", + "echarts-wordcloud": "^2.1.0", + "element-plus": "2.4.2", + "fast-xml-parser": "^4.3.2", + "highlight.js": "^11.9.0", + "jsencrypt": "^3.3.2", + "lodash-es": "^4.17.21", + "min-dash": "^4.1.1", + "mitt": "^3.0.1", + "nprogress": "^0.2.0", + "pinia": "^2.1.7", + "qrcode": "^1.5.3", + "qs": "^6.11.2", + "sortablejs": "^1.15.0", + "steady-xml": "^0.1.0", + "url": "^0.11.3", + "video.js": "^7.21.5", + "vue": "^3.3.8", + "vue-dompurify-html": "^4.1.4", + "vue-i18n": "^9.6.5", + "vue-router": "^4.2.5", + "vue-types": "^5.1.1", + "vuedraggable": "^4.1.0", + "web-storage-cache": "^1.1.1", + "xml-js": "^1.6.11" + }, + "devDependencies": { + "@commitlint/cli": "^18.4.1", + "@commitlint/config-conventional": "^18.4.0", + "@iconify/json": "^2.2.142", + "@intlify/unplugin-vue-i18n": "^1.5.0", + "@purge-icons/generated": "^0.9.0", + "@types/lodash-es": "^4.17.11", + "@types/node": "^20.9.0", + "@types/nprogress": "^0.2.3", + "@types/qrcode": "^1.5.5", + "@types/qs": "^6.9.10", + "@types/sortablejs": "^1.15.5", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "@unocss/transformer-variant-group": "^0.57.4", + "@unocss/eslint-config": "^0.57.4", + "@vitejs/plugin-legacy": "^4.1.1", + "@vitejs/plugin-vue": "^4.4.1", + "@vitejs/plugin-vue-jsx": "^3.0.2", + "autoprefixer": "^10.4.16", + "bpmn-js": "8.9.0", + "bpmn-js-properties-panel": "0.46.0", + "consola": "^3.2.3", + "eslint": "^8.53.0", + "eslint-config-prettier": "^9.0.0", + "eslint-define-config": "^1.24.1", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-vue": "^9.18.1", + "lint-staged": "^15.1.0", + "postcss": "^8.4.31", + "postcss-html": "^1.5.0", + "postcss-scss": "^4.0.9", + "prettier": "^3.1.0", + "rimraf": "^5.0.5", + "rollup": "^4.4.1", + "sass": "^1.69.5", + "stylelint": "^15.11.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-recommended": "^13.0.0", + "stylelint-config-standard": "^34.0.0", + "stylelint-order": "^6.0.3", + "terser": "^5.24.0", + "typescript": "5.2.2", + "unocss": "^0.57.4", + "unplugin-auto-import": "^0.16.7", + "unplugin-element-plus": "^0.8.0", + "unplugin-vue-components": "^0.25.2", + "vite": "4.5.0", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-ejs": "^1.6.4", + "vite-plugin-eslint": "^1.8.1", + "vite-plugin-progress": "^0.0.7", + "vite-plugin-purge-icons": "^0.9.2", + "vite-plugin-svg-icons": "^2.0.1", + "vite-plugin-top-level-await": "^1.3.1", + "vue-eslint-parser": "^9.3.2", + "vue-tsc": "^1.8.22" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://gitee.com/mescode/mes-ui-admin-vue3" + }, + "bugs": { + "url": "https://gitee.com/mescode/mes-ui-admin-vue3/issues" + }, + "homepage": "https://gitee.com/mescode/mes-ui-admin-vue3", + "packageManager": "pnpm@8.6.0", + "engines": { + "node": ">= 16.0.0", + "pnpm": ">=8.6.0" + } +} diff --git a/mes-ui/mes-ui-admin-vue3/postcss.config.js b/mes-ui/mes-ui-admin-vue3/postcss.config.js new file mode 100644 index 00000000..961986e2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {} + } +} diff --git a/mes-ui/mes-ui-admin-vue3/prettier.config.js b/mes-ui/mes-ui-admin-vue3/prettier.config.js new file mode 100644 index 00000000..b014bbf1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/prettier.config.js @@ -0,0 +1,22 @@ +module.exports = { + printWidth: 100, // 每行代码长度(默认80) + tabWidth: 2, // 每个tab相当于多少个空格(默认2)ab进行缩进(默认false) + useTabs: false, // 是否使用tab + semi: false, // 声明结尾使用分号(默认true) + vueIndentScriptAndStyle: false, + singleQuote: true, // 使用单引号(默认false) + quoteProps: 'as-needed', + bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true) + trailingComma: 'none', // 多行使用拖尾逗号(默认none) + jsxSingleQuote: false, + // 箭头函数参数括号 默认avoid 可选 avoid| always + // avoid 能省略括号的时候就省略 例如x => x + // always 总是有括号 + arrowParens: 'always', + insertPragma: false, + requirePragma: false, + proseWrap: 'never', + htmlWhitespaceSensitivity: 'strict', + endOfLine: 'auto', + rangeStart: 0 +} diff --git a/mes-ui/mes-ui-admin-vue3/public/favicon.ico b/mes-ui/mes-ui-admin-vue3/public/favicon.ico new file mode 100644 index 00000000..38621562 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/public/favicon.ico differ diff --git a/mes-ui/mes-ui-admin-vue3/public/home.png b/mes-ui/mes-ui-admin-vue3/public/home.png new file mode 100644 index 00000000..ccd41455 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/public/home.png differ diff --git a/mes-ui/mes-ui-admin-vue3/public/logo.gif b/mes-ui/mes-ui-admin-vue3/public/logo.gif new file mode 100644 index 00000000..fdbd32c6 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/public/logo.gif differ diff --git a/mes-ui/mes-ui-admin-vue3/src/App.vue b/mes-ui/mes-ui-admin-vue3/src/App.vue new file mode 100644 index 00000000..7407d97a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/App.vue @@ -0,0 +1,57 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/api/bpm/activity/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/bpm/activity/index.ts new file mode 100644 index 00000000..870d0d6c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/bpm/activity/index.ts @@ -0,0 +1,8 @@ +import request from '@/config/axios' + +export const getActivityList = async (params) => { + return await request.get({ + url: '/bpm/activity/list', + params + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/bpm/definition/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/bpm/definition/index.ts new file mode 100644 index 00000000..c0e51fab --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/bpm/definition/index.ts @@ -0,0 +1,21 @@ +import request from '@/config/axios' + +export const getProcessDefinitionBpmnXML = async (id: number) => { + return await request.get({ + url: '/bpm/process-definition/get-bpmn-xml?id=' + id + }) +} + +export const getProcessDefinitionPage = async (params) => { + return await request.get({ + url: '/bpm/process-definition/page', + params + }) +} + +export const getProcessDefinitionList = async (params) => { + return await request.get({ + url: '/bpm/process-definition/list', + params + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/bpm/form/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/bpm/form/index.ts new file mode 100644 index 00000000..142ed24c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/bpm/form/index.ts @@ -0,0 +1,56 @@ +import request from '@/config/axios' + +export type FormVO = { + id: number + name: string + conf: string + fields: string[] + status: number + remark: string + createTime: string +} + +// 创建工作流的表单定义 +export const createForm = async (data: FormVO) => { + return await request.post({ + url: '/bpm/form/create', + data: data + }) +} + +// 更新工作流的表单定义 +export const updateForm = async (data: FormVO) => { + return await request.put({ + url: '/bpm/form/update', + data: data + }) +} + +// 删除工作流的表单定义 +export const deleteForm = async (id: number) => { + return await request.delete({ + url: '/bpm/form/delete?id=' + id + }) +} + +// 获得工作流的表单定义 +export const getForm = async (id: number) => { + return await request.get({ + url: '/bpm/form/get?id=' + id + }) +} + +// 获得工作流的表单定义分页 +export const getFormPage = async (params) => { + return await request.get({ + url: '/bpm/form/page', + params + }) +} + +// 获得动态表单的精简列表 +export const getSimpleFormList = async () => { + return await request.get({ + url: '/bpm/form/list-all-simple' + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/bpm/leave/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/bpm/leave/index.ts new file mode 100644 index 00000000..d4fe8d58 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/bpm/leave/index.ts @@ -0,0 +1,27 @@ +import request from '@/config/axios' + +export type LeaveVO = { + id: number + result: number + type: number + reason: string + processInstanceId: string + startTime: string + endTime: string + createTime: string +} + +// 创建请假申请 +export const createLeave = async (data: LeaveVO) => { + return await request.post({ url: '/bpm/oa/leave/create', data: data }) +} + +// 获得请假申请 +export const getLeave = async (id: number) => { + return await request.get({ url: '/bpm/oa/leave/get?id=' + id }) +} + +// 获得请假申请分页 +export const getLeavePage = async (params: PageParam) => { + return await request.get({ url: '/bpm/oa/leave/page', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/bpm/model/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/bpm/model/index.ts new file mode 100644 index 00000000..2e1d4e64 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/bpm/model/index.ts @@ -0,0 +1,59 @@ +import request from '@/config/axios' + +export type ProcessDefinitionVO = { + id: string + version: number + deploymentTIme: string + suspensionState: number +} + +export type ModelVO = { + id: number + formName: string + key: string + name: string + description: string + category: string + formType: number + formId: number + formCustomCreatePath: string + formCustomViewPath: string + processDefinition: ProcessDefinitionVO + status: number + remark: string + createTime: string + bpmnXml: string +} + +export const getModelPage = async (params) => { + return await request.get({ url: '/bpm/model/page', params }) +} + +export const getModel = async (id: number) => { + return await request.get({ url: '/bpm/model/get?id=' + id }) +} + +export const updateModel = async (data: ModelVO) => { + return await request.put({ url: '/bpm/model/update', data: data }) +} + +// 任务状态修改 +export const updateModelState = async (id: number, state: number) => { + const data = { + id: id, + state: state + } + return await request.put({ url: '/bpm/model/update-state', data: data }) +} + +export const createModel = async (data: ModelVO) => { + return await request.post({ url: '/bpm/model/create', data: data }) +} + +export const deleteModel = async (id: number) => { + return await request.delete({ url: '/bpm/model/delete?id=' + id }) +} + +export const deployModel = async (id: number) => { + return await request.post({ url: '/bpm/model/deploy?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/bpm/processInstance/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/bpm/processInstance/index.ts new file mode 100644 index 00000000..10cd3bc8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/bpm/processInstance/index.ts @@ -0,0 +1,41 @@ +import request from '@/config/axios' + +export type Task = { + id: string + name: string +} + +export type ProcessInstanceVO = { + id: number + name: string + processDefinitionId: string + category: string + result: number + tasks: Task[] + fields: string[] + status: number + remark: string + businessKey: string + createTime: string + endTime: string +} + +export const getMyProcessInstancePage = async (params) => { + return await request.get({ url: '/bpm/process-instance/my-page', params }) +} + +export const createProcessInstance = async (data) => { + return await request.post({ url: '/bpm/process-instance/create', data: data }) +} + +export const cancelProcessInstance = async (id: number, reason: string) => { + const data = { + id: id, + reason: reason + } + return await request.delete({ url: '/bpm/process-instance/cancel', data: data }) +} + +export const getProcessInstance = async (id: number) => { + return await request.get({ url: '/bpm/process-instance/get?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/bpm/task/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/bpm/task/index.ts new file mode 100644 index 00000000..df6d8160 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/bpm/task/index.ts @@ -0,0 +1,81 @@ +import request from '@/config/axios' + +export type TaskVO = { + id: number +} + +export const getTodoTaskPage = async (params) => { + return await request.get({ url: '/bpm/task/todo-page', params }) +} + +export const getDoneTaskPage = async (params) => { + return await request.get({ url: '/bpm/task/done-page', params }) +} + +export const completeTask = async (data) => { + return await request.put({ url: '/bpm/task/complete', data }) +} + +export const approveTask = async (data) => { + return await request.put({ url: '/bpm/task/approve', data }) +} + +export const rejectTask = async (data) => { + return await request.put({ url: '/bpm/task/reject', data }) +} +export const backTask = async (data) => { + return await request.put({ url: '/bpm/task/back', data }) +} + +export const updateTaskAssignee = async (data) => { + return await request.put({ url: '/bpm/task/update-assignee', data }) +} + +export const getTaskListByProcessInstanceId = async (processInstanceId) => { + return await request.get({ + url: '/bpm/task/list-by-process-instance-id?processInstanceId=' + processInstanceId + }) +} + +// 导出任务 +export const exportTask = async (params) => { + return await request.download({ url: '/bpm/task/export', params }) +} + +// 获取所有可回退的节点 +export const getReturnList = async (params) => { + return await request.get({ url: '/bpm/task/return-list', params }) +} + +// 回退 +export const returnTask = async (data) => { + return await request.put({ url: '/bpm/task/return', data }) +} + +/** + * 委派 + */ +export const delegateTask = async (data) => { + return await request.put({ url: '/bpm/task/delegate', data }) +} + +/** + * 加签 + */ +export const taskAddSign = async (data) => { + return await request.put({ url: '/bpm/task/create-sign', data }) +} + +/** + * 获取减签任务列表 + */ +export const getChildrenTaskList = async (id: string) => { + return await request.get({ url: '/bpm/task/children-list?taskId=' + id }) +} + +/** + * 减签 + */ +export const taskSubSign = async (data) => { + return await request.delete({ url: '/bpm/task/delete-sign', data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/bpm/taskAssignRule/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/bpm/taskAssignRule/index.ts new file mode 100644 index 00000000..5fbe342d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/bpm/taskAssignRule/index.ts @@ -0,0 +1,29 @@ +import request from '@/config/axios' + +export type TaskAssignVO = { + id: number + modelId: string + processDefinitionId: string + taskDefinitionKey: string + taskDefinitionName: string + options: string[] + type: number +} + +export const getTaskAssignRuleList = async (params) => { + return await request.get({ url: '/bpm/task-assign-rule/list', params }) +} + +export const createTaskAssignRule = async (data: TaskAssignVO) => { + return await request.post({ + url: '/bpm/task-assign-rule/create', + data: data + }) +} + +export const updateTaskAssignRule = async (data: TaskAssignVO) => { + return await request.put({ + url: '/bpm/task-assign-rule/update', + data: data + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/bpm/userGroup/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/bpm/userGroup/index.ts new file mode 100644 index 00000000..035762bf --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/bpm/userGroup/index.ts @@ -0,0 +1,47 @@ +import request from '@/config/axios' + +export type UserGroupVO = { + id: number + name: string + description: string + memberUserIds: number[] + status: number + remark: string + createTime: string +} + +// 创建用户组 +export const createUserGroup = async (data: UserGroupVO) => { + return await request.post({ + url: '/bpm/user-group/create', + data: data + }) +} + +// 更新用户组 +export const updateUserGroup = async (data: UserGroupVO) => { + return await request.put({ + url: '/bpm/user-group/update', + data: data + }) +} + +// 删除用户组 +export const deleteUserGroup = async (id: number) => { + return await request.delete({ url: '/bpm/user-group/delete?id=' + id }) +} + +// 获得用户组 +export const getUserGroup = async (id: number) => { + return await request.get({ url: '/bpm/user-group/get?id=' + id }) +} + +// 获得用户组分页 +export const getUserGroupPage = async (params) => { + return await request.get({ url: '/bpm/user-group/page', params }) +} + +// 获取用户组精简信息列表 +export const getSimpleUserGroupList = async (): Promise => { + return await request.get({ url: '/bpm/user-group/list-all-simple' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/business/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/business/index.ts new file mode 100644 index 00000000..8af2a697 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/business/index.ts @@ -0,0 +1,57 @@ +import request from '@/config/axios' + +export interface BusinessVO { + id: number + name: string + statusTypeId: number + statusId: number + contactNextTime: Date + customerId: number + dealTime: Date + price: number + discountPercent: number + productPrice: number + remark: string + ownerUserId: number + roUserIds: string + rwUserIds: string + endStatus: number + endRemark: string + contactLastTime: Date + followUpStatus: number +} + +// 查询 CRM 商机列表 +export const getBusinessPage = async (params) => { + return await request.get({ url: `/crm/business/page`, params }) +} + +// 查询 CRM 商机列表,基于指定客户 +export const getBusinessPageByCustomer = async (params) => { + return await request.get({ url: `/crm/business/page-by-customer`, params }) +} + +// 查询 CRM 商机详情 +export const getBusiness = async (id: number) => { + return await request.get({ url: `/crm/business/get?id=` + id }) +} + +// 新增 CRM 商机 +export const createBusiness = async (data: BusinessVO) => { + return await request.post({ url: `/crm/business/create`, data }) +} + +// 修改 CRM 商机 +export const updateBusiness = async (data: BusinessVO) => { + return await request.put({ url: `/crm/business/update`, data }) +} + +// 删除 CRM 商机 +export const deleteBusiness = async (id: number) => { + return await request.delete({ url: `/crm/business/delete?id=` + id }) +} + +// 导出 CRM 商机 Excel +export const exportBusiness = async (params) => { + return await request.download({ url: `/crm/business/export-excel`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/businessStatusType/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/businessStatusType/index.ts new file mode 100644 index 00000000..cc4b46aa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/businessStatusType/index.ts @@ -0,0 +1,48 @@ +import request from '@/config/axios' + +export interface BusinessStatusTypeVO { + id: number + name: string + deptIds: number[] + status: boolean +} + +// 查询商机状态类型列表 +export const getBusinessStatusTypePage = async (params) => { + return await request.get({ url: `/crm/business-status-type/page`, params }) +} + +// 查询商机状态类型详情 +export const getBusinessStatusType = async (id: number) => { + return await request.get({ url: `/crm/business-status-type/get?id=` + id }) +} + +// 新增商机状态类型 +export const createBusinessStatusType = async (data: BusinessStatusTypeVO) => { + return await request.post({ url: `/crm/business-status-type/create`, data }) +} + +// 修改商机状态类型 +export const updateBusinessStatusType = async (data: BusinessStatusTypeVO) => { + return await request.put({ url: `/crm/business-status-type/update`, data }) +} + +// 删除商机状态类型 +export const deleteBusinessStatusType = async (id: number) => { + return await request.delete({ url: `/crm/business-status-type/delete?id=` + id }) +} + +// 导出商机状态类型 Excel +export const exportBusinessStatusType = async (params) => { + return await request.download({ url: `/crm/business-status-type/export-excel`, params }) +} + +// 获取商机状态类型信息列表 +export const getBusinessStatusTypeList = async () => { + return await request.get({ url: `/crm/business-status-type/get-simple-list` }) +} + +// 根据类型ID获取商机状态信息列表 +export const getBusinessStatusListByTypeId = async (typeId: number) => { + return await request.get({ url: `/crm/business-status-type/get-status-list?typeId=` + typeId }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/clue/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/clue/index.ts new file mode 100644 index 00000000..39da03d3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/clue/index.ts @@ -0,0 +1,46 @@ +import request from '@/config/axios' + +export interface ClueVO { + id: number + transformStatus: boolean + followUpStatus: boolean + name: string + customerId: number + contactNextTime: Date + telephone: string + mobile: string + address: string + ownerUserId: number + contactLastTime: Date + remark: string +} + +// 查询线索列表 +export const getCluePage = async (params) => { + return await request.get({ url: `/crm/clue/page`, params }) +} + +// 查询线索详情 +export const getClue = async (id: number) => { + return await request.get({ url: `/crm/clue/get?id=` + id }) +} + +// 新增线索 +export const createClue = async (data: ClueVO) => { + return await request.post({ url: `/crm/clue/create`, data }) +} + +// 修改线索 +export const updateClue = async (data: ClueVO) => { + return await request.put({ url: `/crm/clue/update`, data }) +} + +// 删除线索 +export const deleteClue = async (id: number) => { + return await request.delete({ url: `/crm/clue/delete?id=` + id }) +} + +// 导出线索 Excel +export const exportClue = async (params) => { + return await request.download({ url: `/crm/clue/export-excel`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/contact/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/contact/index.ts new file mode 100644 index 00000000..f983cb12 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/contact/index.ts @@ -0,0 +1,67 @@ +import request from '@/config/axios' + +export interface ContactVO { + name: string + nextTime: Date + mobile: string + telephone: string + email: string + post: string + customerId: number + address: string + remark: string + ownerUserId: string + lastTime: Date + id: number + parentId: number + qq: number + wechat: string + sex: number + master: boolean + creatorName: string + updateTime?: Date + createTime?: Date + customerName: string + areaName: string + ownerUserName: string +} + +// 查询 CRM 联系人列表 +export const getContactPage = async (params) => { + return await request.get({ url: `/crm/contact/page`, params }) +} + +// 查询 CRM 联系人列表,基于指定客户 +export const getContactPageByCustomer = async (params: any) => { + return await request.get({ url: `/crm/contact/page-by-customer`, params }) +} + +// 查询 CRM 联系人详情 +export const getContact = async (id: number) => { + return await request.get({ url: `/crm/contact/get?id=` + id }) +} + +// 新增 CRM 联系人 +export const createContact = async (data: ContactVO) => { + return await request.post({ url: `/crm/contact/create`, data }) +} + +// 修改 CRM 联系人 +export const updateContact = async (data: ContactVO) => { + return await request.put({ url: `/crm/contact/update`, data }) +} + +// 删除 CRM 联系人 +export const deleteContact = async (id: number) => { + return await request.delete({ url: `/crm/contact/delete?id=` + id }) +} + +// 导出 CRM 联系人 Excel +export const exportContact = async (params) => { + return await request.download({ url: `/crm/contact/export-excel`, params }) +} + +// 获得 CRM 联系人列表(精简) +export const getSimpleContactList = async () => { + return await request.get({ url: `/crm/contact/simple-all-list` }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/contract/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/contract/index.ts new file mode 100644 index 00000000..3498e843 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/contract/index.ts @@ -0,0 +1,58 @@ +import request from '@/config/axios' + +export interface ContractVO { + id: number + name: string + customerId: number + businessId: number + processInstanceId: number + orderDate: Date + ownerUserId: number + no: string + startTime: Date + endTime: Date + price: number + discountPercent: number + productPrice: number + roUserIds: string + rwUserIds: string + contactId: number + signUserId: number + contactLastTime: Date + remark: string +} + +// 查询 CRM 合同列表 +export const getContractPage = async (params) => { + return await request.get({ url: `/crm/contract/page`, params }) +} + +// 查询 CRM 联系人列表,基于指定客户 +export const getContractPageByCustomer = async (params: any) => { + return await request.get({ url: `/crm/contract/page-by-customer`, params }) +} + +// 查询 CRM 合同详情 +export const getContract = async (id: number) => { + return await request.get({ url: `/crm/contract/get?id=` + id }) +} + +// 新增 CRM 合同 +export const createContract = async (data: ContractVO) => { + return await request.post({ url: `/crm/contract/create`, data }) +} + +// 修改 CRM 合同 +export const updateContract = async (data: ContractVO) => { + return await request.put({ url: `/crm/contract/update`, data }) +} + +// 删除 CRM 合同 +export const deleteContract = async (id: number) => { + return await request.delete({ url: `/crm/contract/delete?id=` + id }) +} + +// 导出 CRM 合同 Excel +export const exportContract = async (params) => { + return await request.download({ url: `/crm/contract/export-excel`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/customer/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/customer/index.ts new file mode 100644 index 00000000..5ef43950 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/customer/index.ts @@ -0,0 +1,69 @@ +import request from '@/config/axios' + +export interface CustomerVO { + id?: number + name: string + industryId: number + level: number + source: number + followUpStatus?: boolean + lockStatus?: boolean + dealStatus?: boolean + mobile: string + telephone: string + website: string + qq: string + wechat: string + email: string + description: string + remark: string + ownerUserId?: number + ownerUserName?: string + ownerUserDept?: string + roUserIds?: string + rwUserIds?: string + areaId?: number + areaName?: string + detailAddress: string + contactLastTime?: Date + contactNextTime: Date + createTime?: Date + updateTime?: Date + creator?: string + creatorName?: string +} + +// 查询客户列表 +export const getCustomerPage = async (params) => { + return await request.get({ url: `/crm/customer/page`, params }) +} + +// 查询客户详情 +export const getCustomer = async (id: number) => { + return await request.get({ url: `/crm/customer/get?id=` + id }) +} + +// 新增客户 +export const createCustomer = async (data: CustomerVO) => { + return await request.post({ url: `/crm/customer/create`, data }) +} + +// 修改客户 +export const updateCustomer = async (data: CustomerVO) => { + return await request.put({ url: `/crm/customer/update`, data }) +} + +// 删除客户 +export const deleteCustomer = async (id: number) => { + return await request.delete({ url: `/crm/customer/delete?id=` + id }) +} + +// 导出客户 Excel +export const exportCustomer = async (params) => { + return await request.download({ url: `/crm/customer/export-excel`, params }) +} + +// 客户列表 +export const queryAllList = async () => { + return await request.get({ url: `/crm/customer/query-all-list` }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/customerLimitConfig/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/customerLimitConfig/index.ts new file mode 100644 index 00000000..86776326 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/customerLimitConfig/index.ts @@ -0,0 +1,49 @@ +import request from '@/config/axios' + +export interface CustomerLimitConfigVO { + id?: number + type?: number + userIds?: string + deptIds?: string + maxCount?: number + dealCountEnabled?: boolean +} + +/** + * 客户限制配置类型 + */ +export enum LimitConfType { + /** + * 拥有客户数限制 + */ + CUSTOMER_QUANTITY_LIMIT = 1, + /** + * 锁定客户数限制 + */ + CUSTOMER_LOCK_LIMIT = 2 +} + +// 查询客户限制配置列表 +export const getCustomerLimitConfigPage = async (params) => { + return await request.get({ url: `/crm/customer-limit-config/page`, params }) +} + +// 查询客户限制配置详情 +export const getCustomerLimitConfig = async (id: number) => { + return await request.get({ url: `/crm/customer-limit-config/get?id=` + id }) +} + +// 新增客户限制配置 +export const createCustomerLimitConfig = async (data: CustomerLimitConfigVO) => { + return await request.post({ url: `/crm/customer-limit-config/create`, data }) +} + +// 修改客户限制配置 +export const updateCustomerLimitConfig = async (data: CustomerLimitConfigVO) => { + return await request.put({ url: `/crm/customer-limit-config/update`, data }) +} + +// 删除客户限制配置 +export const deleteCustomerLimitConfig = async (id: number) => { + return await request.delete({ url: `/crm/customer-limit-config/delete?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/customerPoolConfig/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/customerPoolConfig/index.ts new file mode 100644 index 00000000..3cd8ef28 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/customerPoolConfig/index.ts @@ -0,0 +1,20 @@ +import request from '@/config/axios' +import { ConfigVO } from '@/api/infra/config' + +export interface CustomerPoolConfigVO { + enabled?: boolean + contactExpireDays?: number + dealExpireDays?: number + notifyEnabled?: boolean + notifyDays: number +} + +// 获取客户公海规则设置 +export const getCustomerPoolConfig = async () => { + return await request.get({ url: `/crm/customer-pool-config/get` }) +} + +// 更新客户公海规则设置 +export const saveCustomerPoolConfig = async (data: ConfigVO) => { + return await request.put({ url: `/crm/customer-pool-config/save`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/permission/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/permission/index.ts new file mode 100644 index 00000000..c221b089 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/permission/index.ts @@ -0,0 +1,72 @@ +import request from '@/config/axios' + +export interface PermissionVO { + id?: number // 数据权限编号 + userId: number | undefined // 用户编号 + bizType: number | undefined // Crm 类型 + bizId: number | undefined // Crm 类型数据编号 + level: number | undefined // 权限级别 + deptName?: string // 部门名称 + nickname?: string // 用户昵称 + postNames?: string[] // 岗位名称数组 + createTime?: Date +} + +/** + * CRM 业务类型枚举 + * + * @author HUIHUI + */ +export enum BizTypeEnum { + CRM_LEADS = 1, // 线索 + CRM_CUSTOMER = 2, // 客户 + CRM_CONTACT = 3, // 联系人 + CRM_BUSINESS = 5, // 商机 + CRM_CONTRACT = 6 // 合同 +} + +/** + * CRM 数据权限级别枚举 + */ +export enum PermissionLevelEnum { + OWNER = 1, // 负责人 + READ = 2, // 只读 + WRITE = 3 // 读写 +} + +// 获得数据权限列表(查询团队成员列表) +export const getPermissionList = async (params) => { + return await request.get({ url: `/crm/permission/list`, params }) +} + +// 创建数据权限(新增团队成员) +export const createPermission = async (data: PermissionVO) => { + return await request.post({ url: `/crm/permission/create`, data }) +} + +// 编辑数据权限(修改团队成员权限级别) +export const updatePermission = async (data) => { + return await request.put({ url: `/crm/permission/update`, data }) +} + +// 删除数据权限(删除团队成员) +export const deletePermissionBatch = async (params) => { + return await request.delete({ url: '/crm/permission/delete', params }) +} + +// 删除自己的数据权限(退出团队) +export const deleteSelfPermission = async (id) => { + return await request.delete({ url: '/crm/permission/quit-team?id=' + id }) +} + +// TODO @puhui999:调整下位置 +// 领取公海数据 +export const receive = async (data: { bizType: number; bizId: number }) => { + return await request.put({ url: `/crm/permission/receive`, data }) +} + +// TODO @puhui999:调整下位置 +// 数据放入公海 +export const putPool = async (data: { bizType: number; bizId: number }) => { + return await request.put({ url: `/crm/permission/put-pool`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/product/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/product/index.ts new file mode 100644 index 00000000..cb1ddcda --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/product/index.ts @@ -0,0 +1,43 @@ +import request from '@/config/axios' + +export interface ProductVO { + id: number + name: string + no: string + unit: string + price: number + status: number + categoryId: number + description: string + ownerUserId: number +} + +// 查询产品列表 +export const getProductPage = async (params) => { + return await request.get({ url: `/crm/product/page`, params }) +} + +// 查询产品详情 +export const getProduct = async (id: number) => { + return await request.get({ url: `/crm/product/get?id=` + id }) +} + +// 新增产品 +export const createProduct = async (data: ProductVO) => { + return await request.post({ url: `/crm/product/create`, data }) +} + +// 修改产品 +export const updateProduct = async (data: ProductVO) => { + return await request.put({ url: `/crm/product/update`, data }) +} + +// 删除产品 +export const deleteProduct = async (id: number) => { + return await request.delete({ url: `/crm/product/delete?id=` + id }) +} + +// 导出产品 Excel +export const exportProduct = async (params) => { + return await request.download({ url: `/crm/product/export-excel`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/product/productCategory/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/product/productCategory/index.ts new file mode 100644 index 00000000..6341d1bc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/product/productCategory/index.ts @@ -0,0 +1,33 @@ +import request from '@/config/axios' + +// TODO @zange:挪到 product 下,建个 category 包,挪进去哈; +export interface ProductCategoryVO { + id: number + name: string + parentId: number +} + +// 查询产品分类详情 +export const getProductCategory = async (id: number) => { + return await request.get({ url: `/crm/product-category/get?id=` + id }) +} + +// 新增产品分类 +export const createProductCategory = async (data: ProductCategoryVO) => { + return await request.post({ url: `/crm/product-category/create`, data }) +} + +// 修改产品分类 +export const updateProductCategory = async (data: ProductCategoryVO) => { + return await request.put({ url: `/crm/product-category/update`, data }) +} + +// 删除产品分类 +export const deleteProductCategory = async (id: number) => { + return await request.delete({ url: `/crm/product-category/delete?id=` + id }) +} + +// 产品分类列表 +export const getProductCategoryList = async (params) => { + return await request.get({ url: `/crm/product-category/list`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/receivable/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/receivable/index.ts new file mode 100644 index 00000000..a9812a76 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/receivable/index.ts @@ -0,0 +1,52 @@ +import request from '@/config/axios' + +export interface ReceivableVO { + id: number + no: string + planId: number + customerId: number + contractId: number + auditStatus: number + processInstanceId: number + returnTime: Date + returnType: string + price: number + ownerUserId: number + sort: number + remark: string +} + +// 查询回款列表 +export const getReceivablePage = async (params) => { + return await request.get({ url: `/crm/receivable/page`, params }) +} + +// 查询回款列表 +export const getReceivablePageByCustomer = async (params) => { + return await request.get({ url: `/crm/receivable/page-by-customer`, params }) +} + +// 查询回款详情 +export const getReceivable = async (id: number) => { + return await request.get({ url: `/crm/receivable/get?id=` + id }) +} + +// 新增回款 +export const createReceivable = async (data: ReceivableVO) => { + return await request.post({ url: `/crm/receivable/create`, data }) +} + +// 修改回款 +export const updateReceivable = async (data: ReceivableVO) => { + return await request.put({ url: `/crm/receivable/update`, data }) +} + +// 删除回款 +export const deleteReceivable = async (id: number) => { + return await request.delete({ url: `/crm/receivable/delete?id=` + id }) +} + +// 导出回款 Excel +export const exportReceivable = async (params) => { + return await request.download({ url: `/crm/receivable/export-excel`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/crm/receivable/plan/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/crm/receivable/plan/index.ts new file mode 100644 index 00000000..3ddbd7db --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/crm/receivable/plan/index.ts @@ -0,0 +1,54 @@ +import request from '@/config/axios' + +export interface ReceivablePlanVO { + id: number + period: number + receivableId: number + status: number + checkStatus: string + processInstanceId: number + price: number + returnTime: Date + remindDays: number + remindTime: Date + customerId: number + contractId: number + ownerUserId: number + sort: number + remark: string +} + +// 查询回款计划列表 +export const getReceivablePlanPage = async (params) => { + return await request.get({ url: `/crm/receivable-plan/page`, params }) +} + +// 查询回款计划列表 +export const getReceivablePlanPageByCustomer = async (params) => { + return await request.get({ url: `/crm/receivable-plan/page-by-customer`, params }) +} + +// 查询回款计划详情 +export const getReceivablePlan = async (id: number) => { + return await request.get({ url: `/crm/receivable-plan/get?id=` + id }) +} + +// 新增回款计划 +export const createReceivablePlan = async (data: ReceivablePlanVO) => { + return await request.post({ url: `/crm/receivable-plan/create`, data }) +} + +// 修改回款计划 +export const updateReceivablePlan = async (data: ReceivablePlanVO) => { + return await request.put({ url: `/crm/receivable-plan/update`, data }) +} + +// 删除回款计划 +export const deleteReceivablePlan = async (id: number) => { + return await request.delete({ url: `/crm/receivable-plan/delete?id=` + id }) +} + +// 导出回款计划 Excel +export const exportReceivablePlan = async (params) => { + return await request.download({ url: `/crm/receivable-plan/export-excel`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/apiAccessLog/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/apiAccessLog/index.ts new file mode 100644 index 00000000..c6b4b45f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/apiAccessLog/index.ts @@ -0,0 +1,30 @@ +import request from '@/config/axios' + +export interface ApiAccessLogVO { + id: number + traceId: string + userId: number + userType: number + applicationName: string + requestMethod: string + requestParams: string + requestUrl: string + userIp: string + userAgent: string + beginTime: Date + endTIme: Date + duration: number + resultCode: number + resultMsg: string + createTime: Date +} + +// 查询列表API 访问日志 +export const getApiAccessLogPage = (params: PageParam) => { + return request.get({ url: '/infra/api-access-log/page', params }) +} + +// 导出API 访问日志 +export const exportApiAccessLog = (params) => { + return request.download({ url: '/infra/api-access-log/export-excel', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/apiErrorLog/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/apiErrorLog/index.ts new file mode 100644 index 00000000..59ee2143 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/apiErrorLog/index.ts @@ -0,0 +1,48 @@ +import request from '@/config/axios' + +export interface ApiErrorLogVO { + id: number + traceId: string + userId: number + userType: number + applicationName: string + requestMethod: string + requestParams: string + requestUrl: string + userIp: string + userAgent: string + exceptionTime: Date + exceptionName: string + exceptionMessage: string + exceptionRootCauseMessage: string + exceptionStackTrace: string + exceptionClassName: string + exceptionFileName: string + exceptionMethodName: string + exceptionLineNumber: number + processUserId: number + processStatus: number + processTime: Date + resultCode: number + createTime: Date +} + +// 查询列表API 访问日志 +export const getApiErrorLogPage = (params: PageParam) => { + return request.get({ url: '/infra/api-error-log/page', params }) +} + +// 更新 API 错误日志的处理状态 +export const updateApiErrorLogPage = (id: number, processStatus: number) => { + return request.put({ + url: '/infra/api-error-log/update-status?id=' + id + '&processStatus=' + processStatus + }) +} + +// 导出API 访问日志 +export const exportApiErrorLog = (params) => { + return request.download({ + url: '/infra/api-error-log/export-excel', + params + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/codegen/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/codegen/index.ts new file mode 100644 index 00000000..feff57a2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/codegen/index.ts @@ -0,0 +1,123 @@ +import request from '@/config/axios' + +export type CodegenTableVO = { + id: number + tableId: number + isParentMenuIdValid: boolean + dataSourceConfigId: number + scene: number + tableName: string + tableComment: string + remark: string + moduleName: string + businessName: string + className: string + classComment: string + author: string + createTime: Date + updateTime: Date + templateType: number + parentMenuId: number +} + +export type CodegenColumnVO = { + id: number + tableId: number + columnName: string + dataType: string + columnComment: string + nullable: number + primaryKey: number + autoIncrement: boolean + ordinalPosition: number + javaType: string + javaField: string + dictType: string + example: string + createOperation: number + updateOperation: number + listOperation: number + listOperationCondition: string + listOperationResult: number + htmlType: string +} + +export type DatabaseTableVO = { + name: string + comment: string +} + +export type CodegenDetailVO = { + table: CodegenTableVO + columns: CodegenColumnVO[] +} + +export type CodegenPreviewVO = { + filePath: string + code: string +} + +export type CodegenUpdateReqVO = { + table: CodegenTableVO | any + columns: CodegenColumnVO[] +} + +export type CodegenCreateListReqVO = { + dataSourceConfigId: number + tableNames: string[] +} + +// 查询列表代码生成表定义 +export const getCodegenTableList = (dataSourceConfigId: number) => { + return request.get({ url: '/infra/codegen/table/list?dataSourceConfigId=' + dataSourceConfigId }) +} + +// 查询列表代码生成表定义 +export const getCodegenTablePage = (params: PageParam) => { + return request.get({ url: '/infra/codegen/table/page', params }) +} + +// 查询详情代码生成表定义 +export const getCodegenTable = (id: number) => { + return request.get({ url: '/infra/codegen/detail?tableId=' + id }) +} + +// 新增代码生成表定义 +export const createCodegenTable = (data: CodegenCreateListReqVO) => { + return request.post({ url: '/infra/codegen/create', data }) +} + +// 修改代码生成表定义 +export const updateCodegenTable = (data: CodegenUpdateReqVO) => { + return request.put({ url: '/infra/codegen/update', data }) +} + +// 基于数据库的表结构,同步数据库的表和字段定义 +export const syncCodegenFromDB = (id: number) => { + return request.put({ url: '/infra/codegen/sync-from-db?tableId=' + id }) +} + +// 预览生成代码 +export const previewCodegen = (id: number) => { + return request.get({ url: '/infra/codegen/preview?tableId=' + id }) +} + +// 下载生成代码 +export const downloadCodegen = (id: number) => { + return request.download({ url: '/infra/codegen/download?tableId=' + id }) +} + +// 获得表定义 +export const getSchemaTableList = (params) => { + return request.get({ url: '/infra/codegen/db/table/list', params }) +} + +// 基于数据库的表结构,创建代码生成器的表定义 +export const createCodegenList = (data) => { + return request.post({ url: '/infra/codegen/create-list', data }) +} + +// 删除代码生成表定义 +export const deleteCodegenTable = (id: number) => { + return request.delete({ url: '/infra/codegen/delete?tableId=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/config/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/config/index.ts new file mode 100644 index 00000000..5ef59f33 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/config/index.ts @@ -0,0 +1,48 @@ +import request from '@/config/axios' + +export interface ConfigVO { + id: number | undefined + category: string + name: string + key: string + value: string + type: number + visible: boolean + remark: string + createTime: Date +} + +// 查询参数列表 +export const getConfigPage = (params: PageParam) => { + return request.get({ url: '/infra/config/page', params }) +} + +// 查询参数详情 +export const getConfig = (id: number) => { + return request.get({ url: '/infra/config/get?id=' + id }) +} + +// 根据参数键名查询参数值 +export const getConfigKey = (configKey: string) => { + return request.get({ url: '/infra/config/get-value-by-key?key=' + configKey }) +} + +// 新增参数 +export const createConfig = (data: ConfigVO) => { + return request.post({ url: '/infra/config/create', data }) +} + +// 修改参数 +export const updateConfig = (data: ConfigVO) => { + return request.put({ url: '/infra/config/update', data }) +} + +// 删除参数 +export const deleteConfig = (id: number) => { + return request.delete({ url: '/infra/config/delete?id=' + id }) +} + +// 导出参数 +export const exportConfig = (params) => { + return request.download({ url: '/infra/config/export', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/dataSourceConfig/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/dataSourceConfig/index.ts new file mode 100644 index 00000000..b413f345 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/dataSourceConfig/index.ts @@ -0,0 +1,35 @@ +import request from '@/config/axios' + +export interface DataSourceConfigVO { + id: number | undefined + name: string + url: string + username: string + password: string + createTime?: Date +} + +// 新增数据源配置 +export const createDataSourceConfig = (data: DataSourceConfigVO) => { + return request.post({ url: '/infra/data-source-config/create', data }) +} + +// 修改数据源配置 +export const updateDataSourceConfig = (data: DataSourceConfigVO) => { + return request.put({ url: '/infra/data-source-config/update', data }) +} + +// 删除数据源配置 +export const deleteDataSourceConfig = (id: number) => { + return request.delete({ url: '/infra/data-source-config/delete?id=' + id }) +} + +// 查询数据源配置详情 +export const getDataSourceConfig = (id: number) => { + return request.get({ url: '/infra/data-source-config/get?id=' + id }) +} + +// 查询数据源配置列表 +export const getDataSourceConfigList = () => { + return request.get({ url: '/infra/data-source-config/list' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/dbDoc/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/dbDoc/index.ts new file mode 100644 index 00000000..1a1a36b4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/dbDoc/index.ts @@ -0,0 +1,16 @@ +import request from '@/config/axios' + +// 导出Html +export const exportHtml = () => { + return request.download({ url: '/infra/db-doc/export-html' }) +} + +// 导出Word +export const exportWord = () => { + return request.download({ url: '/infra/db-doc/export-word' }) +} + +// 导出Markdown +export const exportMarkdown = () => { + return request.download({ url: '/infra/db-doc/export-markdown' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo01/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo01/index.ts new file mode 100644 index 00000000..e34a05d1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo01/index.ts @@ -0,0 +1,40 @@ +import request from '@/config/axios' + +export interface Demo01ContactVO { + id: number + name: string + sex: number + birthday: Date + description: string + avatar: string +} + +// 查询示例联系人分页 +export const getDemo01ContactPage = async (params) => { + return await request.get({ url: `/infra/demo01-contact/page`, params }) +} + +// 查询示例联系人详情 +export const getDemo01Contact = async (id: number) => { + return await request.get({ url: `/infra/demo01-contact/get?id=` + id }) +} + +// 新增示例联系人 +export const createDemo01Contact = async (data: Demo01ContactVO) => { + return await request.post({ url: `/infra/demo01-contact/create`, data }) +} + +// 修改示例联系人 +export const updateDemo01Contact = async (data: Demo01ContactVO) => { + return await request.put({ url: `/infra/demo01-contact/update`, data }) +} + +// 删除示例联系人 +export const deleteDemo01Contact = async (id: number) => { + return await request.delete({ url: `/infra/demo01-contact/delete?id=` + id }) +} + +// 导出示例联系人 Excel +export const exportDemo01Contact = async (params) => { + return await request.download({ url: `/infra/demo01-contact/export-excel`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo02/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo02/index.ts new file mode 100644 index 00000000..30e16863 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo02/index.ts @@ -0,0 +1,37 @@ +import request from '@/config/axios' + +export interface Demo02CategoryVO { + id: number + name: string + parentId: number +} + +// 查询示例分类列表 +export const getDemo02CategoryList = async (params) => { + return await request.get({ url: `/infra/demo02-category/list`, params }) +} + +// 查询示例分类详情 +export const getDemo02Category = async (id: number) => { + return await request.get({ url: `/infra/demo02-category/get?id=` + id }) +} + +// 新增示例分类 +export const createDemo02Category = async (data: Demo02CategoryVO) => { + return await request.post({ url: `/infra/demo02-category/create`, data }) +} + +// 修改示例分类 +export const updateDemo02Category = async (data: Demo02CategoryVO) => { + return await request.put({ url: `/infra/demo02-category/update`, data }) +} + +// 删除示例分类 +export const deleteDemo02Category = async (id: number) => { + return await request.delete({ url: `/infra/demo02-category/delete?id=` + id }) +} + +// 导出示例分类 Excel +export const exportDemo02Category = async (params) => { + return await request.download({ url: `/infra/demo02-category/export-excel`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo03/erp/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo03/erp/index.ts new file mode 100644 index 00000000..a2ab5398 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo03/erp/index.ts @@ -0,0 +1,91 @@ +import request from '@/config/axios' + +export interface Demo03StudentVO { + id: number + name: string + sex: number + birthday: Date + description: string +} + +// 查询学生分页 +export const getDemo03StudentPage = async (params) => { + return await request.get({ url: `/infra/demo03-student/page`, params }) +} + +// 查询学生详情 +export const getDemo03Student = async (id: number) => { + return await request.get({ url: `/infra/demo03-student/get?id=` + id }) +} + +// 新增学生 +export const createDemo03Student = async (data: Demo03StudentVO) => { + return await request.post({ url: `/infra/demo03-student/create`, data }) +} + +// 修改学生 +export const updateDemo03Student = async (data: Demo03StudentVO) => { + return await request.put({ url: `/infra/demo03-student/update`, data }) +} + +// 删除学生 +export const deleteDemo03Student = async (id: number) => { + return await request.delete({ url: `/infra/demo03-student/delete?id=` + id }) +} + +// 导出学生 Excel +export const exportDemo03Student = async (params) => { + return await request.download({ url: `/infra/demo03-student/export-excel`, params }) +} + +// ==================== 子表(学生课程) ==================== + +// 获得学生课程分页 +export const getDemo03CoursePage = async (params) => { + return await request.get({ url: `/infra/demo03-student/demo03-course/page`, params }) +} +// 新增学生课程 +export const createDemo03Course = async (data) => { + return await request.post({ url: `/infra/demo03-student/demo03-course/create`, data }) +} + +// 修改学生课程 +export const updateDemo03Course = async (data) => { + return await request.put({ url: `/infra/demo03-student/demo03-course/update`, data }) +} + +// 删除学生课程 +export const deleteDemo03Course = async (id: number) => { + return await request.delete({ url: `/infra/demo03-student/demo03-course/delete?id=` + id }) +} + +// 获得学生课程 +export const getDemo03Course = async (id: number) => { + return await request.get({ url: `/infra/demo03-student/demo03-course/get?id=` + id }) +} + +// ==================== 子表(学生班级) ==================== + +// 获得学生班级分页 +export const getDemo03GradePage = async (params) => { + return await request.get({ url: `/infra/demo03-student/demo03-grade/page`, params }) +} +// 新增学生班级 +export const createDemo03Grade = async (data) => { + return await request.post({ url: `/infra/demo03-student/demo03-grade/create`, data }) +} + +// 修改学生班级 +export const updateDemo03Grade = async (data) => { + return await request.put({ url: `/infra/demo03-student/demo03-grade/update`, data }) +} + +// 删除学生班级 +export const deleteDemo03Grade = async (id: number) => { + return await request.delete({ url: `/infra/demo03-student/demo03-grade/delete?id=` + id }) +} + +// 获得学生班级 +export const getDemo03Grade = async (id: number) => { + return await request.get({ url: `/infra/demo03-student/demo03-grade/get?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo03/inner/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo03/inner/index.ts new file mode 100644 index 00000000..e3663078 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo03/inner/index.ts @@ -0,0 +1,57 @@ +import request from '@/config/axios' + +export interface Demo03StudentVO { + id: number + name: string + sex: number + birthday: Date + description: string +} + +// 查询学生分页 +export const getDemo03StudentPage = async (params) => { + return await request.get({ url: `/infra/demo03-student/page`, params }) +} + +// 查询学生详情 +export const getDemo03Student = async (id: number) => { + return await request.get({ url: `/infra/demo03-student/get?id=` + id }) +} + +// 新增学生 +export const createDemo03Student = async (data: Demo03StudentVO) => { + return await request.post({ url: `/infra/demo03-student/create`, data }) +} + +// 修改学生 +export const updateDemo03Student = async (data: Demo03StudentVO) => { + return await request.put({ url: `/infra/demo03-student/update`, data }) +} + +// 删除学生 +export const deleteDemo03Student = async (id: number) => { + return await request.delete({ url: `/infra/demo03-student/delete?id=` + id }) +} + +// 导出学生 Excel +export const exportDemo03Student = async (params) => { + return await request.download({ url: `/infra/demo03-student/export-excel`, params }) +} + +// ==================== 子表(学生课程) ==================== + +// 获得学生课程列表 +export const getDemo03CourseListByStudentId = async (studentId) => { + return await request.get({ + url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId + }) +} + +// ==================== 子表(学生班级) ==================== + +// 获得学生班级 +export const getDemo03GradeByStudentId = async (studentId) => { + return await request.get({ + url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo03/normal/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo03/normal/index.ts new file mode 100644 index 00000000..e3663078 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/demo/demo03/normal/index.ts @@ -0,0 +1,57 @@ +import request from '@/config/axios' + +export interface Demo03StudentVO { + id: number + name: string + sex: number + birthday: Date + description: string +} + +// 查询学生分页 +export const getDemo03StudentPage = async (params) => { + return await request.get({ url: `/infra/demo03-student/page`, params }) +} + +// 查询学生详情 +export const getDemo03Student = async (id: number) => { + return await request.get({ url: `/infra/demo03-student/get?id=` + id }) +} + +// 新增学生 +export const createDemo03Student = async (data: Demo03StudentVO) => { + return await request.post({ url: `/infra/demo03-student/create`, data }) +} + +// 修改学生 +export const updateDemo03Student = async (data: Demo03StudentVO) => { + return await request.put({ url: `/infra/demo03-student/update`, data }) +} + +// 删除学生 +export const deleteDemo03Student = async (id: number) => { + return await request.delete({ url: `/infra/demo03-student/delete?id=` + id }) +} + +// 导出学生 Excel +export const exportDemo03Student = async (params) => { + return await request.download({ url: `/infra/demo03-student/export-excel`, params }) +} + +// ==================== 子表(学生课程) ==================== + +// 获得学生课程列表 +export const getDemo03CourseListByStudentId = async (studentId) => { + return await request.get({ + url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId + }) +} + +// ==================== 子表(学生班级) ==================== + +// 获得学生班级 +export const getDemo03GradeByStudentId = async (studentId) => { + return await request.get({ + url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/file/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/file/index.ts new file mode 100644 index 00000000..f64bc0d6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/file/index.ts @@ -0,0 +1,17 @@ +import request from '@/config/axios' + +export interface FilePageReqVO extends PageParam { + path?: string + type?: string + createTime?: Date[] +} + +// 查询文件列表 +export const getFilePage = (params: FilePageReqVO) => { + return request.get({ url: '/infra/file/page', params }) +} + +// 删除文件 +export const deleteFile = (id: number) => { + return request.delete({ url: '/infra/file/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/fileConfig/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/fileConfig/index.ts new file mode 100644 index 00000000..ba400542 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/fileConfig/index.ts @@ -0,0 +1,61 @@ +import request from '@/config/axios' + +export interface FileClientConfig { + basePath: string + host?: string + port?: number + username?: string + password?: string + mode?: string + endpoint?: string + bucket?: string + accessKey?: string + accessSecret?: string + domain: string +} + +export interface FileConfigVO { + id: number + name: string + storage?: number + master: boolean + visible: boolean + config: FileClientConfig + remark: string + createTime: Date +} + +// 查询文件配置列表 +export const getFileConfigPage = (params: PageParam) => { + return request.get({ url: '/infra/file-config/page', params }) +} + +// 查询文件配置详情 +export const getFileConfig = (id: number) => { + return request.get({ url: '/infra/file-config/get?id=' + id }) +} + +// 更新文件配置为主配置 +export const updateFileConfigMaster = (id: number) => { + return request.put({ url: '/infra/file-config/update-master?id=' + id }) +} + +// 新增文件配置 +export const createFileConfig = (data: FileConfigVO) => { + return request.post({ url: '/infra/file-config/create', data }) +} + +// 修改文件配置 +export const updateFileConfig = (data: FileConfigVO) => { + return request.put({ url: '/infra/file-config/update', data }) +} + +// 删除文件配置 +export const deleteFileConfig = (id: number) => { + return request.delete({ url: '/infra/file-config/delete?id=' + id }) +} + +// 测试文件配置 +export const testFileConfig = (id: number) => { + return request.get({ url: '/infra/file-config/test?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/job/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/job/index.ts new file mode 100644 index 00000000..033b2cbe --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/job/index.ts @@ -0,0 +1,63 @@ +import request from '@/config/axios' + +export interface JobVO { + id: number + name: string + status: number + handlerName: string + handlerParam: string + cronExpression: string + retryCount: number + retryInterval: number + monitorTimeout: number + createTime: Date +} + +// 任务列表 +export const getJobPage = (params: PageParam) => { + return request.get({ url: '/infra/job/page', params }) +} + +// 任务详情 +export const getJob = (id: number) => { + return request.get({ url: '/infra/job/get?id=' + id }) +} + +// 新增任务 +export const createJob = (data: JobVO) => { + return request.post({ url: '/infra/job/create', data }) +} + +// 修改定时任务调度 +export const updateJob = (data: JobVO) => { + return request.put({ url: '/infra/job/update', data }) +} + +// 删除定时任务调度 +export const deleteJob = (id: number) => { + return request.delete({ url: '/infra/job/delete?id=' + id }) +} + +// 导出定时任务调度 +export const exportJob = (params) => { + return request.download({ url: '/infra/job/export-excel', params }) +} + +// 任务状态修改 +export const updateJobStatus = (id: number, status: number) => { + const params = { + id, + status + } + return request.put({ url: '/infra/job/update-status', params }) +} + +// 定时任务立即执行一次 +export const runJob = (id: number) => { + return request.put({ url: '/infra/job/trigger?id=' + id }) +} + +// 获得定时任务的下 n 次执行时间 +export const getJobNextTimes = (id: number) => { + return request.get({ url: '/infra/job/get_next_times?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/jobLog/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/jobLog/index.ts new file mode 100644 index 00000000..dc80f1d9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/jobLog/index.ts @@ -0,0 +1,33 @@ +import request from '@/config/axios' + +export interface JobLogVO { + id: number + jobId: number + handlerName: string + handlerParam: string + cronExpression: string + executeIndex: string + beginTime: Date + endTime: Date + duration: string + status: number + createTime: string +} + +// 任务日志列表 +export const getJobLogPage = (params: PageParam) => { + return request.get({ url: '/infra/job-log/page', params }) +} + +// 任务日志详情 +export const getJobLog = (id: number) => { + return request.get({ url: '/infra/job-log/get?id=' + id }) +} + +// 导出定时任务日志 +export const exportJobLog = (params) => { + return request.download({ + url: '/infra/job-log/export-excel', + params + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/redis/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/redis/index.ts new file mode 100644 index 00000000..f27be77f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/redis/index.ts @@ -0,0 +1,8 @@ +import request from '@/config/axios' + +/** + * 获取redis 监控信息 + */ +export const getCache = () => { + return request.get({ url: '/infra/redis/get-monitor-info' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/infra/redis/types.ts b/mes-ui/mes-ui-admin-vue3/src/api/infra/redis/types.ts new file mode 100644 index 00000000..548bfe96 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/infra/redis/types.ts @@ -0,0 +1,176 @@ +export interface RedisMonitorInfoVO { + info: RedisInfoVO + dbSize: number + commandStats: RedisCommandStatsVO[] +} + +export interface RedisInfoVO { + io_threaded_reads_processed: string + tracking_clients: string + uptime_in_seconds: string + cluster_connections: string + current_cow_size: string + maxmemory_human: string + aof_last_cow_size: string + master_replid2: string + mem_replication_backlog: string + aof_rewrite_scheduled: string + total_net_input_bytes: string + rss_overhead_ratio: string + hz: string + current_cow_size_age: string + redis_build_id: string + errorstat_BUSYGROUP: string + aof_last_bgrewrite_status: string + multiplexing_api: string + client_recent_max_output_buffer: string + allocator_resident: string + mem_fragmentation_bytes: string + aof_current_size: string + repl_backlog_first_byte_offset: string + tracking_total_prefixes: string + redis_mode: string + redis_git_dirty: string + aof_delayed_fsync: string + allocator_rss_bytes: string + repl_backlog_histlen: string + io_threads_active: string + rss_overhead_bytes: string + total_system_memory: string + loading: string + evicted_keys: string + maxclients: string + cluster_enabled: string + redis_version: string + repl_backlog_active: string + mem_aof_buffer: string + allocator_frag_bytes: string + io_threaded_writes_processed: string + instantaneous_ops_per_sec: string + used_memory_human: string + total_error_replies: string + role: string + maxmemory: string + used_memory_lua: string + rdb_current_bgsave_time_sec: string + used_memory_startup: string + used_cpu_sys_main_thread: string + lazyfree_pending_objects: string + aof_pending_bio_fsync: string + used_memory_dataset_perc: string + allocator_frag_ratio: string + arch_bits: string + used_cpu_user_main_thread: string + mem_clients_normal: string + expired_time_cap_reached_count: string + unexpected_error_replies: string + mem_fragmentation_ratio: string + aof_last_rewrite_time_sec: string + master_replid: string + aof_rewrite_in_progress: string + lru_clock: string + maxmemory_policy: string + run_id: string + latest_fork_usec: string + tracking_total_items: string + total_commands_processed: string + expired_keys: string + errorstat_ERR: string + used_memory: string + module_fork_in_progress: string + errorstat_WRONGPASS: string + aof_buffer_length: string + dump_payload_sanitizations: string + mem_clients_slaves: string + keyspace_misses: string + server_time_usec: string + executable: string + lazyfreed_objects: string + db0: string + used_memory_peak_human: string + keyspace_hits: string + rdb_last_cow_size: string + aof_pending_rewrite: string + used_memory_overhead: string + active_defrag_hits: string + tcp_port: string + uptime_in_days: string + used_memory_peak_perc: string + current_save_keys_processed: string + blocked_clients: string + total_reads_processed: string + expire_cycle_cpu_milliseconds: string + sync_partial_err: string + used_memory_scripts_human: string + aof_current_rewrite_time_sec: string + aof_enabled: string + process_supervised: string + master_repl_offset: string + used_memory_dataset: string + used_cpu_user: string + rdb_last_bgsave_status: string + tracking_total_keys: string + atomicvar_api: string + allocator_rss_ratio: string + client_recent_max_input_buffer: string + clients_in_timeout_table: string + aof_last_write_status: string + mem_allocator: string + used_memory_scripts: string + used_memory_peak: string + process_id: string + master_failover_state: string + errorstat_NOAUTH: string + used_cpu_sys: string + repl_backlog_size: string + connected_slaves: string + current_save_keys_total: string + gcc_version: string + total_system_memory_human: string + sync_full: string + connected_clients: string + module_fork_last_cow_size: string + total_writes_processed: string + allocator_active: string + total_net_output_bytes: string + pubsub_channels: string + current_fork_perc: string + active_defrag_key_hits: string + rdb_changes_since_last_save: string + instantaneous_input_kbps: string + used_memory_rss_human: string + configured_hz: string + expired_stale_perc: string + active_defrag_misses: string + used_cpu_sys_children: string + number_of_cached_scripts: string + sync_partial_ok: string + used_memory_lua_human: string + rdb_last_save_time: string + pubsub_patterns: string + slave_expires_tracked_keys: string + redis_git_sha1: string + used_memory_rss: string + rdb_last_bgsave_time_sec: string + os: string + mem_not_counted_for_evict: string + active_defrag_running: string + rejected_connections: string + aof_rewrite_buffer_length: string + total_forks: string + active_defrag_key_misses: string + allocator_allocated: string + aof_base_size: string + instantaneous_output_kbps: string + second_repl_offset: string + rdb_bgsave_in_progress: string + used_cpu_user_children: string + total_connections_received: string + migrate_cached_sockets: string +} + +export interface RedisCommandStatsVO { + command: string + calls: number + usec: number +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/login/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/login/index.ts new file mode 100644 index 00000000..ef86563b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/login/index.ts @@ -0,0 +1,81 @@ +import request from '@/config/axios' +import { getRefreshToken } from '@/utils/auth' +import type { UserLoginVO } from './types' + +export interface SmsCodeVO { + mobile: string + scene: number +} + +export interface SmsLoginVO { + mobile: string + code: string +} + +// 登录 +export const login = (data: UserLoginVO) => { + return request.post({ url: '/system/auth/login', data }) +} + +// 刷新访问令牌 +export const refreshToken = () => { + return request.post({ url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken() }) +} + +// 使用租户名,获得租户编号 +export const getTenantIdByName = (name: string) => { + return request.get({ url: '/system/tenant/get-id-by-name?name=' + name }) +} + +// 使用租户域名,获得租户信息 +export const getTenantByWebsite = (website: string) => { + return request.get({ url: '/system/tenant/get-by-website?website=' + website }) +} + +// 登出 +export const loginOut = () => { + return request.post({ url: '/system/auth/logout' }) +} + +// 获取用户权限信息 +export const getInfo = () => { + return request.get({ url: '/system/auth/get-permission-info' }) +} + +//获取登录验证码 +export const sendSmsCode = (data: SmsCodeVO) => { + return request.post({ url: '/system/auth/send-sms-code', data }) +} + +// 短信验证码登录 +export const smsLogin = (data: SmsLoginVO) => { + return request.post({ url: '/system/auth/sms-login', data }) +} + +// 社交快捷登录,使用 code 授权码 +export function socialLogin(type: string, code: string, state: string) { + return request.post({ + url: '/system/auth/social-login', + data: { + type, + code, + state + } + }) +} + +// 社交授权的跳转 +export const socialAuthRedirect = (type: number, redirectUri: string) => { + return request.get({ + url: '/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri + }) +} +// 获取验证图片以及 token +export const getCode = (data) => { + return request.postOriginal({ url: 'system/captcha/get', data }) +} + +// 滑动或者点选验证 +export const reqCheck = (data) => { + return request.postOriginal({ url: 'system/captcha/check', data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/login/oauth2/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/login/oauth2/index.ts new file mode 100644 index 00000000..aef1820d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/login/oauth2/index.ts @@ -0,0 +1,41 @@ +import request from '@/config/axios' + +// 获得授权信息 +export const getAuthorize = (clientId: string) => { + return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId }) +} + +// 发起授权 +export const authorize = ( + responseType: string, + clientId: string, + redirectUri: string, + state: string, + autoApprove: boolean, + checkedScopes: string[], + uncheckedScopes: string[] +) => { + // 构建 scopes + const scopes = {} + for (const scope of checkedScopes) { + scopes[scope] = true + } + for (const scope of uncheckedScopes) { + scopes[scope] = false + } + // 发起请求 + return request.post({ + url: '/system/oauth2/authorize', + headers: { + 'Content-type': 'application/x-www-form-urlencoded' + }, + params: { + response_type: responseType, + client_id: clientId, + redirect_uri: redirectUri, + state: state, + auto_approve: autoApprove, + scope: JSON.stringify(scopes) + } + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/login/types.ts b/mes-ui/mes-ui-admin-vue3/src/api/login/types.ts new file mode 100644 index 00000000..fff81225 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/login/types.ts @@ -0,0 +1,31 @@ +export type UserLoginVO = { + username: string + password: string + captchaVerification: string + socialType?: string + socialCode?: string + socialState?: string +} + +export type TokenType = { + id: number // 编号 + accessToken: string // 访问令牌 + refreshToken: string // 刷新令牌 + userId: number // 用户编号 + userType: number //用户类型 + clientId: string //客户端编号 + expiresTime: number //过期时间 +} + +export type UserVO = { + id: number + username: string + nickname: string + deptId: number + email: string + mobile: string + sex: number + avatar: string + loginIp: string + loginDate: string +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/market/banner/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/market/banner/index.ts new file mode 100644 index 00000000..ee65024c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/market/banner/index.ts @@ -0,0 +1,37 @@ +import request from '@/config/axios' + +export interface BannerVO { + id: number + title: string + picUrl: string + status: number + url: string + position: number + sort: number + memo: string +} + +// 查询Banner管理列表 +export const getBannerPage = async (params) => { + return await request.get({ url: `/promotion/banner/page`, params }) +} + +// 查询Banner管理详情 +export const getBanner = async (id: number) => { + return await request.get({ url: `/promotion/banner/get?id=` + id }) +} + +// 新增Banner管理 +export const createBanner = async (data: BannerVO) => { + return await request.post({ url: `/promotion/banner/create`, data }) +} + +// 修改Banner管理 +export const updateBanner = async (data: BannerVO) => { + return await request.put({ url: `/promotion/banner/update`, data }) +} + +// 删除Banner管理 +export const deleteBanner = async (id: number) => { + return await request.delete({ url: `/promotion/banner/delete?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/product/brand.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/brand.ts new file mode 100644 index 00000000..94d53704 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/brand.ts @@ -0,0 +1,61 @@ +import request from '@/config/axios' + +/** + * 商品品牌 + */ +export interface BrandVO { + /** + * 品牌编号 + */ + id?: number + /** + * 品牌名称 + */ + name: string + /** + * 品牌图片 + */ + picUrl: string + /** + * 品牌排序 + */ + sort?: number + /** + * 品牌描述 + */ + description?: string + /** + * 开启状态 + */ + status: number +} + +// 创建商品品牌 +export const createBrand = (data: BrandVO) => { + return request.post({ url: '/product/brand/create', data }) +} + +// 更新商品品牌 +export const updateBrand = (data: BrandVO) => { + return request.put({ url: '/product/brand/update', data }) +} + +// 删除商品品牌 +export const deleteBrand = (id: number) => { + return request.delete({ url: `/product/brand/delete?id=${id}` }) +} + +// 获得商品品牌 +export const getBrand = (id: number) => { + return request.get({ url: `/product/brand/get?id=${id}` }) +} + +// 获得商品品牌列表 +export const getBrandParam = (params: PageParam) => { + return request.get({ url: '/product/brand/page', params }) +} + +// 获得商品品牌精简信息列表 +export const getSimpleBrandList = () => { + return request.get({ url: '/product/brand/list-all-simple' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/product/category.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/category.ts new file mode 100644 index 00000000..8158fc0f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/category.ts @@ -0,0 +1,60 @@ +import request from '@/config/axios' + +/** + * 产品分类 + */ +export interface CategoryVO { + /** + * 分类编号 + */ + id?: number + /** + * 父分类编号 + */ + parentId?: number + /** + * 分类名称 + */ + name: string + /** + * 移动端分类图 + */ + picUrl: string + /** + * PC 端分类图 + */ + bigPicUrl?: string + /** + * 分类排序 + */ + sort: number + /** + * 开启状态 + */ + status: number +} + +// 创建商品分类 +export const createCategory = (data: CategoryVO) => { + return request.post({ url: '/product/category/create', data }) +} + +// 更新商品分类 +export const updateCategory = (data: CategoryVO) => { + return request.put({ url: '/product/category/update', data }) +} + +// 删除商品分类 +export const deleteCategory = (id: number) => { + return request.delete({ url: `/product/category/delete?id=${id}` }) +} + +// 获得商品分类 +export const getCategory = (id: number) => { + return request.get({ url: `/product/category/get?id=${id}` }) +} + +// 获得商品分类列表 +export const getCategoryList = (params: any) => { + return request.get({ url: '/product/category/list', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/product/comment.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/comment.ts new file mode 100644 index 00000000..defdbb93 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/comment.ts @@ -0,0 +1,49 @@ +import request from '@/config/axios' + +export interface CommentVO { + id: number + userId: number + userNickname: string + userAvatar: string + anonymous: boolean + orderId: number + orderItemId: number + spuId: number + spuName: string + skuId: number + visible: boolean + scores: number + descriptionScores: number + benefitScores: number + content: string + picUrls: string + replyStatus: boolean + replyUserId: number + replyContent: string + replyTime: Date +} + +// 查询商品评论列表 +export const getCommentPage = async (params) => { + return await request.get({ url: `/product/comment/page`, params }) +} + +// 查询商品评论详情 +export const getComment = async (id: number) => { + return await request.get({ url: `/product/comment/get?id=` + id }) +} + +// 添加自评 +export const createComment = async (data: CommentVO) => { + return await request.post({ url: `/product/comment/create`, data }) +} + +// 显示 / 隐藏评论 +export const updateCommentVisible = async (data: any) => { + return await request.put({ url: `/product/comment/update-visible`, data }) +} + +// 商家回复 +export const replyComment = async (data: any) => { + return await request.put({ url: `/product/comment/reply`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/product/favorite.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/favorite.ts new file mode 100644 index 00000000..3834eed0 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/favorite.ts @@ -0,0 +1,12 @@ +import request from '@/config/axios' + +export interface Favorite { + id?: number + userId?: string // 用户编号 + spuId?: number | null // 商品 SPU 编号 +} + +// 获得 ProductFavorite 列表 +export const getFavoritePage = (params: PageParam) => { + return request.get({ url: '/product/favorite/page', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/product/property.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/property.ts new file mode 100644 index 00000000..ac8bac59 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/property.ts @@ -0,0 +1,103 @@ +import request from '@/config/axios' + +/** + * 商品属性 + */ +export interface PropertyVO { + id?: number + /** 名称 */ + name: string + /** 备注 */ + remark?: string +} + +/** + * 属性值 + */ +export interface PropertyValueVO { + id?: number + /** 属性项的编号 */ + propertyId?: number + /** 名称 */ + name: string + /** 备注 */ + remark?: string +} + +/** + * 商品属性值的明细 + */ +export interface PropertyValueDetailVO { + /** 属性项的编号 */ + propertyId: number // 属性的编号 + /** 属性的名称 */ + propertyName: string + /** 属性值的编号 */ + valueId: number + /** 属性值的名称 */ + valueName: string +} + +// ------------------------ 属性项 ------------------- + +// 创建属性项 +export const createProperty = (data: PropertyVO) => { + return request.post({ url: '/product/property/create', data }) +} + +// 更新属性项 +export const updateProperty = (data: PropertyVO) => { + return request.put({ url: '/product/property/update', data }) +} + +// 删除属性项 +export const deleteProperty = (id: number) => { + return request.delete({ url: `/product/property/delete?id=${id}` }) +} + +// 获得属性项 +export const getProperty = (id: number): Promise => { + return request.get({ url: `/product/property/get?id=${id}` }) +} + +// 获得属性项分页 +export const getPropertyPage = (params: PageParam) => { + return request.get({ url: '/product/property/page', params }) +} + +// 获得属性项列表 +export const getPropertyList = (params: any) => { + return request.get({ url: '/product/property/list', params }) +} + +// 获得属性项列表 +export const getPropertyListAndValue = (data: any) => { + return request.post({ url: '/product/property/get-value-list', data }) +} + +// ------------------------ 属性值 ------------------- + +// 获得属性值分页 +export const getPropertyValuePage = (params: PageParam & any) => { + return request.get({ url: '/product/property/value/page', params }) +} + +// 获得属性值 +export const getPropertyValue = (id: number): Promise => { + return request.get({ url: `/product/property/value/get?id=${id}` }) +} + +// 创建属性值 +export const createPropertyValue = (data: PropertyValueVO) => { + return request.post({ url: '/product/property/value/create', data }) +} + +// 更新属性值 +export const updatePropertyValue = (data: PropertyValueVO) => { + return request.put({ url: '/product/property/value/update', data }) +} + +// 删除属性值 +export const deletePropertyValue = (id: number) => { + return request.delete({ url: `/product/property/value/delete?id=${id}` }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/product/spu.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/spu.ts new file mode 100644 index 00000000..8ccd02a5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/product/spu.ts @@ -0,0 +1,114 @@ +import request from '@/config/axios' + +export interface Property { + propertyId?: number // 属性编号 + propertyName?: string // 属性名称 + valueId?: number // 属性值编号 + valueName?: string // 属性值名称 +} + +export interface Sku { + id?: number // 商品 SKU 编号 + name?: string // 商品 SKU 名称 + spuId?: number // SPU 编号 + properties?: Property[] // 属性数组 + price?: number | string // 商品价格 + marketPrice?: number | string // 市场价 + costPrice?: number | string // 成本价 + barCode?: string // 商品条码 + picUrl?: string // 图片地址 + stock?: number // 库存 + weight?: number // 商品重量,单位:kg 千克 + volume?: number // 商品体积,单位:m^3 平米 + firstBrokeragePrice?: number | string // 一级分销的佣金 + secondBrokeragePrice?: number | string // 二级分销的佣金 + salesCount?: number // 商品销量 +} + +export interface GiveCouponTemplate { + id?: number + name?: string // 优惠券名称 +} + +export interface Spu { + id?: number + name?: string // 商品名称 + categoryId?: number | undefined // 商品分类 + keyword?: string // 关键字 + unit?: number | undefined // 单位 + picUrl?: string // 商品封面图 + sliderPicUrls?: string[] // 商品轮播图 + introduction?: string // 商品简介 + deliveryTemplateId?: number | undefined // 运费模版 + brandId?: number | undefined // 商品品牌编号 + specType?: boolean // 商品规格 + subCommissionType?: boolean // 分销类型 + skus?: Sku[] // sku数组 + description?: string // 商品详情 + sort?: number // 商品排序 + giveIntegral?: number // 赠送积分 + virtualSalesCount?: number // 虚拟销量 + recommendHot?: boolean // 是否热卖 + recommendBenefit?: boolean // 是否优惠 + recommendBest?: boolean // 是否精品 + recommendNew?: boolean // 是否新品 + recommendGood?: boolean // 是否优品 + price?: number // 商品价格 + salesCount?: number // 商品销量 + marketPrice?: number // 市场价 + costPrice?: number // 成本价 + stock?: number // 商品库存 + createTime?: Date // 商品创建时间 + status?: number // 商品状态 + activityOrders: number[] // 活动排序 +} + +// 获得 Spu 列表 +export const getSpuPage = (params: PageParam) => { + return request.get({ url: '/product/spu/page', params }) +} + +// 获得 Spu 列表 tabsCount +export const getTabsCount = () => { + return request.get({ url: '/product/spu/get-count' }) +} + +// 创建商品 Spu +export const createSpu = (data: Spu) => { + return request.post({ url: '/product/spu/create', data }) +} + +// 更新商品 Spu +export const updateSpu = (data: Spu) => { + return request.put({ url: '/product/spu/update', data }) +} + +// 更新商品 Spu status +export const updateStatus = (data: { id: number; status: number }) => { + return request.put({ url: '/product/spu/update-status', data }) +} + +// 获得商品 Spu +export const getSpu = (id: number) => { + return request.get({ url: `/product/spu/get-detail?id=${id}` }) +} + +// 获得商品 Spu 详情列表 +export const getSpuDetailList = (ids: number[]) => { + return request.get({ url: `/product/spu/list?spuIds=${ids}` }) +} + +// 删除商品 Spu +export const deleteSpu = (id: number) => { + return request.delete({ url: `/product/spu/delete?id=${id}` }) +} + +// 导出商品 Spu Excel +export const exportSpu = async (params) => { + return await request.download({ url: '/product/spu/export', params }) +} + +// 获得商品 SPU 精简列表 +export const getSpuSimpleList = async () => { + return request.get({ url: '/product/spu/list-all-simple' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/article/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/article/index.ts new file mode 100644 index 00000000..9184c7af --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/article/index.ts @@ -0,0 +1,42 @@ +import request from '@/config/axios' + +export interface ArticleVO { + id: number + categoryId: number + title: string + author: string + picUrl: string + introduction: string + browseCount: string + sort: number + status: number + spuId: number + recommendHot: boolean + recommendBanner: boolean + content: string +} + +// 查询文章管理列表 +export const getArticlePage = async (params: any) => { + return await request.get({ url: `/promotion/article/page`, params }) +} + +// 查询文章管理详情 +export const getArticle = async (id: number) => { + return await request.get({ url: `/promotion/article/get?id=` + id }) +} + +// 新增文章管理 +export const createArticle = async (data: ArticleVO) => { + return await request.post({ url: `/promotion/article/create`, data }) +} + +// 修改文章管理 +export const updateArticle = async (data: ArticleVO) => { + return await request.put({ url: `/promotion/article/update`, data }) +} + +// 删除文章管理 +export const deleteArticle = async (id: number) => { + return await request.delete({ url: `/promotion/article/delete?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/articleCategory/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/articleCategory/index.ts new file mode 100644 index 00000000..47f5e934 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/articleCategory/index.ts @@ -0,0 +1,39 @@ +import request from '@/config/axios' + +export interface ArticleCategoryVO { + id: number + name: string + picUrl: string + status: number + sort: number +} + +// 查询文章分类列表 +export const getArticleCategoryPage = async (params) => { + return await request.get({ url: `/promotion/article-category/page`, params }) +} + +// 查询文章分类精简信息列表 +export const getSimpleArticleCategoryList = async () => { + return await request.get({ url: `/promotion/article-category/list-all-simple` }) +} + +// 查询文章分类详情 +export const getArticleCategory = async (id: number) => { + return await request.get({ url: `/promotion/article-category/get?id=` + id }) +} + +// 新增文章分类 +export const createArticleCategory = async (data: ArticleCategoryVO) => { + return await request.post({ url: `/promotion/article-category/create`, data }) +} + +// 修改文章分类 +export const updateArticleCategory = async (data: ArticleCategoryVO) => { + return await request.put({ url: `/promotion/article-category/update`, data }) +} + +// 删除文章分类 +export const deleteArticleCategory = async (id: number) => { + return await request.delete({ url: `/promotion/article-category/delete?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/bargain/bargainActivity.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/bargain/bargainActivity.ts new file mode 100644 index 00000000..9ad219ac --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/bargain/bargainActivity.ts @@ -0,0 +1,68 @@ +import request from '@/config/axios' +import { Sku, Spu } from '@/api/mall/product/spu' + +export interface BargainActivityVO { + id?: number + name?: string + startTime?: Date + endTime?: Date + status?: number + helpMaxCount?: number // 达到该人数,才能砍到低价 + bargainCount?: number // 最大帮砍次数 + totalLimitCount?: number // 最大购买次数 + spuId: number + skuId: number + bargainFirstPrice: number // 砍价起始价格,单位分 + bargainMinPrice: number // 砍价底价 + stock: number // 活动库存 + randomMinPrice?: number // 用户每次砍价的最小金额,单位:分 + randomMaxPrice?: number // 用户每次砍价的最大金额,单位:分 +} + +// 砍价活动所需属性。选择的商品和属性的时候使用方便使用活动的通用封装 +export interface BargainProductVO { + spuId: number + skuId: number + bargainFirstPrice: number // 砍价起始价格,单位分 + bargainMinPrice: number // 砍价底价 + stock: number // 活动库存 +} + +// 扩展 Sku 配置 +export type SkuExtension = Sku & { + productConfig: BargainProductVO +} + +export interface SpuExtension extends Spu { + skus: SkuExtension[] // 重写类型 +} + +// 查询砍价活动列表 +export const getBargainActivityPage = async (params: any) => { + return await request.get({ url: '/promotion/bargain-activity/page', params }) +} + +// 查询砍价活动详情 +export const getBargainActivity = async (id: number) => { + return await request.get({ url: '/promotion/bargain-activity/get?id=' + id }) +} + +// 新增砍价活动 +export const createBargainActivity = async (data: BargainActivityVO) => { + return await request.post({ url: '/promotion/bargain-activity/create', data }) +} + +// 修改砍价活动 +export const updateBargainActivity = async (data: BargainActivityVO) => { + return await request.put({ url: '/promotion/bargain-activity/update', data }) +} + +// 关闭砍价活动 +export const closeBargainActivity = async (id: number) => { + return await request.put({ url: '/promotion/bargain-activity/close?id=' + id }) +} + +// 删除砍价活动 +export const deleteBargainActivity = async (id: number) => { + return await request.delete({ url: '/promotion/bargain-activity/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/bargain/bargainHelp.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/bargain/bargainHelp.ts new file mode 100644 index 00000000..4308ae66 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/bargain/bargainHelp.ts @@ -0,0 +1,14 @@ +import request from '@/config/axios' + +export interface BargainHelpVO { + id: number + record: number + userId: number + reducePrice: number + endTime: Date +} + +// 查询砍价记录列表 +export const getBargainHelpPage = async (params) => { + return await request.get({ url: `/promotion/bargain-help/page`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/bargain/bargainRecord.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/bargain/bargainRecord.ts new file mode 100644 index 00000000..f90b7845 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/bargain/bargainRecord.ts @@ -0,0 +1,19 @@ +import request from '@/config/axios' + +export interface BargainRecordVO { + id: number + activityId: number + userId: number + spuId: number + skuId: number + bargainFirstPrice: number + bargainPrice: number + status: number + orderId: number + endTime: Date +} + +// 查询砍价记录列表 +export const getBargainRecordPage = async (params) => { + return await request.get({ url: `/promotion/bargain-record/page`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/combination/combinationActivity.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/combination/combinationActivity.ts new file mode 100644 index 00000000..062db5c2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/combination/combinationActivity.ts @@ -0,0 +1,66 @@ +import request from '@/config/axios' +import { Sku, Spu } from '@/api/mall/product/spu' + +export interface CombinationActivityVO { + id?: number + name?: string + spuId?: number + totalLimitCount?: number + singleLimitCount?: number + startTime?: Date + endTime?: Date + userSize?: number + totalCount?: number + successCount?: number + orderUserCount?: number + virtualGroup?: number + status?: number + limitDuration?: number + products: CombinationProductVO[] +} + +// 拼团活动所需属性 +export interface CombinationProductVO { + spuId: number + skuId: number + combinationPrice: number // 拼团价格 +} + +// 扩展 Sku 配置 +export type SkuExtension = Sku & { + productConfig: CombinationProductVO +} + +export interface SpuExtension extends Spu { + skus: SkuExtension[] // 重写类型 +} + +// 查询拼团活动列表 +export const getCombinationActivityPage = async (params) => { + return await request.get({ url: '/promotion/combination-activity/page', params }) +} + +// 查询拼团活动详情 +export const getCombinationActivity = async (id: number) => { + return await request.get({ url: '/promotion/combination-activity/get?id=' + id }) +} + +// 新增拼团活动 +export const createCombinationActivity = async (data: CombinationActivityVO) => { + return await request.post({ url: '/promotion/combination-activity/create', data }) +} + +// 修改拼团活动 +export const updateCombinationActivity = async (data: CombinationActivityVO) => { + return await request.put({ url: '/promotion/combination-activity/update', data }) +} + +// 关闭拼团活动 +export const closeCombinationActivity = async (id: number) => { + return await request.put({ url: '/promotion/combination-activity/close?id=' + id }) +} + +// 删除拼团活动 +export const deleteCombinationActivity = async (id: number) => { + return await request.delete({ url: '/promotion/combination-activity/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/combination/combinationRecord.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/combination/combinationRecord.ts new file mode 100644 index 00000000..90e8937e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/combination/combinationRecord.ts @@ -0,0 +1,33 @@ +import request from '@/config/axios' + +export interface CombinationRecordVO { + id: number // 拼团记录编号 + activityId: number // 拼团活动编号 + nickname: string // 用户昵称 + avatar: string // 用户头像 + headId: number // 团长编号 + expireTime: string // 过期时间 + userSize: number // 可参团人数 + userCount: number // 已参团人数 + status: number // 拼团状态 + spuName: string // 商品名字 + picUrl: string // 商品图片 + virtualGroup: boolean // 是否虚拟成团 + startTime: string // 开始时间 (订单付款后开始的时间) + endTime: string // 结束时间(成团时间/失败时间) +} + +// 查询拼团记录列表 +export const getCombinationRecordPage = async (params) => { + return await request.get({ url: '/promotion/combination-record/page', params }) +} + +// 查询一个拼团的完整拼团记录 +export const getCombinationRecordPageByHeadId = async (params) => { + return await request.get({ url: '/promotion/combination-record/page-by-headId', params }) +} + +// 获得拼团记录的概要信息 +export const getCombinationRecordSummary = async () => { + return await request.get({ url: '/promotion/combination-record/get-summary' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/coupon/coupon.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/coupon/coupon.ts new file mode 100644 index 00000000..2ebff5da --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/coupon/coupon.ts @@ -0,0 +1,26 @@ +import request from '@/config/axios' + +// TODO @dhb52:vo 缺少 + +// 删除优惠劵 +export const deleteCoupon = async (id: number) => { + return request.delete({ + url: `/promotion/coupon/delete?id=${id}` + }) +} + +// 获得优惠劵分页 +export const getCouponPage = async (params: PageParam) => { + return request.get({ + url: '/promotion/coupon/page', + params: params + }) +} + +// 发送优惠券 +export const sendCoupon = async (data: any) => { + return request.post({ + url: '/promotion/coupon/send', + data: data + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/coupon/couponTemplate.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/coupon/couponTemplate.ts new file mode 100644 index 00000000..50ae226c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/coupon/couponTemplate.ts @@ -0,0 +1,90 @@ +import request from '@/config/axios' + +export interface CouponTemplateVO { + id: number + name: string + status: number + totalCount: number + takeLimitCount: number + takeType: number + usePrice: number + productScope: number + productScopeValues: number[] + validityType: number + validStartTime: Date + validEndTime: Date + fixedStartTerm: number + fixedEndTerm: number + discountType: number + discountPercent: number + discountPrice: number + discountLimitPrice: number + takeCount: number + useCount: number +} + +// 创建优惠劵模板 +export function createCouponTemplate(data: CouponTemplateVO) { + return request.post({ + url: '/promotion/coupon-template/create', + data: data + }) +} + +// 更新优惠劵模板 +export function updateCouponTemplate(data: CouponTemplateVO) { + return request.put({ + url: '/promotion/coupon-template/update', + data: data + }) +} + +// 更新优惠劵模板的状态 +export function updateCouponTemplateStatus(id: number, status: [0, 1]) { + const data = { + id, + status + } + return request.put({ + url: '/promotion/coupon-template/update-status', + data: data + }) +} + +// 删除优惠劵模板 +export function deleteCouponTemplate(id: number) { + return request.delete({ + url: '/promotion/coupon-template/delete?id=' + id + }) +} + +// 获得优惠劵模板 +export function getCouponTemplate(id: number) { + return request.get({ + url: '/promotion/coupon-template/get?id=' + id + }) +} + +// 获得优惠劵模板分页 +export function getCouponTemplatePage(params: PageParam) { + return request.get({ + url: '/promotion/coupon-template/page', + params: params + }) +} + +// 获得优惠劵模板分页 +export function getCouponTemplateList(ids: number[]) { + return request.get({ + url: `/promotion/coupon-template/list?ids=${ids}` + }) +} + +// 导出优惠劵模板 Excel +export function exportCouponTemplateExcel(params: PageParam) { + return request.get({ + url: '/promotion/coupon-template/export-excel', + params: params, + responseType: 'blob' + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/discount/discountActivity.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/discount/discountActivity.ts new file mode 100644 index 00000000..e755c1bd --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/discount/discountActivity.ts @@ -0,0 +1,60 @@ +import request from '@/config/axios' +import { Sku, Spu } from '@/api/mall/product/spu' + +export interface DiscountActivityVO { + id?: number + spuId?: number + name?: string + status?: number + remark?: string + startTime?: Date + endTime?: Date + products?: DiscountProductVO[] +} +// 限时折扣相关 属性 +export interface DiscountProductVO { + spuId: number + skuId: number + discountType: number + discountPercent: number + discountPrice: number +} + +// 扩展 Sku 配置 +export type SkuExtension = Sku & { + productConfig: DiscountProductVO +} + +export interface SpuExtension extends Spu { + skus: SkuExtension[] // 重写类型 +} + +// 查询限时折扣活动列表 +export const getDiscountActivityPage = async (params) => { + return await request.get({ url: '/promotion/discount-activity/page', params }) +} + +// 查询限时折扣活动详情 +export const getDiscountActivity = async (id: number) => { + return await request.get({ url: '/promotion/discount-activity/get?id=' + id }) +} + +// 新增限时折扣活动 +export const createDiscountActivity = async (data: DiscountActivityVO) => { + return await request.post({ url: '/promotion/discount-activity/create', data }) +} + +// 修改限时折扣活动 +export const updateDiscountActivity = async (data: DiscountActivityVO) => { + return await request.put({ url: '/promotion/discount-activity/update', data }) +} + +// 关闭限时折扣活动 +export const closeDiscountActivity = async (id: number) => { + return await request.put({ url: '/promotion/discount-activity/close?id=' + id }) +} + +// 删除限时折扣活动 +export const deleteDiscountActivity = async (id: number) => { + return await request.delete({ url: '/promotion/discount-activity/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/diy/page.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/diy/page.ts new file mode 100644 index 00000000..1cd34282 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/diy/page.ts @@ -0,0 +1,45 @@ +import request from '@/config/axios' + +export interface DiyPageVO { + id?: number + templateId?: number + name: string + remark: string + previewImageUrls: string[] + property: string +} + +// 查询装修页面列表 +export const getDiyPagePage = async (params: any) => { + return await request.get({ url: `/promotion/diy-page/page`, params }) +} + +// 查询装修页面详情 +export const getDiyPage = async (id: number) => { + return await request.get({ url: `/promotion/diy-page/get?id=` + id }) +} + +// 新增装修页面 +export const createDiyPage = async (data: DiyPageVO) => { + return await request.post({ url: `/promotion/diy-page/create`, data }) +} + +// 修改装修页面 +export const updateDiyPage = async (data: DiyPageVO) => { + return await request.put({ url: `/promotion/diy-page/update`, data }) +} + +// 删除装修页面 +export const deleteDiyPage = async (id: number) => { + return await request.delete({ url: `/promotion/diy-page/delete?id=` + id }) +} + +// 获得装修页面属性 +export const getDiyPageProperty = async (id: number) => { + return await request.get({ url: `/promotion/diy-page/get-property?id=` + id }) +} + +// 更新装修页面属性 +export const updateDiyPageProperty = async (data: DiyPageVO) => { + return await request.put({ url: `/promotion/diy-page/update-property`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/diy/template.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/diy/template.ts new file mode 100644 index 00000000..f8d4bc80 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/diy/template.ts @@ -0,0 +1,58 @@ +import request from '@/config/axios' +import { DiyPageVO } from '@/api/mall/promotion/diy/page' + +export interface DiyTemplateVO { + id?: number + name: string + used: boolean + usedTime?: Date + remark: string + previewImageUrls: string[] + property: string +} + +export interface DiyTemplatePropertyVO extends DiyTemplateVO { + pages: DiyPageVO[] +} + +// 查询装修模板列表 +export const getDiyTemplatePage = async (params: any) => { + return await request.get({ url: `/promotion/diy-template/page`, params }) +} + +// 查询装修模板详情 +export const getDiyTemplate = async (id: number) => { + return await request.get({ url: `/promotion/diy-template/get?id=` + id }) +} + +// 新增装修模板 +export const createDiyTemplate = async (data: DiyTemplateVO) => { + return await request.post({ url: `/promotion/diy-template/create`, data }) +} + +// 修改装修模板 +export const updateDiyTemplate = async (data: DiyTemplateVO) => { + return await request.put({ url: `/promotion/diy-template/update`, data }) +} + +// 删除装修模板 +export const deleteDiyTemplate = async (id: number) => { + return await request.delete({ url: `/promotion/diy-template/delete?id=` + id }) +} + +// 使用装修模板 +export const useDiyTemplate = async (id: number) => { + return await request.put({ url: `/promotion/diy-template/use?id=` + id }) +} + +// 获得装修模板属性 +export const getDiyTemplateProperty = async (id: number) => { + return await request.get({ + url: `/promotion/diy-template/get-property?id=` + id + }) +} + +// 更新装修模板属性 +export const updateDiyTemplateProperty = async (data: DiyTemplateVO) => { + return await request.put({ url: `/promotion/diy-template/update-property`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/seckill/seckillActivity.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/seckill/seckillActivity.ts new file mode 100644 index 00000000..e8346410 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/seckill/seckillActivity.ts @@ -0,0 +1,68 @@ +import request from '@/config/axios' +import { Sku, Spu } from '@/api/mall/product/spu' + +export interface SeckillActivityVO { + id?: number + spuId?: number + name?: string + status?: number + remark?: string + startTime?: Date + endTime?: Date + sort?: number + configIds?: string + orderCount?: number + userCount?: number + totalPrice?: number + totalLimitCount?: number + singleLimitCount?: number + stock?: number + totalStock?: number + products?: SeckillProductVO[] +} + +// 秒杀活动所需属性 +export interface SeckillProductVO { + skuId: number + seckillPrice: number + stock: number +} + +// 扩展 Sku 配置 +export type SkuExtension = Sku & { + productConfig: SeckillProductVO +} + +export interface SpuExtension extends Spu { + skus: SkuExtension[] // 重写类型 +} + +// 查询秒杀活动列表 +export const getSeckillActivityPage = async (params) => { + return await request.get({ url: '/promotion/seckill-activity/page', params }) +} + +// 查询秒杀活动详情 +export const getSeckillActivity = async (id: number) => { + return await request.get({ url: '/promotion/seckill-activity/get?id=' + id }) +} + +// 新增秒杀活动 +export const createSeckillActivity = async (data: SeckillActivityVO) => { + return await request.post({ url: '/promotion/seckill-activity/create', data }) +} + +// 修改秒杀活动 +export const updateSeckillActivity = async (data: SeckillActivityVO) => { + return await request.put({ url: '/promotion/seckill-activity/update', data }) +} + +// 关闭秒杀活动 +export const closeSeckillActivity = async (id: number) => { + return await request.put({ url: '/promotion/seckill-activity/close?id=' + id }) +} + +// 删除秒杀活动 +export const deleteSeckillActivity = async (id: number) => { + return await request.delete({ url: '/promotion/seckill-activity/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/seckill/seckillConfig.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/seckill/seckillConfig.ts new file mode 100644 index 00000000..23ad15ca --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/promotion/seckill/seckillConfig.ts @@ -0,0 +1,49 @@ +import request from '@/config/axios' + +export interface SeckillConfigVO { + id: number + name: string + startTime: string + endTime: string + sliderPicUrls: string[] + status: number +} + +// 查询秒杀时段配置列表 +export const getSeckillConfigPage = async (params) => { + return await request.get({ url: '/promotion/seckill-config/page', params }) +} + +// 查询秒杀时段配置详情 +export const getSeckillConfig = async (id: number) => { + return await request.get({ url: '/promotion/seckill-config/get?id=' + id }) +} + +// 获得所有开启状态的秒杀时段精简列表 +export const getSimpleSeckillConfigList = async () => { + return await request.get({ url: '/promotion/seckill-config/list-all-simple' }) +} + +// 新增秒杀时段配置 +export const createSeckillConfig = async (data: SeckillConfigVO) => { + return await request.post({ url: '/promotion/seckill-config/create', data }) +} + +// 修改秒杀时段配置 +export const updateSeckillConfig = async (data: SeckillConfigVO) => { + return await request.put({ url: '/promotion/seckill-config/update', data }) +} + +// 修改时段配置状态 +export const updateSeckillConfigStatus = (id: number, status: number) => { + const data = { + id, + status + } + return request.put({ url: '/promotion/seckill-config/update-status', data: data }) +} + +// 删除秒杀时段配置 +export const deleteSeckillConfig = async (id: number) => { + return await request.delete({ url: '/promotion/seckill-config/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/common.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/common.ts new file mode 100644 index 00000000..3d964392 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/common.ts @@ -0,0 +1,5 @@ +/** 数据对照 Response VO */ +export interface DataComparisonRespVO { + value: T + reference: T +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/member.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/member.ts new file mode 100644 index 00000000..92af031e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/member.ts @@ -0,0 +1,123 @@ +import request from '@/config/axios' +import dayjs from 'dayjs' +import { DataComparisonRespVO } from '@/api/mall/statistics/common' +import { formatDate } from '@/utils/formatTime' + +/** 会员分析 Request VO */ +export interface MemberAnalyseReqVO { + times: [dayjs.ConfigType, dayjs.ConfigType] +} + +/** 会员分析 Response VO */ +export interface MemberAnalyseRespVO { + visitUserCount: number + orderUserCount: number + payUserCount: number + atv: number + comparison: DataComparisonRespVO +} + +/** 会员分析对照数据 Response VO */ +export interface MemberAnalyseComparisonRespVO { + registerUserCount: number + visitUserCount: number + rechargeUserCount: number +} + +/** 会员地区统计 Response VO */ +export interface MemberAreaStatisticsRespVO { + areaId: number + areaName: string + userCount: number + orderCreateUserCount: number + orderPayUserCount: number + orderPayPrice: number +} + +/** 会员性别统计 Response VO */ +export interface MemberSexStatisticsRespVO { + sex: number + userCount: number +} + +/** 会员统计 Response VO */ +export interface MemberSummaryRespVO { + userCount: number + rechargeUserCount: number + rechargePrice: number + expensePrice: number +} + +/** 会员终端统计 Response VO */ +export interface MemberTerminalStatisticsRespVO { + terminal: number + userCount: number +} + +/** 会员数量统计 Response VO */ +export interface MemberCountRespVO { + /** 用户访问量 */ + visitUserCount: string + /** 注册用户数量 */ + registerUserCount: number +} + +/** 会员注册数量 Response VO */ +export interface MemberRegisterCountRespVO { + date: string + count: number +} + +// 查询会员统计 +export const getMemberSummary = () => { + return request.get({ + url: '/statistics/member/summary' + }) +} + +// 查询会员分析数据 +export const getMemberAnalyse = (params: MemberAnalyseReqVO) => { + return request.get({ + url: '/statistics/member/analyse', + params: { times: [formatDate(params.times[0]), formatDate(params.times[1])] } + }) +} + +// 按照省份,查询会员统计列表 +export const getMemberAreaStatisticsList = () => { + return request.get({ + url: '/statistics/member/area-statistics-list' + }) +} + +// 按照性别,查询会员统计列表 +export const getMemberSexStatisticsList = () => { + return request.get({ + url: '/statistics/member/sex-statistics-list' + }) +} + +// 按照终端,查询会员统计列表 +export const getMemberTerminalStatisticsList = () => { + return request.get({ + url: '/statistics/member/terminal-statistics-list' + }) +} + +// 获得用户数量量对照 +export const getUserCountComparison = () => { + return request.get>({ + url: '/statistics/member/user-count-comparison' + }) +} + +// 获得会员注册数量列表 +export const getMemberRegisterCountList = ( + beginTime: dayjs.ConfigType, + endTime: dayjs.ConfigType +) => { + return request.get({ + url: '/statistics/member/register-count-list', + params: { times: [formatDate(beginTime), formatDate(endTime)] } + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/pay.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/pay.ts new file mode 100644 index 00000000..f5d14c9d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/pay.ts @@ -0,0 +1,12 @@ +import request from '@/config/axios' + +/** 支付统计 */ +export interface PaySummaryRespVO { + /** 充值金额,单位分 */ + rechargePrice: number +} + +/** 获取钱包充值金额 */ +export const getWalletRechargePrice = async () => { + return await request.get({ url: `/statistics/pay/summary` }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/trade.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/trade.ts new file mode 100644 index 00000000..94052597 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/statistics/trade.ts @@ -0,0 +1,119 @@ +import request from '@/config/axios' +import dayjs from 'dayjs' +import { formatDate } from '@/utils/formatTime' +import { DataComparisonRespVO } from '@/api/mall/statistics/common' + +/** 交易统计 Response VO */ +export interface TradeSummaryRespVO { + yesterdayOrderCount: number + monthOrderCount: number + yesterdayPayPrice: number + monthPayPrice: number +} + +/** 交易状况 Request VO */ +export interface TradeTrendReqVO { + times: [dayjs.ConfigType, dayjs.ConfigType] +} + +/** 交易状况统计 Response VO */ +export interface TradeTrendSummaryRespVO { + time: string + turnoverPrice: number + orderPayPrice: number + rechargePrice: number + expensePrice: number + walletPayPrice: number + brokerageSettlementPrice: number + afterSaleRefundPrice: number +} + +/** 交易订单数量 Response VO */ +export interface TradeOrderCountRespVO { + /** 待发货 */ + undelivered?: number + /** 待核销 */ + pickUp?: number + /** 退款中 */ + afterSaleApply?: number + /** 提现待审核 */ + auditingWithdraw?: number +} + +/** 交易订单统计 Response VO */ +export interface TradeOrderSummaryRespVO { + /** 支付订单商品数 */ + orderPayCount?: number + /** 总支付金额,单位:分 */ + orderPayPrice?: number +} + +/** 订单量趋势统计 Response VO */ +export interface TradeOrderTrendRespVO { + /** 日期 */ + date: string + /** 订单数量 */ + orderPayCount: number + /** 订单支付金额 */ + orderPayPrice: number +} + +// 查询交易统计 +export const getTradeStatisticsSummary = () => { + return request.get>({ + url: '/statistics/trade/summary' + }) +} + +// 获得交易状况统计 +export const getTradeTrendSummary = (params: TradeTrendReqVO) => { + return request.get>({ + url: '/statistics/trade/trend/summary', + params: formatDateParam(params) + }) +} + +// 获得交易状况明细 +export const getTradeStatisticsList = (params: TradeTrendReqVO) => { + return request.get({ + url: '/statistics/trade/list', + params: formatDateParam(params) + }) +} + +// 导出交易状况明细 +export const exportTradeStatisticsExcel = (params: TradeTrendReqVO) => { + return request.download({ + url: '/statistics/trade/export-excel', + params: formatDateParam(params) + }) +} + +// 获得交易订单数量 +export const getOrderCount = async () => { + return await request.get({ url: `/statistics/trade/order-count` }) +} + +// 获得交易订单数量对照 +export const getOrderComparison = async () => { + return await request.get>({ + url: `/statistics/trade/order-comparison` + }) +} + +// 获得订单量趋势统计 +export const getOrderCountTrendComparison = ( + type: number, + beginTime: dayjs.ConfigType, + endTime: dayjs.ConfigType +) => { + return request.get[]>({ + url: '/statistics/trade/order-count-trend', + params: { type, beginTime: formatDate(beginTime), endTime: formatDate(endTime) } + }) +} + +/** 时间参数需要格式化, 确保接口能识别 */ +const formatDateParam = (params: TradeTrendReqVO) => { + return { times: [formatDate(params.times[0]), formatDate(params.times[1])] } as TradeTrendReqVO +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/afterSale/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/afterSale/index.ts new file mode 100644 index 00000000..a109ee6b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/afterSale/index.ts @@ -0,0 +1,75 @@ +import request from '@/config/axios' + +export interface TradeAfterSaleVO { + id?: number | null // 售后编号,主键自增 + no?: string // 售后单号 + status?: number | null // 退款状态 + way?: number | null // 售后方式 + type?: number | null // 售后类型 + userId?: number | null // 用户编号 + applyReason?: string // 申请原因 + applyDescription?: string // 补充描述 + applyPicUrls?: string[] // 补充凭证图片 + orderId?: number | null // 交易订单编号 + orderNo?: string // 订单流水号 + orderItemId?: number | null // 交易订单项编号 + spuId?: number | null // 商品 SPU 编号 + spuName?: string // 商品 SPU 名称 + skuId?: number | null // 商品 SKU 编号 + properties?: ProductPropertiesVO[] // 属性数组 + picUrl?: string // 商品图片 + count?: number | null // 退货商品数量 + auditTime?: Date // 审批时间 + auditUserId?: number | null // 审批人 + auditReason?: string // 审批备注 + refundPrice?: number | null // 退款金额,单位:分。 + payRefundId?: number | null // 支付退款编号 + refundTime?: Date // 退款时间 + logisticsId?: number | null // 退货物流公司编号 + logisticsNo?: string // 退货物流单号 + deliveryTime?: Date // 退货时间 + receiveTime?: Date // 收货时间 + receiveReason?: string // 收货备注 +} + +export interface ProductPropertiesVO { + propertyId?: number | null // 属性的编号 + propertyName?: string // 属性的名称 + valueId?: number | null //属性值的编号 + valueName?: string // 属性值的名称 +} + +// 获得交易售后分页 +export const getAfterSalePage = async (params) => { + return await request.get({ url: `/trade/after-sale/page`, params }) +} + +// 获得交易售后详情 +export const getAfterSale = async (id: any) => { + return await request.get({ url: `/trade/after-sale/get-detail?id=${id}` }) +} + +// 同意售后 +export const agree = async (id: any) => { + return await request.put({ url: `/trade/after-sale/agree?id=${id}` }) +} + +// 拒绝售后 +export const disagree = async (data: any) => { + return await request.put({ url: `/trade/after-sale/disagree`, data }) +} + +// 确认收货 +export const receive = async (id: any) => { + return await request.put({ url: `/trade/after-sale/receive?id=${id}` }) +} + +// 拒绝收货 +export const refuse = async (id: any) => { + return await request.put({ url: `/trade/after-sale/refuse?id=${id}` }) +} + +// 确认退款 +export const refund = async (id: any) => { + return await request.put({ url: `/trade/after-sale/refund?id=${id}` }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/brokerage/record/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/brokerage/record/index.ts new file mode 100644 index 00000000..7df9a225 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/brokerage/record/index.ts @@ -0,0 +1,11 @@ +import request from '@/config/axios' + +// 查询佣金记录列表 +export const getBrokerageRecordPage = async (params: any) => { + return await request.get({ url: `/trade/brokerage-record/page`, params }) +} + +// 查询佣金记录详情 +export const getBrokerageRecord = async (id: number) => { + return await request.get({ url: `/trade/brokerage-record/get?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/brokerage/user/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/brokerage/user/index.ts new file mode 100644 index 00000000..1fed3bfa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/brokerage/user/index.ts @@ -0,0 +1,39 @@ +import request from '@/config/axios' + +export interface BrokerageUserVO { + id: number + bindUserId: number + bindUserTime: Date + brokerageEnabled: boolean + brokerageTime: Date + price: number + frozenPrice: number + + nickname: string + avatar: string +} + +// 查询分销用户列表 +export const getBrokerageUserPage = async (params: any) => { + return await request.get({ url: `/trade/brokerage-user/page`, params }) +} + +// 查询分销用户详情 +export const getBrokerageUser = async (id: number) => { + return await request.get({ url: `/trade/brokerage-user/get?id=` + id }) +} + +// 修改推广员 +export const updateBindUser = async (data: any) => { + return await request.put({ url: `/trade/brokerage-user/update-bind-user`, data }) +} + +// 清除推广员 +export const clearBindUser = async (data: any) => { + return await request.put({ url: `/trade/brokerage-user/clear-bind-user`, data }) +} + +// 修改推广资格 +export const updateBrokerageEnabled = async (data: any) => { + return await request.put({ url: `/trade/brokerage-user/update-brokerage-enable`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/brokerage/withdraw/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/brokerage/withdraw/index.ts new file mode 100644 index 00000000..c93286a9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/brokerage/withdraw/index.ts @@ -0,0 +1,39 @@ +import request from '@/config/axios' + +export interface BrokerageWithdrawVO { + id: number + userId: number + price: number + feePrice: number + totalPrice: number + type: number + name: string + accountNo: string + bankName: string + bankAddress: string + accountQrCodeUrl: string + status: number + auditReason: string + auditTime: Date + remark: string +} + +// 查询佣金提现列表 +export const getBrokerageWithdrawPage = async (params: any) => { + return await request.get({ url: `/trade/brokerage-withdraw/page`, params }) +} + +// 查询佣金提现详情 +export const getBrokerageWithdraw = async (id: number) => { + return await request.get({ url: `/trade/brokerage-withdraw/get?id=` + id }) +} + +// 佣金提现 - 通过申请 +export const approveBrokerageWithdraw = async (id: number) => { + return await request.put({ url: `/trade/brokerage-withdraw/approve?id=` + id }) +} + +// 审核佣金提现 - 驳回申请 +export const rejectBrokerageWithdraw = async (data: BrokerageWithdrawVO) => { + return await request.put({ url: `/trade/brokerage-withdraw/reject`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/config/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/config/index.ts new file mode 100644 index 00000000..66a81147 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/config/index.ts @@ -0,0 +1,24 @@ +import request from '@/config/axios' + +export interface ConfigVO { + brokerageEnabled: boolean + brokerageEnabledCondition: number + brokerageBindMode: number + brokeragePosterUrls: string + brokerageFirstPercent: number + brokerageSecondPercent: number + brokerageWithdrawMinPrice: number + brokerageBankNames: string + brokerageFrozenDays: number + brokerageWithdrawTypes: string +} + +// 查询交易中心配置详情 +export const getTradeConfig = async () => { + return await request.get({ url: `/trade/config/get` }) +} + +// 保存交易中心配置 +export const saveTradeConfig = async (data: ConfigVO) => { + return await request.put({ url: `/trade/config/save`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/delivery/express/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/delivery/express/index.ts new file mode 100644 index 00000000..0070bcd6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/delivery/express/index.ts @@ -0,0 +1,45 @@ +import request from '@/config/axios' + +export interface DeliveryExpressVO { + id: number + code: string + name: string + logo: string + sort: number + status: number +} + +// 查询快递公司列表 +export const getDeliveryExpressPage = async (params: PageParam) => { + return await request.get({ url: '/trade/delivery/express/page', params }) +} + +// 查询快递公司详情 +export const getDeliveryExpress = async (id: number) => { + return await request.get({ url: '/trade/delivery/express/get?id=' + id }) +} + +// 获得快递公司精简信息列表 +export const getSimpleDeliveryExpressList = () => { + return request.get({ url: '/trade/delivery/express/list-all-simple' }) +} + +// 新增快递公司 +export const createDeliveryExpress = async (data: DeliveryExpressVO) => { + return await request.post({ url: '/trade/delivery/express/create', data }) +} + +// 修改快递公司 +export const updateDeliveryExpress = async (data: DeliveryExpressVO) => { + return await request.put({ url: '/trade/delivery/express/update', data }) +} + +// 删除快递公司 +export const deleteDeliveryExpress = async (id: number) => { + return await request.delete({ url: '/trade/delivery/express/delete?id=' + id }) +} + +// 导出快递公司 Excel +export const exportDeliveryExpressApi = async (params) => { + return await request.download({ url: '/trade/delivery/express/export-excel', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/delivery/expressTemplate/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/delivery/expressTemplate/index.ts new file mode 100644 index 00000000..9ed23bc1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/delivery/expressTemplate/index.ts @@ -0,0 +1,54 @@ +import request from '@/config/axios' + +export interface DeliveryExpressTemplateVO { + id: number + name: string + chargeMode: number + sort: number + templateCharge: ExpressTemplateChargeVO[] + templateFree: ExpressTemplateFreeVO[] +} + +export declare type ExpressTemplateChargeVO = { + areaIds: number[] + startCount: number + startPrice: number + extraCount: number + extraPrice: number +} + +export declare type ExpressTemplateFreeVO = { + areaIds: number[] + freeCount: number + freePrice: number +} + +// 查询快递运费模板列表 +export const getDeliveryExpressTemplatePage = async (params: PageParam) => { + return await request.get({ url: '/trade/delivery/express-template/page', params }) +} + +// 查询快递运费模板详情 +export const getDeliveryExpressTemplate = async (id: number) => { + return await request.get({ url: '/trade/delivery/express-template/get?id=' + id }) +} + +// 查询快递运费模板详情 +export const getSimpleTemplateList = async () => { + return await request.get({ url: '/trade/delivery/express-template/list-all-simple' }) +} + +// 新增快递运费模板 +export const createDeliveryExpressTemplate = async (data: DeliveryExpressTemplateVO) => { + return await request.post({ url: '/trade/delivery/express-template/create', data }) +} + +// 修改快递运费模板 +export const updateDeliveryExpressTemplate = async (data: DeliveryExpressTemplateVO) => { + return await request.put({ url: '/trade/delivery/express-template/update', data }) +} + +// 删除快递运费模板 +export const deleteDeliveryExpressTemplate = async (id: number) => { + return await request.delete({ url: '/trade/delivery/express-template/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/delivery/pickUpStore/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/delivery/pickUpStore/index.ts new file mode 100644 index 00000000..c3175021 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/delivery/pickUpStore/index.ts @@ -0,0 +1,46 @@ +import request from '@/config/axios' + +export interface DeliveryPickUpStoreVO { + id: number + name: string + introduction: string + phone: string + areaId: number + detailAddress: string + logo: string + openingTime: string + closingTime: string + latitude: number + longitude: number + status: number +} + +// 查询自提门店列表 +export const getDeliveryPickUpStorePage = async (params) => { + return await request.get({ url: '/trade/delivery/pick-up-store/page', params }) +} + +// 查询自提门店详情 +export const getDeliveryPickUpStore = async (id: number) => { + return await request.get({ url: '/trade/delivery/pick-up-store/get?id=' + id }) +} + +// 查询自提门店精简列表 +export const getListAllSimple = async (): Promise => { + return await request.get({ url: '/trade/delivery/pick-up-store/list-all-simple' }) +} + +// 新增自提门店 +export const createDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) => { + return await request.post({ url: '/trade/delivery/pick-up-store/create', data }) +} + +// 修改自提门店 +export const updateDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) => { + return await request.put({ url: '/trade/delivery/pick-up-store/update', data }) +} + +// 删除自提门店 +export const deleteDeliveryPickUpStore = async (id: number) => { + return await request.delete({ url: '/trade/delivery/pick-up-store/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/order/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/order/index.ts new file mode 100644 index 00000000..364483b8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mall/trade/order/index.ts @@ -0,0 +1,188 @@ +import request from '@/config/axios' + +export interface OrderVO { + // ========== 订单基本信息 ========== + id?: number | null // 订单编号 + no?: string // 订单流水号 + createTime?: Date | null // 下单时间 + type?: number | null // 订单类型 + terminal?: number | null // 订单来源 + userId?: number | null // 用户编号 + userIp?: string // 用户 IP + userRemark?: string // 用户备注 + status?: number | null // 订单状态 + productCount?: number | null // 购买的商品数量 + finishTime?: Date | null // 订单完成时间 + cancelTime?: Date | null // 订单取消时间 + cancelType?: number | null // 取消类型 + remark?: string // 商家备注 + + // ========== 价格 + 支付基本信息 ========== + payOrderId?: number | null // 支付订单编号 + payStatus?: boolean // 是否已支付 + payTime?: Date | null // 付款时间 + payChannelCode?: string // 支付渠道 + totalPrice?: number | null // 商品原价(总) + discountPrice?: number | null // 订单优惠(总) + deliveryPrice?: number | null // 运费金额 + adjustPrice?: number | null // 订单调价(总) + payPrice?: number | null // 应付金额(总) + // ========== 收件 + 物流基本信息 ========== + deliveryType?: number | null // 发货方式 + pickUpStoreId?: number // 自提门店编号 + pickUpVerifyCode?: string // 自提核销码 + deliveryTemplateId?: number | null // 配送模板编号 + logisticsId?: number | null // 发货物流公司编号 + logisticsNo?: string // 发货物流单号 + deliveryTime?: Date | null // 发货时间 + receiveTime?: Date | null // 收货时间 + receiverName?: string // 收件人名称 + receiverMobile?: string // 收件人手机 + receiverPostCode?: number | null // 收件人邮编 + receiverAreaId?: number | null // 收件人地区编号 + receiverAreaName?: string //收件人地区名字 + receiverDetailAddress?: string // 收件人详细地址 + + // ========== 售后基本信息 ========== + afterSaleStatus?: number | null // 售后状态 + refundPrice?: number | null // 退款金额 + + // ========== 营销基本信息 ========== + couponId?: number | null // 优惠劵编号 + couponPrice?: number | null // 优惠劵减免金额 + pointPrice?: number | null // 积分抵扣的金额 + vipPrice?: number | null // VIP 减免金额 + + items?: OrderItemRespVO[] // 订单项列表 + // 下单用户信息 + user?: { + id?: number | null + nickname?: string + avatar?: string + } + // 推广用户信息 + brokerageUser?: { + id?: number | null + nickname?: string + avatar?: string + } + // 订单操作日志 + logs?: OrderLogRespVO[] +} + +export interface OrderLogRespVO { + content?: string + createTime?: Date + userType?: number +} + +export interface OrderItemRespVO { + // ========== 订单项基本信息 ========== + id?: number | null // 编号 + userId?: number | null // 用户编号 + orderId?: number | null // 订单编号 + // ========== 商品基本信息 ========== + spuId?: number | null // 商品 SPU 编号 + spuName?: string //商品 SPU 名称 + skuId?: number | null // 商品 SKU 编号 + picUrl?: string //商品图片 + count?: number | null //购买数量 + // ========== 价格 + 支付基本信息 ========== + originalPrice?: number | null //商品原价(总) + originalUnitPrice?: number | null //商品原价(单) + discountPrice?: number | null //商品优惠(总) + payPrice?: number | null //商品实付金额(总) + orderPartPrice?: number | null //子订单分摊金额(总) + orderDividePrice?: number | null //分摊后子订单实付金额(总) + // ========== 营销基本信息 ========== + // TODO 芋艿:在捉摸一下 + // ========== 售后基本信息 ========== + afterSaleStatus?: number | null // 售后状态 + properties?: ProductPropertiesVO[] //属性数组 +} + +export interface ProductPropertiesVO { + propertyId?: number | null // 属性的编号 + propertyName?: string // 属性的名称 + valueId?: number | null //属性值的编号 + valueName?: string // 属性值的名称 +} + +/** 交易订单统计 */ +export interface TradeOrderSummaryRespVO { + /** 订单数量 */ + orderCount?: number + /** 订单金额 */ + orderPayPrice?: string + /** 退款单数 */ + afterSaleCount?: number + /** 退款金额 */ + afterSalePrice?: string +} + +// 查询交易订单列表 +export const getOrderPage = async (params: any) => { + return await request.get({ url: `/trade/order/page`, params }) +} + +// 查询交易订单统计 +export const getOrderSummary = async (params: any) => { + return await request.get({ url: `/trade/order/summary`, params }) +} + +// 查询交易订单详情 +export const getOrder = async (id: number | null) => { + return await request.get({ url: `/trade/order/get-detail?id=` + id }) +} + +// 查询交易订单物流详情 +export const getExpressTrackList = async (id: number | null) => { + return await request.get({ url: `/trade/order/get-express-track-list?id=` + id }) +} + +export interface DeliveryVO { + id: number // 订单编号 + logisticsId: number | null // 物流公司编号 + logisticsNo: string // 物流编号 +} + +// 订单发货 +export const deliveryOrder = async (data: DeliveryVO) => { + return await request.put({ url: `/trade/order/delivery`, data }) +} + +// 订单备注 +export const updateOrderRemark = async (data: any) => { + return await request.put({ url: `/trade/order/update-remark`, data }) +} + +// 订单调价 +export const updateOrderPrice = async (data: any) => { + return await request.put({ url: `/trade/order/update-price`, data }) +} + +// 修改订单地址 +export const updateOrderAddress = async (data: any) => { + return await request.put({ url: `/trade/order/update-address`, data }) +} + +// 订单核销 +export const pickUpOrder = async (id: number) => { + return await request.put({ url: `/trade/order/pick-up-by-id?id=${id}` }) +} + +// 订单核销 +export const pickUpOrderByVerifyCode = async (pickUpVerifyCode: string) => { + return await request.put({ + url: `/trade/order/pick-up-by-verify-code`, + params: { pickUpVerifyCode } + }) +} + +// 查询核销码对应的订单 +export const getOrderByPickUpVerifyCode = async (pickUpVerifyCode: string) => { + return await request.get({ + url: `/trade/order/get-by-pick-up-verify-code`, + params: { pickUpVerifyCode } + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/address/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/address/index.ts new file mode 100644 index 00000000..a914f979 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/address/index.ts @@ -0,0 +1,15 @@ +import request from '@/config/axios' + +export interface AddressVO { + id: number + name: string + mobile: string + areaId: number + detailAddress: string + defaultStatus: boolean +} + +// 查询用户收件地址列表 +export const getAddressList = async (params) => { + return await request.get({ url: `/member/address/list`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/config/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/config/index.ts new file mode 100644 index 00000000..7ddca16b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/config/index.ts @@ -0,0 +1,19 @@ +import request from '@/config/axios' + +export interface ConfigVO { + id: number + pointTradeDeductEnable: number + pointTradeDeductUnitPrice: number + pointTradeDeductMaxPrice: number + pointTradeGivePoint: number +} + +// 查询积分设置详情 +export const getConfig = async () => { + return await request.get({ url: `/member/config/get` }) +} + +// 新增修改积分设置 +export const saveConfig = async (data: ConfigVO) => { + return await request.put({ url: `/member/config/save`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/experience-record/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/experience-record/index.ts new file mode 100644 index 00000000..6d40a48d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/experience-record/index.ts @@ -0,0 +1,22 @@ +import request from '@/config/axios' + +export interface ExperienceRecordVO { + id: number + userId: number + bizId: string + bizType: number + title: string + description: string + experience: number + totalExperience: number +} + +// 查询会员经验记录列表 +export const getExperienceRecordPage = async (params) => { + return await request.get({ url: `/member/experience-record/page`, params }) +} + +// 查询会员经验记录详情 +export const getExperienceRecord = async (id: number) => { + return await request.get({ url: `/member/experience-record/get?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/group/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/group/index.ts new file mode 100644 index 00000000..df3054e2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/group/index.ts @@ -0,0 +1,38 @@ +import request from '@/config/axios' + +export interface GroupVO { + id: number + name: string + remark: string + status: number +} + +// 查询用户分组列表 +export const getGroupPage = async (params: any) => { + return await request.get({ url: `/member/group/page`, params }) +} + +// 查询用户分组详情 +export const getGroup = async (id: number) => { + return await request.get({ url: `/member/group/get?id=` + id }) +} + +// 新增用户分组 +export const createGroup = async (data: GroupVO) => { + return await request.post({ url: `/member/group/create`, data }) +} + +// 查询用户分组 - 精简信息列表 +export const getSimpleGroupList = async () => { + return await request.get({ url: `/member/group/list-all-simple` }) +} + +// 修改用户分组 +export const updateGroup = async (data: GroupVO) => { + return await request.put({ url: `/member/group/update`, data }) +} + +// 删除用户分组 +export const deleteGroup = async (id: number) => { + return await request.delete({ url: `/member/group/delete?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/level/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/level/index.ts new file mode 100644 index 00000000..0ded493a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/level/index.ts @@ -0,0 +1,42 @@ +import request from '@/config/axios' + +export interface LevelVO { + id: number + name: string + experience: number + value: number + discountPercent: number + icon: string + bgUrl: string + status: number +} + +// 查询会员等级列表 +export const getLevelList = async (params) => { + return await request.get({ url: `/member/level/list`, params }) +} + +// 查询会员等级详情 +export const getLevel = async (id: number) => { + return await request.get({ url: `/member/level/get?id=` + id }) +} + +// 查询会员等级 - 精简信息列表 +export const getSimpleLevelList = async () => { + return await request.get({ url: `/member/level/list-all-simple` }) +} + +// 新增会员等级 +export const createLevel = async (data: LevelVO) => { + return await request.post({ url: `/member/level/create`, data }) +} + +// 修改会员等级 +export const updateLevel = async (data: LevelVO) => { + return await request.put({ url: `/member/level/update`, data }) +} + +// 删除会员等级 +export const deleteLevel = async (id: number) => { + return await request.delete({ url: `/member/level/delete?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/point/record/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/point/record/index.ts new file mode 100644 index 00000000..f47ae467 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/point/record/index.ts @@ -0,0 +1,18 @@ +import request from '@/config/axios' + +export interface RecordVO { + id: number + bizId: string + bizType: string + title: string + description: string + point: number + totalPoint: number + userId: number + createDate: Date +} + +// 查询用户积分记录列表 +export const getRecordPage = async (params) => { + return await request.get({ url: `/member/point/record/page`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/signin/config/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/signin/config/index.ts new file mode 100644 index 00000000..50a7d63c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/signin/config/index.ts @@ -0,0 +1,34 @@ +import request from '@/config/axios' + +export interface SignInConfigVO { + id?: number + day?: number + point?: number + experience?: number + status?: number +} + +// 查询积分签到规则列表 +export const getSignInConfigList = async () => { + return await request.get({ url: `/member/sign-in/config/list` }) +} + +// 查询积分签到规则详情 +export const getSignInConfig = async (id: number) => { + return await request.get({ url: `/member/sign-in/config/get?id=` + id }) +} + +// 新增积分签到规则 +export const createSignInConfig = async (data: SignInConfigVO) => { + return await request.post({ url: `/member/sign-in/config/create`, data }) +} + +// 修改积分签到规则 +export const updateSignInConfig = async (data: SignInConfigVO) => { + return await request.put({ url: `/member/sign-in/config/update`, data }) +} + +// 删除积分签到规则 +export const deleteSignInConfig = async (id: number) => { + return await request.delete({ url: `/member/sign-in/config/delete?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/signin/record/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/signin/record/index.ts new file mode 100644 index 00000000..7d137029 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/signin/record/index.ts @@ -0,0 +1,13 @@ +import request from '@/config/axios' + +export interface SignInRecordVO { + id: number + userId: number + day: number + point: number +} + +// 查询用户签到积分列表 +export const getSignInRecordPage = async (params) => { + return await request.get({ url: `/member/sign-in/record/page`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/tag/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/tag/index.ts new file mode 100644 index 00000000..7ff6e9bf --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/tag/index.ts @@ -0,0 +1,36 @@ +import request from '@/config/axios' + +export interface TagVO { + id: number + name: string +} + +// 查询会员标签列表 +export const getMemberTagPage = async (params: any) => { + return await request.get({ url: `/member/tag/page`, params }) +} + +// 查询会员标签详情 +export const getMemberTag = async (id: number) => { + return await request.get({ url: `/member/tag/get?id=` + id }) +} + +// 查询会员标签 - 精简信息列表 +export const getSimpleTagList = async () => { + return await request.get({ url: `/member/tag/list-all-simple` }) +} + +// 新增会员标签 +export const createMemberTag = async (data: TagVO) => { + return await request.post({ url: `/member/tag/create`, data }) +} + +// 修改会员标签 +export const updateMemberTag = async (data: TagVO) => { + return await request.put({ url: `/member/tag/update`, data }) +} + +// 删除会员标签 +export const deleteMemberTag = async (id: number) => { + return await request.delete({ url: `/member/tag/delete?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/member/user/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/member/user/index.ts new file mode 100644 index 00000000..e38206a8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/member/user/index.ts @@ -0,0 +1,53 @@ +import request from '@/config/axios' + +export interface UserVO { + id: number + avatar: string | undefined + birthday: number | undefined + createTime: number | undefined + loginDate: number | undefined + loginIp: string + mark: string + mobile: string + name: string | undefined + nickname: string | undefined + registerIp: string + sex: number + status: number + areaId: number | undefined + areaName: string | undefined + levelName: string | null + point: number | undefined | null + totalPoint: number | undefined | null + experience: number | null | undefined +} + +// 查询会员用户列表 +export const getUserPage = async (params) => { + return await request.get({ url: `/member/user/page`, params }) +} + +// 查询会员用户详情 +export const getUser = async (id: number) => { + return await request.get({ url: `/member/user/get?id=` + id }) +} + +// 修改会员用户 +export const updateUser = async (data: UserVO) => { + return await request.put({ url: `/member/user/update`, data }) +} + +// 修改会员用户等级 +export const updateUserLevel = async (data: any) => { + return await request.put({ url: `/member/user/update-level`, data }) +} + +// 修改会员用户积分 +export const updateUserPoint = async (data: any) => { + return await request.put({ url: `/member/user/update-point`, data }) +} + +// 修改会员用户余额 +export const updateUserBalance = async (data: any) => { + return await request.put({ url: `/member/user/update-balance`, data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/account/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/account/index.ts new file mode 100644 index 00000000..e973cda3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/account/index.ts @@ -0,0 +1,46 @@ +import request from '@/config/axios' + +export interface AccountVO { + id: number + name: string +} + +// 创建公众号账号 +export const createAccount = async (data) => { + return await request.post({ url: '/mp/account/create', data }) +} + +// 更新公众号账号 +export const updateAccount = async (data) => { + return request.put({ url: '/mp/account/update', data: data }) +} + +// 删除公众号账号 +export const deleteAccount = async (id) => { + return request.delete({ url: '/mp/account/delete?id=' + id, method: 'delete' }) +} + +// 获得公众号账号 +export const getAccount = async (id) => { + return request.get({ url: '/mp/account/get?id=' + id }) +} + +// 获得公众号账号分页 +export const getAccountPage = async (query) => { + return request.get({ url: '/mp/account/page', params: query }) +} + +// 获取公众号账号精简信息列表 +export const getSimpleAccountList = async () => { + return request.get({ url: '/mp/account/list-all-simple' }) +} + +// 生成公众号二维码 +export const generateAccountQrCode = async (id) => { + return request.put({ url: '/mp/account/generate-qr-code?id=' + id }) +} + +// 清空公众号 API 配额 +export const clearAccountQuota = async (id) => { + return request.put({ url: '/mp/account/clear-quota?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/autoReply/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/autoReply/index.ts new file mode 100644 index 00000000..5045e6d5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/autoReply/index.ts @@ -0,0 +1,39 @@ +import request from '@/config/axios' + +// 创建公众号的自动回复 +export const createAutoReply = (data) => { + return request.post({ + url: '/mp/auto-reply/create', + data: data + }) +} + +// 更新公众号的自动回复 +export const updateAutoReply = (data) => { + return request.put({ + url: '/mp/auto-reply/update', + data: data + }) +} + +// 删除公众号的自动回复 +export const deleteAutoReply = (id) => { + return request.delete({ + url: '/mp/auto-reply/delete?id=' + id + }) +} + +// 获得公众号的自动回复 +export const getAutoReply = (id) => { + return request.get({ + url: '/mp/auto-reply/get?id=' + id + }) +} + +// 获得公众号的自动回复分页 +export const getAutoReplyPage = (query) => { + return request.get({ + url: '/mp/auto-reply/page', + params: query + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/draft/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/draft/index.ts new file mode 100644 index 00000000..ce6a4431 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/draft/index.ts @@ -0,0 +1,35 @@ +import request from '@/config/axios' + +// 获得公众号草稿分页 +export const getDraftPage = (query) => { + return request.get({ + url: '/mp/draft/page', + params: query + }) +} + +// 创建公众号草稿 +export const createDraft = (accountId, articles) => { + return request.post({ + url: '/mp/draft/create?accountId=' + accountId, + data: { + articles + } + }) +} + +// 更新公众号草稿 +export const updateDraft = (accountId, mediaId, articles) => { + return request.put({ + url: '/mp/draft/update?accountId=' + accountId + '&mediaId=' + mediaId, + method: 'put', + data: articles + }) +} + +// 删除公众号草稿 +export const deleteDraft = (accountId, mediaId) => { + return request.delete({ + url: '/mp/draft/delete?accountId=' + accountId + '&mediaId=' + mediaId + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/freePublish/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/freePublish/index.ts new file mode 100644 index 00000000..beef0262 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/freePublish/index.ts @@ -0,0 +1,23 @@ +import request from '@/config/axios' + +// 获得公众号素材分页 +export const getFreePublishPage = (query) => { + return request.get({ + url: '/mp/free-publish/page', + params: query + }) +} + +// 删除公众号素材 +export const deleteFreePublish = (accountId, articleId) => { + return request.delete({ + url: '/mp/free-publish/delete?accountId=' + accountId + '&articleId=' + articleId + }) +} + +// 发布公众号素材 +export const submitFreePublish = (accountId, mediaId) => { + return request.post({ + url: '/mp/free-publish/submit?accountId=' + accountId + '&mediaId=' + mediaId + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/material/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/material/index.ts new file mode 100644 index 00000000..fcc37abe --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/material/index.ts @@ -0,0 +1,16 @@ +import request from '@/config/axios' + +// 获得公众号素材分页 +export const getMaterialPage = (query) => { + return request.get({ + url: '/mp/material/page', + params: query + }) +} + +// 删除公众号永久素材 +export const deletePermanentMaterial = (id) => { + return request.delete({ + url: '/mp/material/delete-permanent?id=' + id + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/menu/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/menu/index.ts new file mode 100644 index 00000000..cc78647c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/menu/index.ts @@ -0,0 +1,26 @@ +import request from '@/config/axios' + +// 获得公众号菜单列表 +export const getMenuList = (accountId) => { + return request.get({ + url: '/mp/menu/list?accountId=' + accountId + }) +} + +// 保存公众号菜单 +export const saveMenu = (accountId, menus) => { + return request.post({ + url: '/mp/menu/save', + data: { + accountId, + menus + } + }) +} + +// 删除公众号菜单 +export const deleteMenu = (accountId) => { + return request.delete({ + url: '/mp/menu/delete?accountId=' + accountId + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/message/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/message/index.ts new file mode 100644 index 00000000..ad9b95dd --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/message/index.ts @@ -0,0 +1,17 @@ +import request from '@/config/axios' + +// 获得公众号消息分页 +export const getMessagePage = (query: PageParam) => { + return request.get({ + url: '/mp/message/page', + params: query + }) +} + +// 给粉丝发送消息 +export const sendMessage = (data) => { + return request.post({ + url: '/mp/message/send', + data: data + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/statistics/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/statistics/index.ts new file mode 100644 index 00000000..72cae601 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/statistics/index.ts @@ -0,0 +1,33 @@ +import request from '@/config/axios' + +// 获取消息发送概况数据 +export const getUpstreamMessage = (query) => { + return request.get({ + url: '/mp/statistics/upstream-message', + params: query + }) +} + +// 用户增减数据 +export const getUserSummary = (query) => { + return request.get({ + url: '/mp/statistics/user-summary', + params: query + }) +} + +// 获得用户累计数据 +export const getUserCumulate = (query) => { + return request.get({ + url: '/mp/statistics/user-cumulate', + params: query + }) +} + +// 获得接口分析数据 +export const getInterfaceSummary = (query) => { + return request.get({ + url: '/mp/statistics/interface-summary', + params: query + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/tag/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/tag/index.ts new file mode 100644 index 00000000..50183a51 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/tag/index.ts @@ -0,0 +1,60 @@ +import request from '@/config/axios' + +export interface TagVO { + id?: number + name: string + accountId: number + createTime: Date +} + +// 创建公众号标签 +export const createTag = (data: TagVO) => { + return request.post({ + url: '/mp/tag/create', + data: data + }) +} + +// 更新公众号标签 +export const updateTag = (data: TagVO) => { + return request.put({ + url: '/mp/tag/update', + data: data + }) +} + +// 删除公众号标签 +export const deleteTag = (id: number) => { + return request.delete({ + url: '/mp/tag/delete?id=' + id + }) +} + +// 获得公众号标签 +export const getTag = (id: number) => { + return request.get({ + url: '/mp/tag/get?id=' + id + }) +} + +// 获得公众号标签分页 +export const getTagPage = (query: PageParam) => { + return request.get({ + url: '/mp/tag/page', + params: query + }) +} + +// 获取公众号标签精简信息列表 +export const getSimpleTagList = () => { + return request.get({ + url: '/mp/tag/list-all-simple' + }) +} + +// 同步公众号标签 +export const syncTag = (accountId: number) => { + return request.post({ + url: '/mp/tag/sync?accountId=' + accountId + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/mp/user/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/mp/user/index.ts new file mode 100644 index 00000000..b89acc7d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/mp/user/index.ts @@ -0,0 +1,31 @@ +import request from '@/config/axios' + +// 更新公众号粉丝 +export const updateUser = (data) => { + return request.put({ + url: '/mp/user/update', + data: data + }) +} + +// 获得公众号粉丝 +export const getUser = (id) => { + return request.get({ + url: '/mp/user/get?id=' + id + }) +} + +// 获得公众号粉丝分页 +export const getUserPage = (query) => { + return request.get({ + url: '/mp/user/page', + params: query + }) +} + +// 同步公众号粉丝 +export const syncUser = (accountId) => { + return request.post({ + url: '/mp/user/sync?accountId=' + accountId + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/app/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/app/index.ts new file mode 100644 index 00000000..4bb06b36 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/app/index.ts @@ -0,0 +1,65 @@ +import request from '@/config/axios' + +export interface AppVO { + id: number + name: string + status: number + remark: string + payNotifyUrl: string + refundNotifyUrl: string + merchantId: number + merchantName: string + createTime: Date +} + +export interface AppPageReqVO extends PageParam { + name?: string + status?: number + remark?: string + payNotifyUrl?: string + refundNotifyUrl?: string + merchantName?: string + createTime?: Date[] +} + +export interface AppUpdateStatusReqVO { + id: number + status: number +} + +// 查询列表支付应用 +export const getAppPage = (params: AppPageReqVO) => { + return request.get({ url: '/pay/app/page', params }) +} + +// 查询详情支付应用 +export const getApp = (id: number) => { + return request.get({ url: '/pay/app/get?id=' + id }) +} + +// 新增支付应用 +export const createApp = (data: AppVO) => { + return request.post({ url: '/pay/app/create', data }) +} + +// 修改支付应用 +export const updateApp = (data: AppVO) => { + return request.put({ url: '/pay/app/update', data }) +} + +// 支付应用信息状态修改 +export const changeAppStatus = (data: AppUpdateStatusReqVO) => { + return request.put({ url: '/pay/app/update-status', data: data }) +} + +// 删除支付应用 +export const deleteApp = (id: number) => { + return request.delete({ url: '/pay/app/delete?id=' + id }) +} + +// 获得支付应用列表 +export const getAppList = () => { + return request.get({ + url: '/pay/app/list' + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/channel/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/channel/index.ts new file mode 100644 index 00000000..0f4ff424 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/channel/index.ts @@ -0,0 +1,46 @@ +import request from '@/config/axios' + +export interface ChannelVO { + id: number + code: string + config: string + status: number + remark: string + feeRate: number + appId: number + createTime: Date +} + +// 查询列表支付渠道 +export const getChannelPage = (params: PageParam) => { + return request.get({ url: '/pay/channel/page', params }) +} + +// 查询详情支付渠道 +export const getChannel = (appId: string, code: string) => { + const params = { + appId: appId, + code: code + } + return request.get({ url: '/pay/channel/get', params: params }) +} + +// 新增支付渠道 +export const createChannel = (data: ChannelVO) => { + return request.post({ url: '/pay/channel/create', data }) +} + +// 修改支付渠道 +export const updateChannel = (data: ChannelVO) => { + return request.put({ url: '/pay/channel/update', data }) +} + +// 删除支付渠道 +export const deleteChannel = (id: number) => { + return request.delete({ url: '/pay/channel/delete?id=' + id }) +} + +// 导出支付渠道 +export const exportChannel = (params) => { + return request.download({ url: '/pay/channel/export-excel', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/demo/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/demo/index.ts new file mode 100644 index 00000000..3824a8b2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/demo/index.ts @@ -0,0 +1,36 @@ +import request from '@/config/axios' + +export interface DemoOrderVO { + spuId: number + createTime: Date +} + +// 创建示例订单 +export function createDemoOrder(data: DemoOrderVO) { + return request.post({ + url: '/pay/demo-order/create', + data: data + }) +} + +// 获得示例订单 +export function getDemoOrder(id: number) { + return request.get({ + url: '/pay/demo-order/get?id=' + id + }) +} + +// 获得示例订单分页 +export function getDemoOrderPage(query: PageParam) { + return request.get({ + url: '/pay/demo-order/page', + params: query + }) +} + +// 退款示例订单 +export function refundDemoOrder(id) { + return request.put({ + url: '/pay/demo-order/refund?id=' + id + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/demo/transfer/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/demo/transfer/index.ts new file mode 100644 index 00000000..a95b0d5c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/demo/transfer/index.ts @@ -0,0 +1,25 @@ +import request from '@/config/axios' + +export interface DemoTransferVO { + price: number + type: number + userName: string + alipayLogonId: string + openid: string +} + +// 创建示例转账单 +export function createDemoTransfer(data: DemoTransferVO) { + return request.post({ + url: '/pay/demo-transfer/create', + data: data + }) +} + +// 获得示例订单分页 +export function getDemoTransferPage(query: PageParam) { + return request.get({ + url: '/pay/demo-transfer/page', + params: query + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/notify/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/notify/index.ts new file mode 100644 index 00000000..dc8bd887 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/notify/index.ts @@ -0,0 +1,16 @@ +import request from '@/config/axios' + +// 获得支付通知明细 +export const getNotifyTaskDetail = (id) => { + return request.get({ + url: '/pay/notify/get-detail?id=' + id + }) +} + +// 获得支付通知分页 +export const getNotifyTaskPage = (query) => { + return request.get({ + url: '/pay/notify/page', + params: query + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/order/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/order/index.ts new file mode 100644 index 00000000..71960a8a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/order/index.ts @@ -0,0 +1,104 @@ +import request from '@/config/axios' + +export interface OrderVO { + id: number + merchantId: number + appId: number + channelId: number + channelCode: string + merchantOrderId: string + subject: string + body: string + notifyUrl: string + notifyStatus: number + amount: number + channelFeeRate: number + channelFeeAmount: number + status: number + userIp: string + expireTime: Date + successTime: Date + notifyTime: Date + successExtensionId: number + refundStatus: number + refundTimes: number + refundAmount: number + channelUserId: string + channelOrderNo: string + createTime: Date +} + +export interface OrderPageReqVO extends PageParam { + merchantId?: number + appId?: number + channelId?: number + channelCode?: string + merchantOrderId?: string + subject?: string + body?: string + notifyUrl?: string + notifyStatus?: number + amount?: number + channelFeeRate?: number + channelFeeAmount?: number + status?: number + expireTime?: Date[] + successTime?: Date[] + notifyTime?: Date[] + successExtensionId?: number + refundStatus?: number + refundTimes?: number + channelUserId?: string + channelOrderNo?: string + createTime?: Date[] +} + +export interface OrderExportReqVO { + merchantId?: number + appId?: number + channelId?: number + channelCode?: string + merchantOrderId?: string + subject?: string + body?: string + notifyUrl?: string + notifyStatus?: number + amount?: number + channelFeeRate?: number + channelFeeAmount?: number + status?: number + expireTime?: Date[] + successTime?: Date[] + notifyTime?: Date[] + successExtensionId?: number + refundStatus?: number + refundTimes?: number + channelUserId?: string + channelOrderNo?: string + createTime?: Date[] +} + +// 查询列表支付订单 +export const getOrderPage = async (params: OrderPageReqVO) => { + return await request.get({ url: '/pay/order/page', params }) +} + +// 查询详情支付订单 +export const getOrder = async (id: number) => { + return await request.get({ url: '/pay/order/get?id=' + id }) +} + +// 获得支付订单的明细 +export const getOrderDetail = async (id: number) => { + return await request.get({ url: '/pay/order/get-detail?id=' + id }) +} + +// 提交支付订单 +export const submitOrder = async (data: any) => { + return await request.post({ url: '/pay/order/submit', data }) +} + +// 导出支付订单 +export const exportOrder = async (params: OrderExportReqVO) => { + return await request.download({ url: '/pay/order/export-excel', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/refund/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/refund/index.ts new file mode 100644 index 00000000..4b587f22 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/refund/index.ts @@ -0,0 +1,116 @@ +import request from '@/config/axios' + +export interface RefundVO { + id: number + merchantId: number + appId: number + channelId: number + channelCode: string + orderId: string + tradeNo: string + merchantOrderId: string + merchantRefundNo: string + notifyUrl: string + notifyStatus: number + status: number + type: number + payAmount: number + refundAmount: number + reason: string + userIp: string + channelOrderNo: string + channelRefundNo: string + channelErrorCode: string + channelErrorMsg: string + channelExtras: string + expireTime: Date + successTime: Date + notifyTime: Date + createTime: Date +} + +export interface RefundPageReqVO extends PageParam { + merchantId?: number + appId?: number + channelId?: number + channelCode?: string + orderId?: string + tradeNo?: string + merchantOrderId?: string + merchantRefundNo?: string + notifyUrl?: string + notifyStatus?: number + status?: number + type?: number + payAmount?: number + refundAmount?: number + reason?: string + userIp?: string + channelOrderNo?: string + channelRefundNo?: string + channelErrorCode?: string + channelErrorMsg?: string + channelExtras?: string + expireTime?: Date[] + successTime?: Date[] + notifyTime?: Date[] + createTime?: Date[] +} + +export interface PayRefundExportReqVO { + merchantId?: number + appId?: number + channelId?: number + channelCode?: string + orderId?: string + tradeNo?: string + merchantOrderId?: string + merchantRefundNo?: string + notifyUrl?: string + notifyStatus?: number + status?: number + type?: number + payAmount?: number + refundAmount?: number + reason?: string + userIp?: string + channelOrderNo?: string + channelRefundNo?: string + channelErrorCode?: string + channelErrorMsg?: string + channelExtras?: string + expireTime?: Date[] + successTime?: Date[] + notifyTime?: Date[] + createTime?: Date[] +} + +// 查询列表退款订单 +export const getRefundPage = (params: RefundPageReqVO) => { + return request.get({ url: '/pay/refund/page', params }) +} + +// 查询详情退款订单 +export const getRefund = (id: number) => { + return request.get({ url: '/pay/refund/get?id=' + id }) +} + +// 新增退款订单 +export const createRefund = (data: RefundVO) => { + return request.post({ url: '/pay/refund/create', data }) +} + +// 修改退款订单 +export const updateRefund = (data: RefundVO) => { + return request.put({ url: '/pay/refund/update', data }) +} + +// 删除退款订单 +export const deleteRefund = (id: number) => { + return request.delete({ url: '/pay/refund/delete?id=' + id }) +} + +// 导出退款订单 +export const exportRefund = (params: PayRefundExportReqVO) => { + return request.download({ url: '/pay/refund/export-excel', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/transfer/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/transfer/index.ts new file mode 100644 index 00000000..7a58abf4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/transfer/index.ts @@ -0,0 +1,27 @@ +import request from '@/config/axios' + +export interface TransferVO { + appId: number + channelCode: string + merchantTransferId: string + type: number + price: number + subject: string + userName: string + alipayLogonId: string + openid: string +} + +// 新增转账单 +export const createTransfer = async (data: TransferVO) => { + return await request.post({ url: `/pay/transfer/create`, data }) +} + +// 查询转账单列表 +export const getTransferPage = async (params) => { + return await request.get({ url: `/pay/transfer/page`, params }) +} + +export const getTransfer = async (id: number) => { + return await request.get({ url: '/pay/transfer/get?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/wallet/balance/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/wallet/balance/index.ts new file mode 100644 index 00000000..3e5ab369 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/wallet/balance/index.ts @@ -0,0 +1,26 @@ +import request from '@/config/axios' + +/** 用户钱包查询参数 */ +export interface PayWalletUserReqVO { + userId: number +} +/** 钱包 VO */ +export interface WalletVO { + id: number + userId: number + userType: number + balance: number + totalExpense: number + totalRecharge: number + freezePrice: number +} + +/** 查询用户钱包详情 */ +export const getWallet = async (params: PayWalletUserReqVO) => { + return await request.get({ url: `/pay/wallet/get`, params }) +} + +// 查询会员钱包列表 +export const getWalletPage = async (params) => { + return await request.get({ url: `/pay/wallet/page`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/wallet/rechargePackage/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/wallet/rechargePackage/index.ts new file mode 100644 index 00000000..c8e4cc9c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/wallet/rechargePackage/index.ts @@ -0,0 +1,34 @@ +import request from '@/config/axios' + +export interface WalletRechargePackageVO { + id: number + name: string + payPrice: number + bonusPrice: number + status: number +} + +// 查询套餐充值列表 +export const getWalletRechargePackagePage = async (params) => { + return await request.get({ url: '/pay/wallet-recharge-package/page', params }) +} + +// 查询套餐充值详情 +export const getWalletRechargePackage = async (id: number) => { + return await request.get({ url: '/pay/wallet-recharge-package/get?id=' + id }) +} + +// 新增套餐充值 +export const createWalletRechargePackage = async (data: WalletRechargePackageVO) => { + return await request.post({ url: '/pay/wallet-recharge-package/create', data }) +} + +// 修改套餐充值 +export const updateWalletRechargePackage = async (data: WalletRechargePackageVO) => { + return await request.put({ url: '/pay/wallet-recharge-package/update', data }) +} + +// 删除套餐充值 +export const deleteWalletRechargePackage = async (id: number) => { + return await request.delete({ url: '/pay/wallet-recharge-package/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/pay/wallet/transaction/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/pay/wallet/transaction/index.ts new file mode 100644 index 00000000..3377ffaa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/pay/wallet/transaction/index.ts @@ -0,0 +1,14 @@ +import request from '@/config/axios' + +export interface WalletTransactionVO { + id: number + walletId: number + title: string + price: number + balance: number +} + +// 查询会员钱包流水列表 +export const getWalletTransactionPage = async (params) => { + return await request.get({ url: `/pay/wallet-transaction/page`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/report/ureport/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/report/ureport/index.ts new file mode 100644 index 00000000..2a9daea4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/report/ureport/index.ts @@ -0,0 +1,39 @@ +import request from '@/config/axios' + +export interface UReportDataVO { + id: number + name: string + status: number + content: string + remark: string +} + +// 查询Ureport2报表分页 +export const getUReportDataPage = async (params) => { + return await request.get({ url: `/report/ureport-data/page`, params }) +} + +// 查询Ureport2报表详情 +export const getUReportData = async (id: number) => { + return await request.get({ url: `/report/ureport-data/get?id=` + id }) +} + +// 新增Ureport2报表 +export const createUReportData = async (data: UReportDataVO) => { + return await request.post({ url: `/report/ureport-data/create`, data }) +} + +// 修改Ureport2报表 +export const updateUReportData = async (data: UReportDataVO) => { + return await request.put({ url: `/report/ureport-data/update`, data }) +} + +// 删除Ureport2报表 +export const deleteUReportData = async (id: number) => { + return await request.delete({ url: `/report/ureport-data/delete?id=` + id }) +} + +// 导出Ureport2报表 Excel +export const exportUReportData = async (params) => { + return await request.download({ url: `/report/ureport-data/export-excel`, params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/area/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/area/index.ts new file mode 100644 index 00000000..e91a4997 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/area/index.ts @@ -0,0 +1,11 @@ +import request from '@/config/axios' + +// 获得地区树 +export const getAreaTree = async () => { + return await request.get({ url: '/system/area/tree' }) +} + +// 获得 IP 对应的地区名 +export const getAreaByIp = async (ip: string) => { + return await request.get({ url: '/system/area/get-by-ip?ip=' + ip }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/dept/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/dept/index.ts new file mode 100644 index 00000000..04d5c880 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/dept/index.ts @@ -0,0 +1,43 @@ +import request from '@/config/axios' + +export interface DeptVO { + id?: number + name: string + parentId: number + status: number + sort: number + leaderUserId: number + phone: string + email: string + createTime: Date +} + +// 查询部门(精简)列表 +export const getSimpleDeptList = async (): Promise => { + return await request.get({ url: '/system/dept/simple-list' }) +} + +// 查询部门列表 +export const getDeptPage = async (params: PageParam) => { + return await request.get({ url: '/system/dept/list', params }) +} + +// 查询部门详情 +export const getDept = async (id: number) => { + return await request.get({ url: '/system/dept/get?id=' + id }) +} + +// 新增部门 +export const createDept = async (data: DeptVO) => { + return await request.post({ url: '/system/dept/create', data: data }) +} + +// 修改部门 +export const updateDept = async (params: DeptVO) => { + return await request.put({ url: '/system/dept/update', data: params }) +} + +// 删除部门 +export const deleteDept = async (id: number) => { + return await request.delete({ url: '/system/dept/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/dict/dict.data.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/dict/dict.data.ts new file mode 100644 index 00000000..f4286481 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/dict/dict.data.ts @@ -0,0 +1,49 @@ +import request from '@/config/axios' + +export type DictDataVO = { + id: number | undefined + sort: number | undefined + label: string + value: string + dictType: string + status: number + colorType: string + cssClass: string + remark: string + createTime: Date +} + +// 查询字典数据(精简)列表 +export const getSimpleDictDataList = () => { + return request.get({ url: '/system/dict-data/simple-list' }) +} + +// 查询字典数据列表 +export const getDictDataPage = (params: PageParam) => { + return request.get({ url: '/system/dict-data/page', params }) +} + +// 查询字典数据详情 +export const getDictData = (id: number) => { + return request.get({ url: '/system/dict-data/get?id=' + id }) +} + +// 新增字典数据 +export const createDictData = (data: DictDataVO) => { + return request.post({ url: '/system/dict-data/create', data }) +} + +// 修改字典数据 +export const updateDictData = (data: DictDataVO) => { + return request.put({ url: '/system/dict-data/update', data }) +} + +// 删除字典数据 +export const deleteDictData = (id: number) => { + return request.delete({ url: '/system/dict-data/delete?id=' + id }) +} + +// 导出字典类型数据 +export const exportDictData = (params) => { + return request.download({ url: '/system/dict-data/export', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/dict/dict.type.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/dict/dict.type.ts new file mode 100644 index 00000000..eaa5fb6d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/dict/dict.type.ts @@ -0,0 +1,44 @@ +import request from '@/config/axios' + +export type DictTypeVO = { + id: number | undefined + name: string + type: string + status: number + remark: string + createTime: Date +} + +// 查询字典(精简)列表 +export const getSimpleDictTypeList = () => { + return request.get({ url: '/system/dict-type/list-all-simple' }) +} + +// 查询字典列表 +export const getDictTypePage = (params: PageParam) => { + return request.get({ url: '/system/dict-type/page', params }) +} + +// 查询字典详情 +export const getDictType = (id: number) => { + return request.get({ url: '/system/dict-type/get?id=' + id }) +} + +// 新增字典 +export const createDictType = (data: DictTypeVO) => { + return request.post({ url: '/system/dict-type/create', data }) +} + +// 修改字典 +export const updateDictType = (data: DictTypeVO) => { + return request.put({ url: '/system/dict-type/update', data }) +} + +// 删除字典 +export const deleteDictType = (id: number) => { + return request.delete({ url: '/system/dict-type/delete?id=' + id }) +} +// 导出字典类型 +export const exportDictType = (params) => { + return request.download({ url: '/system/dict-type/export', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/errorCode/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/errorCode/index.ts new file mode 100644 index 00000000..8a86a639 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/errorCode/index.ts @@ -0,0 +1,40 @@ +import request from '@/config/axios' + +export interface ErrorCodeVO { + id: number | undefined + type: number + applicationName: string + code: number | undefined + message: string + memo: string + createTime: Date +} + +// 查询错误码列表 +export const getErrorCodePage = (params: PageParam) => { + return request.get({ url: '/system/error-code/page', params }) +} + +// 查询错误码详情 +export const getErrorCode = (id: number) => { + return request.get({ url: '/system/error-code/get?id=' + id }) +} + +// 新增错误码 +export const createErrorCode = (data: ErrorCodeVO) => { + return request.post({ url: '/system/error-code/create', data }) +} + +// 修改错误码 +export const updateErrorCode = (data: ErrorCodeVO) => { + return request.put({ url: '/system/error-code/update', data }) +} + +// 删除错误码 +export const deleteErrorCode = (id: number) => { + return request.delete({ url: '/system/error-code/delete?id=' + id }) +} +// 导出错误码 +export const excelErrorCode = (params) => { + return request.download({ url: '/system/error-code/export-excel', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/loginLog/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/loginLog/index.ts new file mode 100644 index 00000000..7296f257 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/loginLog/index.ts @@ -0,0 +1,25 @@ +import request from '@/config/axios' + +export interface LoginLogVO { + id: number + logType: number + traceId: number + userId: number + userType: number + username: string + result: number + status: number + userIp: string + userAgent: string + createTime: Date +} + +// 查询登录日志列表 +export const getLoginLogPage = (params: PageParam) => { + return request.get({ url: '/system/login-log/page', params }) +} + +// 导出登录日志 +export const exportLoginLog = (params) => { + return request.download({ url: '/system/login-log/export', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/mail/account/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/mail/account/index.ts new file mode 100644 index 00000000..b8506b73 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/mail/account/index.ts @@ -0,0 +1,41 @@ +import request from '@/config/axios' + +export interface MailAccountVO { + id: number + mail: string + username: string + password: string + host: string + port: number + sslEnable: boolean +} + +// 查询邮箱账号列表 +export const getMailAccountPage = async (params: PageParam) => { + return await request.get({ url: '/system/mail-account/page', params }) +} + +// 查询邮箱账号详情 +export const getMailAccount = async (id: number) => { + return await request.get({ url: '/system/mail-account/get?id=' + id }) +} + +// 新增邮箱账号 +export const createMailAccount = async (data: MailAccountVO) => { + return await request.post({ url: '/system/mail-account/create', data }) +} + +// 修改邮箱账号 +export const updateMailAccount = async (data: MailAccountVO) => { + return await request.put({ url: '/system/mail-account/update', data }) +} + +// 删除邮箱账号 +export const deleteMailAccount = async (id: number) => { + return await request.delete({ url: '/system/mail-account/delete?id=' + id }) +} + +// 获得邮箱账号精简列表 +export const getSimpleMailAccountList = async () => { + return request.get({ url: '/system/mail-account/simple-list' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/mail/log/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/mail/log/index.ts new file mode 100644 index 00000000..13172a72 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/mail/log/index.ts @@ -0,0 +1,30 @@ +import request from '@/config/axios' + +export interface MailLogVO { + id: number + userId: number + userType: number + toMail: string + accountId: number + fromMail: string + templateId: number + templateCode: string + templateNickname: string + templateTitle: string + templateContent: string + templateParams: string + sendStatus: number + sendTime: Date + sendMessageId: string + sendException: string +} + +// 查询邮件日志列表 +export const getMailLogPage = async (params: PageParam) => { + return await request.get({ url: '/system/mail-log/page', params }) +} + +// 查询邮件日志详情 +export const getMailLog = async (id: number) => { + return await request.get({ url: '/system/mail-log/get?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/mail/template/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/mail/template/index.ts new file mode 100644 index 00000000..fb7ce5ea --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/mail/template/index.ts @@ -0,0 +1,50 @@ +import request from '@/config/axios' + +export interface MailTemplateVO { + id: number + name: string + code: string + accountId: number + nickname: string + title: string + content: string + params: string + status: number + remark: string +} + +export interface MailSendReqVO { + mail: string + templateCode: string + templateParams: Map +} + +// 查询邮件模版列表 +export const getMailTemplatePage = async (params: PageParam) => { + return await request.get({ url: '/system/mail-template/page', params }) +} + +// 查询邮件模版详情 +export const getMailTemplate = async (id: number) => { + return await request.get({ url: '/system/mail-template/get?id=' + id }) +} + +// 新增邮件模版 +export const createMailTemplate = async (data: MailTemplateVO) => { + return await request.post({ url: '/system/mail-template/create', data }) +} + +// 修改邮件模版 +export const updateMailTemplate = async (data: MailTemplateVO) => { + return await request.put({ url: '/system/mail-template/update', data }) +} + +// 删除邮件模版 +export const deleteMailTemplate = async (id: number) => { + return await request.delete({ url: '/system/mail-template/delete?id=' + id }) +} + +// 发送邮件 +export const sendMail = (data: MailSendReqVO) => { + return request.post({ url: '/system/mail-template/send-mail', data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/menu/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/menu/index.ts new file mode 100644 index 00000000..5a806682 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/menu/index.ts @@ -0,0 +1,49 @@ +import request from '@/config/axios' + +export interface MenuVO { + id: number + name: string + permission: string + type: number + sort: number + parentId: number + path: string + icon: string + component: string + componentName?: string + status: number + visible: boolean + keepAlive: boolean + alwaysShow?: boolean + createTime: Date +} + +// 查询菜单(精简)列表 +export const getSimpleMenusList = () => { + return request.get({ url: '/system/menu/simple-list' }) +} + +// 查询菜单列表 +export const getMenuList = (params) => { + return request.get({ url: '/system/menu/list', params }) +} + +// 获取菜单详情 +export const getMenu = (id: number) => { + return request.get({ url: '/system/menu/get?id=' + id }) +} + +// 新增菜单 +export const createMenu = (data: MenuVO) => { + return request.post({ url: '/system/menu/create', data }) +} + +// 修改菜单 +export const updateMenu = (data: MenuVO) => { + return request.put({ url: '/system/menu/update', data }) +} + +// 删除菜单 +export const deleteMenu = (id: number) => { + return request.delete({ url: '/system/menu/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/notice/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/notice/index.ts new file mode 100644 index 00000000..f6434697 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/notice/index.ts @@ -0,0 +1,42 @@ +import request from '@/config/axios' + +export interface NoticeVO { + id: number | undefined + title: string + type: number + content: string + status: number + remark: string + creator: string + createTime: Date +} + +// 查询公告列表 +export const getNoticePage = (params: PageParam) => { + return request.get({ url: '/system/notice/page', params }) +} + +// 查询公告详情 +export const getNotice = (id: number) => { + return request.get({ url: '/system/notice/get?id=' + id }) +} + +// 新增公告 +export const createNotice = (data: NoticeVO) => { + return request.post({ url: '/system/notice/create', data }) +} + +// 修改公告 +export const updateNotice = (data: NoticeVO) => { + return request.put({ url: '/system/notice/update', data }) +} + +// 删除公告 +export const deleteNotice = (id: number) => { + return request.delete({ url: '/system/notice/delete?id=' + id }) +} + +// 推送公告 +export const pushNotice = (id: number) => { + return request.post({ url: '/system/notice/push?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/notify/message/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/notify/message/index.ts new file mode 100644 index 00000000..e407c77d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/notify/message/index.ts @@ -0,0 +1,49 @@ +import request from '@/config/axios' +import qs from 'qs' + +export interface NotifyMessageVO { + id: number + userId: number + userType: number + templateId: number + templateCode: string + templateNickname: string + templateContent: string + templateType: number + templateParams: string + readStatus: boolean + readTime: Date + createTime: Date +} + +// 查询站内信消息列表 +export const getNotifyMessagePage = async (params: PageParam) => { + return await request.get({ url: '/system/notify-message/page', params }) +} + +// 获得我的站内信分页 +export const getMyNotifyMessagePage = async (params: PageParam) => { + return await request.get({ url: '/system/notify-message/my-page', params }) +} + +// 批量标记已读 +export const updateNotifyMessageRead = async (ids) => { + return await request.put({ + url: '/system/notify-message/update-read?' + qs.stringify({ ids: ids }, { indices: false }) + }) +} + +// 标记所有站内信为已读 +export const updateAllNotifyMessageRead = async () => { + return await request.put({ url: '/system/notify-message/update-all-read' }) +} + +// 获取当前用户的最新站内信列表 +export const getUnreadNotifyMessageList = async () => { + return await request.get({ url: '/system/notify-message/get-unread-list' }) +} + +// 获得当前用户的未读站内信数量 +export const getUnreadNotifyMessageCount = async () => { + return await request.get({ url: '/system/notify-message/get-unread-count' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/notify/template/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/notify/template/index.ts new file mode 100644 index 00000000..44355dff --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/notify/template/index.ts @@ -0,0 +1,49 @@ +import request from '@/config/axios' + +export interface NotifyTemplateVO { + id?: number + name: string + nickname: string + code: string + content: string + type?: number + params: string + status: number + remark: string +} + +export interface NotifySendReqVO { + userId: number | null + templateCode: string + templateParams: Map +} + +// 查询站内信模板列表 +export const getNotifyTemplatePage = async (params: PageParam) => { + return await request.get({ url: '/system/notify-template/page', params }) +} + +// 查询站内信模板详情 +export const getNotifyTemplate = async (id: number) => { + return await request.get({ url: '/system/notify-template/get?id=' + id }) +} + +// 新增站内信模板 +export const createNotifyTemplate = async (data: NotifyTemplateVO) => { + return await request.post({ url: '/system/notify-template/create', data }) +} + +// 修改站内信模板 +export const updateNotifyTemplate = async (data: NotifyTemplateVO) => { + return await request.put({ url: '/system/notify-template/update', data }) +} + +// 删除站内信模板 +export const deleteNotifyTemplate = async (id: number) => { + return await request.delete({ url: '/system/notify-template/delete?id=' + id }) +} + +// 发送站内信 +export const sendNotify = (data: NotifySendReqVO) => { + return request.post({ url: '/system/notify-template/send-notify', data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/oauth2/client.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/oauth2/client.ts new file mode 100644 index 00000000..6f71acad --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/oauth2/client.ts @@ -0,0 +1,47 @@ +import request from '@/config/axios' + +export interface OAuth2ClientVO { + id: number + clientId: string + secret: string + name: string + logo: string + description: string + status: number + accessTokenValiditySeconds: number + refreshTokenValiditySeconds: number + redirectUris: string[] + autoApprove: boolean + authorizedGrantTypes: string[] + scopes: string[] + authorities: string[] + resourceIds: string[] + additionalInformation: string + isAdditionalInformationJson: boolean + createTime: Date +} + +// 查询 OAuth2 客户端的列表 +export const getOAuth2ClientPage = (params: PageParam) => { + return request.get({ url: '/system/oauth2-client/page', params }) +} + +// 查询 OAuth2 客户端的详情 +export const getOAuth2Client = (id: number) => { + return request.get({ url: '/system/oauth2-client/get?id=' + id }) +} + +// 新增 OAuth2 客户端 +export const createOAuth2Client = (data: OAuth2ClientVO) => { + return request.post({ url: '/system/oauth2-client/create', data }) +} + +// 修改 OAuth2 客户端 +export const updateOAuth2Client = (data: OAuth2ClientVO) => { + return request.put({ url: '/system/oauth2-client/update', data }) +} + +// 删除 OAuth2 +export const deleteOAuth2Client = (id: number) => { + return request.delete({ url: '/system/oauth2-client/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/oauth2/token.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/oauth2/token.ts new file mode 100644 index 00000000..ac89ae89 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/oauth2/token.ts @@ -0,0 +1,22 @@ +import request from '@/config/axios' + +export interface OAuth2TokenVO { + id: number + accessToken: string + refreshToken: string + userId: number + userType: number + clientId: string + createTime: Date + expiresTime: Date +} + +// 查询 token列表 +export const getAccessTokenPage = (params: PageParam) => { + return request.get({ url: '/system/oauth2-token/page', params }) +} + +// 删除 token +export const deleteAccessToken = (accessToken: string) => { + return request.delete({ url: '/system/oauth2-token/delete?accessToken=' + accessToken }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/operatelog/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/operatelog/index.ts new file mode 100644 index 00000000..848a5333 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/operatelog/index.ts @@ -0,0 +1,33 @@ +import request from '@/config/axios' + +export type OperateLogVO = { + id: number + userNickname: string + traceId: string + userId: number + module: string + name: string + type: number + content: string + exts: Map + requestMethod: string + requestUrl: string + userIp: string + userAgent: string + javaMethod: string + javaMethodArgs: string + startTime: Date + duration: number + resultCode: number + resultMsg: string + resultData: string +} + +// 查询操作日志列表 +export const getOperateLogPage = (params: PageParam) => { + return request.get({ url: '/system/operate-log/page', params }) +} +// 导出操作日志 +export const exportOperateLog = (params) => { + return request.download({ url: '/system/operate-log/export', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/permission/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/permission/index.ts new file mode 100644 index 00000000..b3c7696b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/permission/index.ts @@ -0,0 +1,42 @@ +import request from '@/config/axios' + +export interface PermissionAssignUserRoleReqVO { + userId: number + roleIds: number[] +} + +export interface PermissionAssignRoleMenuReqVO { + roleId: number + menuIds: number[] +} + +export interface PermissionAssignRoleDataScopeReqVO { + roleId: number + dataScope: number + dataScopeDeptIds: number[] +} + +// 查询角色拥有的菜单权限 +export const getRoleMenuList = async (roleId: number) => { + return await request.get({ url: '/system/permission/list-role-menus?roleId=' + roleId }) +} + +// 赋予角色菜单权限 +export const assignRoleMenu = async (data: PermissionAssignRoleMenuReqVO) => { + return await request.post({ url: '/system/permission/assign-role-menu', data }) +} + +// 赋予角色数据权限 +export const assignRoleDataScope = async (data: PermissionAssignRoleDataScopeReqVO) => { + return await request.post({ url: '/system/permission/assign-role-data-scope', data }) +} + +// 查询用户拥有的角色数组 +export const getUserRoleList = async (userId: number) => { + return await request.get({ url: '/system/permission/list-user-roles?userId=' + userId }) +} + +// 赋予用户角色 +export const assignUserRole = async (data: PermissionAssignUserRoleReqVO) => { + return await request.post({ url: '/system/permission/assign-user-role', data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/post/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/post/index.ts new file mode 100644 index 00000000..0e6f2ca1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/post/index.ts @@ -0,0 +1,46 @@ +import request from '@/config/axios' + +export interface PostVO { + id?: number + name: string + code: string + sort: number + status: number + remark: string + createTime?: Date +} + +// 查询岗位列表 +export const getPostPage = async (params: PageParam) => { + return await request.get({ url: '/system/post/page', params }) +} + +// 获取岗位精简信息列表 +export const getSimplePostList = async (): Promise => { + return await request.get({ url: '/system/post/simple-list' }) +} + +// 查询岗位详情 +export const getPost = async (id: number) => { + return await request.get({ url: '/system/post/get?id=' + id }) +} + +// 新增岗位 +export const createPost = async (data: PostVO) => { + return await request.post({ url: '/system/post/create', data }) +} + +// 修改岗位 +export const updatePost = async (data: PostVO) => { + return await request.put({ url: '/system/post/update', data }) +} + +// 删除岗位 +export const deletePost = async (id: number) => { + return await request.delete({ url: '/system/post/delete?id=' + id }) +} + +// 导出岗位 +export const exportPost = async (params) => { + return await request.download({ url: '/system/post/export', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/role/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/role/index.ts new file mode 100644 index 00000000..3325ddec --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/role/index.ts @@ -0,0 +1,61 @@ +import request from '@/config/axios' + +export interface RoleVO { + id: number + name: string + code: string + sort: number + status: number + type: number + dataScope: number + dataScopeDeptIds: number[] + createTime: Date +} + +export interface UpdateStatusReqVO { + id: number + status: number +} + +// 查询角色列表 +export const getRolePage = async (params: PageParam) => { + return await request.get({ url: '/system/role/page', params }) +} + +// 查询角色(精简)列表 +export const getSimpleRoleList = async (): Promise => { + return await request.get({ url: '/system/role/simple-list' }) +} + +// 查询角色详情 +export const getRole = async (id: number) => { + return await request.get({ url: '/system/role/get?id=' + id }) +} + +// 新增角色 +export const createRole = async (data: RoleVO) => { + return await request.post({ url: '/system/role/create', data }) +} + +// 修改角色 +export const updateRole = async (data: RoleVO) => { + return await request.put({ url: '/system/role/update', data }) +} + +// 修改角色状态 +export const updateRoleStatus = async (data: UpdateStatusReqVO) => { + return await request.put({ url: '/system/role/update-status', data }) +} + +// 删除角色 +export const deleteRole = async (id: number) => { + return await request.delete({ url: '/system/role/delete?id=' + id }) +} + +// 导出角色 +export const exportRole = (params) => { + return request.download({ + url: '/system/role/export-excel', + params + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/sensitiveWord/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/sensitiveWord/index.ts new file mode 100644 index 00000000..1116226f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/sensitiveWord/index.ts @@ -0,0 +1,58 @@ +import request from '@/config/axios' +import qs from 'qs' + +export interface SensitiveWordVO { + id: number + name: string + status: number + description: string + tags: string[] + createTime: Date +} + +export interface SensitiveWordTestReqVO { + text: string + tag: string[] +} + +// 查询敏感词列表 +export const getSensitiveWordPage = (params: PageParam) => { + return request.get({ url: '/system/sensitive-word/page', params }) +} + +// 查询敏感词详情 +export const getSensitiveWord = (id: number) => { + return request.get({ url: '/system/sensitive-word/get?id=' + id }) +} + +// 新增敏感词 +export const createSensitiveWord = (data: SensitiveWordVO) => { + return request.post({ url: '/system/sensitive-word/create', data }) +} + +// 修改敏感词 +export const updateSensitiveWord = (data: SensitiveWordVO) => { + return request.put({ url: '/system/sensitive-word/update', data }) +} + +// 删除敏感词 +export const deleteSensitiveWord = (id: number) => { + return request.delete({ url: '/system/sensitive-word/delete?id=' + id }) +} + +// 导出敏感词 +export const exportSensitiveWord = (params) => { + return request.download({ url: '/system/sensitive-word/export-excel', params }) +} + +// 获取所有敏感词的标签数组 +export const getSensitiveWordTagList = () => { + return request.get({ url: '/system/sensitive-word/get-tags' }) +} + +// 获得文本所包含的不合法的敏感词数组 +export const validateText = (query: SensitiveWordTestReqVO) => { + return request.get({ + url: '/system/sensitive-word/validate-text?' + qs.stringify(query, { arrayFormat: 'repeat' }) + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/sms/smsChannel/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/sms/smsChannel/index.ts new file mode 100644 index 00000000..bcdaa7f9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/sms/smsChannel/index.ts @@ -0,0 +1,43 @@ +import request from '@/config/axios' + +export interface SmsChannelVO { + id: number + code: string + status: number + signature: string + remark: string + apiKey: string + apiSecret: string + callbackUrl: string + createTime: Date +} + +// 查询短信渠道列表 +export const getSmsChannelPage = (params: PageParam) => { + return request.get({ url: '/system/sms-channel/page', params }) +} + +// 获得短信渠道精简列表 +export function getSimpleSmsChannelList() { + return request.get({ url: '/system/sms-channel/simple-list' }) +} + +// 查询短信渠道详情 +export const getSmsChannel = (id: number) => { + return request.get({ url: '/system/sms-channel/get?id=' + id }) +} + +// 新增短信渠道 +export const createSmsChannel = (data: SmsChannelVO) => { + return request.post({ url: '/system/sms-channel/create', data }) +} + +// 修改短信渠道 +export const updateSmsChannel = (data: SmsChannelVO) => { + return request.put({ url: '/system/sms-channel/update', data }) +} + +// 删除短信渠道 +export const deleteSmsChannel = (id: number) => { + return request.delete({ url: '/system/sms-channel/delete?id=' + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/sms/smsLog/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/sms/smsLog/index.ts new file mode 100644 index 00000000..f9891716 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/sms/smsLog/index.ts @@ -0,0 +1,37 @@ +import request from '@/config/axios' + +export interface SmsLogVO { + id: number | null + channelId: number | null + channelCode: string + templateId: number | null + templateCode: string + templateType: number | null + templateContent: string + templateParams: Map | null + apiTemplateId: string + mobile: string + userId: number | null + userType: number | null + sendStatus: number | null + sendTime: Date | null + apiSendCode: string + apiSendMsg: string + apiRequestId: string + apiSerialNo: string + receiveStatus: number | null + receiveTime: Date | null + apiReceiveCode: string + apiReceiveMsg: string + createTime: Date | null +} + +// 查询短信日志列表 +export const getSmsLogPage = (params: PageParam) => { + return request.get({ url: '/system/sms-log/page', params }) +} + +// 导出短信日志 +export const exportSmsLog = (params) => { + return request.download({ url: '/system/sms-log/export-excel', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/sms/smsTemplate/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/sms/smsTemplate/index.ts new file mode 100644 index 00000000..868ddd47 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/sms/smsTemplate/index.ts @@ -0,0 +1,60 @@ +import request from '@/config/axios' + +export interface SmsTemplateVO { + id?: number + type?: number + status: number + code: string + name: string + content: string + remark: string + apiTemplateId: string + channelId?: number + channelCode?: string + params?: string[] + createTime?: Date +} + +export interface SendSmsReqVO { + mobile: string + templateCode: string + templateParams: Map +} + +// 查询短信模板列表 +export const getSmsTemplatePage = (params: PageParam) => { + return request.get({ url: '/system/sms-template/page', params }) +} + +// 查询短信模板详情 +export const getSmsTemplate = (id: number) => { + return request.get({ url: '/system/sms-template/get?id=' + id }) +} + +// 新增短信模板 +export const createSmsTemplate = (data: SmsTemplateVO) => { + return request.post({ url: '/system/sms-template/create', data }) +} + +// 修改短信模板 +export const updateSmsTemplate = (data: SmsTemplateVO) => { + return request.put({ url: '/system/sms-template/update', data }) +} + +// 删除短信模板 +export const deleteSmsTemplate = (id: number) => { + return request.delete({ url: '/system/sms-template/delete?id=' + id }) +} + +// 导出短信模板 +export const exportSmsTemplate = (params) => { + return request.download({ + url: '/system/sms-template/export-excel', + params + }) +} + +// 发送短信 +export const sendSms = (data: SendSmsReqVO) => { + return request.post({ url: '/system/sms-template/send-sms', data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/social/client/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/social/client/index.ts new file mode 100644 index 00000000..bf13ab49 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/social/client/index.ts @@ -0,0 +1,37 @@ +import request from '@/config/axios' + +export interface SocialClientVO { + id: number + name: string + socialType: number + userType: number + clientId: string + clientSecret: string + agentId: string + status: number +} + +// 查询社交客户端列表 +export const getSocialClientPage = async (params) => { + return await request.get({ url: `/system/social-client/page`, params }) +} + +// 查询社交客户端详情 +export const getSocialClient = async (id: number) => { + return await request.get({ url: `/system/social-client/get?id=` + id }) +} + +// 新增社交客户端 +export const createSocialClient = async (data: SocialClientVO) => { + return await request.post({ url: `/system/social-client/create`, data }) +} + +// 修改社交客户端 +export const updateSocialClient = async (data: SocialClientVO) => { + return await request.put({ url: `/system/social-client/update`, data }) +} + +// 删除社交客户端 +export const deleteSocialClient = async (id: number) => { + return await request.delete({ url: `/system/social-client/delete?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/social/user/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/social/user/index.ts new file mode 100644 index 00000000..f11231b7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/social/user/index.ts @@ -0,0 +1,24 @@ +import request from '@/config/axios' + +export interface SocialUserVO { + id: number + type: number + openid: string + token: string + rawTokenInfo: string + nickname: string + avatar: string + rawUserInfo: string + code: string + state: string +} + +// 查询社交用户列表 +export const getSocialUserPage = async (params) => { + return await request.get({ url: `/system/social-user/page`, params }) +} + +// 查询社交用户详情 +export const getSocialUser = async (id: number) => { + return await request.get({ url: `/system/social-user/get?id=` + id }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/tenant/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/tenant/index.ts new file mode 100644 index 00000000..176c3757 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/tenant/index.ts @@ -0,0 +1,62 @@ +import request from '@/config/axios' + +export interface TenantVO { + id: number + name: string + contactName: string + contactMobile: string + status: number + domain: string + packageId: number + username: string + password: string + expireTime: Date + accountCount: number + createTime: Date +} + +export interface TenantPageReqVO extends PageParam { + name?: string + contactName?: string + contactMobile?: string + status?: number + createTime?: Date[] +} + +export interface TenantExportReqVO { + name?: string + contactName?: string + contactMobile?: string + status?: number + createTime?: Date[] +} + +// 查询租户列表 +export const getTenantPage = (params: TenantPageReqVO) => { + return request.get({ url: '/system/tenant/page', params }) +} + +// 查询租户详情 +export const getTenant = (id: number) => { + return request.get({ url: '/system/tenant/get?id=' + id }) +} + +// 新增租户 +export const createTenant = (data: TenantVO) => { + return request.post({ url: '/system/tenant/create', data }) +} + +// 修改租户 +export const updateTenant = (data: TenantVO) => { + return request.put({ url: '/system/tenant/update', data }) +} + +// 删除租户 +export const deleteTenant = (id: number) => { + return request.delete({ url: '/system/tenant/delete?id=' + id }) +} + +// 导出租户 +export const exportTenant = (params: TenantExportReqVO) => { + return request.download({ url: '/system/tenant/export-excel', params }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/tenantPackage/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/tenantPackage/index.ts new file mode 100644 index 00000000..e01375a5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/tenantPackage/index.ts @@ -0,0 +1,42 @@ +import request from '@/config/axios' + +export interface TenantPackageVO { + id: number + name: string + status: number + remark: string + creator: string + updater: string + updateTime: string + menuIds: number[] + createTime: Date +} + +// 查询租户套餐列表 +export const getTenantPackagePage = (params: PageParam) => { + return request.get({ url: '/system/tenant-package/page', params }) +} + +// 获得租户 +export const getTenantPackage = (id: number) => { + return request.get({ url: '/system/tenant-package/get?id=' + id }) +} + +// 新增租户套餐 +export const createTenantPackage = (data: TenantPackageVO) => { + return request.post({ url: '/system/tenant-package/create', data }) +} + +// 修改租户套餐 +export const updateTenantPackage = (data: TenantPackageVO) => { + return request.put({ url: '/system/tenant-package/update', data }) +} + +// 删除租户套餐 +export const deleteTenantPackage = (id: number) => { + return request.delete({ url: '/system/tenant-package/delete?id=' + id }) +} +// 获取租户套餐精简信息列表 +export const getTenantPackageList = () => { + return request.get({ url: '/system/tenant-package/simple-list' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/user/index.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/user/index.ts new file mode 100644 index 00000000..beb6e515 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/user/index.ts @@ -0,0 +1,81 @@ +import request from '@/config/axios' + +export interface UserVO { + id: number + username: string + nickname: string + deptId: number + postIds: string[] + email: string + mobile: string + sex: number + avatar: string + loginIp: string + status: number + remark: string + loginDate: Date + createTime: Date +} + +// 查询用户管理列表 +export const getUserPage = (params: PageParam) => { + return request.get({ url: '/system/user/page', params }) +} + +// 查询所有用户列表 +export const getAllUser = () => { + return request.get({ url: '/system/user/all' }) +} + +// 查询用户详情 +export const getUser = (id: number) => { + return request.get({ url: '/system/user/get?id=' + id }) +} + +// 新增用户 +export const createUser = (data: UserVO) => { + return request.post({ url: '/system/user/create', data }) +} + +// 修改用户 +export const updateUser = (data: UserVO) => { + return request.put({ url: '/system/user/update', data }) +} + +// 删除用户 +export const deleteUser = (id: number) => { + return request.delete({ url: '/system/user/delete?id=' + id }) +} + +// 导出用户 +export const exportUser = (params) => { + return request.download({ url: '/system/user/export', params }) +} + +// 下载用户导入模板 +export const importUserTemplate = () => { + return request.download({ url: '/system/user/get-import-template' }) +} + +// 用户密码重置 +export const resetUserPwd = (id: number, password: string) => { + const data = { + id, + password + } + return request.put({ url: '/system/user/update-password', data: data }) +} + +// 用户状态修改 +export const updateUserStatus = (id: number, status: number) => { + const data = { + id, + status + } + return request.put({ url: '/system/user/update-status', data: data }) +} + +// 获取用户精简信息列表 +export const getSimpleUserList = (): Promise => { + return request.get({ url: '/system/user/simple-list' }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/user/profile.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/user/profile.ts new file mode 100644 index 00000000..1e80e854 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/user/profile.ts @@ -0,0 +1,65 @@ +import request from '@/config/axios' + +export interface ProfileVO { + id: number + username: string + nickname: string + dept: { + id: number + name: string + } + roles: { + id: number + name: string + }[] + posts: { + id: number + name: string + }[] + socialUsers: { + type: number + openid: string + }[] + email: string + mobile: string + sex: number + avatar: string + status: number + remark: string + loginIp: string + loginDate: Date + createTime: Date +} + +export interface UserProfileUpdateReqVO { + nickname: string + email: string + mobile: string + sex: number +} + +// 查询用户个人信息 +export const getUserProfile = () => { + return request.get({ url: '/system/user/profile/get' }) +} + +// 修改用户个人信息 +export const updateUserProfile = (data: UserProfileUpdateReqVO) => { + return request.put({ url: '/system/user/profile/update', data }) +} + +// 用户密码重置 +export const updateUserPassword = (oldPassword: string, newPassword: string) => { + return request.put({ + url: '/system/user/profile/update-password', + data: { + oldPassword: oldPassword, + newPassword: newPassword + } + }) +} + +// 用户头像上传 +export const uploadAvatar = (data) => { + return request.upload({ url: '/system/user/profile/update-avatar', data: data }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/api/system/user/socialUser.ts b/mes-ui/mes-ui-admin-vue3/src/api/system/user/socialUser.ts new file mode 100644 index 00000000..79f4d402 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/api/system/user/socialUser.ts @@ -0,0 +1,31 @@ +import request from '@/config/axios' + +// 社交绑定,使用 code 授权码 +export const socialBind = (type, code, state) => { + return request.post({ + url: '/system/social-user/bind', + data: { + type, + code, + state + } + }) +} + +// 取消社交绑定 +export const socialUnbind = (type, openid) => { + return request.delete({ + url: '/system/social-user/unbind', + data: { + type, + openid + } + }) +} + +// 社交授权的跳转 +export const socialAuthRedirect = (type, redirectUri) => { + return request.get({ + url: '/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/imgs/avatar.gif b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/avatar.gif new file mode 100644 index 00000000..fdbd32c6 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/avatar.gif differ diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/imgs/avatar.jpg b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/avatar.jpg new file mode 100644 index 00000000..d46a70a4 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/avatar.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/imgs/diy/statusBar.png b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/diy/statusBar.png new file mode 100644 index 00000000..b85562e4 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/diy/statusBar.png differ diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/imgs/logo.png b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/logo.png new file mode 100644 index 00000000..7e1043f2 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/logo.png differ diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/imgs/profile.jpg b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/profile.jpg new file mode 100644 index 00000000..e4bcf879 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/profile.jpg differ diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/imgs/wechat.png b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/wechat.png new file mode 100644 index 00000000..6afc5e41 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/src/assets/imgs/wechat.png differ diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/map/json/china.json b/mes-ui/mes-ui-admin-vue3/src/assets/map/json/china.json new file mode 100644 index 00000000..bbc0a832 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/map/json/china.json @@ -0,0 +1,856 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "710000", + "properties": { + "id": "710000", + "cp": [121.509062, 24.044332], + "name": "台湾", + "childNum": 6 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@°Ü¯Û"], + [ + "@@ƛĴÕƊÉɼģºðʀ\\ƎsÆNŌÔĚäœnÜƤɊĂǀĆĴžĤNJŨxĚĮǂƺòƌ‚–âÔ®ĮXŦţƸZûЋƕƑGđ¨ĭMó·ęcëƝɉlÝƯֹÅŃ^Ó·śŃNjƏďíåɛGɉ™¿@ăƑŽ¥ĘWǬÏĶŁâ" + ], + ["@@\\p|WoYG¿¥I†j@¢"], + ["@@…¡‰@ˆV^RqˆBbAŒnTXeRz¤Lž«³I"], + ["@@ÆEE—„kWqë @œ"], + ["@@fced"], + ["@@„¯ɜÄèaì¯ØǓIġĽ"], + ["@@çûĖ롖hòř "] + ], + "encodeOffsets": [ + [[122886, 24033]], + [[123335, 22980]], + [[122375, 24193]], + [[122518, 24117]], + [[124427, 22618]], + [[124862, 26043]], + [[126259, 26318]], + [[127671, 26683]] + ] + } + }, + { + "type": "Feature", + "id": "130000", + "properties": { + "id": "130000", + "cp": [114.502461, 38.045474], + "name": "河北", + "childNum": 3 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@o~†Z]‚ªr‰ºc_ħ²G¼s`jΟnüsœłNX_“M`ǽÓnUK…Ĝēs¤­©yrý§uģŒc†JŠ›e"], + ["@@U`Ts¿m‚"], + [ + "@@oºƋÄd–eVŽDJj£€J|Ådz•Ft~žKŨ¸IÆv|”‡¢r}膎onb˜}`RÎÄn°ÒdÞ²„^®’lnÐèĄlðӜ×]ªÆ}LiĂ±Ö`^°Ç¶p®đDcœŋ`–ZÔ’¶êqvFƚ†N®ĆTH®¦O’¾ŠIbÐã´BĐɢŴÆíȦp–ĐÞXR€·nndOž¤’OÀĈƒ­Qg˜µFo|gȒęSWb©osx|hYh•gŃfmÖĩnº€T̒Sp›¢dYĤ¶UĈjl’ǐpäìë|³kÛfw²Xjz~ÂqbTŠÑ„ěŨ@|oM‡’zv¢ZrÃVw¬ŧĖ¸fŒ°ÐT€ªqŽs{Sž¯r æÝlNd®²Ğ džiGʂJ™¼lr}~K¨ŸƐÌWö€™ÆŠzRš¤lêmĞL΄’@¡|q]SvK€ÑcwpÏρ†ĿćènĪWlĄkT}ˆJ”¤~ƒÈT„d„™pddʾĬŠ”ŽBVt„EÀ¢ôPĎƗè@~‚k–ü\\rÊĔÖæW_§¼F˜†´©òDòj’ˆYÈrbĞāøŀG{ƀ|¦ðrb|ÀH`pʞkv‚GpuARhÞÆǶgʊTǼƹS£¨¡ù³ŘÍ]¿Ây™ôEP xX¶¹܇O¡“gÚ¡IwÃ鑦ÅB‡Ï|Ç°…N«úmH¯‹âŸDùŽyŜžŲIÄuШDž•¸dɂ‡‚FŸƒ•›Oh‡đ©OŸ›iÃ`ww^ƒÌkŸ‘ÑH«ƇǤŗĺtFu…{Z}Ö@U‡´…ʚLg®¯Oı°ÃwŸ ^˜—€VbÉs‡ˆmA…ê]]w„§›RRl£‡ȭµu¯b{ÍDěïÿȧŽuT£ġƒěŗƃĝ“Q¨fV†Ƌ•ƅn­a@‘³@šď„yýIĹÊKšŭfċŰóŒxV@tˆƯŒJ”]eƒR¾fe|rHA˜|h~Ėƍl§ÏŠlTíb ØoˆÅbbx³^zÃĶš¶Sj®A”yÂhðk`š«P€”ˈµEF†Û¬Y¨Ļrõqi¼‰Wi°§’б´°^[ˆÀ|ĠO@ÆxO\\tŽa\\tĕtû{ġŒȧXýĪÓjùÎRb›š^ΛfK[ݏděYfíÙTyŽuUSyŌŏů@Oi½’éŅ­aVcř§ax¹XŻác‡žWU£ôãºQ¨÷Ñws¥qEH‰Ù|‰›šYQoŕÇyáĂ£MðoťÊ‰P¡mšWO¡€v†{ôvîēÜISpÌhp¨ ‘j†deŔQÖj˜X³à™Ĉ[n`Yp@Už–cM`’RKhŒEbœ”pŞlNut®Etq‚nsÁŠgA‹iú‹oH‡qCX‡”hfgu“~ϋWP½¢G^}¯ÅīGCŸÑ^ãziMáļMTÃƘrMc|O_ž¯Ŏ´|‡morDkO\\mĆJfl@cĢ¬¢aĦtRıҙ¾ùƀ^juųœK­ƒUFy™—Ɲ…›īÛ÷ąV×qƥV¿aȉd³B›qPBm›aËđŻģm“Å®VŠ¹d^K‡KoŸnYg“¯Xhqa”Ldu¥•ÍpDž¡KąÅƒkĝęěhq‡}HyÓ]¹ǧ£…Í÷¿qᵧš™g‘¤o^á¾ZE‡¤i`ij{n•ƒOl»ŸWÝĔįhg›F[¿¡—ßkOüš_‰€ū‹i„DZàUtėGylƒ}ŒÓM}€jpEC~¡FtoQi‘šHkk{Ãmï‚" + ] + ], + "encodeOffsets": [[[119712, 40641]], [[121616, 39981]], [[116462, 37237]]] + } + }, + { + "type": "Feature", + "id": "140000", + "properties": { + "id": "140000", + "cp": [111.849248, 36.857014], + "name": "山西", + "childNum": 1 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@Þĩ҃S‰ra}Á€yWix±Üe´lè“ßÓǏok‘ćiµVZģ¡coœ‘TS˹ĪmnÕńe–hZg{gtwªpXaĚThȑp{¶Eh—®RćƑP¿£‘Pmc¸mQÝW•ďȥoÅîɡųAďä³aωJ‘½¥PG­ąSM­™…EÅruµé€‘Yӎ•Ō_d›ĒCo­Èµ]¯_²ÕjāŽK~©ÅØ^ԛkïçămϑk]­±ƒcݯÑÃmQÍ~_a—pm…~ç¡q“ˆu{JÅŧ·Ls}–EyÁÆcI{¤IiCfUc•ƌÃp§]웫vD@¡SÀ‘µM‚ÅwuŽYY‡¡DbÑc¡hƒ×]nkoQdaMç~eD•ÛtT‰©±@¥ù@É¡‰ZcW|WqOJmĩl«ħşvOÓ«IqăV—¥ŸD[mI~Ó¢cehiÍ]Ɠ~ĥqXŠ·eƷœn±“}v•[ěďŽŕ]_‘œ•`‰¹ƒ§ÕōI™o©b­s^}Ét±ū«³p£ÿ·Wµ|¡¥ăFÏs׌¥ŅxŸÊdÒ{ºvĴÎêÌɊ²¶€ü¨|ÞƸµȲ‘LLúÉƎ¤ϊęĔV`„_bª‹S^|ŸdŠzY|dz¥p†ZbÆ£¶ÒK}tĦÔņƠ‚PYzn€ÍvX¶Ěn ĠÔ„zý¦ª˜÷žÑĸَUȌ¸‚dòÜJð´’ìúNM¬ŒXZ´‘¤ŊǸ_tldIš{¦ƀðĠȤ¥NehXnYG‚‡R° ƬDj¬¸|CĞ„Kq‚ºfƐiĺ©ª~ĆOQª ¤@ìǦɌ²æBŒÊ”TœŸ˜ʂōĖ’šĴŞ–ȀœÆÿȄlŤĒö„t”νî¼ĨXhŒ‘˜|ªM¤Ðz" + ], + "encodeOffsets": [[116874, 41716]] + } + }, + { + "type": "Feature", + "id": "150000", + "properties": { + "id": "150000", + "cp": [111.670801, 41.818311], + "name": "内蒙古", + "childNum": 2 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + "@@¯PqƒFB…‰|S•³C|kñ•H‹d‘iÄ¥sˆʼnő…PóÑÑE^‘ÅPpy_YtS™hQ·aHwsOnʼnÚs©iqj›‰€USiº]ïWš‰«gW¡A–Rë¥_ŽsgÁnUI«m‰…„‹]j‡vV¼euhwqA„aW˜ƒ_µj…»çjioQR¹ēÃßt@r³[ÛlćË^ÍÉáG“›OUۗOB±•XŸkŇ¹£k|e]ol™ŸkVͼÕqtaÏõjgÁ£§U^Œ”RLˆËnX°Ç’Bz†^~wfvˆypV ¯„ƫĉ˭ȫƗŷɿÿĿƑ˃ĝÿÃǃßËőó©ǐȍŒĖM×ÍEyx‹þp]Évïè‘vƀnÂĴÖ@‚‰†V~Ĉv¦wĖt—ējyÄDXÄxGQuv_›i¦aBçw‘˛wD™©{ŸtāmQ€{EJ§KPśƘƿ¥@‰sCT•É}ɃwˆƇy±ŸgÑ“}T[÷kÐ禫…SÒ¥¸ëBX½‰HáŵÀğtSÝÂa[ƣ°¯¦P]£ġ“–“Òk®G²„èQ°óMq}EŠóƐÇ\\ƒ‡@áügQ͋u¥Fƒ“T՛¿Jû‡]|mvāÎYua^WoÀa·­ząÒot׶CLƗi¯¤mƎHNJ¤îìɾŊìTdåwsRÖgĒųúÍġäÕ}Q¶—ˆ¿A•†‹[¡Œ{d×uQAƒ›M•xV‹vMOmăl«ct[wº_šÇʊŽŸjb£ĦS_é“QZ“_lwgOiýe`YYLq§IÁˆdz£ÙË[ÕªuƏ³ÍT—s·bÁĽäė[›b[ˆŗfãcn¥îC¿÷µ[ŏÀQ­ōšĉm¿Á^£mJVm‡—L[{Ï_£›F¥Ö{ŹA}…×Wu©ÅaųijƳhB{·TQqÙIķˑZđ©Yc|M¡…L•eVUóK_QWk’_ĥ‘¿ãZ•»X\\ĴuUƒè‡lG®ěłTĠğDєOrÍd‚ÆÍz]‹±…ŭ©ŸÅ’]ŒÅÐ}UË¥©Tċ™ïxgckfWgi\\ÏĒ¥HkµE˜ë{»ÏetcG±ahUiñiWsɁˆ·c–C‚Õk]wȑ|ća}w…VaĚ᠞ŒG°ùnM¬¯†{ȈÐÆA’¥ÄêJxÙ¢”hP¢Ûˆº€µwWOŸóFŽšÁz^ÀŗÎú´§¢T¤ǻƺSė‰ǵhÝÅQgvBHouʝl_o¿Ga{ïq{¥|ſĿHĂ÷aĝÇq‡Z‘ñiñC³ª—…»E`¨åXēÕqÉû[l•}ç@čƘóO¿¡ƒFUsA‰“ʽīccšocƒ‚ƒÇS}„“£‡IS~ălkĩXçmĈ…ŀЂoÐdxÒuL^T{r@¢‘žÍƒĝKén£kQ™‰yšÅõËXŷƏL§~}kqš»IHėDžjĝŸ»ÑÞoŸå°qTt|r©ÏS‹¯·eŨĕx«È[eMˆ¿yuˆ‘pN~¹ÏyN£{©’—g‹ħWí»Í¾s“əšDž_ÃĀɗ±ą™ijĉʍŌŷ—S›É“A‹±åǥɋ@럣R©ąP©}ĹªƏj¹erƒLDĝ·{i«ƫC£µsKCš…GS|úþX”gp›{ÁX¿Ÿć{ƱȏñZáĔyoÁhA™}ŅĆfdʼn„_¹„Y°ėǩÑ¡H¯¶oMQqð¡Ë™|‘Ñ`ƭŁX½·óۓxğįÅcQ‡ˆ“ƒs«tȋDžF“Ÿù^i‘t«Č¯[›hAi©á¥ÇĚ×l|¹y¯YȵƓ‹ñǙµï‚ċ™Ļ|Dœ™üȭ¶¡˜›oŽäÕG\\ďT¿Òõr¯œŸLguÏYęRƩšɷŌO\\İТæ^Ŋ IJȶȆbÜGŽĝ¬¿ĚVĎgª^íu½jÿĕęjık@Ľƒ]ėl¥Ë‡ĭûÁ„ƒėéV©±ćn©­ȇžÍq¯½•YÃÔʼn“ÉNѝÅÝy¹NqáʅDǡËñ­ƁYÅy̱os§ȋµʽǘǏƬɱà‘ưN¢ƔÊuľýľώȪƺɂļžxœZĈ}ÌʼnŪ˜ĺœŽĭFЛĽ̅ȣͽÒŵìƩÇϋÿȮǡŏçƑůĕ~Ǎ›¼ȳÐUf†dIxÿ\\G ˆzâɏÙOº·pqy£†@ŒŠqþ@Ǟ˽IBäƣzsÂZ†ÁàĻdñ°ŕzéØűzșCìDȐĴĺf®ŽÀľưø@ɜÖÞKĊŇƄ§‚͑těï͡VAġÑÑ»d³öǍÝXĉĕÖ{þĉu¸ËʅğU̎éhɹƆ̗̮ȘNJ֥ड़ࡰţાíϲäʮW¬®ҌeרūȠkɬɻ̼ãüfƠSצɩςåȈHϚÎKdzͲOðÏȆƘ¼CϚǚ࢚˼ФԂ¤ƌžĞ̪Qʤ´¼mȠJˀŸƲÀɠmǐnǔĎȆÞǠN~€ʢĜ‚¶ƌĆĘźʆȬ˪ĚĒ¸ĞGȖƴƀj`ĢçĶāàŃºēĢƒĖćšYŒÀŎüôQÐÂŎŞdžŞêƖš˜oˆDĤÕºÑǘÛˤ³̀gńƘĔÀ^žªƂ`ªt¾äƚêĦĀ¼Ð€Ĕǎ¨Ȕ»͠^ˮÊȦƤøxRrŜH¤¸ÂxDĝŒ|ø˂˜ƮÐ¬ɚwɲFjĔ²Äw°dždÀɞ_ĸdîàŎjʜêTЪŌ‡ŜWÈ|tqĢUB~´°ÎFC•ŽU¼pĀēƄN¦¾O¶ŠłKĊOj“Ě”j´ĜYp˜{¦„ˆSĚÍ\\Tš×ªV–÷Ší¨ÅDK°ßtŇĔKš¨ǵÂcḷ̌ĚǣȄĽF‡lġUĵœŇ‹ȣFʉɁƒMğįʏƶɷØŭOǽ«ƽū¹Ʊő̝Ȩ§ȞʘĖiɜɶʦ}¨֪ࠜ̀ƇǬ¹ǨE˦ĥªÔêFŽxúQ„Er´W„rh¤Ɛ \\talĈDJ˜Ü|[Pll̚¸ƎGú´Pž¬W¦†^¦–H]prR“n|or¾wLVnÇIujkmon£cX^Bh`¥V”„¦U¤¸}€xRj–[^xN[~ªŠxQ„‚[`ªHÆÂExx^wšN¶Ê˜|¨ì†˜€MrœdYp‚oRzNy˜ÀDs~€bcfÌ`L–¾n‹|¾T‚°c¨È¢a‚r¤–`[|òDŞĔöxElÖdH„ÀI`„Ď\\Àì~ƎR¼tf•¦^¢ķ¶e”ÐÚMŒptgj–„ɡČÅyġLû™ŇV®ŠÄÈƀ†Ď°P|ªVV†ªj–¬ĚÒêp¬–E|ŬÂc|ÀtƐK fˆ{ĘFĒœƌXƲąo½Ę‘\\¥–o}›Ûu£ç­kX‘{uĩ«āíÓUŅßŢq€Ť¥lyň[€oi{¦‹L‡ń‡ðFȪȖ”ĒL„¿Ì‹ˆfŒ£K£ʺ™oqNŸƒwğc`ue—tOj×°KJ±qƒÆġm‰Ěŗos¬…qehqsuœƒH{¸kH¡Š…ÊRǪÇƌbȆ¢´ä܍¢NìÉʖ¦â©Ġu¦öČ^â£Ăh–šĖMÈÄw‚\\fŦ°W ¢¾luŸD„wŠ\\̀ʉÌÛM…Ā[bӞEn}¶Vc…ê“sƒ" + ] + ], + "encodeOffsets": [[[129102, 52189]]] + } + }, + { + "type": "Feature", + "id": "210000", + "properties": { + "id": "210000", + "cp": [123.429096, 41.796767], + "name": "辽宁", + "childNum": 16 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@L–Ž@@s™a"], + ["@@MnNm"], + ["@@d‚c"], + ["@@eÀ‚C@b‚“‰"], + ["@@f‡…Xwkbr–Ä`qg"], + ["@@^jtW‘Q"], + ["@@~ Y]c"], + ["@@G`ĔN^_¿Z‚ÃM"], + ["@@iX¶B‹Y"], + ["@@„YƒZ"], + ["@@L_{Epf"], + ["@@^WqCT\\"], + ["@@\\[“‹§t|”¤_"], + ["@@m`n_"], + ["@@Ïxnj{q_×^Giip"], + [ + "@@@œé^B†‡ntˆaÊU—˜Ÿ]x ¯ÄPIJ­°h€ʙK³†VˆÕ@Y~†|EvĹsDŽ¦­L^p²ŸÒG ’Ël]„xxÄ_˜fT¤Ď¤cŽœP„–C¨¸TVjbgH²sdÎdHt`Bˆ—²¬GJję¶[ÐhjeXdlwhšðSȦªVÊπ‹Æ‘Z˜ÆŶ®²†^ŒÎyÅÎcPqń“ĚDMħĜŁH­ˆk„çvV[ij¼W–‚YÀäĦ’‘`XlžR`žôLUVžfK–¢†{NZdĒª’YĸÌÚJRr¸SA|ƴgŴĴÆbvªØX~†źBŽ|¦ÕœEž¤Ð`\\|Kˆ˜UnnI]¤ÀÂĊnŎ™R®Ő¿¶\\ÀøíDm¦ÎbŨab‰œaĘ\\ľã‚¸a˜tÎSƐ´©v\\ÖÚÌǴ¤Â‡¨JKr€Z_Z€fjþhPkx€`Y”’RIŒjJcVf~sCN¤ ˆE‚œhæm‰–sHy¨SðÑÌ\\\\ŸĐRZk°IS§fqŒßýáЍÙÉÖ[^¯ǤŲ„ê´\\¦¬ĆPM¯£Ÿˆ»uïpùzEx€žanµyoluqe¦W^£ÊL}ñrkqWňûP™‰UP¡ôJŠoo·ŒU}£Œ„[·¨@XŒĸŸ“‹‹DXm­Ûݏº‡›GU‹CÁª½{íĂ^cj‡k“¶Ã[q¤“LÉö³cux«zZfƒ²BWÇ®Yß½ve±ÃC•ý£W{Ú^’q^sÑ·¨‹ÍOt“¹·C¥‡GD›rí@wÕKţ݋˜Ÿ«V·i}xËÍ÷‘i©ĝ‡ɝǡ]ƒˆ{c™±OW‹³Ya±Ÿ‰_穂Hžĕoƫ€Ňqƒr³‰Lys[„ñ³¯OS–ďOMisZ†±ÅFC¥Pq{‚Ã[Pg}\\—¿ghćO…•k^ģÁFıĉĥM­oEqqZûěʼn³F‘¦oĵ—hŸÕP{¯~TÍlª‰N‰ßY“Ð{Ps{ÃVU™™eĎwk±ʼnVÓ½ŽJãÇÇ»Jm°dhcÀff‘dF~ˆ€ĀeĖ€d`sx² šƒ®EżĀdQ‹Âd^~ăÔHˆ¦\\›LKpĄVez¤NP ǹӗR™ÆąJSh­a[¦´Âghwm€BÐ¨źhI|žVVŽ—Ž|p] Â¼èNä¶ÜBÖ¼“L`‚¼bØæŒKV”ŸpoœúNZÞÒKxpw|ÊEMnzEQšŽIZ”ŽZ‡NBˆčÚFÜçmĩ‚WĪñt‘ÞĵÇñZ«uD‚±|Əlij¥ãn·±PmÍa‰–da‡ CL‡Ǒkùó¡³Ï«QaċϑOÃ¥ÕđQȥċƭy‹³ÃA" + ] + ], + "encodeOffsets": [ + [[123686, 41445]], + [[126019, 40435]], + [[124393, 40128]], + [[126117, 39963]], + [[125322, 40140]], + [[126686, 40700]], + [[126041, 40374]], + [[125584, 40168]], + [[125453, 40165]], + [[125362, 40214]], + [[125280, 40291]], + [[125774, 39997]], + [[125976, 40496]], + [[125822, 39993]], + [[125509, 40217]], + [[122731, 40949]] + ] + } + }, + { + "type": "Feature", + "id": "220000", + "properties": { "id": "220000", "cp": [125.3245, 43.886841], "name": "吉林", "childNum": 1 }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@‘p䔳PClƒFbbÍzš€wBG’ĭ€Z„Åi“»ƒlY­ċ²SgŽkÇ£—^S‰“qd¯•‹R…©éŽ£¯S†\\cZ¹iűƏCuƍÓX‡oR}“M^o•£…R}oªU­F…uuXHlEŕ‡€Ï©¤ÛmTŽþ¤D–²ÄufàÀ­XXȱAe„yYw¬dvõ´KÊ£”\\rµÄl”iˆdā]|DÂVŒœH¹ˆÞ®ÜWnŒC”Œķ W‹§@\\¸‹ƒ~¤‹Vp¸‰póIO¢ŠVOšŇürXql~òÉK]¤¥Xrfkvzpm¶bwyFoúvð‡¼¤ N°ąO¥«³[ƒéǡű_°Õ\\ÚÊĝŽþâőàerR¨­JYlďQ[ ÏYëЧTGz•tnŠß¡gFkMŸāGÁ¤ia É‰™È¹`\\xs€¬dĆkNnuNUŠ–užP@‚vRY¾•–\\¢…ŒGªóĄ~RãÖÎĢù‚đŴÕhQŽxtcæëSɽʼníëlj£ƍG£nj°KƘµDsØÑpyƸ®¿bXp‚]vbÍZuĂ{nˆ^IüœÀSք”¦EŒvRÎûh@℈[‚Əȉô~FNr¯ôçR±ƒ­HÑl•’Ģ–^¤¢‚OðŸŒævxsŒ]ÞÁTĠs¶¿âƊGW¾ìA¦·TѬ†è¥€ÏÐJ¨¼ÒÖ¼ƒƦɄxÊ~S–tD@ŠĂ¼Ŵ¡jlºWžvЉˆzƦZЎ²CH— „Axiukd‹ŒGgetqmcžÛ£Ozy¥cE}|…¾cZ…k‚‰¿uŐã[oxGikfeäT@…šSUwpiÚFM©’£è^ڟ‚`@v¶eň†f h˜eP¶žt“äOlÔUgƒÞzŸU`lœ}ÔÆUvØ_Ō¬Öi^ĉi§²ÃŠB~¡Ĉ™ÚEgc|DC_Ȧm²rBx¼MÔ¦ŮdĨÃâYx‘ƘDVÇĺĿg¿cwÅ\\¹˜¥Yĭlœ¤žOv†šLjM_a W`zļMž·\\swqÝSA‡š—q‰Śij¯Š‘°kŠRē°wx^Đkǂғ„œž“œŽ„‹\\]˜nrĂ}²ĊŲÒøãh·M{yMzysěnĒġV·°“G³¼XÀ““™¤¹i´o¤ŃšŸÈ`̃DzÄUĞd\\i֚ŒˆmÈBĤÜɲDEh LG¾ƀľ{WaŒYÍȏĢĘÔRîĐj‹}Ǟ“ccj‡oUb½š{“h§Ǿ{K‹ƖµÎ÷žGĀÖŠåưÎs­l›•yiē«‹`姝H¥Ae^§„GK}iã\\c]v©ģZ“mÃ|“[M}ģTɟĵ‘Â`À–çm‰‘FK¥ÚíÁbXš³ÌQґHof{‰]e€pt·GŋĜYünĎųVY^’˜ydõkÅZW„«WUa~U·Sb•wGçǑ‚“iW^q‹F‚“›uNĝ—·Ew„‹UtW·Ýďæ©PuqEzwAV•—XR‰ãQ`­©GŒM‡ehc›c”ďϝd‡©ÑW_ϗYƅŒ»…é\\ƒɹ~ǙG³mØ©BšuT§Ĥ½¢Ã_ý‘L¡‘ýŸqT^rme™\\Pp•ZZbƒyŸ’uybQ—efµ]UhĿDCmûvašÙNSkCwn‰cćfv~…Y‹„ÇG" + ], + "encodeOffsets": [[130196, 42528]] + } + }, + { + "type": "Feature", + "id": "230000", + "properties": { + "id": "230000", + "cp": [128.642464, 46.756967], + "name": "黑龙江", + "childNum": 2 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + "@@UƒµNÿ¥īè灋•HÍøƕ¶LŒǽ|g¨|”™Ža¾pViˆdd”~ÈiŒíďÓQġėǐZ΋ŽXb½|ſÃH½ŸKFgɱCģÛÇA‡n™‹jÕc[VĝDZÃ˄Ç_™ £ń³pŽj£º”š¿”»WH´¯”U¸đĢmžtĜyzzNN|g¸÷äűѱĉā~mq^—Œ[ƒ”››”ƒǁÑďlw]¯xQĔ‰¯l‰’€°řĴrŠ™˜BˆÞTxr[tŽ¸ĻN_yŸX`biN™Ku…P›£k‚ZĮ—¦[ºxÆÀdhŽĹŀUÈƗCw’áZħÄŭcÓ¥»NAw±qȥnD`{ChdÙFćš}¢‰A±Äj¨]ĊÕjŋ«×`VuÓś~_kŷVÝyh„“VkÄãPs”Oµ—fŸge‚Ň…µf@u_Ù ÙcŸªNªÙEojVx™T@†ãSefjlwH\\pŏäÀvŠŽlY†½d{†F~¦dyz¤PÜndsrhf‹HcŒvlwjFœ£G˜±DύƥY‡yϊu¹XikĿ¦ÏqƗǀOŜ¨LI|FRĂn sª|Cš˜zxAè¥bœfudTrFWÁ¹Am|˜ĔĕsķÆF‡´Nš‰}ć…UŠÕ@Áijſmužç’uð^ÊýowŒFzØÎĕNőžǏȎôªÌŒDŽàĀÄ˄ĞŀƒʀĀƘŸˮȬƬĊ°ƒUŸzou‡xe]}Ž…AyȑW¯ÌmK‡“Q]‹Īºif¸ÄX|sZt|½ÚUΠlkš^p{f¤lˆºlÆW –€A²˜PVܜPH”Êâ]ÎĈÌÜk´\\@qàsĔÄQºpRij¼èi†`¶—„bXƒrBgxfv»ŽuUiˆŒ^v~”J¬mVp´£Œ´VWrnP½ì¢BX‚¬h™ŠðX¹^TjVœŠriªj™tŊÄm€tPGx¸bgRšŽsT`ZozÆO]’ÒFô҆Oƒ‡ŊŒvŞ”p’cGŒêŠsx´DR–Œ{A†„EOr°Œ•žx|íœbˆ³Wm~DVjºéNN†Ëܲɶ­GƒxŷCStŸ}]ûō•SmtuÇÃĕN•™āg»šíT«u}ç½BĵÞʣ¥ëÊ¡Mێ³ãȅ¡ƋaǩÈÉQ‰†G¢·lG|›„tvgrrf«†ptęŘnŠÅĢr„I²¯LiØsPf˜_vĠd„xM prʹšL¤‹¤‡eˌƒÀđK“žïÙVY§]I‡óáĥ]ķ†Kˆ¥Œj|pŇ\\kzţ¦šnņäÔVĂîĪ¬|vW’®l¤èØr‚˜•xm¶ă~lÄƯĄ̈́öȄEÔ¤ØQĄ–Ą»ƢjȦOǺ¨ìSŖÆƬy”Qœv`–cwƒZSÌ®ü±DŽ]ŀç¬B¬©ńzƺŷɄeeOĨS’Œfm Ċ‚ƀP̎ēz©Ċ‚ÄÕÊmgŸÇsJ¥ƔˆŊśæ’΁Ñqv¿íUOµª‰ÂnĦÁ_½ä@ê텣P}Ġ[@gġ}g“ɊדûÏWXá¢užƻÌsNͽƎÁ§č՛AēeL³àydl›¦ĘVçŁpśdžĽĺſʃQíÜçÛġԏsĕ¬—Ǹ¯YßċġHµ ¡eå`ļƒrĉŘóƢFì“ĎWøxÊk†”ƈdƬv|–I|·©NqńRŀƒ¤é”eŊœŀ›ˆàŀU²ŕƀB‚Q£Ď}L¹Îk@©ĈuǰųǨ”Ú§ƈnTËÇéƟÊcfčŤ^Xm‡—HĊĕË«W·ċëx³ǔķÐċJā‚wİ_ĸ˜Ȁ^ôWr­°oú¬Ħ…ŨK~”ȰCĐ´Ƕ£’fNÎèâw¢XnŮeÂÆĶŽ¾¾xäLĴĘlļO¤ÒĨA¢Êɚ¨®‚ØCÔ ŬGƠ”ƦYĜ‡ĘÜƬDJ—g_ͥœ@čŅĻA“¶¯@wÎqC½Ĉ»NŸăëK™ďÍQ“Ùƫ[«Ãí•gßÔÇOÝáW‘ñuZ“¯ĥ€Ÿŕā¡ÑķJu¤E Ÿå¯°WKɱ_d_}}vyŸõu¬ï¹ÓU±½@gÏ¿rýD‰†g…Cd‰µ—°MFYxw¿CG£‹Rƛ½Õ{]L§{qqąš¿BÇƻğëšܭNJË|c²}Fµ}›ÙRsÓpg±ŠQNqǫŋRwŕnéÑÉKŸ†«SeYR…ŋ‹@{¤SJ}šD Ûǖ֍Ÿ]gr¡µŷjqWÛham³~S«“„›Þ]" + ] + ], + "encodeOffsets": [[[134456, 44547]]] + } + }, + { + "type": "Feature", + "id": "320000", + "properties": { + "id": "320000", + "cp": [119.767413, 33.041544], + "name": "江苏", + "childNum": 1 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@cþÅPiŠ`ZŸRu¥É\\]~°ŽY`µ†Óƒ^phÁbnÀşúŽòa–ĬºTÖŒb‚˜e¦¦€{¸ZâćNpŒ©žHr|^ˆmjhŠSEb\\afv`sz^lkŽlj‹Ätg‹¤D˜­¾Xš¿À’|ДiZ„ȀåB·î}GL¢õcßjaŸyBFµÏC^ĭ•cÙt¿sğH]j{s©HM¢ƒQnDÀ©DaÜތ·jgàiDbPufjDk`dPOîƒhw¡ĥ‡¥šG˜ŸP²ĐobºrY†„î¶aHŢ´ ]´‚rılw³r_{£DB_Ûdåuk|ˆŨ¯F Cºyr{XFy™e³Þċ‡¿Â™kĭB¿„MvÛpm`rÚã”@Ę¹hågËÖƿxnlč¶Åì½Ot¾dJlŠVJʜǀœŞqvnOŠ^ŸJ”Z‘ż·Q}ê͎ÅmµÒ]Žƍ¦Dq}¬R^èĂ´ŀĻĊIԒtžIJyQŐĠMNtœR®òLh‰›Ěs©»œ}OӌGZz¶A\\jĨFˆäOĤ˜HYš†JvÞHNiÜaϚɖnFQlšNM¤ˆB´ĄNöɂtp–Ŭdf先‹qm¿QûŠùއÚb¤uŃJŴu»¹Ą•lȖħŴw̌ŵ²ǹǠ͛hĭłƕrçü±Y™xci‡tğ®jű¢KOķ•Coy`å®VTa­_Ā]ŐÝɞï²ʯÊ^]afYǸÃĆēĪȣJđ͍ôƋĝÄ͎ī‰çÛɈǥ£­ÛmY`ó£Z«§°Ó³QafusNıDž_k}¢m[ÝóDµ—¡RLčiXy‡ÅNïă¡¸iĔϑNÌŕoēdōîåŤûHcs}~Ûwbù¹£¦ÓCt‹OPrƒE^ÒoŠg™ĉIµžÛÅʹK…¤½phMŠü`o怆ŀ" + ], + "encodeOffsets": [[121740, 32276]] + } + }, + { + "type": "Feature", + "id": "330000", + "properties": { + "id": "330000", + "cp": [120.153576, 29.287459], + "name": "浙江", + "childNum": 45 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@E^dQ]K"], + ["@@jX^j‡"], + ["@@sfŠbU‡"], + ["@@qP\\xz[ck"], + ["@@‘Rƒ¢‚FX}°[s_"], + ["@@Cbœ\\—}"], + ["@@e|v\\la{u"], + ["@@v~u}"], + ["@@QxÂF¯}"], + ["@@¹nŒvÞs¯o"], + ["@@rSkUEj"], + ["@@bi­ZŒP"], + ["@@p[}INf"], + ["@@À¿€"], + ["@@¹dnbŒ…"], + ["@@rSŸBnR"], + ["@@g~h}"], + ["@@FlEk"], + ["@@OdPc"], + ["@@v[u\\"], + ["@@FjâL~wyoo~›sµL–\\"], + ["@@¬e¹aNˆ"], + ["@@\\nÔ¡q]L³ë\\ÿ®ŒQ֎"], + ["@@ÊA­©[¬"], + ["@@KxŒv­"], + ["@@@hlIk]"], + ["@@pW{o||j"], + ["@@Md|_mC"], + ["@@¢…X£ÏylD¼XˆtH"], + ["@@hlÜ[LykAvyfw^Ež›¤"], + ["@@fp¤Mus“R"], + ["@@®_ma~•LÁ¬šZ"], + ["@@iM„xZ"], + ["@@ZcYd"], + ["@@Z~dOSo|A¿qZv"], + ["@@@`”EN¡v"], + ["@@|–TY{"], + ["@@@n@m"], + ["@@XWkCT\\"], + ["@@ºwšZRkĕWO¢"], + ["@@™X®±Grƪ\\ÔáXq{‹"], + ["@@ůTG°ĄLHm°UC‹"], + [ + "@@¤Ž€aÜx~}dtüGæţŎíĔcŖpMËВjē¢·ðĄÆMzˆjWKĎ¢Q¶˜À_꒔_Bı€i«pZ€gf€¤Nrq]§ĂN®«H±‡yƳí¾×ŸīàLłčŴǝĂíÀBŖÕªˆŠÁŖHŗʼnåqûõi¨hÜ·ƒñt»¹ýv_[«¸m‰YL¯‰Qª…mĉÅdMˆ•gÇjcº«•ęœ¬­K­´ƒB«Âącoċ\\xKd¡gěŧ«®á’[~ıxu·Å”KsËɏc¢Ù\\ĭƛëbf¹­ģSƒĜkáƉÔ­ĈZB{ŠaM‘µ‰fzʼnfåÂŧįƋǝÊĕġć£g³ne­ą»@­¦S®‚\\ßðCšh™iqªĭiAu‡A­µ”_W¥ƣO\\lċĢttC¨£t`ˆ™PZäuXßBs‡Ļyek€OđġĵHuXBšµ]׌‡­­\\›°®¬F¢¾pµ¼kŘó¬Wät’¸|@ž•L¨¸µr“ºù³Ù~§WI‹ŸZWŽ®’±Ð¨ÒÉx€`‰²pĜ•rOògtÁZ}þÙ]„’¡ŒŸFK‚wsPlU[}¦Rvn`hq¬\\”nQ´ĘRWb”‚_ rtČFI֊kŠŠĦPJ¶ÖÀÖJĈĄTĚòžC ²@Pú…Øzœ©PœCÈڜĒ±„hŖ‡l¬â~nm¨f©–iļ«m‡nt–u†ÖZÜÄj“ŠLŽ®E̜Fª²iÊxبžIÈhhst" + ], + ["@@o\\V’zRZ}y"], + ["@@†@°¡mۛGĕ¨§Ianá[ýƤjfæ‡ØL–•äGr™"] + ], + "encodeOffsets": [ + [[125592, 31553]], + [[125785, 31436]], + [[125729, 31431]], + [[125513, 31380]], + [[125223, 30438]], + [[125115, 30114]], + [[124815, 29155]], + [[124419, 28746]], + [[124095, 28635]], + [[124005, 28609]], + [[125000, 30713]], + [[125111, 30698]], + [[125078, 30682]], + [[125150, 30684]], + [[124014, 28103]], + [[125008, 31331]], + [[125411, 31468]], + [[125329, 31479]], + [[125626, 30916]], + [[125417, 30956]], + [[125254, 30976]], + [[125199, 30997]], + [[125095, 31058]], + [[125083, 30915]], + [[124885, 31015]], + [[125218, 30798]], + [[124867, 30838]], + [[124755, 30788]], + [[124802, 30809]], + [[125267, 30657]], + [[125218, 30578]], + [[125200, 30562]], + [[124968, 30474]], + [[125167, 30396]], + [[124955, 29879]], + [[124714, 29781]], + [[124762, 29462]], + [[124325, 28754]], + [[123990, 28459]], + [[125366, 31477]], + [[125115, 30363]], + [[125369, 31139]], + [[122495, 31878]], + [[125329, 30690]], + [[125192, 30787]] + ] + } + }, + { + "type": "Feature", + "id": "340000", + "properties": { "id": "340000", "cp": [117.283042, 31.26119], "name": "安徽", "childNum": 3 }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@^iuLX^"], + ["@@‚e©Ehl"], + [ + "@@°ZÆëϵmkǀwÌÕæhºgBĝâqÙĊz›ÖgņtÀÁÊÆá’hEz|WzqD¹€Ÿ°E‡ŧl{ævÜcA`¤C`|´qžxIJkq^³³ŸGšµbƒíZ…¹qpa±ď OH—¦™Ħˆx¢„gPícOl_iCveaOjCh߸i݋bÛªCC¿€m„RV§¢A|t^iĠGÀtÚs–d]ĮÐDE¶zAb àiödK¡~H¸íæAžǿYƒ“j{ď¿‘™À½W—®£ChŒÃsiŒkkly]_teu[bFa‰Tig‡n{]Gqªo‹ĈMYá|·¥f¥—őaSÕė™NµñĞ«ImŒ_m¿Âa]uĜp …Z_§{Cƒäg¤°r[_Yj‰ÆOdý“[ŽI[á·¥“Q_n‡ùgL¾mv™ˊBÜƶĊJhšp“c¹˜O]iŠ]œ¥ jtsggJǧw×jÉ©±›EFˍ­‰Ki”ÛÃÕYv…s•ˆm¬njĻª•§emná}k«ŕˆƒgđ²Ù›DǤ›í¡ªOy›†×Où±@DŸñSęćăÕIÕ¿IµĥO‰‰jNÕËT¡¿tNæŇàåyķrĕq§ÄĩsWÆߎF¶žX®¿‰mŒ™w…RIޓfßoG‘³¾©uyH‘į{Ɓħ¯AFnuP…ÍÔzšŒV—dàôº^Ðæd´€‡oG¤{S‰¬ćxã}›ŧ×Kǥĩ«žÕOEзÖdÖsƘѨ[’Û^Xr¢¼˜§xvěƵ`K”§ tÒ´Cvlo¸fzŨð¾NY´ı~ÉĔē…ßúLÃϖ_ÈÏ|]ÂÏFl”g`bšežž€n¾¢pU‚h~ƴĖ¶_‚r sĄ~cž”ƈ]|r c~`¼{À{ȒiJjz`îÀT¥Û³…]’u}›f…ïQl{skl“oNdŸjŸäËzDvčoQŠďHI¦rb“tHĔ~BmlRš—V_„ħTLnñH±’DžœL‘¼L˜ªl§Ťa¸ŒĚlK²€\\RòvDcÎJbt[¤€D@®hh~kt°ǾzÖ@¾ªdb„YhüóZ ň¶vHrľ\\ʗJuxAT|dmÀO„‹[ÃԋG·ĚąĐlŪÚpSJ¨ĸˆLvÞcPæķŨŽ®mАˆálŸwKhïgA¢ųƩޖ¤OȜm’°ŒK´" + ] + ], + "encodeOffsets": [[[121722, 32278]], [[119475, 30423]], [[119168, 35472]]] + } + }, + { + "type": "Feature", + "id": "350000", + "properties": { + "id": "350000", + "cp": [118.306239, 26.075302], + "name": "福建", + "childNum": 18 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@“zht´‡]"], + ["@@aj^~ĆG—©O"], + ["@@ed¨„C}}i"], + ["@@@vˆPGsQ"], + ["@@‰sBz‚ddW]Q"], + ["@@SŽ¨Q“{"], + ["@@NŽVucW"], + ["@@qptBAq"], + ["@@‰’¸[mu"], + ["@@Q\\pD]_"], + ["@@jSwUadpF"], + ["@@eXª~ƒ•"], + ["@@AjvFso"], + ["@@fT–›_Çí\\Ÿ™—v|ba¦jZÆy€°"], + ["@@IjJi"], + ["@@wJI€ˆxš«¼AoNe{M­"], + ["@@K‰±¡Óˆ”ČäeZ"], + [ + "@@k¡¹Eh~c®wBk‹UplÀ¡I•~Māe£bN¨gZý¡a±Öcp©PhžI”Ÿ¢Qq…ÇGj‹|¥U™ g[Ky¬ŏ–v@OpˆtÉEŸF„\\@ åA¬ˆV{Xģ‰ĐBy…cpě…¼³Ăp·¤ƒ¥o“hqqÚ¡ŅLsƒ^ᗞ§qlŸÀhH¨MCe»åÇGD¥zPO£čÙkJA¼ß–ėu›ĕeûҍiÁŧSW¥˜QŠûŗ½ùěcݧSùĩąSWó«íęACµ›eR—åǃRCÒÇZÍ¢‹ź±^dlsŒtjD¸•‚ZpužÔâÒH¾oLUêÃÔjjēò´ĄW‚ƛ…^Ñ¥‹ĦŸ@Çò–ŠmŒƒOw¡õyJ†yD}¢ďÑÈġfŠZd–a©º²z£šN–ƒjD°Ötj¶¬ZSÎ~¾c°¶Ðm˜x‚O¸¢Pl´žSL|¥žA†ȪĖM’ņIJg®áIJČĒü` ŽQF‡¬h|ÓJ@zµ |ê³È ¸UÖŬŬÀEttĸr‚]€˜ðŽM¤ĶIJHtÏ A’†žĬkvsq‡^aÎbvŒd–™fÊòSD€´Z^’xPsÞrv‹ƞŀ˜jJd×ŘÉ ®A–ΦĤd€xĆqAŒ†ZR”ÀMźŒnĊ»ŒİÐZ— YX–æJŠyĊ²ˆ·¶q§·–K@·{s‘Xãô«lŗ¶»o½E¡­«¢±¨Yˆ®Ø‹¶^A™vWĶGĒĢžPlzfˆļŽtàAvWYãšO_‡¤sD§ssČġ[kƤPX¦Ž`¶“ž®ˆBBvĪjv©šjx[L¥àï[F…¼ÍË»ğV`«•Ip™}ccÅĥZE‹ãoP…´B@ŠD—¸m±“z«Ƴ—¿å³BRضˆœWlâþäą`“]Z£Tc— ĹGµ¶H™m@_©—kŒ‰¾xĨ‡ôȉðX«½đCIbćqK³Á‹Äš¬OAwã»aLʼn‡ËĥW[“ÂGI—ÂNxij¤D¢ŽîĎÎB§°_JœGsƒ¥E@…¤uć…P‘å†cuMuw¢BI¿‡]zG¹guĮck\\_" + ] + ], + "encodeOffsets": [ + [[123250, 27563]], + [[122541, 27268]], + [[123020, 27189]], + [[122916, 27125]], + [[122887, 26845]], + [[122808, 26762]], + [[122568, 25912]], + [[122778, 26197]], + [[122515, 26757]], + [[122816, 26587]], + [[123388, 27005]], + [[122450, 26243]], + [[122578, 25962]], + [[121255, 25103]], + [[120987, 24903]], + [[122339, 25802]], + [[121042, 25093]], + [[122439, 26024]] + ] + } + }, + { + "type": "Feature", + "id": "360000", + "properties": { + "id": "360000", + "cp": [115.592151, 27.676493], + "name": "江西", + "childNum": 1 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@ĢĨƐgļˆ¼ÂMD~ņªe^\\^§„ý©j׍cZ†Ø¨zdÒa¶ˆlҍJŒìõ`oz÷@¤u޸´†ôęöY¼‰HČƶajlÞƩ¥éZ[”|h}^U Œ ¥p„ĄžƦO lt¸Æ €Q\\€ŠaÆ|CnÂOjt­ĚĤd’ÈŒF`’¶„@Ð딠¦ōҞ¨Sêv†HĢûXD®…QgėWiØPÞìºr¤dž€NĠ¢l–•ĄtZoœCƞÔºCxrpĠV®Ê{f_Y`_ƒeq’’®Aot`@o‚DXfkp¨|Šs¬\\D‘ÄSfè©Hn¬…^DhÆyøJh“ØxĢĀLʈ„ƠPżċĄwȠ̦G®ǒĤäTŠÆ~ĦwŠ«|TF¡Šn€c³Ïå¹]ĉđxe{ÎӐ†vOEm°BƂĨİ|G’vz½ª´€H’àp”eJ݆Qšxn‹ÀŠW­žEµàXÅĪt¨ÃĖrÄwÀFÎ|ňÓMå¼ibµ¯»åDT±m[“r«_gŽmQu~¥V\\OkxtL E¢‹ƒ‘Ú^~ýê‹Pó–qo슱_Êw§ÑªåƗā¼‹mĉŹ‹¿NQ“…YB‹ąrwģcÍ¥B•Ÿ­ŗÊcØiI—žƝĿuŒqtāwO]‘³YCñTeɕš‹caub͈]trlu€ī…B‘ПGsĵıN£ï—^ķqss¿FūūV՟·´Ç{éĈý‰ÿ›OEˆR_ŸđûIċâJh­ŅıN‘ȩĕB…¦K{Tk³¡OP·wn—µÏd¯}½TÍ«YiµÕsC¯„iM•¤™­•¦¯P|ÿUHv“he¥oFTu‰õ\\ŽOSs‹MòđƇiaºćXŸĊĵà·çhƃ÷ǜ{‘ígu^›đg’m[×zkKN‘¶Õ»lčÓ{XSƉv©_ÈëJbVk„ĔVÀ¤P¾ºÈMÖxlò~ªÚàGĂ¢B„±’ÌŒK˜y’áV‡¼Ã~­…`g›ŸsÙfI›Ƌlę¹e|–~udjˆuTlXµf`¿JdŠ[\\˜„L‚‘²" + ], + "encodeOffsets": [[116689, 26234]] + } + }, + { + "type": "Feature", + "id": "370000", + "properties": { + "id": "370000", + "cp": [118.000923, 36.275807], + "name": "山东", + "childNum": 13 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@Xjd]{K"], + ["@@itbFHy"], + ["@@HlGk"], + ["@@T‚ŒGŸy"], + ["@@K¬˜•‹U"], + ["@@WdXc"], + ["@@PtOs"], + ["@@•LnXhc"], + ["@@ppVƒu]Or"], + ["@@cdzAUa"], + ["@@udRhnCI‡"], + ["@@ˆoIƒpR„"], + [ + "@@Ľč{fzƤî’Kš–ÎMĮ]†—ZFˆ½Y]â£ph’™š¶¨râøÀ†ÎǨ¤^ºÄ”Gzˆ~grĚĜlĞƄLĆdž¢Îo¦–cv“Kb€gr°Wh”mZp ˆL]LºcU‰Æ­n”żĤÌĒœbAnrOAœ´žȊcÀbƦUØrĆUÜøœĬƞ†š˜Ez„VL®öØBkŖÝĐĖ¹ŧ̄±ÀbÎɜnb²ĦhņBĖ›žįĦåXćì@L¯´ywƕCéõė ƿ¸‘lµ¾Z|†ZWyFYŸ¨Mf~C¿`€à_RÇzwƌfQnny´INoƬˆèôº|sT„JUš›‚L„îVj„ǎ¾Ē؍‚Dz²XPn±ŴPè¸ŔLƔÜƺ_T‘üÃĤBBċȉöA´fa„˜M¨{«M`‡¶d¡ô‰Ö°šmȰBÔjjŒ´PM|”c^d¤u•ƒ¤Û´Œä«ƢfPk¶Môlˆ]Lb„}su^ke{lC‘…M•rDŠÇ­]NÑFsmoõľH‰yGă{{çrnÓE‰‹ƕZGª¹Fj¢ïW…uøCǷ돡ąuhÛ¡^Kx•C`C\\bÅxì²ĝÝ¿_N‰īCȽĿåB¥¢·IŖÕy\\‡¹kx‡Ã£Č×GDyÕ¤ÁçFQ¡„KtŵƋ]CgÏAùSed‡cÚź—ŠuYfƒyMmhUWpSyGwMPqŀ—›Á¼zK›¶†G•­Y§Ëƒ@–´śÇµƕBmœ@Io‚g——Z¯u‹TMx}C‘‰VK‚ï{éƵP—™_K«™pÛÙqċtkkù]gŽ‹Tğwo•ɁsMõ³ă‡AN£™MRkmEʕč™ÛbMjÝGu…IZ™—GPģ‡ãħE[iµBEuŸDPԛ~ª¼ętŠœ]ŒûG§€¡QMsğNPŏįzs£Ug{đJĿļā³]ç«Qr~¥CƎÑ^n¶ÆéÎR~Ż¸Y’I“] P‰umŝrƿ›‰›Iā‹[x‰edz‹L‘¯v¯s¬ÁY…~}…ťuŁŒg›ƋpÝĄ_ņī¶ÏSR´ÁP~ž¿Cyžċßdwk´Ss•X|t‰`Ä Èð€AªìÎT°¦Dd–€a^lĎDĶÚY°Ž`ĪŴǒˆ”àŠv\\ebŒZH„ŖR¬ŢƱùęO•ÑM­³FۃWp[ƒ" + ] + ], + "encodeOffsets": [ + [[123806, 39303]], + [[123821, 39266]], + [[123742, 39256]], + [[123702, 39203]], + [[123649, 39066]], + [[123847, 38933]], + [[123580, 38839]], + [[123894, 37288]], + [[123043, 36624]], + [[123344, 38676]], + [[123522, 38857]], + [[123628, 38858]], + [[118260, 36742]] + ] + } + }, + { + "type": "Feature", + "id": "410000", + "properties": { + "id": "410000", + "cp": [113.665412, 33.757975], + "name": "河南", + "childNum": 1 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@•ýL™ùµP³swIÓxcŢĞð†´E®žÚPt†ĴXØx¶˜@«ŕŕQGƒ‹Yfa[şu“ßǩ™đš_X³ijÕčC]kbc•¥CS¯ëÍB©÷‹–³­Siˆ_}m˜YTtž³xlàcȂzÀD}ÂOQ³ÐTĨ¯†ƗòËŖ[hœł‹Ŧv~††}ÂZž«¤lPǕ£ªÝŴÅR§ØnhcŒtâk‡nύ­ľŹUÓÝdKuķ‡I§oTũÙďkęĆH¸ÓŒ\\ăŒ¿PcnS{wBIvɘĽ[GqµuŸŇôYgûƒZcaŽ©@½Õǽys¯}lgg@­C\\£as€IdÍuCQñ[L±ęk·‹ţb¨©kK—’»›KC²‘òGKmĨS`ƒ˜UQ™nk}AGē”sqaJ¥ĐGR‰ĎpCuÌy ã iMc”plk|tRk†ðœev~^‘´†¦ÜŽSí¿_iyjI|ȑ|¿_»d}qŸ^{“Ƈdă}Ÿtqµ`Ƴĕg}V¡om½fa™Ço³TTj¥„tĠ—Ry”K{ùÓjuµ{t}uËR‘iŸvGŠçJFjµŠÍyqΘàQÂFewixGw½Yŷpµú³XU›½ġy™łå‰kÚwZXˆ·l„¢Á¢K”zO„Λ΀jc¼htoDHr…|­J“½}JZ_¯iPq{tę½ĕ¦Zpĵø«kQ…Ťƒ]MÛfaQpě±ǽ¾]u­Fu‹÷nƒ™čįADp}AjmcEǒaª³o³ÆÍSƇĈÙDIzˑ赟^ˆKLœ—i—Þñ€[œƒaA²zz‰Ì÷Dœ|[šíijgf‚ÕÞd®|`ƒĆ~„oĠƑô³Ŋ‘D×°¯CsŠøÀ«ì‰UMhTº¨¸ǡîS–Ô„DruÂÇZ•ÖEŽ’vPZ„žW”~؋ÐtĄE¢¦Ðy¸bŠô´oŬ¬Ž²Ês~€€]®tªašpŎJ¨Öº„_ŠŔ–`’Ŗ^Ѝ\\Ĝu–”~m²Ƹ›¸fW‰ĦrƔ}Î^gjdfÔ¡J}\\n C˜¦þWxªJRÔŠu¬ĨĨmF†dM{\\d\\ŠYÊ¢ú@@¦ª²SŠÜsC–}fNècbpRmlØ^g„d¢aÒ¢CZˆZxvÆ¶N¿’¢T@€uCœ¬^ĊðÄn|žlGl’™Rjsp¢ED}€Fio~ÔNŽ‹„~zkĘHVsDzßjƒŬŒŠŢ`Pûàl¢˜\\ÀœEhŽİgÞē X¼Pk–„|m" + ], + "encodeOffsets": [[118256, 37017]] + } + }, + { + "type": "Feature", + "id": "420000", + "properties": { + "id": "420000", + "cp": [113.298572, 30.684355], + "name": "湖北", + "childNum": 3 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@AB‚"], + ["@@lskt"], + [ + "@@¾«}{ra®pîÃ\\™›{øCŠËyyB±„b\\›ò˜Ý˜jK›‡L ]ĎĽÌ’JyÚCƈćÎT´Å´pb©È‘dFin~BCo°BĎĚømvŒ®E^vǾ½Ĝ²Ro‚bÜeNŽ„^ĺ£R†¬lĶ÷YoĖ¥Ě¾|sOr°jY`~I”¾®I†{GqpCgyl{‡£œÍƒÍyPL“¡ƒ¡¸kW‡xYlÙ抚ŁĢzœ¾žV´W¶ùŸo¾ZHxjwfx„GNÁ•³Xéæl¶‰EièIH‰ u’jÌQ~v|sv¶Ôi|ú¢Fh˜Qsğ¦ƒSiŠBg™ÐE^ÁÐ{–čnOÂȞUÎóĔ†ÊēIJ}Z³½Mŧïeyp·uk³DsѨŸL“¶_œÅuèw»—€¡WqÜ]\\‘Ò§tƗcÕ¸ÕFÏǝĉăxŻČƟO‡ƒKÉġÿ×wg”÷IÅzCg†]m«ªGeçÃTC’«[‰t§{loWeC@ps_Bp‘­r‘„f_``Z|ei¡—oċMqow€¹DƝӛDYpûs•–‹Ykıǃ}s¥ç³[§ŸcYŠ§HK„«Qy‰]¢“wwö€¸ïx¼ņ¾Xv®ÇÀµRĠЋžHMž±cÏd„ƒǍũȅȷ±DSyúĝ£ŤĀàtÖÿï[îb\\}pĭÉI±Ñy…¿³x¯N‰o‰|¹H™ÏÛm‹júË~Tš•u˜ęjCöAwě¬R’đl¯ Ñb­‰ŇT†Ŀ_[Œ‘IčĄʿnM¦ğ\\É[T·™k¹œ©oĕ@A¾w•ya¥Y\\¥Âaz¯ãÁ¡k¥ne£Ûw†E©Êō¶˓uoj_Uƒ¡cF¹­[Wv“P©w—huÕyBF“ƒ`R‹qJUw\\i¡{jŸŸEPïÿ½fć…QÑÀQ{ž‚°‡fLԁ~wXg—ītêݾ–ĺ‘Hdˆ³fJd]‹HJ²…E€ƒoU¥†HhwQsƐ»Xmg±çve›]Dm͂PˆoCc¾‹_h”–høYrŊU¶eD°Č_N~øĹĚ·`z’]Äþp¼…äÌQŒv\\rCŒé¾TnkžŐڀÜa‡“¼ÝƆĢ¶Ûo…d…ĔňТJq’Pb ¾|JŒ¾fXŠƐîĨ_Z¯À}úƲ‹N_ĒĊ^„‘ĈaŐyp»CÇĕKŠšñL³ŠġMŒ²wrIÒŭxjb[œžn«øœ˜—æˆàƒ ^²­h¯Ú€ŐªÞ¸€Y²ĒVø}Ā^İ™´‚LŠÚm„¥ÀJÞ{JVŒųÞŃx×sxxƈē ģMř–ÚðòIf–Ċ“Œ\\Ʈ±ŒdʧĘD†vČ_Àæ~DŒċ´A®µ†¨ØLV¦êHÒ¤" + ] + ], + "encodeOffsets": [[[113712, 34000]], [[115612, 30507]], [[113649, 34054]]] + } + }, + { + "type": "Feature", + "id": "430000", + "properties": { "id": "430000", "cp": [111.782279, 28.09409], "name": "湖南", "childNum": 3 }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@—n„FTs"], + ["@@ßÅÆችÔXr—†CO™“…ËR‘ïÿĩ­TooQyšÓ[‹ŅBE¬–ÎÓXa„į§Ã¸G °ITxp‰úxÚij¥Ïš–Ģ¾ŠedžÄ©ĸG…œàGh‚€M¤–Â_U}Ċ}¢pczfŠþg¤€”ÇòAV‘‹M"], + [ + "@@©K—ƒA·³CQ±Á«³BUŠƑ¹AŠtćOw™D]ŒJiØSm¯b£‘ylƒ›X…HËѱH•«–‘C^õľA–Å§¤É¥„ïyuǙuA¢^{ÌC´­¦ŷJ£^[†“ª¿‡ĕ~•Ƈ…•N… skóā‡¹¿€ï]ă~÷O§­@—Vm¡‹Qđ¦¢Ĥ{ºjԏŽŒª¥nf´•~ÕoŸž×Ûą‹MąıuZœmZcÒ IJĪ²SÊDŽŶ¨ƚƒ’CÖŎªQؼrŭŽ­«}NÏürʬŒmjr€@ĘrTW ­SsdHzƓ^ÇÂyUi¯DÅYlŹu{hTœ}mĉ–¹¥ě‰Dÿë©ıÓ[Oº£ž“¥ót€ł¹MՄžƪƒ`Pš…Di–ÛUŠ¾Å‌ìˆU’ñB“È£ýhe‰dy¡oċ€`pfmjP~‚kZa…ZsÐd°wj§ƒ@€Ĵ®w~^‚kÀÅKvNmX\\¨a“”сqvíó¿F„¤¡@ũÑVw}S@j}¾«pĂr–ªg àÀ²NJ¶¶Dô…K‚|^ª†Ž°LX¾ŴäPĪ±œ£EXd›”^¶›IJÞܓ~‘u¸ǔ˜Ž›MRhsR…e†`ÄofIÔ\\Ø  i”ćymnú¨cj ¢»–GČìƊÿШXeĈĀ¾Oð Fi ¢|[jVxrIQŒ„_E”zAN¦zLU`œcªx”OTu RLÄ¢dV„i`p˔vŎµªÉžF~ƒØ€d¢ºgİàw¸Áb[¦Zb¦–z½xBĖ@ªpº›šlS¸Ö\\Ĕ[N¥ˀmĎă’J\\‹ŀ`€…ňSڊĖÁĐiO“Ĝ«BxDõĚiv—ž–S™Ì}iùŒžÜnšÐºGŠ{Šp°M´w†ÀÒzJ²ò¨ oTçüöoÛÿñŽőФ‚ùTz²CȆȸǎŪƒƑÐc°dPÎŸğ˶[Ƚu¯½WM¡­Éž“’B·rížnZŸÒ `‡¨GA¾\\pē˜XhÆRC­üWGġu…T靧Ŏѝ©ò³I±³}_‘‹EÃħg®ęisÁPDmÅ{‰b[Rşs·€kPŸŽƥƒóRo”O‹ŸVŸ~]{g\\“êYƪ¦kÝbiċƵŠGZ»Ěõ…ó·³vŝž£ø@pyö_‹ëŽIkѵ‡bcѧy…×dY؎ªiþž¨ƒ[]f]Ņ©C}ÁN‡»hĻħƏ’ĩ" + ] + ], + "encodeOffsets": [[[115640, 30489]], [[112543, 27312]], [[116690, 26230]]] + } + }, + { + "type": "Feature", + "id": "440000", + "properties": { + "id": "440000", + "cp": [113.280637, 23.125178], + "name": "广东", + "childNum": 24 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@QdˆAua"], + ["@@ƒlxDLo"], + ["@@sbhNLo"], + ["@@Ă āŸ"], + ["@@WltO[["], + ["@@Krœ]S"], + ["@@e„„I]y"], + ["@@I|„Mym"], + ["@@ƒÛ³LSŒž¼Y"], + ["@@nvºB–ëui©`¾"], + ["@@zdšÛ›Jw®"], + ["@@†°…¯"], + ["@@a yAª¸ËJIx،@€ĀHAmßV¡o•fu•o"], + ["@@šs‰ŗÃÔėAƁ›ZšÄ ~°ČP‚‹äh"], + ["@@‹¶Ý’Ì‚vmĞh­ı‡Q"], + ["@@HœŠdSjĒ¢D}war…“u«ZqadYM"], + ["@@elŒ\\LqqU"], + ["@@~rMo\\"], + ["@@f„^ƒC"], + ["@@øPªoj÷ÍÝħXČx”°Q¨ıXNv"], + ["@@gÇƳˆŽˆ”oˆŠˆ[~tly"], + ["@@E–ÆC¿‘"], + ["@@OŽP"], + [ + "@@w‹†đóg‰™ĝ—[³‹¡VÙæÅöM̳¹pÁaËýý©D©Ü“JŹƕģGą¤{Ùū…ǘO²«BƱéA—Ò‰ĥ‡¡«BhlmtÃPµyU¯uc“d·w_bŝcīímGOŽ|KP’ȏ‡ŹãŝIŕŭŕ@Óoo¿ē‹±ß}Ž…ŭ‚ŸIJWÈCőâUâǙI›ğʼn©I›ijEׅÁ”³Aó›wXJþ±ÌŒÜӔĨ£L]ĈÙƺZǾĆĖMĸĤfŒÎĵl•ŨnȈ‘ĐtF”Š–FĤ–‚êk¶œ^k°f¶gŠŽœ}®Fa˜f`vXŲxl˜„¦–ÔÁ²¬ÐŸ¦pqÊ̲ˆi€XŸØRDÎ}†Ä@ZĠ’s„x®AR~®ETtĄZ†–ƈfŠŠHâÒÐA†µ\\S¸„^wĖkRzŠalŽŜ|E¨ÈNĀňZTŒ’pBh£\\ŒĎƀuXĖtKL–¶G|Ž»ĺEļĞ~ÜĢÛĊrˆO˜Ùîvd]nˆ¬VœÊĜ°R֟pM††–‚ƂªFbwžEÀˆ˜©Œž\\…¤]ŸI®¥D³|ˎ]CöAŤ¦…æ’´¥¸Lv¼€•¢ĽBaô–F~—š®²GÌҐEY„„œzk¤’°ahlV՞I^‹šCxĈPŽsB‰ƒºV‰¸@¾ªR²ĨN]´_eavSi‡vc•}p}Đ¼ƌkJœÚe thœ†_¸ ºx±ò_xN›Ë‹²‘@ƒă¡ßH©Ùñ}wkNÕ¹ÇO½¿£ĕ]ly_WìIžÇª`ŠuTÅxYĒÖ¼k֞’µ‚MžjJÚwn\\h‘œĒv]îh|’È›Ƅøègž¸Ķß ĉĈWb¹ƀdéʌNTtP[ŠöSvrCZžžaGuœbo´ŖÒÇА~¡zCI…özx¢„Pn‹•‰Èñ @ŒĥÒ¦†]ƞŠV}³ăĔñiiÄÓVépKG½Ä‘ÓávYo–C·sit‹iaÀy„ŧΡÈYDÑům}‰ý|m[węõĉZÅxUO}÷N¹³ĉo_qtă“qwµŁYلǝŕ¹tïÛUïmRCº…ˆĭ|µ›ÕÊK™½R‘ē ó]‘–GªęAx–»HO£|ām‡¡diď×YïYWªʼnOeÚtĐ«zđ¹T…ā‡úE™á²\\‹ķÍ}jYàÙÆſ¿Çdğ·ùTßÇţʄ¡XgWÀLJğ·¿ÃˆOj YÇ÷Qě‹i" + ] + ], + "encodeOffsets": [ + [[117381, 22988]], + [[116552, 22934]], + [[116790, 22617]], + [[116973, 22545]], + [[116444, 22536]], + [[116931, 22515]], + [[116496, 22490]], + [[116453, 22449]], + [[113301, 21439]], + [[118726, 21604]], + [[118709, 21486]], + [[113210, 20816]], + [[115482, 22082]], + [[113171, 21585]], + [[113199, 21590]], + [[115232, 22102]], + [[115739, 22373]], + [[115134, 22184]], + [[113056, 21175]], + [[119573, 21271]], + [[119957, 24020]], + [[115859, 22356]], + [[116561, 22649]], + [[116285, 22746]] + ] + } + }, + { + "type": "Feature", + "id": "450000", + "properties": { "id": "450000", "cp": [108.320004, 22.82402], "name": "广西", "childNum": 2 }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@H– TQ§•A"], + [ + "@@ĨʪƒLƒƊDÎĹĐCǦė¸zÚGn£¾›rªŀÜt¬@֛ڈSx~øOŒ˜ŶÐÂæȠ\\„ÈÜObĖw^oބLf¬°bI lTØB̈F£Ć¹gñĤaY“t¿¤VSñœK¸¤nM†¼‚JE±„½¸šŠño‹ÜCƆæĪ^ŠĚQÖ¦^‡ˆˆf´Q†üÜʝz¯šlzUĺš@쇀p¶n]sxtx¶@„~ÒĂJb©gk‚{°‚~c°`ԙ¬rV\\“la¼¤ôá`¯¹LC†ÆbŒxEræO‚v[H­˜„[~|aB£ÖsºdAĐzNÂðsŽÞƔ…Ĥªbƒ–ab`ho¡³F«èVloŽ¤™ÔRzpp®SŽĪº¨ÖƒºN…ij„d`’a”¦¤F³ºDÎńĀìŠCžĜº¦Ċ•~nS›|gźvZkCÆj°zVÈÁƔ]LÊFZg…čP­kini«‹qǀcz͔Y®¬Ů»qR×ō©DՄ‘§ƙǃŵTÉĩ±ŸıdÑnYY›IJvNĆƌØÜ Öp–}e³¦m‹©iÓ|¹Ÿħņ›|ª¦QF¢Â¬ʖovg¿em‡^ucà÷gՎuŒíÙćĝ}FĻ¼Ĺ{µHK•sLSđƃr‹č¤[Ag‘oS‹ŇYMÿ§Ç{Fśbky‰lQxĕƒ]T·¶[B…ÑÏGáşşƇe€…•ăYSs­FQ}­Bƒw‘tYğÃ@~…C̀Q ×W‡j˱rÉ¥oÏ ±«ÓÂ¥•ƒ€k—ŽwWűŒmcih³K›~‰µh¯e]lµ›él•Eģ‰•E“ďs‡’mǖŧē`ãògK_ÛsUʝ“ćğ¶hŒöŒO¤Ǜn³Žc‘`¡y‹¦C‘ez€YŠwa™–‘[ďĵűMę§]X˜Î_‚훘Û]é’ÛUćİÕBƣ±…dƒy¹T^džûÅÑŦ·‡PĻþÙ`K€¦˜…¢ÍeœĥR¿Œ³£[~Œäu¼dl‰t‚†W¸oRM¢ď\\zœ}Æzdvň–{ÎXF¶°Â_„ÒÂÏL©Ö•TmuŸ¼ãl‰›īkiqéfA„·Êµ\\őDc¥ÝF“y›Ôć˜c€űH_hL܋êĺШc}rn`½„Ì@¸¶ªVLŒŠhŒ‹\\•Ţĺk~ŽĠið°|gŒtTĭĸ^x‘vK˜VGréAé‘bUu›MJ‰VÃO¡…qĂXËS‰ģãlýàŸ_ju‡YÛÒB†œG^˜é֊¶§ŽƒEG”ÅzěƒƯ¤Ek‡N[kdåucé¬dnYpAyČ{`]þ¯T’bÜÈk‚¡Ġ•vŒàh„ÂƄ¢J" + ] + ], + "encodeOffsets": [[[111707, 21520]], [[107619, 25527]]] + } + }, + { + "type": "Feature", + "id": "460000", + "properties": { "id": "460000", "cp": [109.83119, 19.031971], "name": "海南", "childNum": 1 }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@š¦Ŝil¢”XƦ‘ƞò–ïè§ŞCêɕrŧůÇąĻõ™·ĉ³œ̅kÇm@ċȧƒŧĥ‰Ľʉ­ƅſ“ȓÒ˦ŝE}ºƑ[ÍĜȋ gÎfǐÏĤ¨êƺ\\Ɔ¸ĠĎvʄȀœÐ¾jNðĀÒRŒšZdž™zÐŘΰH¨Ƣb²_Ġ " + ], + "encodeOffsets": [[112750, 20508]] + } + }, + { + "type": "Feature", + "id": "510000", + "properties": { + "id": "510000", + "cp": [104.065735, 30.659462], + "name": "四川", + "childNum": 2 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@LqKr"], + [ + "@@Š[ĻéV£ž_ţġñpG •réÏ·~ąSfy×͂·ºſƽiÍıƣıĻmHH}siaX@iÇ°ÁÃ×t«ƒ­Tƒ¤J–JJŒyJ•ÈŠ`Ohߦ¡uËhIyCjmÿw…ZG……Ti‹SˆsO‰žB²ŸfNmsPaˆ{M{ŠõE‘^Hj}gYpaeuž¯‘oáwHjÁ½M¡pM“–uå‡mni{fk”\\oƒÎqCw†EZ¼K›ĝŠƒAy{m÷L‡wO×SimRI¯rK™õBS«sFe‡]fµ¢óY_ÆPRcue°Cbo׌bd£ŌIHgtrnyPt¦foaXďx›lBowz‹_{ÊéWiêE„GhܸºuFĈIxf®Ž•Y½ĀǙ]¤EyŸF²ċ’w¸¿@g¢§RGv»–áŸW`ÃĵJwi]t¥wO­½a[׈]`Ãi­üL€¦LabbTÀå’c}Íh™Æhˆ‹®BH€î|Ék­¤S†y£„ia©taį·Ɖ`ō¥Uh“O…ƒĝLk}©Fos‰´›Jm„µlŁu—…ø–nÑJWΪ–YÀïAetTžŅ‚ӍG™Ë«bo‰{ıwodƟ½ƒžOġܑµxàNÖ¾P²§HKv¾–]|•B‡ÆåoZ`¡Ø`ÀmºĠ~ÌЧnDž¿¤]wğ@sƒ‰rğu‰~‘Io”[é±¹ ¿žſđӉ@q‹gˆ¹zƱřaí°KtǤV»Ã[ĩǭƑ^ÇÓ@ỗs›Zϕ‹œÅĭ€Ƌ•ěpwDóÖሯneQˌq·•GCœýS]xŸ·ý‹q³•O՜Œ¶Qzßti{ř‰áÍÇWŝŭñzÇW‹pç¿JŒ™‚Xœĩè½cŒF–ÂLiVjx}\\N†ŇĖ¥Ge–“JA¼ÄHfÈu~¸Æ«dE³ÉMA|b˜Ò…˜ćhG¬CM‚õŠ„ƤąAvƒüV€éŀ‰_V̳ĐwQj´·ZeÈÁ¨X´Æ¡Qu·»Ÿ“˜ÕZ³ġqDo‰y`L¬gdp°şŠp¦ėìÅĮZŽ°Iä”h‚‘ˆzŠĵœf²å ›ĚрKp‹IN|‹„Ñz]ń……·FU×é»R³™MƒÉ»GM«€ki€™ér™}Ã`¹ăÞmȝnÁîRǀ³ĜoİzŔwǶVÚ£À]ɜ»ĆlƂ²Ġ…þTº·àUȞÏʦ¶†I’«dĽĢdĬ¿–»Ĕ׊h\\c¬†ä²GêëĤł¥ÀǿżÃÆMº}BÕĢyFVvw–ˆxBèĻĒ©Ĉ“tCĢɽŠȣ¦āæ·HĽî“ôNԓ~^¤Ɗœu„œ^s¼{TA¼ø°¢İªDè¾Ň¶ÝJ‘®Z´ğ~Sn|ªWÚ©òzPOȸ‚bð¢|‹øĞŠŒœŒQìÛÐ@Ğ™ǎRS¤Á§d…i“´ezÝúØã]Hq„kIŸþËQǦÃsǤ[E¬ÉŪÍxXƒ·ÖƁİlƞ¹ª¹|XÊwn‘ÆƄmÀêErĒtD®ċæcQƒ”E®³^ĭ¥©l}äQto˜ŖÜqƎkµ–„ªÔĻĴ¡@Ċ°B²Èw^^RsºTĀ£ŚæœQP‘JvÄz„^Đ¹Æ¯fLà´GC²‘dt˜­ĀRt¼¤ĦOðğfÔðDŨŁĞƘïžPȆ®âbMüÀXZ ¸£@Ś›»»QÉ­™]d“sÖ×_͖_ÌêŮPrĔĐÕGĂeZÜîĘqBhtO ¤tE[h|Y‹Ô‚ZśÎs´xº±UŒ’ñˆt|O’ĩĠºNbgþŠJy^dÂY Į„]Řz¦gC‚³€R`ĀŠz’¢AjŒ¸CL„¤RÆ»@­Ŏk\\Ç´£YW}z@Z}‰Ã¶“oû¶]´^N‡Ò}èN‚ª–P˜Íy¹`S°´†ATe€VamdUĐwʄvĮÕ\\ƒu‹Æŗ¨Yp¹àZÂm™Wh{á„}WØǍ•Éüw™ga§áCNęÎ[ĀÕĪgÖɪX˜øx¬½Ů¦¦[€—„NΆL€ÜUÖ´òrÙŠxR^–†J˜k„ijnDX{Uƒ~ET{ļº¦PZc”jF²Ė@Žp˜g€ˆ¨“B{ƒu¨ŦyhoÚD®¯¢˜ WòàFΤ¨GDäz¦kŮPœġq˚¥À]€Ÿ˜eŽâÚ´ªKxī„Pˆ—Ö|æ[xäJÞĥ‚s’NÖ½ž€I†¬nĨY´®Ð—ƐŠ€mD™ŝuäđđEb…e’e_™v¡}ìęNJē}q”É埁T¯µRs¡M@}ůa†a­¯wvƉåZwž\\Z{åû^›" + ] + ], + "encodeOffsets": [[[108815, 30935]], [[110617, 31811]]] + } + }, + { + "type": "Feature", + "id": "520000", + "properties": { + "id": "520000", + "cp": [106.713478, 26.578343], + "name": "贵州", + "childNum": 3 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@†G\\†lY£‘in"], + ["@@q‚|ˆ‚mc¯tχVSÎ"], + [ + "@@hÑ£Is‡NgßH†›HªķÃh_¹ƒ¡ĝħń¦uيùŽgS¯JHŸ|sÝÅtÁïyMDč»eÕtA¤{b\\}—ƒG®u\\åPFq‹wÅaD…žK°ºâ_£ùbµ”mÁ‹ÛœĹM[q|hlaªāI}тƒµ@swtwm^oµˆD鼊yV™ky°ÉžûÛR…³‚‡eˆ‡¥]RՋěħ[ƅåÛDpŒ”J„iV™™‰ÂF²I…»mN·£›LbÒYb—WsÀbŽ™pki™TZĄă¶HŒq`……ĥ_JŸ¯ae«ƒKpÝx]aĕÛPƒÇȟ[ÁåŵÏő—÷Pw}‡TœÙ@Õs«ĿÛq©½œm¤ÙH·yǥĘĉBµĨÕnđ]K„©„œá‹ŸG纍§Õßg‡ǗĦTèƤƺ{¶ÉHÎd¾ŚÊ·OÐjXWrãLyzÉAL¾ę¢bĶėy_qMĔąro¼hĊžw¶øV¤w”²Ĉ]ʚKx|`ź¦ÂÈdr„cȁbe¸›`I¼čTF´¼Óýȃr¹ÍJ©k_șl³´_pН`oÒhŽ¶pa‚^ÓĔ}D»^Xyœ`d˜[Kv…JPhèhCrĂĚÂ^Êƌ wˆZL­Ġ£šÁbrzOIl’MM”ĪŐžËr×ÎeŦŽtw|Œ¢mKjSǘňĂStÎŦEtqFT†¾†E쬬ôxÌO¢Ÿ KŠ³ŀºäY†„”PVgŎ¦Ŋm޼VZwVlŒ„z¤…ž£Tl®ctĽÚó{G­A‡ŒÇgeš~Αd¿æaSba¥KKûj®_ć^\\ؾbP®¦x^sxjĶI_Ä X‚⼕Hu¨Qh¡À@Ëô}Ž±žGNìĎlT¸ˆ…`V~R°tbÕĊ`¸úÛtπFDu€[ƒMfqGH·¥yA‰ztMFe|R‚_Gk†ChZeÚ°to˜v`x‹b„ŒDnÐ{E}šZ˜è€x—†NEފREn˜[Pv@{~rĆAB§‚EO¿|UZ~ì„Uf¨J²ĂÝƀ‚sª–B`„s¶œfvö¦ŠÕ~dÔq¨¸º»uù[[§´sb¤¢zþFœ¢Æ…Àhˆ™ÂˆW\\ıŽËI݊o±ĭŠ£þˆÊs}¡R]ŒěƒD‚g´VG¢‚j±®è†ºÃmpU[Á›‘Œëº°r›ÜbNu¸}Žº¼‡`ni”ºÔXĄ¤¼Ôdaµ€Á_À…†ftQQgœR—‘·Ǔ’v”}Ýלĵ]µœ“Wc¤F²›OĩųãW½¯K‚©…]€{†LóµCIµ±Mß¿hŸ•©āq¬o‚½ž~@i~TUxŪÒ¢@ƒ£ÀEîôruń‚”“‚b[§nWuMÆLl¿]x}ij­€½" + ] + ], + "encodeOffsets": [[[112158, 27383]], [[112105, 27474]], [[112095, 27476]]] + } + }, + { + "type": "Feature", + "id": "530000", + "properties": { + "id": "530000", + "cp": [101.512251, 24.740609], + "name": "云南", + "childNum": 1 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@[„ùx½}ÑRH‘YīĺûsÍn‘iEoã½Ya²ė{c¬ĝg•ĂsA•ØÅwď‚õzFjw}—«Dx¿}UũlŸê™@•HÅ­F‰¨ÇoJ´Ónũuą¡Ã¢pÒŌ“Ø TF²‚xa²ËX€‚cʋlHîAßËŁkŻƑŷÉ©h™W­æßU‡“Ës¡¦}•teèƶStǀÇ}Fd£j‹ĈZĆÆ‹¤T‚č\\Dƒ}O÷š£Uˆ§~ŃG™‚åŃDĝ¸œTsd¶¶Bªš¤u¢ŌĎo~t¾ÍŶÒtD¦Ú„iôö‰€z›ØX²ghįh½Û±¯€ÿm·zR¦Ɵ`ªŊÃh¢rOԍ´£Ym¼èêf¯ŪĽn„†cÚbŒw\\zlvWžªâˆ ¦g–mĿBş£¢ƹřbĥkǫßeeZkÙIKueT»sVesb‘aĕ  ¶®dNœĄÄpªyŽ¼—„³BE˜®l‡ŽGœŭCœǶwêżĔÂe„pÍÀQƞpC„–¼ŲÈ­AÎô¶R„ä’Q^Øu¬°š_Èôc´¹ò¨P΢hlϦ´Ħ“Æ´sâDŽŲPnÊD^¯°’Upv†}®BP̪–jǬx–Söwlfòªv€qĸ|`H€­viļ€ndĜ­Ćhň•‚em·FyށqóžSį¯‘³X_ĞçêtryvL¤§z„¦c¦¥jnŞk˜ˆlD¤øz½ĜàžĂŧMÅ|áƆàÊcðÂF܎‚áŢ¥\\\\º™İøÒÐJĴ‡„îD¦zK²ǏÎEh~’CD­hMn^ÌöÄ©ČZÀžaü„fɭyœpį´ěFűk]Ôě¢qlÅĆÙa¶~Äqššê€ljN¬¼H„ÊšNQ´ê¼VظE††^ŃÒyŒƒM{ŒJLoÒœęæŸe±Ķ›y‰’‡gã“¯JYÆĭĘëo¥Š‰o¯hcK«z_pŠrC´ĢÖY”—¼ v¸¢RŽÅW³Â§fǸYi³xR´ďUˊ`êĿU„û€uĆBƒƣö‰N€DH«Ĉg†——Ñ‚aB{ÊNF´¬c·Åv}eÇÃGB»”If•¦HňĕM…~[iwjUÁKE•Ž‹¾dĪçW›šI‹èÀŒoÈXòyŞŮÈXâÎŚŠj|àsRy‹µÖ›–Pr´þŒ ¸^wþTDŔ–Hr¸‹žRÌmf‡żÕâCôox–ĜƌÆĮŒ›Ð–œY˜tâŦÔ@]ÈǮƒ\\Ī¼Ä£UsȯLbîƲŚºyh‡rŒŠ@ĒԝƀŸÀ²º\\êp“’JŠ}ĠvŠqt„Ġ@^xÀ£È†¨mËÏğ}n¹_¿¢×Y_æpˆÅ–A^{½•Lu¨GO±Õ½ßM¶w’ÁĢۂP‚›Ƣ¼pcIJxŠ|ap̬HšÐŒŊSfsðBZ¿©“XÏÒK•k†÷Eû¿‰S…rEFsÕūk”óVǥʼniTL‚¡n{‹uxţÏh™ôŝ¬ğōN“‘NJkyPaq™Âğ¤K®‡YŸxÉƋÁ]āęDqçgOg†ILu—\\_gz—]W¼ž~CÔē]bµogpў_oď`´³Țkl`IªºÎȄqÔþž»E³ĎSJ»œ_f·‚adÇqƒÇc¥Á_Źw{™L^ɱćx“U£µ÷xgĉp»ĆqNē`rĘzaĵĚ¡K½ÊBzyäKXqiWPÏɸ½řÍcÊG|µƕƣG˛÷Ÿk°_^ý|_zċBZocmø¯hhcæ\\lˆMFlư£Ĝ„ÆyH“„F¨‰µêÕ]—›HA…àӄ^it `þßäkŠĤÎT~Wlÿ¨„ÔPzUC–NVv [jâôDôď[}ž‰z¿–msSh‹¯{jïğl}šĹ[–őŒ‰gK‹©U·µË@¾ƒm_~q¡f¹…ÅË^»‘f³ø}Q•„¡Ö˳gͱ^ǁ…\\ëÃA_—¿bW›Ï[¶ƛ鏝£F{īZgm@|kHǭƁć¦UĔťƒ×ë}ǝƒeďºȡȘÏíBə£āĘPªij¶“ʼnÿ‡y©n‰ď£G¹¡I›Š±LÉĺÑdĉ܇W¥˜‰}g˜Á†{aqÃ¥aŠıęÏZ—ï`" + ], + "encodeOffsets": [[104636, 22969]] + } + }, + { + "type": "Feature", + "id": "540000", + "properties": { "id": "540000", "cp": [89.132212, 30.860361], "name": "西藏", "childNum": 1 }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@hžľxŽŖ‰xƒÒVŽ†ºÅâAĪÝȆµę¯Ňa±r_w~uSÕň‘qOj]ɄQ…£Z……UDûoY’»©M[‹L¼qãË{V͕çWViŽ]ë©Ä÷àyƛh›ÚU°ŒŒa”d„cQƒ~Mx¥™cc¡ÙaSyF—ցk­ŒuRýq¿Ôµ•QĽ³aG{¿FµëªéĜÿª@¬·–K‰·àariĕĀ«V»Ŷ™Ĵū˜gèLǴŇƶaf‹tŒèBŚ£^Šâ†ǐÝ®–šM¦ÁǞÿ¬LhŸŽJ¾óƾƺcxw‹f]Y…´ƒ¦|œQLn°aœdĊ…œ\\¨o’œǀÍŎœ´ĩĀd`tÊQŞŕ|‚¨C^©œĈ¦„¦ÎJĊ{ŽëĎjª²rЉšl`¼Ą[t|¦St辉PŒÜK¸€d˜Ƅı]s¤—î_v¹ÎVòŦj˜£Əsc—¬_Ğ´|Ł˜¦AvŽ¦w`ăaÝaa­¢e¤ı²©ªSªšÈMĄwžÉØŔì@T‘¤—Ę™\\õª@”þo´­xA s”ÂtŎKzó´ÇĊµ¢rž^nĊ­Æ¬×üGž¢‚³ {âĊ]š™G‚~bÀgVjzlhǶf€žOšfdŠ‰ªB]pj„•TO–tĊ‚n¤}®¦ƒČ¥d¢¼»ddš”Y¼Žt—¢eȤJ¤}Ǿ¡°§¤AГlc@ĝ”sªćļđAç‡wx•UuzEÖġ~AN¹ÄÅȀŻ¦¿ģŁéì±H…ãd«g[؉¼ēÀ•cīľġ¬cJ‘µ…ÐʥVȝ¸ßS¹†ý±ğkƁ¼ą^ɛ¤Ûÿ‰b[}¬ōõÃ]ËNm®g@•Bg}ÍF±ǐyL¥íCˆƒIij€Ï÷њį[¹¦[⚍EÛïÁÉdƅß{âNÆāŨߝ¾ě÷yC£‡k­´ÓH@¹†TZ¥¢įƒ·ÌAЧ®—Zc…v½ŸZ­¹|ŕWZqgW“|ieZÅYVӁqdq•bc²R@†c‡¥Rã»Ge†ŸeƃīQ•}J[ғK…¬Ə|o’ėjġĠÑN¡ð¯EBčnwôɍėªƒ²•CλŹġǝʅįĭạ̃ūȹ]ΓͧgšsgȽóϧµǛ†ęgſ¶ҍć`ĘąŌJޚä¤rÅň¥ÖÁUětęuůÞiĊÄÀ\\Æs¦ÓRb|Â^řÌkÄŷ¶½÷‡f±iMݑ›‰@ĥ°G¬ÃM¥n£Øą‚ğ¯ß”§aëbéüÑOčœk£{\\‘eµª×M‘šÉfm«Ƒ{Å׃Gŏǩãy³©WÑăû‚··‘Q—òı}¯ã‰I•éÕÂZ¨īès¶ZÈsŽæĔTŘvŽgÌsN@îá¾ó@‰˜ÙwU±ÉT廣TđŸWxq¹Zo‘b‹s[׌¯cĩv‡Œėŧ³BM|¹k‰ªħ—¥TzNYnݍßpęrñĠĉRS~½ŠěVVŠµ‚õ‡«ŒM££µB•ĉ¥áºae~³AuĐh`Ü³ç@BۘïĿa©|z²Ý¼D”£àč²‹ŸƒIƒû›I ā€óK¥}rÝ_Á´éMaň¨€~ªSĈ½Ž½KÙóĿeƃÆBŽ·¬ën×W|Uº}LJrƳ˜lŒµ`bÔ`QˆˆÐÓ@s¬ñIŒÍ@ûws¡åQÑßÁ`ŋĴ{Ī“T•ÚÅTSij‚‹Yo|Ç[ǾµMW¢ĭiÕØ¿@˜šMh…pÕ]j†éò¿OƇĆƇp€êĉâlØw–ěsˆǩ‚ĵ¸c…bU¹ř¨WavquSMzeo_^gsÏ·¥Ó@~¯¿RiīB™Š\\”qTGªÇĜçPoŠÿfñòą¦óQīÈáP•œābß{ƒZŗĸIæńhnszÁCËìñšÏ·ąĚÝUm®ó­L·ăU›Èíoù´Êj°ŁŤ_uµ^‘°Œìǖ@tĶĒ¡Æ‡M³Ģ«˜İĨÅ®ğ†RŽāð“ggheÆ¢z‚Ê©Ô\\°ÝĎz~ź¤Pn–MĪÖB£Ÿk™n鄧żćŠ˜ĆK„Ē°¼L¶è‰âz¨u¦¥LDĘz¬ýÎmĘd¾ß”Fz“hg²™Fy¦ĝ¤ċņbΛ@y‚Ąæm°NĮZRÖíŽJ²öLĸÒ¨Y®ƌÐV‰à˜tt_ڀÂyĠzž]Ţh€zĎ{†ĢX”ˆc|šÐqŽšfO¢¤ög‚ÌHNŽ„PKŖœŽ˜Uú´xx[xˆvĐCûĀŠìÖT¬¸^}Ìsòd´_Ž‡KgžLĴ…ÀBon|H@–Êx˜—¦BpŰˆŌ¿fµƌA¾zLjRxŠ¶F”œkĄźRzŀˆ~¶[”´Hnª–VƞuĒ­È¨ƎcƽÌm¸ÁÈM¦x͊ëÀxdžB’šú^´W†£–d„kɾĬpœw‚˂ØɦļĬIŚœÊ•n›Ŕa¸™~J°î”lɌxĤÊÈðhÌ®‚g˜T´øŽàCˆŽÀ^ªerrƘdž¢İP|Ė ŸWœªĦ^¶´ÂL„aT±üWƜ˜ǀRšŶUńšĖ[QhlLüA†‹Ü\\†qR›Ą©" + ], + "encodeOffsets": [[90849, 37210]] + } + }, + { + "type": "Feature", + "id": "610000", + "properties": { + "id": "610000", + "cp": [108.948024, 34.263161], + "name": "陕西", + "childNum": 1 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@˜p¢—ȮµšûG™Ħ}Ħšðǚ¶òƄ€jɂz°{ºØkÈęâ¦jª‚Bg‚\\œċ°s¬Ž’]jžú ‚E”Ȍdž¬s„t‡”RˆÆdĠݎwܔ¸ôW¾ƮłÒ_{’Ìšû¼„jº¹¢GǪÒ¯ĘƒZ`ºŊƒecņąš~BÂgzpâēòYǠȰÌTΨÂWœ|fcŸă§uF—Œ@NŸ¢XLƒŠRMº[ğȣſï|¥J™kc`sʼnǷ’Y¹‹W@µ÷K…ãï³ÛIcñ·VȋڍÒķø©—þ¥ƒy‚ÓŸğęmWµÎumZyOŅƟĥÓ~sÑL¤µaŅY¦ocyZ{‰y c]{ŒTa©ƒ`U_Ěē£ωÊƍKù’K¶ȱÝƷ§{û»ÅÁȹÍéuij|¹cÑd‘ŠìUYƒŽO‘uF–ÕÈYvÁCqӃT•Ǣí§·S¹NgŠV¬ë÷Át‡°Dد’C´ʼnƒópģ}„ċcE˅FŸŸéGU¥×K…§­¶³B‹Č}C¿åċ`wġB·¤őcƭ²ő[Å^axwQO…ÿEËߌ•ĤNĔŸwƇˆÄŠńwĪ­Šo[„_KÓª³“ÙnK‰Çƒěœÿ]ď€ă_d©·©Ýŏ°Ù®g]±„Ÿ‡ß˜å›—¬÷m\\›iaǑkěX{¢|ZKlçhLt€Ňîŵ€œè[€É@ƉĄEœ‡tƇÏ˜³­ħZ«mJ…›×¾‘MtÝĦ£IwÄå\\Õ{‡˜ƒOwĬ©LÙ³ÙgBƕŀr̛ĢŭO¥lãyC§HÍ£ßEñŸX¡—­°ÙCgpťz‘ˆb`wI„vA|§”‡—hoĕ@E±“iYd¥OĻ¹S|}F@¾oAO²{tfžÜ—¢Fǂ҈W²°BĤh^Wx{@„¬‚­F¸¡„ķn£P|ŸªĴ@^ĠĈæb–Ôc¶l˜Yi…–^Mi˜cĎ°Â[ä€vï¶gv@À“Ĭ·lJ¸sn|¼u~a]’ÆÈtŌºJp’ƒþ£KKf~Š¦UbyäIšĺãn‡Ô¿^­žŵMT–hĠܤko¼Ŏìąǜh`[tŒRd²IJ_œXPrɲ‰l‘‚XžiL§àƒ–¹ŽH˜°Ȧqº®QC—bA†„ŌJ¸ĕÚ³ĺ§ `d¨YjžiZvRĺ±öVKkjGȊĐePОZmļKÀ€‚[ŠŽ`ösìh†ïÎoĬdtKÞ{¬èÒÒBŒÔpIJÇĬJŊ¦±J«ˆY§‹@·pH€µàåVKe›pW†ftsAÅqC·¬ko«pHÆuK@oŸHĆۄķhx“e‘n›S³àǍrqƶRbzy€¸ËАl›¼EºpĤ¼Œx¼½~Ğ’”à@†ÚüdK^ˆmÌSj" + ], + "encodeOffsets": [[110234, 38774]] + } + }, + { + "type": "Feature", + "id": "620000", + "properties": { + "id": "620000", + "cp": [103.823557, 36.058039], + "name": "甘肃", + "childNum": 2 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@VuUv"], + [ + "@@ũ‹EĠtt~nkh`Q‰¦ÅÄÜdw˜Ab×ĠąJˆ¤DüègĺqBqœj°lI¡ĨÒ¤úSHbš‡ŠjΑBŠ°aZˆ¢KJŽ’O[|A£žDx}Nì•HUnrk„ kp€¼Y kMJn[aG‚áÚÏ[½rc†}aQxOgsPMnUs‡nc‹Z…ž–sKúvA›t„Þġ’£®ĀYKdnFwš¢JE°”Latf`¼h¬we|€Æ‡šbj}GA€·~WŽ”—`†¢MC¤tL©IJ°qdf”O‚“bÞĬ¹ttu`^ZúE`Œ[@„Æsîz®¡’C„ƳƜG²“R‘¢R’m”fŽwĸg܃‚ą G@pzJM½mŠhVy¸uÈÔO±¨{LfæU¶ßGĂq\\ª¬‡²I‚¥IʼnÈīoı‹ÓÑAçÑ|«LÝcspīðÍg…të_õ‰\\ĉñLYnĝg’ŸRǡÁiHLlõUĹ²uQjYi§Z_c¨Ÿ´ĹĖÙ·ŋI…ƒaBD˜­R¹ȥr—¯G•ºß„K¨jWk’ɱŠOq›Wij\\a­‹Q\\sg_ĆǛōëp»£lğۀgS•ŶN®À]ˆÓäm™ĹãJaz¥V}‰Le¤L„ýo‘¹IsŋÅÇ^‘Žbz…³tmEÁ´aŠ¹cčecÇN•ĊãÁ\\č¯—dNj•]j†—ZµkÓda•ćå]ğij@ ©O{¤ĸm¢ƒE·®ƒ«|@Xwg]Aģ±¯‡XǁÑdzªc›wQÚŝñsÕ³ÛV_ýƒ˜¥\\ů¥©¾÷w—Ž©WÕÊĩhÿÖÁRo¸V¬âDb¨šhûx–Ê×nj~Zâƒg|šXÁnßYoº§ZÅŘvŒ[„ĭÖʃuďxcVbnUSf…B¯³_Tzº—ΕO©çMÑ~Mˆ³]µ^püµ”ŠÄY~y@X~¤Z³€[Èōl@®Å¼£QKƒ·Di‹¡By‘ÿ‰Q_´D¥hŗyƒ^ŸĭÁZ]cIzý‰ah¹MĪğP‘s{ò‡‹‘²Vw¹t³Ŝˁ[ŽÑ}X\\gsFŸ£sPAgěp×ëfYHāďÖqēŭOÏë“dLü•\\iŒ”t^c®šRʺ¶—¢H°mˆ‘rYŸ£BŸ¹čIoľu¶uI]vģSQ{ƒUŻ”Å}QÂ|̋°ƅ¤ĩŪU ęĄžÌZҞ\\v˜²PĔ»ƢNHƒĂyAmƂwVmž`”]ȏb•”H`‰Ì¢²ILvĜ—H®¤Dlt_„¢JJÄämèÔDëþgºƫ™”aʎÌrêYi~ ÎݤNpÀA¾Ĕ¼b…ð÷’Žˆ‡®‚”üs”zMzÖĖQdȨý†v§Tè|ªH’þa¸|šÐ ƒwKĢx¦ivr^ÿ ¸l öæfƟĴ·PJv}n\\h¹¶v†·À|\\ƁĚN´Ĝ€çèÁz]ġ¤²¨QÒŨTIl‡ªťØ}¼˗ƦvÄùØE‹’«Fï˛Iq”ōŒTvāÜŏ‚íÛߜÛV—j³âwGăÂíNOŠˆŠPìyV³ʼnĖýZso§HіiYw[߆\\X¦¥c]ÔƩÜ·«j‡ÐqvÁ¦m^ċ±R™¦΋ƈťĚgÀ»IïĨʗƮŽ°Ɲ˜ĻþÍAƉſ±tÍEÕÞāNU͗¡\\ſčåÒʻĘm ƭÌŹöʥ’ëQ¤µ­ÇcƕªoIýˆ‰Iɐ_mkl³ă‰Ɠ¦j—¡Yz•Ňi–}Msßõ–īʋ —}ƒÁVmŸ_[n}eı­Uĥ¼‘ª•I{ΧDӜƻėoj‘qYhĹT©oūĶ£]ďxĩ‹ǑMĝ‰q`B´ƃ˺Ч—ç~™²ņj@”¥@đ´ί}ĥtPńǾV¬ufӃÉC‹tÓ̻‰…¹£G³€]ƖƾŎĪŪĘ̖¨ʈĢƂlɘ۪üºňUðǜȢƢż̌ȦǼ‚ĤŊɲĖ­Kq´ï¦—ºĒDzņɾªǀÞĈĂD†½ĄĎÌŗĞrôñnŽœN¼â¾ʄľԆ|DŽŽ֦ज़ȗlj̘̭ɺƅêgV̍ʆĠ·ÌĊv|ýĖÕWĊǎÞ´õ¼cÒÒBĢ͢UĜð͒s¨ňƃLĉÕÝ@ɛƯ÷¿Ľ­ĹeȏijëCȚDŲyê×Ŗyò¯ļcÂßY…tÁƤyAã˾J@ǝrý‹‰@¤…rz¸oP¹ɐÚyᐇHŸĀ[Jw…cVeȴϜ»ÈŽĖ}ƒŰŐèȭǢόĀƪÈŶë;Ñ̆ȤМľĮEŔ—ĹŊũ~ËUă{ŸĻƹɁύȩþĽvĽƓÉ@ē„ĽɲßǐƫʾǗĒpäWÐxnsÀ^ƆwW©¦cÅ¡Ji§vúF¶Ž¨c~c¼īŒeXǚ‹\\đ¾JŽwÀďksãA‹fÕ¦L}wa‚o”Z’‹D½†Ml«]eÒÅaɲáo½FõÛ]ĻÒ¡wYR£¢rvÓ®y®LF‹LzĈ„ôe]gx}•|KK}xklL]c¦£fRtív¦†PĤoH{tK" + ] + ], + "encodeOffsets": [[[108619, 36299]], [[108589, 36341]]] + } + }, + { + "type": "Feature", + "id": "630000", + "properties": { "id": "630000", "cp": [96.778916, 35.623178], "name": "青海", "childNum": 2 }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@InJm"], + [ + "@@CƒÆ½OŃĦsΰ~Ē³¦@@“Ņiš±è}ؘƄ˹A³r_ĞŠǒNĪŒĐw¤^ŬĵªpĺSZg’rpiƼĘԛ¨C|͖J’©Ħ»®VIJ~f\\m `Un„˜~ʌŸ•ĬàöNt•~ňjy–¢Zi˜Ɣ¥ĄŠk´nl`JʇŠJþ©pdƖ®È£¶ìRʦ‘źõƮËnŸʼėæÑƀĎ[‚˜¢VÎĂMÖÝÎF²sƊƀÎBļýƞ—¯ʘƭðħ¼Jh¿ŦęΌƇš¥²Q]Č¥nuÂÏriˆ¸¬ƪÛ^Ó¦d€¥[Wà…x\\ZŽjҕ¨GtpþYŊĕ´€zUO뇉P‰îMĄÁxH´á˜iÜUà›îÜՁĂÛSuŎ‹r“œJð̬EŒ‘FÁú×uÃÎkr“Ē{V}İ«O_ÌËĬ©ŽÓŧSRѱ§Ģ£^ÂyèçěM³Ƃę{[¸¿u…ºµ[gt£¸OƤĿéYŸõ·kĀŸq]juw¥Dĩƍ€õÇPéĽG‘ž©ã‡¤G…uȧþRcÕĕNy“yût“ˆ­‡ø‘†ï»a½ē¿BMoį£ŸÍj}éZËqbʍš“Ƭh¹ìÿÓAçãnIáI`ƒks£CG­ě˜Uy×Cy•…’Ÿ@¶ʡÊBnāzG„ơMē¼±O÷õJËĚăVŸĪũƆ£Œ¯{ËL½Ìzż“„VR|ĠTbuvJvµhĻĖH”Aëáa…­OÇðñęNw‡…œľ·L›mI±íĠĩPÉ×®ÿs—’cB³±JKßĊ«`…ađ»·QAmO’‘Vţéÿ¤¹SQt]]Çx€±¯A@ĉij¢Óļ©•ƒl¶ÅÛr—ŕspãRk~¦ª]Į­´“FR„åd­ČsCqđéFn¿Åƃm’Éx{W©ºƝºįkÕƂƑ¸wWūЩÈFž£\\tÈ¥ÄRÈýÌJ ƒlGr^×äùyÞ³fj”c†€¨£ÂZ|ǓMĝšÏ@ëÜőR‹›ĝ‰Œ÷¡{aïȷPu°ËXÙ{©TmĠ}Y³’­ÞIňµç½©C¡į÷¯B»|St»›]vƒųƒs»”}MÓ ÿʪƟǭA¡fs˜»PY¼c¡»¦c„ċ­¥£~msĉP•–Siƒ^o©A‰Šec‚™PeǵŽkg‚yUi¿h}aH™šĉ^|ᴟ¡HØûÅ«ĉ®]m€¡qĉ¶³ÈyôōLÁst“BŸ®wn±ă¥HSòėš£˜S’ë@לÊăxÇN©™©T±ª£IJ¡fb®ÞbŽb_Ą¥xu¥B—ž{łĝ³«`d˜Ɛt—¤ťiñžÍUuºí`£˜^tƃIJc—·ÛLO‹½Šsç¥Ts{ă\\_»™kϊ±q©čiìĉ|ÍIƒ¥ć¥›€]ª§D{ŝŖÉR_sÿc³Īō›ƿΑ›§p›[ĉ†›c¯bKm›R¥{³„Z†e^ŽŒwx¹dƽŽôIg §Mĕ ƹĴ¿—ǣÜ̓]‹Ý–]snåA{‹eŒƭ`ǻŊĿ\\ijŬű”YÂÿ¬jĖqŽßbŠ¸•L«¸©@ěĀ©ê¶ìÀEH|´bRľž–Ó¶rÀQþ‹vl®Õ‚E˜TzÜdb ˜hw¤{LR„ƒd“c‹b¯‹ÙVgœ‚ƜßzÃô쮍^jUèXΖ|UäÌ»rKŽ\\ŒªN‘¼pZCü†VY††¤ɃRi^rPҒTÖ}|br°qňbĚ°ªiƶGQ¾²„x¦PœmlŜ‘[Ĥ¡ΞsĦŸÔÏâ\\ªÚŒU\\f…¢N²§x|¤§„xĔsZPòʛ²SÐqF`ª„VƒÞŜĶƨVZŒÌL`ˆ¢dŐIqr\\oäõ–F礻Ŷ×h¹]Clـ\\¦ďÌį¬řtTӺƙgQÇÓHţĒ”´ÃbEÄlbʔC”|CˆŮˆk„Ʈ[ʼ¬ňœ´KŮÈΰÌĪ¶ƶlð”ļA†TUvdTŠG†º̼ŠÔ€ŒsÊDԄveOg" + ] + ], + "encodeOffsets": [[[105308, 37219]], [[95370, 40081]]] + } + }, + { + "type": "Feature", + "id": "640000", + "properties": { "id": "640000", "cp": [106.278179, 37.26637], "name": "宁夏", "childNum": 2 }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + "@@KëÀęĞ«OęȿȕŸı]ʼn¡åįÕÔ«Ǵõƪ™ĚQÐZhv K°›öqÀѐS[ÃÖHƖčË‡nL]ûc…Ùß@‚“ĝ‘¾}w»»‹oģF¹œ»kÌÏ·{zPƒ§B­¢íyÅt@ƒ@áš]Yv_ssģ¼i߁”ĻL¾ġsKD£¡N_…“˜X¸}B~Haiˆ™Åf{«x»ge_bs“KF¯¡Ix™mELcÿZ¤­Ģ‘ƒÝœsuBLù•t†ŒYdˆmVtNmtOPhRw~bd…¾qÐ\\âÙH\\bImlNZŸ»loƒŸqlVm–Gā§~QCw¤™{A\\‘PKŸNY‡¯bF‡kC¥’sk‹Šs_Ã\\ă«¢ħkJi¯r›rAhĹûç£CU‡ĕĊ_ԗBixÅُĄnªÑaM~ħpOu¥sîeQ¥¤^dkKwlL~{L~–hw^‚ófćƒKyEŒ­K­zuÔ¡qQ¤xZÑ¢^ļöܾEpž±âbÊÑÆ^fk¬…NC¾‘Œ“YpxbK~¥Že֎ŒäBlt¿Đx½I[ĒǙŒWž‹f»Ĭ}d§dµùEuj¨‚IÆ¢¥dXªƅx¿]mtÏwßRĶŒX¢͎vÆzƂZò®ǢÌʆCrâºMÞzžÆMҔÊÓŊZľ–r°Î®Ȉmª²ĈUªĚøºˆĮ¦ÌĘk„^FłĬhĚiĀĖ¾iİbjÕ" + ], + ["@@mfwěwMrŢªv@G‰"] + ], + "encodeOffsets": [[[109366, 40242]], [[108600, 36303]]] + } + }, + { + "type": "Feature", + "id": "650000", + "properties": { "id": "650000", "cp": [85.617733, 40.792818], "name": "新疆", "childNum": 1 }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@QØĔ²X¨”~ǘBºjʐßØvK”ƔX¨vĊOžÃƒ·¢i@~c—‡ĝe_«”Eš“}QxgɪëÏÃ@sÅyXoŖ{ô«ŸuX…ê•Îf`œC‚¹ÂÿÐGĮÕĞXŪōŸMźÈƺQèĽôe|¿ƸJR¤ĘEjcUóº¯Ĩ_ŘÁMª÷Ð¥Oéȇ¿ÖğǤǷÂF҇zÉx[]­Ĥĝ‰œ¦EP}ûƥé¿İƷTėƫœŕƅ™ƱB»Đ±’ēO…¦E–•}‘`cȺrĦáŖuҞª«IJ‡πdƺÏØZƴwʄ¤ĖGЙǂZĶƒèH¶}ÚZצʥĪï|ÇĦMŔ»İĝLj‹ì¥Βœba­¯¥ǕǚkĆŵĦɑĺƯxūД̵nơʃĽá½M»›òmqóŘĝč˾ăC…ćāƿÝɽ©DZŅ¹đ¥˜³ðLrÁ®ɱĕģʼnǻ̋ȥơŻǛȡVï¹Ň۩ûkɗġƁ§ʇė̕ĩũƽō^ƕŠUv£ƁQï“Ƶkŏ½ΉÃŭdzLқʻ«ƭ\\lƒ‡ŭD‡“{ʓDkaFÃÄa“³ŤđÔGRÈƚhSӹŚsİ«ĐË[¥ÚDkº^Øg¼ŵ¸£EÍö•€ůʼnT¡c_‡ËKY‹ƧUśĵ„݃U_©rETÏʜ±OñtYwē¨ƒ{£¨uM³x½şL©Ùá[ÓÐĥ Νtģ¢\\‚ś’nkO›w¥±ƒT»ƷFɯàĩÞáB¹Æ…ÑUw„੍žĽw[“mG½Èå~‡Æ÷QyŠěCFmĭZī—ŵVÁ™ƿQƛ—ûXS²‰b½KϽĉS›©ŷXĕŸ{ŽĕK·¥Ɨcqq©f¿]‡ßDõU³h—­gËÇïģÉɋw“k¯í}I·šœbmœÉ–ř›īJɥĻˁ×xo›ɹī‡l•c…¤³Xù]‘™DžA¿w͉ì¥wÇN·ÂËnƾƍdǧđ®Ɲv•Um©³G\\“}µĿ‡QyŹl㓛µEw‰LJQ½yƋBe¶ŋÀů‡ož¥A—˜Éw@•{Gpm¿Aij†ŽKLhˆ³`ñcËtW‚±»ÕS‰ëüÿďD‡u\\wwwù³—V›LŕƒOMËGh£õP¡™er™Ïd{“‡ġWÁ…č|yšg^ğyÁzÙs`—s|ÉåªÇ}m¢Ń¨`x¥’ù^•}ƒÌ¥H«‰Yªƅ”Aйn~ź¯šf¤áÀz„gŠÇDIԝ´AňĀ҄¶ûEYospõD[{ù°]u›Jq•U•|Soċxţ[õÔĥkŋÞŭZ˺óYËüċrw €ÞkrťË¿XGÉbřaDü·Ē÷Aê[Ää€I®BÕИÞ_¢āĠpŠÛÄȉĖġDKwbm‡ÄNô‡ŠfœƫVÉvi†dz—H‘‹QµâFšù­Âœ³¦{YGžƒd¢ĚÜO „€{Ö¦ÞÍÀPŒ^b–ƾŠlŽ[„vt×ĈÍE˨¡Đ~´î¸ùÎh€uè`¸ŸHÕŔVºwĠââWò‡@{œÙNÝ´ə²ȕn{¿¥{l—÷eé^e’ďˆXj©î\\ªÑò˜Üìc\\üqˆÕ[Č¡xoÂċªbØ­Œø|€¶ȴZdÆšońéŒGš\\”¼C°ÌƁn´nxšÊOĨ’Ūƴĸ¢¸òTxÊǪMīИÖŲÃɎOvˆʦƢ~FŽ‡Rěò—¿ġ~åŊœú‰Nšžš¸qŽ’Ę[Ĕ¶ÂćnÒPĒÜvúĀÊbÖ{Äî¸~Ŕünp¤ÂH¾œĄYÒ©ÊfºmԈĘcDoĬMŬ’˜S¤„s²‚”ʘچžȂVŦ –ŽèW°ªB|IJXŔþÈJĦÆæFĚêŠYĂªĂ]øªŖNÞüA€’fɨJ€˜¯ÎrDDšĤ€`€mz\\„§~D¬{vJÂ˜«lµĂb–¤p€ŌŰNĄ¨ĊXW|ų ¿¾ɄĦƐMT”‡òP˜÷fØĶK¢ȝ˔Sô¹òEð­”`Ɩ½ǒÂň×äı–§ĤƝ§C~¡‚hlå‚ǺŦŞkâ’~}ŽFøàIJaĞ‚fƠ¥Ž„Ŕdž˜®U¸ˆźXœv¢aƆúŪtŠųƠjd•ƺŠƺÅìnrh\\ĺ¯äɝĦ]èpĄ¦´LƞĬŠ´ƤǬ˼Ēɸ¤rºǼ²¨zÌPðŀbþ¹ļD¢¹œ\\ĜÑŚŸ¶ZƄ³àjĨoâŠȴLʉȮŒĐ­ĚăŽÀêZǚŐ¤qȂ\\L¢ŌİfÆs|zºeªÙæ§΢{Ā´ƐÚ¬¨Ĵà²łhʺKÞºÖTŠiƢ¾ªì°`öøu®Ê¾ãØ" + ], + "encodeOffsets": [[88824, 50096]] + } + }, + { + "type": "Feature", + "id": "110000", + "properties": { + "id": "110000", + "cp": [116.405285, 39.904989], + "name": "北京", + "childNum": 1 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@ĽOÁ›ûtŷmiÍt_H»Ĩ±d`Š¹­{bw…Yr“³S]§§o¹€qGtm_Sŧ€“oa›‹FLg‘QN_•dV€@Zom_ć\\ߚc±x¯oœRcfe…£’o§ËgToÛJíĔóu…|wP¤™XnO¢ÉˆŦ¯rNÄā¤zâŖÈRpŢZŠœÚ{GŠrFt¦Òx§ø¹RóäV¤XdˆżâºWbwŚ¨Ud®bêņ¾‘jnŎGŃŶŠnzÚSeîĜZczî¾i]͜™QaúÍÔiþĩȨWĢ‹ü|Ėu[qb[swP@ÅğP¿{\\‡¥A¨Ï‘Ѩj¯ŠX\\¯œMK‘pA³[H…īu}}" + ], + "encodeOffsets": [[120023, 41045]] + } + }, + { + "type": "Feature", + "id": "120000", + "properties": { + "id": "120000", + "cp": [117.190182, 39.125596], + "name": "天津", + "childNum": 1 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + "@@ŬgX§Ü«E…¶Ḟ“¬O_™ïlÁg“z±AXe™µÄĵ{¶]gitgšIj·›¥îakS€‰¨ÐƎk}ĕ{gB—qGf{¿a†U^fI“ư‹³õ{YƒıëNĿžk©ïËZŏ‘R§òoY×Ógc…ĥs¡bġ«@dekąI[nlPqCnp{ˆō³°`{PNdƗqSÄĻNNâyj]äžÒD ĬH°Æ]~¡HO¾ŒX}ÐxŒgp“gWˆrDGˆŒpù‚Š^L‚ˆrzWxˆZ^¨´T\\|~@I‰zƒ–bĤ‹œjeĊªz£®Ĕvě€L†mV¾Ô_ȔNW~zbĬvG†²ZmDM~”~" + ], + "encodeOffsets": [[120237, 41215]] + } + }, + { + "type": "Feature", + "id": "310000", + "properties": { + "id": "310000", + "cp": [121.472644, 31.231706], + "name": "上海", + "childNum": 6 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@ɧư¬EpƸÁxc‡"], + ["@@©„ªƒ"], + ["@@”MA‹‘š"], + ["@@Qp݁E§ÉC¾"], + ["@@bŝՕÕEȣÚƥêImɇǦèÜĠŒÚžÃƌÃ͎ó"], + ["@@ǜûȬɋŠŭ™×^‰sYŒɍDŋ‘ŽąñCG²«ªč@h–_p¯A{‡oloY€¬j@IJ`•gQڛhr|ǀ^MIJvtbe´R¯Ô¬¨YŽô¤r]ì†Ƭį"] + ], + "encodeOffsets": [ + [[124702, 32062]], + [[124547, 32200]], + [[124808, 31991]], + [[124726, 32110]], + [[124903, 32376]], + [[124438, 32149]] + ] + } + }, + { + "type": "Feature", + "id": "500000", + "properties": { + "id": "500000", + "cp": [107.304962, 29.533155], + "name": "重庆", + "childNum": 2 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + "@@vjG~nGŘŬĶȂƀƾ¹¸ØÎezĆT¸}êЖqHŸðqĖ䒊¥^CƒIj–²p…\\_ æüY|[YxƊæuž°xb®…Űb@~¢NQt°¶‚S栓Ê~rljĔëĚ¢~šuf`‘‚†fa‚ĔJåĊ„nÖ]„jƎćÊ@Š£¾a®£Ű{ŶĕF‹ègLk{Y|¡ĜWƔtƬJÑxq‹±ĢN´‰òK‰™–LÈüD|s`ŋ’ć]ƒÃ‰`đŒMûƱ½~Y°ħ`ƏíW‰½eI‹½{aŸ‘OIrÏ¡ĕŇa†p†µÜƅġ‘œ^ÖÛbÙŽŏml½S‹êqDu[R‹ãË»†ÿw`»y‘¸_ĺę}÷`M¯ċfCVµqʼn÷Z•gg“Œ`d½pDO‡ÎCnœ^uf²ènh¼WtƏxRGg¦…pV„†FI±ŽG^ŒIc´ec‡’G•ĹÞ½sëĬ„h˜xW‚}Kӈe­Xsbk”F¦›L‘ØgTkïƵNï¶}Gy“w\\oñ¡nmĈzjŸ•@™Óc£»Wă¹Ój“_m»ˆ¹·~MvÛaqœ»­‰êœ’\\ÂoVnŽÓØ͙²«‹bq¿efE „€‹Ĝ^Qž~ Évý‡ş¤²Į‰pEİ}zcĺƒL‹½‡š¿gņ›¡ýE¡ya£³t\\¨\\vú»¼§·Ñr_oÒý¥u‚•_n»_ƒ•At©Þűā§IVeëƒY}{VPÀFA¨ąB}q@|Ou—\\Fm‰QF݅Mw˜å}]•€|FmϋCaƒwŒu_p—¯sfÙgY…DHl`{QEfNysBŠ¦zG¸rHe‚„N\\CvEsÐùÜ_·ÖĉsaQ¯€}_U‡†xÃđŠq›NH¬•Äd^ÝŰR¬ã°wećJEž·vÝ·Hgƒ‚éFXjÉê`|yŒpxkAwœWĐpb¥eOsmzwqChóUQl¥F^laf‹anòsr›EvfQdÁUVf—ÎvÜ^efˆtET¬ôA\\œ¢sJŽnQTjP؈xøK|nBz‰„œĞ»LY‚…FDxӄvr“[ehľš•vN”¢o¾NiÂxGp⬐z›bfZo~hGi’]öF|‰|Nb‡tOMn eA±ŠtPT‡LjpYQ|†SH††YĀxinzDJ€Ìg¢và¥Pg‰_–ÇzII‹€II•„£®S¬„Øs쐣ŒN" + ], + ["@@ifjN@s"] + ], + "encodeOffsets": [[[109628, 30765]], [[111725, 31320]]] + } + }, + { + "type": "Feature", + "id": "810000", + "properties": { + "id": "810000", + "cp": [114.173355, 22.320048], + "name": "香港", + "childNum": 5 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + ["@@AlBk"], + ["@@mŽn"], + ["@@EpFo"], + ["@@ea¢pl¸Eõ¹‡hj[ƒ]ÔCΖ@lj˜¡uBXŸ…•´‹AI¹…[‹yDUˆ]W`çwZkmc–…M›žp€Åv›}I‹oJlcaƒfёKŽ°ä¬XJmРđhI®æÔtSHn€Eˆ„ÒrÈc"], + ["@@rMUw‡AS®€e"] + ], + "encodeOffsets": [ + [[117111, 23002]], + [[117072, 22876]], + [[117045, 22887]], + [[116975, 23082]], + [[116882, 22747]] + ] + } + }, + { + "type": "Feature", + "id": "820000", + "properties": { "id": "820000", "cp": [113.54909, 22.198951], "name": "澳门", "childNum": 1 }, + "geometry": { + "type": "Polygon", + "coordinates": ["@@kÊd°å§s"], + "encodeOffsets": [[116279, 22639]] + } + } + ], + "UTF8Encoding": true +} diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/403.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/403.svg new file mode 100644 index 00000000..45005961 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/403.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/404.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/404.svg new file mode 100644 index 00000000..5244d8d4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/500.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/500.svg new file mode 100644 index 00000000..9c020927 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/500.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/icon.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/icon.svg new file mode 100644 index 00000000..7024becf --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/login-bg.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/login-bg.svg new file mode 100644 index 00000000..bbe06c16 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/login-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/login-box-bg.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/login-box-bg.svg new file mode 100644 index 00000000..ab100403 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/login-box-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_balance.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_balance.svg new file mode 100644 index 00000000..5395b236 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_balance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_expenditure_balance.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_expenditure_balance.svg new file mode 100644 index 00000000..02d498cd --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_expenditure_balance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_level.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_level.svg new file mode 100644 index 00000000..cbcc686d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_level.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_point.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_point.svg new file mode 100644 index 00000000..b849ddb4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_point.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_recharge_balance.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_recharge_balance.svg new file mode 100644 index 00000000..7519bb23 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/member_recharge_balance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/message.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/message.svg new file mode 100644 index 00000000..14ca8172 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/money.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/money.svg new file mode 100644 index 00000000..c1580de1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_app.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_app.svg new file mode 100644 index 00000000..ebf11883 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_bar.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_bar.svg new file mode 100644 index 00000000..eb1e1e84 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_bar.svg @@ -0,0 +1,2 @@ + diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_pc.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_pc.svg new file mode 100644 index 00000000..2a752770 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_pc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_qr.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_qr.svg new file mode 100644 index 00000000..48337508 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_qr.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_wap.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_wap.svg new file mode 100644 index 00000000..87075dbb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/alipay_wap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/mock.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/mock.svg new file mode 100644 index 00000000..27b09ead --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/mock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_app.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_app.svg new file mode 100644 index 00000000..ad40b2a2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_app.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_bar.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_bar.svg new file mode 100644 index 00000000..11292e6e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_bar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_lite.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_lite.svg new file mode 100644 index 00000000..0c925cf3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_lite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_native.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_native.svg new file mode 100644 index 00000000..bf3ba2b6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_native.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_pub.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_pub.svg new file mode 100644 index 00000000..3a6d15b7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/pay/icon/wx_pub.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/peoples.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/peoples.svg new file mode 100644 index 00000000..aab852e5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/assets/svgs/shopping.svg b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/shopping.svg new file mode 100644 index 00000000..f395bc7f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/assets/svgs/shopping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mes-ui/mes-ui-admin-vue3/src/components/AppLinkInput/AppLinkSelectDialog.vue b/mes-ui/mes-ui-admin-vue3/src/components/AppLinkInput/AppLinkSelectDialog.vue new file mode 100644 index 00000000..a536ac13 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/AppLinkInput/AppLinkSelectDialog.vue @@ -0,0 +1,198 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/AppLinkInput/data.ts b/mes-ui/mes-ui-admin-vue3/src/components/AppLinkInput/data.ts new file mode 100644 index 00000000..fd7780f1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/AppLinkInput/data.ts @@ -0,0 +1,246 @@ +// APP 链接类型(需要特殊处理,例如商品详情) +export const enum APP_LINK_TYPE_ENUM { + // 拼团活动 + ACTIVITY_COMBINATION, + // 秒杀活动 + ACTIVITY_SECKILL, + // 文章详情 + ARTICLE_DETAIL, + // 优惠券详情 + COUPON_DETAIL, + // 自定义页面详情 + DIY_PAGE_DETAIL, + // 品类列表 + PRODUCT_CATEGORY_LIST, + // 商品列表 + PRODUCT_LIST, + // 商品详情 + PRODUCT_DETAIL_NORMAL, + // 拼团商品详情 + PRODUCT_DETAIL_COMBINATION, + // 积分商品详情 + PRODUCT_DETAIL_POINT, + // 秒杀商品详情 + PRODUCT_DETAIL_SECKILL +} + +// APP 链接列表(做一下持久化?) +export const APP_LINK_GROUP_LIST = [ + { + name: '商城', + links: [ + { + name: '首页', + path: '/pages/index/index' + }, + { + name: '商品分类', + path: '/pages/index/category', + type: APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST + }, + { + name: '购物车', + path: '/pages/index/cart' + }, + { + name: '个人中心', + path: '/pages/index/user' + }, + { + name: '商品搜索', + path: '/pages/index/search' + }, + { + name: '自定义页面', + path: '/pages/index/page', + type: APP_LINK_TYPE_ENUM.DIY_PAGE_DETAIL + }, + { + name: '客服', + path: '/pages/chat/index' + }, + { + name: '系统设置', + path: '/pages/public/setting' + }, + { + name: '问题反馈', + path: '/pages/public/feedback' + }, + { + name: '常见问题', + path: '/pages/public/faq' + } + ] + }, + { + name: '商品', + links: [ + { + name: '商品列表', + path: '/pages/goods/list', + type: APP_LINK_TYPE_ENUM.PRODUCT_LIST + }, + { + name: '商品详情', + path: '/pages/goods/index', + type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_NORMAL + }, + { + name: '拼团商品详情', + path: '/pages/goods/groupon', + type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_COMBINATION + }, + { + name: '秒杀商品详情', + path: '/pages/goods/seckill', + type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL + }, + { + name: '积分商品详情', + path: '/pages/goods/score', + type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_POINT + } + ] + }, + { + name: '营销活动', + links: [ + { + name: '拼团订单', + path: '/pages/activity/groupon/order' + }, + { + name: '营销商品', + path: '/pages/activity/index' + }, + { + name: '拼团活动', + path: '/pages/activity/groupon/list', + type: APP_LINK_TYPE_ENUM.ACTIVITY_COMBINATION + }, + { + name: '秒杀活动', + path: '/pages/activity/seckill/list', + type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL + }, + { + name: '签到中心', + path: '/pages/app/sign' + }, + { + name: '积分商城', + path: '/pages/app/score-shop' + }, + { + name: '优惠券中心', + path: '/pages/coupon/list' + }, + { + name: '优惠券详情', + path: '/pages/coupon/detail', + type: APP_LINK_TYPE_ENUM.COUPON_DETAIL + }, + { + name: '文章详情', + path: '/pages/public/richtext', + type: APP_LINK_TYPE_ENUM.ARTICLE_DETAIL + } + ] + }, + { + name: '分销商城', + links: [ + { + name: '分销中心', + path: '/pages/commission/index' + }, + { + name: '申请分销商', + path: '/pages/commission/apply' + }, + { + name: '推广商品', + path: '/pages/commission/goods' + }, + { + name: '分销订单', + path: '/pages/commission/order' + }, + { + name: '分享记录', + path: '/pages/commission/share-log' + }, + { + name: '我的团队', + path: '/pages/commission/team' + } + ] + }, + { + name: '支付', + links: [ + { + name: '充值余额', + path: '/pages/pay/recharge' + }, + { + name: '充值记录', + path: '/pages/pay/recharge-log' + }, + { + name: '申请提现', + path: '/pages/pay/withdraw' + }, + { + name: '提现记录', + path: '/pages/pay/withdraw-log' + } + ] + }, + { + name: '用户中心', + links: [ + { + name: '用户信息', + path: '/pages/user/info' + }, + { + name: '用户订单', + path: '/pages/order/list' + }, + { + name: '售后订单', + path: '/pages/order/aftersale/list' + }, + { + name: '商品收藏', + path: '/pages/user/goods-collect' + }, + { + name: '浏览记录', + path: '/pages/user/goods-log' + }, + { + name: '地址管理', + path: '/pages/user/address/list' + }, + { + name: '发票管理', + path: '/pages/user/invoice/list' + }, + { + name: '用户佣金', + path: '/pages/user/wallet/commission' + }, + { + name: '用户余额', + path: '/pages/user/wallet/money' + }, + { + name: '用户积分', + path: '/pages/user/wallet/score' + } + ] + } +] diff --git a/mes-ui/mes-ui-admin-vue3/src/components/AppLinkInput/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/AppLinkInput/index.vue new file mode 100644 index 00000000..a01386b9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/AppLinkInput/index.vue @@ -0,0 +1,43 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Backtop/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Backtop/index.ts new file mode 100644 index 00000000..96de88d6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Backtop/index.ts @@ -0,0 +1,3 @@ +import Backtop from './src/Backtop.vue' + +export { Backtop } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Backtop/src/Backtop.vue b/mes-ui/mes-ui-admin-vue3/src/components/Backtop/src/Backtop.vue new file mode 100644 index 00000000..5d79f51a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Backtop/src/Backtop.vue @@ -0,0 +1,17 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Card/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Card/index.ts new file mode 100644 index 00000000..f4c0d86c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Card/index.ts @@ -0,0 +1,3 @@ +import CardTitle from './src/CardTitle.vue' + +export { CardTitle } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Card/src/CardTitle.vue b/mes-ui/mes-ui-admin-vue3/src/components/Card/src/CardTitle.vue new file mode 100644 index 00000000..76a83564 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Card/src/CardTitle.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ColorInput/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/ColorInput/index.vue new file mode 100644 index 00000000..63ff73cf --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ColorInput/index.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ConfigGlobal/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/ConfigGlobal/index.ts new file mode 100644 index 00000000..dda2462c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ConfigGlobal/index.ts @@ -0,0 +1,3 @@ +import ConfigGlobal from './src/ConfigGlobal.vue' + +export { ConfigGlobal } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue b/mes-ui/mes-ui-admin-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue new file mode 100644 index 00000000..5bd90cf1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue @@ -0,0 +1,62 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ContentDetailWrap/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/ContentDetailWrap/index.ts new file mode 100644 index 00000000..1871cac7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ContentDetailWrap/index.ts @@ -0,0 +1,3 @@ +import ContentDetailWrap from './src/ContentDetailWrap.vue' + +export { ContentDetailWrap } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ContentDetailWrap/src/ContentDetailWrap.vue b/mes-ui/mes-ui-admin-vue3/src/components/ContentDetailWrap/src/ContentDetailWrap.vue new file mode 100644 index 00000000..a9eacc01 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ContentDetailWrap/src/ContentDetailWrap.vue @@ -0,0 +1,58 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ContentWrap/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/ContentWrap/index.ts new file mode 100644 index 00000000..8c22cc83 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ContentWrap/index.ts @@ -0,0 +1,3 @@ +import ContentWrap from './src/ContentWrap.vue' + +export { ContentWrap } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ContentWrap/src/ContentWrap.vue b/mes-ui/mes-ui-admin-vue3/src/components/ContentWrap/src/ContentWrap.vue new file mode 100644 index 00000000..e3bd5972 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ContentWrap/src/ContentWrap.vue @@ -0,0 +1,34 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/CountTo/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/CountTo/index.ts new file mode 100644 index 00000000..2119f023 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/CountTo/index.ts @@ -0,0 +1,3 @@ +import CountTo from './src/CountTo.vue' + +export { CountTo } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/CountTo/src/CountTo.vue b/mes-ui/mes-ui-admin-vue3/src/components/CountTo/src/CountTo.vue new file mode 100644 index 00000000..7a19bec7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/CountTo/src/CountTo.vue @@ -0,0 +1,182 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Crontab/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Crontab/index.ts new file mode 100644 index 00000000..6beeef86 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Crontab/index.ts @@ -0,0 +1,2 @@ +import Crontab from './src/Crontab.vue' +export { Crontab } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Crontab/src/Crontab.vue b/mes-ui/mes-ui-admin-vue3/src/components/Crontab/src/Crontab.vue new file mode 100644 index 00000000..90b40b2f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Crontab/src/Crontab.vue @@ -0,0 +1,1011 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Cropper/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/index.ts new file mode 100644 index 00000000..8fcc6183 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/index.ts @@ -0,0 +1,4 @@ +import CropperImage from './src/Cropper.vue' +import CropperAvatar from './src/CropperAvatar.vue' + +export { CropperImage, CropperAvatar } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/CopperModal.vue b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/CopperModal.vue new file mode 100644 index 00000000..27052b8a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/CopperModal.vue @@ -0,0 +1,261 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/Cropper.vue b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/Cropper.vue new file mode 100644 index 00000000..871aed8f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/Cropper.vue @@ -0,0 +1,183 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/CropperAvatar.vue b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/CropperAvatar.vue new file mode 100644 index 00000000..55a7d34b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/CropperAvatar.vue @@ -0,0 +1,142 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/types.ts b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/types.ts new file mode 100644 index 00000000..bcad3b45 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Cropper/src/types.ts @@ -0,0 +1,8 @@ +import type Cropper from 'cropperjs' + +export interface CropendResult { + imgBase64: string + imgInfo: Cropper.Data +} + +export type { Cropper } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Descriptions/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Descriptions/index.ts new file mode 100644 index 00000000..243bc397 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Descriptions/index.ts @@ -0,0 +1,4 @@ +import Descriptions from './src/Descriptions.vue' +import DescriptionsItemLabel from './src/DescriptionsItemLabel.vue' + +export { Descriptions, DescriptionsItemLabel } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Descriptions/src/Descriptions.vue b/mes-ui/mes-ui-admin-vue3/src/components/Descriptions/src/Descriptions.vue new file mode 100644 index 00000000..06e1096a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Descriptions/src/Descriptions.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Descriptions/src/DescriptionsItemLabel.vue b/mes-ui/mes-ui-admin-vue3/src/components/Descriptions/src/DescriptionsItemLabel.vue new file mode 100644 index 00000000..4efb2fb7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Descriptions/src/DescriptionsItemLabel.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Dialog/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Dialog/index.ts new file mode 100644 index 00000000..1655dadc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Dialog/index.ts @@ -0,0 +1,3 @@ +import Dialog from './src/Dialog.vue' + +export { Dialog } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Dialog/src/Dialog.vue b/mes-ui/mes-ui-admin-vue3/src/components/Dialog/src/Dialog.vue new file mode 100644 index 00000000..a1eb550c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Dialog/src/Dialog.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DictTag/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/DictTag/index.ts new file mode 100644 index 00000000..4db27420 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DictTag/index.ts @@ -0,0 +1,3 @@ +import DictTag from './src/DictTag.vue' + +export { DictTag } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DictTag/src/DictTag.vue b/mes-ui/mes-ui-admin-vue3/src/components/DictTag/src/DictTag.vue new file mode 100644 index 00000000..db37f714 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DictTag/src/DictTag.vue @@ -0,0 +1,60 @@ + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/ComponentContainer.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/ComponentContainer.vue new file mode 100644 index 00000000..e6f1beb3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/ComponentContainer.vue @@ -0,0 +1,235 @@ + + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/ComponentContainerProperty.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/ComponentContainerProperty.vue new file mode 100644 index 00000000..a8187898 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/ComponentContainerProperty.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/ComponentLibrary.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/ComponentLibrary.vue new file mode 100644 index 00000000..e319a5c2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/ComponentLibrary.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Carousel/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Carousel/config.ts new file mode 100644 index 00000000..3e74a511 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Carousel/config.ts @@ -0,0 +1,50 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 轮播图属性 */ +export interface CarouselProperty { + // 类型:默认 | 卡片 + type: 'default' | 'card' + // 指示器样式:点 | 数字 + indicator: 'dot' | 'number' + // 是否自动播放 + autoplay: boolean + // 播放间隔 + interval: number + // 轮播内容 + items: CarouselItemProperty[] + // 组件样式 + style: ComponentStyle +} +// 轮播内容属性 +export interface CarouselItemProperty { + // 类型:图片 | 视频 + type: 'img' | 'video' + // 图片链接 + imgUrl: string + // 视频链接 + videoUrl: string + // 跳转链接 + url: string +} + +// 定义组件 +export const component = { + id: 'Carousel', + name: '轮播图', + icon: 'system-uicons:carousel', + property: { + type: 'default', + indicator: 'dot', + autoplay: false, + interval: 3, + items: [ + { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' }, + { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' } + ] as CarouselItemProperty[], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Carousel/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Carousel/index.vue new file mode 100644 index 00000000..360b4a49 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Carousel/index.vue @@ -0,0 +1,43 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Carousel/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Carousel/property.vue new file mode 100644 index 00000000..972ba507 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Carousel/property.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/component.tsx b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/component.tsx new file mode 100644 index 00000000..1542cad6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/component.tsx @@ -0,0 +1,78 @@ +import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate' +import { CouponTemplateValidityTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants' +import { floatToFixed2 } from '@/utils' +import { formatDate } from '@/utils/formatTime' + +// 优惠值 +export const CouponDiscount = defineComponent({ + name: 'CouponDiscount', + props: { + coupon: { + type: CouponTemplateApi.CouponTemplateVO + } + }, + setup(props) { + const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO + // 折扣 + let value = coupon.discountPercent + '' + let suffix = ' 折' + // 满减 + if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) { + value = floatToFixed2(coupon.discountPrice) + suffix = ' 元' + } + return () => ( +
+ {value} + {suffix} +
+ ) + } +}) + +// 优惠描述 +export const CouponDiscountDesc = defineComponent({ + name: 'CouponDiscountDesc', + props: { + coupon: { + type: CouponTemplateApi.CouponTemplateVO + } + }, + setup(props) { + const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO + // 使用条件 + const useCondition = coupon.usePrice > 0 ? `满${floatToFixed2(coupon.usePrice)}元,` : '' + // 优惠描述 + const discountDesc = + coupon.discountType === PromotionDiscountTypeEnum.PRICE.type + ? `减${floatToFixed2(coupon.discountPrice)}元` + : `打${coupon.discountPercent}折` + return () => ( +
+ {useCondition} + {discountDesc} +
+ ) + } +}) + +// 有效期 +export const CouponValidTerm = defineComponent({ + name: 'CouponValidTerm', + props: { + coupon: { + type: CouponTemplateApi.CouponTemplateVO + } + }, + setup(props) { + const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO + const text = + coupon.validityType === CouponTemplateValidityTypeEnum.DATE.type + ? `有效期:${formatDate(coupon.validStartTime, 'YYYY-MM-DD')} 至 ${formatDate( + coupon.validEndTime, + 'YYYY-MM-DD' + )}` + : `领取后第 ${coupon.fixedStartTerm} - ${coupon.fixedEndTerm} 天内可用` + return () =>
{text}
+ } +}) diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/config.ts new file mode 100644 index 00000000..304533d1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/config.ts @@ -0,0 +1,47 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 商品卡片属性 */ +export interface CouponCardProperty { + // 列数 + columns: number + // 背景图 + bgImg: string + // 文字颜色 + textColor: string + // 按钮样式 + button: { + // 颜色 + color: string + // 背景颜色 + bgColor: string + } + // 间距 + space: number + // 优惠券编号列表 + couponIds: number[] + // 组件样式 + style: ComponentStyle +} + +// 定义组件 +export const component = { + id: 'CouponCard', + name: '优惠券', + icon: 'ep:ticket', + property: { + columns: 1, + bgImg: '', + textColor: '#E9B461', + button: { + color: '#434343', + bgColor: '' + }, + space: 0, + couponIds: [], + style: { + bgType: 'color', + bgColor: '', + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/index.vue new file mode 100644 index 00000000..3e2302af --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/index.vue @@ -0,0 +1,142 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/property.vue new file mode 100644 index 00000000..4f32c21e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/CouponCard/property.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Divider/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Divider/config.ts new file mode 100644 index 00000000..9b553604 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Divider/config.ts @@ -0,0 +1,29 @@ +import { DiyComponent } from '@/components/DiyEditor/util' + +/** 分割线属性 */ +export interface DividerProperty { + // 高度 + height: number + // 线宽 + lineWidth: number + // 边距类型 + paddingType: 'none' | 'horizontal' + // 颜色 + lineColor: string + // 类型 + borderType: 'solid' | 'dashed' | 'dotted' | 'none' +} + +// 定义组件 +export const component = { + id: 'Divider', + name: '分割线', + icon: 'tdesign:component-divider-vertical', + property: { + height: 30, + lineWidth: 1, + paddingType: 'none', + lineColor: '#dcdfe6', + borderType: 'solid' + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Divider/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Divider/index.vue new file mode 100644 index 00000000..f7785043 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Divider/index.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Divider/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Divider/property.vue new file mode 100644 index 00000000..3d7be26d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/Divider/property.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ImageBar/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ImageBar/config.ts new file mode 100644 index 00000000..68edf728 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ImageBar/config.ts @@ -0,0 +1,27 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 图片展示属性 */ +export interface ImageBarProperty { + // 图片链接 + imgUrl: string + // 跳转链接 + url: string + // 组件样式 + style: ComponentStyle +} + +// 定义组件 +export const component = { + id: 'ImageBar', + name: '图片展示', + icon: 'ep:picture', + property: { + imgUrl: '', + url: '', + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ImageBar/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ImageBar/index.vue new file mode 100644 index 00000000..d9685b50 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ImageBar/index.vue @@ -0,0 +1,24 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ImageBar/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ImageBar/property.vue new file mode 100644 index 00000000..d8163615 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ImageBar/property.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MagicCube/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MagicCube/config.ts new file mode 100644 index 00000000..bd3120bb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MagicCube/config.ts @@ -0,0 +1,48 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 广告魔方属性 */ +export interface MagicCubeProperty { + // 上圆角 + borderRadiusTop: number + // 下圆角 + borderRadiusBottom: number + // 间隔 + space: number + // 导航菜单列表 + list: MagicCubeItemProperty[] + // 组件样式 + style: ComponentStyle +} +/** 广告魔方项目属性 */ +export interface MagicCubeItemProperty { + // 图标链接 + imgUrl: string + // 链接 + url: string + // 宽 + width: number + // 高 + height: number + // 上 + top: number + // 左 + left: number +} + +// 定义组件 +export const component = { + id: 'MagicCube', + name: '广告魔方', + icon: 'bi:columns', + property: { + borderRadiusTop: 0, + borderRadiusBottom: 0, + space: 0, + list: [], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MagicCube/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MagicCube/index.vue new file mode 100644 index 00000000..48fb6c75 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MagicCube/index.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MagicCube/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MagicCube/property.vue new file mode 100644 index 00000000..fe938e5b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MagicCube/property.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuGrid/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuGrid/config.ts new file mode 100644 index 00000000..b5a5d97d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuGrid/config.ts @@ -0,0 +1,78 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' +import { cloneDeep } from 'lodash-es' + +/** 宫格导航属性 */ +export interface MenuGridProperty { + // 列数 + column: number + // 导航菜单列表 + list: MenuGridItemProperty[] + // 组件样式 + style: ComponentStyle +} +/** 宫格导航项目属性 */ +export interface MenuGridItemProperty { + // 图标链接 + iconUrl: string + // 标题 + title: string + // 标题颜色 + titleColor: string + // 副标题 + subtitle: string + // 副标题颜色 + subtitleColor: string + // 链接 + url: string + // 角标 + badge: { + // 是否显示 + show: boolean + // 角标文字 + text: string + // 角标文字颜色 + textColor: string + // 角标背景颜色 + bgColor: string + } +} + +export const EMPTY_MENU_GRID_ITEM_PROPERTY = { + title: '标题', + titleColor: '#333', + subtitle: '副标题', + subtitleColor: '#bbb', + badge: { + show: false, + textColor: '#fff', + bgColor: '#FF6000' + } +} as MenuGridItemProperty + +// 定义组件 +export const component = { + id: 'MenuGrid', + name: '宫格导航', + icon: 'bi:grid-3x3-gap', + property: { + column: 3, + list: [cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY)], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + marginLeft: 8, + marginRight: 8, + padding: 8, + paddingTop: 8, + paddingRight: 8, + paddingBottom: 8, + paddingLeft: 8, + borderRadius: 8, + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + borderBottomRightRadius: 8, + borderBottomLeftRadius: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuGrid/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuGrid/index.vue new file mode 100644 index 00000000..1c5ef1dc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuGrid/index.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuGrid/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuGrid/property.vue new file mode 100644 index 00000000..b92e2099 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuGrid/property.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuList/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuList/config.ts new file mode 100644 index 00000000..c42674f3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuList/config.ts @@ -0,0 +1,47 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' +import { cloneDeep } from 'lodash-es' + +/** 列表导航属性 */ +export interface MenuListProperty { + // 导航菜单列表 + list: MenuListItemProperty[] + // 组件样式 + style: ComponentStyle +} +/** 列表导航项目属性 */ +export interface MenuListItemProperty { + // 图标链接 + iconUrl: string + // 标题 + title: string + // 标题颜色 + titleColor: string + // 副标题 + subtitle: string + // 副标题颜色 + subtitleColor: string + // 链接 + url: string +} + +export const EMPTY_MENU_LIST_ITEM_PROPERTY = { + title: '标题', + titleColor: '#333', + subtitle: '副标题', + subtitleColor: '#bbb' +} + +// 定义组件 +export const component = { + id: 'MenuList', + name: '列表导航', + icon: 'fa-solid:list', + property: { + list: [cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY)], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuList/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuList/index.vue new file mode 100644 index 00000000..9a56fd94 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuList/index.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuList/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuList/property.vue new file mode 100644 index 00000000..0ed6035c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuList/property.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuSwiper/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuSwiper/config.ts new file mode 100644 index 00000000..fe5f4e87 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuSwiper/config.ts @@ -0,0 +1,66 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' +import { cloneDeep } from 'lodash-es' + +/** 菜单导航属性 */ +export interface MenuSwiperProperty { + // 布局: 图标+文字 | 图标 + layout: 'iconText' | 'icon' + // 行数 + row: number + // 列数 + column: number + // 导航菜单列表 + list: MenuSwiperItemProperty[] + // 组件样式 + style: ComponentStyle +} +/** 菜单导航项目属性 */ +export interface MenuSwiperItemProperty { + // 图标链接 + iconUrl: string + // 标题 + title: string + // 标题颜色 + titleColor: string + // 链接 + url: string + // 角标 + badge: { + // 是否显示 + show: boolean + // 角标文字 + text: string + // 角标文字颜色 + textColor: string + // 角标背景颜色 + bgColor: string + } +} + +export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = { + title: '标题', + titleColor: '#333', + badge: { + show: false, + textColor: '#fff', + bgColor: '#FF6000' + } +} as MenuSwiperItemProperty + +// 定义组件 +export const component = { + id: 'MenuSwiper', + name: '菜单导航', + icon: 'bi:grid-3x2-gap', + property: { + layout: 'iconText', + row: 1, + column: 3, + list: [cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuSwiper/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuSwiper/index.vue new file mode 100644 index 00000000..6ae6439c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuSwiper/index.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue new file mode 100644 index 00000000..31e158ce --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NavigationBar/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NavigationBar/config.ts new file mode 100644 index 00000000..f722d525 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NavigationBar/config.ts @@ -0,0 +1,38 @@ +import { DiyComponent } from '@/components/DiyEditor/util' + +/** 顶部导航栏属性 */ +export interface NavigationBarProperty { + // 页面标题 + title: string + // 页面描述 + description: string + // 顶部导航高度 + navBarHeight: number + // 页面背景颜色 + backgroundColor: string + // 页面背景图片 + backgroundImage: string + // 样式类型:默认 | 沉浸式 + styleType: 'default' | 'immersion' + // 常驻显示 + alwaysShow: boolean + // 是否显示返回按钮 + showGoBack: boolean +} + +// 定义组件 +export const component = { + id: 'NavigationBar', + name: '顶部导航栏', + icon: 'tabler:layout-navbar', + property: { + title: '页面标题', + description: '', + navBarHeight: 35, + backgroundColor: '#fff', + backgroundImage: '', + styleType: 'default', + alwaysShow: true, + showGoBack: true + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NavigationBar/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NavigationBar/index.vue new file mode 100644 index 00000000..f5cfff8c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NavigationBar/index.vue @@ -0,0 +1,62 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NavigationBar/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NavigationBar/property.vue new file mode 100644 index 00000000..c4ca4588 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NavigationBar/property.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NoticeBar/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NoticeBar/config.ts new file mode 100644 index 00000000..4c297ef6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NoticeBar/config.ts @@ -0,0 +1,39 @@ +import { DiyComponent } from '@/components/DiyEditor/util' + +/** 公告栏属性 */ +export interface NoticeBarProperty { + // 图标地址 + iconUrl: string + // 公告内容列表 + contents: NoticeContentProperty[] + // 背景颜色 + backgroundColor: string + // 文字颜色 + textColor: string +} + +/** 内容属性 */ +export interface NoticeContentProperty { + // 内容文字 + text: string + // 链接地址 + url: string +} + +// 定义组件 +export const component = { + id: 'NoticeBar', + name: '公告栏', + icon: 'ep:bell', + property: { + iconUrl: 'http://mall.mes.iocoder.cn/static/images/xinjian.png', + contents: [ + { + text: '', + url: '' + } + ], + backgroundColor: '#fff', + textColor: '#333' + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NoticeBar/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NoticeBar/index.vue new file mode 100644 index 00000000..fce1afbb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NoticeBar/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NoticeBar/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NoticeBar/property.vue new file mode 100644 index 00000000..a3eefebe --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/NoticeBar/property.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PageConfig/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PageConfig/config.ts new file mode 100644 index 00000000..f8e45e45 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PageConfig/config.ts @@ -0,0 +1,23 @@ +import { DiyComponent } from '@/components/DiyEditor/util' + +/** 页面设置属性 */ +export interface PageConfigProperty { + // 页面描述 + description: string + // 页面背景颜色 + backgroundColor: string + // 页面背景图片 + backgroundImage: string +} + +// 定义页面组件 +export const component = { + id: 'PageConfig', + name: '页面设置', + icon: 'ep:document', + property: { + description: '', + backgroundColor: '#f5f5f5', + backgroundImage: '' + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PageConfig/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PageConfig/property.vue new file mode 100644 index 00000000..278bc940 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PageConfig/property.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductCard/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductCard/config.ts new file mode 100644 index 00000000..735b6ba0 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductCard/config.ts @@ -0,0 +1,97 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 商品卡片属性 */ +export interface ProductCardProperty { + // 布局类型:单列大图 | 单列小图 | 双列 + layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' + // 商品字段 + fields: { + // 商品名称 + name: ProductCardFieldProperty + // 商品简介 + introduction: ProductCardFieldProperty + // 商品价格 + price: ProductCardFieldProperty + // 商品市场价 + marketPrice: ProductCardFieldProperty + // 商品销量 + salesCount: ProductCardFieldProperty + // 商品库存 + stock: ProductCardFieldProperty + } + // 角标 + badge: { + // 是否显示 + show: boolean + // 角标图片 + imgUrl: string + } + // 按钮 + btnBuy: { + // 类型:文字 | 图片 + type: 'text' | 'img' + // 文字 + text: string + // 文字按钮:背景渐变起始颜色 + bgBeginColor: string + // 文字按钮:背景渐变结束颜色 + bgEndColor: string + // 图片按钮:图片地址 + imgUrl: string + } + // 上圆角 + borderRadiusTop: number + // 下圆角 + borderRadiusBottom: number + // 间距 + space: number + // 商品编号列表 + spuIds: number[] + // 组件样式 + style: ComponentStyle +} +// 商品字段 +export interface ProductCardFieldProperty { + // 是否显示 + show: boolean + // 颜色 + color: string +} + +// 定义组件 +export const component = { + id: 'ProductCard', + name: '商品卡片', + icon: 'fluent:text-column-two-left-24-filled', + property: { + layoutType: 'oneColBigImg', + fields: { + name: { show: true, color: '#000' }, + introduction: { show: true, color: '#999' }, + price: { show: true, color: '#ff3000' }, + marketPrice: { show: true, color: '#c4c4c4' }, + salesCount: { show: true, color: '#c4c4c4' }, + stock: { show: false, color: '#c4c4c4' } + }, + badge: { show: false, imgUrl: '' }, + btnBuy: { + type: 'text', + text: '立即购买', + // todo: @owen 根据主题色配置 + bgBeginColor: '#FF6000', + bgEndColor: '#FE832A', + imgUrl: '' + }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + spuIds: [], + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductCard/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductCard/index.vue new file mode 100644 index 00000000..a6894ed9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductCard/index.vue @@ -0,0 +1,165 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductCard/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductCard/property.vue new file mode 100644 index 00000000..cfa5008b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductCard/property.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductList/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductList/config.ts new file mode 100644 index 00000000..1f168323 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductList/config.ts @@ -0,0 +1,64 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 商品栏属性 */ +export interface ProductListProperty { + // 布局类型:双列 | 三列 | 水平滑动 + layoutType: 'twoCol' | 'threeCol' | 'horizSwiper' + // 商品字段 + fields: { + // 商品名称 + name: ProductListFieldProperty + // 商品价格 + price: ProductListFieldProperty + } + // 角标 + badge: { + // 是否显示 + show: boolean + // 角标图片 + imgUrl: string + } + // 上圆角 + borderRadiusTop: number + // 下圆角 + borderRadiusBottom: number + // 间距 + space: number + // 商品编号列表 + spuIds: number[] + // 组件样式 + style: ComponentStyle +} +// 商品字段 +export interface ProductListFieldProperty { + // 是否显示 + show: boolean + // 颜色 + color: string +} + +// 定义组件 +export const component = { + id: 'ProductList', + name: '商品栏', + icon: 'fluent:text-column-two-24-filled', + property: { + layoutType: 'twoCol', + fields: { + name: { show: true, color: '#000' }, + price: { show: true, color: '#ff3000' } + }, + badge: { show: false, imgUrl: '' }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + spuIds: [], + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductList/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductList/index.vue new file mode 100644 index 00000000..3ba63677 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductList/index.vue @@ -0,0 +1,131 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductList/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductList/property.vue new file mode 100644 index 00000000..e9cf7c01 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/ProductList/property.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionArticle/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionArticle/config.ts new file mode 100644 index 00000000..c6270c2a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionArticle/config.ts @@ -0,0 +1,25 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 营销文章属性 */ +export interface PromotionArticleProperty { + // 文章编号 + id: number + // 组件样式 + style: ComponentStyle +} + +// 定义组件 +export const component = { + id: 'PromotionArticle', + name: '营销文章', + icon: 'ph:article-medium', + property: { + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionArticle/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionArticle/index.vue new file mode 100644 index 00000000..cca3635d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionArticle/index.vue @@ -0,0 +1,27 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionArticle/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionArticle/property.vue new file mode 100644 index 00000000..c3bcb21b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionArticle/property.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts new file mode 100644 index 00000000..3ec2a75b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts @@ -0,0 +1,64 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 拼团属性 */ +export interface PromotionCombinationProperty { + // 布局类型:单列 | 三列 + layoutType: 'oneCol' | 'threeCol' + // 商品字段 + fields: { + // 商品名称 + name: PromotionCombinationFieldProperty + // 商品价格 + price: PromotionCombinationFieldProperty + } + // 角标 + badge: { + // 是否显示 + show: boolean + // 角标图片 + imgUrl: string + } + // 上圆角 + borderRadiusTop: number + // 下圆角 + borderRadiusBottom: number + // 间距 + space: number + // 拼团活动编号 + activityId: number + // 组件样式 + style: ComponentStyle +} +// 商品字段 +export interface PromotionCombinationFieldProperty { + // 是否显示 + show: boolean + // 颜色 + color: string +} + +// 定义组件 +export const component = { + id: 'PromotionCombination', + name: '拼团', + icon: 'mdi:account-group', + property: { + activityId: undefined, + layoutType: 'oneCol', + fields: { + name: { show: true, color: '#000' }, + price: { show: true, color: '#ff3000' } + }, + badge: { show: false, imgUrl: '' }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue new file mode 100644 index 00000000..fe6f3a83 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue @@ -0,0 +1,125 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue new file mode 100644 index 00000000..ec09dc45 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts new file mode 100644 index 00000000..800398be --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts @@ -0,0 +1,64 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 秒杀属性 */ +export interface PromotionSeckillProperty { + // 布局类型:单列 | 三列 + layoutType: 'oneCol' | 'threeCol' + // 商品字段 + fields: { + // 商品名称 + name: PromotionSeckillFieldProperty + // 商品价格 + price: PromotionSeckillFieldProperty + } + // 角标 + badge: { + // 是否显示 + show: boolean + // 角标图片 + imgUrl: string + } + // 上圆角 + borderRadiusTop: number + // 下圆角 + borderRadiusBottom: number + // 间距 + space: number + // 秒杀活动编号 + activityId: number + // 组件样式 + style: ComponentStyle +} +// 商品字段 +export interface PromotionSeckillFieldProperty { + // 是否显示 + show: boolean + // 颜色 + color: string +} + +// 定义组件 +export const component = { + id: 'PromotionSeckill', + name: '秒杀', + icon: 'mdi:calendar-time', + property: { + activityId: undefined, + layoutType: 'oneCol', + fields: { + name: { show: true, color: '#000' }, + price: { show: true, color: '#ff3000' } + }, + badge: { show: false, imgUrl: '' }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue new file mode 100644 index 00000000..1b4113b6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue @@ -0,0 +1,125 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue new file mode 100644 index 00000000..87537822 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/SearchBar/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/SearchBar/config.ts new file mode 100644 index 00000000..ef47b27c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/SearchBar/config.ts @@ -0,0 +1,43 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 搜索框属性 */ +export interface SearchProperty { + height: number // 搜索栏高度 + showScan: boolean // 显示扫一扫 + borderRadius: number // 框体样式 + placeholder: string // 占位文字 + placeholderPosition: PlaceholderPosition // 占位文字位置 + backgroundColor: string // 框体颜色 + textColor: string // 字体颜色 + hotKeywords: string[] // 热词 + style: ComponentStyle +} + +// 文字位置 +export type PlaceholderPosition = 'left' | 'center' + +// 定义组件 +export const component = { + id: 'SearchBar', + name: '搜索框', + icon: 'ep:search', + property: { + height: 28, + showScan: false, + borderRadius: 0, + placeholder: '搜索商品', + placeholderPosition: 'left', + backgroundColor: 'rgb(238, 238, 238)', + textColor: 'rgb(150, 151, 153)', + hotKeywords: [], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + paddingTop: 8, + paddingRight: 8, + paddingBottom: 8, + paddingLeft: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/SearchBar/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/SearchBar/index.vue new file mode 100644 index 00000000..9de261ad --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/SearchBar/index.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/SearchBar/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/SearchBar/property.vue new file mode 100644 index 00000000..d121a1e3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/SearchBar/property.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TabBar/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TabBar/config.ts new file mode 100644 index 00000000..6bf80866 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TabBar/config.ts @@ -0,0 +1,97 @@ +import { DiyComponent } from '@/components/DiyEditor/util' + +/** 底部导航菜单属性 */ +export interface TabBarProperty { + // 选项列表 + items: TabBarItemProperty[] + // 主题 + theme: string + // 样式 + style: TabBarStyle +} + +// 选项属性 +export interface TabBarItemProperty { + // 标签文字 + text: string + // 链接 + url: string + // 默认图标链接 + iconUrl: string + // 选中的图标链接 + activeIconUrl: string +} + +// 样式 +export interface TabBarStyle { + // 背景类型 + bgType: 'color' | 'img' + // 背景颜色 + bgColor: string + // 图片链接 + bgImg: string + // 默认颜色 + color: string + // 选中的颜色 + activeColor: string +} + +// 定义组件 +export const component = { + id: 'TabBar', + name: '底部导航', + icon: 'fluent:table-bottom-row-16-filled', + property: { + theme: 'red', + style: { + bgType: 'color', + bgColor: '#fff', + color: '#282828', + activeColor: '#fc4141' + }, + items: [ + { + text: '首页', + url: '/pages/index/index', + iconUrl: 'http://mall.mes.iocoder.cn/static/images/1-001.png', + activeIconUrl: 'http://mall.mes.iocoder.cn/static/images/1-002.png' + }, + { + text: '分类', + url: '/pages/index/category?id=3', + iconUrl: 'http://mall.mes.iocoder.cn/static/images/2-001.png', + activeIconUrl: 'http://mall.mes.iocoder.cn/static/images/2-002.png' + }, + { + text: '购物车', + url: '/pages/index/cart', + iconUrl: 'http://mall.mes.iocoder.cn/static/images/3-001.png', + activeIconUrl: 'http://mall.mes.iocoder.cn/static/images/3-002.png' + }, + { + text: '我的', + url: '/pages/index/user', + iconUrl: 'http://mall.mes.iocoder.cn/static/images/4-001.png', + activeIconUrl: 'http://mall.mes.iocoder.cn/static/images/4-002.png' + } + ] + } +} as DiyComponent + +export const THEME_LIST = [ + { id: 'red', name: '中国红', icon: 'icon-park-twotone:theme', color: '#d10019' }, + { id: 'orange', name: '桔橙', icon: 'icon-park-twotone:theme', color: '#f37b1d' }, + { id: 'gold', name: '明黄', icon: 'icon-park-twotone:theme', color: '#fbbd08' }, + { id: 'green', name: '橄榄绿', icon: 'icon-park-twotone:theme', color: '#8dc63f' }, + { id: 'cyan', name: '天青', icon: 'icon-park-twotone:theme', color: '#1cbbb4' }, + { id: 'blue', name: '海蓝', icon: 'icon-park-twotone:theme', color: '#0081ff' }, + { id: 'purple', name: '姹紫', icon: 'icon-park-twotone:theme', color: '#6739b6' }, + { id: 'brightRed', name: '嫣红', icon: 'icon-park-twotone:theme', color: '#e54d42' }, + { id: 'forestGreen', name: '森绿', icon: 'icon-park-twotone:theme', color: '#39b54a' }, + { id: 'mauve', name: '木槿', icon: 'icon-park-twotone:theme', color: '#9c26b0' }, + { id: 'pink', name: '桃粉', icon: 'icon-park-twotone:theme', color: '#e03997' }, + { id: 'brown', name: '棕褐', icon: 'icon-park-twotone:theme', color: '#a5673f' }, + { id: 'grey', name: '玄灰', icon: 'icon-park-twotone:theme', color: '#8799a3' }, + { id: 'gray', name: '草灰', icon: 'icon-park-twotone:theme', color: '#aaaaaa' }, + { id: 'black', name: '墨黑', icon: 'icon-park-twotone:theme', color: '#333333' } +] diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TabBar/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TabBar/index.vue new file mode 100644 index 00000000..4325d1e3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TabBar/index.vue @@ -0,0 +1,59 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TabBar/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TabBar/property.vue new file mode 100644 index 00000000..a7634a5b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TabBar/property.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TitleBar/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TitleBar/config.ts new file mode 100644 index 00000000..3d486cc3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TitleBar/config.ts @@ -0,0 +1,65 @@ +import { DiyComponent } from '@/components/DiyEditor/util' + +/** 标题栏属性 */ +export interface TitleBarProperty { + // 主标题 + title: string + // 副标题 + description: string + // 标题大小 + titleSize: number + // 描述大小 + descriptionSize: number + // 标题粗细 + titleWeight: number + // 显示位置 + position: 'left' | 'center' + // 描述粗细 + descriptionWeight: number + // 标题颜色 + titleColor: string + // 描述颜色 + descriptionColor: string + // 背景颜色 + backgroundColor: string + // 底部分割线 + showBottomBorder: false + // 查看更多 + more: { + // 是否显示查看更多 + show: false + // 样式选择 + type: 'text' | 'icon' | 'all' + // 自定义文字 + text: string + // 链接 + url: string + } +} + +// 定义组件 +export const component = { + id: 'TitleBar', + name: '标题栏', + icon: 'material-symbols:line-start', + property: { + title: '主标题', + description: '副标题', + titleSize: 16, + descriptionSize: 12, + titleWeight: 400, + position: 'left', + descriptionWeight: 200, + titleColor: 'rgba(50, 50, 51, 10)', + descriptionColor: 'rgba(150, 151, 153, 10)', + backgroundColor: 'rgba(255, 255, 255, 10)', + showBottomBorder: false, + more: { + //查看更多 + show: false, + type: 'icon', + text: '查看更多', + url: '' + } + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TitleBar/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TitleBar/index.vue new file mode 100644 index 00000000..047a6c06 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TitleBar/index.vue @@ -0,0 +1,80 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TitleBar/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TitleBar/property.vue new file mode 100644 index 00000000..941e6d92 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/TitleBar/property.vue @@ -0,0 +1,115 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCard/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCard/config.ts new file mode 100644 index 00000000..7b337761 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCard/config.ts @@ -0,0 +1,21 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 用户卡片属性 */ +export interface UserCardProperty { + // 组件样式 + style: ComponentStyle +} + +// 定义组件 +export const component = { + id: 'UserCard', + name: '用户卡片', + icon: 'mdi:user-card-details', + property: { + style: { + bgType: 'color', + bgColor: '', + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCard/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCard/index.vue new file mode 100644 index 00000000..14b447c6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCard/index.vue @@ -0,0 +1,29 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCard/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCard/property.vue new file mode 100644 index 00000000..43dfad2c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCard/property.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCoupon/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCoupon/config.ts new file mode 100644 index 00000000..92eba9b6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCoupon/config.ts @@ -0,0 +1,23 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 用户卡券属性 */ +export interface UserCouponProperty { + // 组件样式 + style: ComponentStyle +} + +// 定义组件 +export const component = { + id: 'UserCoupon', + name: '用户卡券', + icon: 'ep:ticket', + property: { + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCoupon/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCoupon/index.vue new file mode 100644 index 00000000..27ad310a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCoupon/index.vue @@ -0,0 +1,15 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCoupon/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCoupon/property.vue new file mode 100644 index 00000000..f902e04e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserCoupon/property.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserOrder/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserOrder/config.ts new file mode 100644 index 00000000..f9c5a6db --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserOrder/config.ts @@ -0,0 +1,23 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 用户订单属性 */ +export interface UserOrderProperty { + // 组件样式 + style: ComponentStyle +} + +// 定义组件 +export const component = { + id: 'UserOrder', + name: '用户订单', + icon: 'ep:list', + property: { + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserOrder/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserOrder/index.vue new file mode 100644 index 00000000..450ae548 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserOrder/index.vue @@ -0,0 +1,13 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserOrder/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserOrder/property.vue new file mode 100644 index 00000000..42df7410 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserOrder/property.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserWallet/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserWallet/config.ts new file mode 100644 index 00000000..4e0955f5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserWallet/config.ts @@ -0,0 +1,23 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 用户资产属性 */ +export interface UserWalletProperty { + // 组件样式 + style: ComponentStyle +} + +// 定义组件 +export const component = { + id: 'UserWallet', + name: '用户资产', + icon: 'ep:wallet-filled', + property: { + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserWallet/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserWallet/index.vue new file mode 100644 index 00000000..0efc9371 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserWallet/index.vue @@ -0,0 +1,15 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserWallet/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserWallet/property.vue new file mode 100644 index 00000000..549367e3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/UserWallet/property.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/VideoPlayer/config.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/VideoPlayer/config.ts new file mode 100644 index 00000000..30501cb8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/VideoPlayer/config.ts @@ -0,0 +1,37 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 视频播放属性 */ +export interface VideoPlayerProperty { + // 视频链接 + videoUrl: string + // 封面链接 + posterUrl: string + // 是否自动播放 + autoplay: boolean + // 组件样式 + style: VideoPlayerStyle +} + +// 视频播放样式 +export interface VideoPlayerStyle extends ComponentStyle { + // 视频高度 + height: number +} + +// 定义组件 +export const component = { + id: 'VideoPlayer', + name: '视频播放', + icon: 'ep:video-play', + property: { + videoUrl: '', + posterUrl: '', + autoplay: false, + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + height: 300 + } as ComponentStyle + } +} as DiyComponent diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/VideoPlayer/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/VideoPlayer/index.vue new file mode 100644 index 00000000..fa9a914f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/VideoPlayer/index.vue @@ -0,0 +1,30 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/VideoPlayer/property.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/VideoPlayer/property.vue new file mode 100644 index 00000000..96f317d6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/VideoPlayer/property.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/index.ts new file mode 100644 index 00000000..c0dc67da --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/components/mobile/index.ts @@ -0,0 +1,61 @@ +/* + * 组件注册 + * + * 组件规范: + * 1. 每个子目录就是一个独立的组件,每个目录包括以下三个文件: + * 2. config.ts:组件配置,必选,用于定义组件、组件默认的属性、定义属性的类型 + * 3. index.vue:组件展示,用于展示组件的渲染效果。可以不提供,如 Page(页面设置),只需要属性配置表单即可 + * 4. property.vue:组件属性表单,用于配置组件,必选, + * + * 注: + * 组件ID以config.ts中配置的id为准,与组件目录的名称无关,但还是建议组件目录的名称与组件ID保持一致 + */ + +// 导入组件界面模块 +const viewModules: Record = import.meta.glob('./*/*.vue') +// 导入配置模块 +const configModules: Record = import.meta.glob('./*/config.ts', { eager: true }) + +// 界面模块 +const components = {} +// 组件配置模块 +const componentConfigs = {} + +// 组件界面的类型 +type ViewType = 'index' | 'property' + +/** + * 注册组件的界面模块 + * + * @param componentId 组件ID + * @param configPath 配置模块的文件路径 + * @param viewType 组件界面的类型 + */ +const registerComponentViewModule = ( + componentId: string, + configPath: string, + viewType: ViewType +) => { + const viewPath = configPath.replace('config.ts', `${viewType}.vue`) + const viewModule = viewModules[viewPath] + if (viewModule) { + // 定义异步组件 + components[componentId] = defineAsyncComponent(viewModule) + } +} + +// 注册 +Object.keys(configModules).forEach((modulePath: string) => { + const component = configModules[modulePath].component + const componentId = component?.id + if (componentId) { + // 注册组件 + componentConfigs[componentId] = component + // 注册预览界面 + registerComponentViewModule(componentId, modulePath, 'index') + // 注册属性配置表单 + registerComponentViewModule(`${componentId}Property`, modulePath, 'property') + } +}) + +export { components, componentConfigs } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/index.vue new file mode 100644 index 00000000..44cb10a7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/index.vue @@ -0,0 +1,470 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/util.ts b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/util.ts new file mode 100644 index 00000000..606e53a2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DiyEditor/util.ts @@ -0,0 +1,127 @@ +import { ref, Ref } from 'vue' +import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/PageConfig/config' +import { NavigationBarProperty } from '@/components/DiyEditor/components/mobile/NavigationBar/config' +import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/config' + +// 页面装修组件 +export interface DiyComponent { + // 组件唯一标识 + id: string + // 组件名称 + name: string + // 组件图标 + icon: string + // 组件属性 + property: T +} + +// 页面装修组件库 +export interface DiyComponentLibrary { + // 组件库名称 + name: string + // 是否展开 + extended: boolean + // 组件列表 + components: string[] +} + +// 组件样式 +export interface ComponentStyle { + // 背景类型 + bgType: 'color' | 'img' + // 背景颜色 + bgColor: string + // 背景图片 + bgImg: string + // 外边距 + margin: number + marginTop: number + marginRight: number + marginBottom: number + marginLeft: number + // 内边距 + padding: number + paddingTop: number + paddingRight: number + paddingBottom: number + paddingLeft: number + // 边框圆角 + borderRadius: number + borderTopLeftRadius: number + borderTopRightRadius: number + borderBottomRightRadius: number + borderBottomLeftRadius: number +} + +// 页面配置 +export interface PageConfig { + // 页面属性 + page: PageConfigProperty + // 顶部导航栏属性 + navigationBar: NavigationBarProperty + // 底部导航菜单属性 + tabBar?: TabBarProperty + // 页面组件列表 + components: PageComponent[] +} +// 页面组件,只保留组件ID,组件属性 +export interface PageComponent extends Pick, 'id' | 'property'> {} + +// 属性表单监听 +export function usePropertyForm(modelValue: T, emit: Function): { formData: Ref } { + const formData = ref() + // 监听属性数据变动 + watch( + () => modelValue, + () => { + formData.value = modelValue + }, + { + deep: true, + immediate: true + } + ) + // 监听表单数据变动 + watch( + () => formData.value, + () => { + emit('update:modelValue', formData.value) + }, + { + deep: true + } + ) + + return { formData } +} + +// 页面组件库 +export const PAGE_LIBS = [ + { + name: '基础组件', + extended: true, + components: ['SearchBar', 'NoticeBar', 'MenuSwiper', 'MenuGrid', 'MenuList'] + }, + { + name: '图文组件', + extended: true, + components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider', 'MagicCube'] + }, + { name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] }, + { + name: '用户组件', + extended: true, + components: ['UserCard', 'UserOrder', 'UserWallet', 'UserCoupon'] + }, + { + name: '营销组件', + extended: true, + components: [ + 'PromotionCombination', + 'PromotionSeckill', + 'PromotionPoint', + 'CouponCard', + 'PromotionArticle' + ] + } +] as DiyComponentLibrary[] diff --git a/mes-ui/mes-ui-admin-vue3/src/components/DocAlert/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/DocAlert/index.vue new file mode 100644 index 00000000..3a3feab7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/DocAlert/index.vue @@ -0,0 +1,34 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Echart/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Echart/index.ts new file mode 100644 index 00000000..48220921 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Echart/index.ts @@ -0,0 +1,3 @@ +import Echart from './src/Echart.vue' + +export { Echart } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Echart/src/Echart.vue b/mes-ui/mes-ui-admin-vue3/src/components/Echart/src/Echart.vue new file mode 100644 index 00000000..fd3342dd --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Echart/src/Echart.vue @@ -0,0 +1,115 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Editor/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Editor/index.ts new file mode 100644 index 00000000..3fbf0a9c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Editor/index.ts @@ -0,0 +1,8 @@ +import Editor from './src/Editor.vue' +import { IDomEditor } from '@wangeditor/editor' + +export interface EditorExpose { + getEditorRef: () => Promise +} + +export { Editor } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Editor/src/Editor.vue b/mes-ui/mes-ui-admin-vue3/src/components/Editor/src/Editor.vue new file mode 100644 index 00000000..ec40bca2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Editor/src/Editor.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Error/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Error/index.ts new file mode 100644 index 00000000..a52c6f97 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Error/index.ts @@ -0,0 +1,3 @@ +import Error from './src/Error.vue' + +export { Error } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Error/src/Error.vue b/mes-ui/mes-ui-admin-vue3/src/components/Error/src/Error.vue new file mode 100644 index 00000000..3fd7a176 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Error/src/Error.vue @@ -0,0 +1,58 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Form/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Form/index.ts new file mode 100644 index 00000000..484c7a22 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Form/index.ts @@ -0,0 +1,15 @@ +import Form from './src/Form.vue' +import { ElForm } from 'element-plus' +import { FormSchema, FormSetPropsType } from '@/types/form' + +export interface FormExpose { + setValues: (data: Recordable) => void + setProps: (props: Recordable) => void + delSchema: (field: string) => void + addSchema: (formSchema: FormSchema, index?: number) => void + setSchema: (schemaProps: FormSetPropsType[]) => void + formModel: Recordable + getElFormRef: () => ComponentRef +} + +export { Form } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Form/src/Form.vue b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/Form.vue new file mode 100644 index 00000000..3acc10ab --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/Form.vue @@ -0,0 +1,307 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Form/src/componentMap.ts b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/componentMap.ts new file mode 100644 index 00000000..5af9b40d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/componentMap.ts @@ -0,0 +1,55 @@ +import type { Component } from 'vue' +import { + ElCascader, + ElCheckboxGroup, + ElColorPicker, + ElDatePicker, + ElInput, + ElInputNumber, + ElRadioGroup, + ElRate, + ElSelect, + ElSelectV2, + ElTreeSelect, + ElSlider, + ElSwitch, + ElTimePicker, + ElTimeSelect, + ElTransfer, + ElAutocomplete, + ElDivider +} from 'element-plus' +import { InputPassword } from '@/components/InputPassword' +import { Editor } from '@/components/Editor' +import { UploadImg, UploadImgs, UploadFile } from '@/components/UploadFile' +import { ComponentName } from '@/types/components' + +const componentMap: Recordable = { + Radio: ElRadioGroup, + Checkbox: ElCheckboxGroup, + CheckboxButton: ElCheckboxGroup, + Input: ElInput, + Autocomplete: ElAutocomplete, + InputNumber: ElInputNumber, + Select: ElSelect, + Cascader: ElCascader, + Switch: ElSwitch, + Slider: ElSlider, + TimePicker: ElTimePicker, + DatePicker: ElDatePicker, + Rate: ElRate, + ColorPicker: ElColorPicker, + Transfer: ElTransfer, + Divider: ElDivider, + TimeSelect: ElTimeSelect, + SelectV2: ElSelectV2, + TreeSelect: ElTreeSelect, + RadioButton: ElRadioGroup, + InputPassword: InputPassword, + Editor: Editor, + UploadImg: UploadImg, + UploadImgs: UploadImgs, + UploadFile: UploadFile +} + +export { componentMap } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Form/src/components/useRenderCheckbox.tsx b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/components/useRenderCheckbox.tsx new file mode 100644 index 00000000..e1518395 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/components/useRenderCheckbox.tsx @@ -0,0 +1,26 @@ +import { FormSchema } from '@/types/form' +import { ElCheckbox, ElCheckboxButton } from 'element-plus' +import { defineComponent } from 'vue' + +export const useRenderCheckbox = () => { + const renderCheckboxOptions = (item: FormSchema) => { + // 如果有别名,就取别名 + const labelAlias = item?.componentProps?.optionsAlias?.labelField + const valueAlias = item?.componentProps?.optionsAlias?.valueField + const Com = (item.component === 'Checkbox' ? ElCheckbox : ElCheckboxButton) as ReturnType< + typeof defineComponent + > + return item?.componentProps?.options?.map((option) => { + const { ...other } = option + return ( + + {option[labelAlias || 'label']} + + ) + }) + } + + return { + renderCheckboxOptions + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Form/src/components/useRenderRadio.tsx b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/components/useRenderRadio.tsx new file mode 100644 index 00000000..d1005ca5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/components/useRenderRadio.tsx @@ -0,0 +1,26 @@ +import { FormSchema } from '@/types/form' +import { ElRadio, ElRadioButton } from 'element-plus' +import { defineComponent } from 'vue' + +export const useRenderRadio = () => { + const renderRadioOptions = (item: FormSchema) => { + // 如果有别名,就取别名 + const labelAlias = item?.componentProps?.optionsAlias?.labelField + const valueAlias = item?.componentProps?.optionsAlias?.valueField + const Com = (item.component === 'Radio' ? ElRadio : ElRadioButton) as ReturnType< + typeof defineComponent + > + return item?.componentProps?.options?.map((option) => { + const { ...other } = option + return ( + + {option[labelAlias || 'label']} + + ) + }) + } + + return { + renderRadioOptions + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Form/src/components/useRenderSelect.tsx b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/components/useRenderSelect.tsx new file mode 100644 index 00000000..59b72e68 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/components/useRenderSelect.tsx @@ -0,0 +1,57 @@ +import { FormSchema } from '@/types/form' +import { ComponentOptions } from '@/types/components' +import { ElOption, ElOptionGroup } from 'element-plus' +import { getSlot } from '@/utils/tsxHelper' +import { Slots } from 'vue' + +export const useRenderSelect = (slots: Slots) => { + // 渲染 select options + const renderSelectOptions = (item: FormSchema) => { + // 如果有别名,就取别名 + const labelAlias = item?.componentProps?.optionsAlias?.labelField + return item?.componentProps?.options?.map((option) => { + if (option?.options?.length) { + return ( + + {() => { + return option?.options?.map((v) => { + return renderSelectOptionItem(item, v) + }) + }} + + ) + } else { + return renderSelectOptionItem(item, option) + } + }) + } + + // 渲染 select option item + const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => { + // 如果有别名,就取别名 + const labelAlias = item?.componentProps?.optionsAlias?.labelField + const valueAlias = item?.componentProps?.optionsAlias?.valueField + + const { label, value, ...other } = option + + return ( + + {{ + default: () => + // option 插槽名规则,{field}-option + item?.componentProps?.optionsSlot + ? getSlot(slots, `${item.field}-option`, { item: option }) + : undefined + }} + + ) + } + + return { + renderSelectOptions + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Form/src/helper.ts b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/helper.ts new file mode 100644 index 00000000..cdfc8caa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/helper.ts @@ -0,0 +1,148 @@ +import type { Slots } from 'vue' +import { getSlot } from '@/utils/tsxHelper' +import { PlaceholderModel } from './types' +import { FormSchema } from '@/types/form' +import { ColProps } from '@/types/components' + +/** + * + * @param schema 对应组件数据 + * @returns 返回提示信息对象 + * @description 用于自动设置placeholder + */ +export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => { + const { t } = useI18n() + const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword'] + const selectMap = ['Select', 'SelectV2', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect'] + if (textMap.includes(schema?.component as string)) { + return { + placeholder: t('common.inputText') + schema.label + } + } + if (selectMap.includes(schema?.component as string)) { + // 一些范围选择器 + const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange'] + if ( + twoTextMap.includes( + (schema?.componentProps?.type || schema?.componentProps?.isRange) as string + ) + ) { + return { + startPlaceholder: t('common.startTimeText'), + endPlaceholder: t('common.endTimeText'), + rangeSeparator: '-' + } + } else { + return { + placeholder: t('common.selectText') + schema.label + } + } + } + return {} +} + +/** + * + * @param col 内置栅格 + * @returns 返回栅格属性 + * @description 合并传入进来的栅格属性 + */ +export const setGridProp = (col: ColProps = {}): ColProps => { + const colProps: ColProps = { + // 如果有span,代表用户优先级更高,所以不需要默认栅格 + ...(col.span + ? {} + : { + xs: 24, + sm: 12, + md: 12, + lg: 12, + xl: 12 + }), + ...col + } + return colProps +} + +/** + * + * @param item 传入的组件属性 + * @returns 默认添加 clearable 属性 + */ +export const setComponentProps = (item: FormSchema): Recordable => { + const notNeedClearable = ['ColorPicker'] + const componentProps: Recordable = notNeedClearable.includes(item.component as string) + ? { ...item.componentProps } + : { + clearable: true, + ...item.componentProps + } + // 需要删除额外的属性 + delete componentProps?.slots + return componentProps +} + +/** + * + * @param slots 插槽 + * @param slotsProps 插槽属性 + * @param field 字段名 + */ +export const setItemComponentSlots = ( + slots: Slots, + slotsProps: Recordable = {}, + field: string +): Recordable => { + const slotObj: Recordable = {} + for (const key in slotsProps) { + if (slotsProps[key]) { + // 由于组件有可能重复,需要有一个唯一的前缀 + slotObj[key] = (data: Recordable) => { + return getSlot(slots, `${field}-${key}`, data) + } + } + } + return slotObj +} + +/** + * + * @param schema Form表单结构化数组 + * @param formModel FormModel + * @returns FormModel + * @description 生成对应的formModel + */ +export const initModel = (schema: FormSchema[], formModel: Recordable) => { + const model: Recordable = { ...formModel } + schema.map((v) => { + // 如果是hidden,就删除对应的值 + if (v.hidden) { + delete model[v.field] + } else if (v.component && v.component !== 'Divider') { + const hasField = Reflect.has(model, v.field) + // 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值 + model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : '' + } + }) + return model +} + +/** + * @param slots 插槽 + * @param field 字段名 + * @returns 返回FormIiem插槽 + */ +export const setFormItemSlots = (slots: Slots, field: string): Recordable => { + const slotObj: Recordable = {} + if (slots[`${field}-error`]) { + slotObj['error'] = (data: Recordable) => { + return getSlot(slots, `${field}-error`, data) + } + } + if (slots[`${field}-label`]) { + slotObj['label'] = (data: Recordable) => { + return getSlot(slots, `${field}-label`, data) + } + } + return slotObj +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Form/src/types.ts b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/types.ts new file mode 100644 index 00000000..dcd01e78 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Form/src/types.ts @@ -0,0 +1,17 @@ +import { FormSchema } from '@/types/form' + +export interface PlaceholderModel { + placeholder?: string + startPlaceholder?: string + endPlaceholder?: string + rangeSeparator?: string +} + +export type FormProps = { + schema?: FormSchema[] + isCol?: boolean + model?: Recordable + autoSetPlaceholder?: boolean + isCustom?: boolean + labelWidth?: string | number +} & Recordable diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Highlight/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Highlight/index.ts new file mode 100644 index 00000000..3e2d9ed6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Highlight/index.ts @@ -0,0 +1,3 @@ +import Highlight from './src/Highlight.vue' + +export { Highlight } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Highlight/src/Highlight.vue b/mes-ui/mes-ui-admin-vue3/src/components/Highlight/src/Highlight.vue new file mode 100644 index 00000000..ef923a9a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Highlight/src/Highlight.vue @@ -0,0 +1,65 @@ + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/IFrame/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/IFrame/index.ts new file mode 100644 index 00000000..9f8cf24a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/IFrame/index.ts @@ -0,0 +1,3 @@ +import IFrame from './src/IFrame.vue' + +export { IFrame } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/IFrame/src/IFrame.vue b/mes-ui/mes-ui-admin-vue3/src/components/IFrame/src/IFrame.vue new file mode 100644 index 00000000..19de51a3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/IFrame/src/IFrame.vue @@ -0,0 +1,32 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Icon/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Icon/index.ts new file mode 100644 index 00000000..33d1de38 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Icon/index.ts @@ -0,0 +1,4 @@ +import Icon from './src/Icon.vue' +import IconSelect from './src/IconSelect.vue' + +export { Icon, IconSelect } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Icon/src/Icon.vue b/mes-ui/mes-ui-admin-vue3/src/components/Icon/src/Icon.vue new file mode 100644 index 00000000..4246539f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Icon/src/Icon.vue @@ -0,0 +1,86 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Icon/src/IconSelect.vue b/mes-ui/mes-ui-admin-vue3/src/components/Icon/src/IconSelect.vue new file mode 100644 index 00000000..d4a5b074 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Icon/src/IconSelect.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Icon/src/data.ts b/mes-ui/mes-ui-admin-vue3/src/components/Icon/src/data.ts new file mode 100644 index 00000000..2a4ed5a3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Icon/src/data.ts @@ -0,0 +1,1961 @@ +export const IconJson = { + 'ep:': [ + 'add-location', + 'aim', + 'alarm-clock', + 'apple', + 'arrow-down', + 'arrow-down-bold', + 'arrow-left', + 'arrow-left-bold', + 'arrow-right', + 'arrow-right-bold', + 'arrow-up', + 'arrow-up-bold', + 'avatar', + 'back', + 'baseball', + 'basketball', + 'bell', + 'bell-filled', + 'bicycle', + 'bottom', + 'bottom-left', + 'bottom-right', + 'bowl', + 'box', + 'briefcase', + 'brush', + 'brush-filled', + 'burger', + 'calendar', + 'camera', + 'camera-filled', + 'caret-bottom', + 'caret-left', + 'caret-right', + 'caret-top', + 'cellphone', + 'chat-dot-round', + 'chat-dot-square', + 'chat-line-round', + 'chat-line-square', + 'chat-round', + 'chat-square', + 'check', + 'checked', + 'cherry', + 'chicken', + 'circle-check', + 'circle-check-filled', + 'circle-close', + 'circle-close-filled', + 'circle-plus', + 'circle-plus-filled', + 'clock', + 'close', + 'close-bold', + 'cloudy', + 'coffee', + 'coffee-cup', + 'coin', + 'cold-drink', + 'collection', + 'collection-tag', + 'comment', + 'compass', + 'connection', + 'coordinate', + 'copy-document', + 'cpu', + 'credit-card', + 'crop', + 'd-arrow-left', + 'd-arrow-right', + 'd-caret', + 'data-analysis', + 'data-board', + 'data-line', + 'delete', + 'delete-filled', + 'delete-location', + 'dessert', + 'discount', + 'dish', + 'dish-dot', + 'document', + 'document-add', + 'document-checked', + 'document-copy', + 'document-delete', + 'document-remove', + 'download', + 'drizzling', + 'edit', + 'edit-pen', + 'eleme', + 'eleme-filled', + 'expand', + 'failed', + 'female', + 'files', + 'film', + 'filter', + 'finished', + 'first-aid-kit', + 'flag', + 'fold', + 'folder', + 'folder-add', + 'folder-checked', + 'folder-delete', + 'folder-opened', + 'folder-remove', + 'food', + 'football', + 'fork-spoon', + 'fries', + 'full-screen', + 'goblet', + 'goblet-full', + 'goblet-square', + 'goblet-square-full', + 'goods', + 'goods-filled', + 'grape', + 'grid', + 'guide', + 'headset', + 'help', + 'help-filled', + 'histogram', + 'home-filled', + 'hot-water', + 'house', + 'ice-cream', + 'ice-cream-round', + 'ice-cream-square', + 'ice-drink', + 'ice-tea', + 'info-filled', + 'iphone', + 'key', + 'knife-fork', + 'lightning', + 'link', + 'list', + 'loading', + 'location', + 'location-filled', + 'location-information', + 'lock', + 'lollipop', + 'magic-stick', + 'magnet', + 'male', + 'management', + 'map-location', + 'medal', + 'menu', + 'message', + 'message-box', + 'mic', + 'microphone', + 'milk-tea', + 'minus', + 'money', + 'monitor', + 'moon', + 'moon-night', + 'more', + 'more-filled', + 'mostly-cloudy', + 'mouse', + 'mug', + 'mute', + 'mute-notification', + 'no-smoking', + 'notebook', + 'notification', + 'odometer', + 'office-building', + 'open', + 'operation', + 'opportunity', + 'orange', + 'paperclip', + 'partly-cloudy', + 'pear', + 'phone', + 'phone-filled', + 'picture', + 'picture-filled', + 'picture-rounded', + 'pie-chart', + 'place', + 'platform', + 'plus', + 'pointer', + 'position', + 'postcard', + 'pouring', + 'present', + 'price-tag', + 'printer', + 'promotion', + 'question-filled', + 'rank', + 'reading', + 'reading-lamp', + 'refresh', + 'refresh-left', + 'refresh-right', + 'refrigerator', + 'remove', + 'remove-filled', + 'right', + 'scale-to-original', + 'school', + 'scissor', + 'search', + 'select', + 'sell', + 'semi-select', + 'service', + 'set-up', + 'setting', + 'share', + 'ship', + 'shop', + 'shopping-bag', + 'shopping-cart', + 'shopping-cart-full', + 'smoking', + 'soccer', + 'sold-out', + 'sort', + 'sort-down', + 'sort-up', + 'stamp', + 'star', + 'star-filled', + 'stopwatch', + 'success-filled', + 'sugar', + 'suitcase', + 'sunny', + 'sunrise', + 'sunset', + 'switch', + 'switch-button', + 'takeaway-box', + 'ticket', + 'tickets', + 'timer', + 'toilet-paper', + 'tools', + 'top', + 'top-left', + 'top-right', + 'trend-charts', + 'trophy', + 'turn-off', + 'umbrella', + 'unlock', + 'upload', + 'upload-filled', + 'user', + 'user-filled', + 'van', + 'video-camera', + 'video-camera-filled', + 'video-pause', + 'video-play', + 'view', + 'wallet', + 'wallet-filled', + 'warning', + 'warning-filled', + 'watch', + 'watermelon', + 'wind-power', + 'zoom-in', + 'zoom-out' + ], + 'fa:': [ + '500px', + 'address-book', + 'address-book-o', + 'address-card', + 'address-card-o', + 'adjust', + 'adn', + 'align-center', + 'align-justify', + 'align-left', + 'amazon', + 'ambulance', + 'american-sign-language-interpreting', + 'anchor', + 'android', + 'angellist', + 'angle-double-left', + 'angle-double-up', + 'angle-down', + 'angle-left', + 'angle-up', + 'apple', + 'archive', + 'area-chart', + 'arrow-circle-left', + 'arrow-circle-o-left', + 'arrow-circle-o-up', + 'arrow-circle-up', + 'arrow-left', + 'arrow-up', + 'arrows', + 'arrows-alt', + 'arrows-h', + 'arrows-v', + 'assistive-listening-systems', + 'asterisk', + 'at', + 'audio-description', + 'automobile', + 'backward', + 'balance-scale', + 'ban', + 'bandcamp', + 'bank', + 'bar-chart', + 'barcode', + 'bars', + 'bath', + 'battery', + 'battery-0', + 'battery-1', + 'battery-2', + 'battery-3', + 'bed', + 'beer', + 'behance', + 'behance-square', + 'bell', + 'bell-o', + 'bell-slash', + 'bell-slash-o', + 'bicycle', + 'binoculars', + 'birthday-cake', + 'bitbucket', + 'bitbucket-square', + 'bitcoin', + 'black-tie', + 'blind', + 'bluetooth', + 'bluetooth-b', + 'bold', + 'bolt', + 'bomb', + 'book', + 'bookmark', + 'bookmark-o', + 'braille', + 'briefcase', + 'bug', + 'building', + 'building-o', + 'bullhorn', + 'bullseye', + 'bus', + 'buysellads', + 'cab', + 'calculator', + 'calendar', + 'calendar-check-o', + 'calendar-minus-o', + 'calendar-o', + 'calendar-plus-o', + 'calendar-times-o', + 'camera', + 'camera-retro', + 'caret-down', + 'caret-left', + 'caret-square-o-left', + 'caret-square-o-up', + 'caret-up', + 'cart-arrow-down', + 'cart-plus', + 'cc', + 'cc-amex', + 'cc-diners-club', + 'cc-discover', + 'cc-jcb', + 'cc-mastercard', + 'cc-paypal', + 'cc-stripe', + 'cc-visa', + 'certificate', + 'chain', + 'chain-broken', + 'check', + 'check-circle', + 'check-circle-o', + 'check-square', + 'check-square-o', + 'chevron-circle-left', + 'chevron-circle-up', + 'chevron-down', + 'chevron-left', + 'chevron-up', + 'child', + 'chrome', + 'circle', + 'circle-o', + 'circle-o-notch', + 'circle-thin', + 'clipboard', + 'clock-o', + 'clone', + 'close', + 'cloud', + 'cloud-download', + 'cloud-upload', + 'cny', + 'code', + 'code-fork', + 'codepen', + 'codiepie', + 'coffee', + 'cog', + 'cogs', + 'columns', + 'comment', + 'comment-o', + 'commenting', + 'commenting-o', + 'comments', + 'comments-o', + 'compass', + 'compress', + 'connectdevelop', + 'contao', + 'copy', + 'copyright', + 'creative-commons', + 'credit-card', + 'credit-card-alt', + 'crop', + 'crosshairs', + 'css3', + 'cube', + 'cubes', + 'cut', + 'cutlery', + 'dashboard', + 'dashcube', + 'database', + 'deaf', + 'dedent', + 'delicious', + 'desktop', + 'deviantart', + 'diamond', + 'digg', + 'dollar', + 'dot-circle-o', + 'download', + 'dribbble', + 'drivers-license', + 'drivers-license-o', + 'dropbox', + 'drupal', + 'edge', + 'edit', + 'eercast', + 'eject', + 'ellipsis-h', + 'ellipsis-v', + 'empire', + 'envelope', + 'envelope-o', + 'envelope-open', + 'envelope-open-o', + 'envelope-square', + 'envira', + 'eraser', + 'etsy', + 'eur', + 'exchange', + 'exclamation', + 'exclamation-circle', + 'exclamation-triangle', + 'expand', + 'expeditedssl', + 'external-link', + 'external-link-square', + 'eye', + 'eye-slash', + 'eyedropper', + 'fa', + 'facebook', + 'facebook-official', + 'facebook-square', + 'fast-backward', + 'fax', + 'feed', + 'female', + 'fighter-jet', + 'file', + 'file-archive-o', + 'file-audio-o', + 'file-code-o', + 'file-excel-o', + 'file-image-o', + 'file-movie-o', + 'file-o', + 'file-pdf-o', + 'file-powerpoint-o', + 'file-text', + 'file-text-o', + 'file-word-o', + 'film', + 'filter', + 'fire', + 'fire-extinguisher', + 'firefox', + 'first-order', + 'flag', + 'flag-checkered', + 'flag-o', + 'flask', + 'flickr', + 'floppy-o', + 'folder', + 'folder-o', + 'folder-open', + 'folder-open-o', + 'font', + 'fonticons', + 'fort-awesome', + 'forumbee', + 'foursquare', + 'free-code-camp', + 'frown-o', + 'futbol-o', + 'gamepad', + 'gavel', + 'gbp', + 'genderless', + 'get-pocket', + 'gg', + 'gg-circle', + 'gift', + 'git', + 'git-square', + 'github', + 'github-alt', + 'github-square', + 'gitlab', + 'gittip', + 'glass', + 'glide', + 'glide-g', + 'globe', + 'google', + 'google-plus', + 'google-plus-circle', + 'google-plus-square', + 'google-wallet', + 'graduation-cap', + 'grav', + 'group', + 'h-square', + 'hacker-news', + 'hand-grab-o', + 'hand-lizard-o', + 'hand-o-left', + 'hand-o-up', + 'hand-paper-o', + 'hand-peace-o', + 'hand-pointer-o', + 'hand-scissors-o', + 'hand-spock-o', + 'handshake-o', + 'hashtag', + 'hdd-o', + 'header', + 'headphones', + 'heart', + 'heart-o', + 'heartbeat', + 'history', + 'home', + 'hospital-o', + 'hourglass', + 'hourglass-1', + 'hourglass-2', + 'hourglass-3', + 'hourglass-o', + 'houzz', + 'html5', + 'i-cursor', + 'id-badge', + 'ils', + 'image', + 'imdb', + 'inbox', + 'indent', + 'industry', + 'info', + 'info-circle', + 'inr', + 'instagram', + 'internet-explorer', + 'intersex', + 'ioxhost', + 'italic', + 'joomla', + 'jsfiddle', + 'key', + 'keyboard-o', + 'krw', + 'language', + 'laptop', + 'lastfm', + 'lastfm-square', + 'leaf', + 'leanpub', + 'lemon-o', + 'level-up', + 'life-bouy', + 'lightbulb-o', + 'line-chart', + 'linkedin', + 'linkedin-square', + 'linode', + 'linux', + 'list', + 'list-alt', + 'list-ol', + 'list-ul', + 'location-arrow', + 'lock', + 'long-arrow-left', + 'long-arrow-up', + 'low-vision', + 'magic', + 'magnet', + 'mail-forward', + 'mail-reply', + 'mail-reply-all', + 'male', + 'map', + 'map-marker', + 'map-o', + 'map-pin', + 'map-signs', + 'mars', + 'mars-double', + 'mars-stroke', + 'mars-stroke-h', + 'mars-stroke-v', + 'maxcdn', + 'meanpath', + 'medium', + 'medkit', + 'meetup', + 'meh-o', + 'mercury', + 'microchip', + 'microphone', + 'microphone-slash', + 'minus', + 'minus-circle', + 'minus-square', + 'minus-square-o', + 'mixcloud', + 'mobile', + 'modx', + 'money', + 'moon-o', + 'motorcycle', + 'mouse-pointer', + 'music', + 'neuter', + 'newspaper-o', + 'object-group', + 'object-ungroup', + 'odnoklassniki', + 'odnoklassniki-square', + 'opencart', + 'openid', + 'opera', + 'optin-monster', + 'pagelines', + 'paint-brush', + 'paper-plane', + 'paper-plane-o', + 'paperclip', + 'paragraph', + 'pause', + 'pause-circle', + 'pause-circle-o', + 'paw', + 'paypal', + 'pencil', + 'pencil-square', + 'percent', + 'phone', + 'phone-square', + 'pie-chart', + 'pied-piper', + 'pied-piper-alt', + 'pied-piper-pp', + 'pinterest', + 'pinterest-p', + 'pinterest-square', + 'plane', + 'play', + 'play-circle', + 'play-circle-o', + 'plug', + 'plus', + 'plus-circle', + 'plus-square', + 'plus-square-o', + 'podcast', + 'power-off', + 'print', + 'product-hunt', + 'puzzle-piece', + 'qq', + 'qrcode', + 'question', + 'question-circle', + 'question-circle-o', + 'quora', + 'quote-left', + 'quote-right', + 'ra', + 'random', + 'ravelry', + 'recycle', + 'reddit', + 'reddit-alien', + 'reddit-square', + 'refresh', + 'registered', + 'renren', + 'repeat', + 'retweet', + 'road', + 'rocket', + 'rotate-left', + 'rouble', + 'rss-square', + 'safari', + 'scribd', + 'search', + 'search-minus', + 'search-plus', + 'sellsy', + 'server', + 'share-alt', + 'share-alt-square', + 'share-square', + 'share-square-o', + 'shield', + 'ship', + 'shirtsinbulk', + 'shopping-bag', + 'shopping-basket', + 'shopping-cart', + 'shower', + 'sign-in', + 'sign-language', + 'sign-out', + 'signal', + 'simplybuilt', + 'sitemap', + 'skyatlas', + 'skype', + 'slack', + 'sliders', + 'slideshare', + 'smile-o', + 'snapchat', + 'snapchat-ghost', + 'snapchat-square', + 'snowflake-o', + 'sort', + 'sort-alpha-asc', + 'sort-alpha-desc', + 'sort-amount-asc', + 'sort-amount-desc', + 'sort-asc', + 'sort-numeric-asc', + 'sort-numeric-desc', + 'soundcloud', + 'space-shuttle', + 'spinner', + 'spoon', + 'spotify', + 'square', + 'square-o', + 'stack-exchange', + 'stack-overflow', + 'star', + 'star-half', + 'star-half-empty', + 'star-o', + 'steam', + 'steam-square', + 'step-backward', + 'stethoscope', + 'sticky-note', + 'sticky-note-o', + 'stop', + 'stop-circle', + 'stop-circle-o', + 'street-view', + 'strikethrough', + 'stumbleupon', + 'stumbleupon-circle', + 'subscript', + 'subway', + 'suitcase', + 'sun-o', + 'superpowers', + 'superscript', + 'table', + 'tablet', + 'tag', + 'tags', + 'tasks', + 'telegram', + 'television', + 'tencent-weibo', + 'terminal', + 'text-height', + 'text-width', + 'th', + 'th-large', + 'th-list', + 'themeisle', + 'thermometer', + 'thermometer-0', + 'thermometer-1', + 'thermometer-2', + 'thermometer-3', + 'thumb-tack', + 'thumbs-down', + 'thumbs-o-up', + 'thumbs-up', + 'ticket', + 'times-circle', + 'times-circle-o', + 'times-rectangle', + 'times-rectangle-o', + 'tint', + 'toggle-off', + 'toggle-on', + 'trademark', + 'train', + 'transgender-alt', + 'trash', + 'trash-o', + 'tree', + 'trello', + 'tripadvisor', + 'trophy', + 'truck', + 'try', + 'tty', + 'tumblr', + 'tumblr-square', + 'twitch', + 'twitter', + 'twitter-square', + 'umbrella', + 'underline', + 'universal-access', + 'unlock', + 'unlock-alt', + 'upload', + 'usb', + 'user', + 'user-circle', + 'user-circle-o', + 'user-md', + 'user-o', + 'user-plus', + 'user-secret', + 'user-times', + 'venus', + 'venus-double', + 'venus-mars', + 'viacoin', + 'viadeo', + 'viadeo-square', + 'video-camera', + 'vimeo', + 'vimeo-square', + 'vine', + 'vk', + 'volume-control-phone', + 'volume-down', + 'volume-off', + 'volume-up', + 'wechat', + 'weibo', + 'whatsapp', + 'wheelchair', + 'wheelchair-alt', + 'wifi', + 'wikipedia-w', + 'window-maximize', + 'window-minimize', + 'window-restore', + 'windows', + 'wordpress', + 'wpbeginner', + 'wpexplorer', + 'wpforms', + 'wrench', + 'xing', + 'xing-square', + 'y-combinator', + 'yahoo', + 'yelp', + 'yoast', + 'youtube', + 'youtube-play', + 'youtube-square' + ], + 'fa-solid:': [ + 'abacus', + 'ad', + 'address-book', + 'address-card', + 'adjust', + 'air-freshener', + 'align-center', + 'align-justify', + 'align-left', + 'align-right', + 'allergies', + 'ambulance', + 'american-sign-language-interpreting', + 'anchor', + 'angle-double-down', + 'angle-double-left', + 'angle-double-right', + 'angle-double-up', + 'angle-down', + 'angle-left', + 'angle-right', + 'angle-up', + 'angry', + 'ankh', + 'apple-alt', + 'archive', + 'archway', + 'arrow-alt-circle-down', + 'arrow-alt-circle-left', + 'arrow-alt-circle-right', + 'arrow-alt-circle-up', + 'arrow-circle-down', + 'arrow-circle-left', + 'arrow-circle-right', + 'arrow-circle-up', + 'arrow-down', + 'arrow-left', + 'arrow-right', + 'arrow-up', + 'arrows-alt', + 'arrows-alt-h', + 'arrows-alt-v', + 'assistive-listening-systems', + 'asterisk', + 'at', + 'atlas', + 'atom', + 'audio-description', + 'award', + 'baby', + 'baby-carriage', + 'backspace', + 'backward', + 'bacon', + 'bacteria', + 'bacterium', + 'bahai', + 'balance-scale', + 'balance-scale-left', + 'balance-scale-right', + 'ban', + 'band-aid', + 'barcode', + 'bars', + 'baseball-ball', + 'basketball-ball', + 'bath', + 'battery-empty', + 'battery-full', + 'battery-half', + 'battery-quarter', + 'battery-three-quarters', + 'bed', + 'beer', + 'bell', + 'bell-slash', + 'bezier-curve', + 'bible', + 'bicycle', + 'biking', + 'binoculars', + 'biohazard', + 'birthday-cake', + 'blender', + 'blender-phone', + 'blind', + 'blog', + 'bold', + 'bolt', + 'bomb', + 'bone', + 'bong', + 'book', + 'book-dead', + 'book-medical', + 'book-open', + 'book-reader', + 'bookmark', + 'border-all', + 'border-none', + 'border-style', + 'bowling-ball', + 'box', + 'box-open', + 'box-tissue', + 'boxes', + 'braille', + 'brain', + 'bread-slice', + 'briefcase', + 'briefcase-medical', + 'broadcast-tower', + 'broom', + 'brush', + 'bug', + 'building', + 'bullhorn', + 'bullseye', + 'burn', + 'bus', + 'bus-alt', + 'business-time', + 'calculator', + 'calculator-alt', + 'calendar', + 'calendar-alt', + 'calendar-check', + 'calendar-day', + 'calendar-minus', + 'calendar-plus', + 'calendar-times', + 'calendar-week', + 'camera', + 'camera-retro', + 'campground', + 'candy-cane', + 'cannabis', + 'capsules', + 'car', + 'car-alt', + 'car-battery', + 'car-crash', + 'car-side', + 'caravan', + 'caret-down', + 'caret-left', + 'caret-right', + 'caret-square-down', + 'caret-square-left', + 'caret-square-right', + 'caret-square-up', + 'caret-up', + 'carrot', + 'cart-arrow-down', + 'cart-plus', + 'cash-register', + 'cat', + 'certificate', + 'chair', + 'chalkboard', + 'chalkboard-teacher', + 'charging-station', + 'chart-area', + 'chart-bar', + 'chart-line', + 'chart-pie', + 'check', + 'check-circle', + 'check-double', + 'check-square', + 'cheese', + 'chess', + 'chess-bishop', + 'chess-board', + 'chess-king', + 'chess-knight', + 'chess-pawn', + 'chess-queen', + 'chess-rook', + 'chevron-circle-down', + 'chevron-circle-left', + 'chevron-circle-right', + 'chevron-circle-up', + 'chevron-down', + 'chevron-left', + 'chevron-right', + 'chevron-up', + 'child', + 'church', + 'circle', + 'circle-notch', + 'city', + 'clinic-medical', + 'clipboard', + 'clipboard-check', + 'clipboard-list', + 'clock', + 'clone', + 'closed-captioning', + 'cloud', + 'cloud-download-alt', + 'cloud-meatball', + 'cloud-moon', + 'cloud-moon-rain', + 'cloud-rain', + 'cloud-showers-heavy', + 'cloud-sun', + 'cloud-sun-rain', + 'cloud-upload-alt', + 'cocktail', + 'code', + 'code-branch', + 'coffee', + 'cog', + 'cogs', + 'coins', + 'columns', + 'comment', + 'comment-alt', + 'comment-dollar', + 'comment-dots', + 'comment-medical', + 'comment-slash', + 'comments', + 'comments-dollar', + 'compact-disc', + 'compass', + 'compress', + 'compress-alt', + 'compress-arrows-alt', + 'concierge-bell', + 'cookie', + 'cookie-bite', + 'copy', + 'copyright', + 'couch', + 'credit-card', + 'crop', + 'crop-alt', + 'cross', + 'crosshairs', + 'crow', + 'crown', + 'crutch', + 'cube', + 'cubes', + 'cut', + 'database', + 'deaf', + 'democrat', + 'desktop', + 'dharmachakra', + 'diagnoses', + 'dice', + 'dice-d20', + 'dice-d6', + 'dice-five', + 'dice-four', + 'dice-one', + 'dice-six', + 'dice-three', + 'dice-two', + 'digital-tachograph', + 'directions', + 'disease', + 'divide', + 'dizzy', + 'dna', + 'dog', + 'dollar-sign', + 'dolly', + 'dolly-flatbed', + 'donate', + 'door-closed', + 'door-open', + 'dot-circle', + 'dove', + 'download', + 'drafting-compass', + 'dragon', + 'draw-polygon', + 'drum', + 'drum-steelpan', + 'drumstick-bite', + 'dumbbell', + 'dumpster', + 'dumpster-fire', + 'dungeon', + 'edit', + 'egg', + 'eject', + 'ellipsis-h', + 'ellipsis-v', + 'empty-set', + 'envelope', + 'envelope-open', + 'envelope-open-text', + 'envelope-square', + 'equals', + 'eraser', + 'ethernet', + 'euro-sign', + 'exchange-alt', + 'exclamation', + 'exclamation-circle', + 'exclamation-triangle', + 'expand', + 'expand-alt', + 'expand-arrows-alt', + 'external-link-alt', + 'external-link-square-alt', + 'eye', + 'eye-dropper', + 'eye-slash', + 'fan', + 'fast-backward', + 'fast-forward', + 'faucet', + 'fax', + 'feather', + 'feather-alt', + 'female', + 'fighter-jet', + 'file', + 'file-alt', + 'file-archive', + 'file-audio', + 'file-code', + 'file-contract', + 'file-csv', + 'file-download', + 'file-excel', + 'file-export', + 'file-image', + 'file-import', + 'file-invoice', + 'file-invoice-dollar', + 'file-medical', + 'file-medical-alt', + 'file-pdf', + 'file-powerpoint', + 'file-prescription', + 'file-signature', + 'file-upload', + 'file-video', + 'file-word', + 'fill', + 'fill-drip', + 'film', + 'filter', + 'fingerprint', + 'fire', + 'fire-alt', + 'fire-extinguisher', + 'first-aid', + 'fish', + 'fist-raised', + 'flag', + 'flag-checkered', + 'flag-usa', + 'flask', + 'flushed', + 'folder', + 'folder-minus', + 'folder-open', + 'folder-plus', + 'font', + 'football-ball', + 'forward', + 'frog', + 'frown', + 'frown-open', + 'function', + 'funnel-dollar', + 'futbol', + 'gamepad', + 'gas-pump', + 'gavel', + 'gem', + 'genderless', + 'ghost', + 'gift', + 'gifts', + 'glass-cheers', + 'glass-martini', + 'glass-martini-alt', + 'glass-whiskey', + 'glasses', + 'globe', + 'globe-africa', + 'globe-americas', + 'globe-asia', + 'globe-europe', + 'golf-ball', + 'gopuram', + 'graduation-cap', + 'greater-than', + 'greater-than-equal', + 'grimace', + 'grin', + 'grin-alt', + 'grin-beam', + 'grin-beam-sweat', + 'grin-hearts', + 'grin-squint', + 'grin-squint-tears', + 'grin-stars', + 'grin-tears', + 'grin-tongue', + 'grin-tongue-squint', + 'grin-tongue-wink', + 'grin-wink', + 'grip-horizontal', + 'grip-lines', + 'grip-lines-vertical', + 'grip-vertical', + 'guitar', + 'h-square', + 'hamburger', + 'hammer', + 'hamsa', + 'hand-holding', + 'hand-holding-heart', + 'hand-holding-medical', + 'hand-holding-usd', + 'hand-holding-water', + 'hand-lizard', + 'hand-middle-finger', + 'hand-paper', + 'hand-peace', + 'hand-point-down', + 'hand-point-left', + 'hand-point-right', + 'hand-point-up', + 'hand-pointer', + 'hand-rock', + 'hand-scissors', + 'hand-sparkles', + 'hand-spock', + 'hands', + 'hands-helping', + 'hands-wash', + 'handshake', + 'handshake-alt-slash', + 'handshake-slash', + 'hanukiah', + 'hard-hat', + 'hashtag', + 'hat-cowboy', + 'hat-cowboy-side', + 'hat-wizard', + 'hdd', + 'head-side-cough', + 'head-side-cough-slash', + 'head-side-mask', + 'head-side-virus', + 'heading', + 'headphones', + 'headphones-alt', + 'headset', + 'heart', + 'heart-broken', + 'heartbeat', + 'helicopter', + 'highlighter', + 'hiking', + 'hippo', + 'history', + 'hockey-puck', + 'holly-berry', + 'home', + 'horse', + 'horse-head', + 'hospital', + 'hospital-alt', + 'hospital-symbol', + 'hospital-user', + 'hot-tub', + 'hotdog', + 'hotel', + 'hourglass', + 'hourglass-end', + 'hourglass-half', + 'hourglass-start', + 'house-damage', + 'house-user', + 'hryvnia', + 'i-cursor', + 'ice-cream', + 'icicles', + 'icons', + 'id-badge', + 'id-card', + 'id-card-alt', + 'igloo', + 'image', + 'images', + 'inbox', + 'indent', + 'industry', + 'infinity', + 'info', + 'info-circle', + 'integral', + 'intersection', + 'italic', + 'jedi', + 'joint', + 'journal-whills', + 'kaaba', + 'key', + 'keyboard', + 'khanda', + 'kiss', + 'kiss-beam', + 'kiss-wink-heart', + 'kiwi-bird', + 'lambda', + 'landmark', + 'language', + 'laptop', + 'laptop-code', + 'laptop-house', + 'laptop-medical', + 'laugh', + 'laugh-beam', + 'laugh-squint', + 'laugh-wink', + 'layer-group', + 'leaf', + 'lemon', + 'less-than', + 'less-than-equal', + 'level-down-alt', + 'level-up-alt', + 'life-ring', + 'lightbulb', + 'link', + 'lira-sign', + 'list', + 'list-alt', + 'list-ol', + 'list-ul', + 'location-arrow', + 'lock', + 'lock-open', + 'long-arrow-alt-down', + 'long-arrow-alt-left', + 'long-arrow-alt-right', + 'long-arrow-alt-up', + 'low-vision', + 'luggage-cart', + 'lungs', + 'lungs-virus', + 'magic', + 'magnet', + 'mail-bulk', + 'male', + 'map', + 'map-marked', + 'map-marked-alt', + 'map-marker', + 'map-marker-alt', + 'map-pin', + 'map-signs', + 'marker', + 'mars', + 'mars-double', + 'mars-stroke', + 'mars-stroke-h', + 'mars-stroke-v', + 'mask', + 'medal', + 'medkit', + 'meh', + 'meh-blank', + 'meh-rolling-eyes', + 'memory', + 'menorah', + 'mercury', + 'meteor', + 'microchip', + 'microphone', + 'microphone-alt', + 'microphone-alt-slash', + 'microphone-slash', + 'microscope', + 'minus', + 'minus-circle', + 'minus-square', + 'mitten', + 'mobile', + 'mobile-alt', + 'money-bill', + 'money-bill-alt', + 'money-bill-wave', + 'money-bill-wave-alt', + 'money-check', + 'money-check-alt', + 'monument', + 'moon', + 'mortar-pestle', + 'mosque', + 'motorcycle', + 'mountain', + 'mouse', + 'mouse-pointer', + 'mug-hot', + 'music', + 'network-wired', + 'neuter', + 'newspaper', + 'not-equal', + 'notes-medical', + 'object-group', + 'object-ungroup', + 'oil-can', + 'om', + 'omega', + 'otter', + 'outdent', + 'pager', + 'paint-brush', + 'paint-roller', + 'palette', + 'pallet', + 'paper-plane', + 'paperclip', + 'parachute-box', + 'paragraph', + 'parking', + 'passport', + 'pastafarianism', + 'paste', + 'pause', + 'pause-circle', + 'paw', + 'peace', + 'pen', + 'pen-alt', + 'pen-fancy', + 'pen-nib', + 'pen-square', + 'pencil-alt', + 'pencil-ruler', + 'people-arrows', + 'people-carry', + 'pepper-hot', + 'percent', + 'percentage', + 'person-booth', + 'phone', + 'phone-alt', + 'phone-slash', + 'phone-square', + 'phone-square-alt', + 'phone-volume', + 'photo-video', + 'pi', + 'piggy-bank', + 'pills', + 'pizza-slice', + 'place-of-worship', + 'plane', + 'plane-arrival', + 'plane-departure', + 'plane-slash', + 'play', + 'play-circle', + 'plug', + 'plus', + 'plus-circle', + 'plus-square', + 'podcast', + 'poll', + 'poll-h', + 'poo', + 'poo-storm', + 'poop', + 'portrait', + 'pound-sign', + 'power-off', + 'pray', + 'praying-hands', + 'prescription', + 'prescription-bottle', + 'prescription-bottle-alt', + 'print', + 'procedures', + 'project-diagram', + 'pump-medical', + 'pump-soap', + 'puzzle-piece', + 'qrcode', + 'question', + 'question-circle', + 'quidditch', + 'quote-left', + 'quote-right', + 'quran', + 'radiation', + 'radiation-alt', + 'rainbow', + 'random', + 'receipt', + 'record-vinyl', + 'recycle', + 'redo', + 'redo-alt', + 'registered', + 'remove-format', + 'reply', + 'reply-all', + 'republican', + 'restroom', + 'retweet', + 'ribbon', + 'ring', + 'road', + 'robot', + 'rocket', + 'route', + 'rss', + 'rss-square', + 'ruble-sign', + 'ruler', + 'ruler-combined', + 'ruler-horizontal', + 'ruler-vertical', + 'running', + 'rupee-sign', + 'sad-cry', + 'sad-tear', + 'satellite', + 'satellite-dish', + 'save', + 'school', + 'screwdriver', + 'scroll', + 'sd-card', + 'search', + 'search-dollar', + 'search-location', + 'search-minus', + 'search-plus', + 'seedling', + 'server', + 'shapes', + 'share', + 'share-alt', + 'share-alt-square', + 'share-square', + 'shekel-sign', + 'shield-alt', + 'shield-virus', + 'ship', + 'shipping-fast', + 'shoe-prints', + 'shopping-bag', + 'shopping-basket', + 'shopping-cart', + 'shower', + 'shuttle-van', + 'sigma', + 'sign', + 'sign-in-alt', + 'sign-language', + 'sign-out-alt', + 'signal', + 'signal-alt', + 'signal-alt-slash', + 'signal-slash', + 'signature', + 'sim-card', + 'sink', + 'sitemap', + 'skating', + 'skiing', + 'skiing-nordic', + 'skull', + 'skull-crossbones', + 'slash', + 'sleigh', + 'sliders-h', + 'smile', + 'smile-beam', + 'smile-wink', + 'smog', + 'smoking', + 'smoking-ban', + 'sms', + 'snowboarding', + 'snowflake', + 'snowman', + 'snowplow', + 'soap', + 'socks', + 'solar-panel', + 'sort', + 'sort-alpha-down', + 'sort-alpha-down-alt', + 'sort-alpha-up', + 'sort-alpha-up-alt', + 'sort-amount-down', + 'sort-amount-down-alt', + 'sort-amount-up', + 'sort-amount-up-alt', + 'sort-down', + 'sort-numeric-down', + 'sort-numeric-down-alt', + 'sort-numeric-up', + 'sort-numeric-up-alt', + 'sort-up', + 'spa', + 'space-shuttle', + 'spell-check', + 'spider', + 'spinner', + 'splotch', + 'spray-can', + 'square', + 'square-full', + 'square-root', + 'square-root-alt', + 'stamp', + 'star', + 'star-and-crescent', + 'star-half', + 'star-half-alt', + 'star-of-david', + 'star-of-life', + 'step-backward', + 'step-forward', + 'stethoscope', + 'sticky-note', + 'stop', + 'stop-circle', + 'stopwatch', + 'stopwatch-20', + 'store', + 'store-alt', + 'store-alt-slash', + 'store-slash', + 'stream', + 'street-view', + 'strikethrough', + 'stroopwafel', + 'subscript', + 'subway', + 'suitcase', + 'suitcase-rolling', + 'sun', + 'superscript', + 'surprise', + 'swatchbook', + 'swimmer', + 'swimming-pool', + 'synagogue', + 'sync', + 'sync-alt', + 'syringe', + 'table', + 'table-tennis', + 'tablet', + 'tablet-alt', + 'tablets', + 'tachometer-alt', + 'tag', + 'tags', + 'tally', + 'tape', + 'tasks', + 'taxi', + 'teeth', + 'teeth-open', + 'temperature-high', + 'temperature-low', + 'tenge', + 'terminal', + 'text-height', + 'text-width', + 'th', + 'th-large', + 'th-list', + 'theater-masks', + 'thermometer', + 'thermometer-empty', + 'thermometer-full', + 'thermometer-half', + 'thermometer-quarter', + 'thermometer-three-quarters', + 'theta', + 'thumbs-down', + 'thumbs-up', + 'thumbtack', + 'ticket-alt', + 'tilde', + 'times', + 'times-circle', + 'tint', + 'tint-slash', + 'tired', + 'toggle-off', + 'toggle-on', + 'toilet', + 'toilet-paper', + 'toilet-paper-slash', + 'toolbox', + 'tools', + 'tooth', + 'torah', + 'torii-gate', + 'tractor', + 'trademark', + 'traffic-light', + 'trailer', + 'train', + 'tram', + 'transgender', + 'transgender-alt', + 'trash', + 'trash-alt', + 'trash-restore', + 'trash-restore-alt', + 'tree', + 'trophy', + 'truck', + 'truck-loading', + 'truck-monster', + 'truck-moving', + 'truck-pickup', + 'tshirt', + 'tty', + 'tv', + 'umbrella', + 'umbrella-beach', + 'underline', + 'undo', + 'undo-alt', + 'union', + 'universal-access', + 'university', + 'unlink', + 'unlock', + 'unlock-alt', + 'upload', + 'user', + 'user-alt', + 'user-alt-slash', + 'user-astronaut', + 'user-check', + 'user-circle', + 'user-clock', + 'user-cog', + 'user-edit', + 'user-friends', + 'user-graduate', + 'user-injured', + 'user-lock', + 'user-md', + 'user-minus', + 'user-ninja', + 'user-nurse', + 'user-plus', + 'user-secret', + 'user-shield', + 'user-slash', + 'user-tag', + 'user-tie', + 'user-times', + 'users', + 'users-cog', + 'users-slash', + 'utensil-spoon', + 'utensils', + 'value-absolute', + 'vector-square', + 'venus', + 'venus-double', + 'venus-mars', + 'vest', + 'vest-patches', + 'vial', + 'vials', + 'video', + 'video-slash', + 'vihara', + 'virus', + 'virus-slash', + 'viruses', + 'voicemail', + 'volleyball-ball', + 'volume', + 'volume-down', + 'volume-mute', + 'volume-off', + 'volume-slash', + 'volume-up', + 'vote-yea', + 'vr-cardboard', + 'walking', + 'wallet', + 'warehouse', + 'water', + 'wave-square', + 'weight', + 'weight-hanging', + 'wheelchair', + 'wifi', + 'wifi-slash', + 'wind', + 'window-close', + 'window-maximize', + 'window-minimize', + 'window-restore', + 'wine-bottle', + 'wine-glass', + 'wine-glass-alt', + 'won-sign', + 'wrench', + 'x-ray', + 'yen-sign', + 'yin-yang' + ] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ImageViewer/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/ImageViewer/index.ts new file mode 100644 index 00000000..38681356 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ImageViewer/index.ts @@ -0,0 +1,33 @@ +import ImageViewer from './src/ImageViewer.vue' +import { isClient } from '@/utils/is' +import { createVNode, render, VNode } from 'vue' +import { ImageViewerProps } from './src/types' + +let instance: Nullable = null + +export function createImageViewer(options: ImageViewerProps) { + if (!isClient) return + const { + urlList, + initialIndex = 0, + infinite = true, + hideOnClickModal = false, + appendToBody = false, + zIndex = 2000, + show = true + } = options + + const propsData: Partial = {} + const container = document.createElement('div') + propsData.urlList = urlList + propsData.initialIndex = initialIndex + propsData.infinite = infinite + propsData.hideOnClickModal = hideOnClickModal + propsData.appendToBody = appendToBody + propsData.zIndex = zIndex + propsData.show = show + + document.body.appendChild(container) + instance = createVNode(ImageViewer, propsData) + render(instance, container) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ImageViewer/src/ImageViewer.vue b/mes-ui/mes-ui-admin-vue3/src/components/ImageViewer/src/ImageViewer.vue new file mode 100644 index 00000000..5c4921ed --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ImageViewer/src/ImageViewer.vue @@ -0,0 +1,35 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ImageViewer/src/types.ts b/mes-ui/mes-ui-admin-vue3/src/components/ImageViewer/src/types.ts new file mode 100644 index 00000000..1932d74d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ImageViewer/src/types.ts @@ -0,0 +1,9 @@ +export interface ImageViewerProps { + urlList?: string[] + zIndex?: number + initialIndex?: number + infinite?: boolean + hideOnClickModal?: boolean + appendToBody?: boolean + show?: boolean +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Infotip/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Infotip/index.ts new file mode 100644 index 00000000..413fa5f4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Infotip/index.ts @@ -0,0 +1,3 @@ +import Infotip from './src/Infotip.vue' + +export { Infotip } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Infotip/src/Infotip.vue b/mes-ui/mes-ui-admin-vue3/src/components/Infotip/src/Infotip.vue new file mode 100644 index 00000000..0afd6928 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Infotip/src/Infotip.vue @@ -0,0 +1,54 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/InputPassword/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/InputPassword/index.ts new file mode 100644 index 00000000..1dcc38e9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/InputPassword/index.ts @@ -0,0 +1,3 @@ +import InputPassword from './src/InputPassword.vue' + +export { InputPassword } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/InputPassword/src/InputPassword.vue b/mes-ui/mes-ui-admin-vue3/src/components/InputPassword/src/InputPassword.vue new file mode 100644 index 00000000..b8c93e7d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/InputPassword/src/InputPassword.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/InputWithColor/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/InputWithColor/index.vue new file mode 100644 index 00000000..2bc53172 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/InputWithColor/index.vue @@ -0,0 +1,59 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/MagicCubeEditor/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/MagicCubeEditor/index.vue new file mode 100644 index 00000000..26ea179d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/MagicCubeEditor/index.vue @@ -0,0 +1,270 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/MagicCubeEditor/util.ts b/mes-ui/mes-ui-admin-vue3/src/components/MagicCubeEditor/util.ts new file mode 100644 index 00000000..e7c64658 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/MagicCubeEditor/util.ts @@ -0,0 +1,72 @@ +// 坐标点 +export interface Point { + x: number + y: number +} + +// 矩形 +export interface Rect { + // 左上角 X 轴坐标 + left: number + // 左上角 Y 轴坐标 + top: number + // 右下角 X 轴坐标 + right: number + // 右下角 Y 轴坐标 + bottom: number + // 矩形宽度 + width: number + // 矩形高度 + height: number +} + +/** + * 判断两个矩形是否重叠 + * @param a 矩形 A + * @param b 矩形 B + */ +export const isOverlap = (a: Rect, b: Rect): boolean => { + return ( + a.left < b.left + b.width && + a.left + a.width > b.left && + a.top < b.top + b.height && + a.height + a.top > b.top + ) +} +/** + * 检查坐标点是否在矩形内 + * @param hotArea 矩形 + * @param point 坐标 + */ +export const isContains = (hotArea: Rect, point: Point): boolean => { + return ( + point.x >= hotArea.left && + point.x < hotArea.right && + point.y >= hotArea.top && + point.y < hotArea.bottom + ) +} + +/** + * 在两个坐标点中间,创建一个矩形 + * + * 存在以下情况: + * 1. 两个坐标点是同一个位置,只占一个位置的正方形,宽高都为 1 + * 2. X 轴坐标相同,只占一行的矩形,高度为 1 + * 3. Y 轴坐标相同,只占一列的矩形,宽度为 1 + * 4. 多行多列的矩形 + * + * @param a 坐标点一 + * @param b 坐标点二 + */ +export const createRect = (a: Point, b: Point): Rect => { + // 计算矩形的范围 + const [left, left2] = [a.x, b.x].sort() + const [top, top2] = [a.y, b.y].sort() + const right = left2 + 1 + const bottom = top2 + 1 + const height = bottom - top + const width = right - left + + return { left, right, top, bottom, height, width } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Pagination/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/Pagination/index.vue new file mode 100644 index 00000000..b88997b1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Pagination/index.vue @@ -0,0 +1,87 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Qrcode/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Qrcode/index.ts new file mode 100644 index 00000000..ce461612 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Qrcode/index.ts @@ -0,0 +1,3 @@ +import Qrcode from './src/Qrcode.vue' + +export { Qrcode } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Qrcode/src/Qrcode.vue b/mes-ui/mes-ui-admin-vue3/src/components/Qrcode/src/Qrcode.vue new file mode 100644 index 00000000..f0ce7b79 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Qrcode/src/Qrcode.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/RouterSearch/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/RouterSearch/index.vue new file mode 100644 index 00000000..e9310b8f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/RouterSearch/index.vue @@ -0,0 +1,111 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Search/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Search/index.ts new file mode 100644 index 00000000..fcc6f163 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Search/index.ts @@ -0,0 +1,3 @@ +import Search from './src/Search.vue' + +export { Search } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Search/src/Search.vue b/mes-ui/mes-ui-admin-vue3/src/components/Search/src/Search.vue new file mode 100644 index 00000000..3218a63a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Search/src/Search.vue @@ -0,0 +1,157 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/ShortcutDateRangePicker/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/ShortcutDateRangePicker/index.vue new file mode 100644 index 00000000..117c079a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/ShortcutDateRangePicker/index.vue @@ -0,0 +1,84 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Sticky/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Sticky/index.ts new file mode 100644 index 00000000..5e1de45e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Sticky/index.ts @@ -0,0 +1,3 @@ +import Sticky from './src/Sticky.vue' + +export { Sticky } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Sticky/src/Sticky.vue b/mes-ui/mes-ui-admin-vue3/src/components/Sticky/src/Sticky.vue new file mode 100644 index 00000000..28ecbcb8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Sticky/src/Sticky.vue @@ -0,0 +1,143 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/SummaryCard/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/SummaryCard/index.vue new file mode 100644 index 00000000..52da6da9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/SummaryCard/index.vue @@ -0,0 +1,52 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Table/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Table/index.ts new file mode 100644 index 00000000..689f64a8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Table/index.ts @@ -0,0 +1,12 @@ +import Table from './src/Table.vue' +import { ElTable } from 'element-plus' +import { TableSetPropsType } from '@/types/table' + +export interface TableExpose { + setProps: (props: Recordable) => void + setColumn: (columnProps: TableSetPropsType[]) => void + selections: Recordable[] + elTableRef: ComponentRef +} + +export { Table } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Table/src/Table.vue b/mes-ui/mes-ui-admin-vue3/src/components/Table/src/Table.vue new file mode 100644 index 00000000..279a9fac --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Table/src/Table.vue @@ -0,0 +1,311 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Table/src/helper.ts b/mes-ui/mes-ui-admin-vue3/src/components/Table/src/helper.ts new file mode 100644 index 00000000..d8b34a8a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Table/src/helper.ts @@ -0,0 +1,8 @@ +export const setIndex = (reserveIndex: boolean, index: number, size: number, current: number) => { + const newIndex = index + 1 + if (reserveIndex) { + return size * (current - 1) + newIndex + } else { + return newIndex + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Table/src/types.ts b/mes-ui/mes-ui-admin-vue3/src/components/Table/src/types.ts new file mode 100644 index 00000000..1c7ff765 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Table/src/types.ts @@ -0,0 +1,26 @@ +import { Pagination, TableColumn } from '@/types/table' + +export type TableProps = { + pageSize?: number + currentPage?: number + // 是否多选 + selection?: boolean + // 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip, + showOverflowTooltip?: boolean + // 表头 + columns?: TableColumn[] + // 是否展示分页 + pagination?: Pagination | undefined + // 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key) + reserveSelection?: boolean + // 加载状态 + loading?: boolean + // 是否叠加索引 + reserveIndex?: boolean + // 对齐方式 + align?: 'left' | 'center' | 'right' + // 表头对齐方式 + headerAlign?: 'left' | 'center' | 'right' + data?: Recordable + expand?: boolean +} & Recordable diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Tooltip/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Tooltip/index.ts new file mode 100644 index 00000000..ab66ddff --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Tooltip/index.ts @@ -0,0 +1,3 @@ +import Tooltip from './src/Tooltip.vue' + +export { Tooltip } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Tooltip/src/Tooltip.vue b/mes-ui/mes-ui-admin-vue3/src/components/Tooltip/src/Tooltip.vue new file mode 100644 index 00000000..1a2e09cc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Tooltip/src/Tooltip.vue @@ -0,0 +1,17 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/index.ts new file mode 100644 index 00000000..97c1d665 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/index.ts @@ -0,0 +1,5 @@ +import UploadImg from './src/UploadImg.vue' +import UploadImgs from './src/UploadImgs.vue' +import UploadFile from './src/UploadFile.vue' + +export { UploadImg, UploadImgs, UploadFile } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/src/UploadFile.vue b/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/src/UploadFile.vue new file mode 100644 index 00000000..c1f3e4e2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/src/UploadFile.vue @@ -0,0 +1,195 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/src/UploadImg.vue b/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/src/UploadImg.vue new file mode 100644 index 00000000..101801e6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/src/UploadImg.vue @@ -0,0 +1,276 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/src/UploadImgs.vue b/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/src/UploadImgs.vue new file mode 100644 index 00000000..91bb5e31 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/UploadFile/src/UploadImgs.vue @@ -0,0 +1,309 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Verifition/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/index.ts new file mode 100644 index 00000000..bcfe6d94 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/index.ts @@ -0,0 +1,3 @@ +import Verify from './src/Verify.vue' + +export { Verify } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify.vue b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify.vue new file mode 100644 index 00000000..b7b50486 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify.vue @@ -0,0 +1,441 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify/VerifyPoints.vue b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify/VerifyPoints.vue new file mode 100644 index 00000000..9d04f291 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify/VerifyPoints.vue @@ -0,0 +1,250 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify/VerifySlide.vue b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify/VerifySlide.vue new file mode 100644 index 00000000..f3c7bfea --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify/VerifySlide.vue @@ -0,0 +1,376 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify/index.ts new file mode 100644 index 00000000..0daa63a5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/Verify/index.ts @@ -0,0 +1,4 @@ +import VerifySlide from './VerifySlide.vue' +import VerifyPoints from './VerifyPoints.vue' + +export { VerifySlide, VerifyPoints } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/utils/ase.ts b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/utils/ase.ts new file mode 100644 index 00000000..d2e6b988 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/utils/ase.ts @@ -0,0 +1,14 @@ +import CryptoJS from 'crypto-js' +/** + * @word 要加密的内容 + * @keyWord String 服务器随机返回的关键字 + * */ +export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') { + const key = CryptoJS.enc.Utf8.parse(keyWord) + const srcs = CryptoJS.enc.Utf8.parse(word) + const encrypted = CryptoJS.AES.encrypt(srcs, key, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }) + return encrypted.toString() +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/utils/util.ts b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/utils/util.ts new file mode 100644 index 00000000..15c16270 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/Verifition/src/utils/util.ts @@ -0,0 +1,97 @@ +export function resetSize(vm) { + let img_width, img_height, bar_width, bar_height //图片的宽度、高度,移动条的宽度、高度 + const EmployeeWindow = window as any + const parentWidth = vm.$el.parentNode.offsetWidth || EmployeeWindow.offsetWidth + const parentHeight = vm.$el.parentNode.offsetHeight || EmployeeWindow.offsetHeight + if (vm.imgSize.width.indexOf('%') != -1) { + img_width = (parseInt(vm.imgSize.width) / 100) * parentWidth + 'px' + } else { + img_width = vm.imgSize.width + } + + if (vm.imgSize.height.indexOf('%') != -1) { + img_height = (parseInt(vm.imgSize.height) / 100) * parentHeight + 'px' + } else { + img_height = vm.imgSize.height + } + + if (vm.barSize.width.indexOf('%') != -1) { + bar_width = (parseInt(vm.barSize.width) / 100) * parentWidth + 'px' + } else { + bar_width = vm.barSize.width + } + + if (vm.barSize.height.indexOf('%') != -1) { + bar_height = (parseInt(vm.barSize.height) / 100) * parentHeight + 'px' + } else { + bar_height = vm.barSize.height + } + + return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height } +} + +export const _code_chars = [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z' +] +export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0'] +export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC'] diff --git a/mes-ui/mes-ui-admin-vue3/src/components/VerticalButtonGroup/index.vue b/mes-ui/mes-ui-admin-vue3/src/components/VerticalButtonGroup/index.vue new file mode 100644 index 00000000..9c78ea27 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/VerticalButtonGroup/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/XButton/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/XButton/index.ts new file mode 100644 index 00000000..be0f0d4f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/XButton/index.ts @@ -0,0 +1,4 @@ +import XButton from './src/XButton.vue' +import XTextButton from './src/XTextButton.vue' + +export { XButton, XTextButton } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/XButton/src/XButton.vue b/mes-ui/mes-ui-admin-vue3/src/components/XButton/src/XButton.vue new file mode 100644 index 00000000..40cba1ac --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/XButton/src/XButton.vue @@ -0,0 +1,50 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/XButton/src/XTextButton.vue b/mes-ui/mes-ui-admin-vue3/src/components/XButton/src/XTextButton.vue new file mode 100644 index 00000000..b1a922b3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/XButton/src/XTextButton.vue @@ -0,0 +1,49 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue new file mode 100644 index 00000000..3fe21944 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue @@ -0,0 +1,704 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue new file mode 100644 index 00000000..a7958adb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue @@ -0,0 +1,647 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/index.ts new file mode 100644 index 00000000..85228468 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/index.ts @@ -0,0 +1,8 @@ +import MyProcessDesigner from './ProcessDesigner.vue' + +MyProcessDesigner.install = function (Vue) { + Vue.component(MyProcessDesigner.name, MyProcessDesigner) +} + +// 流程图的设计器,可编辑 +export default MyProcessDesigner diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/index2.ts b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/index2.ts new file mode 100644 index 00000000..ebe8ca78 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/index2.ts @@ -0,0 +1,8 @@ +import MyProcessViewer from './ProcessViewer.vue' + +MyProcessViewer.install = function (Vue) { + Vue.component(MyProcessViewer.name, MyProcessViewer) +} + +// 流程图的查看器,不可编辑 +export default MyProcessViewer diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/contentPadProvider.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/contentPadProvider.js new file mode 100644 index 00000000..87834931 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/contentPadProvider.js @@ -0,0 +1,423 @@ +import { assign, forEach, isArray } from 'min-dash' + +import { is } from 'bpmn-js/lib/util/ModelUtil' + +import { isExpanded, isEventSubProcess } from 'bpmn-js/lib/util/DiUtil' + +import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil' + +import { getChildLanes } from 'bpmn-js/lib/features/modeling/util/LaneUtil' + +import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse' + +/** + * A provider for BPMN 2.0 elements context pad + */ +export default function ContextPadProvider( + config, + injector, + eventBus, + contextPad, + modeling, + elementFactory, + connect, + create, + popupMenu, + canvas, + rules, + translate +) { + config = config || {} + + contextPad.registerProvider(this) + + this._contextPad = contextPad + + this._modeling = modeling + + this._elementFactory = elementFactory + this._connect = connect + this._create = create + this._popupMenu = popupMenu + this._canvas = canvas + this._rules = rules + this._translate = translate + + if (config.autoPlace !== false) { + this._autoPlace = injector.get('autoPlace', false) + } + + eventBus.on('create.end', 250, function (event) { + const context = event.context, + shape = context.shape + + if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) { + return + } + + const entries = contextPad.getEntries(shape) + + if (entries.replace) { + entries.replace.action.click(event, shape) + } + }) +} + +ContextPadProvider.$inject = [ + 'config.contextPad', + 'injector', + 'eventBus', + 'contextPad', + 'modeling', + 'elementFactory', + 'connect', + 'create', + 'popupMenu', + 'canvas', + 'rules', + 'translate', + 'elementRegistry' +] + +ContextPadProvider.prototype.getContextPadEntries = function (element) { + const contextPad = this._contextPad, + modeling = this._modeling, + elementFactory = this._elementFactory, + connect = this._connect, + create = this._create, + popupMenu = this._popupMenu, + canvas = this._canvas, + rules = this._rules, + autoPlace = this._autoPlace, + translate = this._translate + + const actions = {} + + if (element.type === 'label') { + return actions + } + + const businessObject = element.businessObject + + function startConnect(event, element) { + connect.start(event, element) + } + + function removeElement() { + modeling.removeElements([element]) + } + + function getReplaceMenuPosition(element) { + const Y_OFFSET = 5 + + const diagramContainer = canvas.getContainer(), + pad = contextPad.getPad(element).html + + const diagramRect = diagramContainer.getBoundingClientRect(), + padRect = pad.getBoundingClientRect() + + const top = padRect.top - diagramRect.top + const left = padRect.left - diagramRect.left + + const pos = { + x: left, + y: top + padRect.height + Y_OFFSET + } + + return pos + } + + /** + * Create an append action + * + * @param {string} type + * @param {string} className + * @param {string} [title] + * @param {Object} [options] + * + * @return {Object} descriptor + */ + function appendAction(type, className, title, options) { + if (typeof title !== 'string') { + options = title + title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') }) + } + + function appendStart(event, element) { + const shape = elementFactory.createShape(assign({ type: type }, options)) + create.start(event, shape, { + source: element + }) + } + + const append = autoPlace + ? function (event, element) { + const shape = elementFactory.createShape(assign({ type: type }, options)) + + autoPlace.append(element, shape) + } + : appendStart + + return { + group: 'model', + className: className, + title: title, + action: { + dragstart: appendStart, + click: append + } + } + } + + function splitLaneHandler(count) { + return function (event, element) { + // actual split + modeling.splitLane(element, count) + + // refresh context pad after split to + // get rid of split icons + contextPad.open(element, true) + } + } + + if (isAny(businessObject, ['bpmn:Lane', 'bpmn:Participant']) && isExpanded(businessObject)) { + const childLanes = getChildLanes(element) + + assign(actions, { + 'lane-insert-above': { + group: 'lane-insert-above', + className: 'bpmn-icon-lane-insert-above', + title: translate('Add Lane above'), + action: { + click: function (event, element) { + modeling.addLane(element, 'top') + } + } + } + }) + + if (childLanes.length < 2) { + if (element.height >= 120) { + assign(actions, { + 'lane-divide-two': { + group: 'lane-divide', + className: 'bpmn-icon-lane-divide-two', + title: translate('Divide into two Lanes'), + action: { + click: splitLaneHandler(2) + } + } + }) + } + + if (element.height >= 180) { + assign(actions, { + 'lane-divide-three': { + group: 'lane-divide', + className: 'bpmn-icon-lane-divide-three', + title: translate('Divide into three Lanes'), + action: { + click: splitLaneHandler(3) + } + } + }) + } + } + + assign(actions, { + 'lane-insert-below': { + group: 'lane-insert-below', + className: 'bpmn-icon-lane-insert-below', + title: translate('Add Lane below'), + action: { + click: function (event, element) { + modeling.addLane(element, 'bottom') + } + } + } + }) + } + + if (is(businessObject, 'bpmn:FlowNode')) { + if (is(businessObject, 'bpmn:EventBasedGateway')) { + assign(actions, { + 'append.receive-task': appendAction( + 'bpmn:ReceiveTask', + 'bpmn-icon-receive-task', + translate('Append ReceiveTask') + ), + 'append.message-intermediate-event': appendAction( + 'bpmn:IntermediateCatchEvent', + 'bpmn-icon-intermediate-event-catch-message', + translate('Append MessageIntermediateCatchEvent'), + { eventDefinitionType: 'bpmn:MessageEventDefinition' } + ), + 'append.timer-intermediate-event': appendAction( + 'bpmn:IntermediateCatchEvent', + 'bpmn-icon-intermediate-event-catch-timer', + translate('Append TimerIntermediateCatchEvent'), + { eventDefinitionType: 'bpmn:TimerEventDefinition' } + ), + 'append.condition-intermediate-event': appendAction( + 'bpmn:IntermediateCatchEvent', + 'bpmn-icon-intermediate-event-catch-condition', + translate('Append ConditionIntermediateCatchEvent'), + { eventDefinitionType: 'bpmn:ConditionalEventDefinition' } + ), + 'append.signal-intermediate-event': appendAction( + 'bpmn:IntermediateCatchEvent', + 'bpmn-icon-intermediate-event-catch-signal', + translate('Append SignalIntermediateCatchEvent'), + { eventDefinitionType: 'bpmn:SignalEventDefinition' } + ) + }) + } else if ( + isEventType(businessObject, 'bpmn:BoundaryEvent', 'bpmn:CompensateEventDefinition') + ) { + assign(actions, { + 'append.compensation-activity': appendAction( + 'bpmn:Task', + 'bpmn-icon-task', + translate('Append compensation activity'), + { + isForCompensation: true + } + ) + }) + } else if ( + !is(businessObject, 'bpmn:EndEvent') && + !businessObject.isForCompensation && + !isEventType(businessObject, 'bpmn:IntermediateThrowEvent', 'bpmn:LinkEventDefinition') && + !isEventSubProcess(businessObject) + ) { + assign(actions, { + 'append.end-event': appendAction( + 'bpmn:EndEvent', + 'bpmn-icon-end-event-none', + translate('Append EndEvent') + ), + 'append.gateway': appendAction( + 'bpmn:ExclusiveGateway', + 'bpmn-icon-gateway-none', + translate('Append Gateway') + ), + 'append.append-task': appendAction( + 'bpmn:UserTask', + 'bpmn-icon-user-task', + translate('Append Task') + ), + 'append.intermediate-event': appendAction( + 'bpmn:IntermediateThrowEvent', + 'bpmn-icon-intermediate-event-none', + translate('Append Intermediate/Boundary Event') + ) + }) + } + } + + if (!popupMenu.isEmpty(element, 'bpmn-replace')) { + // Replace menu entry + assign(actions, { + replace: { + group: 'edit', + className: 'bpmn-icon-screw-wrench', + title: '修改类型', + action: { + click: function (event, element) { + const position = assign(getReplaceMenuPosition(element), { + cursor: { x: event.x, y: event.y } + }) + + popupMenu.open(element, 'bpmn-replace', position) + } + } + } + }) + } + + if ( + isAny(businessObject, [ + 'bpmn:FlowNode', + 'bpmn:InteractionNode', + 'bpmn:DataObjectReference', + 'bpmn:DataStoreReference' + ]) + ) { + assign(actions, { + 'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation'), + + connect: { + group: 'connect', + className: 'bpmn-icon-connection-multi', + title: translate( + 'Connect using ' + + (businessObject.isForCompensation ? '' : 'Sequence/MessageFlow or ') + + 'Association' + ), + action: { + click: startConnect, + dragstart: startConnect + } + } + }) + } + + if (isAny(businessObject, ['bpmn:DataObjectReference', 'bpmn:DataStoreReference'])) { + assign(actions, { + connect: { + group: 'connect', + className: 'bpmn-icon-connection-multi', + title: translate('Connect using DataInputAssociation'), + action: { + click: startConnect, + dragstart: startConnect + } + } + }) + } + + if (is(businessObject, 'bpmn:Group')) { + assign(actions, { + 'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation') + }) + } + + // delete element entry, only show if allowed by rules + let deleteAllowed = rules.allowed('elements.delete', { elements: [element] }) + + if (isArray(deleteAllowed)) { + // was the element returned as a deletion candidate? + deleteAllowed = deleteAllowed[0] === element + } + + if (deleteAllowed) { + assign(actions, { + delete: { + group: 'edit', + className: 'bpmn-icon-trash', + title: translate('Remove'), + action: { + click: removeElement + } + } + }) + } + + return actions +} + +// helpers ///////// + +function isEventType(eventBo, type, definition) { + const isType = eventBo.$instanceOf(type) + let isDefinition = false + + const definitions = eventBo.eventDefinitions || [] + forEach(definitions, function (def) { + if (def.$type === definition) { + isDefinition = true + } + }) + + return isType && isDefinition +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/index.js new file mode 100644 index 00000000..80009efc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/index.js @@ -0,0 +1,6 @@ +import CustomContextPadProvider from './contentPadProvider' + +export default { + __init__: ['contextPadProvider'], + contextPadProvider: ['type', CustomContextPadProvider] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/defaultEmpty.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/defaultEmpty.js new file mode 100644 index 00000000..f3bc894f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/defaultEmpty.js @@ -0,0 +1,24 @@ +export default (key, name, type) => { + if (!type) type = 'camunda' + const TYPE_TARGET = { + activiti: 'http://activiti.org/bpmn', + camunda: 'http://bpmn.io/schema/bpmn', + flowable: 'http://flowable.org/bpmn' + } + return ` + + + + + + + +` +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json new file mode 100644 index 00000000..db5e4901 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json @@ -0,0 +1,994 @@ +{ + "name": "Activiti", + "uri": "http://activiti.org/bpmn", + "prefix": "activiti", + "xml": { + "tagAlias": "lowerCase" + }, + "associations": [], + "types": [ + { + "name": "Definitions", + "isAbstract": true, + "extends": ["bpmn:Definitions"], + "properties": [ + { + "name": "diagramRelationId", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "InOutBinding", + "superClass": ["Element"], + "isAbstract": true, + "properties": [ + { + "name": "source", + "isAttr": true, + "type": "String" + }, + { + "name": "sourceExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "target", + "isAttr": true, + "type": "String" + }, + { + "name": "businessKey", + "isAttr": true, + "type": "String" + }, + { + "name": "local", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "variables", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "In", + "superClass": ["InOutBinding"], + "meta": { + "allowedIn": ["bpmn:CallActivity"] + } + }, + { + "name": "Out", + "superClass": ["InOutBinding"], + "meta": { + "allowedIn": ["bpmn:CallActivity"] + } + }, + { + "name": "AsyncCapable", + "isAbstract": true, + "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"], + "properties": [ + { + "name": "async", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "asyncBefore", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "asyncAfter", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "exclusive", + "isAttr": true, + "type": "Boolean", + "default": true + } + ] + }, + { + "name": "JobPriorized", + "isAbstract": true, + "extends": ["bpmn:Process", "activiti:AsyncCapable"], + "properties": [ + { + "name": "jobPriority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "SignalEventDefinition", + "isAbstract": true, + "extends": ["bpmn:SignalEventDefinition"], + "properties": [ + { + "name": "async", + "isAttr": true, + "type": "Boolean", + "default": false + } + ] + }, + { + "name": "ErrorEventDefinition", + "isAbstract": true, + "extends": ["bpmn:ErrorEventDefinition"], + "properties": [ + { + "name": "errorCodeVariable", + "isAttr": true, + "type": "String" + }, + { + "name": "errorMessageVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Error", + "isAbstract": true, + "extends": ["bpmn:Error"], + "properties": [ + { + "name": "activiti:errorMessage", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "PotentialStarter", + "superClass": ["Element"], + "properties": [ + { + "name": "resourceAssignmentExpression", + "type": "bpmn:ResourceAssignmentExpression" + } + ] + }, + { + "name": "FormSupported", + "isAbstract": true, + "extends": ["bpmn:StartEvent", "bpmn:UserTask"], + "properties": [ + { + "name": "formHandlerClass", + "isAttr": true, + "type": "String" + }, + { + "name": "formKey", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "TemplateSupported", + "isAbstract": true, + "extends": ["bpmn:Process", "bpmn:FlowElement"], + "properties": [ + { + "name": "modelerTemplate", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Initiator", + "isAbstract": true, + "extends": ["bpmn:StartEvent"], + "properties": [ + { + "name": "initiator", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ScriptTask", + "isAbstract": true, + "extends": ["bpmn:ScriptTask"], + "properties": [ + { + "name": "resultVariable", + "isAttr": true, + "type": "String" + }, + { + "name": "resource", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Process", + "isAbstract": true, + "extends": ["bpmn:Process"], + "properties": [ + { + "name": "candidateStarterGroups", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateStarterUsers", + "isAttr": true, + "type": "String" + }, + { + "name": "versionTag", + "isAttr": true, + "type": "String" + }, + { + "name": "historyTimeToLive", + "isAttr": true, + "type": "String" + }, + { + "name": "isStartableInTasklist", + "isAttr": true, + "type": "Boolean", + "default": true + }, + { + "name": "executionListener", + "isAbstract": true, + "type": "Expression" + } + ] + }, + { + "name": "EscalationEventDefinition", + "isAbstract": true, + "extends": ["bpmn:EscalationEventDefinition"], + "properties": [ + { + "name": "escalationCodeVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "FormalExpression", + "isAbstract": true, + "extends": ["bpmn:FormalExpression"], + "properties": [ + { + "name": "resource", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "multiinstance_type", + "superClass": ["Element"] + }, + { + "name": "multiinstance_condition", + "superClass": ["Element"] + }, + { + "name": "Assignable", + "extends": ["bpmn:UserTask"], + "properties": [ + { + "name": "assignee", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateUsers", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateGroups", + "isAttr": true, + "type": "String" + }, + { + "name": "dueDate", + "isAttr": true, + "type": "String" + }, + { + "name": "followUpDate", + "isAttr": true, + "type": "String" + }, + { + "name": "priority", + "isAttr": true, + "type": "String" + }, + { + "name": "multiinstance_condition", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "CallActivity", + "extends": ["bpmn:CallActivity"], + "properties": [ + { + "name": "calledElementBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "calledElementVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "calledElementVersionTag", + "isAttr": true, + "type": "String" + }, + { + "name": "calledElementTenantId", + "isAttr": true, + "type": "String" + }, + { + "name": "caseRef", + "isAttr": true, + "type": "String" + }, + { + "name": "caseBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "caseVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "caseTenantId", + "isAttr": true, + "type": "String" + }, + { + "name": "variableMappingClass", + "isAttr": true, + "type": "String" + }, + { + "name": "variableMappingDelegateExpression", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ServiceTaskLike", + "extends": [ + "bpmn:ServiceTask", + "bpmn:BusinessRuleTask", + "bpmn:SendTask", + "bpmn:MessageEventDefinition" + ], + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "resultVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "DmnCapable", + "extends": ["bpmn:BusinessRuleTask"], + "properties": [ + { + "name": "decisionRef", + "isAttr": true, + "type": "String" + }, + { + "name": "decisionRefBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "decisionRefVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "mapDecisionResult", + "isAttr": true, + "type": "String", + "default": "resultList" + }, + { + "name": "decisionRefTenantId", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ExternalCapable", + "extends": ["activiti:ServiceTaskLike"], + "properties": [ + { + "name": "type", + "isAttr": true, + "type": "String" + }, + { + "name": "topic", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "TaskPriorized", + "extends": ["bpmn:Process", "activiti:ExternalCapable"], + "properties": [ + { + "name": "taskPriority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Properties", + "superClass": ["Element"], + "meta": { + "allowedIn": ["*"] + }, + "properties": [ + { + "name": "values", + "type": "Property", + "isMany": true + } + ] + }, + { + "name": "Property", + "superClass": ["Element"], + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "value", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "Connector", + "superClass": ["Element"], + "meta": { + "allowedIn": ["activiti:ServiceTaskLike"] + }, + "properties": [ + { + "name": "inputOutput", + "type": "InputOutput" + }, + { + "name": "connectorId", + "type": "String" + } + ] + }, + { + "name": "InputOutput", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:FlowNode", "activiti:Connector"] + }, + "properties": [ + { + "name": "inputOutput", + "type": "InputOutput" + }, + { + "name": "connectorId", + "type": "String" + }, + { + "name": "inputParameters", + "isMany": true, + "type": "InputParameter" + }, + { + "name": "outputParameters", + "isMany": true, + "type": "OutputParameter" + } + ] + }, + { + "name": "InputOutputParameter", + "properties": [ + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + }, + { + "name": "definition", + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "InputOutputParameterDefinition", + "isAbstract": true + }, + { + "name": "List", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "items", + "isMany": true, + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "Map", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "entries", + "isMany": true, + "type": "Entry" + } + ] + }, + { + "name": "Entry", + "properties": [ + { + "name": "key", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + }, + { + "name": "definition", + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "Value", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "id", + "isAttr": true, + "type": "String" + }, + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "Script", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "scriptFormat", + "isAttr": true, + "type": "String" + }, + { + "name": "resource", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "Field", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "activiti:ServiceTaskLike", + "activiti:ExecutionListener", + "activiti:TaskListener" + ] + }, + "properties": [ + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "expression", + "type": "String" + }, + { + "name": "stringValue", + "isAttr": true, + "type": "String" + }, + { + "name": "string", + "type": "String" + } + ] + }, + { + "name": "InputParameter", + "superClass": ["InputOutputParameter"] + }, + { + "name": "OutputParameter", + "superClass": ["InputOutputParameter"] + }, + { + "name": "Collectable", + "isAbstract": true, + "extends": ["bpmn:MultiInstanceLoopCharacteristics"], + "superClass": ["activiti:AsyncCapable"], + "properties": [ + { + "name": "collection", + "isAttr": true, + "type": "String" + }, + { + "name": "elementVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "FailedJobRetryTimeCycle", + "superClass": ["Element"], + "meta": { + "allowedIn": ["activiti:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"] + }, + "properties": [ + { + "name": "body", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "ExecutionListener", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "bpmn:Task", + "bpmn:ServiceTask", + "bpmn:UserTask", + "bpmn:BusinessRuleTask", + "bpmn:ScriptTask", + "bpmn:ReceiveTask", + "bpmn:ManualTask", + "bpmn:ExclusiveGateway", + "bpmn:SequenceFlow", + "bpmn:ParallelGateway", + "bpmn:InclusiveGateway", + "bpmn:EventBasedGateway", + "bpmn:StartEvent", + "bpmn:IntermediateCatchEvent", + "bpmn:IntermediateThrowEvent", + "bpmn:EndEvent", + "bpmn:BoundaryEvent", + "bpmn:CallActivity", + "bpmn:SubProcess", + "bpmn:Process" + ] + }, + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "event", + "isAttr": true, + "type": "String" + }, + { + "name": "script", + "type": "Script" + }, + { + "name": "fields", + "type": "Field", + "isMany": true + } + ] + }, + { + "name": "TaskListener", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "event", + "isAttr": true, + "type": "String" + }, + { + "name": "script", + "type": "Script" + }, + { + "name": "fields", + "type": "Field", + "isMany": true + } + ] + }, + { + "name": "FormProperty", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"] + }, + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "required", + "type": "String", + "isAttr": true + }, + { + "name": "readable", + "type": "String", + "isAttr": true + }, + { + "name": "writable", + "type": "String", + "isAttr": true + }, + { + "name": "variable", + "type": "String", + "isAttr": true + }, + { + "name": "expression", + "type": "String", + "isAttr": true + }, + { + "name": "datePattern", + "type": "String", + "isAttr": true + }, + { + "name": "default", + "type": "String", + "isAttr": true + }, + { + "name": "values", + "type": "Value", + "isMany": true + } + ] + }, + { + "name": "FormProperty", + "superClass": ["Element"], + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "label", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "datePattern", + "type": "String", + "isAttr": true + }, + { + "name": "defaultValue", + "type": "String", + "isAttr": true + }, + { + "name": "properties", + "type": "Properties" + }, + { + "name": "validation", + "type": "Validation" + }, + { + "name": "values", + "type": "Value", + "isMany": true + } + ] + }, + { + "name": "Validation", + "superClass": ["Element"], + "properties": [ + { + "name": "constraints", + "type": "Constraint", + "isMany": true + } + ] + }, + { + "name": "Constraint", + "superClass": ["Element"], + "properties": [ + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "config", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "ConditionalEventDefinition", + "isAbstract": true, + "extends": ["bpmn:ConditionalEventDefinition"], + "properties": [ + { + "name": "variableName", + "isAttr": true, + "type": "String" + }, + { + "name": "variableEvent", + "isAttr": true, + "type": "String" + } + ] + } + ], + "emumerations": [] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json new file mode 100644 index 00000000..79b86bca --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json @@ -0,0 +1,1010 @@ +{ + "name": "Camunda", + "uri": "http://camunda.org/schema/1.0/bpmn", + "prefix": "camunda", + "xml": { + "tagAlias": "lowerCase" + }, + "associations": [], + "types": [ + { + "name": "Definitions", + "isAbstract": true, + "extends": ["bpmn:Definitions"], + "properties": [ + { + "name": "diagramRelationId", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "InOutBinding", + "superClass": ["Element"], + "isAbstract": true, + "properties": [ + { + "name": "source", + "isAttr": true, + "type": "String" + }, + { + "name": "sourceExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "target", + "isAttr": true, + "type": "String" + }, + { + "name": "businessKey", + "isAttr": true, + "type": "String" + }, + { + "name": "local", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "variables", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "In", + "superClass": ["InOutBinding"], + "meta": { + "allowedIn": ["bpmn:CallActivity", "bpmn:SignalEventDefinition"] + } + }, + { + "name": "Out", + "superClass": ["InOutBinding"], + "meta": { + "allowedIn": ["bpmn:CallActivity"] + } + }, + { + "name": "AsyncCapable", + "isAbstract": true, + "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"], + "properties": [ + { + "name": "async", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "asyncBefore", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "asyncAfter", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "exclusive", + "isAttr": true, + "type": "Boolean", + "default": true + } + ] + }, + { + "name": "JobPriorized", + "isAbstract": true, + "extends": ["bpmn:Process", "camunda:AsyncCapable"], + "properties": [ + { + "name": "jobPriority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "SignalEventDefinition", + "isAbstract": true, + "extends": ["bpmn:SignalEventDefinition"], + "properties": [ + { + "name": "async", + "isAttr": true, + "type": "Boolean", + "default": false + } + ] + }, + { + "name": "ErrorEventDefinition", + "isAbstract": true, + "extends": ["bpmn:ErrorEventDefinition"], + "properties": [ + { + "name": "errorCodeVariable", + "isAttr": true, + "type": "String" + }, + { + "name": "errorMessageVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Error", + "isAbstract": true, + "extends": ["bpmn:Error"], + "properties": [ + { + "name": "camunda:errorMessage", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "PotentialStarter", + "superClass": ["Element"], + "properties": [ + { + "name": "resourceAssignmentExpression", + "type": "bpmn:ResourceAssignmentExpression" + } + ] + }, + { + "name": "FormSupported", + "isAbstract": true, + "extends": ["bpmn:StartEvent", "bpmn:UserTask"], + "properties": [ + { + "name": "formHandlerClass", + "isAttr": true, + "type": "String" + }, + { + "name": "formKey", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "TemplateSupported", + "isAbstract": true, + "extends": ["bpmn:Process", "bpmn:FlowElement"], + "properties": [ + { + "name": "modelerTemplate", + "isAttr": true, + "type": "String" + }, + { + "name": "modelerTemplateVersion", + "isAttr": true, + "type": "Integer" + } + ] + }, + { + "name": "Initiator", + "isAbstract": true, + "extends": ["bpmn:StartEvent"], + "properties": [ + { + "name": "initiator", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ScriptTask", + "isAbstract": true, + "extends": ["bpmn:ScriptTask"], + "properties": [ + { + "name": "resultVariable", + "isAttr": true, + "type": "String" + }, + { + "name": "resource", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Process", + "isAbstract": true, + "extends": ["bpmn:Process"], + "properties": [ + { + "name": "candidateStarterGroups", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateStarterUsers", + "isAttr": true, + "type": "String" + }, + { + "name": "versionTag", + "isAttr": true, + "type": "String" + }, + { + "name": "historyTimeToLive", + "isAttr": true, + "type": "String" + }, + { + "name": "isStartableInTasklist", + "isAttr": true, + "type": "Boolean", + "default": true + } + ] + }, + { + "name": "EscalationEventDefinition", + "isAbstract": true, + "extends": ["bpmn:EscalationEventDefinition"], + "properties": [ + { + "name": "escalationCodeVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "FormalExpression", + "isAbstract": true, + "extends": ["bpmn:FormalExpression"], + "properties": [ + { + "name": "resource", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Assignable", + "extends": ["bpmn:UserTask"], + "properties": [ + { + "name": "assignee", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateUsers", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateGroups", + "isAttr": true, + "type": "String" + }, + { + "name": "dueDate", + "isAttr": true, + "type": "String" + }, + { + "name": "followUpDate", + "isAttr": true, + "type": "String" + }, + { + "name": "priority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "CallActivity", + "extends": ["bpmn:CallActivity"], + "properties": [ + { + "name": "calledElementBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "calledElementVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "calledElementVersionTag", + "isAttr": true, + "type": "String" + }, + { + "name": "calledElementTenantId", + "isAttr": true, + "type": "String" + }, + { + "name": "caseRef", + "isAttr": true, + "type": "String" + }, + { + "name": "caseBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "caseVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "caseTenantId", + "isAttr": true, + "type": "String" + }, + { + "name": "variableMappingClass", + "isAttr": true, + "type": "String" + }, + { + "name": "variableMappingDelegateExpression", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ServiceTaskLike", + "extends": [ + "bpmn:ServiceTask", + "bpmn:BusinessRuleTask", + "bpmn:SendTask", + "bpmn:MessageEventDefinition" + ], + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "resultVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "DmnCapable", + "extends": ["bpmn:BusinessRuleTask"], + "properties": [ + { + "name": "decisionRef", + "isAttr": true, + "type": "String" + }, + { + "name": "decisionRefBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "decisionRefVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "mapDecisionResult", + "isAttr": true, + "type": "String", + "default": "resultList" + }, + { + "name": "decisionRefTenantId", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ExternalCapable", + "extends": ["camunda:ServiceTaskLike"], + "properties": [ + { + "name": "type", + "isAttr": true, + "type": "String" + }, + { + "name": "topic", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "TaskPriorized", + "extends": ["bpmn:Process", "camunda:ExternalCapable"], + "properties": [ + { + "name": "taskPriority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Properties", + "superClass": ["Element"], + "meta": { + "allowedIn": ["*"] + }, + "properties": [ + { + "name": "values", + "type": "Property", + "isMany": true + } + ] + }, + { + "name": "Property", + "superClass": ["Element"], + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "value", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "Connector", + "superClass": ["Element"], + "meta": { + "allowedIn": ["camunda:ServiceTaskLike"] + }, + "properties": [ + { + "name": "inputOutput", + "type": "InputOutput" + }, + { + "name": "connectorId", + "type": "String" + } + ] + }, + { + "name": "InputOutput", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:FlowNode", "camunda:Connector"] + }, + "properties": [ + { + "name": "inputOutput", + "type": "InputOutput" + }, + { + "name": "connectorId", + "type": "String" + }, + { + "name": "inputParameters", + "isMany": true, + "type": "InputParameter" + }, + { + "name": "outputParameters", + "isMany": true, + "type": "OutputParameter" + } + ] + }, + { + "name": "InputOutputParameter", + "properties": [ + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + }, + { + "name": "definition", + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "InputOutputParameterDefinition", + "isAbstract": true + }, + { + "name": "List", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "items", + "isMany": true, + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "Map", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "entries", + "isMany": true, + "type": "Entry" + } + ] + }, + { + "name": "Entry", + "properties": [ + { + "name": "key", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + }, + { + "name": "definition", + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "Value", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "id", + "isAttr": true, + "type": "String" + }, + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "Script", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "scriptFormat", + "isAttr": true, + "type": "String" + }, + { + "name": "resource", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "Field", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "camunda:ServiceTaskLike", + "camunda:ExecutionListener", + "camunda:TaskListener" + ] + }, + "properties": [ + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "expression", + "type": "String" + }, + { + "name": "stringValue", + "isAttr": true, + "type": "String" + }, + { + "name": "string", + "type": "String" + } + ] + }, + { + "name": "InputParameter", + "superClass": ["InputOutputParameter"] + }, + { + "name": "OutputParameter", + "superClass": ["InputOutputParameter"] + }, + { + "name": "Collectable", + "isAbstract": true, + "extends": ["bpmn:MultiInstanceLoopCharacteristics"], + "superClass": ["camunda:AsyncCapable"], + "properties": [ + { + "name": "collection", + "isAttr": true, + "type": "String" + }, + { + "name": "elementVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "FailedJobRetryTimeCycle", + "superClass": ["Element"], + "meta": { + "allowedIn": ["camunda:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"] + }, + "properties": [ + { + "name": "body", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "ExecutionListener", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "bpmn:Task", + "bpmn:ServiceTask", + "bpmn:UserTask", + "bpmn:BusinessRuleTask", + "bpmn:ScriptTask", + "bpmn:ReceiveTask", + "bpmn:ManualTask", + "bpmn:ExclusiveGateway", + "bpmn:SequenceFlow", + "bpmn:ParallelGateway", + "bpmn:InclusiveGateway", + "bpmn:EventBasedGateway", + "bpmn:StartEvent", + "bpmn:IntermediateCatchEvent", + "bpmn:IntermediateThrowEvent", + "bpmn:EndEvent", + "bpmn:BoundaryEvent", + "bpmn:CallActivity", + "bpmn:SubProcess", + "bpmn:Process" + ] + }, + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "event", + "isAttr": true, + "type": "String" + }, + { + "name": "script", + "type": "Script" + }, + { + "name": "fields", + "type": "Field", + "isMany": true + } + ] + }, + { + "name": "TaskListener", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "event", + "isAttr": true, + "type": "String" + }, + { + "name": "script", + "type": "Script" + }, + { + "name": "fields", + "type": "Field", + "isMany": true + }, + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "eventDefinitions", + "type": "bpmn:TimerEventDefinition", + "isMany": true + } + ] + }, + { + "name": "FormProperty", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"] + }, + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "required", + "type": "String", + "isAttr": true + }, + { + "name": "readable", + "type": "String", + "isAttr": true + }, + { + "name": "writable", + "type": "String", + "isAttr": true + }, + { + "name": "variable", + "type": "String", + "isAttr": true + }, + { + "name": "expression", + "type": "String", + "isAttr": true + }, + { + "name": "datePattern", + "type": "String", + "isAttr": true + }, + { + "name": "default", + "type": "String", + "isAttr": true + }, + { + "name": "values", + "type": "Value", + "isMany": true + } + ] + }, + { + "name": "FormData", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"] + }, + "properties": [ + { + "name": "fields", + "type": "FormField", + "isMany": true + }, + { + "name": "businessKey", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "FormField", + "superClass": ["Element"], + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "label", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "datePattern", + "type": "String", + "isAttr": true + }, + { + "name": "defaultValue", + "type": "String", + "isAttr": true + }, + { + "name": "properties", + "type": "Properties" + }, + { + "name": "validation", + "type": "Validation" + }, + { + "name": "values", + "type": "Value", + "isMany": true + } + ] + }, + { + "name": "Validation", + "superClass": ["Element"], + "properties": [ + { + "name": "constraints", + "type": "Constraint", + "isMany": true + } + ] + }, + { + "name": "Constraint", + "superClass": ["Element"], + "properties": [ + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "config", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "ConditionalEventDefinition", + "isAbstract": true, + "extends": ["bpmn:ConditionalEventDefinition"], + "properties": [ + { + "name": "variableName", + "isAttr": true, + "type": "String" + }, + { + "name": "variableEvents", + "isAttr": true, + "type": "String" + } + ] + } + ], + "emumerations": [] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json new file mode 100644 index 00000000..7fe7ad14 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json @@ -0,0 +1,1207 @@ +{ + "name": "Flowable", + "uri": "http://flowable.org/bpmn", + "prefix": "flowable", + "xml": { + "tagAlias": "lowerCase" + }, + "associations": [], + "types": [ + { + "name": "InOutBinding", + "superClass": ["Element"], + "isAbstract": true, + "properties": [ + { + "name": "source", + "isAttr": true, + "type": "String" + }, + { + "name": "sourceExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "target", + "isAttr": true, + "type": "String" + }, + { + "name": "businessKey", + "isAttr": true, + "type": "String" + }, + { + "name": "local", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "variables", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "In", + "superClass": ["InOutBinding"], + "meta": { + "allowedIn": ["bpmn:CallActivity"] + } + }, + { + "name": "Out", + "superClass": ["InOutBinding"], + "meta": { + "allowedIn": ["bpmn:CallActivity"] + } + }, + { + "name": "AsyncCapable", + "isAbstract": true, + "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"], + "properties": [ + { + "name": "async", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "asyncBefore", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "asyncAfter", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "exclusive", + "isAttr": true, + "type": "Boolean", + "default": true + } + ] + }, + { + "name": "JobPriorized", + "isAbstract": true, + "extends": ["bpmn:Process", "flowable:AsyncCapable"], + "properties": [ + { + "name": "jobPriority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "SignalEventDefinition", + "isAbstract": true, + "extends": ["bpmn:SignalEventDefinition"], + "properties": [ + { + "name": "async", + "isAttr": true, + "type": "Boolean", + "default": false + } + ] + }, + { + "name": "ErrorEventDefinition", + "isAbstract": true, + "extends": ["bpmn:ErrorEventDefinition"], + "properties": [ + { + "name": "errorCodeVariable", + "isAttr": true, + "type": "String" + }, + { + "name": "errorMessageVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Error", + "isAbstract": true, + "extends": ["bpmn:Error"], + "properties": [ + { + "name": "flowable:errorMessage", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "PotentialStarter", + "superClass": ["Element"], + "properties": [ + { + "name": "resourceAssignmentExpression", + "type": "bpmn:ResourceAssignmentExpression" + } + ] + }, + { + "name": "FormSupported", + "isAbstract": true, + "extends": ["bpmn:StartEvent", "bpmn:UserTask"], + "properties": [ + { + "name": "formHandlerClass", + "isAttr": true, + "type": "String" + }, + { + "name": "formKey", + "isAttr": true, + "type": "String" + }, + { + "name": "formType", + "isAttr": true, + "type": "String" + }, + { + "name": "formReadOnly", + "isAttr": true, + "type": "Boolean", + "default": false + }, + { + "name": "formInit", + "isAttr": true, + "type": "Boolean", + "default": true + } + ] + }, + { + "name": "TemplateSupported", + "isAbstract": true, + "extends": ["bpmn:Process", "bpmn:FlowElement"], + "properties": [ + { + "name": "modelerTemplate", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Initiator", + "isAbstract": true, + "extends": ["bpmn:StartEvent"], + "properties": [ + { + "name": "initiator", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ScriptTask", + "isAbstract": true, + "extends": ["bpmn:ScriptTask"], + "properties": [ + { + "name": "resultVariable", + "isAttr": true, + "type": "String" + }, + { + "name": "resource", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Process", + "isAbstract": true, + "extends": ["bpmn:Process"], + "properties": [ + { + "name": "candidateStarterGroups", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateStarterUsers", + "isAttr": true, + "type": "String" + }, + { + "name": "versionTag", + "isAttr": true, + "type": "String" + }, + { + "name": "historyTimeToLive", + "isAttr": true, + "type": "String" + }, + { + "name": "isStartableInTasklist", + "isAttr": true, + "type": "Boolean", + "default": true + } + ] + }, + { + "name": "EscalationEventDefinition", + "isAbstract": true, + "extends": ["bpmn:EscalationEventDefinition"], + "properties": [ + { + "name": "escalationCodeVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "FormalExpression", + "isAbstract": true, + "extends": ["bpmn:FormalExpression"], + "properties": [ + { + "name": "resource", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Assignable", + "extends": ["bpmn:UserTask"], + "properties": [ + { + "name": "assignee", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateUsers", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateGroups", + "isAttr": true, + "type": "String" + }, + { + "name": "dueDate", + "isAttr": true, + "type": "String" + }, + { + "name": "followUpDate", + "isAttr": true, + "type": "String" + }, + { + "name": "priority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Assignee", + "supperClass": "Element", + "meta": { + "allowedIn": ["*"] + }, + "properties": [ + { + "name": "label", + "type": "String", + "isAttr": true + }, + { + "name": "viewId", + "type": "Number", + "isAttr": true + } + ] + }, + { + "name": "CallActivity", + "extends": ["bpmn:CallActivity"], + "properties": [ + { + "name": "calledElementBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "calledElementVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "calledElementVersionTag", + "isAttr": true, + "type": "String" + }, + { + "name": "calledElementTenantId", + "isAttr": true, + "type": "String" + }, + { + "name": "caseRef", + "isAttr": true, + "type": "String" + }, + { + "name": "caseBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "caseVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "caseTenantId", + "isAttr": true, + "type": "String" + }, + { + "name": "variableMappingClass", + "isAttr": true, + "type": "String" + }, + { + "name": "variableMappingDelegateExpression", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ServiceTaskLike", + "extends": [ + "bpmn:ServiceTask", + "bpmn:BusinessRuleTask", + "bpmn:SendTask", + "bpmn:MessageEventDefinition" + ], + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "resultVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "DmnCapable", + "extends": ["bpmn:BusinessRuleTask"], + "properties": [ + { + "name": "decisionRef", + "isAttr": true, + "type": "String" + }, + { + "name": "decisionRefBinding", + "isAttr": true, + "type": "String", + "default": "latest" + }, + { + "name": "decisionRefVersion", + "isAttr": true, + "type": "String" + }, + { + "name": "mapDecisionResult", + "isAttr": true, + "type": "String", + "default": "resultList" + }, + { + "name": "decisionRefTenantId", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "ExternalCapable", + "extends": ["flowable:ServiceTaskLike"], + "properties": [ + { + "name": "type", + "isAttr": true, + "type": "String" + }, + { + "name": "topic", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "TaskPriorized", + "extends": ["bpmn:Process", "flowable:ExternalCapable"], + "properties": [ + { + "name": "taskPriority", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Properties", + "superClass": ["Element"], + "meta": { + "allowedIn": ["*"] + }, + "properties": [ + { + "name": "values", + "type": "Property", + "isMany": true + } + ] + }, + { + "name": "Property", + "superClass": ["Element"], + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "value", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "Button", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "code", + "type": "String", + "isAttr": true + }, + { + "name": "isHide", + "type": "String", + "isAttr": true + }, + { + "name": "next", + "type": "String", + "isAttr": true + }, + { + "name": "sort", + "type": "Integer", + "isAttr": true + } + ] + }, + { + "name": "Assignee", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "value", + "type": "String", + "isAttr": true + }, + { + "name": "condition", + "type": "String", + "isAttr": true + }, + { + "name": "operationType", + "type": "String", + "isAttr": true + }, + { + "name": "sort", + "type": "Integer", + "isAttr": true + } + ] + }, + { + "name": "Connector", + "superClass": ["Element"], + "meta": { + "allowedIn": ["flowable:ServiceTaskLike"] + }, + "properties": [ + { + "name": "inputOutput", + "type": "InputOutput" + }, + { + "name": "connectorId", + "type": "String" + } + ] + }, + { + "name": "InputOutput", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:FlowNode", "flowable:Connector"] + }, + "properties": [ + { + "name": "inputOutput", + "type": "InputOutput" + }, + { + "name": "connectorId", + "type": "String" + }, + { + "name": "inputParameters", + "isMany": true, + "type": "InputParameter" + }, + { + "name": "outputParameters", + "isMany": true, + "type": "OutputParameter" + } + ] + }, + { + "name": "InputOutputParameter", + "properties": [ + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + }, + { + "name": "definition", + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "InputOutputParameterDefinition", + "isAbstract": true + }, + { + "name": "List", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "items", + "isMany": true, + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "Map", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "entries", + "isMany": true, + "type": "Entry" + } + ] + }, + { + "name": "Entry", + "properties": [ + { + "name": "key", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + }, + { + "name": "definition", + "type": "InputOutputParameterDefinition" + } + ] + }, + { + "name": "Value", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "id", + "isAttr": true, + "type": "String" + }, + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "Script", + "superClass": ["InputOutputParameterDefinition"], + "properties": [ + { + "name": "scriptFormat", + "isAttr": true, + "type": "String" + }, + { + "name": "resource", + "isAttr": true, + "type": "String" + }, + { + "name": "value", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "Field", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "flowable:ServiceTaskLike", + "flowable:ExecutionListener", + "flowable:TaskListener" + ] + }, + "properties": [ + { + "name": "name", + "isAttr": true, + "type": "String" + }, + { + "name": "expression", + "type": "String" + }, + { + "name": "stringValue", + "isAttr": true, + "type": "String" + }, + { + "name": "string", + "type": "String" + } + ] + }, + { + "name": "ChildField", + "superClass": ["Element"], + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "required", + "type": "String", + "isAttr": true + }, + { + "name": "readable", + "type": "String", + "isAttr": true + }, + { + "name": "writable", + "type": "String", + "isAttr": true + }, + { + "name": "variable", + "type": "String", + "isAttr": true + }, + { + "name": "expression", + "type": "String", + "isAttr": true + }, + { + "name": "datePattern", + "type": "String", + "isAttr": true + }, + { + "name": "default", + "type": "String", + "isAttr": true + }, + { + "name": "values", + "type": "Value", + "isMany": true + } + ] + }, + { + "name": "InputParameter", + "superClass": ["InputOutputParameter"] + }, + { + "name": "OutputParameter", + "superClass": ["InputOutputParameter"] + }, + { + "name": "Collectable", + "isAbstract": true, + "extends": ["bpmn:MultiInstanceLoopCharacteristics"], + "superClass": ["flowable:AsyncCapable"], + "properties": [ + { + "name": "collection", + "isAttr": true, + "type": "String" + }, + { + "name": "elementVariable", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "FailedJobRetryTimeCycle", + "superClass": ["Element"], + "meta": { + "allowedIn": ["flowable:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"] + }, + "properties": [ + { + "name": "body", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "ExecutionListener", + "superClass": ["Element"], + "meta": { + "allowedIn": [ + "bpmn:Task", + "bpmn:ServiceTask", + "bpmn:UserTask", + "bpmn:BusinessRuleTask", + "bpmn:ScriptTask", + "bpmn:ReceiveTask", + "bpmn:ManualTask", + "bpmn:ExclusiveGateway", + "bpmn:SequenceFlow", + "bpmn:ParallelGateway", + "bpmn:InclusiveGateway", + "bpmn:EventBasedGateway", + "bpmn:StartEvent", + "bpmn:IntermediateCatchEvent", + "bpmn:IntermediateThrowEvent", + "bpmn:EndEvent", + "bpmn:BoundaryEvent", + "bpmn:CallActivity", + "bpmn:SubProcess", + "bpmn:Process" + ] + }, + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "event", + "isAttr": true, + "type": "String" + }, + { + "name": "script", + "type": "Script" + }, + { + "name": "fields", + "type": "Field", + "isMany": true + } + ] + }, + { + "name": "TaskListener", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "expression", + "isAttr": true, + "type": "String" + }, + { + "name": "class", + "isAttr": true, + "type": "String" + }, + { + "name": "delegateExpression", + "isAttr": true, + "type": "String" + }, + { + "name": "event", + "isAttr": true, + "type": "String" + }, + { + "name": "script", + "type": "Script" + }, + { + "name": "fields", + "type": "Field", + "isMany": true + } + ] + }, + { + "name": "FormProperty", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"] + }, + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "required", + "type": "String", + "isAttr": true + }, + { + "name": "readable", + "type": "String", + "isAttr": true + }, + { + "name": "writable", + "type": "String", + "isAttr": true + }, + { + "name": "variable", + "type": "String", + "isAttr": true + }, + { + "name": "expression", + "type": "String", + "isAttr": true + }, + { + "name": "datePattern", + "type": "String", + "isAttr": true + }, + { + "name": "default", + "type": "String", + "isAttr": true + }, + { + "name": "values", + "type": "Value", + "isMany": true + }, + { + "name": "children", + "type": "ChildField", + "isMany": true + }, + { + "name": "extensionElements", + "type": "bpmn:ExtensionElements", + "isMany": true + } + ] + }, + { + "name": "FormData", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"] + }, + "properties": [ + { + "name": "fields", + "type": "FormField", + "isMany": true + }, + { + "name": "businessKey", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "FormField", + "superClass": ["Element"], + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "label", + "type": "String", + "isAttr": true + }, + { + "name": "type", + "type": "String", + "isAttr": true + }, + { + "name": "datePattern", + "type": "String", + "isAttr": true + }, + { + "name": "defaultValue", + "type": "String", + "isAttr": true + }, + { + "name": "properties", + "type": "Properties" + }, + { + "name": "validation", + "type": "Validation" + }, + { + "name": "values", + "type": "Value", + "isMany": true + } + ] + }, + { + "name": "Validation", + "superClass": ["Element"], + "properties": [ + { + "name": "constraints", + "type": "Constraint", + "isMany": true + } + ] + }, + { + "name": "Constraint", + "superClass": ["Element"], + "properties": [ + { + "name": "name", + "type": "String", + "isAttr": true + }, + { + "name": "config", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "ConditionalEventDefinition", + "isAbstract": true, + "extends": ["bpmn:ConditionalEventDefinition"], + "properties": [ + { + "name": "variableName", + "isAttr": true, + "type": "String" + }, + { + "name": "variableEvent", + "isAttr": true, + "type": "String" + } + ] + }, + { + "name": "Condition", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:SequenceFlow"] + }, + "properties": [ + { + "name": "id", + "type": "String", + "isAttr": true + }, + { + "name": "field", + "type": "String", + "isAttr": true + }, + { + "name": "compare", + "type": "String", + "isAttr": true + }, + { + "name": "value", + "type": "String", + "isAttr": true + }, + { + "name": "logic", + "type": "String", + "isAttr": true + }, + { + "name": "sort", + "type": "Integer", + "isAttr": true + } + ] + } + ], + "emumerations": [] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/activitiExtension.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/activitiExtension.js new file mode 100644 index 00000000..56ef38aa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/activitiExtension.js @@ -0,0 +1,83 @@ +'use strict' + +import { some } from 'min-dash' + +// const some = require('min-dash').some +// const some = some + +const ALLOWED_TYPES = { + FailedJobRetryTimeCycle: [ + 'bpmn:StartEvent', + 'bpmn:BoundaryEvent', + 'bpmn:IntermediateCatchEvent', + 'bpmn:Activity' + ], + Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'], + Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'] +} + +function is(element, type) { + return element && typeof element.$instanceOf === 'function' && element.$instanceOf(type) +} + +function exists(element) { + return element && element.length +} + +function includesType(collection, type) { + return ( + exists(collection) && + some(collection, function (element) { + return is(element, type) + }) + ) +} + +function anyType(element, types) { + return some(types, function (type) { + return is(element, type) + }) +} + +function isAllowed(propName, propDescriptor, newElement) { + const name = propDescriptor.name, + types = ALLOWED_TYPES[name.replace(/activiti:/, '')] + + return name === propName && anyType(newElement, types) +} + +function ActivitiModdleExtension(eventBus) { + eventBus.on( + 'property.clone', + function (context) { + const newElement = context.newElement, + propDescriptor = context.propertyDescriptor + + this.canCloneProperty(newElement, propDescriptor) + }, + this + ) +} + +ActivitiModdleExtension.$inject = ['eventBus'] + +ActivitiModdleExtension.prototype.canCloneProperty = function (newElement, propDescriptor) { + if (isAllowed('activiti:FailedJobRetryTimeCycle', propDescriptor, newElement)) { + return ( + includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') || + includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') || + is(newElement.loopCharacteristics, 'bpmn:MultiInstanceLoopCharacteristics') + ) + } + + if (isAllowed('activiti:Connector', propDescriptor, newElement)) { + return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition') + } + + if (isAllowed('activiti:Field', propDescriptor, newElement)) { + return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition') + } +} + +// module.exports = ActivitiModdleExtension; +export default ActivitiModdleExtension diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/index.js new file mode 100644 index 00000000..c22ca345 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/index.js @@ -0,0 +1,11 @@ +/* + * @author igdianov + * address https://github.com/igdianov/activiti-bpmn-moddle + * */ + +import activitiExtension from './activitiExtension' + +export default { + __init__: ['ActivitiModdleExtension'], + ActivitiModdleExtension: ['type', activitiExtension] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/extension.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/extension.js new file mode 100644 index 00000000..b8c37a57 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/extension.js @@ -0,0 +1,151 @@ +'use strict' + +import { isFunction, isObject, some } from 'min-dash' + +// const isFunction = isFunction, +// isObject = isObject, +// some = some +// const isFunction = require('min-dash').isFunction, +// isObject = require('min-dash').isObject, +// some = require('min-dash').some + +const WILDCARD = '*' + +function CamundaModdleExtension(eventBus) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this + + eventBus.on('moddleCopy.canCopyProperty', function (context) { + const property = context.property, + parent = context.parent + + return self.canCopyProperty(property, parent) + }) +} + +CamundaModdleExtension.$inject = ['eventBus'] + +/** + * Check wether to disallow copying property. + */ +CamundaModdleExtension.prototype.canCopyProperty = function (property, parent) { + // (1) check wether property is allowed in parent + if (isObject(property) && !isAllowedInParent(property, parent)) { + return false + } + + // (2) check more complex scenarios + + if (is(property, 'camunda:InputOutput') && !this.canHostInputOutput(parent)) { + return false + } + + if (isAny(property, ['camunda:Connector', 'camunda:Field']) && !this.canHostConnector(parent)) { + return false + } + + if (is(property, 'camunda:In') && !this.canHostIn(parent)) { + return false + } +} + +CamundaModdleExtension.prototype.canHostInputOutput = function (parent) { + // allowed in camunda:Connector + const connector = getParent(parent, 'camunda:Connector') + + if (connector) { + return true + } + + // special rules inside bpmn:FlowNode + const flowNode = getParent(parent, 'bpmn:FlowNode') + + if (!flowNode) { + return false + } + + if (isAny(flowNode, ['bpmn:StartEvent', 'bpmn:Gateway', 'bpmn:BoundaryEvent'])) { + return false + } + + return !(is(flowNode, 'bpmn:SubProcess') && flowNode.get('triggeredByEvent')) +} + +CamundaModdleExtension.prototype.canHostConnector = function (parent) { + const serviceTaskLike = getParent(parent, 'camunda:ServiceTaskLike') + + if (is(serviceTaskLike, 'bpmn:MessageEventDefinition')) { + // only allow on throw and end events + return getParent(parent, 'bpmn:IntermediateThrowEvent') || getParent(parent, 'bpmn:EndEvent') + } + + return true +} + +CamundaModdleExtension.prototype.canHostIn = function (parent) { + const callActivity = getParent(parent, 'bpmn:CallActivity') + + if (callActivity) { + return true + } + + const signalEventDefinition = getParent(parent, 'bpmn:SignalEventDefinition') + + if (signalEventDefinition) { + // only allow on throw and end events + return getParent(parent, 'bpmn:IntermediateThrowEvent') || getParent(parent, 'bpmn:EndEvent') + } + + return true +} + +// module.exports = CamundaModdleExtension; +export default CamundaModdleExtension + +// helpers ////////// + +function is(element, type) { + return element && isFunction(element.$instanceOf) && element.$instanceOf(type) +} + +function isAny(element, types) { + return some(types, function (t) { + return is(element, t) + }) +} + +function getParent(element, type) { + if (!type) { + return element.$parent + } + + if (is(element, type)) { + return element + } + + if (!element.$parent) { + return + } + + return getParent(element.$parent, type) +} + +function isAllowedInParent(property, parent) { + // (1) find property descriptor + const descriptor = property.$type && property.$model.getTypeDescriptor(property.$type) + + const allowedIn = descriptor && descriptor.meta && descriptor.meta.allowedIn + + if (!allowedIn || isWildcard(allowedIn)) { + return true + } + + // (2) check wether property has parent of allowed type + return some(allowedIn, function (type) { + return getParent(parent, type) + }) +} + +function isWildcard(allowedIn) { + return allowedIn.indexOf(WILDCARD) !== -1 +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/index.js new file mode 100644 index 00000000..1da1bc70 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/index.js @@ -0,0 +1,8 @@ +'use strict' + +import extension from './extension' + +export default { + __init__: ['camundaModdleExtension'], + camundaModdleExtension: ['type', extension] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/flowableExtension.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/flowableExtension.js new file mode 100644 index 00000000..3dcea677 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/flowableExtension.js @@ -0,0 +1,83 @@ +'use strict' + +import { some } from 'min-dash' + +// const some = some +// const some = require('min-dash').some + +const ALLOWED_TYPES = { + FailedJobRetryTimeCycle: [ + 'bpmn:StartEvent', + 'bpmn:BoundaryEvent', + 'bpmn:IntermediateCatchEvent', + 'bpmn:Activity' + ], + Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'], + Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'] +} + +function is(element, type) { + return element && typeof element.$instanceOf === 'function' && element.$instanceOf(type) +} + +function exists(element) { + return element && element.length +} + +function includesType(collection, type) { + return ( + exists(collection) && + some(collection, function (element) { + return is(element, type) + }) + ) +} + +function anyType(element, types) { + return some(types, function (type) { + return is(element, type) + }) +} + +function isAllowed(propName, propDescriptor, newElement) { + const name = propDescriptor.name, + types = ALLOWED_TYPES[name.replace(/flowable:/, '')] + + return name === propName && anyType(newElement, types) +} + +function FlowableModdleExtension(eventBus) { + eventBus.on( + 'property.clone', + function (context) { + const newElement = context.newElement, + propDescriptor = context.propertyDescriptor + + this.canCloneProperty(newElement, propDescriptor) + }, + this + ) +} + +FlowableModdleExtension.$inject = ['eventBus'] + +FlowableModdleExtension.prototype.canCloneProperty = function (newElement, propDescriptor) { + if (isAllowed('flowable:FailedJobRetryTimeCycle', propDescriptor, newElement)) { + return ( + includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') || + includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') || + is(newElement.loopCharacteristics, 'bpmn:MultiInstanceLoopCharacteristics') + ) + } + + if (isAllowed('flowable:Connector', propDescriptor, newElement)) { + return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition') + } + + if (isAllowed('flowable:Field', propDescriptor, newElement)) { + return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition') + } +} + +// module.exports = FlowableModdleExtension; +export default FlowableModdleExtension diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/index.js new file mode 100644 index 00000000..6d59b67f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/index.js @@ -0,0 +1,10 @@ +/* + * @author igdianov + * address https://github.com/igdianov/activiti-bpmn-moddle + * */ +import flowableExtension from './flowableExtension' + +export default { + __init__: ['FlowableModdleExtension'], + FlowableModdleExtension: ['type', flowableExtension] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js new file mode 100644 index 00000000..5e2803b5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js @@ -0,0 +1,221 @@ +import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider' +import { assign } from 'min-dash' + +export default function CustomPalette( + palette, + create, + elementFactory, + spaceTool, + lassoTool, + handTool, + globalConnect, + translate +) { + PaletteProvider.call( + this, + palette, + create, + elementFactory, + spaceTool, + lassoTool, + handTool, + globalConnect, + translate, + 2000 + ) +} + +const F = function () {} // 核心,利用空对象作为中介; +F.prototype = PaletteProvider.prototype // 核心,将父类的原型赋值给空对象F; + +// 利用中介函数重写原型链方法 +F.prototype.getPaletteEntries = function () { + const actions = {}, + create = this._create, + elementFactory = this._elementFactory, + spaceTool = this._spaceTool, + lassoTool = this._lassoTool, + handTool = this._handTool, + globalConnect = this._globalConnect, + translate = this._translate + + function createAction(type, group, className, title, options) { + function createListener(event) { + const shape = elementFactory.createShape(assign({ type: type }, options)) + + if (options) { + shape.businessObject.di.isExpanded = options.isExpanded + } + + create.start(event, shape) + } + + const shortType = type.replace(/^bpmn:/, '') + + return { + group: group, + className: className, + title: title || translate('Create {type}', { type: shortType }), + action: { + dragstart: createListener, + click: createListener + } + } + } + + function createSubprocess(event) { + const subProcess = elementFactory.createShape({ + type: 'bpmn:SubProcess', + x: 0, + y: 0, + isExpanded: true + }) + + const startEvent = elementFactory.createShape({ + type: 'bpmn:StartEvent', + x: 40, + y: 82, + parent: subProcess + }) + + create.start(event, [subProcess, startEvent], { + hints: { + autoSelect: [startEvent] + } + }) + } + + function createParticipant(event) { + create.start(event, elementFactory.createParticipantShape()) + } + + assign(actions, { + 'hand-tool': { + group: 'tools', + className: 'bpmn-icon-hand-tool', + title: '激活抓手工具', + // title: translate("Activate the hand tool"), + action: { + click: function (event) { + handTool.activateHand(event) + } + } + }, + 'lasso-tool': { + group: 'tools', + className: 'bpmn-icon-lasso-tool', + title: translate('Activate the lasso tool'), + action: { + click: function (event) { + lassoTool.activateSelection(event) + } + } + }, + 'space-tool': { + group: 'tools', + className: 'bpmn-icon-space-tool', + title: translate('Activate the create/remove space tool'), + action: { + click: function (event) { + spaceTool.activateSelection(event) + } + } + }, + 'global-connect-tool': { + group: 'tools', + className: 'bpmn-icon-connection-multi', + title: translate('Activate the global connect tool'), + action: { + click: function (event) { + globalConnect.toggle(event) + } + } + }, + 'tool-separator': { + group: 'tools', + separator: true + }, + 'create.start-event': createAction( + 'bpmn:StartEvent', + 'event', + 'bpmn-icon-start-event-none', + translate('Create StartEvent') + ), + 'create.intermediate-event': createAction( + 'bpmn:IntermediateThrowEvent', + 'event', + 'bpmn-icon-intermediate-event-none', + translate('Create Intermediate/Boundary Event') + ), + 'create.end-event': createAction( + 'bpmn:EndEvent', + 'event', + 'bpmn-icon-end-event-none', + translate('Create EndEvent') + ), + 'create.exclusive-gateway': createAction( + 'bpmn:ExclusiveGateway', + 'gateway', + 'bpmn-icon-gateway-none', + translate('Create Gateway') + ), + 'create.user-task': createAction( + 'bpmn:UserTask', + 'activity', + 'bpmn-icon-user-task', + translate('Create User Task') + ), + 'create.data-object': createAction( + 'bpmn:DataObjectReference', + 'data-object', + 'bpmn-icon-data-object', + translate('Create DataObjectReference') + ), + 'create.data-store': createAction( + 'bpmn:DataStoreReference', + 'data-store', + 'bpmn-icon-data-store', + translate('Create DataStoreReference') + ), + 'create.subprocess-expanded': { + group: 'activity', + className: 'bpmn-icon-subprocess-expanded', + title: translate('Create expanded SubProcess'), + action: { + dragstart: createSubprocess, + click: createSubprocess + } + }, + 'create.participant-expanded': { + group: 'collaboration', + className: 'bpmn-icon-participant', + title: translate('Create Pool/Participant'), + action: { + dragstart: createParticipant, + click: createParticipant + } + }, + 'create.group': createAction( + 'bpmn:Group', + 'artifact', + 'bpmn-icon-group', + translate('Create Group') + ) + }) + + return actions +} + +CustomPalette.$inject = [ + 'palette', + 'create', + 'elementFactory', + 'spaceTool', + 'lassoTool', + 'handTool', + 'globalConnect', + 'translate' +] + +CustomPalette.prototype = new F() // 核心,将 F的实例赋值给子类; +CustomPalette.prototype.constructor = CustomPalette // 修复子类CustomPalette的构造器指向,防止原型链的混乱; diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/palette/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/palette/index.js new file mode 100644 index 00000000..8e4f3ac9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/palette/index.js @@ -0,0 +1,22 @@ +// import PaletteModule from "diagram-js/lib/features/palette"; +// import CreateModule from "diagram-js/lib/features/create"; +// import SpaceToolModule from "diagram-js/lib/features/space-tool"; +// import LassoToolModule from "diagram-js/lib/features/lasso-tool"; +// import HandToolModule from "diagram-js/lib/features/hand-tool"; +// import GlobalConnectModule from "diagram-js/lib/features/global-connect"; +// import translate from "diagram-js/lib/i18n/translate"; +// +// import PaletteProvider from "./paletteProvider"; +// +// export default { +// __depends__: [PaletteModule, CreateModule, SpaceToolModule, LassoToolModule, HandToolModule, GlobalConnectModule, translate], +// __init__: ["paletteProvider"], +// paletteProvider: ["type", PaletteProvider] +// }; + +import CustomPalette from './CustomPalette' + +export default { + __init__: ['paletteProvider'], + paletteProvider: ['type', CustomPalette] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js new file mode 100644 index 00000000..7098981c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js @@ -0,0 +1,213 @@ +import { assign } from 'min-dash' + +/** + * A palette provider for BPMN 2.0 elements. + */ +export default function PaletteProvider( + palette, + create, + elementFactory, + spaceTool, + lassoTool, + handTool, + globalConnect, + translate +) { + this._palette = palette + this._create = create + this._elementFactory = elementFactory + this._spaceTool = spaceTool + this._lassoTool = lassoTool + this._handTool = handTool + this._globalConnect = globalConnect + this._translate = translate + + palette.registerProvider(this) +} + +PaletteProvider.$inject = [ + 'palette', + 'create', + 'elementFactory', + 'spaceTool', + 'lassoTool', + 'handTool', + 'globalConnect', + 'translate' +] + +PaletteProvider.prototype.getPaletteEntries = function () { + const actions = {}, + create = this._create, + elementFactory = this._elementFactory, + spaceTool = this._spaceTool, + lassoTool = this._lassoTool, + handTool = this._handTool, + globalConnect = this._globalConnect, + translate = this._translate + + function createAction(type, group, className, title, options) { + function createListener(event) { + const shape = elementFactory.createShape(assign({ type: type }, options)) + + if (options) { + shape.businessObject.di.isExpanded = options.isExpanded + } + + create.start(event, shape) + } + + const shortType = type.replace(/^bpmn:/, '') + + return { + group: group, + className: className, + title: title || translate('Create {type}', { type: shortType }), + action: { + dragstart: createListener, + click: createListener + } + } + } + + function createSubprocess(event) { + const subProcess = elementFactory.createShape({ + type: 'bpmn:SubProcess', + x: 0, + y: 0, + isExpanded: true + }) + + const startEvent = elementFactory.createShape({ + type: 'bpmn:StartEvent', + x: 40, + y: 82, + parent: subProcess + }) + + create.start(event, [subProcess, startEvent], { + hints: { + autoSelect: [startEvent] + } + }) + } + + function createParticipant(event) { + create.start(event, elementFactory.createParticipantShape()) + } + + assign(actions, { + 'hand-tool': { + group: 'tools', + className: 'bpmn-icon-hand-tool', + title: translate('Activate the hand tool'), + action: { + click: function (event) { + handTool.activateHand(event) + } + } + }, + 'lasso-tool': { + group: 'tools', + className: 'bpmn-icon-lasso-tool', + title: translate('Activate the lasso tool'), + action: { + click: function (event) { + lassoTool.activateSelection(event) + } + } + }, + 'space-tool': { + group: 'tools', + className: 'bpmn-icon-space-tool', + title: translate('Activate the create/remove space tool'), + action: { + click: function (event) { + spaceTool.activateSelection(event) + } + } + }, + 'global-connect-tool': { + group: 'tools', + className: 'bpmn-icon-connection-multi', + title: translate('Activate the global connect tool'), + action: { + click: function (event) { + globalConnect.toggle(event) + } + } + }, + 'tool-separator': { + group: 'tools', + separator: true + }, + 'create.start-event': createAction( + 'bpmn:StartEvent', + 'event', + 'bpmn-icon-start-event-none', + translate('Create StartEvent') + ), + 'create.intermediate-event': createAction( + 'bpmn:IntermediateThrowEvent', + 'event', + 'bpmn-icon-intermediate-event-none', + translate('Create Intermediate/Boundary Event') + ), + 'create.end-event': createAction( + 'bpmn:EndEvent', + 'event', + 'bpmn-icon-end-event-none', + translate('Create EndEvent') + ), + 'create.exclusive-gateway': createAction( + 'bpmn:ExclusiveGateway', + 'gateway', + 'bpmn-icon-gateway-none', + translate('Create Gateway') + ), + 'create.user-task': createAction( + 'bpmn:UserTask', + 'activity', + 'bpmn-icon-user-task', + translate('Create User Task') + ), + 'create.data-object': createAction( + 'bpmn:DataObjectReference', + 'data-object', + 'bpmn-icon-data-object', + translate('Create DataObjectReference') + ), + 'create.data-store': createAction( + 'bpmn:DataStoreReference', + 'data-store', + 'bpmn-icon-data-store', + translate('Create DataStoreReference') + ), + 'create.subprocess-expanded': { + group: 'activity', + className: 'bpmn-icon-subprocess-expanded', + title: translate('Create expanded SubProcess'), + action: { + dragstart: createSubprocess, + click: createSubprocess + } + }, + 'create.participant-expanded': { + group: 'collaboration', + className: 'bpmn-icon-participant', + title: translate('Create Pool/Participant'), + action: { + dragstart: createParticipant, + click: createParticipant + } + }, + 'create.group': createAction( + 'bpmn:Group', + 'artifact', + 'bpmn-icon-group', + translate('Create Group') + ) + }) + + return actions +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js new file mode 100644 index 00000000..c1b99e12 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js @@ -0,0 +1,44 @@ +// import translations from "./zh"; +// +// export default function customTranslate(template, replacements) { +// replacements = replacements || {}; +// +// // Translate +// template = translations[template] || template; +// +// // Replace +// return template.replace(/{([^}]+)}/g, function(_, key) { +// let str = replacements[key]; +// if ( +// translations[replacements[key]] !== null && +// translations[replacements[key]] !== "undefined" +// ) { +// // eslint-disable-next-line no-mixed-spaces-and-tabs +// str = translations[replacements[key]]; +// // eslint-disable-next-line no-mixed-spaces-and-tabs +// } +// return str || "{" + key + "}"; +// }); +// } + +export default function customTranslate(translations) { + return function (template, replacements) { + replacements = replacements || {} + // Translate + template = translations[template] || template + + // Replace + return template.replace(/{([^}]+)}/g, function (_, key) { + let str = replacements[key] + if ( + translations[replacements[key]] !== null && + translations[replacements[key]] !== undefined + ) { + // eslint-disable-next-line no-mixed-spaces-and-tabs + str = translations[replacements[key]] + // eslint-disable-next-line no-mixed-spaces-and-tabs + } + return str || '{' + key + '}' + }) + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js new file mode 100644 index 00000000..777db3e7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js @@ -0,0 +1,240 @@ +/** + * This is a sample file that should be replaced with the actual translation. + * + * Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available + * translations and labels to translate. + */ +export default { + // 添加部分 + 'Append EndEvent': '追加结束事件', + 'Append Gateway': '追加网关', + 'Append Task': '追加任务', + 'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件', + + 'Activate the global connect tool': '激活全局连接工具', + 'Append {type}': '添加 {type}', + 'Add Lane above': '在上面添加道', + 'Divide into two Lanes': '分割成两个道', + 'Divide into three Lanes': '分割成三个道', + 'Add Lane below': '在下面添加道', + 'Append compensation activity': '追加补偿活动', + 'Change type': '修改类型', + 'Connect using Association': '使用关联连接', + 'Connect using Sequence/MessageFlow or Association': '使用顺序/消息流或者关联连接', + 'Connect using DataInputAssociation': '使用数据输入关联连接', + Remove: '移除', + 'Activate the hand tool': '激活抓手工具', + 'Activate the lasso tool': '激活套索工具', + 'Activate the create/remove space tool': '激活创建/删除空间工具', + 'Create expanded SubProcess': '创建扩展子过程', + 'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件', + 'Create Pool/Participant': '创建池/参与者', + 'Parallel Multi Instance': '并行多重事件', + 'Sequential Multi Instance': '时序多重事件', + DataObjectReference: '数据对象参考', + DataStoreReference: '数据存储参考', + Loop: '循环', + 'Ad-hoc': '即席', + 'Create {type}': '创建 {type}', + Task: '任务', + 'Send Task': '发送任务', + 'Receive Task': '接收任务', + 'User Task': '用户任务', + 'Manual Task': '手工任务', + 'Business Rule Task': '业务规则任务', + 'Service Task': '服务任务', + 'Script Task': '脚本任务', + 'Call Activity': '调用活动', + 'Sub Process (collapsed)': '子流程(折叠的)', + 'Sub Process (expanded)': '子流程(展开的)', + 'Start Event': '开始事件', + StartEvent: '开始事件', + 'Intermediate Throw Event': '中间事件', + 'End Event': '结束事件', + EndEvent: '结束事件', + 'Create StartEvent': '创建开始事件', + 'Create EndEvent': '创建结束事件', + 'Create Task': '创建任务', + 'Create User Task': '创建用户任务', + 'Create Gateway': '创建网关', + 'Create DataObjectReference': '创建数据对象', + 'Create DataStoreReference': '创建数据存储', + 'Create Group': '创建分组', + 'Create Intermediate/Boundary Event': '创建中间/边界事件', + 'Message Start Event': '消息开始事件', + 'Timer Start Event': '定时开始事件', + 'Conditional Start Event': '条件开始事件', + 'Signal Start Event': '信号开始事件', + 'Error Start Event': '错误开始事件', + 'Escalation Start Event': '升级开始事件', + 'Compensation Start Event': '补偿开始事件', + 'Message Start Event (non-interrupting)': '消息开始事件(非中断)', + 'Timer Start Event (non-interrupting)': '定时开始事件(非中断)', + 'Conditional Start Event (non-interrupting)': '条件开始事件(非中断)', + 'Signal Start Event (non-interrupting)': '信号开始事件(非中断)', + 'Escalation Start Event (non-interrupting)': '升级开始事件(非中断)', + 'Message Intermediate Catch Event': '消息中间捕获事件', + 'Message Intermediate Throw Event': '消息中间抛出事件', + 'Timer Intermediate Catch Event': '定时中间捕获事件', + 'Escalation Intermediate Throw Event': '升级中间抛出事件', + 'Conditional Intermediate Catch Event': '条件中间捕获事件', + 'Link Intermediate Catch Event': '链接中间捕获事件', + 'Link Intermediate Throw Event': '链接中间抛出事件', + 'Compensation Intermediate Throw Event': '补偿中间抛出事件', + 'Signal Intermediate Catch Event': '信号中间捕获事件', + 'Signal Intermediate Throw Event': '信号中间抛出事件', + 'Message End Event': '消息结束事件', + 'Escalation End Event': '定时结束事件', + 'Error End Event': '错误结束事件', + 'Cancel End Event': '取消结束事件', + 'Compensation End Event': '补偿结束事件', + 'Signal End Event': '信号结束事件', + 'Terminate End Event': '终止结束事件', + 'Message Boundary Event': '消息边界事件', + 'Message Boundary Event (non-interrupting)': '消息边界事件(非中断)', + 'Timer Boundary Event': '定时边界事件', + 'Timer Boundary Event (non-interrupting)': '定时边界事件(非中断)', + 'Escalation Boundary Event': '升级边界事件', + 'Escalation Boundary Event (non-interrupting)': '升级边界事件(非中断)', + 'Conditional Boundary Event': '条件边界事件', + 'Conditional Boundary Event (non-interrupting)': '条件边界事件(非中断)', + 'Error Boundary Event': '错误边界事件', + 'Cancel Boundary Event': '取消边界事件', + 'Signal Boundary Event': '信号边界事件', + 'Signal Boundary Event (non-interrupting)': '信号边界事件(非中断)', + 'Compensation Boundary Event': '补偿边界事件', + 'Exclusive Gateway': '互斥网关', + 'Parallel Gateway': '并行网关', + 'Inclusive Gateway': '相容网关', + 'Complex Gateway': '复杂网关', + 'Event based Gateway': '事件网关', + Transaction: '转运', + 'Sub Process': '子流程', + 'Event Sub Process': '事件子流程', + 'Collapsed Pool': '折叠池', + 'Expanded Pool': '展开池', + + // Errors + 'no parent for {element} in {parent}': '在{parent}里,{element}没有父类', + 'no shape type specified': '没有指定的形状类型', + 'flow elements must be children of pools/participants': '流元素必须是池/参与者的子类', + 'out of bounds release': 'out of bounds release', + 'more than {count} child lanes': '子道大于{count} ', + 'element required': '元素不能为空', + 'diagram not part of bpmn:Definitions': '流程图不符合bpmn规范', + 'no diagram to display': '没有可展示的流程图', + 'no process or collaboration to display': '没有可展示的流程/协作', + 'element {element} referenced by {referenced}#{property} not yet drawn': + '由{referenced}#{property}引用的{element}元素仍未绘制', + 'already rendered {element}': '{element} 已被渲染', + 'failed to import {element}': '导入{element}失败', + //属性面板的参数 + Id: '编号', + Name: '名称', + General: '常规', + Details: '详情', + 'Message Name': '消息名称', + Message: '消息', + Initiator: '创建者', + 'Asynchronous Continuations': '持续异步', + 'Asynchronous Before': '异步前', + 'Asynchronous After': '异步后', + 'Job Configuration': '工作配置', + Exclusive: '排除', + 'Job Priority': '工作优先级', + 'Retry Time Cycle': '重试时间周期', + Documentation: '文档', + 'Element Documentation': '元素文档', + 'History Configuration': '历史配置', + 'History Time To Live': '历史的生存时间', + Forms: '表单', + 'Form Key': '表单key', + 'Form Fields': '表单字段', + 'Business Key': '业务key', + 'Form Field': '表单字段', + ID: '编号', + Type: '类型', + Label: '名称', + 'Default Value': '默认值', + 'Default Flow': '默认流转路径', + 'Conditional Flow': '条件流转路径', + 'Sequence Flow': '普通流转路径', + Validation: '校验', + 'Add Constraint': '添加约束', + Config: '配置', + Properties: '属性', + 'Add Property': '添加属性', + Value: '值', + Listeners: '监听器', + 'Execution Listener': '执行监听', + 'Event Type': '事件类型', + 'Listener Type': '监听器类型', + 'Java Class': 'Java类', + Expression: '表达式', + 'Must provide a value': '必须提供一个值', + 'Delegate Expression': '代理表达式', + Script: '脚本', + 'Script Format': '脚本格式', + 'Script Type': '脚本类型', + 'Inline Script': '内联脚本', + 'External Script': '外部脚本', + Resource: '资源', + 'Field Injection': '字段注入', + Extensions: '扩展', + 'Input/Output': '输入/输出', + 'Input Parameters': '输入参数', + 'Output Parameters': '输出参数', + Parameters: '参数', + 'Output Parameter': '输出参数', + 'Timer Definition Type': '定时器定义类型', + 'Timer Definition': '定时器定义', + Date: '日期', + Duration: '持续', + Cycle: '循环', + Signal: '信号', + 'Signal Name': '信号名称', + Escalation: '升级', + Error: '错误', + 'Link Name': '链接名称', + Condition: '条件名称', + 'Variable Name': '变量名称', + 'Variable Event': '变量事件', + 'Specify more than one variable change event as a comma separated list.': + '多个变量事件以逗号隔开', + 'Wait for Completion': '等待完成', + 'Activity Ref': '活动参考', + 'Version Tag': '版本标签', + Executable: '可执行文件', + 'External Task Configuration': '扩展任务配置', + 'Task Priority': '任务优先级', + External: '外部', + Connector: '连接器', + 'Must configure Connector': '必须配置连接器', + 'Connector Id': '连接器编号', + Implementation: '实现方式', + 'Field Injections': '字段注入', + Fields: '字段', + 'Result Variable': '结果变量', + Topic: '主题', + 'Configure Connector': '配置连接器', + 'Input Parameter': '输入参数', + Assignee: '代理人', + 'Candidate Users': '候选用户', + 'Candidate Groups': '候选组', + 'Due Date': '到期时间', + 'Follow Up Date': '跟踪日期', + Priority: '优先级', + 'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)': + '跟踪日期必须符合EL表达式,如: ${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00', + 'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)': + '跟踪日期必须符合EL表达式,如: ${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00', + Variables: '变量', + 'Candidate Starter Configuration': '候选人起动器配置', + 'Candidate Starter Groups': '候选人起动器组', + 'This maps to the process definition key.': '这映射到流程定义键。', + 'Candidate Starter Users': '候选人起动器的用户', + 'Specify more than one user as a comma separated list.': '指定多个用户作为逗号分隔的列表。', + 'Tasklist Configuration': 'Tasklist配置', + Startable: '启动', + 'Specify more than one group as a comma separated list.': '指定多个组作为逗号分隔的列表。' +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/index.ts new file mode 100644 index 00000000..ce44a3c5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/index.ts @@ -0,0 +1,11 @@ +import MyProcessDesigner from './designer' +import MyProcessPenal from './penal' +import MyProcessViewer from './designer/index2' + +import './theme/index.scss' +import 'bpmn-js/dist/assets/diagram-js.css' +import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css' +import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css' +import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css' + +export { MyProcessDesigner, MyProcessPenal, MyProcessViewer } diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/palette/ProcessPalette.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/palette/ProcessPalette.vue new file mode 100644 index 00000000..ba97d967 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/palette/ProcessPalette.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue new file mode 100644 index 00000000..377592f4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue @@ -0,0 +1,211 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue new file mode 100644 index 00000000..639c1cb2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue @@ -0,0 +1,184 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue new file mode 100644 index 00000000..345670ae --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue @@ -0,0 +1,191 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue new file mode 100644 index 00000000..da1d1ae9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue @@ -0,0 +1,465 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/index.js new file mode 100644 index 00000000..7fa56170 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/index.js @@ -0,0 +1,7 @@ +import MyPropertiesPanel from './PropertiesPanel.vue' + +MyPropertiesPanel.install = function (Vue) { + Vue.component(MyPropertiesPanel.name, MyPropertiesPanel) +} + +export default MyPropertiesPanel diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue new file mode 100644 index 00000000..45ee8f93 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue @@ -0,0 +1,403 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue new file mode 100644 index 00000000..9464883c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue @@ -0,0 +1,451 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/template.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/template.js new file mode 100644 index 00000000..430dc64b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/template.js @@ -0,0 +1,178 @@ +export const template = (isTaskListener) => { + return ` +
+ + + + + + + + +
+ 添加监听器 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + ${ + isTaskListener + ? "" + + "" + + "" + + "" + + "" + + "" + + '' + + '' + + "" + + "" + + '' + : '' + } + + +

+ 注入字段: + 添加字段 +

+ + + + + + + + + + +
+ 取 消 + 保 存 +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ ` +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts new file mode 100644 index 00000000..5f46abd0 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts @@ -0,0 +1,62 @@ +// 初始化表单数据 +export function initListenerForm(listener) { + let self = { + ...listener + } + if (listener.script) { + self = { + ...listener, + ...listener.script, + scriptType: listener.script.resource ? 'externalScript' : 'inlineScript' + } + } + if (listener.event === 'timeout' && listener.eventDefinitions) { + if (listener.eventDefinitions.length) { + let k = '' + for (const key in listener.eventDefinitions[0]) { + console.log(listener.eventDefinitions, key) + if (key.indexOf('time') !== -1) { + k = key + self.eventDefinitionType = key.replace('time', '').toLowerCase() + } + } + console.log(k) + self.eventTimeDefinitions = listener.eventDefinitions[0][k].body + } + } + return self +} + +export function initListenerType(listener) { + let listenerType + if (listener.class) listenerType = 'classListener' + if (listener.expression) listenerType = 'expressionListener' + if (listener.delegateExpression) listenerType = 'delegateExpressionListener' + if (listener.script) listenerType = 'scriptListener' + return { + ...JSON.parse(JSON.stringify(listener)), + ...(listener.script ?? {}), + listenerType: listenerType + } +} + +export const listenerType = { + classListener: 'Java 类', + expressionListener: '表达式', + delegateExpressionListener: '代理表达式', + scriptListener: '脚本' +} + +export const eventType = { + create: '创建', + assignment: '指派', + complete: '完成', + delete: '删除', + update: '更新', + timeout: '超时' +} + +export const fieldType = { + string: '字符串', + expression: '表达式' +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue new file mode 100644 index 00000000..28db5aa7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue @@ -0,0 +1,254 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue new file mode 100644 index 00000000..05532c69 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue @@ -0,0 +1,55 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue new file mode 100644 index 00000000..494b3d97 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue @@ -0,0 +1,169 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/signal-message/SignalAndMessage.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/signal-message/SignalAndMessage.vue new file mode 100644 index 00000000..f38f31c6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/signal-message/SignalAndMessage.vue @@ -0,0 +1,113 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue new file mode 100644 index 00000000..33a12a74 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue @@ -0,0 +1,86 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/task-components/ReceiveTask.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/task-components/ReceiveTask.vue new file mode 100644 index 00000000..83ed24eb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/task-components/ReceiveTask.vue @@ -0,0 +1,125 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/task-components/ScriptTask.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/task-components/ScriptTask.vue new file mode 100644 index 00000000..683fef3b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/task-components/ScriptTask.vue @@ -0,0 +1,99 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue new file mode 100644 index 00000000..7b793dbc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue @@ -0,0 +1,98 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/element-variables.scss b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/element-variables.scss new file mode 100644 index 00000000..49bd326d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/element-variables.scss @@ -0,0 +1,70 @@ +/* 改变主题色变量 */ +$--color-primary: #1890ff; +$--color-danger: #ff4d4f; + +/* 改变 icon 字体路径变量,必需 */ +$--font-path: '~element-ui/lib/theme-chalk/fonts'; + +@import '~element-ui/packages/theme-chalk/src/index'; + +.el-table td, +.el-table th { + color: #333; +} +.el-drawer__header { + padding: 16px 16px 8px 16px; + margin: 0; + line-height: 24px; + font-size: 18px; + color: #303133; + box-sizing: border-box; + border-bottom: 1px solid #e8e8e8; +} +div[class^='el-drawer']:focus, +span:focus { + outline: none; +} +.el-drawer__body { + box-sizing: border-box; + padding: 16px; + width: 100%; + overflow-y: auto; +} + +.el-dialog { + margin-top: 50vh !important; + transform: translateY(-50%); + overflow: hidden; +} +.el-dialog__wrapper { + overflow: hidden; + max-height: 100vh; +} +.el-dialog__header { + padding: 16px 16px 8px 16px; + box-sizing: border-box; + border-bottom: 1px solid #e8e8e8; +} +.el-dialog__body { + padding: 16px; + max-height: 80vh; + box-sizing: border-box; + overflow-y: auto; +} +.el-dialog__footer { + padding: 16px; + box-sizing: border-box; + border-top: 1px solid #e8e8e8; +} +.el-dialog__close { + font-weight: 600; +} +.el-select { + width: 100%; +} +.el-divider:not(.el-divider--horizontal) { + margin: 0 8px; +} +.el-divider.el-divider--horizontal { + margin: 16px 0; +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/index.scss b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/index.scss new file mode 100644 index 00000000..2e60fad4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/index.scss @@ -0,0 +1,2 @@ +@import './process-designer.scss'; +@import './process-panel.scss'; diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/process-designer.scss b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/process-designer.scss new file mode 100644 index 00000000..6af945da --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/process-designer.scss @@ -0,0 +1,161 @@ +@import 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; +@import 'bpmn-js-token-simulation/assets/css/font-awesome.min.css'; +@import 'bpmn-js-token-simulation/assets/css/normalize.css'; + +// 边框被 token-simulation 样式覆盖了 +.djs-palette { + background: var(--palette-background-color); + border: solid 1px var(--palette-border-color) !important; + border-radius: 2px; +} + +.my-process-designer { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + box-sizing: border-box; + .my-process-designer__header { + width: 100%; + min-height: 36px; + .el-button { + text-align: center; + } + .el-button-group { + margin: 4px; + } + .el-tooltip__popper { + .el-button { + width: 100%; + text-align: left; + padding-left: 8px; + padding-right: 8px; + } + .el-button:hover { + background: rgba(64, 158, 255, 0.8); + color: #ffffff; + } + } + .align { + position: relative; + i { + &:after { + content: '|'; + position: absolute; + // transform: rotate(90deg) translate(200%, 60%); + transform: rotate(180deg) translate(271%, -10%); + } + } + } + .align.align-left i { + transform: rotate(90deg); + } + .align.align-right i { + transform: rotate(-90deg); + } + .align.align-top i { + transform: rotate(180deg); + } + .align.align-bottom i { + transform: rotate(0deg); + } + .align.align-center i { + transform: rotate(0deg); + &:after { + // transform: rotate(90deg) translate(0, 60%); + transform: rotate(0deg) translate(-0%, -5%); + } + } + .align.align-middle i { + transform: rotate(-90deg); + &:after { + // transform: rotate(90deg) translate(0, 60%); + transform: rotate(0deg) translate(0, -10%); + } + } + } + .my-process-designer__container { + display: inline-flex; + width: 100%; + flex: 1; + .my-process-designer__canvas { + flex: 1; + height: 100%; + position: relative; + background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') + repeat !important; + div.toggle-mode { + display: none; + } + } + .my-process-designer__property-panel { + height: 100%; + overflow: scroll; + overflow-y: auto; + z-index: 10; + * { + box-sizing: border-box; + } + } + svg { + width: 100%; + height: 100%; + min-height: 100%; + overflow: hidden; + } + } +} + +//侧边栏配置 +// .djs-palette .two-column .open { +.open { + // .djs-palette.open { + .djs-palette-entries { + div[class^='bpmn-icon-']:before, + div[class*='bpmn-icon-']:before { + line-height: unset; + } + div.entry { + position: relative; + } + div.entry:hover { + &::after { + width: max-content; + content: attr(title); + vertical-align: text-bottom; + position: absolute; + right: -10px; + top: 0; + bottom: 0; + overflow: hidden; + transform: translateX(100%); + font-size: 0.5em; + display: inline-block; + text-decoration: inherit; + font-variant: normal; + text-transform: none; + background: #fafafa; + box-shadow: 0 0 6px #eeeeee; + border: 1px solid #cccccc; + box-sizing: border-box; + padding: 0 16px; + border-radius: 4px; + z-index: 100; + } + } + } +} +pre { + margin: 0; + height: 100%; + overflow: hidden; + max-height: calc(80vh - 32px); + overflow-y: auto; +} +.hljs { + word-break: break-word; + white-space: pre-wrap; +} +.hljs * { + font-family: Consolas, Monaco, monospace; +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/process-panel.scss b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/process-panel.scss new file mode 100644 index 00000000..f840cdde --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/theme/process-panel.scss @@ -0,0 +1,107 @@ +.process-panel__container { + box-sizing: border-box; + padding: 0 8px; + border-left: 1px solid #eeeeee; + box-shadow: 0 0 8px #cccccc; + max-height: 100%; + overflow-y: scroll; +} +.panel-tab__title { + font-weight: 600; + padding: 0 8px; + font-size: 1.1em; + line-height: 1.2em; + i { + margin-right: 8px; + font-size: 1.2em; + } +} +.panel-tab__content { + width: 100%; + box-sizing: border-box; + border-top: 1px solid #eeeeee; + padding: 8px 16px; + .panel-tab__content--title { + display: flex; + justify-content: space-between; + padding-bottom: 8px; + span { + flex: 1; + text-align: left; + } + } +} +.element-property { + width: 100%; + display: flex; + align-items: flex-start; + margin: 8px 0; + .element-property__label { + display: block; + width: 90px; + text-align: right; + overflow: hidden; + padding-right: 12px; + line-height: 32px; + font-size: 14px; + box-sizing: border-box; + } + .element-property__value { + flex: 1; + line-height: 32px; + } + .el-form-item { + width: 100%; + margin-bottom: 0; + padding-bottom: 18px; + } +} +.list-property { + flex-direction: column; + .element-listener-item { + width: 100%; + display: inline-grid; + grid-template-columns: 16px auto 32px 32px; + grid-column-gap: 8px; + } + .element-listener-item + .element-listener-item { + margin-top: 8px; + } +} +.listener-filed__title { + display: inline-flex; + width: 100%; + justify-content: space-between; + align-items: center; + margin-top: 0; + span { + width: 200px; + text-align: left; + font-size: 14px; + } + i { + margin-right: 8px; + } +} +.element-drawer__button { + margin-top: 8px; + width: 100%; + display: inline-flex; + justify-content: space-around; +} +.element-drawer__button > .el-button { + width: 100%; +} + +.el-collapse-item__content { + padding-bottom: 0; +} +.el-input.is-disabled .el-input__inner { + color: #999999; +} +.el-form-item.el-form-item--mini { + margin-bottom: 0; + & + .el-form-item { + margin-top: 16px; + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/utils.ts b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/utils.ts new file mode 100644 index 00000000..bb6c5d52 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/package/utils.ts @@ -0,0 +1,77 @@ +import { toRaw } from 'vue' +const bpmnInstances = () => (window as any)?.bpmnInstances +// 创建监听器实例 +export function createListenerObject(options, isTask, prefix) { + const listenerObj = Object.create(null) + listenerObj.event = options.event + isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段 + switch (options.listenerType) { + case 'scriptListener': + listenerObj.script = createScriptObject(options, prefix) + break + case 'expressionListener': + listenerObj.expression = options.expression + break + case 'delegateExpressionListener': + listenerObj.delegateExpression = options.delegateExpression + break + default: + listenerObj.class = options.class + } + // 注入字段 + if (options.fields) { + listenerObj.fields = options.fields.map((field) => { + return createFieldObject(field, prefix) + }) + } + // 任务监听器的 定时器 设置 + if (isTask && options.event === 'timeout' && !!options.eventDefinitionType) { + const timeDefinition = bpmnInstances().moddle.create('bpmn:FormalExpression', { + body: options.eventTimeDefinitions + }) + const TimerEventDefinition = bpmnInstances().moddle.create('bpmn:TimerEventDefinition', { + id: `TimerEventDefinition_${uuid(8)}`, + [`time${options.eventDefinitionType.replace(/^\S/, (s) => s.toUpperCase())}`]: timeDefinition + }) + listenerObj.eventDefinitions = [TimerEventDefinition] + } + return bpmnInstances().moddle.create( + `${prefix}:${isTask ? 'TaskListener' : 'ExecutionListener'}`, + listenerObj + ) +} + +// 创建 监听器的注入字段 实例 +export function createFieldObject(option, prefix) { + const { name, fieldType, string, expression } = option + const fieldConfig = fieldType === 'string' ? { name, string } : { name, expression } + return bpmnInstances().moddle.create(`${prefix}:Field`, fieldConfig) +} + +// 创建脚本实例 +export function createScriptObject(options, prefix) { + const { scriptType, scriptFormat, value, resource } = options + const scriptConfig = + scriptType === 'inlineScript' ? { scriptFormat, value } : { scriptFormat, resource } + return bpmnInstances().moddle.create(`${prefix}:Script`, scriptConfig) +} + +// 更新元素扩展属性 +export function updateElementExtensions(element, extensionList) { + const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { + values: extensionList + }) + bpmnInstances().modeling.updateProperties(toRaw(element), { + extensionElements: extensions + }) +} + +// 创建一个id +export function uuid(length = 8, chars?) { + let result = '' + const charsString = chars || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + for (let i = length; i > 0; --i) { + result += charsString[Math.floor(Math.random() * charsString.length)] + } + return result +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/highlight/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/highlight/index.js new file mode 100644 index 00000000..5df38c9a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/highlight/index.js @@ -0,0 +1,5 @@ +const hljs = require('highlight.js/lib/core') +hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml')) +hljs.registerLanguage('json', require('highlight.js/lib/languages/json')) + +module.exports = hljs diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/custom-renderer/CustomRenderer.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/custom-renderer/CustomRenderer.js new file mode 100644 index 00000000..e8760315 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/custom-renderer/CustomRenderer.js @@ -0,0 +1,14 @@ +import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer' + +export default function CustomRenderer(config, eventBus, styles, pathMap, canvas, textRenderer) { + BpmnRenderer.call(this, config, eventBus, styles, pathMap, canvas, textRenderer, 2000) + + this.handlers['label'] = function () { + return null + } +} + +const F = function () {} // 核心,利用空对象作为中介; +F.prototype = BpmnRenderer.prototype // 核心,将父类的原型赋值给空对象F; +CustomRenderer.prototype = new F() // 核心,将 F的实例赋值给子类; +CustomRenderer.prototype.constructor = CustomRenderer // 修复子类CustomRenderer的构造器指向,防止原型链的混乱; diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/custom-renderer/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/custom-renderer/index.js new file mode 100644 index 00000000..79d8bd04 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/custom-renderer/index.js @@ -0,0 +1,6 @@ +import CustomRenderer from './CustomRenderer' + +export default { + __init__: ['customRenderer'], + customRenderer: ['type', CustomRenderer] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/rules/CustomRules.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/rules/CustomRules.js new file mode 100644 index 00000000..9fa1d14a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/rules/CustomRules.js @@ -0,0 +1,16 @@ +import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules' +import inherits from 'inherits' + +export default function CustomRules(eventBus) { + BpmnRules.call(this, eventBus) +} + +inherits(CustomRules, BpmnRules) + +CustomRules.prototype.canDrop = function () { + return false +} + +CustomRules.prototype.canMove = function () { + return false +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/rules/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/rules/index.js new file mode 100644 index 00000000..12cf05a7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/modules/rules/index.js @@ -0,0 +1,6 @@ +import CustomRules from './CustomRules' + +export default { + __init__: ['customRules'], + customRules: ['type', CustomRules] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/translations.ts b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/translations.ts new file mode 100644 index 00000000..5f9b9a51 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/translations.ts @@ -0,0 +1,25 @@ +/** + * This is a sample file that should be replaced with the actual translation. + * + * Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available + * translations and labels to translate. + */ +export default { + 'Exclusive Gateway': 'Exklusives Gateway', + 'Parallel Gateway': 'Paralleles Gateway', + 'Inclusive Gateway': 'Inklusives Gateway', + 'Complex Gateway': 'Komplexes Gateway', + 'Event based Gateway': 'Ereignis-basiertes Gateway', + 'Message Start Event': '消息启动事件', + 'Timer Start Event': '定时启动事件', + 'Conditional Start Event': '条件启动事件', + 'Signal Start Event': '信号启动事件', + 'Error Start Event': '错误启动事件', + 'Escalation Start Event': '升级启动事件', + 'Compensation Start Event': '补偿启动事件', + 'Message Start Event (non-interrupting)': '消息启动事件 (非中断)', + 'Timer Start Event (non-interrupting)': '定时启动事件 (非中断)', + 'Conditional Start Event (non-interrupting)': '条件启动事件 (非中断)', + 'Signal Start Event (non-interrupting)': '信号启动事件 (非中断)', + 'Escalation Start Event (non-interrupting)': '升级启动事件 (非中断)' +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/utils/directive/clickOutSide.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/utils/directive/clickOutSide.js new file mode 100644 index 00000000..bb71d442 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/utils/directive/clickOutSide.js @@ -0,0 +1,39 @@ +//outside.js + +const ctx = '@@clickoutsideContext' + +export default { + bind(el, binding, vnode) { + const ele = el + const documentHandler = (e) => { + if (!vnode.context || ele.contains(e.target)) { + return false + } + // 调用指令回调 + if (binding.expression) { + vnode.context[el[ctx].methodName](e) + } else { + el[ctx].bindingFn(e) + } + } + // 将方法添加到ele + ele[ctx] = { + documentHandler, + methodName: binding.expression, + bindingFn: binding.value + } + + setTimeout(() => { + document.addEventListener('touchstart', documentHandler) // 为document绑定事件 + }) + }, + update(el, binding) { + const ele = el + ele[ctx].methodName = binding.expression + ele[ctx].bindingFn = binding.value + }, + unbind(el) { + document.removeEventListener('touchstart', el[ctx].documentHandler) // 解绑 + delete el[ctx] + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/utils/index.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/utils/index.js new file mode 100644 index 00000000..7d970ecd --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/utils/index.js @@ -0,0 +1,10 @@ +export function debounce(fn, delay = 500) { + let timer + return function (...args) { + if (timer) { + clearTimeout(timer) + timer = null + } + timer = setTimeout(fn.bind(this, ...args), delay) + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/utils/xml2json.js b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/utils/xml2json.js new file mode 100644 index 00000000..fe1a52fb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/bpmnProcessDesigner/src/utils/xml2json.js @@ -0,0 +1,50 @@ +function xmlStr2XmlObj(xmlStr) { + let xmlObj = {} + if (document.all) { + const xmlDom = new window.ActiveXObject('Microsoft.XMLDOM') + xmlDom.loadXML(xmlStr) + xmlObj = xmlDom + } else { + xmlObj = new DOMParser().parseFromString(xmlStr, 'text/xml') + } + return xmlObj +} + +function xml2json(xml) { + try { + let obj = {} + if (xml.children.length > 0) { + for (let i = 0; i < xml.children.length; i++) { + const item = xml.children.item(i) + const nodeName = item.nodeName + if (typeof obj[nodeName] == 'undefined') { + obj[nodeName] = xml2json(item) + } else { + if (typeof obj[nodeName].push == 'undefined') { + const old = obj[nodeName] + obj[nodeName] = [] + obj[nodeName].push(old) + } + obj[nodeName].push(xml2json(item)) + } + } + } else { + obj = xml.textContent + } + return obj + } catch (e) { + console.log(e.message) + } +} + +function xmlObj2json(xml) { + const xmlObj = xmlStr2XmlObj(xml) + console.log(xmlObj) + let jsonObj = {} + if (xmlObj.childNodes.length > 0) { + jsonObj = xml2json(xmlObj) + } + return jsonObj +} + +export default xmlObj2json diff --git a/mes-ui/mes-ui-admin-vue3/src/components/index.ts b/mes-ui/mes-ui-admin-vue3/src/components/index.ts new file mode 100644 index 00000000..4d030c37 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/components/index.ts @@ -0,0 +1,6 @@ +import type { App } from 'vue' +import { Icon } from './Icon' + +export const setupGlobCom = (app: App): void => { + app.component('Icon', Icon) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/config/axios/config.ts b/mes-ui/mes-ui-admin-vue3/src/config/axios/config.ts new file mode 100644 index 00000000..81165087 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/config/axios/config.ts @@ -0,0 +1,28 @@ +const config: { + base_url: string + result_code: number | string + default_headers: AxiosHeaders + request_timeout: number +} = { + /** + * api请求基础路径 + */ + base_url: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL, + /** + * 接口成功返回状态码 + */ + result_code: 200, + + /** + * 接口请求超时时间 + */ + request_timeout: 30000, + + /** + * 默认接口请求类型 + * 可选值:application/x-www-form-urlencoded multipart/form-data + */ + default_headers: 'application/json' +} + +export { config } diff --git a/mes-ui/mes-ui-admin-vue3/src/config/axios/errorCode.ts b/mes-ui/mes-ui-admin-vue3/src/config/axios/errorCode.ts new file mode 100644 index 00000000..94d719f8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/config/axios/errorCode.ts @@ -0,0 +1,6 @@ +export default { + '401': '认证失败,无法访问系统资源', + '403': '当前操作没有权限', + '404': '访问资源不存在', + default: '系统未知错误,请反馈给管理员' +} diff --git a/mes-ui/mes-ui-admin-vue3/src/config/axios/index.ts b/mes-ui/mes-ui-admin-vue3/src/config/axios/index.ts new file mode 100644 index 00000000..79e558da --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/config/axios/index.ts @@ -0,0 +1,51 @@ +import { service } from './service' + +import { config } from './config' + +const { default_headers } = config + +const request = (option: any) => { + const { url, method, params, data, headersType, responseType, ...config } = option + return service({ + url: url, + method, + params, + data, + ...config, + responseType: responseType, + headers: { + 'Content-Type': headersType || default_headers + } + }) +} +export default { + get: async (option: any) => { + const res = await request({ method: 'GET', ...option }) + return res.data as unknown as T + }, + post: async (option: any) => { + const res = await request({ method: 'POST', ...option }) + return res.data as unknown as T + }, + postOriginal: async (option: any) => { + const res = await request({ method: 'POST', ...option }) + return res + }, + delete: async (option: any) => { + const res = await request({ method: 'DELETE', ...option }) + return res.data as unknown as T + }, + put: async (option: any) => { + const res = await request({ method: 'PUT', ...option }) + return res.data as unknown as T + }, + download: async (option: any) => { + const res = await request({ method: 'GET', responseType: 'blob', ...option }) + return res as unknown as Promise + }, + upload: async (option: any) => { + option.headersType = 'multipart/form-data' + const res = await request({ method: 'POST', ...option }) + return res as unknown as Promise + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/config/axios/service.ts b/mes-ui/mes-ui-admin-vue3/src/config/axios/service.ts new file mode 100644 index 00000000..6413e945 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/config/axios/service.ts @@ -0,0 +1,243 @@ +import axios, { + AxiosError, + AxiosInstance, + AxiosRequestHeaders, + AxiosResponse, + InternalAxiosRequestConfig +} from 'axios' + +import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' +import qs from 'qs' +import { config } from '@/config/axios/config' +import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth' +import errorCode from './errorCode' + +import { resetRouter } from '@/router' +import { useCache } from '@/hooks/web/useCache' + +const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE +const { result_code, base_url, request_timeout } = config + +// 需要忽略的提示。忽略后,自动 Promise.reject('error') +const ignoreMsgs = [ + '无效的刷新令牌', // 刷新令牌被删除时,不用提示 + '刷新令牌已过期' // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面 +] +// 是否显示重新登录 +export const isRelogin = { show: false } +// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现 +// 请求队列 +let requestList: any[] = [] +// 是否正在刷新中 +let isRefreshToken = false +// 请求白名单,无须token的接口 +const whiteList: string[] = ['/login', '/refresh-token'] + +// 创建axios实例 +const service: AxiosInstance = axios.create({ + baseURL: base_url, // api 的 base_url + timeout: request_timeout, // 请求超时时间 + withCredentials: false // 禁用 Cookie 等信息 +}) + +// request拦截器 +service.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + // 是否需要设置 token + let isToken = (config!.headers || {}).isToken === false + whiteList.some((v) => { + if (config.url) { + config.url.indexOf(v) > -1 + return (isToken = false) + } + }) + if (getAccessToken() && !isToken) { + ;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token + } + // 设置租户 + if (tenantEnable && tenantEnable === 'true') { + const tenantId = getTenantId() + if (tenantId) (config as Recordable).headers['tenant-id'] = tenantId + } + const params = config.params || {} + const data = config.data || false + if ( + config.method?.toUpperCase() === 'POST' && + (config.headers as AxiosRequestHeaders)['Content-Type'] === + 'application/x-www-form-urlencoded' + ) { + config.data = qs.stringify(data) + } + // get参数编码 + if (config.method?.toUpperCase() === 'GET' && params) { + let url = config.url + '?' + for (const propName of Object.keys(params)) { + const value = params[propName] + if (value !== void 0 && value !== null && typeof value !== 'undefined') { + if (typeof value === 'object') { + for (const val of Object.keys(value)) { + const params = propName + '[' + val + ']' + const subPart = encodeURIComponent(params) + '=' + url += subPart + encodeURIComponent(value[val]) + '&' + } + } else { + url += `${propName}=${encodeURIComponent(value)}&` + } + } + } + // 给 get 请求加上时间戳参数,避免从缓存中拿数据 + // const now = new Date().getTime() + // params = params.substring(0, url.length - 1) + `?_t=${now}` + url = url.slice(0, -1) + config.params = {} + config.url = url + } + return config + }, + (error: AxiosError) => { + // Do something with request error + console.log(error) // for debug + Promise.reject(error) + } +) + +// response 拦截器 +service.interceptors.response.use( + async (response: AxiosResponse) => { + const { data } = response + const config = response.config + if (!data) { + // 返回“[HTTP]请求没有返回值”; + throw new Error() + } + const { t } = useI18n() + // 未设置状态码则默认成功状态 + const code = data.code || result_code + // 二进制数据则直接返回 + if ( + response.request.responseType === 'blob' || + response.request.responseType === 'arraybuffer' + ) { + return response.data + } + // 获取错误信息 + const msg = data.msg || errorCode[code] || errorCode['default'] + if (ignoreMsgs.indexOf(msg) !== -1) { + // 如果是忽略的错误码,直接返回 msg 异常 + return Promise.reject(msg) + } else if (code === 401) { + // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了 + if (!isRefreshToken) { + isRefreshToken = true + // 1. 如果获取不到刷新令牌,则只能执行登出操作 + if (!getRefreshToken()) { + return handleAuthorized() + } + // 2. 进行刷新访问令牌 + try { + const refreshTokenRes = await refreshToken() + // 2.1 刷新成功,则回放队列的请求 + 当前请求 + setToken((await refreshTokenRes).data.data) + config.headers!.Authorization = 'Bearer ' + getAccessToken() + requestList.forEach((cb: any) => { + cb() + }) + requestList = [] + return service(config) + } catch (e) { + // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。 + // 2.2 刷新失败,只回放队列的请求 + requestList.forEach((cb: any) => { + cb() + }) + // 提示是否要登出。即不回放当前请求!不然会形成递归 + return handleAuthorized() + } finally { + requestList = [] + isRefreshToken = false + } + } else { + // 添加到队列,等待刷新获取到新的令牌 + return new Promise((resolve) => { + requestList.push(() => { + config.headers!.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改 + resolve(service(config)) + }) + }) + } + } else if (code === 500) { + ElMessage.error(t('sys.api.errMsg500')) + return Promise.reject(new Error(msg)) + } else if (code === 901) { + ElMessage.error({ + offset: 300, + dangerouslyUseHTMLString: true, + message: + '
' + + t('sys.api.errMsg901') + + '
' + + '
 
' + + '
参考 https://doc.iocoder.cn/ 教程
' + + '
 
' + + '
5 分钟搭建本地环境
' + }) + return Promise.reject(new Error(msg)) + } else if (code !== 200) { + if (msg === '无效的刷新令牌') { + // hard coding:忽略这个提示,直接登出 + console.log(msg) + } else { + ElNotification.error({ title: msg }) + } + return Promise.reject('error') + } else { + return data + } + }, + (error: AxiosError) => { + console.log('err' + error) // for debug + let { message } = error + const { t } = useI18n() + if (message === 'Network Error') { + message = t('sys.api.errorMessage') + } else if (message.includes('timeout')) { + message = t('sys.api.apiTimeoutMessage') + } else if (message.includes('Request failed with status code')) { + message = t('sys.api.apiRequestFailed') + message.substr(message.length - 3) + } + ElMessage.error(message) + return Promise.reject(error) + } +) + +const refreshToken = async () => { + axios.defaults.headers.common['tenant-id'] = getTenantId() + return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken()) +} +const handleAuthorized = () => { + const { t } = useI18n() + if (!isRelogin.show) { + // 如果已经到重新登录页面则不进行弹窗提示 + if (window.location.href.includes('login?redirect=')) { + return + } + isRelogin.show = true + ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), { + showCancelButton: false, + closeOnClickModal: false, + showClose: false, + confirmButtonText: t('login.relogin'), + type: 'warning' + }).then(() => { + const { wsCache } = useCache() + resetRouter() // 重置静态路由表 + wsCache.clear() + removeToken() + isRelogin.show = false + // 干掉token后再走一次路由让它过router.beforeEach的校验 + window.location.href = window.location.href + }) + } + return Promise.reject(t('sys.api.timeoutMessage')) +} +export { service } diff --git a/mes-ui/mes-ui-admin-vue3/src/directives/index.ts b/mes-ui/mes-ui-admin-vue3/src/directives/index.ts new file mode 100644 index 00000000..89cc8ba1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/directives/index.ts @@ -0,0 +1,13 @@ +import type { App } from 'vue' +import { hasRole } from './permission/hasRole' +import { hasPermi } from './permission/hasPermi' + +/** + * 导出指令:v-xxx + * @methods hasRole 用户权限,用法: v-hasRole + * @methods hasPermi 按钮权限,用法: v-hasPermi + */ +export const setupAuth = (app: App) => { + hasRole(app) + hasPermi(app) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/directives/permission/hasPermi.ts b/mes-ui/mes-ui-admin-vue3/src/directives/permission/hasPermi.ts new file mode 100644 index 00000000..d86d2f54 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/directives/permission/hasPermi.ts @@ -0,0 +1,27 @@ +import type { App } from 'vue' +import { CACHE_KEY, useCache } from '@/hooks/web/useCache' + +const { t } = useI18n() // 国际化 + +export function hasPermi(app: App) { + app.directive('hasPermi', (el, binding) => { + const { wsCache } = useCache() + const { value } = binding + const all_permission = '*:*:*' + const permissions = wsCache.get(CACHE_KEY.USER).permissions + + if (value && value instanceof Array && value.length > 0) { + const permissionFlag = value + + const hasPermissions = permissions.some((permission: string) => { + return all_permission === permission || permissionFlag.includes(permission) + }) + + if (!hasPermissions) { + el.parentNode && el.parentNode.removeChild(el) + } + } else { + throw new Error(t('permission.hasPermission')) + } + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/directives/permission/hasRole.ts b/mes-ui/mes-ui-admin-vue3/src/directives/permission/hasRole.ts new file mode 100644 index 00000000..31a352a7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/directives/permission/hasRole.ts @@ -0,0 +1,27 @@ +import type { App } from 'vue' +import { CACHE_KEY, useCache } from '@/hooks/web/useCache' + +const { t } = useI18n() // 国际化 + +export function hasRole(app: App) { + app.directive('hasRole', (el, binding) => { + const { wsCache } = useCache() + const { value } = binding + const super_admin = 'admin' + const roles = wsCache.get(CACHE_KEY.USER).roles + + if (value && value instanceof Array && value.length > 0) { + const roleFlag = value + + const hasRole = roles.some((role: string) => { + return super_admin === role || roleFlag.includes(role) + }) + + if (!hasRole) { + el.parentNode && el.parentNode.removeChild(el) + } + } else { + throw new Error(t('permission.hasRole')) + } + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/event/useScrollTo.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/event/useScrollTo.ts new file mode 100644 index 00000000..92aec872 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/event/useScrollTo.ts @@ -0,0 +1,60 @@ +export interface ScrollToParams { + el: HTMLElement + to: number + position: string + duration?: number + callback?: () => void +} + +const easeInOutQuad = (t: number, b: number, c: number, d: number) => { + t /= d / 2 + if (t < 1) { + return (c / 2) * t * t + b + } + t-- + return (-c / 2) * (t * (t - 2) - 1) + b +} +const move = (el: HTMLElement, position: string, amount: number) => { + el[position] = amount +} + +export function useScrollTo({ + el, + position = 'scrollLeft', + to, + duration = 500, + callback +}: ScrollToParams) { + const isActiveRef = ref(false) + const start = el[position] + const change = to - start + const increment = 20 + let currentTime = 0 + + function animateScroll() { + if (!unref(isActiveRef)) { + return + } + currentTime += increment + const val = easeInOutQuad(currentTime, start, change, duration) + move(el, position, val) + if (currentTime < duration && unref(isActiveRef)) { + requestAnimationFrame(animateScroll) + } else { + if (callback) { + callback() + } + } + } + + function run() { + isActiveRef.value = true + animateScroll() + } + + function stop() { + isActiveRef.value = false + } + + return { start: run, stop } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useCache.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useCache.ts new file mode 100644 index 00000000..6d2a9318 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useCache.ts @@ -0,0 +1,27 @@ +/** + * 配置浏览器本地存储的方式,可直接存储对象数组。 + */ + +import WebStorageCache from 'web-storage-cache' + +type CacheType = 'localStorage' | 'sessionStorage' + +export const CACHE_KEY = { + IS_DARK: 'isDark', + USER: 'user', + LANG: 'lang', + THEME: 'theme', + LAYOUT: 'layout', + ROLE_ROUTERS: 'roleRouters', + DICT_CACHE: 'dictCache' +} + +export const useCache = (type: CacheType = 'localStorage') => { + const wsCache: WebStorageCache = new WebStorageCache({ + storage: type + }) + + return { + wsCache + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useConfigGlobal.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useConfigGlobal.ts new file mode 100644 index 00000000..afb3db3a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useConfigGlobal.ts @@ -0,0 +1,9 @@ +import { ConfigGlobalTypes } from '@/types/configGlobal' + +export const useConfigGlobal = () => { + const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes + + return { + configGlobal + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useCrudSchemas.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useCrudSchemas.ts new file mode 100644 index 00000000..458b57ec --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useCrudSchemas.ts @@ -0,0 +1,326 @@ +import { reactive } from 'vue' +import { AxiosPromise } from 'axios' +import { findIndex } from '@/utils' +import { eachTree, filter, treeMap } from '@/utils/tree' +import { getBoolDictOptions, getDictOptions, getIntDictOptions } from '@/utils/dict' + +import { FormSchema } from '@/types/form' +import { TableColumn } from '@/types/table' +import { DescriptionsSchema } from '@/types/descriptions' +import { ComponentOptions, ComponentProps } from '@/types/components' +import { DictTag } from '@/components/DictTag' +import { cloneDeep, merge } from 'lodash-es' + +export type CrudSchema = Omit & { + isSearch?: boolean // 是否在查询显示 + search?: CrudSearchParams // 查询的详细配置 + isTable?: boolean // 是否在列表显示 + table?: CrudTableParams // 列表的详细配置 + isForm?: boolean // 是否在表单显示 + form?: CrudFormParams // 表单的详细配置 + isDetail?: boolean // 是否在详情显示 + detail?: CrudDescriptionsParams // 详情的详细配置 + children?: CrudSchema[] + dictType?: string // 字典类型 + dictClass?: 'string' | 'number' | 'boolean' // 字典数据类型 string | number | boolean +} + +type CrudSearchParams = { + // 是否显示在查询项 + show?: boolean + // 接口 + api?: () => Promise + // 搜索字段 + field?: string +} & Omit + +type CrudTableParams = { + // 是否显示表头 + show?: boolean + // 列宽配置 + width?: number | string + // 列是否固定在左侧或者右侧 + fixed?: 'left' | 'right' +} & Omit +type CrudFormParams = { + // 是否显示表单项 + show?: boolean + // 接口 + api?: () => Promise +} & Omit + +type CrudDescriptionsParams = { + // 是否显示表单项 + show?: boolean +} & Omit + +interface AllSchemas { + searchSchema: FormSchema[] + tableColumns: TableColumn[] + formSchema: FormSchema[] + detailSchema: DescriptionsSchema[] +} + +const { t } = useI18n() + +// 过滤所有结构 +export const useCrudSchemas = ( + crudSchema: CrudSchema[] +): { + allSchemas: AllSchemas +} => { + // 所有结构数据 + const allSchemas = reactive({ + searchSchema: [], + tableColumns: [], + formSchema: [], + detailSchema: [] + }) + + const searchSchema = filterSearchSchema(crudSchema, allSchemas) + allSchemas.searchSchema = searchSchema || [] + + const tableColumns = filterTableSchema(crudSchema) + allSchemas.tableColumns = tableColumns || [] + + const formSchema = filterFormSchema(crudSchema, allSchemas) + allSchemas.formSchema = formSchema + + const detailSchema = filterDescriptionsSchema(crudSchema) + allSchemas.detailSchema = detailSchema + + return { + allSchemas + } +} + +// 过滤 Search 结构 +const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => { + const searchSchema: FormSchema[] = [] + + // 获取字典列表队列 + const searchRequestTask: Array<() => Promise> = [] + eachTree(crudSchema, (schemaItem: CrudSchema) => { + // 判断是否显示 + if (schemaItem?.isSearch || schemaItem.search?.show) { + let component = schemaItem?.search?.component || 'Input' + const options: ComponentOptions[] = [] + let comonentProps: ComponentProps = {} + if (schemaItem.dictType) { + const allOptions: ComponentOptions = { label: '全部', value: '' } + options.push(allOptions) + getDictOptions(schemaItem.dictType).forEach((dict) => { + options.push(dict) + }) + comonentProps = { + options: options + } + if (!schemaItem.search?.component) component = 'Select' + } + + // updated by AKing: 解决了当使用默认的dict选项时,form中事件不能触发的问题 + const searchSchemaItem = merge( + { + // 默认为 input + component, + ...schemaItem.search, + field: schemaItem.field, + label: schemaItem.search?.label || schemaItem.label + }, + { componentProps: comonentProps } + ) + if (searchSchemaItem.api) { + searchRequestTask.push(async () => { + const res = await (searchSchemaItem.api as () => AxiosPromise)() + if (res) { + const index = findIndex(allSchemas.searchSchema, (v: FormSchema) => { + return v.field === searchSchemaItem.field + }) + if (index !== -1) { + allSchemas.searchSchema[index]!.componentProps!.options = filterOptions( + res, + searchSchemaItem.componentProps.optionsAlias?.labelField + ) + } + } + }) + } + // 删除不必要的字段 + delete searchSchemaItem.show + + searchSchema.push(searchSchemaItem) + } + }) + for (const task of searchRequestTask) { + task() + } + return searchSchema +} + +// 过滤 table 结构 +const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => { + const tableColumns = treeMap(crudSchema, { + conversion: (schema: CrudSchema) => { + if (schema?.isTable !== false && schema?.table?.show !== false) { + // add by 芋艿:增加对 dict 字典数据的支持 + if (!schema.formatter && schema.dictType) { + schema.formatter = (_: Recordable, __: TableColumn, cellValue: any) => { + return h(DictTag, { + type: schema.dictType!, // ! 表示一定不为空 + value: cellValue + }) + } + } + return { + ...schema.table, + ...schema + } + } + } + }) + + // 第一次过滤会有 undefined 所以需要二次过滤 + return filter(tableColumns as TableColumn[], (data) => { + if (data.children === void 0) { + delete data.children + } + return !!data.field + }) +} + +// 过滤 form 结构 +const filterFormSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => { + const formSchema: FormSchema[] = [] + + // 获取字典列表队列 + const formRequestTask: Array<() => Promise> = [] + + eachTree(crudSchema, (schemaItem: CrudSchema) => { + // 判断是否显示 + if (schemaItem?.isForm !== false && schemaItem?.form?.show !== false) { + let component = schemaItem?.form?.component || 'Input' + let defaultValue: any = '' + if (schemaItem.form?.value) { + defaultValue = schemaItem.form?.value + } else { + if (component === 'InputNumber') { + defaultValue = 0 + } + } + let comonentProps: ComponentProps = {} + if (schemaItem.dictType) { + const options: ComponentOptions[] = [] + if (schemaItem.dictClass && schemaItem.dictClass === 'number') { + getIntDictOptions(schemaItem.dictType).forEach((dict) => { + options.push(dict) + }) + } else if (schemaItem.dictClass && schemaItem.dictClass === 'boolean') { + getBoolDictOptions(schemaItem.dictType).forEach((dict) => { + options.push(dict) + }) + } else { + getDictOptions(schemaItem.dictType).forEach((dict) => { + options.push(dict) + }) + } + comonentProps = { + options: options + } + if (!(schemaItem.form && schemaItem.form.component)) component = 'Select' + } + + // updated by AKing: 解决了当使用默认的dict选项时,form中事件不能触发的问题 + const formSchemaItem = merge( + { + // 默认为 input + component, + value: defaultValue, + ...schemaItem.form, + field: schemaItem.field, + label: schemaItem.form?.label || schemaItem.label + }, + { componentProps: comonentProps } + ) + + if (formSchemaItem.api) { + formRequestTask.push(async () => { + const res = await (formSchemaItem.api as () => AxiosPromise)() + if (res) { + const index = findIndex(allSchemas.formSchema, (v: FormSchema) => { + return v.field === formSchemaItem.field + }) + if (index !== -1) { + allSchemas.formSchema[index]!.componentProps!.options = filterOptions( + res, + formSchemaItem.componentProps.optionsAlias?.labelField + ) + } + } + }) + } + + // 删除不必要的字段 + delete formSchemaItem.show + + formSchema.push(formSchemaItem) + } + }) + + for (const task of formRequestTask) { + task() + } + return formSchema +} + +// 过滤 descriptions 结构 +const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[] => { + const descriptionsSchema: FormSchema[] = [] + + eachTree(crudSchema, (schemaItem: CrudSchema) => { + // 判断是否显示 + if (schemaItem?.isDetail !== false && schemaItem.detail?.show !== false) { + const descriptionsSchemaItem = { + ...schemaItem.detail, + field: schemaItem.field, + label: schemaItem.detail?.label || schemaItem.label + } + if (schemaItem.dictType) { + descriptionsSchemaItem.dictType = schemaItem.dictType + } + if (schemaItem.detail?.dateFormat || schemaItem.formatter == 'formatDate') { + // 优先使用 detail 下的配置,如果没有默认为 YYYY-MM-DD HH:mm:ss + descriptionsSchemaItem.dateFormat = schemaItem?.detail?.dateFormat + ? schemaItem?.detail?.dateFormat + : 'YYYY-MM-DD HH:mm:ss' + } + + // 删除不必要的字段 + delete descriptionsSchemaItem.show + + descriptionsSchema.push(descriptionsSchemaItem) + } + }) + + return descriptionsSchema +} + +// 给options添加国际化 +const filterOptions = (options: Recordable, labelField?: string) => { + return options?.map((v: Recordable) => { + if (labelField) { + v['labelField'] = t(v.labelField) + } else { + v['label'] = t(v.label) + } + return v + }) +} + +// 将 tableColumns 指定 fields 放到最前面 +export const sortTableColumns = (tableColumns: TableColumn[], field: string) => { + const fieldIndex = tableColumns.findIndex((item) => item.field === field) + const fieldColumn = cloneDeep(tableColumns[fieldIndex]) + tableColumns.splice(fieldIndex, 1) + // 添加到开头 + tableColumns.unshift(fieldColumn) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useDesign.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useDesign.ts new file mode 100644 index 00000000..8ee3b38c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useDesign.ts @@ -0,0 +1,18 @@ +import variables from '@/styles/global.module.scss' + +export const useDesign = () => { + const scssVariables = variables + + /** + * @param scope 类名 + * @returns 返回空间名-类名 + */ + const getPrefixCls = (scope: string) => { + return `${scssVariables.namespace}-${scope}` + } + + return { + variables: scssVariables, + getPrefixCls + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useEmitt.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useEmitt.ts new file mode 100644 index 00000000..d4efea72 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useEmitt.ts @@ -0,0 +1,22 @@ +import mitt from 'mitt' + +interface Option { + name: string // 事件名称 + callback: Fn // 回调 +} + +const emitter = mitt() + +export const useEmitt = (option?: Option) => { + if (option) { + emitter.on(option.name, option.callback) + + onBeforeUnmount(() => { + emitter.off(option.name) + }) + } + + return { + emitter + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useForm.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useForm.ts new file mode 100644 index 00000000..53a8a94d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useForm.ts @@ -0,0 +1,94 @@ +import type { Form, FormExpose } from '@/components/Form' +import type { ElForm } from 'element-plus' +import type { FormProps } from '@/components/Form/src/types' +import { FormSchema, FormSetPropsType } from '@/types/form' + +export const useForm = (props?: FormProps) => { + // From实例 + const formRef = ref() + + // ElForm实例 + const elFormRef = ref>() + + /** + * @param ref Form实例 + * @param elRef ElForm实例 + */ + const register = (ref: typeof Form & FormExpose, elRef: ComponentRef) => { + formRef.value = ref + elFormRef.value = elRef + } + + const getForm = async () => { + await nextTick() + const form = unref(formRef) + if (!form) { + console.error('The form is not registered. Please use the register method to register') + } + return form + } + + // 一些内置的方法 + const methods: { + setProps: (props: Recordable) => void + setValues: (data: Recordable) => void + getFormData: () => Promise + setSchema: (schemaProps: FormSetPropsType[]) => void + addSchema: (formSchema: FormSchema, index?: number) => void + delSchema: (field: string) => void + } = { + setProps: async (props: FormProps = {}) => { + const form = await getForm() + form?.setProps(props) + if (props.model) { + form?.setValues(props.model) + } + }, + + setValues: async (data: Recordable) => { + const form = await getForm() + form?.setValues(data) + }, + + /** + * @param schemaProps 需要设置的schemaProps + */ + setSchema: async (schemaProps: FormSetPropsType[]) => { + const form = await getForm() + form?.setSchema(schemaProps) + }, + + /** + * @param formSchema 需要新增数据 + * @param index 在哪里新增 + */ + addSchema: async (formSchema: FormSchema, index?: number) => { + const form = await getForm() + form?.addSchema(formSchema, index) + }, + + /** + * @param field 删除哪个数据 + */ + delSchema: async (field: string) => { + const form = await getForm() + form?.delSchema(field) + }, + + /** + * @returns form data + */ + getFormData: async (): Promise => { + const form = await getForm() + return form?.formModel as T + } + } + + props && methods.setProps(props) + + return { + register, + elFormRef, + methods + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useGuide.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useGuide.ts new file mode 100644 index 00000000..7fd2fb09 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useGuide.ts @@ -0,0 +1,49 @@ +import { Config, driver } from 'driver.js' +import 'driver.js/dist/driver.css' +import { useDesign } from '@/hooks/web/useDesign' +import { useI18n } from '@/hooks/web/useI18n' + +const { t } = useI18n() + +const { variables } = useDesign() + +export const useGuide = (options?: Config) => { + const driverObj = driver( + options || { + showProgress: true, + nextBtnText: t('common.nextLabel'), + prevBtnText: t('common.prevLabel'), + doneBtnText: t('common.doneLabel'), + steps: [ + { + element: `#${variables.namespace}-menu`, + popover: { + title: t('common.menu'), + description: t('common.menuDes'), + side: 'right' + } + }, + { + element: `#${variables.namespace}-tool-header`, + popover: { + title: t('common.tool'), + description: t('common.toolDes'), + side: 'left' + } + }, + { + element: `#${variables.namespace}-tags-view`, + popover: { + title: t('common.tagsView'), + description: t('common.tagsViewDes'), + side: 'bottom' + } + } + ] + } + ) + + return { + ...driverObj + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useI18n.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useI18n.ts new file mode 100644 index 00000000..d1ab70fa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useI18n.ts @@ -0,0 +1,53 @@ +import { i18n } from '@/plugins/vueI18n' + +type I18nGlobalTranslation = { + (key: string): string + (key: string, locale: string): string + (key: string, locale: string, list: unknown[]): string + (key: string, locale: string, named: Record): string + (key: string, list: unknown[]): string + (key: string, named: Record): string +} + +type I18nTranslationRestParameters = [string, any] + +const getKey = (namespace: string | undefined, key: string) => { + if (!namespace) { + return key + } + if (key.startsWith(namespace)) { + return key + } + return `${namespace}.${key}` +} + +export const useI18n = ( + namespace?: string +): { + t: I18nGlobalTranslation +} => { + const normalFn = { + t: (key: string) => { + return getKey(namespace, key) + } + } + + if (!i18n) { + return normalFn + } + + const { t, ...methods } = i18n.global + + const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => { + if (!key) return '' + if (!key.includes('.') && !namespace) return key + //@ts-ignore + return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters)) + } + return { + ...methods, + t: tFn + } +} + +export const t = (key: string) => key diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useIcon.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useIcon.ts new file mode 100644 index 00000000..35002049 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useIcon.ts @@ -0,0 +1,8 @@ +import { h } from 'vue' +import type { VNode } from 'vue' +import { Icon } from '@/components/Icon' +import { IconTypes } from '@/types/icon' + +export const useIcon = (props: IconTypes): VNode => { + return h(Icon, props) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useLocale.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useLocale.ts new file mode 100644 index 00000000..c65070ef --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useLocale.ts @@ -0,0 +1,35 @@ +import { i18n } from '@/plugins/vueI18n' +import { useLocaleStoreWithOut } from '@/store/modules/locale' +import { setHtmlPageLang } from '@/plugins/vueI18n/helper' + +const setI18nLanguage = (locale: LocaleType) => { + const localeStore = useLocaleStoreWithOut() + + if (i18n.mode === 'legacy') { + i18n.global.locale = locale + } else { + ;(i18n.global.locale as any).value = locale + } + localeStore.setCurrentLocale({ + lang: locale + }) + setHtmlPageLang(locale) +} + +export const useLocale = () => { + // Switching the language will change the locale of useI18n + // And submit to configuration modification + const changeLocale = async (locale: LocaleType) => { + const globalI18n = i18n.global + + const langModule = await import(`../../locales/${locale}.ts`) + + globalI18n.setLocaleMessage(locale, langModule.default) + + setI18nLanguage(locale) + } + + return { + changeLocale + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useMessage.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useMessage.ts new file mode 100644 index 00000000..ac2b552e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useMessage.ts @@ -0,0 +1,95 @@ +import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' +import { useI18n } from './useI18n' +export const useMessage = () => { + const { t } = useI18n() + return { + // 消息提示 + info(content: string) { + ElMessage.info(content) + }, + // 错误消息 + error(content: string) { + ElMessage.error(content) + }, + // 成功消息 + success(content: string) { + ElMessage.success(content) + }, + // 警告消息 + warning(content: string) { + ElMessage.warning(content) + }, + // 弹出提示 + alert(content: string) { + ElMessageBox.alert(content, t('common.confirmTitle')) + }, + // 错误提示 + alertError(content: string) { + ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'error' }) + }, + // 成功提示 + alertSuccess(content: string) { + ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'success' }) + }, + // 警告提示 + alertWarning(content: string) { + ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'warning' }) + }, + // 通知提示 + notify(content: string) { + ElNotification.info(content) + }, + // 错误通知 + notifyError(content: string) { + ElNotification.error(content) + }, + // 成功通知 + notifySuccess(content: string) { + ElNotification.success(content) + }, + // 警告通知 + notifyWarning(content: string) { + ElNotification.warning(content) + }, + // 确认窗体 + confirm(content: string, tip?: string) { + return ElMessageBox.confirm(content, tip ? tip : t('common.confirmTitle'), { + confirmButtonText: t('common.ok'), + cancelButtonText: t('common.cancel'), + type: 'warning' + }) + }, + // 删除窗体 + delConfirm(content?: string, tip?: string) { + return ElMessageBox.confirm( + content ? content : t('common.delMessage'), + tip ? tip : t('common.confirmTitle'), + { + confirmButtonText: t('common.ok'), + cancelButtonText: t('common.cancel'), + type: 'warning' + } + ) + }, + // 导出窗体 + exportConfirm(content?: string, tip?: string) { + return ElMessageBox.confirm( + content ? content : t('common.exportMessage'), + tip ? tip : t('common.confirmTitle'), + { + confirmButtonText: t('common.ok'), + cancelButtonText: t('common.cancel'), + type: 'warning' + } + ) + }, + // 提交内容 + prompt(content: string, tip: string) { + return ElMessageBox.prompt(content, tip, { + confirmButtonText: t('common.ok'), + cancelButtonText: t('common.cancel'), + type: 'warning' + }) + } + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useNProgress.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useNProgress.ts new file mode 100644 index 00000000..6d8c0b91 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useNProgress.ts @@ -0,0 +1,33 @@ +import { useCssVar } from '@vueuse/core' +import type { NProgressOptions } from 'nprogress' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' + +const primaryColor = useCssVar('--el-color-primary', document.documentElement) + +export const useNProgress = () => { + NProgress.configure({ showSpinner: false } as NProgressOptions) + + const initColor = async () => { + await nextTick() + const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef + if (bar) { + bar.style.background = unref(primaryColor.value) + } + } + + initColor() + + const start = () => { + NProgress.start() + } + + const done = () => { + NProgress.done() + } + + return { + start, + done + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useNetwork.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useNetwork.ts new file mode 100644 index 00000000..66fa4464 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useNetwork.ts @@ -0,0 +1,21 @@ +import { ref, onBeforeUnmount } from 'vue' + +const useNetwork = () => { + const online = ref(true) + + const updateNetwork = () => { + online.value = navigator.onLine + } + + window.addEventListener('online', updateNetwork) + window.addEventListener('offline', updateNetwork) + + onBeforeUnmount(() => { + window.removeEventListener('online', updateNetwork) + window.removeEventListener('offline', updateNetwork) + }) + + return { online } +} + +export { useNetwork } diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useNow.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useNow.ts new file mode 100644 index 00000000..09d3176b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useNow.ts @@ -0,0 +1,60 @@ +import { dateUtil } from '@/utils/dateUtil' +import { reactive, toRefs } from 'vue' +import { tryOnMounted, tryOnUnmounted } from '@vueuse/core' + +export const useNow = (immediate = true) => { + let timer: IntervalHandle + + const state = reactive({ + year: 0, + month: 0, + week: '', + day: 0, + hour: '', + minute: '', + second: 0, + meridiem: '' + }) + + const update = () => { + const now = dateUtil() + + const h = now.format('HH') + const m = now.format('mm') + const s = now.get('s') + + state.year = now.get('y') + state.month = now.get('M') + 1 + state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()] + state.day = now.get('date') + state.hour = h + state.minute = m + state.second = s + + state.meridiem = now.format('A') + } + + function start() { + update() + clearInterval(timer) + timer = setInterval(() => update(), 1000) + } + + function stop() { + clearInterval(timer) + } + + tryOnMounted(() => { + immediate && start() + }) + + tryOnUnmounted(() => { + stop() + }) + + return { + ...toRefs(state), + start, + stop + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/usePageLoading.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/usePageLoading.ts new file mode 100644 index 00000000..bb89457d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/usePageLoading.ts @@ -0,0 +1,18 @@ +import { useAppStoreWithOut } from '@/store/modules/app' + +const appStore = useAppStoreWithOut() + +export const usePageLoading = () => { + const loadStart = () => { + appStore.setPageLoading(true) + } + + const loadDone = () => { + appStore.setPageLoading(false) + } + + return { + loadStart, + loadDone + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTable.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTable.ts new file mode 100644 index 00000000..361dd678 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTable.ts @@ -0,0 +1,223 @@ +import download from '@/utils/download' +import { Table, TableExpose } from '@/components/Table' +import { ElMessage, ElMessageBox, ElTable } from 'element-plus' +import { computed, nextTick, reactive, ref, unref, watch } from 'vue' +import type { TableProps } from '@/components/Table/src/types' + +import { TableSetPropsType } from '@/types/table' + +const { t } = useI18n() +interface ResponseType { + list: T[] + total?: number +} + +interface UseTableConfig { + getListApi: (option: any) => Promise + delListApi?: (option: any) => Promise + exportListApi?: (option: any) => Promise + // 返回数据格式配置 + response?: ResponseType + // 默认传递的参数 + defaultParams?: Recordable + props?: TableProps +} + +interface TableObject { + pageSize: number + currentPage: number + total: number + tableList: T[] + params: any + loading: boolean + exportLoading: boolean + currentRow: Nullable +} + +export const useTable = (config?: UseTableConfig) => { + const tableObject = reactive>({ + // 页数 + pageSize: 10, + // 当前页 + currentPage: 1, + // 总条数 + total: 10, + // 表格数据 + tableList: [], + // AxiosConfig 配置 + params: { + ...(config?.defaultParams || {}) + }, + // 加载中 + loading: true, + // 导出加载中 + exportLoading: false, + // 当前行的数据 + currentRow: null + }) + + const paramsObj = computed(() => { + return { + ...tableObject.params, + pageSize: tableObject.pageSize, + pageNo: tableObject.currentPage + } + }) + + watch( + () => tableObject.currentPage, + () => { + methods.getList() + } + ) + + watch( + () => tableObject.pageSize, + () => { + // 当前页不为1时,修改页数后会导致多次调用getList方法 + if (tableObject.currentPage === 1) { + methods.getList() + } else { + tableObject.currentPage = 1 + methods.getList() + } + } + ) + + // Table实例 + const tableRef = ref() + + // ElTable实例 + const elTableRef = ref>() + + const register = (ref: typeof Table & TableExpose, elRef: ComponentRef) => { + tableRef.value = ref + elTableRef.value = elRef + } + + const getTable = async () => { + await nextTick() + const table = unref(tableRef) + if (!table) { + console.error('The table is not registered. Please use the register method to register') + } + return table + } + + const delData = async (ids: string | number | string[] | number[]) => { + let idsLength = 1 + if (ids instanceof Array) { + idsLength = ids.length + await Promise.all( + ids.map(async (id: string | number) => { + await (config?.delListApi && config?.delListApi(id)) + }) + ) + } else { + await (config?.delListApi && config?.delListApi(ids)) + } + ElMessage.success(t('common.delSuccess')) + + // 计算出临界点 + tableObject.currentPage = + tableObject.total % tableObject.pageSize === idsLength || tableObject.pageSize === 1 + ? tableObject.currentPage > 1 + ? tableObject.currentPage - 1 + : tableObject.currentPage + : tableObject.currentPage + await methods.getList() + } + + const methods = { + getList: async () => { + tableObject.loading = true + const res = await config?.getListApi(unref(paramsObj)).finally(() => { + tableObject.loading = false + }) + if (res) { + tableObject.tableList = (res as unknown as ResponseType).list + tableObject.total = (res as unknown as ResponseType).total ?? 0 + } + }, + setProps: async (props: TableProps = {}) => { + const table = await getTable() + table?.setProps(props) + }, + setColumn: async (columnProps: TableSetPropsType[]) => { + const table = await getTable() + table?.setColumn(columnProps) + }, + getSelections: async () => { + const table = await getTable() + return (table?.selections || []) as T[] + }, + // 与Search组件结合 + setSearchParams: (data: Recordable) => { + tableObject.params = Object.assign(tableObject.params, { + pageSize: tableObject.pageSize, + pageNo: 1, + ...data + }) + // 页码不等于1时更新页码重新获取数据,页码等于1时重新获取数据 + if (tableObject.currentPage !== 1) { + tableObject.currentPage = 1 + } else { + methods.getList() + } + }, + // 删除数据 + delList: async ( + ids: string | number | string[] | number[], + multiple: boolean, + message = true + ) => { + const tableRef = await getTable() + if (multiple) { + if (!tableRef?.selections.length) { + ElMessage.warning(t('common.delNoData')) + return + } + } + if (message) { + ElMessageBox.confirm(t('common.delMessage'), t('common.confirmTitle'), { + confirmButtonText: t('common.ok'), + cancelButtonText: t('common.cancel'), + type: 'warning' + }).then(async () => { + await delData(ids) + }) + } else { + await delData(ids) + } + }, + // 导出列表 + exportList: async (fileName: string) => { + tableObject.exportLoading = true + ElMessageBox.confirm(t('common.exportMessage'), t('common.confirmTitle'), { + confirmButtonText: t('common.ok'), + cancelButtonText: t('common.cancel'), + type: 'warning' + }) + .then(async () => { + const res = await config?.exportListApi?.(unref(paramsObj) as unknown as T) + if (res) { + download.excel(res as unknown as Blob, fileName) + } + }) + .finally(() => { + tableObject.exportLoading = false + }) + } + } + + config?.props && methods.setProps(config.props) + + return { + register, + elTableRef, + tableObject, + methods, + // add by 芋艿:返回 tableMethods 属性,和 tableObject 更统一 + tableMethods: methods + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTagsView.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTagsView.ts new file mode 100644 index 00000000..31eadb02 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTagsView.ts @@ -0,0 +1,63 @@ +import { useTagsViewStoreWithOut } from '@/store/modules/tagsView' +import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router' +import { computed, nextTick, unref } from 'vue' + +export const useTagsView = () => { + const tagsViewStore = useTagsViewStoreWithOut() + + const { replace, currentRoute } = useRouter() + + const selectedTag = computed(() => tagsViewStore.getSelectedTag) + + const closeAll = (callback?: Fn) => { + tagsViewStore.delAllViews() + callback?.() + } + + const closeLeft = (callback?: Fn) => { + tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeRight = (callback?: Fn) => { + tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeOther = (callback?: Fn) => { + tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { + if (view?.meta?.affix) return + tagsViewStore.delView(view || unref(currentRoute)) + + callback?.() + } + + const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { + tagsViewStore.delCachedView() + const { path, query } = view || unref(currentRoute) + await nextTick() + replace({ + path: '/redirect' + path, + query: query + }) + callback?.() + } + + const setTitle = (title: string, path?: string) => { + tagsViewStore.setTitle(title, path) + } + + return { + closeAll, + closeLeft, + closeRight, + closeOther, + closeCurrent, + refreshPage, + setTitle + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTimeAgo.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTimeAgo.ts new file mode 100644 index 00000000..a6da2819 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTimeAgo.ts @@ -0,0 +1,49 @@ +import { useTimeAgo as useTimeAgoCore, UseTimeAgoMessages } from '@vueuse/core' +import { useLocaleStoreWithOut } from '@/store/modules/locale' + +const TIME_AGO_MESSAGE_MAP: { + 'zh-CN': UseTimeAgoMessages + en: UseTimeAgoMessages +} = { + // @ts-ignore + 'zh-CN': { + justNow: '刚刚', + past: (n) => (n.match(/\d/) ? `${n}前` : n), + future: (n) => (n.match(/\d/) ? `${n}后` : n), + month: (n, past) => (n === 1 ? (past ? '上个月' : '下个月') : `${n} 个月`), + year: (n, past) => (n === 1 ? (past ? '去年' : '明年') : `${n} 年`), + day: (n, past) => (n === 1 ? (past ? '昨天' : '明天') : `${n} 天`), + week: (n, past) => (n === 1 ? (past ? '上周' : '下周') : `${n} 周`), + hour: (n) => `${n} 小时`, + minute: (n) => `${n} 分钟`, + second: (n) => `${n} 秒` + }, + // @ts-ignore + en: { + justNow: 'just now', + past: (n) => (n.match(/\d/) ? `${n} ago` : n), + future: (n) => (n.match(/\d/) ? `in ${n}` : n), + month: (n, past) => + n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`, + year: (n, past) => + n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`, + day: (n, past) => (n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`), + week: (n, past) => + n === 1 ? (past ? 'last week' : 'next week') : `${n} week${n > 1 ? 's' : ''}`, + hour: (n) => `${n} hour${n > 1 ? 's' : ''}`, + minute: (n) => `${n} minute${n > 1 ? 's' : ''}`, + second: (n) => `${n} second${n > 1 ? 's' : ''}` + } +} + +export const useTimeAgo = (time: Date | number | string) => { + const localeStore = useLocaleStoreWithOut() + + const currentLocale = computed(() => localeStore.getCurrentLocale) + + const timeAgo = useTimeAgoCore(time, { + messages: TIME_AGO_MESSAGE_MAP[unref(currentLocale).lang] + }) + + return timeAgo +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTitle.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTitle.ts new file mode 100644 index 00000000..020a9b77 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useTitle.ts @@ -0,0 +1,24 @@ +import { watch, ref } from 'vue' +import { isString } from '@/utils/is' +import { useAppStoreWithOut } from '@/store/modules/app' + +const appStore = useAppStoreWithOut() + +export const useTitle = (newTitle?: string) => { + const { t } = useI18n() + const title = ref( + newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle + ) + + watch( + title, + (n, o) => { + if (isString(n) && n !== o && document) { + document.title = n + } + }, + { immediate: true } + ) + + return title +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useValidator.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useValidator.ts new file mode 100644 index 00000000..151e35b2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useValidator.ts @@ -0,0 +1,60 @@ +import { useI18n } from '@/hooks/web/useI18n' +import { FormItemRule } from 'element-plus' + +const { t } = useI18n() + +interface LengthRange { + min: number + max: number + message?: string +} + +export const useValidator = () => { + const required = (message?: string): FormItemRule => { + return { + required: true, + message: message || t('common.required') + } + } + + const lengthRange = (options: LengthRange): FormItemRule => { + const { min, max, message } = options + + return { + min, + max, + message: message || t('common.lengthRange', { min, max }) + } + } + + const notSpace = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (val?.indexOf(' ') !== -1) { + callback(new Error(message || t('common.notSpace'))) + } else { + callback() + } + } + } + } + + const notSpecialCharacters = (message?: string): FormItemRule => { + return { + validator: (_, val, callback) => { + if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { + callback(new Error(message || t('common.notSpecialCharacters'))) + } else { + callback() + } + } + } + } + + return { + required, + lengthRange, + notSpace, + notSpecialCharacters + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/hooks/web/useWatermark.ts b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useWatermark.ts new file mode 100644 index 00000000..4a313594 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/hooks/web/useWatermark.ts @@ -0,0 +1,55 @@ +const domSymbol = Symbol('watermark-dom') + +export function useWatermark(appendEl: HTMLElement | null = document.body) { + let func: Fn = () => {} + const id = domSymbol.toString() + const clear = () => { + const domId = document.getElementById(id) + if (domId) { + const el = appendEl + el && el.removeChild(domId) + } + window.removeEventListener('resize', func) + } + const createWatermark = (str: string) => { + clear() + + const can = document.createElement('canvas') + can.width = 300 + can.height = 240 + + const cans = can.getContext('2d') + if (cans) { + cans.rotate((-20 * Math.PI) / 120) + cans.font = '15px Vedana' + cans.fillStyle = 'rgba(0, 0, 0, 0.15)' + cans.textAlign = 'left' + cans.textBaseline = 'middle' + cans.fillText(str, can.width / 20, can.height) + } + + const div = document.createElement('div') + div.id = id + div.style.pointerEvents = 'none' + div.style.top = '0px' + div.style.left = '0px' + div.style.position = 'absolute' + div.style.zIndex = '100000000' + div.style.width = document.documentElement.clientWidth + 'px' + div.style.height = document.documentElement.clientHeight + 'px' + div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat' + const el = appendEl + el && el.appendChild(div) + return id + } + + function setWatermark(str: string) { + createWatermark(str) + func = () => { + createWatermark(str) + } + window.addEventListener('resize', func) + } + + return { setWatermark, clear } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/Layout.vue b/mes-ui/mes-ui-admin-vue3/src/layout/Layout.vue new file mode 100644 index 00000000..43f9b69d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/Layout.vue @@ -0,0 +1,78 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/AppView.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/AppView.vue new file mode 100644 index 00000000..ffdf11f5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/AppView.vue @@ -0,0 +1,61 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Breadcrumb/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Breadcrumb/index.ts new file mode 100644 index 00000000..93ffe705 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Breadcrumb/index.ts @@ -0,0 +1,3 @@ +import Breadcrumb from './src/Breadcrumb.vue' + +export { Breadcrumb } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Breadcrumb/src/Breadcrumb.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Breadcrumb/src/Breadcrumb.vue new file mode 100644 index 00000000..4079a066 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Breadcrumb/src/Breadcrumb.vue @@ -0,0 +1,130 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Breadcrumb/src/helper.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Breadcrumb/src/helper.ts new file mode 100644 index 00000000..fb3ec197 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Breadcrumb/src/helper.ts @@ -0,0 +1,31 @@ +import { pathResolve } from '@/utils/routerHelper' +import type { RouteMeta } from 'vue-router' + +export const filterBreadcrumb = ( + routes: AppRouteRecordRaw[], + parentPath = '' +): AppRouteRecordRaw[] => { + const res: AppRouteRecordRaw[] = [] + + for (const route of routes) { + const meta = route?.meta as RouteMeta + if (meta.hidden && !meta.canTo) { + continue + } + + const data: AppRouteRecordRaw = + !meta.alwaysShow && route.children?.length === 1 + ? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) } + : { ...route } + + data.path = pathResolve(parentPath, data.path) + + if (data.children) { + data.children = filterBreadcrumb(data.children, data.path) + } + if (data) { + res.push(data) + } + } + return res +} diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Collapse/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Collapse/index.ts new file mode 100644 index 00000000..73f65a3a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Collapse/index.ts @@ -0,0 +1,3 @@ +import Collapse from './src/Collapse.vue' + +export { Collapse } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Collapse/src/Collapse.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Collapse/src/Collapse.vue new file mode 100644 index 00000000..ecb6890f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Collapse/src/Collapse.vue @@ -0,0 +1,36 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/ContextMenu/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/ContextMenu/index.ts new file mode 100644 index 00000000..2a7c1f0d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/ContextMenu/index.ts @@ -0,0 +1,10 @@ +import ContextMenu from './src/ContextMenu.vue' +import { ElDropdown } from 'element-plus' +import type { RouteLocationNormalizedLoaded } from 'vue-router' + +export interface ContextMenuExpose { + elDropdownMenuRef: ComponentRef + tagItem: RouteLocationNormalizedLoaded +} + +export { ContextMenu } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/ContextMenu/src/ContextMenu.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/ContextMenu/src/ContextMenu.vue new file mode 100644 index 00000000..90eea4c2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/ContextMenu/src/ContextMenu.vue @@ -0,0 +1,76 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Footer/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Footer/index.ts new file mode 100644 index 00000000..bd052e0c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Footer/index.ts @@ -0,0 +1,3 @@ +import Footer from './src/Footer.vue' + +export { Footer } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Footer/src/Footer.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Footer/src/Footer.vue new file mode 100644 index 00000000..3eede386 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Footer/src/Footer.vue @@ -0,0 +1,24 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/LocaleDropdown/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/LocaleDropdown/index.ts new file mode 100644 index 00000000..d02e640f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/LocaleDropdown/index.ts @@ -0,0 +1,3 @@ +import LocaleDropdown from './src/LocaleDropdown.vue' + +export { LocaleDropdown } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/LocaleDropdown/src/LocaleDropdown.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/LocaleDropdown/src/LocaleDropdown.vue new file mode 100644 index 00000000..95132db2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/LocaleDropdown/src/LocaleDropdown.vue @@ -0,0 +1,52 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Logo/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Logo/index.ts new file mode 100644 index 00000000..1c4224c9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Logo/index.ts @@ -0,0 +1,3 @@ +import Logo from './src/Logo.vue' + +export { Logo } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Logo/src/Logo.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Logo/src/Logo.vue new file mode 100644 index 00000000..d241130d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Logo/src/Logo.vue @@ -0,0 +1,88 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/index.ts new file mode 100644 index 00000000..a6ec6965 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/index.ts @@ -0,0 +1,3 @@ +import Menu from './src/Menu.vue' + +export { Menu } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/Menu.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/Menu.vue new file mode 100644 index 00000000..9033616f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/Menu.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/components/useRenderMenuItem.tsx b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/components/useRenderMenuItem.tsx new file mode 100644 index 00000000..17a520a6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/components/useRenderMenuItem.tsx @@ -0,0 +1,59 @@ +import { ElSubMenu, ElMenuItem } from 'element-plus' +import type { RouteMeta } from 'vue-router' +import { hasOneShowingChild } from '../helper' +import { isUrl } from '@/utils/is' +import { useRenderMenuTitle } from './useRenderMenuTitle' +import { useDesign } from '@/hooks/web/useDesign' +import { pathResolve } from '@/utils/routerHelper' + +export const useRenderMenuItem = ( + // allRouters: AppRouteRecordRaw[] = [], + menuMode: 'vertical' | 'horizontal' +) => { + const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { + return routers.map((v) => { + const meta = (v.meta ?? {}) as RouteMeta + if (!meta.hidden) { + const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) + const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath(allRouters, v.path).join('/') + + const { renderMenuTitle } = useRenderMenuTitle() + + if ( + oneShowingChild && + (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && + !meta?.alwaysShow + ) { + return ( + + {{ + default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta) + }} + + ) + } else { + const { getPrefixCls } = useDesign() + + const preFixCls = getPrefixCls('menu-popper') + return ( + + {{ + title: () => renderMenuTitle(meta), + default: () => renderMenuItem(v.children!, fullPath) + }} + + ) + } + } + }) + } + + return { + renderMenuItem + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx new file mode 100644 index 00000000..fc30b900 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx @@ -0,0 +1,22 @@ +import type { RouteMeta } from 'vue-router' +import { Icon } from '@/components/Icon' + +export const useRenderMenuTitle = () => { + const renderMenuTitle = (meta: RouteMeta) => { + const { t } = useI18n() + const { title = 'Please set title', icon } = meta + + return icon ? ( + <> + + {t(title as string)} + + ) : ( + {t(title as string)} + ) + } + + return { + renderMenuTitle + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/helper.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/helper.ts new file mode 100644 index 00000000..c26f5f4b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Menu/src/helper.ts @@ -0,0 +1,54 @@ +import type { RouteMeta } from 'vue-router' +import { findPath } from '@/utils/tree' + +type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean } + +interface HasOneShowingChild { + oneShowingChild?: boolean + onlyOneChild?: OnlyOneChildType +} + +export const getAllParentPath = (treeData: T[], path: string) => { + const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[] + return (menuList || []).map((item) => item.path) +} + +export const hasOneShowingChild = ( + children: AppRouteRecordRaw[] = [], + parent: AppRouteRecordRaw +): HasOneShowingChild => { + const onlyOneChild = ref() + + const showingChildren = children.filter((v) => { + const meta = (v.meta ?? {}) as RouteMeta + if (meta.hidden) { + return false + } else { + // Temp set(will be used if only has one showing child) + onlyOneChild.value = v + return true + } + }) + + // When there is only one child router, the child router is displayed by default + if (showingChildren.length === 1) { + return { + oneShowingChild: true, + onlyOneChild: unref(onlyOneChild) + } + } + + // Show parent if there are no child router to display + if (!showingChildren.length) { + onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } + return { + oneShowingChild: true, + onlyOneChild: unref(onlyOneChild) + } + } + + return { + oneShowingChild: false, + onlyOneChild: unref(onlyOneChild) + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Message/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Message/index.ts new file mode 100644 index 00000000..dfe02076 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Message/index.ts @@ -0,0 +1,3 @@ +import Message from './src/Message.vue' + +export { Message } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Message/src/Message.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Message/src/Message.vue new file mode 100644 index 00000000..6bd7724a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Message/src/Message.vue @@ -0,0 +1,126 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Screenfull/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Screenfull/index.ts new file mode 100644 index 00000000..faec2d8e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Screenfull/index.ts @@ -0,0 +1,3 @@ +import Screenfull from './src/Screenfull.vue' + +export { Screenfull } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Screenfull/src/Screenfull.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Screenfull/src/Screenfull.vue new file mode 100644 index 00000000..4c045f25 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Screenfull/src/Screenfull.vue @@ -0,0 +1,32 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/index.ts new file mode 100644 index 00000000..b64c9add --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/index.ts @@ -0,0 +1,3 @@ +import Setting from './src/Setting.vue' + +export { Setting } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/Setting.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/Setting.vue new file mode 100644 index 00000000..e1908b63 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/Setting.vue @@ -0,0 +1,299 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/components/ColorRadioPicker.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/components/ColorRadioPicker.vue new file mode 100644 index 00000000..fcc5e758 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/components/ColorRadioPicker.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/components/InterfaceDisplay.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/components/InterfaceDisplay.vue new file mode 100644 index 00000000..ebbbf4bc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/components/InterfaceDisplay.vue @@ -0,0 +1,224 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/components/LayoutRadioPicker.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/components/LayoutRadioPicker.vue new file mode 100644 index 00000000..801686c2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/Setting/src/components/LayoutRadioPicker.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/SizeDropdown/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/SizeDropdown/index.ts new file mode 100644 index 00000000..516488d6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/SizeDropdown/index.ts @@ -0,0 +1,3 @@ +import SizeDropdown from './src/SizeDropdown.vue' + +export { SizeDropdown } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/SizeDropdown/src/SizeDropdown.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/SizeDropdown/src/SizeDropdown.vue new file mode 100644 index 00000000..3e152244 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/SizeDropdown/src/SizeDropdown.vue @@ -0,0 +1,40 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/TabMenu/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/TabMenu/index.ts new file mode 100644 index 00000000..b5fd71cd --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/TabMenu/index.ts @@ -0,0 +1,3 @@ +import TabMenu from './src/TabMenu.vue' + +export { TabMenu } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/TabMenu/src/TabMenu.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/TabMenu/src/TabMenu.vue new file mode 100644 index 00000000..c4f63a3f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/TabMenu/src/TabMenu.vue @@ -0,0 +1,240 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/TabMenu/src/helper.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/TabMenu/src/helper.ts new file mode 100644 index 00000000..cce39323 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/TabMenu/src/helper.ts @@ -0,0 +1,51 @@ +import { getAllParentPath } from '@/layout/components/Menu/src/helper' +import type { RouteMeta } from 'vue-router' +import { isUrl } from '@/utils/is' +import { cloneDeep } from 'lodash-es' + +export type TabMapTypes = { + [key: string]: string[] +} + +export const tabPathMap = reactive({}) + +export const initTabMap = (routes: AppRouteRecordRaw[]) => { + for (const v of routes) { + const meta = (v.meta ?? {}) as RouteMeta + if (!meta?.hidden) { + tabPathMap[v.path] = [] + } + } +} + +export const filterMenusPath = ( + routes: AppRouteRecordRaw[], + allRoutes: AppRouteRecordRaw[] +): AppRouteRecordRaw[] => { + const res: AppRouteRecordRaw[] = [] + for (const v of routes) { + let data: Nullable = null + const meta = (v.meta ?? {}) as RouteMeta + if (!meta.hidden || meta.canTo) { + const allParentPath = getAllParentPath(allRoutes, v.path) + + const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/') + + data = cloneDeep(v) + data.path = fullPath + if (v.children && data) { + data.children = filterMenusPath(v.children, allRoutes) + } + + if (data) { + res.push(data) + } + + if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) { + tabPathMap[allParentPath[0]].push(fullPath) + } + } + } + + return res +} diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/TagsView/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/TagsView/index.ts new file mode 100644 index 00000000..30e604a8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/TagsView/index.ts @@ -0,0 +1,3 @@ +import TagsView from './src/TagsView.vue' + +export { TagsView } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/TagsView/src/TagsView.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/TagsView/src/TagsView.vue new file mode 100644 index 00000000..7db0cf6f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/TagsView/src/TagsView.vue @@ -0,0 +1,585 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/TagsView/src/helper.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/TagsView/src/helper.ts new file mode 100644 index 00000000..22f6a507 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/TagsView/src/helper.ts @@ -0,0 +1,21 @@ +import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router' +import { pathResolve } from '@/utils/routerHelper' + +export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => { + let tags: RouteLocationNormalizedLoaded[] = [] + routes.forEach((route) => { + const meta = route.meta as RouteMeta + const tagPath = pathResolve(parentPath, route.path) + if (meta?.affix) { + tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded) + } + if (route.children) { + const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath) + if (tempTags.length >= 1) { + tags = [...tags, ...tempTags] + } + } + }) + + return tags +} diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/ThemeSwitch/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/ThemeSwitch/index.ts new file mode 100644 index 00000000..823a2765 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/ThemeSwitch/index.ts @@ -0,0 +1,3 @@ +import ThemeSwitch from './src/ThemeSwitch.vue' + +export { ThemeSwitch } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/ThemeSwitch/src/ThemeSwitch.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/ThemeSwitch/src/ThemeSwitch.vue new file mode 100644 index 00000000..39a8cfd7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/ThemeSwitch/src/ThemeSwitch.vue @@ -0,0 +1,46 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/ToolHeader.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/ToolHeader.vue new file mode 100644 index 00000000..0b8d00d5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/ToolHeader.vue @@ -0,0 +1,95 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/UserInfo/index.ts b/mes-ui/mes-ui-admin-vue3/src/layout/components/UserInfo/index.ts new file mode 100644 index 00000000..c3a34aba --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/UserInfo/index.ts @@ -0,0 +1,3 @@ +import UserInfo from './src/UserInfo.vue' + +export { UserInfo } diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/UserInfo/src/UserInfo.vue b/mes-ui/mes-ui-admin-vue3/src/layout/components/UserInfo/src/UserInfo.vue new file mode 100644 index 00000000..a5a92da3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/UserInfo/src/UserInfo.vue @@ -0,0 +1,78 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/layout/components/useRenderLayout.tsx b/mes-ui/mes-ui-admin-vue3/src/layout/components/useRenderLayout.tsx new file mode 100644 index 00000000..1110cd86 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/layout/components/useRenderLayout.tsx @@ -0,0 +1,306 @@ +import { computed } from 'vue' +import { useAppStore } from '@/store/modules/app' +import { Menu } from '@/layout/components/Menu' +import { TabMenu } from '@/layout/components/TabMenu' +import { TagsView } from '@/layout/components/TagsView' +import { Logo } from '@/layout/components/Logo' +import AppView from './AppView.vue' +import ToolHeader from './ToolHeader.vue' +import { ElScrollbar } from 'element-plus' +import { useDesign } from '@/hooks/web/useDesign' + +const { getPrefixCls } = useDesign() + +const prefixCls = getPrefixCls('layout') + +const appStore = useAppStore() + +const pageLoading = computed(() => appStore.getPageLoading) + +// 标签页 +const tagsView = computed(() => appStore.getTagsView) + +// 菜单折叠 +const collapse = computed(() => appStore.getCollapse) + +// logo +const logo = computed(() => appStore.logo) + +// 固定头部 +const fixedHeader = computed(() => appStore.getFixedHeader) + +// 是否是移动端 +const mobile = computed(() => appStore.getMobile) + +// 固定菜单 +const fixedMenu = computed(() => appStore.getFixedMenu) + +export const useRenderLayout = () => { + const renderClassic = () => { + return ( + <> +
+ {logo.value ? ( + + ) : undefined} + +
+
+ +
+ + + {tagsView.value ? ( + + ) : undefined} +
+ + +
+
+ + ) + } + + const renderTopLeft = () => { + return ( + <> +
+ {logo.value ? : undefined} + + +
+
+ +
+ + {tagsView.value ? ( + + ) : undefined} + + + +
+
+ + ) + } + + const renderTop = () => { + return ( + <> +
+ {logo.value ? : undefined} + + +
+
+ + {tagsView.value ? ( + + ) : undefined} + + + +
+ + ) + } + + const renderCutMenu = () => { + return ( + <> +
+ {logo.value ? : undefined} + + +
+
+ +
+ + {tagsView.value ? ( + + ) : undefined} + + + +
+
+ + ) + } + + return { + renderClassic, + renderTopLeft, + renderTop, + renderCutMenu + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/locales/en.ts b/mes-ui/mes-ui-admin-vue3/src/locales/en.ts new file mode 100644 index 00000000..4f4d4895 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/locales/en.ts @@ -0,0 +1,447 @@ +export default { + common: { + inputText: 'Please input', + selectText: 'Please select', + startTimeText: 'Start time', + endTimeText: 'End time', + login: 'Login', + required: 'This is required', + loginOut: 'Login out', + document: 'Document', + profile: 'User Center', + reminder: 'Reminder', + loginOutMessage: 'Exit the system?', + back: 'Back', + ok: 'OK', + save: 'Save', + cancel: 'Cancel', + close: 'Close', + reload: 'Reload current', + success: 'Success', + closeTab: 'Close current', + closeTheLeftTab: 'Close left', + closeTheRightTab: 'Close right', + closeOther: 'Close other', + closeAll: 'Close all', + prevLabel: 'Prev', + nextLabel: 'Next', + skipLabel: 'Jump', + doneLabel: 'End', + menu: 'Menu', + menuDes: 'Menu bar rendered in routed structure', + collapse: 'Collapse', + collapseDes: 'Expand and zoom the menu bar', + tagsView: 'Tags view', + tagsViewDes: 'Used to record routing history', + tool: 'Tool', + toolDes: 'Used to set up custom systems', + query: 'Query', + reset: 'Reset', + shrink: 'Put away', + expand: 'Expand', + confirmTitle: 'System Hint', + exportMessage: 'Whether to confirm export data item?', + importMessage: 'Whether to confirm import data item?', + createSuccess: 'Create Success', + updateSuccess: 'Update Success', + delMessage: 'Delete the selected data?', + delDataMessage: 'Delete the data?', + delNoData: 'Please select the data to delete', + delSuccess: 'Deleted successfully', + index: 'Index', + status: 'Status', + createTime: 'Create Time', + updateTime: 'Update Time', + copy: 'Copy', + copySuccess: 'Copy Success', + copyError: 'Copy Error' + }, + error: { + noPermission: `Sorry, you don't have permission to access this page.`, + pageError: 'Sorry, the page you visited does not exist.', + networkError: 'Sorry, the server reported an error.', + returnToHome: 'Return to home' + }, + permission: { + hasPermission: `Please set the operation permission label value`, + hasRole: `Please set the role permission tag value` + }, + setting: { + projectSetting: 'Project setting', + theme: 'Theme', + layout: 'Layout', + systemTheme: 'System theme', + menuTheme: 'Menu theme', + interfaceDisplay: 'Interface display', + breadcrumb: 'Breadcrumb', + breadcrumbIcon: 'Breadcrumb icon', + collapseMenu: 'Collapse menu', + hamburgerIcon: 'Hamburger icon', + screenfullIcon: 'Screenfull icon', + sizeIcon: 'Size icon', + localeIcon: 'Locale icon', + messageIcon: 'Message icon', + tagsView: 'Tags view', + logo: 'Logo', + greyMode: 'Grey mode', + fixedHeader: 'Fixed header', + headerTheme: 'Header theme', + cutMenu: 'Cut Menu', + copy: 'Copy', + clearAndReset: 'Clear cache and reset', + copySuccess: 'Copy success', + copyFailed: 'Copy failed', + footer: 'Footer', + uniqueOpened: 'Unique opened', + tagsViewIcon: 'Tags view icon', + reExperienced: 'Please exit the login experience again', + fixedMenu: 'Fixed menu' + }, + size: { + default: 'Default', + large: 'Large', + small: 'Small' + }, + login: { + welcome: 'Welcome to the system', + message: 'Backstage management system', + tenantname: 'TenantName', + username: 'Username', + password: 'Password', + code: 'verification code', + login: 'Sign in', + relogin: 'Sign in again', + otherLogin: 'Sign in with', + register: 'Register', + checkPassword: 'Confirm password', + remember: 'Remember me', + hasUser: 'Existing account? Go to login', + forgetPassword: 'Forget password?', + tenantNamePlaceholder: 'Please Enter Tenant Name', + usernamePlaceholder: 'Please Enter Username', + passwordPlaceholder: 'Please Enter Password', + codePlaceholder: 'Please Enter Verification Code', + mobileTitle: 'Mobile sign in', + mobileNumber: 'Mobile Number', + mobileNumberPlaceholder: 'Plaease Enter Mobile Number', + backLogin: 'back', + getSmsCode: 'Get SMS Code', + btnMobile: 'Mobile sign in', + btnQRCode: 'QR code sign in', + qrcode: 'Scan the QR code to log in', + btnRegister: 'Sign up', + SmsSendMsg: 'code has been sent' + }, + captcha: { + verification: 'Please complete security verification', + slide: 'Swipe right to complete verification', + point: 'Please click', + success: 'Verification succeeded', + fail: 'verification failed' + }, + router: { + login: 'Login', + home: 'Home', + analysis: 'Analysis', + workplace: 'Workplace' + }, + analysis: { + newUser: 'New user', + unreadInformation: 'Unread information', + transactionAmount: 'Transaction amount', + totalShopping: 'Total Shopping', + monthlySales: 'Monthly sales', + userAccessSource: 'User access source', + january: 'January', + february: 'February', + march: 'March', + april: 'April', + may: 'May', + june: 'June', + july: 'July', + august: 'August', + september: 'September', + october: 'October', + november: 'November', + december: 'December', + estimate: 'Estimate', + actual: 'Actual', + directAccess: 'Airect access', + mailMarketing: 'Mail marketing', + allianceAdvertising: 'Alliance advertising', + videoAdvertising: 'Video advertising', + searchEngines: 'Search engines', + weeklyUserActivity: 'Weekly user activity', + activeQuantity: 'Active quantity', + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday', + sunday: 'Sunday' + }, + workplace: { + welcome: 'Hello', + happyDay: 'Wish you happy every day!', + toady: `It's sunny today`, + notice: 'Announcement', + project: 'Project', + access: 'Project access', + toDo: 'To do', + introduction: 'A serious introduction', + shortcutOperation: 'Quick entry', + operation: 'Operation', + index: 'Index', + personal: 'Personal', + team: 'Team', + quote: 'Quote', + contribution: 'Contribution', + hot: 'Hot', + yield: 'Yield', + dynamic: 'Dynamic', + push: 'push', + follow: 'Follow' + }, + form: { + input: 'Input', + inputNumber: 'InputNumber', + default: 'Default', + icon: 'Icon', + mixed: 'Mixed', + textarea: 'Textarea', + slot: 'Slot', + position: 'Position', + autocomplete: 'Autocomplete', + select: 'Select', + selectGroup: 'Select Group', + selectV2: 'SelectV2', + cascader: 'Cascader', + switch: 'Switch', + rate: 'Rate', + colorPicker: 'Color Picker', + transfer: 'Transfer', + render: 'Render', + radio: 'Radio', + button: 'Button', + checkbox: 'Checkbox', + slider: 'Slider', + datePicker: 'Date Picker', + shortcuts: 'Shortcuts', + today: 'Today', + yesterday: 'Yesterday', + aWeekAgo: 'A week ago', + week: 'Week', + year: 'Year', + month: 'Month', + dates: 'Dates', + daterange: 'Date Range', + monthrange: 'Month Range', + dateTimePicker: 'DateTimePicker', + dateTimerange: 'Datetime Range', + timePicker: 'Time Picker', + timeSelect: 'Time Select', + inputPassword: 'input Password', + passwordStrength: 'Password Strength', + operate: 'operate', + change: 'Change', + restore: 'Restore', + disabled: 'Disabled', + disablement: 'Disablement', + delete: 'Delete', + add: 'Add', + setValue: 'Set value', + resetValue: 'Reset value', + set: 'Set', + subitem: 'Subitem', + formValidation: 'Form validation', + verifyReset: 'Verify reset', + remark: 'Remark' + }, + watermark: { + watermark: 'Watermark' + }, + table: { + table: 'Table', + index: 'Index', + title: 'Title', + author: 'Author', + createTime: 'Create time', + action: 'Action', + pagination: 'pagination', + reserveIndex: 'Reserve index', + restoreIndex: 'Restore index', + showSelections: 'Show selections', + hiddenSelections: 'Restore selections', + showExpandedRows: 'Show expanded rows', + hiddenExpandedRows: 'Hidden expanded rows', + header: 'Header' + }, + action: { + create: 'Create', + add: 'Add', + del: 'Delete', + delete: 'Delete', + edit: 'Edit', + update: 'Update', + preview: 'Preview', + more: 'More', + sync: 'Sync', + save: 'Save', + detail: 'Detail', + export: 'Export', + import: 'Import', + generate: 'Generate', + logout: 'Login Out', + test: 'Test', + typeCreate: 'Dict Type Create', + typeUpdate: 'Dict Type Eidt', + dataCreate: 'Dict Data Create', + dataUpdate: 'Dict Data Eidt', + fileUpload: 'File Upload' + }, + dialog: { + dialog: 'Dialog', + open: 'Open', + close: 'Close' + }, + sys: { + api: { + operationFailed: 'Operation failed', + errorTip: 'Error Tip', + errorMessage: 'The operation failed, the system is abnormal!', + timeoutMessage: 'Login timed out, please log in again!', + apiTimeoutMessage: 'The interface request timed out, please refresh the page and try again!', + apiRequestFailed: 'The interface request failed, please try again later!', + networkException: 'network anomaly', + networkExceptionMsg: + 'Please check if your network connection is normal! The network is abnormal', + + errMsg401: 'The user does not have permission (token, user name, password error)!', + errMsg403: 'The user is authorized, but access is forbidden!', + errMsg404: 'Network request error, the resource was not found!', + errMsg405: 'Network request error, request method not allowed!', + errMsg408: 'Network request timed out!', + errMsg500: 'Server error, please contact the administrator!', + errMsg501: 'The network is not implemented!', + errMsg502: 'Network Error!', + errMsg503: 'The service is unavailable, the server is temporarily overloaded or maintained!', + errMsg504: 'Network timeout!', + errMsg505: 'The http version does not support the request!', + errMsg901: 'Demo mode, no write operations are possible!' + }, + app: { + logoutTip: 'Reminder', + logoutMessage: 'Confirm to exit the system?', + menuLoading: 'Menu loading...' + }, + exception: { + backLogin: 'Back Login', + backHome: 'Back Home', + subTitle403: "Sorry, you don't have access to this page.", + subTitle404: 'Sorry, the page you visited does not exist.', + subTitle500: 'Sorry, the server is reporting an error.', + noDataTitle: 'No data on the current page.', + networkErrorTitle: 'Network Error', + networkErrorSubTitle: + 'Sorry, Your network connection has been disconnected, please check your network!' + }, + lock: { + unlock: 'Click to unlock', + alert: 'Lock screen password error', + backToLogin: 'Back to login', + entry: 'Enter the system', + placeholder: 'Please enter the lock screen password or user password' + }, + login: { + backSignIn: 'Back sign in', + mobileSignInFormTitle: 'Mobile sign in', + qrSignInFormTitle: 'Qr code sign in', + signInFormTitle: 'Sign in', + signUpFormTitle: 'Sign up', + forgetFormTitle: 'Reset password', + + signInTitle: 'Backstage management system', + signInDesc: 'Enter your personal details and get started!', + policy: 'I agree to the xxx Privacy Policy', + scanSign: `scanning the code to complete the login`, + + loginButton: 'Sign in', + registerButton: 'Sign up', + rememberMe: 'Remember me', + forgetPassword: 'Forget Password?', + otherSignIn: 'Sign in with', + + // notify + loginSuccessTitle: 'Login successful', + loginSuccessDesc: 'Welcome back', + + // placeholder + accountPlaceholder: 'Please input username', + passwordPlaceholder: 'Please input password', + smsPlaceholder: 'Please input sms code', + mobilePlaceholder: 'Please input mobile', + policyPlaceholder: 'Register after checking', + diffPwd: 'The two passwords are inconsistent', + + userName: 'Username', + password: 'Password', + confirmPassword: 'Confirm Password', + email: 'Email', + smsCode: 'SMS code', + mobile: 'Mobile' + } + }, + profile: { + user: { + title: 'Personal Information', + username: 'User Name', + nickname: 'Nick Name', + mobile: 'Phone Number', + email: 'User Mail', + dept: 'Department', + posts: 'Position', + roles: 'Own Role', + sex: 'Sex', + man: 'Man', + woman: 'Woman', + createTime: 'Created Date' + }, + info: { + title: 'Basic Information', + basicInfo: 'Basic Information', + resetPwd: 'Reset Password', + userSocial: 'Social Information' + }, + rules: { + nickname: 'Please Enter User Nickname', + mail: 'Please Input The Email Address', + truemail: 'Please Input The Correct Email Address', + phone: 'Please Enter The Phone Number', + truephone: 'Please Enter The Correct Phone Number' + }, + password: { + oldPassword: 'Old PassWord', + newPassword: 'New Password', + confirmPassword: 'Confirm Password', + oldPwdMsg: 'Please Enter Old Password', + newPwdMsg: 'Please Enter New Password', + cfPwdMsg: 'Please Enter Confirm Password', + diffPwd: 'The Passwords Entered Twice No Match' + } + }, + cropper: { + selectImage: 'Select Image', + uploadSuccess: 'Uploaded success!', + modalTitle: 'Avatar upload', + okText: 'Confirm and upload', + btn_reset: 'Reset', + btn_rotate_left: 'Counterclockwise rotation', + btn_rotate_right: 'Clockwise rotation', + btn_scale_x: 'Flip horizontal', + btn_scale_y: 'Flip vertical', + btn_zoom_in: 'Zoom in', + btn_zoom_out: 'Zoom out', + preview: 'Preivew' + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/locales/zh-CN.ts b/mes-ui/mes-ui-admin-vue3/src/locales/zh-CN.ts new file mode 100644 index 00000000..6346a3d3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/locales/zh-CN.ts @@ -0,0 +1,442 @@ +export default { + common: { + inputText: '请输入', + selectText: '请选择', + startTimeText: '开始时间', + endTimeText: '结束时间', + login: '登录', + required: '该项为必填项', + loginOut: '退出系统', + document: '项目文档', + profile: '个人中心', + reminder: '温馨提示', + loginOutMessage: '是否退出本系统?', + back: '返回', + ok: '确定', + save: '保存', + cancel: '取消', + close: '关闭', + reload: '重新加载', + success: '成功', + closeTab: '关闭标签页', + closeTheLeftTab: '关闭左侧标签页', + closeTheRightTab: '关闭右侧标签页', + closeOther: '关闭其他标签页', + closeAll: '关闭全部标签页', + prevLabel: '上一步', + nextLabel: '下一步', + skipLabel: '跳过', + doneLabel: '结束', + menu: '菜单', + menuDes: '以路由的结构渲染的菜单栏', + collapse: '展开缩收', + collapseDes: '展开和缩放菜单栏', + tagsView: '标签页', + tagsViewDes: '用于记录路由历史记录', + tool: '工具', + toolDes: '用于设置定制系统', + query: '查询', + reset: '重置', + shrink: '收起', + expand: '展开', + confirmTitle: '系统提示', + exportMessage: '是否确认导出数据项?', + importMessage: '是否确认导入数据项?', + createSuccess: '新增成功', + updateSuccess: '修改成功', + delMessage: '是否删除所选中数据?', + delDataMessage: '是否删除数据?', + delNoData: '请选择需要删除的数据', + delSuccess: '删除成功', + index: '序号', + status: '状态', + createTime: '创建时间', + updateTime: '更新时间', + copy: '复制', + copySuccess: '复制成功', + copyError: '复制失败' + }, + error: { + noPermission: `抱歉,您无权访问此页面。`, + pageError: '抱歉,您访问的页面不存在。', + networkError: '抱歉,服务器报告错误。', + returnToHome: '返回首页' + }, + permission: { + hasPermission: `请设置操作权限标签值`, + hasRole: `请设置角色权限标签值` + }, + setting: { + projectSetting: '项目配置', + theme: '主题', + layout: '布局', + systemTheme: '系统主题', + menuTheme: '菜单主题', + interfaceDisplay: '界面显示', + breadcrumb: '面包屑', + breadcrumbIcon: '面包屑图标', + collapseMenu: '折叠菜单', + hamburgerIcon: '折叠图标', + screenfullIcon: '全屏图标', + sizeIcon: '尺寸图标', + localeIcon: '多语言图标', + messageIcon: '消息图标', + tagsView: '标签页', + logo: '标志', + greyMode: '灰色模式', + fixedHeader: '固定头部', + headerTheme: '头部主题', + cutMenu: '切割菜单', + copy: '拷贝', + clearAndReset: '清除缓存并且重置', + copySuccess: '拷贝成功', + copyFailed: '拷贝失败', + footer: '页脚', + uniqueOpened: '菜单手风琴', + tagsViewIcon: '标签页图标', + reExperienced: '请重新退出登录体验', + fixedMenu: '固定菜单' + }, + size: { + default: '默认', + large: '大', + small: '小' + }, + login: { + welcome: '欢迎使用本系统', + message: '开箱即用的中后台管理系统', + tenantname: '租户名称', + username: '用户名', + password: '密码', + code: '验证码', + login: '登录', + relogin: '重新登录', + otherLogin: '其他登录方式', + register: '注册', + checkPassword: '确认密码', + remember: '记住我', + hasUser: '已有账号?去登录', + forgetPassword: '忘记密码?', + tenantNamePlaceholder: '请输入租户名称', + usernamePlaceholder: '请输入用户名', + passwordPlaceholder: '请输入密码', + codePlaceholder: '请输入验证码', + mobileTitle: '手机登录', + mobileNumber: '手机号码', + mobileNumberPlaceholder: '请输入手机号码', + backLogin: '返回', + getSmsCode: '获取验证码', + btnMobile: '手机登录', + btnQRCode: '二维码登录', + qrcode: '扫描二维码登录', + btnRegister: '注册', + SmsSendMsg: '验证码已发送' + }, + captcha: { + verification: '请完成安全验证', + slide: '向右滑动完成验证', + point: '请依次点击', + success: '验证成功', + fail: '验证失败' + }, + router: { + login: '登录', + socialLogin: '社交登录', + home: '首页', + analysis: '分析页', + workplace: '工作台' + }, + analysis: { + newUser: '新增用户', + unreadInformation: '未读消息', + transactionAmount: '成交金额', + totalShopping: '购物总量', + monthlySales: '每月销售额', + userAccessSource: '用户访问来源', + january: '一月', + february: '二月', + march: '三月', + april: '四月', + may: '五月', + june: '六月', + july: '七月', + august: '八月', + september: '九月', + october: '十月', + november: '十一月', + december: '十二月', + estimate: '预计', + actual: '实际', + directAccess: '直接访问', + mailMarketing: '邮件营销', + allianceAdvertising: '联盟广告', + videoAdvertising: '视频广告', + searchEngines: '搜索引擎', + weeklyUserActivity: '每周用户活跃量', + activeQuantity: '活跃量', + monday: '周一', + tuesday: '周二', + wednesday: '周三', + thursday: '周四', + friday: '周五', + saturday: '周六', + sunday: '周日' + }, + workplace: { + welcome: '你好', + happyDay: '祝你开心每一天!', + toady: '今日晴', + notice: '通知公告', + project: '项目数', + access: '项目访问', + toDo: '待办', + introduction: '一个正经的简介', + shortcutOperation: '快捷入口', + operation: '操作', + index: '指数', + personal: '个人', + team: '团队', + quote: '引用', + contribution: '贡献', + hot: '热度', + yield: '产量', + dynamic: '动态', + push: '推送', + follow: '关注' + }, + form: { + input: '输入框', + inputNumber: '数字输入框', + default: '默认', + icon: '图标', + mixed: '复合型', + textarea: '多行文本', + slot: '插槽', + position: '位置', + autocomplete: '自动补全', + select: '选择器', + selectGroup: '选项分组', + selectV2: '虚拟列表选择器', + cascader: '级联选择器', + switch: '开关', + rate: '评分', + colorPicker: '颜色选择器', + transfer: '穿梭框', + render: '渲染器', + radio: '单选框', + button: '按钮', + checkbox: '多选框', + slider: '滑块', + datePicker: '日期选择器', + shortcuts: '快捷选项', + today: '今天', + yesterday: '昨天', + aWeekAgo: '一周前', + week: '周', + year: '年', + month: '月', + dates: '日期', + daterange: '日期范围', + monthrange: '月份范围', + dateTimePicker: '日期时间选择器', + dateTimerange: '日期时间范围', + timePicker: '时间选择器', + timeSelect: '时间选择', + inputPassword: '密码输入框', + passwordStrength: '密码强度', + operate: '操作', + change: '更改', + restore: '还原', + disabled: '禁用', + disablement: '解除禁用', + delete: '删除', + add: '添加', + setValue: '设置值', + resetValue: '重置值', + set: '设置', + subitem: '子项', + formValidation: '表单验证', + verifyReset: '验证重置', + remark: '备注' + }, + watermark: { + watermark: '水印' + }, + table: { + table: '表格', + index: '序号', + title: '标题', + author: '作者', + createTime: '创建时间', + action: '操作', + pagination: '分页', + reserveIndex: '叠加序号', + restoreIndex: '还原序号', + showSelections: '显示多选', + hiddenSelections: '隐藏多选', + showExpandedRows: '显示展开行', + hiddenExpandedRows: '隐藏展开行', + header: '头部' + }, + action: { + create: '新增', + add: '新增', + del: '删除', + delete: '删除', + edit: '编辑', + update: '编辑', + preview: '预览', + more: '更多', + sync: '同步', + save: '保存', + detail: '详情', + export: '导出', + import: '导入', + generate: '生成', + logout: '强制退出', + test: '测试', + typeCreate: '字典类型新增', + typeUpdate: '字典类型编辑', + dataCreate: '字典数据新增', + dataUpdate: '字典数据编辑' + }, + dialog: { + dialog: '弹窗', + open: '打开', + close: '关闭' + }, + sys: { + api: { + operationFailed: '操作失败', + errorTip: '错误提示', + errorMessage: '操作失败,系统异常!', + timeoutMessage: '登录超时,请重新登录!', + apiTimeoutMessage: '接口请求超时,请刷新页面重试!', + apiRequestFailed: '请求出错,请稍候重试', + networkException: '网络异常', + networkExceptionMsg: '网络异常,请检查您的网络连接是否正常!', + errMsg401: '用户没有权限(令牌、用户名、密码错误)!', + errMsg403: '用户得到授权,但是访问是被禁止的。!', + errMsg404: '网络请求错误,未找到该资源!', + errMsg405: '网络请求错误,请求方法未允许!', + errMsg408: '网络请求超时!', + errMsg500: '服务器错误,请联系管理员!', + errMsg501: '网络未实现!', + errMsg502: '网络错误!', + errMsg503: '服务不可用,服务器暂时过载或维护!', + errMsg504: '网络超时!', + errMsg505: 'http版本不支持该请求!', + errMsg901: '演示模式,无法进行写操作!' + }, + app: { + logoutTip: '温馨提醒', + logoutMessage: '是否确认退出系统?', + menuLoading: '菜单加载中...' + }, + exception: { + backLogin: '返回登录', + backHome: '返回首页', + subTitle403: '抱歉,您无权访问此页面。', + subTitle404: '抱歉,您访问的页面不存在。', + subTitle500: '抱歉,服务器报告错误。', + noDataTitle: '当前页无数据', + networkErrorTitle: '网络错误', + networkErrorSubTitle: '抱歉,您的网络连接已断开,请检查您的网络!' + }, + lock: { + unlock: '点击解锁', + alert: '锁屏密码错误', + backToLogin: '返回登录', + entry: '进入系统', + placeholder: '请输入锁屏密码或者用户密码' + }, + login: { + backSignIn: '返回', + signInFormTitle: '登录', + ssoFormTitle: '三方授权', + mobileSignInFormTitle: '手机登录', + qrSignInFormTitle: '二维码登录', + signUpFormTitle: '注册', + forgetFormTitle: '重置密码', + signInTitle: '开箱即用的中后台管理系统', + signInDesc: '输入您的个人详细信息开始使用!', + policy: '我同意xxx隐私政策', + scanSign: `扫码后点击"确认",即可完成登录`, + loginButton: '登录', + registerButton: '注册', + rememberMe: '记住我', + forgetPassword: '忘记密码?', + otherSignIn: '其他登录方式', + // notify + loginSuccessTitle: '登录成功', + loginSuccessDesc: '欢迎回来', + // placeholder + accountPlaceholder: '请输入账号', + passwordPlaceholder: '请输入密码', + smsPlaceholder: '请输入验证码', + mobilePlaceholder: '请输入手机号码', + policyPlaceholder: '勾选后才能注册', + diffPwd: '两次输入密码不一致', + userName: '账号', + password: '密码', + confirmPassword: '确认密码', + email: '邮箱', + smsCode: '短信验证码', + mobile: '手机号码' + } + }, + profile: { + user: { + title: '个人信息', + username: '用户名称', + nickname: '用户昵称', + mobile: '手机号码', + email: '用户邮箱', + dept: '所属部门', + posts: '所属岗位', + roles: '所属角色', + sex: '性别', + man: '男', + woman: '女', + createTime: '创建日期' + }, + info: { + title: '基本信息', + basicInfo: '基本资料', + resetPwd: '修改密码', + userSocial: '社交信息' + }, + rules: { + nickname: '请输入用户昵称', + mail: '请输入邮箱地址', + truemail: '请输入正确的邮箱地址', + phone: '请输入正确的手机号码', + truephone: '请输入正确的手机号码' + }, + password: { + oldPassword: '旧密码', + newPassword: '新密码', + confirmPassword: '确认密码', + oldPwdMsg: '请输入旧密码', + newPwdMsg: '请输入新密码', + cfPwdMsg: '请输入确认密码', + pwdRules: '长度在 6 到 20 个字符', + diffPwd: '两次输入密码不一致' + } + }, + cropper: { + selectImage: '选择图片', + uploadSuccess: '上传成功', + modalTitle: '头像上传', + okText: '确认并上传', + btn_reset: '重置', + btn_rotate_left: '逆时针旋转', + btn_rotate_right: '顺时针旋转', + btn_scale_x: '水平翻转', + btn_scale_y: '垂直翻转', + btn_zoom_in: '放大', + btn_zoom_out: '缩小', + preview: '预览' + }, + 'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错 +} diff --git a/mes-ui/mes-ui-admin-vue3/src/main.ts b/mes-ui/mes-ui-admin-vue3/src/main.ts new file mode 100644 index 00000000..76c72473 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/main.ts @@ -0,0 +1,72 @@ +// 引入unocss css +import '@/plugins/unocss' + +// 导入全局的svg图标 +import '@/plugins/svgIcon' + +// 初始化多语言 +import { setupI18n } from '@/plugins/vueI18n' + +// 引入状态管理 +import { setupStore } from '@/store' + +// 全局组件 +import { setupGlobCom } from '@/components' + +// 引入 element-plus +import { setupElementPlus } from '@/plugins/elementPlus' + +// 引入 form-create +import { setupFormCreate } from '@/plugins/formCreate' + +// 引入全局样式 +import '@/styles/index.scss' + +// 引入动画 +import '@/plugins/animate.css' + +// 路由 +import router, { setupRouter } from '@/router' + +// 权限 +import { setupAuth } from '@/directives' + +import { createApp } from 'vue' + +import App from './App.vue' + +import './permission' + +import '@/plugins/tongji' // 百度统计 +import Logger from '@/utils/Logger' + +import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患 + +// 创建实例 +const setupAll = async () => { + const app = createApp(App) + + await setupI18n(app) + + setupStore(app) + + setupGlobCom(app) + + setupElementPlus(app) + + setupFormCreate(app) + + setupRouter(app) + + setupAuth(app) + + await router.isReady() + + app.use(VueDOMPurifyHTML) + + app.mount('#app') +} + +setupAll() + +Logger.prettyPrimary(`欢迎使用`, import.meta.env.VITE_APP_TITLE) diff --git a/mes-ui/mes-ui-admin-vue3/src/permission.ts b/mes-ui/mes-ui-admin-vue3/src/permission.ts new file mode 100644 index 00000000..0698dc88 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/permission.ts @@ -0,0 +1,70 @@ +import router from './router' +import type { RouteRecordRaw } from 'vue-router' +import { isRelogin } from '@/config/axios/service' +import { getAccessToken } from '@/utils/auth' +import { useTitle } from '@/hooks/web/useTitle' +import { useNProgress } from '@/hooks/web/useNProgress' +import { usePageLoading } from '@/hooks/web/usePageLoading' +import { useDictStoreWithOut } from '@/store/modules/dict' +import { useUserStoreWithOut } from '@/store/modules/user' +import { usePermissionStoreWithOut } from '@/store/modules/permission' + +const { start, done } = useNProgress() + +const { loadStart, loadDone } = usePageLoading() +// 路由不重定向白名单 +const whiteList = [ + '/login', + '/social-login', + '/auth-redirect', + '/bind', + '/register', + '/oauthLogin/gitee' +] + +// 路由加载前 +router.beforeEach(async (to, from, next) => { + start() + loadStart() + if (getAccessToken()) { + if (to.path === '/login') { + next({ path: '/' }) + } else { + // 获取所有字典 + const dictStore = useDictStoreWithOut() + const userStore = useUserStoreWithOut() + const permissionStore = usePermissionStoreWithOut() + if (!dictStore.getIsSetDict) { + await dictStore.setDictMap() + } + if (!userStore.getIsSetUser) { + isRelogin.show = true + await userStore.setUserInfoAction() + isRelogin.show = false + // 后端过滤菜单 + await permissionStore.generateRoutes() + permissionStore.getAddRouters.forEach((route) => { + router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表 + }) + const redirectPath = from.query.redirect || to.path + const redirect = decodeURIComponent(redirectPath as string) + const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect } + next(nextData) + } else { + next() + } + } + } else { + if (whiteList.indexOf(to.path) !== -1) { + next() + } else { + next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页 + } + } +}) + +router.afterEach((to) => { + useTitle(to?.meta?.title as string) + done() // 结束Progress + loadDone() +}) diff --git a/mes-ui/mes-ui-admin-vue3/src/plugins/animate.css/index.ts b/mes-ui/mes-ui-admin-vue3/src/plugins/animate.css/index.ts new file mode 100644 index 00000000..3e934513 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/plugins/animate.css/index.ts @@ -0,0 +1 @@ +import 'animate.css' diff --git a/mes-ui/mes-ui-admin-vue3/src/plugins/echarts/index.ts b/mes-ui/mes-ui-admin-vue3/src/plugins/echarts/index.ts new file mode 100644 index 00000000..c7ad7a17 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/plugins/echarts/index.ts @@ -0,0 +1,47 @@ +import * as echarts from 'echarts/core' + +import { + BarChart, + LineChart, + PieChart, + MapChart, + PictorialBarChart, + RadarChart, + GaugeChart +} from 'echarts/charts' + +import { + TitleComponent, + TooltipComponent, + GridComponent, + PolarComponent, + AriaComponent, + ParallelComponent, + LegendComponent, + ToolboxComponent, + VisualMapComponent +} from 'echarts/components' + +import { CanvasRenderer } from 'echarts/renderers' + +echarts.use([ + LegendComponent, + TitleComponent, + TooltipComponent, + ToolboxComponent, + GridComponent, + PolarComponent, + AriaComponent, + ParallelComponent, + VisualMapComponent, + BarChart, + LineChart, + PieChart, + MapChart, + CanvasRenderer, + PictorialBarChart, + RadarChart, + GaugeChart +]) + +export default echarts diff --git a/mes-ui/mes-ui-admin-vue3/src/plugins/elementPlus/index.ts b/mes-ui/mes-ui-admin-vue3/src/plugins/elementPlus/index.ts new file mode 100644 index 00000000..0ae2a8b2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/plugins/elementPlus/index.ts @@ -0,0 +1,17 @@ +import type { App } from 'vue' +// 需要全局引入一些组件,如ElScrollbar,不然一些下拉项样式有问题 +import { ElLoading, ElScrollbar, ElButton } from 'element-plus' + +const plugins = [ElLoading] + +const components = [ElScrollbar, ElButton] + +export const setupElementPlus = (app: App) => { + plugins.forEach((plugin) => { + app.use(plugin) + }) + + components.forEach((component) => { + app.component(component.name, component) + }) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/plugins/formCreate/index.ts b/mes-ui/mes-ui-admin-vue3/src/plugins/formCreate/index.ts new file mode 100644 index 00000000..a6cb8213 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/plugins/formCreate/index.ts @@ -0,0 +1,43 @@ +import type { App } from 'vue' +// 👇使用 form-create 需额外全局引入 element plus 组件 +import { + ElAside, + ElPopconfirm, + ElHeader, + ElMain, + ElContainer, + ElDivider, + ElTransfer, + ElAlert, + ElTabs, + ElTable, + ElTableColumn, + ElTabPane +} from 'element-plus' + +import formCreate from '@form-create/element-ui' +import install from '@form-create/element-ui/auto-import' + +const components = [ + ElAside, + ElPopconfirm, + ElHeader, + ElMain, + ElContainer, + ElDivider, + ElTransfer, + ElAlert, + ElTabs, + ElTable, + ElTableColumn, + ElTabPane +] + +// 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档 +export const setupFormCreate = (app: App) => { + components.forEach((component) => { + app.component(component.name, component) + }) + formCreate.use(install) + app.use(formCreate) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/plugins/svgIcon/index.ts b/mes-ui/mes-ui-admin-vue3/src/plugins/svgIcon/index.ts new file mode 100644 index 00000000..b5b7f70d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/plugins/svgIcon/index.ts @@ -0,0 +1,3 @@ +import 'virtual:svg-icons-register' + +import '@purge-icons/generated' diff --git a/mes-ui/mes-ui-admin-vue3/src/plugins/tongji/index.ts b/mes-ui/mes-ui-admin-vue3/src/plugins/tongji/index.ts new file mode 100644 index 00000000..ec261a16 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/plugins/tongji/index.ts @@ -0,0 +1,23 @@ +import router from '@/router' + +// 用于 router push +window._hmt = window._hmt || [] +// HM_ID +const HM_ID = import.meta.env.VITE_APP_BAIDU_CODE +;(function () { + // 有值的时候,才开启 + if (!HM_ID) { + return + } + const hm = document.createElement('script') + hm.src = 'https://hm.baidu.com/hm.js?' + HM_ID + const s = document.getElementsByTagName('script')[0] + s.parentNode.insertBefore(hm, s) +})() + +router.afterEach(function (to) { + if (!HM_ID) { + return + } + _hmt.push(['_trackPageview', to.fullPath]) +}) diff --git a/mes-ui/mes-ui-admin-vue3/src/plugins/unocss/index.ts b/mes-ui/mes-ui-admin-vue3/src/plugins/unocss/index.ts new file mode 100644 index 00000000..d366b5a2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/plugins/unocss/index.ts @@ -0,0 +1 @@ +import 'virtual:uno.css' diff --git a/mes-ui/mes-ui-admin-vue3/src/plugins/vueI18n/helper.ts b/mes-ui/mes-ui-admin-vue3/src/plugins/vueI18n/helper.ts new file mode 100644 index 00000000..da6bc8c9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/plugins/vueI18n/helper.ts @@ -0,0 +1,3 @@ +export const setHtmlPageLang = (locale: LocaleType) => { + document.querySelector('html')?.setAttribute('lang', locale) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/plugins/vueI18n/index.ts b/mes-ui/mes-ui-admin-vue3/src/plugins/vueI18n/index.ts new file mode 100644 index 00000000..f845b13f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/plugins/vueI18n/index.ts @@ -0,0 +1,42 @@ +import type { App } from 'vue' +import { createI18n } from 'vue-i18n' +import { useLocaleStoreWithOut } from '@/store/modules/locale' +import type { I18n, I18nOptions } from 'vue-i18n' +import { setHtmlPageLang } from './helper' + +export let i18n: ReturnType + +const createI18nOptions = async (): Promise => { + const localeStore = useLocaleStoreWithOut() + const locale = localeStore.getCurrentLocale + const localeMap = localeStore.getLocaleMap + const defaultLocal = await import(`../../locales/${locale.lang}.ts`) + const message = defaultLocal.default ?? {} + + setHtmlPageLang(locale.lang) + + localeStore.setCurrentLocale({ + lang: locale.lang + // elLocale: elLocal + }) + + return { + legacy: false, + locale: locale.lang, + fallbackLocale: locale.lang, + messages: { + [locale.lang]: message + }, + availableLocales: localeMap.map((v) => v.lang), + sync: true, + silentTranslationWarn: true, + missingWarn: false, + silentFallbackWarn: true + } +} + +export const setupI18n = async (app: App) => { + const options = await createI18nOptions() + i18n = createI18n(options) as I18n + app.use(i18n) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/router/index.ts b/mes-ui/mes-ui-admin-vue3/src/router/index.ts new file mode 100644 index 00000000..8f66ca31 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/router/index.ts @@ -0,0 +1,28 @@ +import type { App } from 'vue' +import type { RouteRecordRaw } from 'vue-router' +import { createRouter, createWebHistory } from 'vue-router' +import remainingRouter from './modules/remaining' + +// 创建路由实例 +const router = createRouter({ + history: createWebHistory(), // createWebHashHistory URL带#,createWebHistory URL不带# + strict: true, + routes: remainingRouter as RouteRecordRaw[], + scrollBehavior: () => ({ left: 0, top: 0 }) +}) + +export const resetRouter = (): void => { + const resetWhiteNameList = ['Redirect', 'Login', 'NoFind', 'Root'] + router.getRoutes().forEach((route) => { + const { name } = route + if (name && !resetWhiteNameList.includes(name as string)) { + router.hasRoute(name) && router.removeRoute(name) + } + }) +} + +export const setupRouter = (app: App) => { + app.use(router) +} + +export default router diff --git a/mes-ui/mes-ui-admin-vue3/src/router/modules/remaining.ts b/mes-ui/mes-ui-admin-vue3/src/router/modules/remaining.ts new file mode 100644 index 00000000..ae564fe6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/router/modules/remaining.ts @@ -0,0 +1,521 @@ +import { Layout } from '@/utils/routerHelper' + +const { t } = useI18n() +/** + * redirect: noredirect 当设置 noredirect 的时候该路由在面包屑导航中不可被点击 + * name:'router-name' 设定路由的名字,一定要填写不然使用时会出现各种问题 + * meta : { + hidden: true 当设置 true 的时候该路由不会再侧边栏出现 如404,login等页面(默认 false) + + alwaysShow: true 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式, + 只有一个时,会将那个子路由当做根路由显示在侧边栏, + 若你想不管路由下面的 children 声明的个数都显示你的根路由, + 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则, + 一直显示根路由(默认 false) + + title: 'title' 设置该路由在侧边栏和面包屑中展示的名字 + + icon: 'svg-name' 设置该路由的图标 + + noCache: true 如果设置为true,则不会被 缓存(默认 false) + + breadcrumb: false 如果设置为false,则不会在breadcrumb面包屑中显示(默认 true) + + affix: true 如果设置为true,则会一直固定在tag项中(默认 false) + + noTagsView: true 如果设置为true,则不会出现在tag中(默认 false) + + activeMenu: '/dashboard' 显示高亮的路由路径 + + followAuth: '/dashboard' 跟随哪个路由进行权限过滤 + + canTo: true 设置为true即使hidden为true,也依然可以进行路由跳转(默认 false) + } + **/ +const remainingRouter: AppRouteRecordRaw[] = [ + { + path: '/redirect', + component: Layout, + name: 'Redirect', + children: [ + { + path: '/redirect/:path(.*)', + name: 'Redirect', + component: () => import('@/views/Redirect/Redirect.vue'), + meta: {} + } + ], + meta: { + hidden: true, + noTagsView: true + } + }, + { + path: '/', + component: Layout, + redirect: '/index', + name: 'Home', + meta: {}, + children: [ + { + path: 'index', + component: () => import('@/views/Home/Index.vue'), + name: 'Index', + meta: { + title: t('router.home'), + icon: 'ep:home-filled', + noCache: false, + affix: true + } + } + ] + }, + { + path: '/user', + component: Layout, + name: 'UserInfo', + meta: { + hidden: true + }, + children: [ + { + path: 'profile', + component: () => import('@/views/Profile/Index.vue'), + name: 'Profile', + meta: { + canTo: true, + hidden: true, + noTagsView: false, + icon: 'ep:user', + title: t('common.profile') + } + }, + { + path: 'notify-message', + component: () => import('@/views/system/notify/my/index.vue'), + name: 'MyNotifyMessage', + meta: { + canTo: true, + hidden: true, + noTagsView: false, + icon: 'ep:message', + title: '我的站内信' + } + } + ] + }, + + { + path: '/dict', + component: Layout, + name: 'dict', + meta: { + hidden: true + }, + children: [ + { + path: 'type/data/:dictType', + component: () => import('@/views/system/dict/data/index.vue'), + name: 'SystemDictData', + meta: { + title: '字典数据', + noCache: true, + hidden: true, + canTo: true, + icon: '', + activeMenu: '/system/dict' + } + } + ] + }, + + { + path: '/codegen', + component: Layout, + name: 'CodegenEdit', + meta: { + hidden: true + }, + children: [ + { + path: 'edit', + component: () => import('@/views/infra/codegen/EditTable.vue'), + name: 'InfraCodegenEditTable', + meta: { + noCache: true, + hidden: true, + canTo: true, + icon: 'ep:edit', + title: '修改生成配置', + activeMenu: 'infra/codegen/index' + } + } + ] + }, + { + path: '/job', + component: Layout, + name: 'JobL', + meta: { + hidden: true + }, + children: [ + { + path: 'job-log', + component: () => import('@/views/infra/job/logger/index.vue'), + name: 'InfraJobLog', + meta: { + noCache: true, + hidden: true, + canTo: true, + icon: 'ep:edit', + title: '调度日志', + activeMenu: 'infra/job/index' + } + } + ] + }, + { + path: '/login', + component: () => import('@/views/Login/Login.vue'), + name: 'Login', + meta: { + hidden: true, + title: t('router.login'), + noTagsView: true + } + }, + { + path: '/sso', + component: () => import('@/views/Login/Login.vue'), + name: 'SSOLogin', + meta: { + hidden: true, + title: t('router.login'), + noTagsView: true + } + }, + { + path: '/social-login', + component: () => import('@/views/Login/SocialLogin.vue'), + name: 'SocialLogin', + meta: { + hidden: true, + title: t('router.socialLogin'), + noTagsView: true + } + }, + { + path: '/403', + component: () => import('@/views/Error/403.vue'), + name: 'NoAccess', + meta: { + hidden: true, + title: '403', + noTagsView: true + } + }, + { + path: '/404', + component: () => import('@/views/Error/404.vue'), + name: 'NoFound', + meta: { + hidden: true, + title: '404', + noTagsView: true + } + }, + { + path: '/500', + component: () => import('@/views/Error/500.vue'), + name: 'Error', + meta: { + hidden: true, + title: '500', + noTagsView: true + } + }, + { + path: '/bpm', + component: Layout, + name: 'bpm', + meta: { + hidden: true + }, + children: [ + { + path: '/manager/form/edit', + component: () => import('@/views/bpm/form/editor/index.vue'), + name: 'BpmFormEditor', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '设计流程表单', + activeMenu: '/bpm/manager/form' + } + }, + { + path: '/manager/model/edit', + component: () => import('@/views/bpm/model/editor/index.vue'), + name: 'BpmModelEditor', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '设计流程', + activeMenu: '/bpm/manager/model' + } + }, + { + path: '/manager/definition', + component: () => import('@/views/bpm/definition/index.vue'), + name: 'BpmProcessDefinition', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '流程定义', + activeMenu: '/bpm/manager/model' + } + }, + { + path: '/manager/task-assign-rule', + component: () => import('@/views/bpm/taskAssignRule/index.vue'), + name: 'BpmTaskAssignRuleList', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '任务分配规则' + } + }, + { + path: '/process-instance/create', + component: () => import('@/views/bpm/processInstance/create/index.vue'), + name: 'BpmProcessInstanceCreate', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '发起流程', + activeMenu: 'bpm/processInstance/create' + } + }, + { + path: '/process-instance/detail', + component: () => import('@/views/bpm/processInstance/detail/index.vue'), + name: 'BpmProcessInstanceDetail', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '流程详情', + activeMenu: 'bpm/processInstance/detail' + } + }, + { + path: '/bpm/oa/leave/create', + component: () => import('@/views/bpm/oa/leave/create.vue'), + name: 'OALeaveCreate', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '发起 OA 请假', + activeMenu: '/bpm/oa/leave' + } + }, + { + path: '/bpm/oa/leave/detail', + component: () => import('@/views/bpm/oa/leave/detail.vue'), + name: 'OALeaveDetail', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '查看 OA 请假', + activeMenu: '/bpm/oa/leave' + } + } + ] + }, + { + path: '/mall/product', // 商品中心 + component: Layout, + name: 'ProductCenter', + meta: { + hidden: true + }, + children: [ + { + path: 'spu/add', + component: () => import('@/views/mall/product/spu/form/index.vue'), + name: 'ProductSpuAdd', + meta: { + noCache: true, + hidden: true, + canTo: true, + icon: 'ep:edit', + title: '商品添加', + activeMenu: '/mall/product/spu' + } + }, + { + path: 'spu/edit/:id(\\d+)', + component: () => import('@/views/mall/product/spu/form/index.vue'), + name: 'ProductSpuEdit', + meta: { + noCache: true, + hidden: true, + canTo: true, + icon: 'ep:edit', + title: '商品编辑', + activeMenu: '/mall/product/spu' + } + }, + { + path: 'spu/detail/:id(\\d+)', + component: () => import('@/views/mall/product/spu/form/index.vue'), + name: 'ProductSpuDetail', + meta: { + noCache: true, + hidden: true, + canTo: true, + icon: 'ep:view', + title: '商品详情', + activeMenu: '/mall/product/spu' + } + }, + { + path: 'property/value/:propertyId(\\d+)', + component: () => import('@/views/mall/product/property/value/index.vue'), + name: 'ProductPropertyValue', + meta: { + noCache: true, + hidden: true, + canTo: true, + icon: 'ep:view', + title: '商品属性值', + activeMenu: '/product/property' + } + } + ] + }, + { + path: '/mall/trade', // 交易中心 + component: Layout, + name: 'TradeCenter', + meta: { + hidden: true + }, + children: [ + { + path: 'order/detail/:id(\\d+)', + component: () => import('@/views/mall/trade/order/detail/index.vue'), + name: 'TradeOrderDetail', + meta: { title: '订单详情', icon: 'ep:view', activeMenu: '/mall/trade/order' } + }, + { + path: 'after-sale/detail/:id(\\d+)', + component: () => import('@/views/mall/trade/afterSale/detail/index.vue'), + name: 'TradeAfterSaleDetail', + meta: { title: '退款详情', icon: 'ep:view', activeMenu: '/mall/trade/after-sale' } + } + ] + }, + { + path: '/member', + component: Layout, + name: 'MemberCenter', + meta: { hidden: true }, + children: [ + { + path: 'user/detail/:id', + name: 'MemberUserDetail', + meta: { + title: '会员详情', + noCache: true, + hidden: true + }, + component: () => import('@/views/member/user/detail/index.vue') + } + ] + }, + { + path: '/pay', + component: Layout, + name: 'pay', + meta: { hidden: true }, + children: [ + { + path: 'cashier', + name: 'PayCashier', + meta: { + title: '收银台', + noCache: true, + hidden: true + }, + component: () => import('@/views/pay/cashier/index.vue') + } + ] + }, + { + path: '/diy', + name: 'DiyCenter', + meta: { hidden: true }, + component: Layout, + children: [ + { + path: 'template/decorate/:id', + name: 'DiyTemplateDecorate', + meta: { + title: '模板装修', + noCache: true, + hidden: true + }, + component: () => import('@/views/mall/promotion/diy/template/decorate.vue') + }, + { + path: 'page/decorate/:id', + name: 'DiyPageDecorate', + meta: { + title: '页面装修', + noCache: true, + hidden: true + }, + component: () => import('@/views/mall/promotion/diy/page/decorate.vue') + } + ] + }, + { + path: '/crm', + component: Layout, + name: 'CrmCenter', + meta: { hidden: true }, + children: [ + { + path: 'customer/detail/:id', + name: 'CrmCustomerDetail', + meta: { + title: '客户详情', + noCache: true, + hidden: true + }, + component: () => import('@/views/crm/customer/detail/index.vue') + }, + { + path: 'contact/detail/:id', + name: 'CrmContactDetail', + meta: { + title: '联系人详情', + noCache: true, + hidden: true + }, + component: () => import('@/views/crm/contact/detail/index.vue') + } + ] + } +] + +export default remainingRouter diff --git a/mes-ui/mes-ui-admin-vue3/src/store/index.ts b/mes-ui/mes-ui-admin-vue3/src/store/index.ts new file mode 100644 index 00000000..65964ea8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/store/index.ts @@ -0,0 +1,10 @@ +import type { App } from 'vue' +import { createPinia } from 'pinia' + +const store = createPinia() + +export const setupStore = (app: App) => { + app.use(store) +} + +export { store } diff --git a/mes-ui/mes-ui-admin-vue3/src/store/modules/app.ts b/mes-ui/mes-ui-admin-vue3/src/store/modules/app.ts new file mode 100644 index 00000000..1d0c797a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/store/modules/app.ts @@ -0,0 +1,276 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +import { setCssVar, humpToUnderline } from '@/utils' +import { ElMessage } from 'element-plus' +import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import { ElementPlusSize } from '@/types/elementPlus' +import { LayoutType } from '@/types/layout' +import { ThemeTypes } from '@/types/theme' + +const { wsCache } = useCache() + +interface AppState { + breadcrumb: boolean + breadcrumbIcon: boolean + collapse: boolean + uniqueOpened: boolean + hamburger: boolean + screenfull: boolean + search: boolean + size: boolean + locale: boolean + message: boolean + tagsView: boolean + tagsViewIcon: boolean + logo: boolean + fixedHeader: boolean + greyMode: boolean + pageLoading: boolean + layout: LayoutType + title: string + userInfo: string + isDark: boolean + currentSize: ElementPlusSize + sizeMap: ElementPlusSize[] + mobile: boolean + footer: boolean + theme: ThemeTypes + fixedMenu: boolean +} + +export const useAppStore = defineStore('app', { + state: (): AppState => { + return { + userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突 + sizeMap: ['default', 'large', 'small'], + mobile: false, // 是否是移动端 + title: import.meta.env.VITE_APP_TITLE, // 标题 + pageLoading: false, // 路由跳转loading + + breadcrumb: true, // 面包屑 + breadcrumbIcon: true, // 面包屑图标 + collapse: false, // 折叠菜单 + uniqueOpened: true, // 是否只保持一个子菜单的展开 + hamburger: true, // 折叠图标 + screenfull: true, // 全屏图标 + search: true, // 搜索图标 + size: true, // 尺寸图标 + locale: true, // 多语言图标 + message: true, // 消息图标 + tagsView: true, // 标签页 + tagsViewIcon: true, // 是否显示标签图标 + logo: true, // logo + fixedHeader: true, // 固定toolheader + footer: true, // 显示页脚 + greyMode: false, // 是否开始灰色模式,用于特殊悼念日 + fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单 + + layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局 + isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式 + currentSize: wsCache.get('default') || 'default', // 组件尺寸 + theme: wsCache.get(CACHE_KEY.THEME) || { + // 主题色 + elColorPrimary: '#409eff', + // 左侧菜单边框颜色 + leftMenuBorderColor: 'inherit', + // 左侧菜单背景颜色 + leftMenuBgColor: '#001529', + // 左侧菜单浅色背景颜色 + leftMenuBgLightColor: '#0f2438', + // 左侧菜单选中背景颜色 + leftMenuBgActiveColor: 'var(--el-color-primary)', + // 左侧菜单收起选中背景颜色 + leftMenuCollapseBgActiveColor: 'var(--el-color-primary)', + // 左侧菜单字体颜色 + leftMenuTextColor: '#bfcbd9', + // 左侧菜单选中字体颜色 + leftMenuTextActiveColor: '#fff', + // logo字体颜色 + logoTitleTextColor: '#fff', + // logo边框颜色 + logoBorderColor: 'inherit', + // 头部背景颜色 + topHeaderBgColor: '#fff', + // 头部字体颜色 + topHeaderTextColor: 'inherit', + // 头部悬停颜色 + topHeaderHoverColor: '#f6f6f6', + // 头部边框颜色 + topToolBorderColor: '#eee' + } + } + }, + getters: { + getBreadcrumb(): boolean { + return this.breadcrumb + }, + getBreadcrumbIcon(): boolean { + return this.breadcrumbIcon + }, + getCollapse(): boolean { + return this.collapse + }, + getUniqueOpened(): boolean { + return this.uniqueOpened + }, + getHamburger(): boolean { + return this.hamburger + }, + getScreenfull(): boolean { + return this.screenfull + }, + getSize(): boolean { + return this.size + }, + getLocale(): boolean { + return this.locale + }, + getMessage(): boolean { + return this.message + }, + getTagsView(): boolean { + return this.tagsView + }, + getTagsViewIcon(): boolean { + return this.tagsViewIcon + }, + getLogo(): boolean { + return this.logo + }, + getFixedHeader(): boolean { + return this.fixedHeader + }, + getGreyMode(): boolean { + return this.greyMode + }, + getFixedMenu(): boolean { + return this.fixedMenu + }, + getPageLoading(): boolean { + return this.pageLoading + }, + getLayout(): LayoutType { + return this.layout + }, + getTitle(): string { + return this.title + }, + getUserInfo(): string { + return this.userInfo + }, + getIsDark(): boolean { + return this.isDark + }, + getCurrentSize(): ElementPlusSize { + return this.currentSize + }, + getSizeMap(): ElementPlusSize[] { + return this.sizeMap + }, + getMobile(): boolean { + return this.mobile + }, + getTheme(): ThemeTypes { + return this.theme + }, + getFooter(): boolean { + return this.footer + } + }, + actions: { + setBreadcrumb(breadcrumb: boolean) { + this.breadcrumb = breadcrumb + }, + setBreadcrumbIcon(breadcrumbIcon: boolean) { + this.breadcrumbIcon = breadcrumbIcon + }, + setCollapse(collapse: boolean) { + this.collapse = collapse + }, + setUniqueOpened(uniqueOpened: boolean) { + this.uniqueOpened = uniqueOpened + }, + setHamburger(hamburger: boolean) { + this.hamburger = hamburger + }, + setScreenfull(screenfull: boolean) { + this.screenfull = screenfull + }, + setSize(size: boolean) { + this.size = size + }, + setLocale(locale: boolean) { + this.locale = locale + }, + setMessage(message: boolean) { + this.message = message + }, + setTagsView(tagsView: boolean) { + this.tagsView = tagsView + }, + setTagsViewIcon(tagsViewIcon: boolean) { + this.tagsViewIcon = tagsViewIcon + }, + setLogo(logo: boolean) { + this.logo = logo + }, + setFixedHeader(fixedHeader: boolean) { + this.fixedHeader = fixedHeader + }, + setGreyMode(greyMode: boolean) { + this.greyMode = greyMode + }, + setFixedMenu(fixedMenu: boolean) { + wsCache.set('fixedMenu', fixedMenu) + this.fixedMenu = fixedMenu + }, + setPageLoading(pageLoading: boolean) { + this.pageLoading = pageLoading + }, + setLayout(layout: LayoutType) { + if (this.mobile && layout !== 'classic') { + ElMessage.warning('移动端模式下不支持切换其他布局') + return + } + this.layout = layout + wsCache.set(CACHE_KEY.LAYOUT, this.layout) + }, + setTitle(title: string) { + this.title = title + }, + setIsDark(isDark: boolean) { + this.isDark = isDark + if (this.isDark) { + document.documentElement.classList.add('dark') + document.documentElement.classList.remove('light') + } else { + document.documentElement.classList.add('light') + document.documentElement.classList.remove('dark') + } + wsCache.set(CACHE_KEY.IS_DARK, this.isDark) + }, + setCurrentSize(currentSize: ElementPlusSize) { + this.currentSize = currentSize + wsCache.set('currentSize', this.currentSize) + }, + setMobile(mobile: boolean) { + this.mobile = mobile + }, + setTheme(theme: ThemeTypes) { + this.theme = Object.assign(this.theme, theme) + wsCache.set(CACHE_KEY.THEME, this.theme) + }, + setCssVarTheme() { + for (const key in this.theme) { + setCssVar(`--${humpToUnderline(key)}`, this.theme[key]) + } + }, + setFooter(footer: boolean) { + this.footer = footer + } + } +}) + +export const useAppStoreWithOut = () => { + return useAppStore(store) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/store/modules/dict.ts b/mes-ui/mes-ui-admin-vue3/src/store/modules/dict.ts new file mode 100644 index 00000000..e239fb00 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/store/modules/dict.ts @@ -0,0 +1,104 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +// @ts-ignore +import { DictDataVO } from '@/api/system/dict/types' +import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +const { wsCache } = useCache('sessionStorage') +import { getSimpleDictDataList } from '@/api/system/dict/dict.data' + +export interface DictValueType { + value: any + label: string + clorType?: string + cssClass?: string +} +export interface DictTypeType { + dictType: string + dictValue: DictValueType[] +} +export interface DictState { + dictMap: Map + isSetDict: boolean +} + +export const useDictStore = defineStore('dict', { + state: (): DictState => ({ + dictMap: new Map(), + isSetDict: false + }), + getters: { + getDictMap(): Recordable { + const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE) + if (dictMap) { + this.dictMap = dictMap + } + return this.dictMap + }, + getIsSetDict(): boolean { + return this.isSetDict + } + }, + actions: { + async setDictMap() { + const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE) + if (dictMap) { + this.dictMap = dictMap + this.isSetDict = true + } else { + const res = await getSimpleDictDataList() + // 设置数据 + const dictDataMap = new Map() + res.forEach((dictData: DictDataVO) => { + // 获得 dictType 层级 + const enumValueObj = dictDataMap[dictData.dictType] + if (!enumValueObj) { + dictDataMap[dictData.dictType] = [] + } + // 处理 dictValue 层级 + dictDataMap[dictData.dictType].push({ + value: dictData.value, + label: dictData.label, + colorType: dictData.colorType, + cssClass: dictData.cssClass + }) + }) + this.dictMap = dictDataMap + this.isSetDict = true + wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期 + } + }, + getDictByType(type: string) { + if (!this.isSetDict) { + this.setDictMap() + } + return this.dictMap[type] + }, + async resetDict() { + wsCache.delete(CACHE_KEY.DICT_CACHE) + const res = await getSimpleDictDataList() + // 设置数据 + const dictDataMap = new Map() + res.forEach((dictData: DictDataVO) => { + // 获得 dictType 层级 + const enumValueObj = dictDataMap[dictData.dictType] + if (!enumValueObj) { + dictDataMap[dictData.dictType] = [] + } + // 处理 dictValue 层级 + dictDataMap[dictData.dictType].push({ + value: dictData.value, + label: dictData.label, + colorType: dictData.colorType, + cssClass: dictData.cssClass + }) + }) + this.dictMap = dictDataMap + this.isSetDict = true + wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期 + } + } +}) + +export const useDictStoreWithOut = () => { + return useDictStore(store) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/store/modules/locale.ts b/mes-ui/mes-ui-admin-vue3/src/store/modules/locale.ts new file mode 100644 index 00000000..1fc772a7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/store/modules/locale.ts @@ -0,0 +1,59 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import en from 'element-plus/es/locale/lang/en' +import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import { LocaleDropdownType } from '@/types/localeDropdown' + +const { wsCache } = useCache() + +const elLocaleMap = { + 'zh-CN': zhCn, + en: en +} +interface LocaleState { + currentLocale: LocaleDropdownType + localeMap: LocaleDropdownType[] +} + +export const useLocaleStore = defineStore('locales', { + state: (): LocaleState => { + return { + currentLocale: { + lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN', + elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN'] + }, + // 多语言 + localeMap: [ + { + lang: 'zh-CN', + name: '简体中文' + }, + { + lang: 'en', + name: 'English' + } + ] + } + }, + getters: { + getCurrentLocale(): LocaleDropdownType { + return this.currentLocale + }, + getLocaleMap(): LocaleDropdownType[] { + return this.localeMap + } + }, + actions: { + setCurrentLocale(localeMap: LocaleDropdownType) { + // this.locale = Object.assign(this.locale, localeMap) + this.currentLocale.lang = localeMap?.lang + this.currentLocale.elLocale = elLocaleMap[localeMap?.lang] + wsCache.set(CACHE_KEY.LANG, localeMap?.lang) + } + } +}) + +export const useLocaleStoreWithOut = () => { + return useLocaleStore(store) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/store/modules/permission.ts b/mes-ui/mes-ui-admin-vue3/src/store/modules/permission.ts new file mode 100644 index 00000000..c729cea0 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/store/modules/permission.ts @@ -0,0 +1,67 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +import { cloneDeep } from 'lodash-es' +import remainingRouter from '@/router/modules/remaining' +import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper' +import { CACHE_KEY, useCache } from '@/hooks/web/useCache' + +const { wsCache } = useCache() + +export interface PermissionState { + routers: AppRouteRecordRaw[] + addRouters: AppRouteRecordRaw[] + menuTabRouters: AppRouteRecordRaw[] +} + +export const usePermissionStore = defineStore('permission', { + state: (): PermissionState => ({ + routers: [], + addRouters: [], + menuTabRouters: [] + }), + getters: { + getRouters(): AppRouteRecordRaw[] { + return this.routers + }, + getAddRouters(): AppRouteRecordRaw[] { + return flatMultiLevelRoutes(cloneDeep(this.addRouters)) + }, + getMenuTabRouters(): AppRouteRecordRaw[] { + return this.menuTabRouters + } + }, + actions: { + async generateRoutes(): Promise { + return new Promise(async (resolve) => { + // 获得菜单列表,它在登录的时候,setUserInfoAction 方法中已经进行获取 + let res: AppCustomRouteRecordRaw[] = [] + if (wsCache.get(CACHE_KEY.ROLE_ROUTERS)) { + res = wsCache.get(CACHE_KEY.ROLE_ROUTERS) as AppCustomRouteRecordRaw[] + } + const routerMap: AppRouteRecordRaw[] = generateRoute(res) + // 动态路由,404一定要放到最后面 + this.addRouters = routerMap.concat([ + { + path: '/:path(.*)*', + redirect: '/404', + name: '404Page', + meta: { + hidden: true, + breadcrumb: false + } + } + ]) + // 渲染菜单的所有路由 + this.routers = cloneDeep(remainingRouter).concat(routerMap) + resolve() + }) + }, + setMenuTabRouters(routers: AppRouteRecordRaw[]): void { + this.menuTabRouters = routers + } + } +}) + +export const usePermissionStoreWithOut = () => { + return usePermissionStore(store) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/store/modules/tagsView.ts b/mes-ui/mes-ui-admin-vue3/src/store/modules/tagsView.ts new file mode 100644 index 00000000..a60d0e45 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/store/modules/tagsView.ts @@ -0,0 +1,140 @@ +import router from '@/router' +import type { RouteLocationNormalizedLoaded } from 'vue-router' +import { getRawRoute } from '@/utils/routerHelper' +import { defineStore } from 'pinia' +import { store } from '../index' +import { findIndex } from '@/utils' + +export interface TagsViewState { + visitedViews: RouteLocationNormalizedLoaded[] + cachedViews: Set +} + +export const useTagsViewStore = defineStore('tagsView', { + state: (): TagsViewState => ({ + visitedViews: [], + cachedViews: new Set() + }), + getters: { + getVisitedViews(): RouteLocationNormalizedLoaded[] { + return this.visitedViews + }, + getCachedViews(): string[] { + return Array.from(this.cachedViews) + } + }, + actions: { + // 新增缓存和tag + addView(view: RouteLocationNormalizedLoaded): void { + this.addVisitedView(view) + this.addCachedView() + }, + // 新增tag + addVisitedView(view: RouteLocationNormalizedLoaded) { + if (this.visitedViews.some((v) => v.path === view.path)) return + if (view.meta?.noTagsView) return + this.visitedViews.push( + Object.assign({}, view, { + title: view.meta?.title || 'no-name' + }) + ) + }, + // 新增缓存 + addCachedView() { + const cacheMap: Set = new Set() + for (const v of this.visitedViews) { + const item = getRawRoute(v) + const needCache = !item.meta?.noCache + if (!needCache) { + continue + } + const name = item.name as string + cacheMap.add(name) + } + if (Array.from(this.cachedViews).sort().toString() === Array.from(cacheMap).sort().toString()) + return + this.cachedViews = cacheMap + }, + // 删除某个 + delView(view: RouteLocationNormalizedLoaded) { + this.delVisitedView(view) + this.delCachedView() + }, + // 删除tag + delVisitedView(view: RouteLocationNormalizedLoaded) { + for (const [i, v] of this.visitedViews.entries()) { + if (v.path === view.path) { + this.visitedViews.splice(i, 1) + break + } + } + }, + // 删除缓存 + delCachedView() { + const route = router.currentRoute.value + const index = findIndex(this.getCachedViews, (v) => v === route.name) + if (index > -1) { + this.cachedViews.delete(this.getCachedViews[index]) + } + }, + // 删除所有缓存和tag + delAllViews() { + this.delAllVisitedViews() + this.delCachedView() + }, + // 删除所有tag + delAllVisitedViews() { + // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) + this.visitedViews = [] + }, + // 删除其他 + delOthersViews(view: RouteLocationNormalizedLoaded) { + this.delOthersVisitedViews(view) + this.addCachedView() + }, + // 删除其他tag + delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { + this.visitedViews = this.visitedViews.filter((v) => { + return v?.meta?.affix || v.path === view.path + }) + }, + // 删除左侧 + delLeftViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex( + this.visitedViews, + (v) => v.path === view.path + ) + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.path === view.path || i > index + }) + this.addCachedView() + } + }, + // 删除右侧 + delRightViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex( + this.visitedViews, + (v) => v.path === view.path + ) + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.path === view.path || i < index + }) + this.addCachedView() + } + }, + updateVisitedView(view: RouteLocationNormalizedLoaded) { + for (let v of this.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + } + } +}) + +export const useTagsViewStoreWithOut = () => { + return useTagsViewStore(store) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/store/modules/user.ts b/mes-ui/mes-ui-admin-vue3/src/store/modules/user.ts new file mode 100644 index 00000000..1f801fef --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/store/modules/user.ts @@ -0,0 +1,84 @@ +import { store } from '../index' +import { defineStore } from 'pinia' +import { getAccessToken, removeToken } from '@/utils/auth' +import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import { getInfo, loginOut } from '@/api/login' + +const { wsCache } = useCache() + +interface UserVO { + id: number + avatar: string + nickname: string +} +interface UserInfoVO { + permissions: string[] + roles: string[] + isSetUser: boolean + user: UserVO +} + +export const useUserStore = defineStore('admin-user', { + state: (): UserInfoVO => ({ + permissions: [], + roles: [], + isSetUser: false, + user: { + id: 0, + avatar: '', + nickname: '' + } + }), + getters: { + getPermissions(): string[] { + return this.permissions + }, + getRoles(): string[] { + return this.roles + }, + getIsSetUser(): boolean { + return this.isSetUser + }, + getUser(): UserVO { + return this.user + } + }, + actions: { + async setUserInfoAction() { + if (!getAccessToken()) { + this.resetState() + return null + } + let userInfo = wsCache.get(CACHE_KEY.USER) + if (!userInfo) { + userInfo = await getInfo() + } + this.permissions = userInfo.permissions + this.roles = userInfo.roles + this.user = userInfo.user + this.isSetUser = true + wsCache.set(CACHE_KEY.USER, userInfo) + wsCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus) + }, + async loginOut() { + await loginOut() + removeToken() + wsCache.clear() + this.resetState() + }, + resetState() { + this.permissions = [] + this.roles = [] + this.isSetUser = false + this.user = { + id: 0, + avatar: '', + nickname: '' + } + } + } +}) + +export const useUserStoreWithOut = () => { + return useUserStore(store) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/styles/global.module.scss b/mes-ui/mes-ui-admin-vue3/src/styles/global.module.scss new file mode 100644 index 00000000..8448a924 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/styles/global.module.scss @@ -0,0 +1,6 @@ +@import './variables.scss'; +// 导出变量 +:export { + namespace: $namespace; + elNamespace: $elNamespace; +} diff --git a/mes-ui/mes-ui-admin-vue3/src/styles/index.scss b/mes-ui/mes-ui-admin-vue3/src/styles/index.scss new file mode 100644 index 00000000..0952bd07 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/styles/index.scss @@ -0,0 +1,35 @@ +@import './var.css'; +@import 'element-plus/theme-chalk/dark/css-vars.css'; + +.reset-margin [class*='el-icon'] + span { + margin-left: 2px !important; +} + +// 解决抽屉弹出时,body宽度变化的问题 +.el-popup-parent--hidden { + width: 100% !important; +} + +// 解决表格内容超过表格总宽度后,横向滚动条前端顶不到表格边缘的问题 +.el-scrollbar__bar { + display: flex; + justify-content: flex-start; +} + +/* nprogress 适配 element-plus 的主题色 */ +#nprogress { + & .bar { + background-color: var(--el-color-primary) !important; + } + + & .peg { + box-shadow: + 0 0 10px var(--el-color-primary), + 0 0 5px var(--el-color-primary) !important; + } + + & .spinner-icon { + border-top-color: var(--el-color-primary); + border-left-color: var(--el-color-primary); + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/styles/theme.scss b/mes-ui/mes-ui-admin-vue3/src/styles/theme.scss new file mode 100644 index 00000000..39b03b3d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/styles/theme.scss @@ -0,0 +1,6 @@ +// .text-color { +// color: var(--el-text-color-regular); +// } +// .dark .dark\:text-color { +// color: rgba(255, 255, 255, var(--dark-text-color)); +// } diff --git a/mes-ui/mes-ui-admin-vue3/src/styles/var.css b/mes-ui/mes-ui-admin-vue3/src/styles/var.css new file mode 100644 index 00000000..63459ba6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/styles/var.css @@ -0,0 +1,66 @@ +:root { + --login-bg-color: #293146; + + --left-menu-max-width: 200px; + + --left-menu-min-width: 64px; + + --left-menu-bg-color: #001529; + + --left-menu-bg-light-color: #0f2438; + + --left-menu-bg-active-color: var(--el-color-primary); + + --left-menu-text-color: #bfcbd9; + + --left-menu-text-active-color: #fff; + + --left-menu-collapse-bg-active-color: var(--el-color-primary); + /* left menu end */ + + /* logo start */ + --logo-height: 50px; + + --logo-title-text-color: #fff; + /* logo end */ + + /* header start */ + --top-header-bg-color: '#fff'; + + --top-header-text-color: 'inherit'; + + --top-header-hover-color: #f6f6f6; + + --top-tool-height: var(--logo-height); + + --top-tool-p-x: 0; + + --tags-view-height: 35px; + /* header start */ + + /* tab menu start */ + --tab-menu-max-width: 80px; + + --tab-menu-min-width: 30px; + + --tab-menu-collapse-height: 36px; + /* tab menu end */ + + --app-content-padding: 20px; + + --app-content-bg-color: #f5f7f9; + + --app-footer-height: 50px; + + --transition-time-02: 0.2s; +} + +.dark { + --app-content-bg-color: var(--el-bg-color); +} + +html, +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/mes-ui/mes-ui-admin-vue3/src/styles/variables.scss b/mes-ui/mes-ui-admin-vue3/src/styles/variables.scss new file mode 100644 index 00000000..00b66f1f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/styles/variables.scss @@ -0,0 +1,4 @@ +// 命名空间 +$namespace: v; +// el命名空间 +$elNamespace: el; diff --git a/mes-ui/mes-ui-admin-vue3/src/types/components.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/components.d.ts new file mode 100644 index 00000000..8de1f335 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/components.d.ts @@ -0,0 +1,56 @@ +export type ComponentName = + | 'Radio' + | 'RadioButton' + | 'Checkbox' + | 'CheckboxButton' + | 'Input' + | 'Autocomplete' + | 'InputNumber' + | 'Select' + | 'Cascader' + | 'Switch' + | 'Slider' + | 'TimePicker' + | 'DatePicker' + | 'Rate' + | 'ColorPicker' + | 'Transfer' + | 'Divider' + | 'TimeSelect' + | 'SelectV2' + | 'TreeSelect' + | 'InputPassword' + | 'Editor' + | 'UploadImg' + | 'UploadImgs' + | 'UploadFile' + +export type ColProps = { + span?: number + xs?: number + sm?: number + md?: number + lg?: number + xl?: number + tag?: string +} + +export type ComponentOptions = { + label?: string + value?: FormValueType + disabled?: boolean + key?: string | number + children?: ComponentOptions[] + options?: ComponentOptions[] +} & Recordable + +export type ComponentOptionsAlias = { + labelField?: string + valueField?: string +} + +export type ComponentProps = { + optionsAlias?: ComponentOptionsAlias + options?: ComponentOptions[] + optionsSlot?: boolean +} & Recordable diff --git a/mes-ui/mes-ui-admin-vue3/src/types/configGlobal.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/configGlobal.d.ts new file mode 100644 index 00000000..f6d7b3c3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/configGlobal.d.ts @@ -0,0 +1,4 @@ +import { ElementPlusSize } from './elementPlus' +export interface ConfigGlobalTypes { + size?: ElementPlusSize +} diff --git a/mes-ui/mes-ui-admin-vue3/src/types/contextMenu.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/contextMenu.d.ts new file mode 100644 index 00000000..0738d0e3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/contextMenu.d.ts @@ -0,0 +1,7 @@ +export type contextMenuSchema = { + disabled?: boolean + divided?: boolean + icon?: string + label: string + command?: (item: contextMenuSchema) => void +} diff --git a/mes-ui/mes-ui-admin-vue3/src/types/descriptions.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/descriptions.d.ts new file mode 100644 index 00000000..35c0b81b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/descriptions.d.ts @@ -0,0 +1,13 @@ +export interface DescriptionsSchema { + span?: number // 占多少分 + field: string // 字段名 + label?: string // label名 + width?: string | number + minWidth?: string | number + align?: 'left' | 'center' | 'right' + labelAlign?: 'left' | 'center' | 'right' + className?: string + labelClassName?: string + dateFormat?: string // add by 星语:支持时间的格式化 + dictType?: string // add by 星语:支持 dict 字典数据 +} diff --git a/mes-ui/mes-ui-admin-vue3/src/types/elementPlus.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/elementPlus.d.ts new file mode 100644 index 00000000..2c6b76e7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/elementPlus.d.ts @@ -0,0 +1,3 @@ +export type ElementPlusSize = 'default' | 'small' | 'large' + +export type ElementPlusInfoType = 'success' | 'info' | 'warning' | 'danger' diff --git a/mes-ui/mes-ui-admin-vue3/src/types/form.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/form.d.ts new file mode 100644 index 00000000..980c8cc6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/form.d.ts @@ -0,0 +1,44 @@ +import type { CSSProperties } from 'vue' +import { ColProps, ComponentProps, ComponentName } from '@/types/components' +import type { AxiosPromise } from 'axios' + +export type FormSetPropsType = { + field: string + path: string + value: any +} + +export type FormValueType = string | number | string[] | number[] | boolean | undefined | null + +export type FormItemProps = { + labelWidth?: string | number + required?: boolean + rules?: Recordable + error?: string + showMessage?: boolean + inlineMessage?: boolean + style?: CSSProperties +} + +export type FormSchema = { + // 唯一值 + field: string + // 标题 + label?: string + // 提示 + labelMessage?: string + // col组件属性 + colProps?: ColProps + // 表单组件属性,slots对应的是表单组件的插槽,规则:${field}-xxx,具体可以查看element-plus文档 + componentProps?: { slots?: Recordable } & ComponentProps + // formItem组件属性 + formItemProps?: FormItemProps + // 渲染的组件 + component?: ComponentName + // 初始值 + value?: FormValueType + // 是否隐藏 + hidden?: boolean + // 远程加载下拉项 + api?: () => AxiosPromise +} diff --git a/mes-ui/mes-ui-admin-vue3/src/types/icon.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/icon.d.ts new file mode 100644 index 00000000..d1ffcdb5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/icon.d.ts @@ -0,0 +1,5 @@ +export interface IconTypes { + size?: number + color?: string + icon: string +} diff --git a/mes-ui/mes-ui-admin-vue3/src/types/infoTip.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/infoTip.d.ts new file mode 100644 index 00000000..6eff083d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/infoTip.d.ts @@ -0,0 +1,4 @@ +export interface TipSchema { + label: string + keys?: string[] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/types/layout.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/layout.d.ts new file mode 100644 index 00000000..cad3e2af --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/layout.d.ts @@ -0,0 +1 @@ +export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu' diff --git a/mes-ui/mes-ui-admin-vue3/src/types/localeDropdown.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/localeDropdown.d.ts new file mode 100644 index 00000000..c749dce7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/localeDropdown.d.ts @@ -0,0 +1,10 @@ +export interface Language { + el: Recordable + name: string +} + +export interface LocaleDropdownType { + lang: LocaleType + name?: string + elLocale?: Language +} diff --git a/mes-ui/mes-ui-admin-vue3/src/types/qrcode.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/qrcode.d.ts new file mode 100644 index 00000000..86cdf0b9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/qrcode.d.ts @@ -0,0 +1,9 @@ +export interface QrcodeLogo { + src?: string + logoSize?: number + bgColor?: string + borderSize?: number + crossOrigin?: string + borderRadius?: number + logoRadius?: number +} diff --git a/mes-ui/mes-ui-admin-vue3/src/types/table.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/table.d.ts new file mode 100644 index 00000000..9cb4205b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/table.d.ts @@ -0,0 +1,44 @@ +export type TableColumn = { + field: string + label?: string + width?: number | string + fixed?: 'left' | 'right' + children?: TableColumn[] +} & Recordable + +export type VxeTableColumn = { + field: string + title?: string + children?: TableColumn[] +} & Recordable + +export type TableSlotDefault = { + row: Recordable + column: TableColumn + $index: number +} & Recordable + +export interface Pagination { + small?: boolean + background?: boolean + pageSize?: number + defaultPageSize?: number + total?: number + pageCount?: number + pagerCount?: number + currentPage?: number + defaultCurrentPage?: number + layout?: string + pageSizes?: number[] + popperClass?: string + prevText?: string + nextText?: string + disabled?: boolean + hideOnSinglePage?: boolean +} + +export interface TableSetPropsType { + field: string + path: string + value: any +} diff --git a/mes-ui/mes-ui-admin-vue3/src/types/theme.d.ts b/mes-ui/mes-ui-admin-vue3/src/types/theme.d.ts new file mode 100644 index 00000000..ad649b02 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/types/theme.d.ts @@ -0,0 +1,16 @@ +export type ThemeTypes = { + elColorPrimary?: string + leftMenuBorderColor?: string + leftMenuBgColor?: string + leftMenuBgLightColor?: string + leftMenuBgActiveColor?: string + leftMenuCollapseBgActiveColor?: string + leftMenuTextColor?: string + leftMenuTextActiveColor?: string + logoTitleTextColor?: string + logoBorderColor?: string + topHeaderBgColor?: string + topHeaderTextColor?: string + topHeaderHoverColor?: string + topToolBorderColor?: string +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/Logger.ts b/mes-ui/mes-ui-admin-vue3/src/utils/Logger.ts new file mode 100644 index 00000000..ca58df21 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/Logger.ts @@ -0,0 +1,100 @@ +const isArray = function (obj: any): boolean { + return Object.prototype.toString.call(obj) === '[object Array]' +} + +const Logger = () => {} + +Logger.typeColor = function (type: string) { + let color = '' + switch (type) { + case 'primary': + color = '#2d8cf0' + break + case 'success': + color = '#19be6b' + break + case 'info': + color = '#909399' + break + case 'warn': + color = '#ff9900' + break + case 'error': + color = '#f03f14' + break + default: + color = '#35495E' + break + } + return color +} + +Logger.print = function (type = 'default', text: any, back = false) { + if (typeof text === 'object') { + // 如果是對象則調用打印對象方式 + isArray(text) ? console.table(text) : console.dir(text) + return + } + if (back) { + // 如果是打印帶背景圖的 + console.log( + `%c ${text} `, + `background:${Logger.typeColor(type)}; padding: 2px; border-radius: 4px; color: #fff;` + ) + } else { + console.log( + `%c ${text} `, + `border: 1px solid ${Logger.typeColor(type)}; + padding: 2px; border-radius: 4px; + color: ${Logger.typeColor(type)};` + ) + } +} + +Logger.printBack = function (type = 'primary', text) { + this.print(type, text, true) +} + +Logger.pretty = function (type = 'primary', title, text) { + if (typeof text === 'object') { + console.group('Console Group', title) + console.log( + `%c ${title}`, + `background:${Logger.typeColor(type)};border:1px solid ${Logger.typeColor(type)}; + padding: 1px; border-radius: 4px; color: #fff;` + ) + isArray(text) ? console.table(text) : console.dir(text) + console.groupEnd() + return + } + console.log( + `%c ${title} %c ${text} %c`, + `background:${Logger.typeColor(type)};border:1px solid ${Logger.typeColor(type)}; + padding: 1px; border-radius: 4px 0 0 4px; color: #fff;`, + `border:1px solid ${Logger.typeColor(type)}; + padding: 1px; border-radius: 0 4px 4px 0; color: ${Logger.typeColor(type)};`, + 'background:transparent' + ) +} + +Logger.prettyPrimary = function (title, ...text) { + text.forEach((t) => this.pretty('primary', title, t)) +} + +Logger.prettySuccess = function (title, ...text) { + text.forEach((t) => this.pretty('success', title, t)) +} + +Logger.prettyWarn = function (title, ...text) { + text.forEach((t) => this.pretty('warn', title, t)) +} + +Logger.prettyError = function (title, ...text) { + text.forEach((t) => this.pretty('error', title, t)) +} + +Logger.prettyInfo = function (title, ...text) { + text.forEach((t) => this.pretty('info', title, t)) +} + +export default Logger diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/auth.ts b/mes-ui/mes-ui-admin-vue3/src/utils/auth.ts new file mode 100644 index 00000000..7da49b08 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/auth.ts @@ -0,0 +1,92 @@ +import { useCache } from '@/hooks/web/useCache' +import { TokenType } from '@/api/login/types' +import { decrypt, encrypt } from '@/utils/jsencrypt' + +const { wsCache } = useCache() + +const AccessTokenKey = 'ACCESS_TOKEN' +const RefreshTokenKey = 'REFRESH_TOKEN' + +// 获取token +export const getAccessToken = () => { + // 此处与TokenKey相同,此写法解决初始化时Cookies中不存在TokenKey报错 + return wsCache.get(AccessTokenKey) ? wsCache.get(AccessTokenKey) : wsCache.get('ACCESS_TOKEN') +} + +// 刷新token +export const getRefreshToken = () => { + return wsCache.get(RefreshTokenKey) +} + +// 设置token +export const setToken = (token: TokenType) => { + wsCache.set(RefreshTokenKey, token.refreshToken) + wsCache.set(AccessTokenKey, token.accessToken) +} + +// 删除token +export const removeToken = () => { + wsCache.delete(AccessTokenKey) + wsCache.delete(RefreshTokenKey) +} + +/** 格式化token(jwt格式) */ +export const formatToken = (token: string): string => { + return 'Bearer ' + token +} +// ========== 账号相关 ========== + +const LoginFormKey = 'LOGINFORM' + +export type LoginFormType = { + tenantName: string + username: string + password: string + rememberMe: boolean +} + +export const getLoginForm = () => { + const loginForm: LoginFormType = wsCache.get(LoginFormKey) + if (loginForm) { + loginForm.password = decrypt(loginForm.password) as string + } + return loginForm +} + +export const setLoginForm = (loginForm: LoginFormType) => { + loginForm.password = encrypt(loginForm.password) as string + wsCache.set(LoginFormKey, loginForm, { exp: 30 * 24 * 60 * 60 }) +} + +export const removeLoginForm = () => { + wsCache.delete(LoginFormKey) +} + +// ========== 租户相关 ========== + +const TenantIdKey = 'TENANT_ID' +const TenantNameKey = 'TENANT_NAME' + +export const getTenantName = () => { + return wsCache.get(TenantNameKey) +} + +export const setTenantName = (username: string) => { + wsCache.set(TenantNameKey, username, { exp: 30 * 24 * 60 * 60 }) +} + +export const removeTenantName = () => { + wsCache.delete(TenantNameKey) +} + +export const getTenantId = () => { + return wsCache.get(TenantIdKey) +} + +export const setTenantId = (username: string) => { + wsCache.set(TenantIdKey, username) +} + +export const removeTenantId = () => { + wsCache.delete(TenantIdKey) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/color.ts b/mes-ui/mes-ui-admin-vue3/src/utils/color.ts new file mode 100644 index 00000000..13424e57 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/color.ts @@ -0,0 +1,174 @@ +/** + * 判断是否 十六进制颜色值. + * 输入形式可为 #fff000 #f00 + * + * @param String color 十六进制颜色值 + * @return Boolean + */ +export const isHexColor = (color: string) => { + const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/ + return reg.test(color) +} + +/** + * RGB 颜色值转换为 十六进制颜色值. + * r, g, 和 b 需要在 [0, 255] 范围内 + * + * @return String 类似#ff00ff + * @param r + * @param g + * @param b + */ +export const rgbToHex = (r: number, g: number, b: number) => { + // tslint:disable-next-line:no-bitwise + const hex = ((r << 16) | (g << 8) | b).toString(16) + return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex +} + +/** + * Transform a HEX color to its RGB representation + * @param {string} hex The color to transform + * @returns The RGB representation of the passed color + */ +export const hexToRGB = (hex: string, opacity?: number) => { + let sHex = hex.toLowerCase() + if (isHexColor(hex)) { + if (sHex.length === 4) { + let sColorNew = '#' + for (let i = 1; i < 4; i += 1) { + sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1)) + } + sHex = sColorNew + } + const sColorChange: number[] = [] + for (let i = 1; i < 7; i += 2) { + sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2))) + } + return opacity + ? 'RGBA(' + sColorChange.join(',') + ',' + opacity + ')' + : 'RGB(' + sColorChange.join(',') + ')' + } + return sHex +} + +export const colorIsDark = (color: string) => { + if (!isHexColor(color)) return + const [r, g, b] = hexToRGB(color) + .replace(/(?:\(|\)|rgb|RGB)*/g, '') + .split(',') + .map((item) => Number(item)) + return r * 0.299 + g * 0.578 + b * 0.114 < 192 +} + +/** + * Darkens a HEX color given the passed percentage + * @param {string} color The color to process + * @param {number} amount The amount to change the color by + * @returns {string} The HEX representation of the processed color + */ +export const darken = (color: string, amount: number) => { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color + amount = Math.trunc((255 * amount) / 100) + return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight( + color.substring(2, 4), + amount + )}${subtractLight(color.substring(4, 6), amount)}` +} + +/** + * Lightens a 6 char HEX color according to the passed percentage + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed color represented as HEX + */ +export const lighten = (color: string, amount: number) => { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color + amount = Math.trunc((255 * amount) / 100) + return `#${addLight(color.substring(0, 2), amount)}${addLight( + color.substring(2, 4), + amount + )}${addLight(color.substring(4, 6), amount)}` +} + +/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */ +/** + * Sums the passed percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +const addLight = (color: string, amount: number) => { + const cc = parseInt(color, 16) + amount + const c = cc > 255 ? 255 : cc + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` +} + +/** + * Calculates luminance of an rgb color + * @param {number} r red + * @param {number} g green + * @param {number} b blue + */ +const luminanace = (r: number, g: number, b: number) => { + const a = [r, g, b].map((v) => { + v /= 255 + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4) + }) + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722 +} + +/** + * Calculates contrast between two rgb colors + * @param {string} rgb1 rgb color 1 + * @param {string} rgb2 rgb color 2 + */ +const contrast = (rgb1: string[], rgb2: number[]) => { + return ( + (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) / + (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05) + ) +} + +/** + * Determines what the best text color is (black or white) based con the contrast with the background + * @param hexColor - Last selected color by the user + */ +export const calculateBestTextColor = (hexColor: string) => { + const rgbColor = hexToRGB(hexColor.substring(1)) + const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0]) + + return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF' +} + +/** + * Subtracts the indicated percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +const subtractLight = (color: string, amount: number) => { + const cc = parseInt(color, 16) - amount + const c = cc < 0 ? 0 : cc + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` +} + +// 预设颜色 +export const PREDEFINE_COLORS = [ + '#ff4500', + '#ff8c00', + '#ffd700', + '#90ee90', + '#00ced1', + '#1e90ff', + '#c71585', + '#409EFF', + '#909399', + '#C0C4CC', + '#b7390b', + '#ff7800', + '#fad400', + '#5b8c5f', + '#00babd', + '#1f73c3', + '#711f57' +] diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/constants.ts b/mes-ui/mes-ui-admin-vue3/src/utils/constants.ts new file mode 100644 index 00000000..41891a4a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/constants.ts @@ -0,0 +1,416 @@ +/** + * Created by 芋道源码 + * + * 枚举类 + */ + +// ========== COMMON 模块 ========== +// 全局通用状态枚举 +export const CommonStatusEnum = { + ENABLE: 0, // 开启 + DISABLE: 1 // 禁用 +} + +// 全局用户类型枚举 +export const UserTypeEnum = { + MEMBER: 1, // 会员 + ADMIN: 2 // 管理员 +} + +// ========== SYSTEM 模块 ========== +/** + * 菜单的类型枚举 + */ +export const SystemMenuTypeEnum = { + DIR: 1, // 目录 + MENU: 2, // 菜单 + BUTTON: 3 // 按钮 +} + +/** + * 角色的类型枚举 + */ +export const SystemRoleTypeEnum = { + SYSTEM: 1, // 内置角色 + CUSTOM: 2 // 自定义角色 +} + +/** + * 数据权限的范围枚举 + */ +export const SystemDataScopeEnum = { + ALL: 1, // 全部数据权限 + DEPT_CUSTOM: 2, // 指定部门数据权限 + DEPT_ONLY: 3, // 部门数据权限 + DEPT_AND_CHILD: 4, // 部门及以下数据权限 + DEPT_SELF: 5 // 仅本人数据权限 +} + +/** + * 用户的社交平台的类型枚举 + */ +export const SystemUserSocialTypeEnum = { + DINGTALK: { + title: '钉钉', + type: 20, + source: 'dingtalk', + img: 'https://s1.ax1x.com/2022/05/22/OzMDRs.png' + }, + WECHAT_ENTERPRISE: { + title: '企业微信', + type: 30, + source: 'wechat_enterprise', + img: 'https://s1.ax1x.com/2022/05/22/OzMrzn.png' + } +} + +// ========== INFRA 模块 ========== +/** + * 代码生成模板类型 + */ +export const InfraCodegenTemplateTypeEnum = { + CRUD: 1, // 基础 CRUD + TREE: 2, // 树形 CRUD + SUB: 3 // 主子表 CRUD +} + +/** + * 任务状态的枚举 + */ +export const InfraJobStatusEnum = { + INIT: 0, // 初始化中 + NORMAL: 1, // 运行中 + STOP: 2 // 暂停运行 +} + +/** + * API 异常数据的处理状态 + */ +export const InfraApiErrorLogProcessStatusEnum = { + INIT: 0, // 未处理 + DONE: 1, // 已处理 + IGNORE: 2 // 已忽略 +} + +// ========== PAY 模块 ========== +/** + * 支付渠道枚举 + */ +export const PayChannelEnum = { + WX_PUB: { + code: 'wx_pub', + name: '微信 JSAPI 支付' + }, + WX_LITE: { + code: 'wx_lite', + name: '微信小程序支付' + }, + WX_APP: { + code: 'wx_app', + name: '微信 APP 支付' + }, + WX_BAR: { + code: 'wx_bar', + name: '微信条码支付' + }, + ALIPAY_PC: { + code: 'alipay_pc', + name: '支付宝 PC 网站支付' + }, + ALIPAY_WAP: { + code: 'alipay_wap', + name: '支付宝 WAP 网站支付' + }, + ALIPAY_APP: { + code: 'alipay_app', + name: '支付宝 APP 支付' + }, + ALIPAY_QR: { + code: 'alipay_qr', + name: '支付宝扫码支付' + }, + ALIPAY_BAR: { + code: 'alipay_bar', + name: '支付宝条码支付' + }, + MOCK: { + code: 'mock', + name: '模拟支付' + } +} + +/** + * 支付的展示模式每局 + */ +export const PayDisplayModeEnum = { + URL: { + mode: 'url' + }, + IFRAME: { + mode: 'iframe' + }, + FORM: { + mode: 'form' + }, + QR_CODE: { + mode: 'qr_code' + }, + APP: { + mode: 'app' + } +} + +/** + * 支付类型枚举 + */ +export const PayType = { + WECHAT: 'WECHAT', + ALIPAY: 'ALIPAY', + MOCK: 'MOCK' +} + +/** + * 支付订单状态枚举 + */ +export const PayOrderStatusEnum = { + WAITING: { + status: 0, + name: '未支付' + }, + SUCCESS: { + status: 10, + name: '已支付' + }, + CLOSED: { + status: 20, + name: '未支付' + } +} + +// ========== MALL - 商品模块 ========== +/** + * 商品 SPU 状态 + */ +export const ProductSpuStatusEnum = { + RECYCLE: { + status: -1, + name: '回收站' + }, + DISABLE: { + status: 0, + name: '下架' + }, + ENABLE: { + status: 1, + name: '上架' + } +} + +// ========== MALL - 营销模块 ========== +/** + * 优惠劵模板的有限期类型的枚举 + */ +export const CouponTemplateValidityTypeEnum = { + DATE: { + type: 1, + name: '固定日期可用' + }, + TERM: { + type: 2, + name: '领取之后可用' + } +} + +/** + * 优惠劵模板的领取方式的枚举 + */ +export const CouponTemplateTakeTypeEnum = { + USER: { + type: 1, + name: '直接领取' + }, + ADMIN: { + type: 2, + name: '指定发放' + }, + REGISTER: { + type: 3, + name: '新人券' + } +} + +/** + * 营销的商品范围枚举 + */ +export const PromotionProductScopeEnum = { + ALL: { + scope: 1, + name: '通用劵' + }, + SPU: { + scope: 2, + name: '商品劵' + }, + CATEGORY: { + scope: 3, + name: '品类劵' + } +} + +/** + * 营销的条件类型枚举 + */ +export const PromotionConditionTypeEnum = { + PRICE: { + type: 10, + name: '满 N 元' + }, + COUNT: { + type: 20, + name: '满 N 件' + } +} + +/** + * 优惠类型枚举 + */ +export const PromotionDiscountTypeEnum = { + PRICE: { + type: 1, + name: '满减' + }, + PERCENT: { + type: 2, + name: '折扣' + } +} + +// ========== MALL - 交易模块 ========== +/** + * 分销关系绑定模式枚举 + */ +export const BrokerageBindModeEnum = { + ANYTIME: { + mode: 1, + name: '首次绑定' + }, + REGISTER: { + mode: 2, + name: '注册绑定' + }, + OVERRIDE: { + mode: 3, + name: '覆盖绑定' + } +} +/** + * 分佣模式枚举 + */ +export const BrokerageEnabledConditionEnum = { + ALL: { + condition: 1, + name: '人人分销' + }, + ADMIN: { + condition: 2, + name: '指定分销' + } +} +/** + * 佣金记录业务类型枚举 + */ +export const BrokerageRecordBizTypeEnum = { + ORDER: { + type: 1, + name: '获得推广佣金' + }, + WITHDRAW: { + type: 2, + name: '提现申请' + } +} +/** + * 佣金提现状态枚举 + */ +export const BrokerageWithdrawStatusEnum = { + AUDITING: { + status: 0, + name: '审核中' + }, + AUDIT_SUCCESS: { + status: 10, + name: '审核通过' + }, + AUDIT_FAIL: { + status: 20, + name: '审核不通过' + }, + WITHDRAW_SUCCESS: { + status: 11, + name: '提现成功' + }, + WITHDRAW_FAIL: { + status: 21, + name: '提现失败' + } +} +/** + * 佣金提现类型枚举 + */ +export const BrokerageWithdrawTypeEnum = { + WALLET: { + type: 1, + name: '钱包' + }, + BANK: { + type: 2, + name: '银行卡' + }, + WECHAT: { + type: 3, + name: '微信' + }, + ALIPAY: { + type: 4, + name: '支付宝' + } +} + +/** + * 配送方式枚举 + */ +export const DeliveryTypeEnum = { + EXPRESS: { + type: 1, + name: '快递发货' + }, + PICK_UP: { + type: 2, + name: '到店自提' + } +} +/** + * 交易订单 - 状态 + */ +export const TradeOrderStatusEnum = { + UNPAID: { + status: 0, + name: '待支付' + }, + UNDELIVERED: { + status: 10, + name: '待发货' + }, + DELIVERED: { + status: 20, + name: '已发货' + }, + COMPLETED: { + status: 30, + name: '已完成' + }, + CANCELED: { + status: 40, + name: '已取消' + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/dict.ts b/mes-ui/mes-ui-admin-vue3/src/utils/dict.ts new file mode 100644 index 00000000..98ed9797 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/dict.ts @@ -0,0 +1,208 @@ +/** + * 数据字典工具类 + */ +import { useDictStoreWithOut } from '@/store/modules/dict' +import { ElementPlusInfoType } from '@/types/elementPlus' + +const dictStore = useDictStoreWithOut() + +/** + * 获取 dictType 对应的数据字典数组 + * + * @param dictType 数据类型 + * @returns {*|Array} 数据字典数组 + */ +export interface DictDataType { + dictType: string + label: string + value: string | number | boolean + colorType: ElementPlusInfoType | '' + cssClass: string +} + +export interface NumberDictDataType extends DictDataType { + value: number +} + +export const getDictOptions = (dictType: string) => { + return dictStore.getDictByType(dictType) || [] +} + +export const getIntDictOptions = (dictType: string): NumberDictDataType[] => { + // 获得通用的 DictDataType 列表 + const dictOptions: DictDataType[] = getDictOptions(dictType) + // 转换成 number 类型的 NumberDictDataType 类型 + // why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警 + const dictOption: NumberDictDataType[] = [] + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: parseInt(dict.value + '') + }) + }) + return dictOption +} + +export const getStrDictOptions = (dictType: string) => { + const dictOption: DictDataType[] = [] + const dictOptions: DictDataType[] = getDictOptions(dictType) + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: dict.value + '' + }) + }) + return dictOption +} + +export const getBoolDictOptions = (dictType: string) => { + const dictOption: DictDataType[] = [] + const dictOptions: DictDataType[] = getDictOptions(dictType) + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: dict.value + '' === 'true' + }) + }) + return dictOption +} + +/** + * 获取指定字典类型的指定值对应的字典对象 + * @param dictType 字典类型 + * @param value 字典值 + * @return DictDataType 字典对象 + */ +export const getDictObj = (dictType: string, value: any): DictDataType | undefined => { + const dictOptions: DictDataType[] = getDictOptions(dictType) + for (const dict of dictOptions) { + if (dict.value === value + '') { + return dict + } + } +} + +/** + * 获得字典数据的文本展示 + * + * @param dictType 字典类型 + * @param value 字典数据的值 + * @return 字典名称 + */ +export const getDictLabel = (dictType: string, value: any): string => { + const dictOptions: DictDataType[] = getDictOptions(dictType) + const dictLabel = ref('') + dictOptions.forEach((dict: DictDataType) => { + if (dict.value === value + '') { + dictLabel.value = dict.label + } + }) + return dictLabel.value +} + +export enum DICT_TYPE { + USER_TYPE = 'user_type', + COMMON_STATUS = 'common_status', + SYSTEM_TENANT_PACKAGE_ID = 'system_tenant_package_id', + TERMINAL = 'terminal', // 终端 + + // ========== SYSTEM 模块 ========== + SYSTEM_USER_SEX = 'system_user_sex', + SYSTEM_MENU_TYPE = 'system_menu_type', + SYSTEM_ROLE_TYPE = 'system_role_type', + SYSTEM_DATA_SCOPE = 'system_data_scope', + SYSTEM_NOTICE_TYPE = 'system_notice_type', + SYSTEM_OPERATE_TYPE = 'system_operate_type', + SYSTEM_LOGIN_TYPE = 'system_login_type', + SYSTEM_LOGIN_RESULT = 'system_login_result', + SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code', + SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type', + SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status', + SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status', + SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type', + SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type', + SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status', + SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type', + SYSTEM_SOCIAL_TYPE = 'system_social_type', + + // ========== INFRA 模块 ========== + INFRA_BOOLEAN_STRING = 'infra_boolean_string', + INFRA_JOB_STATUS = 'infra_job_status', + INFRA_JOB_LOG_STATUS = 'infra_job_log_status', + INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status', + INFRA_CONFIG_TYPE = 'infra_config_type', + INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type', + INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type', + INFRA_CODEGEN_SCENE = 'infra_codegen_scene', + INFRA_FILE_STORAGE = 'infra_file_storage', + + // ========== BPM 模块 ========== + BPM_MODEL_CATEGORY = 'bpm_model_category', + BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', + BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type', + BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', + BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result', + BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script', + BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type', + + // ========== PAY 模块 ========== + PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型 + PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态 + PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态 + PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态 + PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态 + PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态 + PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态 + + // ========== MP 模块 ========== + MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 + MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型 + + // ========== MALL - 会员模块 ========== + MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型 + MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型 + + // ========== MALL - 商品模块 ========== + PRODUCT_UNIT = 'product_unit', // 商品单位 + PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态 + PROMOTION_TYPE_ENUM = 'promotion_type_enum', // 营销类型枚举 + + // ========== MALL - 交易模块 ========== + EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式 + TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 售后 - 状态 + TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 售后 - 方式 + TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 售后 - 类型 + TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型 + TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态 + TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态 + TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式 + BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 分佣模式 + BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 分销关系绑定模式 + BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 佣金提现银行 + BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 佣金提现类型 + BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 佣金业务类型 + BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 佣金状态 + BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 佣金提现状态 + + // ========== MALL - 营销模块 ========== + PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型 + PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 营销的商品范围 + PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型 + PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态 + PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式 + PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态 + PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举 + PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态 + PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态 + PROMOTION_BANNER_POSITION = 'promotion_banner_position', // banner 定位 + + // ========== CRM - 客户管理模块 ========== + CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态 + CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型 + CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式 + CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', + CRM_CUSTOMER_LEVEL = 'crm_customer_level', + CRM_CUSTOMER_SOURCE = 'crm_customer_source', + CRM_PRODUCT_STATUS = 'crm_product_status', + CRM_PERMISSION_LEVEL = 'crm_permission_level' // CRM 数据权限的级别 +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/domUtils.ts b/mes-ui/mes-ui-admin-vue3/src/utils/domUtils.ts new file mode 100644 index 00000000..dbc1989c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/domUtils.ts @@ -0,0 +1,289 @@ +import { isServer } from './is' +const ieVersion = isServer ? 0 : Number((document as any).documentMode) +const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g +const MOZ_HACK_REGEXP = /^moz([A-Z])/ + +export interface ViewportOffsetResult { + left: number + top: number + right: number + bottom: number + rightIncludeBody: number + bottomIncludeBody: number +} + +/* istanbul ignore next */ +const trim = function (string: string) { + return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '') +} + +/* istanbul ignore next */ +const camelCase = function (name: string) { + return name + .replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) { + return offset ? letter.toUpperCase() : letter + }) + .replace(MOZ_HACK_REGEXP, 'Moz$1') +} + +/* istanbul ignore next */ +export function hasClass(el: Element, cls: string) { + if (!el || !cls) return false + if (cls.indexOf(' ') !== -1) { + throw new Error('className should not contain space.') + } + if (el.classList) { + return el.classList.contains(cls) + } else { + return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1 + } +} + +/* istanbul ignore next */ +export function addClass(el: Element, cls: string) { + if (!el) return + let curClass = el.className + const classes = (cls || '').split(' ') + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i] + if (!clsName) continue + + if (el.classList) { + el.classList.add(clsName) + } else if (!hasClass(el, clsName)) { + curClass += ' ' + clsName + } + } + if (!el.classList) { + el.className = curClass + } +} + +/* istanbul ignore next */ +export function removeClass(el: Element, cls: string) { + if (!el || !cls) return + const classes = cls.split(' ') + let curClass = ' ' + el.className + ' ' + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i] + if (!clsName) continue + + if (el.classList) { + el.classList.remove(clsName) + } else if (hasClass(el, clsName)) { + curClass = curClass.replace(' ' + clsName + ' ', ' ') + } + } + if (!el.classList) { + el.className = trim(curClass) + } +} + +export function getBoundingClientRect(element: Element): DOMRect | number { + if (!element || !element.getBoundingClientRect) { + return 0 + } + return element.getBoundingClientRect() +} + +/** + * 获取当前元素的left、top偏移 + * left:元素最左侧距离文档左侧的距离 + * top:元素最顶端距离文档顶端的距离 + * right:元素最右侧距离文档右侧的距离 + * bottom:元素最底端距离文档底端的距离 + * rightIncludeBody:元素最左侧距离文档右侧的距离 + * bottomIncludeBody:元素最底端距离文档最底部的距离 + * + * @description: + */ +export function getViewportOffset(element: Element): ViewportOffsetResult { + const doc = document.documentElement + + const docScrollLeft = doc.scrollLeft + const docScrollTop = doc.scrollTop + const docClientLeft = doc.clientLeft + const docClientTop = doc.clientTop + + const pageXOffset = window.pageXOffset + const pageYOffset = window.pageYOffset + + const box = getBoundingClientRect(element) + + const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect + + const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0) + const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0) + const offsetLeft = retLeft + pageXOffset + const offsetTop = rectTop + pageYOffset + + const left = offsetLeft - scrollLeft + const top = offsetTop - scrollTop + + const clientWidth = window.document.documentElement.clientWidth + const clientHeight = window.document.documentElement.clientHeight + return { + left: left, + top: top, + right: clientWidth - rectWidth - left, + bottom: clientHeight - rectHeight - top, + rightIncludeBody: clientWidth - left, + bottomIncludeBody: clientHeight - top + } +} + +/* istanbul ignore next */ +export const on = function ( + element: HTMLElement | Document | Window, + event: string, + handler: EventListenerOrEventListenerObject +): void { + if (element && event && handler) { + element.addEventListener(event, handler, false) + } +} + +/* istanbul ignore next */ +export const off = function ( + element: HTMLElement | Document | Window, + event: string, + handler: any +): void { + if (element && event && handler) { + element.removeEventListener(event, handler, false) + } +} + +/* istanbul ignore next */ +export const once = function (el: HTMLElement, event: string, fn: EventListener): void { + const listener = function (this: any, ...args: unknown[]) { + if (fn) { + // @ts-ignore + fn.apply(this, args) + } + off(el, event, listener) + } + on(el, event, listener) +} + +/* istanbul ignore next */ +export const getStyle = + ieVersion < 9 + ? function (element: Element | any, styleName: string) { + if (isServer) return + if (!element || !styleName) return null + styleName = camelCase(styleName) + if (styleName === 'float') { + styleName = 'styleFloat' + } + try { + switch (styleName) { + case 'opacity': + try { + return element.filters.item('alpha').opacity / 100 + } catch (e) { + return 1.0 + } + default: + return element.style[styleName] || element.currentStyle + ? element.currentStyle[styleName] + : null + } + } catch (e) { + return element.style[styleName] + } + } + : function (element: Element | any, styleName: string) { + if (isServer) return + if (!element || !styleName) return null + styleName = camelCase(styleName) + if (styleName === 'float') { + styleName = 'cssFloat' + } + try { + const computed = (document as any).defaultView.getComputedStyle(element, '') + return element.style[styleName] || computed ? computed[styleName] : null + } catch (e) { + return element.style[styleName] + } + } + +/* istanbul ignore next */ +export function setStyle(element: Element | any, styleName: any, value: any) { + if (!element || !styleName) return + + if (typeof styleName === 'object') { + for (const prop in styleName) { + if (Object.prototype.hasOwnProperty.call(styleName, prop)) { + setStyle(element, prop, styleName[prop]) + } + } + } else { + styleName = camelCase(styleName) + if (styleName === 'opacity' && ieVersion < 9) { + element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')' + } else { + element.style[styleName] = value + } + } +} + +/* istanbul ignore next */ +export const isScroll = (el: Element, vertical: any) => { + if (isServer) return + + const determinedDirection = vertical !== null || vertical !== undefined + const overflow = determinedDirection + ? vertical + ? getStyle(el, 'overflow-y') + : getStyle(el, 'overflow-x') + : getStyle(el, 'overflow') + + return overflow.match(/(scroll|auto)/) +} + +/* istanbul ignore next */ +export const getScrollContainer = (el: Element, vertical?: any) => { + if (isServer) return + + let parent: any = el + while (parent) { + if ([window, document, document.documentElement].includes(parent)) { + return window + } + if (isScroll(parent, vertical)) { + return parent + } + parent = parent.parentNode + } + + return parent +} + +/* istanbul ignore next */ +export const isInContainer = (el: Element, container: any) => { + if (isServer || !el || !container) return false + + const elRect = el.getBoundingClientRect() + let containerRect + + if ([window, document, document.documentElement, null, undefined].includes(container)) { + containerRect = { + top: 0, + right: window.innerWidth, + bottom: window.innerHeight, + left: 0 + } + } else { + containerRect = container.getBoundingClientRect() + } + + return ( + elRect.top < containerRect.bottom && + elRect.bottom > containerRect.top && + elRect.right > containerRect.left && + elRect.left < containerRect.right + ) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/download.ts b/mes-ui/mes-ui-admin-vue3/src/utils/download.ts new file mode 100644 index 00000000..ab200149 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/download.ts @@ -0,0 +1,38 @@ +const download0 = (data: Blob, fileName: string, mineType: string) => { + // 创建 blob + const blob = new Blob([data], { type: mineType }) + // 创建 href 超链接,点击进行下载 + window.URL = window.URL || window.webkitURL + const href = URL.createObjectURL(blob) + const downA = document.createElement('a') + downA.href = href + downA.download = fileName + downA.click() + // 销毁超连接 + window.URL.revokeObjectURL(href) +} + +const download = { + // 下载 Excel 方法 + excel: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/vnd.ms-excel') + }, + // 下载 Word 方法 + word: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/msword') + }, + // 下载 Zip 方法 + zip: (data: Blob, fileName: string) => { + download0(data, fileName, 'application/zip') + }, + // 下载 Html 方法 + html: (data: Blob, fileName: string) => { + download0(data, fileName, 'text/html') + }, + // 下载 Markdown 方法 + markdown: (data: Blob, fileName: string) => { + download0(data, fileName, 'text/markdown') + } +} + +export default download diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/filt.ts b/mes-ui/mes-ui-admin-vue3/src/utils/filt.ts new file mode 100644 index 00000000..b1a7b2c7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/filt.ts @@ -0,0 +1,157 @@ +export const openWindow = ( + url: string, + opt?: { + target?: '_self' | '_blank' | string + noopener?: boolean + noreferrer?: boolean + } +) => { + const { target = '__blank', noopener = true, noreferrer = true } = opt || {} + const feature: string[] = [] + + noopener && feature.push('noopener=yes') + noreferrer && feature.push('noreferrer=yes') + + window.open(url, target, feature.join(',')) +} + +/** + * @description: base64 to blob + */ +export const dataURLtoBlob = (base64Buf: string): Blob => { + const arr = base64Buf.split(',') + const typeItem = arr[0] + const mime = typeItem.match(/:(.*?);/)![1] + const bstr = window.atob(arr[1]) + let n = bstr.length + const u8arr = new Uint8Array(n) + while (n--) { + u8arr[n] = bstr.charCodeAt(n) + } + return new Blob([u8arr], { type: mime }) +} + +/** + * img url to base64 + * @param url + */ +export const urlToBase64 = (url: string, mineType?: string): Promise => { + return new Promise((resolve, reject) => { + let canvas = document.createElement('CANVAS') as Nullable + const ctx = canvas!.getContext('2d') + + const img = new Image() + img.crossOrigin = '' + img.onload = function () { + if (!canvas || !ctx) { + return reject() + } + canvas.height = img.height + canvas.width = img.width + ctx.drawImage(img, 0, 0) + const dataURL = canvas.toDataURL(mineType || 'image/png') + canvas = null + resolve(dataURL) + } + img.src = url + }) +} + +/** + * Download online pictures + * @param url + * @param filename + * @param mime + * @param bom + */ +export const downloadByOnlineUrl = ( + url: string, + filename: string, + mime?: string, + bom?: BlobPart +) => { + urlToBase64(url).then((base64) => { + downloadByBase64(base64, filename, mime, bom) + }) +} + +/** + * Download pictures based on base64 + * @param buf + * @param filename + * @param mime + * @param bom + */ +export const downloadByBase64 = (buf: string, filename: string, mime?: string, bom?: BlobPart) => { + const base64Buf = dataURLtoBlob(buf) + downloadByData(base64Buf, filename, mime, bom) +} + +/** + * Download according to the background interface file stream + * @param {*} data + * @param {*} filename + * @param {*} mime + * @param {*} bom + */ +export const downloadByData = (data: BlobPart, filename: string, mime?: string, bom?: BlobPart) => { + const blobData = typeof bom !== 'undefined' ? [bom, data] : [data] + const blob = new Blob(blobData, { type: mime || 'application/octet-stream' }) + + const blobURL = window.URL.createObjectURL(blob) + const tempLink = document.createElement('a') + tempLink.style.display = 'none' + tempLink.href = blobURL + tempLink.setAttribute('download', filename) + if (typeof tempLink.download === 'undefined') { + tempLink.setAttribute('target', '_blank') + } + document.body.appendChild(tempLink) + tempLink.click() + document.body.removeChild(tempLink) + window.URL.revokeObjectURL(blobURL) +} + +/** + * Download file according to file address + * @param {*} sUrl + */ +export const downloadByUrl = ({ + url, + target = '_blank', + fileName +}: { + url: string + target?: '_self' | '_blank' + fileName?: string +}): boolean => { + const isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1 + const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1 + + if (/(iP)/g.test(window.navigator.userAgent)) { + console.error('Your browser does not support download!') + return false + } + if (isChrome || isSafari) { + const link = document.createElement('a') + link.href = url + link.target = target + + if (link.download !== undefined) { + link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length) + } + + if (document.createEvent) { + const e = document.createEvent('MouseEvents') + e.initEvent('click', true, true) + link.dispatchEvent(e) + return true + } + } + if (url.indexOf('?') === -1) { + url += '?download' + } + + openWindow(url, { target }) + return true +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/formCreate.ts b/mes-ui/mes-ui-admin-vue3/src/utils/formCreate.ts new file mode 100644 index 00000000..6d7dbc7f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/formCreate.ts @@ -0,0 +1,54 @@ +/** + * 针对 https://github.com/xaboy/form-create-designer 封装的工具类 + */ + +// 编码表单 Conf +export const encodeConf = (designerRef: object) => { + // @ts-ignore + return JSON.stringify(designerRef.value.getOption()) +} + +// 编码表单 Fields +export const encodeFields = (designerRef: object) => { + // @ts-ignore + const rule = designerRef.value.getRule() + const fields: string[] = [] + rule.forEach((item) => { + fields.push(JSON.stringify(item)) + }) + return fields +} + +// 解码表单 Fields +export const decodeFields = (fields: string[]) => { + const rule: object[] = [] + fields.forEach((item) => { + rule.push(JSON.parse(item)) + }) + return rule +} + +// 设置表单的 Conf 和 Fields +export const setConfAndFields = (designerRef: object, conf: string, fields: string) => { + // @ts-ignore + designerRef.value.setOption(JSON.parse(conf)) + // @ts-ignore + designerRef.value.setRule(decodeFields(fields)) +} + +// 设置表单的 Conf 和 Fields +export const setConfAndFields2 = ( + detailPreview: object, + conf: string, + fields: string, + value?: object +) => { + // @ts-ignore + detailPreview.value.option = JSON.parse(conf) + // @ts-ignore + detailPreview.value.rule = decodeFields(fields) + if (value) { + // @ts-ignore + detailPreview.value.value = value + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/formRules.ts b/mes-ui/mes-ui-admin-vue3/src/utils/formRules.ts new file mode 100644 index 00000000..2989867f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/formRules.ts @@ -0,0 +1,7 @@ +const { t } = useI18n() + +// 必填项 +export const required = { + required: true, + message: t('common.required') +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/formatTime.ts b/mes-ui/mes-ui-admin-vue3/src/utils/formatTime.ts new file mode 100644 index 00000000..8b8dd438 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/formatTime.ts @@ -0,0 +1,342 @@ +import dayjs from 'dayjs' + +/** + * 日期快捷选项适用于 el-date-picker + */ +export const defaultShortcuts = [ + { + text: '今天', + value: () => { + return new Date() + } + }, + { + text: '昨天', + value: () => { + const date = new Date() + date.setTime(date.getTime() - 3600 * 1000 * 24) + return [date, date] + } + }, + { + text: '最近七天', + value: () => { + const date = new Date() + date.setTime(date.getTime() - 3600 * 1000 * 24 * 7) + return [date, new Date()] + } + }, + { + text: '最近 30 天', + value: () => { + const date = new Date() + date.setTime(date.getTime() - 3600 * 1000 * 24 * 30) + return [date, new Date()] + } + }, + { + text: '本月', + value: () => { + const date = new Date() + date.setDate(1) // 设置为当前月的第一天 + return [date, new Date()] + } + }, + { + text: '今年', + value: () => { + const date = new Date() + return [new Date(`${date.getFullYear()}-01-01`), date] + } + } +] + +/** + * 时间日期转换 + * @param date 当前时间,new Date() 格式 + * @param format 需要转换的时间格式字符串 + * @description format 字符串随意,如 `YYYY-mm、YYYY-mm-dd` + * @description format 季度:"YYYY-mm-dd HH:MM:SS QQQQ" + * @description format 星期:"YYYY-mm-dd HH:MM:SS WWW" + * @description format 几周:"YYYY-mm-dd HH:MM:SS ZZZ" + * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ" + * @returns 返回拼接后的时间字符串 + */ +export function formatDate(date: Date, format?: string): string { + // 日期不存在,则返回空 + if (!date) { + return '' + } + // 日期存在,则进行格式化 + if (format === undefined) { + format = 'YYYY-MM-DD HH:mm:ss' + } + return dayjs(date).format(format) +} + +/** + * 获取当前的日期+时间 + */ +export function getNowDateTime() { + return dayjs() +} + +/** + * 获取当前日期是第几周 + * @param dateTime 当前传入的日期值 + * @returns 返回第几周数字值 + */ +export function getWeek(dateTime: Date): number { + const temptTime = new Date(dateTime.getTime()) + // 周几 + const weekday = temptTime.getDay() || 7 + // 周1+5天=周六 + temptTime.setDate(temptTime.getDate() - weekday + 1 + 5) + let firstDay = new Date(temptTime.getFullYear(), 0, 1) + const dayOfWeek = firstDay.getDay() + let spendDay = 1 + if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1 + firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay) + const d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000) + return Math.ceil(d / 7) +} + +/** + * 将时间转换为 `几秒前`、`几分钟前`、`几小时前`、`几天前` + * @param param 当前时间,new Date() 格式或者字符串时间格式 + * @param format 需要转换的时间格式字符串 + * @description param 10秒: 10 * 1000 + * @description param 1分: 60 * 1000 + * @description param 1小时: 60 * 60 * 1000 + * @description param 24小时:60 * 60 * 24 * 1000 + * @description param 3天: 60 * 60* 24 * 1000 * 3 + * @returns 返回拼接后的时间字符串 + */ +export function formatPast(param: string | Date, format = 'YYYY-mm-dd HH:MM:SS'): string { + // 传入格式处理、存储转换值 + let t: any, s: number + // 获取js 时间戳 + let time: number = new Date().getTime() + // 是否是对象 + typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param) + // 当前时间戳 - 传入时间戳 + time = Number.parseInt(`${time - t}`) + if (time < 10000) { + // 10秒内 + return '刚刚' + } else if (time < 60000 && time >= 10000) { + // 超过10秒少于1分钟内 + s = Math.floor(time / 1000) + return `${s}秒前` + } else if (time < 3600000 && time >= 60000) { + // 超过1分钟少于1小时 + s = Math.floor(time / 60000) + return `${s}分钟前` + } else if (time < 86400000 && time >= 3600000) { + // 超过1小时少于24小时 + s = Math.floor(time / 3600000) + return `${s}小时前` + } else if (time < 259200000 && time >= 86400000) { + // 超过1天少于3天内 + s = Math.floor(time / 86400000) + return `${s}天前` + } else { + // 超过3天 + const date = typeof param === 'string' || 'object' ? new Date(param) : param + return formatDate(date, format) + } +} + +/** + * 时间问候语 + * @param param 当前时间,new Date() 格式 + * @description param 调用 `formatAxis(new Date())` 输出 `上午好` + * @returns 返回拼接后的时间字符串 + */ +export function formatAxis(param: Date): string { + const hour: number = new Date(param).getHours() + if (hour < 6) return '凌晨好' + else if (hour < 9) return '早上好' + else if (hour < 12) return '上午好' + else if (hour < 14) return '中午好' + else if (hour < 17) return '下午好' + else if (hour < 19) return '傍晚好' + else if (hour < 22) return '晚上好' + else return '夜里好' +} + +/** + * 将毫秒,转换成时间字符串。例如说,xx 分钟 + * + * @param ms 毫秒 + * @returns {string} 字符串 + */ +export function formatPast2(ms) { + const day = Math.floor(ms / (24 * 60 * 60 * 1000)) + const hour = Math.floor(ms / (60 * 60 * 1000) - day * 24) + const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60) + const second = Math.floor(ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60) + if (day > 0) { + return day + '天' + hour + '小时' + minute + '分钟' + } + if (hour > 0) { + return hour + '小时' + minute + '分钟' + } + if (minute > 0) { + return minute + '分钟' + } + if (second > 0) { + return second + '秒' + } else { + return 0 + '秒' + } +} + +/** + * element plus 的时间 Formatter 实现,使用 YYYY-MM-DD HH:mm:ss 格式 + * + * @param row 行数据 + * @param column 字段 + * @param cellValue 字段值 + */ +// @ts-ignore +export const dateFormatter = (row, column, cellValue): string => { + if (!cellValue) { + return '' + } + return formatDate(cellValue) +} + +/** + * element plus 的时间 Formatter 实现,使用 YYYY-MM-DD 格式 + * + * @param row 行数据 + * @param column 字段 + * @param cellValue 字段值 + */ +// @ts-ignore +export const dateFormatter2 = (row, column, cellValue) => { + if (!cellValue) { + return + } + return formatDate(cellValue, 'YYYY-MM-DD') +} + +/** + * 设置起始日期,时间为00:00:00 + * @param param 传入日期 + * @returns 带时间00:00:00的日期 + */ +export function beginOfDay(param: Date) { + return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 0, 0, 0) +} + +/** + * 设置结束日期,时间为23:59:59 + * @param param 传入日期 + * @returns 带时间23:59:59的日期 + */ +export function endOfDay(param: Date) { + return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 23, 59, 59) +} + +/** + * 计算两个日期间隔天数 + * @param param1 日期1 + * @param param2 日期2 + */ +export function betweenDay(param1: Date, param2: Date) { + param1 = convertDate(param1) + param2 = convertDate(param2) + // 计算差值 + return Math.floor((param2.getTime() - param1.getTime()) / (24 * 3600 * 1000)) +} + +/** + * 日期计算 + * @param param1 日期 + * @param param2 添加的时间 + */ +export function addTime(param1: Date, param2: number) { + param1 = convertDate(param1) + return new Date(param1.getTime() + param2) +} + +/** + * 日期转换 + * @param param 日期 + */ +export function convertDate(param: Date | string) { + if (typeof param === 'string') { + return new Date(param) + } + return param +} + +/** + * 指定的两个日期, 是否为同一天 + * @param a 日期 A + * @param b 日期 B + */ +export function isSameDay(a: dayjs.ConfigType, b: dayjs.ConfigType): boolean { + if (!a || !b) return false + + const aa = dayjs(a) + const bb = dayjs(b) + return aa.year() == bb.year() && aa.month() == bb.month() && aa.day() == bb.day() +} + +/** + * 获取一天的开始时间、截止时间 + * @param date 日期 + * @param days 天数 + */ +export function getDayRange( + date: dayjs.ConfigType, + days: number +): [dayjs.ConfigType, dayjs.ConfigType] { + const day = dayjs(date).add(days, 'd') + return getDateRange(day, day) +} + +/** + * 获取最近7天的开始时间、截止时间 + */ +export function getLast7Days(): [dayjs.ConfigType, dayjs.ConfigType] { + const lastWeekDay = dayjs().subtract(7, 'd') + const yesterday = dayjs().subtract(1, 'd') + return getDateRange(lastWeekDay, yesterday) +} + +/** + * 获取最近30天的开始时间、截止时间 + */ +export function getLast30Days(): [dayjs.ConfigType, dayjs.ConfigType] { + const lastMonthDay = dayjs().subtract(30, 'd') + const yesterday = dayjs().subtract(1, 'd') + return getDateRange(lastMonthDay, yesterday) +} + +/** + * 获取最近1年的开始时间、截止时间 + */ +export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] { + const lastYearDay = dayjs().subtract(1, 'y') + const yesterday = dayjs().subtract(1, 'd') + return getDateRange(lastYearDay, yesterday) +} + +/** + * 获取指定日期的开始时间、截止时间 + * @param beginDate 开始日期 + * @param endDate 截止日期 + */ +export function getDateRange( + beginDate: dayjs.ConfigType, + endDate: dayjs.ConfigType +): [string, string] { + return [ + dayjs(beginDate).startOf('d').format('YYYY-MM-DD HH:mm:ss'), + dayjs(endDate).endOf('d').format('YYYY-MM-DD HH:mm:ss') + ] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/formatter.ts b/mes-ui/mes-ui-admin-vue3/src/utils/formatter.ts new file mode 100644 index 00000000..8777f322 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/formatter.ts @@ -0,0 +1,7 @@ +import { floatToFixed2 } from '@/utils' + +// 格式化金额【分转元】 +// @ts-ignore +export const fenToYuanFormat = (_, __, cellValue: any, ___) => { + return `¥${floatToFixed2(cellValue)}` +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/index.ts b/mes-ui/mes-ui-admin-vue3/src/utils/index.ts new file mode 100644 index 00000000..6ee53e71 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/index.ts @@ -0,0 +1,287 @@ +import { toNumber } from 'lodash-es' + +/** + * + * @param component 需要注册的组件 + * @param alias 组件别名 + * @returns any + */ +export const withInstall = (component: T, alias?: string) => { + const comp = component as any + comp.install = (app: any) => { + app.component(comp.name || comp.displayName, component) + if (alias) { + app.config.globalProperties[alias] = component + } + } + return component as T & Plugin +} + +/** + * @param str 需要转下划线的驼峰字符串 + * @returns 字符串下划线 + */ +export const humpToUnderline = (str: string): string => { + return str.replace(/([A-Z])/g, '-$1').toLowerCase() +} + +/** + * @param str 需要转驼峰的下划线字符串 + * @returns 字符串驼峰 + */ +export const underlineToHump = (str: string): string => { + if (!str) return '' + return str.replace(/\-(\w)/g, (_, letter: string) => { + return letter.toUpperCase() + }) +} + +/** + * 驼峰转横杠 + */ +export const humpToDash = (str: string): string => { + return str.replace(/([A-Z])/g, '-$1').toLowerCase() +} + +export const setCssVar = (prop: string, val: any, dom = document.documentElement) => { + dom.style.setProperty(prop, val) +} + +/** + * 查找数组对象的某个下标 + * @param {Array} ary 查找的数组 + * @param {Functon} fn 判断的方法 + */ +// eslint-disable-next-line +export const findIndex = (ary: Array, fn: Fn): number => { + if (ary.findIndex) { + return ary.findIndex(fn) + } + let index = -1 + ary.some((item: T, i: number, ary: Array) => { + const ret: T = fn(item, i, ary) + if (ret) { + index = i + return ret + } + }) + return index +} + +export const trim = (str: string) => { + return str.replace(/(^\s*)|(\s*$)/g, '') +} + +/** + * @param {Date | number | string} time 需要转换的时间 + * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss + */ +export function formatTime(time: Date | number | string, fmt: string) { + if (!time) return '' + else { + const date = new Date(time) + const o = { + 'M+': date.getMonth() + 1, + 'd+': date.getDate(), + 'H+': date.getHours(), + 'm+': date.getMinutes(), + 's+': date.getSeconds(), + 'q+': Math.floor((date.getMonth() + 3) / 3), + S: date.getMilliseconds() + } + if (/(y+)/.test(fmt)) { + fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) + } + for (const k in o) { + if (new RegExp('(' + k + ')').test(fmt)) { + fmt = fmt.replace( + RegExp.$1, + RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length) + ) + } + } + return fmt + } +} + +/** + * 生成随机字符串 + */ +export function toAnyString() { + const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => { + const r: number = (Math.random() * 16) | 0 + const v: number = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString() + }) + return str +} + +/** + * 首字母大写 + */ +export function firstUpperCase(str: string) { + return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) +} + +export const generateUUID = () => { + if (typeof crypto === 'object') { + if (typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') { + const callback = (c: any) => { + const num = Number(c) + return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString( + 16 + ) + } + return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback) + } + } + let timestamp = new Date().getTime() + let performanceNow = + (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0 + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + let random = Math.random() * 16 + if (timestamp > 0) { + random = (timestamp + random) % 16 | 0 + timestamp = Math.floor(timestamp / 16) + } else { + random = (performanceNow + random) % 16 | 0 + performanceNow = Math.floor(performanceNow / 16) + } + return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16) + }) +} + +/** + * element plus 的文件大小 Formatter 实现 + * + * @param row 行数据 + * @param column 字段 + * @param cellValue 字段值 + */ +// @ts-ignore +export const fileSizeFormatter = (row, column, cellValue) => { + const fileSize = cellValue + const unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const srcSize = parseFloat(fileSize) + const index = Math.floor(Math.log(srcSize) / Math.log(1024)) + const size = srcSize / Math.pow(1024, index) + const sizeStr = size.toFixed(2) //保留的小数位数 + return sizeStr + ' ' + unitArr[index] +} + +/** + * 将值复制到目标对象,且以目标对象属性为准,例:target: {a:1} source:{a:2,b:3} 结果为:{a:2} + * @param target 目标对象 + * @param source 源对象 + */ +export const copyValueToTarget = (target, source) => { + const newObj = Object.assign({}, target, source) + // 删除多余属性 + Object.keys(newObj).forEach((key) => { + // 如果不是target中的属性则删除 + if (Object.keys(target).indexOf(key) === -1) { + delete newObj[key] + } + }) + // 更新目标对象值 + Object.assign(target, newObj) +} + +/** + * 将一个整数转换为分数保留两位小数 + * @param num + */ +export const formatToFraction = (num: number | string | undefined): number => { + if (typeof num === 'undefined') return 0 + const parsedNumber = typeof num === 'string' ? parseFloat(num) : num + return parseFloat((parsedNumber / 100).toFixed(2)) +} + +/** + * 将一个数转换为 1.00 这样 + * 数据呈现的时候使用 + * + * @param num 整数 + */ +export const floatToFixed2 = (num: number | string | undefined): string => { + let str = '0.00' + if (typeof num === 'undefined') { + return str + } + const f = formatToFraction(num) + const decimalPart = f.toString().split('.')[1] + const len = decimalPart ? decimalPart.length : 0 + switch (len) { + case 0: + str = f.toString() + '.00' + break + case 1: + str = f.toString() + '0' + break + case 2: + str = f.toString() + break + } + return str +} + +/** + * 将一个分数转换为整数 + * @param num + */ +export const convertToInteger = (num: number | string | undefined): number => { + if (typeof num === 'undefined') return 0 + const parsedNumber = typeof num === 'string' ? parseFloat(num) : num + // TODO 分转元后还有小数则四舍五入 + return Math.round(parsedNumber * 100) +} + +/** + * 元转分 + */ +export const yuanToFen = (amount: string | number): number => { + return convertToInteger(amount) +} + +/** + * 分转元 + */ +export const fenToYuan = (price: string | number): number => { + return formatToFraction(price) +} + +/** + * 计算环比 + * + * @param value 当前数值 + * @param reference 对比数值 + */ +export const calculateRelativeRate = (value?: number, reference?: number) => { + // 防止除0 + if (!reference) return 0 + + return ((100 * ((value || 0) - reference)) / reference).toFixed(0) +} + +/** + * 获取链接的参数值 + * @param key 参数键名 + * @param urlStr 链接地址,默认为当前浏览器的地址 + */ +export const getUrlValue = (key: string, urlStr: string = location.href): string => { + if (!urlStr || !key) return '' + const url = new URL(decodeURIComponent(urlStr)) + return url.searchParams.get(key) ?? '' +} + +/** + * 获取链接的参数值(值类型) + * @param key 参数键名 + * @param urlStr 链接地址,默认为当前浏览器的地址 + */ +export const getUrlNumberValue = (key: string, urlStr: string = location.href): number => { + return toNumber(getUrlValue(key, urlStr)) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/is.ts b/mes-ui/mes-ui-admin-vue3/src/utils/is.ts new file mode 100644 index 00000000..eec86a93 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/is.ts @@ -0,0 +1,117 @@ +// copy to vben-admin + +const toString = Object.prototype.toString + +export const is = (val: unknown, type: string) => { + return toString.call(val) === `[object ${type}]` +} + +export const isDef = (val?: T): val is T => { + return typeof val !== 'undefined' +} + +export const isUnDef = (val?: T): val is T => { + return !isDef(val) +} + +export const isObject = (val: any): val is Record => { + return val !== null && is(val, 'Object') +} + +export const isEmpty = (val: T): val is T => { + if (val === null) { + return true + } + if (isArray(val) || isString(val)) { + return val.length === 0 + } + + if (val instanceof Map || val instanceof Set) { + return val.size === 0 + } + + if (isObject(val)) { + return Object.keys(val).length === 0 + } + + return false +} + +export const isDate = (val: unknown): val is Date => { + return is(val, 'Date') +} + +export const isNull = (val: unknown): val is null => { + return val === null +} + +export const isNullAndUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) && isNull(val) +} + +export const isNullOrUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) || isNull(val) +} + +export const isNumber = (val: unknown): val is number => { + return is(val, 'Number') +} + +export const isPromise = (val: unknown): val is Promise => { + return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch) +} + +export const isString = (val: unknown): val is string => { + return is(val, 'String') +} + +export const isFunction = (val: unknown): val is Function => { + return typeof val === 'function' +} + +export const isBoolean = (val: unknown): val is boolean => { + return is(val, 'Boolean') +} + +export const isRegExp = (val: unknown): val is RegExp => { + return is(val, 'RegExp') +} + +export const isArray = (val: any): val is Array => { + return val && Array.isArray(val) +} + +export const isWindow = (val: any): val is Window => { + return typeof window !== 'undefined' && is(val, 'Window') +} + +export const isElement = (val: unknown): val is Element => { + return isObject(val) && !!val.tagName +} + +export const isMap = (val: unknown): val is Map => { + return is(val, 'Map') +} + +export const isServer = typeof window === 'undefined' + +export const isClient = !isServer + +export const isUrl = (path: string): boolean => { + const reg = + /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/ + return reg.test(path) +} + +export const isDark = (): boolean => { + return window.matchMedia('(prefers-color-scheme: dark)').matches +} + +// 是否是图片链接 +export const isImgPath = (path: string): boolean => { + return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path) +} + +export const isEmptyVal = (val: any): boolean => { + return val === '' || val === null || val === undefined +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/jsencrypt.ts b/mes-ui/mes-ui-admin-vue3/src/utils/jsencrypt.ts new file mode 100644 index 00000000..374d5f64 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/jsencrypt.ts @@ -0,0 +1,31 @@ +import { JSEncrypt } from 'jsencrypt' + +// 密钥对生成 http://web.chacuo.net/netrsakeypair + +const publicKey = + 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' + +const privateKey = + 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' + + '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' + + 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' + + 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' + + 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' + + 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' + + 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' + + 'UP8iWi1Qw0Y=' + +// 加密 +export const encrypt = (txt: string) => { + const encryptor = new JSEncrypt() + encryptor.setPublicKey(publicKey) // 设置公钥 + return encryptor.encrypt(txt) // 对数据进行加密 +} + +// 解密 +export const decrypt = (txt: string) => { + const encryptor = new JSEncrypt() + encryptor.setPrivateKey(privateKey) // 设置私钥 + return encryptor.decrypt(txt) // 对数据进行解密 +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/permission.ts b/mes-ui/mes-ui-admin-vue3/src/utils/permission.ts new file mode 100644 index 00000000..a63ee628 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/permission.ts @@ -0,0 +1,45 @@ +import { CACHE_KEY, useCache } from '@/hooks/web/useCache' + +const { t } = useI18n() // 国际化 + +/** + * 字符权限校验 + * @param {Array} value 校验值 + * @returns {Boolean} + */ +export function checkPermi(value: string[]) { + if (value && value instanceof Array && value.length > 0) { + const { wsCache } = useCache() + const permissionDatas = value + const all_permission = '*:*:*' + const permissions = wsCache.get(CACHE_KEY.USER).permissions + const hasPermission = permissions.some((permission) => { + return all_permission === permission || permissionDatas.includes(permission) + }) + return !!hasPermission + } else { + console.error(t('permission.hasPermission')) + return false + } +} + +/** + * 角色权限校验 + * @param {string[]} value 校验值 + * @returns {Boolean} + */ +export function checkRole(value: string[]) { + if (value && value instanceof Array && value.length > 0) { + const { wsCache } = useCache() + const permissionRoles = value + const super_admin = 'admin' + const roles = wsCache.get(CACHE_KEY.USER).roles + const hasRole = roles.some((role) => { + return super_admin === role || permissionRoles.includes(role) + }) + return !!hasRole + } else { + console.error(t('permission.hasRole')) + return false + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/propTypes.ts b/mes-ui/mes-ui-admin-vue3/src/utils/propTypes.ts new file mode 100644 index 00000000..863f55cc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/propTypes.ts @@ -0,0 +1,24 @@ +import { VueTypeValidableDef, VueTypesInterface, createTypes, toValidableType } from 'vue-types' +import { CSSProperties } from 'vue' + +type PropTypes = VueTypesInterface & { + readonly style: VueTypeValidableDef +} +const newPropTypes = createTypes({ + func: undefined, + bool: undefined, + string: undefined, + number: undefined, + object: undefined, + integer: undefined +}) as PropTypes + +class propTypes extends newPropTypes { + static get style() { + return toValidableType('style', { + type: [String, Object] + }) + } +} + +export { propTypes } diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/routerHelper.ts b/mes-ui/mes-ui-admin-vue3/src/utils/routerHelper.ts new file mode 100644 index 00000000..d9fe42aa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/routerHelper.ts @@ -0,0 +1,241 @@ +import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue-router' +import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' +import { isUrl } from '@/utils/is' +import { cloneDeep, omit } from 'lodash-es' + +const modules = import.meta.glob('../views/**/*.{vue,tsx}') +/** + * 注册一个异步组件 + * @param componentPath 例:/bpm/oa/leave/detail + */ +export const registerComponent = (componentPath: string) => { + for (const item in modules) { + if (item.includes(componentPath)) { + // 使用异步组件的方式来动态加载组件 + // @ts-ignore + return defineAsyncComponent(modules[item]) + } + } +} +/* Layout */ +export const Layout = () => import('@/layout/Layout.vue') + +export const getParentLayout = () => { + return () => + new Promise((resolve) => { + resolve({ + name: 'ParentLayout' + }) + }) +} + +// 按照路由中meta下的rank等级升序来排序路由 +export const ascending = (arr: any[]) => { + arr.forEach((v) => { + if (v?.meta?.rank === null) v.meta.rank = undefined + if (v?.meta?.rank === 0) { + if (v.name !== 'home' && v.path !== '/') { + console.warn('rank only the home page can be 0') + } + } + }) + return arr.sort((a: { meta: { rank: number } }, b: { meta: { rank: number } }) => { + return a?.meta?.rank - b?.meta?.rank + }) +} + +export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormalized => { + if (!route) return route + const { matched, ...opt } = route + return { + ...opt, + matched: (matched + ? matched.map((item) => ({ + meta: item.meta, + name: item.name, + path: item.path + })) + : undefined) as RouteRecordNormalized[] + } +} + +// 后端控制路由生成 +export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => { + const res: AppRouteRecordRaw[] = [] + const modulesRoutesKeys = Object.keys(modules) + for (const route of routes) { + const meta = { + title: route.name, + icon: route.icon, + hidden: !route.visible, + noCache: !route.keepAlive, + alwaysShow: + route.children && + route.children.length === 1 && + (route.alwaysShow !== undefined ? route.alwaysShow : true) + } + // 路由地址转首字母大写驼峰,作为路由名称,适配keepAlive + let data: AppRouteRecordRaw = { + path: route.path, + name: + route.componentName && route.componentName.length > 0 + ? route.componentName + : toCamelCase(route.path, true), + redirect: route.redirect, + meta: meta + } + //处理顶级非目录路由 + if (!route.children && route.parentId == 0 && route.component) { + data.component = Layout + data.meta = {} + data.name = toCamelCase(route.path, true) + 'Parent' + data.redirect = '' + meta.alwaysShow = true + const childrenData: AppRouteRecordRaw = { + path: '', + name: + route.componentName && route.componentName.length > 0 + ? route.componentName + : toCamelCase(route.path, true), + redirect: route.redirect, + meta: meta + } + const index = route?.component + ? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component)) + : modulesRoutesKeys.findIndex((ev) => ev.includes(route.path)) + childrenData.component = modules[modulesRoutesKeys[index]] + data.children = [childrenData] + } else { + // 目录 + if (route.children) { + data.component = Layout + data.redirect = getRedirect(route.path, route.children) + // 外链 + } else if (isUrl(route.path)) { + data = { + path: '/external-link', + component: Layout, + meta: { + name: route.name + }, + children: [data] + } as AppRouteRecordRaw + // 菜单 + } else { + // 对后端传component组件路径和不传做兼容(如果后端传component组件路径,那么path可以随便写,如果不传,component组件路径会根path保持一致) + const index = route?.component + ? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component)) + : modulesRoutesKeys.findIndex((ev) => ev.includes(route.path)) + data.component = modules[modulesRoutesKeys[index]] + } + if (route.children) { + data.children = generateRoute(route.children) + } + } + res.push(data as AppRouteRecordRaw) + } + return res +} +export const getRedirect = (parentPath: string, children: AppCustomRouteRecordRaw[]) => { + if (!children || children.length == 0) { + return parentPath + } + const path = generateRoutePath(parentPath, children[0].path) + // 递归子节点 + if (children[0].children) return getRedirect(path, children[0].children) +} +const generateRoutePath = (parentPath: string, path: string) => { + if (parentPath.endsWith('/')) { + parentPath = parentPath.slice(0, -1) // 移除默认的 / + } + if (!path.startsWith('/')) { + path = '/' + path + } + return parentPath + path +} +export const pathResolve = (parentPath: string, path: string) => { + if (isUrl(path)) return path + const childPath = path.startsWith('/') || !path ? path : `/${path}` + return `${parentPath}${childPath}`.replace(/\/\//g, '/') +} + +// 路由降级 +export const flatMultiLevelRoutes = (routes: AppRouteRecordRaw[]) => { + const modules: AppRouteRecordRaw[] = cloneDeep(routes) + for (let index = 0; index < modules.length; index++) { + const route = modules[index] + if (!isMultipleRoute(route)) { + continue + } + promoteRouteLevel(route) + } + return modules +} + +// 层级是否大于2 +const isMultipleRoute = (route: AppRouteRecordRaw) => { + if (!route || !Reflect.has(route, 'children') || !route.children?.length) { + return false + } + + const children = route.children + + let flag = false + for (let index = 0; index < children.length; index++) { + const child = children[index] + if (child.children?.length) { + flag = true + break + } + } + return flag +} + +// 生成二级路由 +const promoteRouteLevel = (route: AppRouteRecordRaw) => { + let router: Router | null = createRouter({ + routes: [route as RouteRecordRaw], + history: createWebHashHistory() + }) + + const routes = router.getRoutes() + addToChildren(routes, route.children || [], route) + router = null + + route.children = route.children?.map((item) => omit(item, 'children')) +} + +// 添加所有子菜单 +const addToChildren = ( + routes: RouteRecordNormalized[], + children: AppRouteRecordRaw[], + routeModule: AppRouteRecordRaw +) => { + for (let index = 0; index < children.length; index++) { + const child = children[index] + const route = routes.find((item) => item.name === child.name) + if (!route) { + continue + } + routeModule.children = routeModule.children || [] + if (!routeModule.children.find((item) => item.name === route.name)) { + routeModule.children?.push(route as unknown as AppRouteRecordRaw) + } + if (child.children?.length) { + addToChildren(routes, child.children, routeModule) + } + } +} +const toCamelCase = (str: string, upperCaseFirst: boolean) => { + str = (str || '') + .replace(/-(.)/g, function (group1: string) { + return group1.toUpperCase() + }) + .replaceAll('-', '') + + if (upperCaseFirst && str) { + str = str.charAt(0).toUpperCase() + str.slice(1) + } + + return str +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/tree.ts b/mes-ui/mes-ui-admin-vue3/src/utils/tree.ts new file mode 100644 index 00000000..91059ef9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/tree.ts @@ -0,0 +1,400 @@ +interface TreeHelperConfig { + id: string + children: string + pid: string +} + +const DEFAULT_CONFIG: TreeHelperConfig = { + id: 'id', + children: 'children', + pid: 'pid' +} +export const defaultProps = { + children: 'children', + label: 'name', + value: 'id', + isLeaf: 'leaf', + emitPath: false // 用于 cascader 组件:在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值 +} + +const getConfig = (config: Partial) => Object.assign({}, DEFAULT_CONFIG, config) + +// tree from list +export const listToTree = (list: any[], config: Partial = {}): T[] => { + const conf = getConfig(config) as TreeHelperConfig + const nodeMap = new Map() + const result: T[] = [] + const { id, children, pid } = conf + + for (const node of list) { + node[children] = node[children] || [] + nodeMap.set(node[id], node) + } + for (const node of list) { + const parent = nodeMap.get(node[pid]) + ;(parent ? parent.children : result).push(node) + } + return result +} + +export const treeToList = (tree: any, config: Partial = {}): T => { + config = getConfig(config) + const { children } = config + const result: any = [...tree] + for (let i = 0; i < result.length; i++) { + if (!result[i][children!]) continue + result.splice(i + 1, 0, ...result[i][children!]) + } + return result +} + +export const findNode = ( + tree: any, + func: Fn, + config: Partial = {} +): T | null => { + config = getConfig(config) + const { children } = config + const list = [...tree] + for (const node of list) { + if (func(node)) return node + node[children!] && list.push(...node[children!]) + } + return null +} + +export const findNodeAll = ( + tree: any, + func: Fn, + config: Partial = {} +): T[] => { + config = getConfig(config) + const { children } = config + const list = [...tree] + const result: T[] = [] + for (const node of list) { + func(node) && result.push(node) + node[children!] && list.push(...node[children!]) + } + return result +} + +export const findPath = ( + tree: any, + func: Fn, + config: Partial = {} +): T | T[] | null => { + config = getConfig(config) + const path: T[] = [] + const list = [...tree] + const visitedSet = new Set() + const { children } = config + while (list.length) { + const node = list[0] + if (visitedSet.has(node)) { + path.pop() + list.shift() + } else { + visitedSet.add(node) + node[children!] && list.unshift(...node[children!]) + path.push(node) + if (func(node)) { + return path + } + } + } + return null +} + +export const findPathAll = (tree: any, func: Fn, config: Partial = {}) => { + config = getConfig(config) + const path: any[] = [] + const list = [...tree] + const result: any[] = [] + const visitedSet = new Set(), + { children } = config + while (list.length) { + const node = list[0] + if (visitedSet.has(node)) { + path.pop() + list.shift() + } else { + visitedSet.add(node) + node[children!] && list.unshift(...node[children!]) + path.push(node) + func(node) && result.push([...path]) + } + } + return result +} + +export const filter = ( + tree: T[], + func: (n: T) => boolean, + config: Partial = {} +): T[] => { + config = getConfig(config) + const children = config.children as string + + function listFilter(list: T[]) { + return list + .map((node: any) => ({ ...node })) + .filter((node) => { + node[children] = node[children] && listFilter(node[children]) + return func(node) || (node[children] && node[children].length) + }) + } + + return listFilter(tree) +} + +export const forEach = ( + tree: T[], + func: (n: T) => any, + config: Partial = {} +): void => { + config = getConfig(config) + const list: any[] = [...tree] + const { children } = config + for (let i = 0; i < list.length; i++) { + // func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿 + if (func(list[i])) { + return + } + children && list[i][children] && list.splice(i + 1, 0, ...list[i][children]) + } +} + +/** + * @description: Extract tree specified structure + */ +export const treeMap = ( + treeData: T[], + opt: { children?: string; conversion: Fn } +): T[] => { + return treeData.map((item) => treeMapEach(item, opt)) +} + +/** + * @description: Extract tree specified structure + */ +export const treeMapEach = ( + data: any, + { children = 'children', conversion }: { children?: string; conversion: Fn } +) => { + const haveChildren = Array.isArray(data[children]) && data[children].length > 0 + const conversionData = conversion(data) || {} + if (haveChildren) { + return { + ...conversionData, + [children]: data[children].map((i: number) => + treeMapEach(i, { + children, + conversion + }) + ) + } + } else { + return { + ...conversionData + } + } +} + +/** + * 递归遍历树结构 + * @param treeDatas 树 + * @param callBack 回调 + * @param parentNode 父节点 + */ +export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => { + treeDatas.forEach((element) => { + const newNode = callBack(element, parentNode) || element + if (element.children) { + eachTree(element.children, callBack, newNode) + } + }) +} + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + */ +export const handleTree = (data: any[], id?: string, parentId?: string, children?: string) => { + if (!Array.isArray(data)) { + console.warn('data must be an array') + return [] + } + const config = { + id: id || 'id', + parentId: parentId || 'parentId', + childrenList: children || 'children' + } + + const childrenListMap = {} + const nodeIds = {} + const tree: any[] = [] + + for (const d of data) { + const parentId = d[config.parentId] + if (childrenListMap[parentId] == null) { + childrenListMap[parentId] = [] + } + nodeIds[d[config.id]] = d + childrenListMap[parentId].push(d) + } + + for (const d of data) { + const parentId = d[config.parentId] + if (nodeIds[parentId] == null) { + tree.push(d) + } + } + + for (const t of tree) { + adaptToChildrenList(t) + } + + function adaptToChildrenList(o) { + if (childrenListMap[o[config.id]] !== null) { + o[config.childrenList] = childrenListMap[o[config.id]] + } + if (o[config.childrenList]) { + for (const c of o[config.childrenList]) { + adaptToChildrenList(c) + } + } + } + + return tree +} + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + * @param {*} rootId 根Id 默认 0 + */ +// @ts-ignore +export const handleTree2 = (data, id, parentId, children, rootId) => { + id = id || 'id' + parentId = parentId || 'parentId' + // children = children || 'children' + rootId = + rootId || + Math.min( + ...data.map((item) => { + return item[parentId] + }) + ) || + 0 + // 对源数据深度克隆 + const cloneData = JSON.parse(JSON.stringify(data)) + // 循环所有项 + const treeData = cloneData.filter((father) => { + const branchArr = cloneData.filter((child) => { + // 返回每一项的子级数组 + return father[id] === child[parentId] + }) + branchArr.length > 0 ? (father.children = branchArr) : '' + // 返回第一层 + return father[parentId] === rootId + }) + return treeData !== '' ? treeData : data +} + +/** + * 校验选中的节点,是否为指定 level + * + * @param tree 要操作的树结构数据 + * @param nodeId 需要判断在什么层级的数据 + * @param level 检查的级别, 默认检查到二级 + * @return true 是;false 否 + */ +export const checkSelectedNode = (tree: any[], nodeId: any, level = 2): boolean => { + if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) { + console.warn('tree must be an array') + return false + } + + // 校验是否是一级节点 + if (tree.some((item) => item.id === nodeId)) { + return false + } + + // 递归计数 + let count = 1 + + // 深层次校验 + function performAThoroughValidation(arr: any[]): boolean { + count += 1 + for (const item of arr) { + if (item.id === nodeId) { + return true + } else if (typeof item.children !== 'undefined' && item.children.length !== 0) { + if (performAThoroughValidation(item.children)) { + return true + } + } + } + return false + } + + for (const item of tree) { + count = 1 + if (performAThoroughValidation(item.children)) { + // 找到后对比是否是期望的层级 + if (count >= level) { + return true + } + } + } + + return false +} + +/** + * 获取节点的完整结构 + * @param tree 树数据 + * @param nodeId 节点 id + */ +export const treeToString = (tree: any[], nodeId) => { + if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) { + console.warn('tree must be an array') + return '' + } + // 校验是否是一级节点 + const node = tree.find((item) => item.id === nodeId) + if (typeof node !== 'undefined') { + return node.name + } + let str = '' + + function performAThoroughValidation(arr) { + for (const item of arr) { + if (item.id === nodeId) { + str += ` / ${item.name}` + return true + } else if (typeof item.children !== 'undefined' && item.children.length !== 0) { + str += ` / ${item.name}` + if (performAThoroughValidation(item.children)) { + return true + } + } + } + return false + } + + for (const item of tree) { + str = `${item.name}` + if (performAThoroughValidation(item.children)) { + break + } + } + return str +} diff --git a/mes-ui/mes-ui-admin-vue3/src/utils/tsxHelper.ts b/mes-ui/mes-ui-admin-vue3/src/utils/tsxHelper.ts new file mode 100644 index 00000000..6087fa30 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/utils/tsxHelper.ts @@ -0,0 +1,16 @@ +import { Slots } from 'vue' +import { isFunction } from '@/utils/is' + +export const getSlot = (slots: Slots, slot = 'default', data?: Recordable) => { + // Reflect.has 判断一个对象是否存在某个属性 + if (!slots || !Reflect.has(slots, slot)) { + return null + } + if (!isFunction(slots[slot])) { + console.error(`${slot} is not a function!`) + return null + } + const slotFn = slots[slot] + if (!slotFn) return null + return slotFn(data) +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Error/403.vue b/mes-ui/mes-ui-admin-vue3/src/views/Error/403.vue new file mode 100644 index 00000000..a3ec4877 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Error/403.vue @@ -0,0 +1,8 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Error/404.vue b/mes-ui/mes-ui-admin-vue3/src/views/Error/404.vue new file mode 100644 index 00000000..f6a08de2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Error/404.vue @@ -0,0 +1,7 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Error/500.vue b/mes-ui/mes-ui-admin-vue3/src/views/Error/500.vue new file mode 100644 index 00000000..998487d2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Error/500.vue @@ -0,0 +1,7 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Home/Index.vue b/mes-ui/mes-ui-admin-vue3/src/views/Home/Index.vue new file mode 100644 index 00000000..121ec6a8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Home/Index.vue @@ -0,0 +1,381 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Home/Index2.vue b/mes-ui/mes-ui-admin-vue3/src/views/Home/Index2.vue new file mode 100644 index 00000000..c9429ab1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Home/Index2.vue @@ -0,0 +1,319 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Home/echarts-data.ts b/mes-ui/mes-ui-admin-vue3/src/views/Home/echarts-data.ts new file mode 100644 index 00000000..56093f4b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Home/echarts-data.ts @@ -0,0 +1,308 @@ +import { EChartsOption } from 'echarts' + +const { t } = useI18n() + +export const lineOptions: EChartsOption = { + title: { + text: t('analysis.monthlySales'), + left: 'center' + }, + xAxis: { + data: [ + t('analysis.january'), + t('analysis.february'), + t('analysis.march'), + t('analysis.april'), + t('analysis.may'), + t('analysis.june'), + t('analysis.july'), + t('analysis.august'), + t('analysis.september'), + t('analysis.october'), + t('analysis.november'), + t('analysis.december') + ], + boundaryGap: false, + axisTick: { + show: false + } + }, + grid: { + left: 20, + right: 20, + bottom: 20, + top: 80, + containLabel: true + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross' + }, + padding: [5, 10] + }, + yAxis: { + axisTick: { + show: false + } + }, + legend: { + data: [t('analysis.estimate'), t('analysis.actual')], + top: 50 + }, + series: [ + { + name: t('analysis.estimate'), + smooth: true, + type: 'line', + data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123], + animationDuration: 2800, + animationEasing: 'cubicInOut' + }, + { + name: t('analysis.actual'), + smooth: true, + type: 'line', + itemStyle: {}, + data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123], + animationDuration: 2800, + animationEasing: 'quadraticOut' + } + ] +} + +export const pieOptions: EChartsOption = { + title: { + text: t('analysis.userAccessSource'), + left: 'center' + }, + tooltip: { + trigger: 'item', + formatter: '{a}
{b} : {c} ({d}%)' + }, + legend: { + orient: 'vertical', + left: 'left', + data: [ + t('analysis.directAccess'), + t('analysis.mailMarketing'), + t('analysis.allianceAdvertising'), + t('analysis.videoAdvertising'), + t('analysis.searchEngines') + ] + }, + series: [ + { + name: t('analysis.userAccessSource'), + type: 'pie', + radius: '55%', + center: ['50%', '60%'], + data: [ + { value: 335, name: t('analysis.directAccess') }, + { value: 310, name: t('analysis.mailMarketing') }, + { value: 234, name: t('analysis.allianceAdvertising') }, + { value: 135, name: t('analysis.videoAdvertising') }, + { value: 1548, name: t('analysis.searchEngines') } + ] + } + ] +} + +export const barOptions: EChartsOption = { + title: { + text: t('analysis.weeklyUserActivity'), + left: 'center' + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + } + }, + grid: { + left: 50, + right: 20, + bottom: 20 + }, + xAxis: { + type: 'category', + data: [ + t('analysis.monday'), + t('analysis.tuesday'), + t('analysis.wednesday'), + t('analysis.thursday'), + t('analysis.friday'), + t('analysis.saturday'), + t('analysis.sunday') + ], + axisTick: { + alignWithLabel: true + } + }, + yAxis: { + type: 'value' + }, + series: [ + { + name: t('analysis.activeQuantity'), + data: [13253, 34235, 26321, 12340, 24643, 1322, 1324], + type: 'bar' + } + ] +} + +export const radarOption: EChartsOption = { + legend: { + data: [t('workplace.personal'), t('workplace.team')] + }, + radar: { + // shape: 'circle', + indicator: [ + { name: t('workplace.quote'), max: 65 }, + { name: t('workplace.contribution'), max: 160 }, + { name: t('workplace.hot'), max: 300 }, + { name: t('workplace.yield'), max: 130 }, + { name: t('workplace.follow'), max: 100 } + ] + }, + series: [ + { + name: `xxx${t('workplace.index')}`, + type: 'radar', + data: [ + { + value: [42, 30, 20, 35, 80], + name: t('workplace.personal') + }, + { + value: [50, 140, 290, 100, 90], + name: t('workplace.team') + } + ] + } + ] +} + +export const wordOptions = { + series: [ + { + type: 'wordCloud', + gridSize: 2, + sizeRange: [12, 50], + rotationRange: [-90, 90], + shape: 'pentagon', + width: 600, + height: 400, + drawOutOfBound: true, + textStyle: { + color: function () { + return ( + 'rgb(' + + [ + Math.round(Math.random() * 160), + Math.round(Math.random() * 160), + Math.round(Math.random() * 160) + ].join(',') + + ')' + ) + } + }, + emphasis: { + textStyle: { + shadowBlur: 10, + shadowColor: '#333' + } + }, + data: [ + { + name: 'Sam S Club', + value: 10000, + textStyle: { + color: 'black' + }, + emphasis: { + textStyle: { + color: 'red' + } + } + }, + { + name: 'Macys', + value: 6181 + }, + { + name: 'Amy Schumer', + value: 4386 + }, + { + name: 'Jurassic World', + value: 4055 + }, + { + name: 'Charter Communications', + value: 2467 + }, + { + name: 'Chick Fil A', + value: 2244 + }, + { + name: 'Planet Fitness', + value: 1898 + }, + { + name: 'Pitch Perfect', + value: 1484 + }, + { + name: 'Express', + value: 1112 + }, + { + name: 'Home', + value: 965 + }, + { + name: 'Johnny Depp', + value: 847 + }, + { + name: 'Lena Dunham', + value: 582 + }, + { + name: 'Lewis Hamilton', + value: 555 + }, + { + name: 'KXAN', + value: 550 + }, + { + name: 'Mary Ellen Mark', + value: 462 + }, + { + name: 'Farrah Abraham', + value: 366 + }, + { + name: 'Rita Ora', + value: 360 + }, + { + name: 'Serena Williams', + value: 282 + }, + { + name: 'NCAA baseball tournament', + value: 273 + }, + { + name: 'Point Break', + value: 265 + } + ] + } + ] +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Home/types.ts b/mes-ui/mes-ui-admin-vue3/src/views/Home/types.ts new file mode 100644 index 00000000..e6313d36 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Home/types.ts @@ -0,0 +1,55 @@ +export type WorkplaceTotal = { + project: number + access: number + todo: number +} + +export type Project = { + name: string + icon: string + message: string + personal: string + time: Date | number | string +} + +export type Notice = { + title: string + type: string + keys: string[] + date: Date | number | string +} + +export type Shortcut = { + name: string + icon: string + url: string +} + +export type RadarData = { + personal: number + team: number + max: number + name: string +} +export type AnalysisTotalTypes = { + users: number + messages: number + moneys: number + shoppings: number +} + +export type UserAccessSource = { + value: number + name: string +} + +export type WeeklyUserActivity = { + value: number + name: string +} + +export type MonthlySales = { + name: string + estimate: number + actual: number +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/Login.vue b/mes-ui/mes-ui-admin-vue3/src/views/Login/Login.vue new file mode 100644 index 00000000..19ffe2d7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/Login.vue @@ -0,0 +1,104 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/SocialLogin.vue b/mes-ui/mes-ui-admin-vue3/src/views/Login/SocialLogin.vue new file mode 100644 index 00000000..6bbfc1df --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/SocialLogin.vue @@ -0,0 +1,343 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/components/LoginForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/LoginForm.vue new file mode 100644 index 00000000..699d42b5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/LoginForm.vue @@ -0,0 +1,348 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/components/LoginFormTitle.vue b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/LoginFormTitle.vue new file mode 100644 index 00000000..cdf4facc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/LoginFormTitle.vue @@ -0,0 +1,26 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/components/MobileForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/MobileForm.vue new file mode 100644 index 00000000..7f5d9942 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/MobileForm.vue @@ -0,0 +1,226 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/components/QrCodeForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/QrCodeForm.vue new file mode 100644 index 00000000..31d28453 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/QrCodeForm.vue @@ -0,0 +1,30 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/components/RegisterForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/RegisterForm.vue new file mode 100644 index 00000000..23b3bd42 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/RegisterForm.vue @@ -0,0 +1,142 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/components/SSOLogin.vue b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/SSOLogin.vue new file mode 100644 index 00000000..f31ab0e5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/SSOLogin.vue @@ -0,0 +1,199 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/components/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/index.ts new file mode 100644 index 00000000..204ad73d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/index.ts @@ -0,0 +1,8 @@ +import LoginForm from './LoginForm.vue' +import MobileForm from './MobileForm.vue' +import LoginFormTitle from './LoginFormTitle.vue' +import RegisterForm from './RegisterForm.vue' +import QrCodeForm from './QrCodeForm.vue' +import SSOLoginVue from './SSOLogin.vue' + +export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue } diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Login/components/useLogin.ts b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/useLogin.ts new file mode 100644 index 00000000..b4a02f8f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Login/components/useLogin.ts @@ -0,0 +1,42 @@ +import { Ref } from 'vue' + +export enum LoginStateEnum { + LOGIN, + REGISTER, + RESET_PASSWORD, + MOBILE, + QR_CODE, + SSO +} + +const currentState = ref(LoginStateEnum.LOGIN) + +export function useLoginState() { + function setLoginState(state: LoginStateEnum) { + currentState.value = state + } + const getLoginState = computed(() => currentState.value) + + function handleBackLogin() { + setLoginState(LoginStateEnum.LOGIN) + } + + return { + setLoginState, + getLoginState, + handleBackLogin + } +} + +export function useFormValid(formRef: Ref) { + async function validForm() { + const form = unref(formRef) + if (!form) return + const data = await form.validate() + return data as T + } + + return { + validForm + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Profile/Index.vue b/mes-ui/mes-ui-admin-vue3/src/views/Profile/Index.vue new file mode 100644 index 00000000..8e1695b5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Profile/Index.vue @@ -0,0 +1,65 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/BasicInfo.vue b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/BasicInfo.vue new file mode 100644 index 00000000..e2189b15 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/BasicInfo.vue @@ -0,0 +1,92 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/ProfileUser.vue b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/ProfileUser.vue new file mode 100644 index 00000000..0d469efb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/ProfileUser.vue @@ -0,0 +1,99 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/ResetPwd.vue b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/ResetPwd.vue new file mode 100644 index 00000000..477be91f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/ResetPwd.vue @@ -0,0 +1,73 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/UserAvatar.vue b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/UserAvatar.vue new file mode 100644 index 00000000..c20168fa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/UserAvatar.vue @@ -0,0 +1,39 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/UserSocial.vue b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/UserSocial.vue new file mode 100644 index 00000000..b7f955be --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/UserSocial.vue @@ -0,0 +1,107 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/index.ts new file mode 100644 index 00000000..9e1883cf --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Profile/components/index.ts @@ -0,0 +1,7 @@ +import BasicInfo from './BasicInfo.vue' +import ProfileUser from './ProfileUser.vue' +import ResetPwd from './ResetPwd.vue' +import UserAvatarVue from './UserAvatar.vue' +import UserSocial from './UserSocial.vue' + +export { BasicInfo, ProfileUser, ResetPwd, UserAvatarVue, UserSocial } diff --git a/mes-ui/mes-ui-admin-vue3/src/views/Redirect/Redirect.vue b/mes-ui/mes-ui-admin-vue3/src/views/Redirect/Redirect.vue new file mode 100644 index 00000000..f7717ce7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/Redirect/Redirect.vue @@ -0,0 +1,28 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/definition/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/definition/index.vue new file mode 100644 index 00000000..1c179e78 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/definition/index.vue @@ -0,0 +1,174 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/form/editor/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/form/editor/index.vue new file mode 100644 index 00000000..b7c45cab --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/form/editor/index.vue @@ -0,0 +1,119 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/form/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/form/index.vue new file mode 100644 index 00000000..0cdf0456 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/form/index.vue @@ -0,0 +1,193 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/group/UserGroupForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/group/UserGroupForm.vue new file mode 100644 index 00000000..35d833ea --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/group/UserGroupForm.vue @@ -0,0 +1,132 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/group/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/group/index.vue new file mode 100644 index 00000000..957ffc89 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/group/index.vue @@ -0,0 +1,189 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/ModelForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/ModelForm.vue new file mode 100644 index 00000000..0bd54091 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/ModelForm.vue @@ -0,0 +1,230 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/ModelImportForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/ModelImportForm.vue new file mode 100644 index 00000000..74f10ffd --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/ModelImportForm.vue @@ -0,0 +1,140 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/editor/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/editor/index.vue new file mode 100644 index 00000000..f5c0ec6e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/editor/index.vue @@ -0,0 +1,105 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/index.vue new file mode 100644 index 00000000..e5da7519 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/model/index.vue @@ -0,0 +1,404 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/oa/leave/create.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/oa/leave/create.vue new file mode 100644 index 00000000..a47228c9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/oa/leave/create.vue @@ -0,0 +1,89 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/oa/leave/detail.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/oa/leave/detail.vue new file mode 100644 index 00000000..87036d8e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/oa/leave/detail.vue @@ -0,0 +1,51 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/oa/leave/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/oa/leave/index.vue new file mode 100644 index 00000000..72966db8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/oa/leave/index.vue @@ -0,0 +1,235 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/create/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/create/index.vue new file mode 100644 index 00000000..a10e0208 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/create/index.vue @@ -0,0 +1,133 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue new file mode 100644 index 00000000..0a2057dd --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue @@ -0,0 +1,57 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue new file mode 100644 index 00000000..363874cf --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue @@ -0,0 +1,96 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue new file mode 100644 index 00000000..97287e99 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue @@ -0,0 +1,128 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue new file mode 100644 index 00000000..40cd200e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue @@ -0,0 +1,97 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskDelegateForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskDelegateForm.vue new file mode 100644 index 00000000..dc757a0c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskDelegateForm.vue @@ -0,0 +1,86 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue new file mode 100644 index 00000000..f93bf2c5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue @@ -0,0 +1,90 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue new file mode 100644 index 00000000..61f7d68c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue @@ -0,0 +1,85 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue new file mode 100644 index 00000000..6adf1de8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue @@ -0,0 +1,83 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/index.vue new file mode 100644 index 00000000..ba6c1298 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/detail/index.vue @@ -0,0 +1,312 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/index.vue new file mode 100644 index 00000000..a9dd46e9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/processInstance/index.vue @@ -0,0 +1,250 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/task/done/TaskDetail.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/task/done/TaskDetail.vue new file mode 100644 index 00000000..5bc06f19 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/task/done/TaskDetail.vue @@ -0,0 +1,51 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/task/done/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/task/done/index.vue new file mode 100644 index 00000000..d2033d5d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/task/done/index.vue @@ -0,0 +1,148 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/task/todo/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/task/todo/index.vue new file mode 100644 index 00000000..c8876887 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/task/todo/index.vue @@ -0,0 +1,137 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue new file mode 100644 index 00000000..9b215e0f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue @@ -0,0 +1,250 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/bpm/taskAssignRule/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/bpm/taskAssignRule/index.vue new file mode 100644 index 00000000..0fe9bde6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/bpm/taskAssignRule/index.vue @@ -0,0 +1,136 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/business/BusinessForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/business/BusinessForm.vue new file mode 100644 index 00000000..53f5cb8d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/business/BusinessForm.vue @@ -0,0 +1,279 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/business/components/BusinessList.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/business/components/BusinessList.vue new file mode 100644 index 00000000..31411e84 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/business/components/BusinessList.vue @@ -0,0 +1,107 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/business/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/business/index.vue new file mode 100644 index 00000000..c1c63fa1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/business/index.vue @@ -0,0 +1,207 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/businessStatusType/BusinessStatusTypeForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/businessStatusType/BusinessStatusTypeForm.vue new file mode 100644 index 00000000..edf41966 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/businessStatusType/BusinessStatusTypeForm.vue @@ -0,0 +1,167 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/businessStatusType/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/businessStatusType/index.vue new file mode 100644 index 00000000..3f7389be --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/businessStatusType/index.vue @@ -0,0 +1,171 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/clue/ClueForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/clue/ClueForm.vue new file mode 100644 index 00000000..1b2637c9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/clue/ClueForm.vue @@ -0,0 +1,151 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/clue/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/clue/index.vue new file mode 100644 index 00000000..cf742276 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/clue/index.vue @@ -0,0 +1,220 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue new file mode 100644 index 00000000..da37e556 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue @@ -0,0 +1,224 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerLimitConfig/CustomerLimitConfigList.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerLimitConfig/CustomerLimitConfigList.vue new file mode 100644 index 00000000..d0181421 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerLimitConfig/CustomerLimitConfigList.vue @@ -0,0 +1,154 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerLimitConfig/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerLimitConfig/index.vue new file mode 100644 index 00000000..d385f46b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerLimitConfig/index.vue @@ -0,0 +1,19 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerPoolConfig/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerPoolConfig/index.vue new file mode 100644 index 00000000..c7db3301 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/config/customerPoolConfig/index.vue @@ -0,0 +1,133 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/ContactForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/ContactForm.vue new file mode 100644 index 00000000..055cf5be --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/ContactForm.vue @@ -0,0 +1,304 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/OwerSelect.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/OwerSelect.vue new file mode 100644 index 00000000..5ed0fe50 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/OwerSelect.vue @@ -0,0 +1,72 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/components/ContactList.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/components/ContactList.vue new file mode 100644 index 00000000..b41c5456 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/components/ContactList.vue @@ -0,0 +1,112 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/detail/ContactBasicInfo.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/detail/ContactBasicInfo.vue new file mode 100644 index 00000000..12583bb5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/detail/ContactBasicInfo.vue @@ -0,0 +1,24 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/detail/ContactDetails.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/detail/ContactDetails.vue new file mode 100644 index 00000000..6b31a30c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/detail/ContactDetails.vue @@ -0,0 +1,77 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/detail/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/detail/index.vue new file mode 100644 index 00000000..6a6e71bf --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/detail/index.vue @@ -0,0 +1,134 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/index.vue new file mode 100644 index 00000000..70ba4b8c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contact/index.vue @@ -0,0 +1,321 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contract/ContractForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contract/ContractForm.vue new file mode 100644 index 00000000..5d5578f9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contract/ContractForm.vue @@ -0,0 +1,228 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contract/components/ContractList.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contract/components/ContractList.vue new file mode 100644 index 00000000..8a45ea7b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contract/components/ContractList.vue @@ -0,0 +1,132 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/contract/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/contract/index.vue new file mode 100644 index 00000000..26ff403a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/contract/index.vue @@ -0,0 +1,223 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/CustomerForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/CustomerForm.vue new file mode 100644 index 00000000..99f7e858 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/CustomerForm.vue @@ -0,0 +1,266 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/detail/CustomerDetailsHeader.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/detail/CustomerDetailsHeader.vue new file mode 100644 index 00000000..dd4f7f25 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/detail/CustomerDetailsHeader.vue @@ -0,0 +1,57 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/detail/CustomerDetailsInfo.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/detail/CustomerDetailsInfo.vue new file mode 100644 index 00000000..20bfd5b8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/detail/CustomerDetailsInfo.vue @@ -0,0 +1,74 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/detail/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/detail/index.vue new file mode 100644 index 00000000..f21a6ac3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/detail/index.vue @@ -0,0 +1,70 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/index.vue new file mode 100644 index 00000000..edad31b1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/customer/index.vue @@ -0,0 +1,287 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/permission/components/PermissionForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/permission/components/PermissionForm.vue new file mode 100644 index 00000000..5af376cf --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/permission/components/PermissionForm.vue @@ -0,0 +1,122 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/permission/components/PermissionList.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/permission/components/PermissionList.vue new file mode 100644 index 00000000..a05405e6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/permission/components/PermissionList.vue @@ -0,0 +1,140 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/product/ProductDetail.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/ProductDetail.vue new file mode 100644 index 00000000..2bce1e7f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/ProductDetail.vue @@ -0,0 +1,70 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/product/ProductForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/ProductForm.vue new file mode 100644 index 00000000..4974bb80 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/ProductForm.vue @@ -0,0 +1,209 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/product/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/index.vue new file mode 100644 index 00000000..ddbe1fc9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/index.vue @@ -0,0 +1,240 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/product/productCategory/ProductCategoryForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/productCategory/ProductCategoryForm.vue new file mode 100644 index 00000000..7e813769 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/productCategory/ProductCategoryForm.vue @@ -0,0 +1,110 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/product/productCategory/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/productCategory/index.vue new file mode 100644 index 00000000..7a011123 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/product/productCategory/index.vue @@ -0,0 +1,138 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/ReceivableForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/ReceivableForm.vue new file mode 100644 index 00000000..60206bf7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/ReceivableForm.vue @@ -0,0 +1,197 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/components/ReceivableList.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/components/ReceivableList.vue new file mode 100644 index 00000000..5f7881f2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/components/ReceivableList.vue @@ -0,0 +1,125 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/index.vue new file mode 100644 index 00000000..c5478c2f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/index.vue @@ -0,0 +1,220 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/plan/ReceivablePlanForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/plan/ReceivablePlanForm.vue new file mode 100644 index 00000000..29897b15 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/plan/ReceivablePlanForm.vue @@ -0,0 +1,190 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/plan/components/ReceivablePlanList.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/plan/components/ReceivablePlanList.vue new file mode 100644 index 00000000..dc150cbc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/plan/components/ReceivablePlanList.vue @@ -0,0 +1,128 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/plan/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/plan/index.vue new file mode 100644 index 00000000..decd79bc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/crm/receivable/plan/index.vue @@ -0,0 +1,226 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue new file mode 100644 index 00000000..0fd5aec3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue @@ -0,0 +1,67 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/apiAccessLog/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/apiAccessLog/index.vue new file mode 100644 index 00000000..b196da11 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/apiAccessLog/index.vue @@ -0,0 +1,219 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue new file mode 100644 index 00000000..41153a28 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue @@ -0,0 +1,81 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/apiErrorLog/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/apiErrorLog/index.vue new file mode 100644 index 00000000..ca145a76 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/apiErrorLog/index.vue @@ -0,0 +1,252 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/build/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/build/index.vue new file mode 100644 index 00000000..11bfc999 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/build/index.vue @@ -0,0 +1,143 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/EditTable.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/EditTable.vue new file mode 100644 index 00000000..f8473e36 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/EditTable.vue @@ -0,0 +1,87 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/ImportTable.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/ImportTable.vue new file mode 100644 index 00000000..6cd4610a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/ImportTable.vue @@ -0,0 +1,151 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/PreviewCode.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/PreviewCode.vue new file mode 100644 index 00000000..b6a307d2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/PreviewCode.vue @@ -0,0 +1,222 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/BasicInfoForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/BasicInfoForm.vue new file mode 100644 index 00000000..18593004 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/BasicInfoForm.vue @@ -0,0 +1,87 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/ColumInfoForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/ColumInfoForm.vue new file mode 100644 index 00000000..737c2e2b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/ColumInfoForm.vue @@ -0,0 +1,153 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/GenerateInfoForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/GenerateInfoForm.vue new file mode 100644 index 00000000..d2a01cc0 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/GenerateInfoForm.vue @@ -0,0 +1,385 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/index.ts new file mode 100644 index 00000000..1634a76f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/components/index.ts @@ -0,0 +1,4 @@ +import BasicInfoForm from './BasicInfoForm.vue' +import ColumInfoForm from './ColumInfoForm.vue' +import GenerateInfoForm from './GenerateInfoForm.vue' +export { BasicInfoForm, ColumInfoForm, GenerateInfoForm } diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/index.vue new file mode 100644 index 00000000..69c3d125 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/codegen/index.vue @@ -0,0 +1,258 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/config/ConfigForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/config/ConfigForm.vue new file mode 100644 index 00000000..f61face0 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/config/ConfigForm.vue @@ -0,0 +1,131 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/config/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/config/index.vue new file mode 100644 index 00000000..c7838c27 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/config/index.vue @@ -0,0 +1,228 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/dataSourceConfig/DataSourceConfigForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/dataSourceConfig/DataSourceConfigForm.vue new file mode 100644 index 00000000..e2a4eaab --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/dataSourceConfig/DataSourceConfigForm.vue @@ -0,0 +1,111 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/dataSourceConfig/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/dataSourceConfig/index.vue new file mode 100644 index 00000000..92bd3013 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/dataSourceConfig/index.vue @@ -0,0 +1,106 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/infra/dbDoc/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/infra/dbDoc/index.vue new file mode 100644 index 00000000..fa9364b0 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/infra/dbDoc/index.vue @@ -0,0 +1,59 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/delivery/pickUpStore/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/delivery/pickUpStore/index.vue new file mode 100644 index 00000000..6ffb3c42 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/delivery/pickUpStore/index.vue @@ -0,0 +1,188 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/components/OrderTableColumn.vue b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/components/OrderTableColumn.vue new file mode 100644 index 00000000..5d1e25ee --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/components/OrderTableColumn.vue @@ -0,0 +1,263 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/components/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/components/index.ts new file mode 100644 index 00000000..9cce9fac --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/components/index.ts @@ -0,0 +1,3 @@ +import OrderTableColumn from './OrderTableColumn.vue' + +export { OrderTableColumn } diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/detail/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/detail/index.vue new file mode 100644 index 00000000..67e54767 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/detail/index.vue @@ -0,0 +1,426 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderDeliveryForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderDeliveryForm.vue new file mode 100644 index 00000000..2411f1ce --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderDeliveryForm.vue @@ -0,0 +1,99 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderPickUpForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderPickUpForm.vue new file mode 100644 index 00000000..529263c4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderPickUpForm.vue @@ -0,0 +1,108 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderUpdateAddressForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderUpdateAddressForm.vue new file mode 100644 index 00000000..9c857064 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderUpdateAddressForm.vue @@ -0,0 +1,98 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderUpdatePriceForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderUpdatePriceForm.vue new file mode 100644 index 00000000..eb932ffa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderUpdatePriceForm.vue @@ -0,0 +1,92 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue new file mode 100644 index 00000000..8be1b603 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue @@ -0,0 +1,70 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/index.vue new file mode 100644 index 00000000..c892292a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mall/trade/order/index.vue @@ -0,0 +1,354 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/config/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/config/index.vue new file mode 100644 index 00000000..38196690 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/config/index.vue @@ -0,0 +1,119 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/group/GroupForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/group/GroupForm.vue new file mode 100644 index 00000000..14510b0f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/group/GroupForm.vue @@ -0,0 +1,112 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/group/components/MemberGroupSelect.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/group/components/MemberGroupSelect.vue new file mode 100644 index 00000000..78a993a8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/group/components/MemberGroupSelect.vue @@ -0,0 +1,45 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/group/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/group/index.vue new file mode 100644 index 00000000..552a72a6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/group/index.vue @@ -0,0 +1,174 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/level/LevelForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/level/LevelForm.vue new file mode 100644 index 00000000..7e6873cc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/level/LevelForm.vue @@ -0,0 +1,175 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/level/components/MemberLevelSelect.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/level/components/MemberLevelSelect.vue new file mode 100644 index 00000000..2a603e69 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/level/components/MemberLevelSelect.vue @@ -0,0 +1,45 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/level/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/level/index.vue new file mode 100644 index 00000000..1347b7ef --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/level/index.vue @@ -0,0 +1,169 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/point/record/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/point/record/index.vue new file mode 100644 index 00000000..dc8a35ec --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/point/record/index.vue @@ -0,0 +1,159 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/signin/config/SignInConfigForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/signin/config/SignInConfigForm.vue new file mode 100644 index 00000000..616fd8fc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/signin/config/SignInConfigForm.vue @@ -0,0 +1,132 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/signin/config/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/signin/config/index.vue new file mode 100644 index 00000000..da38dad6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/signin/config/index.vue @@ -0,0 +1,104 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/signin/record/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/signin/record/index.vue new file mode 100644 index 00000000..754663e5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/signin/record/index.vue @@ -0,0 +1,132 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/tag/TagForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/tag/TagForm.vue new file mode 100644 index 00000000..d45ea589 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/tag/TagForm.vue @@ -0,0 +1,91 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/tag/components/MemberTagSelect.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/tag/components/MemberTagSelect.vue new file mode 100644 index 00000000..ebff61ea --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/tag/components/MemberTagSelect.vue @@ -0,0 +1,68 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/tag/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/tag/index.vue new file mode 100644 index 00000000..05a886a7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/tag/index.vue @@ -0,0 +1,153 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/UserForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/UserForm.vue new file mode 100644 index 00000000..0da4ef61 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/UserForm.vue @@ -0,0 +1,179 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/UserLevelUpdateForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/UserLevelUpdateForm.vue new file mode 100644 index 00000000..e583f4a9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/UserLevelUpdateForm.vue @@ -0,0 +1,101 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/UserPointUpdateForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/UserPointUpdateForm.vue new file mode 100644 index 00000000..967ebe03 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/UserPointUpdateForm.vue @@ -0,0 +1,128 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/components/balance-list.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/components/balance-list.vue new file mode 100644 index 00000000..3e9d1785 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/components/balance-list.vue @@ -0,0 +1,14 @@ + + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserAccountInfo.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserAccountInfo.vue new file mode 100644 index 00000000..56a6ab63 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserAccountInfo.vue @@ -0,0 +1,87 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserAddressList.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserAddressList.vue new file mode 100644 index 00000000..a37caba1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserAddressList.vue @@ -0,0 +1,54 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserBasicInfo.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserBasicInfo.vue new file mode 100644 index 00000000..075450e9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserBasicInfo.vue @@ -0,0 +1,85 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserBrokerageList.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserBrokerageList.vue new file mode 100644 index 00000000..db88787b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserBrokerageList.vue @@ -0,0 +1,125 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserCouponList.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserCouponList.vue new file mode 100644 index 00000000..2279b8aa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserCouponList.vue @@ -0,0 +1,190 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserExperienceRecordList.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserExperienceRecordList.vue new file mode 100644 index 00000000..64414ad1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserExperienceRecordList.vue @@ -0,0 +1,158 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserFavoriteList.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserFavoriteList.vue new file mode 100644 index 00000000..afab9a08 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserFavoriteList.vue @@ -0,0 +1,96 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserOrderList.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserOrderList.vue new file mode 100644 index 00000000..b6870bc3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserOrderList.vue @@ -0,0 +1,279 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserPointList.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserPointList.vue new file mode 100644 index 00000000..9754b297 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserPointList.vue @@ -0,0 +1,152 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserSignList.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserSignList.vue new file mode 100644 index 00000000..c8972741 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/UserSignList.vue @@ -0,0 +1,135 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/index.vue new file mode 100644 index 00000000..6237cca6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/detail/index.vue @@ -0,0 +1,135 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/member/user/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/member/user/index.vue new file mode 100644 index 00000000..de52c6a3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/member/user/index.vue @@ -0,0 +1,311 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/account/AccountForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/account/AccountForm.vue new file mode 100644 index 00000000..c721013c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/account/AccountForm.vue @@ -0,0 +1,160 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/account/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/account/index.vue new file mode 100644 index 00000000..212035a2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/account/index.vue @@ -0,0 +1,195 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/components/ReplyForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/components/ReplyForm.vue new file mode 100644 index 00000000..1c9dee49 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/components/ReplyForm.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/components/ReplyTable.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/components/ReplyTable.vue new file mode 100644 index 00000000..2abe9f24 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/components/ReplyTable.vue @@ -0,0 +1,115 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/components/types.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/components/types.ts new file mode 100644 index 00000000..68bc5c94 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/components/types.ts @@ -0,0 +1,7 @@ +// 消息类型(Follow: 关注时回复;Message: 消息回复;Keyword: 关键词回复) +// 作为 tab.name,enum 的数字不能随意修改,与 api 参数相关 +export enum MsgType { + Follow = 1, + Message = 2, + Keyword = 3 +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/index.vue new file mode 100644 index 00000000..0b006470 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/autoReply/index.vue @@ -0,0 +1,241 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-account-select/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-account-select/index.ts new file mode 100644 index 00000000..97556b2f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-account-select/index.ts @@ -0,0 +1,3 @@ +import WxAccountSelect from './main.vue' + +export default WxAccountSelect diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-account-select/main.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-account-select/main.vue new file mode 100644 index 00000000..2a6ca50f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-account-select/main.vue @@ -0,0 +1,47 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-location/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-location/index.ts new file mode 100644 index 00000000..14ba8644 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-location/index.ts @@ -0,0 +1,3 @@ +import WxLocation from './main.vue' + +export default WxLocation diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-location/main.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-location/main.vue new file mode 100644 index 00000000..80eada76 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-location/main.vue @@ -0,0 +1,73 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-material-select/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-material-select/index.ts new file mode 100644 index 00000000..eeda31d5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-material-select/index.ts @@ -0,0 +1,6 @@ +import WxMaterialSelect from './main.vue' +import { NewsType, MaterialType } from './types' + +export { NewsType, MaterialType } + +export default WxMaterialSelect diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-material-select/main.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-material-select/main.vue new file mode 100644 index 00000000..aad25ea8 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-material-select/main.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-material-select/types.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-material-select/types.ts new file mode 100644 index 00000000..d4add1d5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-material-select/types.ts @@ -0,0 +1,11 @@ +export enum NewsType { + Draft = '2', + Published = '1' +} + +export enum MaterialType { + Image = 'image', + Voice = 'voice', + Video = 'video', + News = 'news' +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/card.scss b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/card.scss new file mode 100644 index 00000000..7fbbe802 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/card.scss @@ -0,0 +1,116 @@ +.avue-card { + &__item { + margin-bottom: 16px; + border: 1px solid #e8e8e8; + background-color: #fff; + box-sizing: border-box; + color: rgba(0, 0, 0, 0.65); + font-size: 14px; + font-variant: tabular-nums; + line-height: 1.5; + list-style: none; + font-feature-settings: 'tnum'; + cursor: pointer; + height: 200px; + + &:hover { + border-color: rgba(0, 0, 0, 0.09); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09); + } + + &--add { + border: 1px dashed #000; + width: 100%; + color: rgba(0, 0, 0, 0.45); + background-color: #fff; + border-color: #d9d9d9; + border-radius: 2px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + + i { + margin-right: 10px; + } + + &:hover { + color: #40a9ff; + background-color: #fff; + border-color: #40a9ff; + } + } + } + + &__body { + display: flex; + padding: 24px; + } + + &__detail { + flex: 1; + } + + &__avatar { + width: 48px; + height: 48px; + border-radius: 48px; + overflow: hidden; + margin-right: 12px; + + img { + width: 100%; + height: 100%; + } + } + + &__title { + color: rgba(0, 0, 0, 0.85); + margin-bottom: 12px; + font-size: 16px; + + &:hover { + color: #1890ff; + } + } + + &__info { + color: rgba(0, 0, 0, 0.45); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + height: 64px; + } + + &__menu { + display: flex; + justify-content: space-around; + height: 50px; + background: #f7f9fa; + color: rgba(0, 0, 0, 0.45); + text-align: center; + line-height: 50px; + + &:hover { + color: #1890ff; + } + } +} + +/** joolun 额外加的 */ +.avue-comment__main { + flex: unset !important; + border-radius: 5px !important; + margin: 0 8px !important; +} + +.avue-comment__header { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.avue-comment__body { + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/comment.scss b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/comment.scss new file mode 100644 index 00000000..7812c2a3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/comment.scss @@ -0,0 +1,126 @@ +/* 来自 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */ +.avue-comment { + margin-bottom: 30px; + display: flex; + align-items: flex-start; + + &--reverse { + flex-direction: row-reverse; + + .avue-comment__main { + &:before, + &:after { + left: auto; + right: -8px; + border-width: 8px 0 8px 8px; + } + + &:before { + border-left-color: #dedede; + } + + &:after { + border-left-color: #f8f8f8; + margin-right: 1px; + margin-left: auto; + } + } + } + + &__avatar { + width: 48px; + height: 48px; + border-radius: 50%; + border: 1px solid transparent; + box-sizing: border-box; + vertical-align: middle; + } + + &__header { + padding: 5px 15px; + background: #f8f8f8; + border-bottom: 1px solid #eee; + display: flex; + align-items: center; + justify-content: space-between; + } + + &__author { + font-weight: 700; + font-size: 14px; + color: #999; + } + + &__main { + flex: 1; + margin: 0 20px; + position: relative; + border: 1px solid #dedede; + border-radius: 2px; + + &:before, + &:after { + position: absolute; + top: 10px; + left: -8px; + right: 100%; + width: 0; + height: 0; + display: block; + content: ' '; + border-color: transparent; + border-style: solid solid outset; + border-width: 8px 8px 8px 0; + pointer-events: none; + } + + &:before { + border-right-color: #dedede; + z-index: 1; + } + + &:after { + border-right-color: #f8f8f8; + margin-left: 1px; + z-index: 2; + } + } + + &__body { + padding: 15px; + overflow: hidden; + background: #fff; + font-family: + Segoe UI, + Lucida Grande, + Helvetica, + Arial, + Microsoft YaHei, + FreeSans, + Arimo, + Droid Sans, + wenquanyi micro hei, + Hiragino Sans GB, + Hiragino Sans GB W3, + FontAwesome, + sans-serif; + color: #333; + font-size: 14px; + } + + blockquote { + margin: 0; + font-family: + Georgia, + Times New Roman, + Times, + Kai, + Kaiti SC, + KaiTi, + BiauKai, + FontAwesome, + serif; + padding: 1px 0 1px 15px; + border-left: 4px solid #ddd; + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/components/Msg.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/components/Msg.vue new file mode 100644 index 00000000..c35e268e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/components/Msg.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/components/MsgEvent.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/components/MsgEvent.vue new file mode 100644 index 00000000..77beda48 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/components/MsgEvent.vue @@ -0,0 +1,49 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/components/MsgList.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/components/MsgList.vue new file mode 100644 index 00000000..ce7063b2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/components/MsgList.vue @@ -0,0 +1,62 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/index.ts new file mode 100644 index 00000000..fd9eddd7 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/index.ts @@ -0,0 +1,6 @@ +import WxMsg from './main.vue' +import { MsgType } from './types' + +export { MsgType } + +export default WxMsg diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/main.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/main.vue new file mode 100644 index 00000000..8b7cc3a2 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/main.vue @@ -0,0 +1,192 @@ + + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/types.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/types.ts new file mode 100644 index 00000000..38a0ff86 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-msg/types.ts @@ -0,0 +1,17 @@ +export enum MsgType { + Event = 'event', + Text = 'text', + Voice = 'voice', + Image = 'image', + Video = 'video', + Link = 'link', + Location = 'location', + Music = 'music', + News = 'news' +} + +export interface User { + nickname: string + avatar: string + accountId: number +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-music/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-music/index.ts new file mode 100644 index 00000000..c4211261 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-music/index.ts @@ -0,0 +1,3 @@ +import WxMusic from './main.vue' + +export default WxMusic diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-music/main.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-music/main.vue new file mode 100644 index 00000000..6b44f449 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-music/main.vue @@ -0,0 +1,62 @@ + + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-news/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-news/index.ts new file mode 100644 index 00000000..e68f4d5d --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-news/index.ts @@ -0,0 +1,3 @@ +import WxNews from './main.vue' + +export default WxNews diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-news/main.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-news/main.vue new file mode 100644 index 00000000..154291b3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-news/main.vue @@ -0,0 +1,119 @@ + + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabImage.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabImage.vue new file mode 100644 index 00000000..a2915779 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabImage.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabMusic.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabMusic.vue new file mode 100644 index 00000000..c7caecb9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabMusic.vue @@ -0,0 +1,116 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabNews.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabNews.vue new file mode 100644 index 00000000..565b1fba --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabNews.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabText.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabText.vue new file mode 100644 index 00000000..307e48f4 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabText.vue @@ -0,0 +1,22 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabVideo.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabVideo.vue new file mode 100644 index 00000000..7d67d2fa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabVideo.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabVoice.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabVoice.vue new file mode 100644 index 00000000..5a7a42d5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/TabVoice.vue @@ -0,0 +1,160 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/types.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/types.ts new file mode 100644 index 00000000..3e07d6e5 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/components/types.ts @@ -0,0 +1,54 @@ +enum ReplyType { + News = 'news', + Image = 'image', + Voice = 'voice', + Video = 'video', + Music = 'music', + Text = 'text' +} + +interface _Reply { + accountId: number + type: ReplyType + name?: string | null + content?: string | null + mediaId?: string | null + url?: string | null + title?: string | null + description?: string | null + thumbMediaId?: string | null + thumbMediaUrl?: string | null + musicUrl?: string | null + hqMusicUrl?: string | null + introduction?: string | null + articles?: any[] +} + +type Reply = _Reply //Partial<_Reply> + +enum NewsType { + Published = '1', + Draft = '2' +} + +/** 利用旧的reply[accountId, type]初始化新的Reply */ +const createEmptyReply = (old: Reply | Ref): Reply => { + return { + accountId: unref(old).accountId, + type: unref(old).type, + name: null, + content: null, + mediaId: null, + url: null, + title: null, + description: null, + thumbMediaId: null, + thumbMediaUrl: null, + musicUrl: null, + hqMusicUrl: null, + introduction: null, + articles: [] + } +} + +export { Reply, NewsType, ReplyType, createEmptyReply } diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/index.ts new file mode 100644 index 00000000..d1da217e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/index.ts @@ -0,0 +1,7 @@ +import { Reply, NewsType, ReplyType, createEmptyReply } from './components/types' + +import WxReplySelect from './main.vue' + +export type { Reply } +export { createEmptyReply, NewsType, ReplyType } +export default WxReplySelect diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/main.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/main.vue new file mode 100644 index 00000000..2c9d5f21 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-reply/main.vue @@ -0,0 +1,208 @@ + + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-video-play/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-video-play/index.ts new file mode 100644 index 00000000..91e00efa --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-video-play/index.ts @@ -0,0 +1,3 @@ +import WxVideoPlayer from './main.vue' + +export default WxVideoPlayer diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-video-play/main.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-video-play/main.vue new file mode 100644 index 00000000..d544bbea --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-video-play/main.vue @@ -0,0 +1,73 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-voice-play/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-voice-play/index.ts new file mode 100644 index 00000000..9eb78e02 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-voice-play/index.ts @@ -0,0 +1,3 @@ +import WxVoicePlayer from './main.vue' + +export default WxVoicePlayer diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-voice-play/main.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-voice-play/main.vue new file mode 100644 index 00000000..fe7f0cab --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/components/wx-voice-play/main.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/CoverSelect.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/CoverSelect.vue new file mode 100644 index 00000000..499f1a64 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/CoverSelect.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/DraftTable.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/DraftTable.vue new file mode 100644 index 00000000..bb512d88 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/DraftTable.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/NewsForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/NewsForm.vue new file mode 100644 index 00000000..9b1e4745 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/NewsForm.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/index.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/index.ts new file mode 100644 index 00000000..51e843d3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/index.ts @@ -0,0 +1,7 @@ +import type { Article, NewsItem, NewsItemList } from './types' +import { createEmptyNewsItem } from './types' +import DraftTable from './DraftTable.vue' +import NewsForm from './NewsForm.vue' + +export { DraftTable, NewsForm, createEmptyNewsItem } +export type { Article, NewsItem, NewsItemList } diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/types.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/types.ts new file mode 100644 index 00000000..a8cf00c3 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/components/types.ts @@ -0,0 +1,40 @@ +interface NewsItem { + title: string + thumbMediaId: string + author: string + digest: string + showCoverPic: string + content: string + contentSourceUrl: string + needOpenComment: string + onlyFansCanComment: string + thumbUrl: string +} + +interface NewsItemList { + newsItem: NewsItem[] +} + +interface Article { + mediaId: string + content: NewsItemList + updateTime: number +} + +const createEmptyNewsItem = (): NewsItem => { + return { + title: '', + thumbMediaId: '', + author: '', + digest: '', + showCoverPic: '', + content: '', + contentSourceUrl: '', + needOpenComment: '', + onlyFansCanComment: '', + thumbUrl: '' + } +} + +export type { Article, NewsItem, NewsItemList } +export { createEmptyNewsItem } diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/editor-config.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/editor-config.ts new file mode 100644 index 00000000..ee3b95ec --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/editor-config.ts @@ -0,0 +1,75 @@ +import { IEditorConfig } from '@wangeditor/editor' +import { getAccessToken, getTenantId } from '@/utils/auth' + +const message = useMessage() + +type InsertFnType = (url: string, alt: string, href: string) => void + +export const createEditorConfig = ( + server: string, + accountId: number | undefined +): Partial => { + return { + MENU_CONF: { + ['uploadImage']: { + server, + // 单个文件的最大体积限制,默认为 2M + maxFileSize: 5 * 1024 * 1024, + // 最多可上传几个文件,默认为 100 + maxNumberOfFiles: 10, + // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 [] + allowedFileTypes: ['image/*'], + + // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。 + meta: { + accountId: accountId, + type: 'image' + }, + // 将 meta 拼接到 url 参数中,默认 false + metaWithUrl: true, + + // 自定义增加 http header + headers: { + Accept: '*', + Authorization: 'Bearer ' + getAccessToken(), + 'tenant-id': getTenantId() + }, + + // 跨域是否传递 cookie ,默认为 false + withCredentials: true, + + // 超时时间,默认为 10 秒 + timeout: 5 * 1000, // 5 秒 + + // form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image + fieldName: 'file', + + // 上传之前触发 + onBeforeUpload(file: File) { + console.log(file) + return file + }, + // 上传进度的回调函数 + onProgress(progress: number) { + // progress 是 0-100 的数字 + console.log('progress', progress) + }, + onSuccess(file: File, res: any) { + console.log('onSuccess', file, res) + }, + onFailed(file: File, res: any) { + message.alertError(res.message) + console.log('onFailed', file, res) + }, + onError(file: File, err: any, res: any) { + message.alertError(err.message) + console.error('onError', file, err, res) + }, + // 自定义插入图片 + customInsert(res: any, insertFn: InsertFnType) { + insertFn(res.data.url, 'image', res.data.url) + } + } + } + } +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/index.vue new file mode 100644 index 00000000..db24596a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/index.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/mock.js b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/mock.js new file mode 100644 index 00000000..aac10d22 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/draft/mock.js @@ -0,0 +1,151 @@ +export default { + list: [ + { + mediaId: 'r6ryvl6LrxBU0miaST4Y-q-G9pdsmZw0OYG4FzHQkKfpLfEwIH51wy2bxisx8PvW', + content: { + newsItem: [ + { + title: '我是标题(OOO)', + author: '我是作者', + digest: '我是摘要', + content: '我是内容', + contentSourceUrl: 'https://www.iocoder.cn', + thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn', + showCoverPic: 0, + needOpenComment: 0, + onlyFansCanComment: 0, + url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9XaFphcmtJVFh3VEc4Q1MxQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN2QxTE56SFBCYXc2RE9NcUxIeS1CQjJuUHhTWjBlN2VOeGRpRi1fZUhwN1FNQjdrQV9yRU9EU0hibHREZmZoVW5acnZrN3ZjaWsxejR3RGpKczBzTHFIM0dFNFZWVkpBc0dWWlAzUEhlVmpnfn4%3D&chksm=1f6354802814dd969ef83c0f3babe555c614270b30bc383beaf7ffd13b0257f0fe5ced9af694#rd', + thumbUrl: + 'http://test.mes.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png' + }, + { + title: '我是标题(XXX)', + author: '我是作者', + digest: '我是摘要', + content: '我是内容', + contentSourceUrl: 'https://www.iocoder.cn', + thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn', + showCoverPic: 0, + needOpenComment: 0, + onlyFansCanComment: 0, + url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9yTlYwOEs1clpwcE5OUEhCQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN0NSMjFqN3N1aUZMbFNVLTZHN2ZDME9qOGp2THk2RFNlSTlKZ3Y1czFVZDdQQm5IeUg3dEppSUtpQUh5SExOOTRkT3dHNUdBdHdWSWlOendlREV3dS1jUEVQbFpiVTZmVW5iRWhZcGdkNTFRfn4%3D&chksm=1f6354802814dd96a403151cd44c7da4eecf0e475d25423e46ecd795b513bafd829a75daef9b#rd', + thumbUrl: + 'http://test.mes.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png' + } + ] + }, + updateTime: 1673655730 + }, + { + mediaId: 'r6ryvl6LrxBU0miaST4Y-jGpXnO73ihN0lsNXknCRQHapp2xgHMRxHKG50LituFe', + content: { + newsItem: [ + { + title: '我是标题(修改)', + author: '我是作者', + digest: '我是摘要', + content: '我是内容', + contentSourceUrl: 'https://www.iocoder.cn', + thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn', + showCoverPic: 0, + needOpenComment: 0, + onlyFansCanComment: 0, + url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl95WVFXYndIZnZJd0t5cjgvQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN1dlNURPbWswbEF4RDd5dVJTdjQ4cm9Cc0Q1TWhpMUh6SE1hVEE3ZHljaHhlZjZYSGF5N2JNSHpDTlh6ajNZbkpGTGpTcUQ4M3NMdW41ZUpXNFZZQ1VKbVlaMVp5ekxEV1czREdsY1dOYTZnfn4%3D&chksm=1f6354be2814dda8e6238037c2ebd52b1c8e80e93249a861ad80e4d40e5ca7207233475ca689#rd', + thumbUrl: + 'http://test.mes.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png' + } + ] + }, + updateTime: 1673655584 + }, + { + mediaId: 'r6ryvl6LrxBU0miaST4Y-v5SrbNCPpD6M_p3TmSrYwTjKogs-0DMJgmjMyNZPeMO', + content: { + newsItem: [ + { + title: '1321', + author: '3232', + digest: '1333', + content: '

444

', + contentSourceUrl: 'http://www.iocoder.cn', + thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-tlQmcl3RdC-Jcgns6IQtf7zenGy3b86WLT7GzUcrb1T', + showCoverPic: 0, + needOpenComment: 0, + onlyFansCanComment: 0, + url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9jelJiaDAzbmdpSkJOZ2M2QWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNDNXVVc2ZDRYeTY0Zm1weXR6dE9vQWh1TzEwbEpUVnRfVzJyaGFDNXBkZ0ZXM2JFOTNaRHNhOHRUeFdEanhMeS01X01kMUNWQ1BpRER3cjYwTl9pMnpFLUJhZXFucVVfM1pDUXlTUEl1S25nfn4%3D&chksm=1f6354bc2814ddaa56a90ad5bc3d078601c8d1589ba01827a8170587bc830ff9747b5f59c3a0#rd', + thumbUrl: + 'http://mmbiz.qpic.cn/mmbiz_png/btUmCVHwbJUoicwBiacjVeQbu6QxgBVrukfSJXz509boa21SpH8OVHAqXCJiaiaAaHQJNxwwsa0gHRXVr0G5EZYamw/0?wx_fmt=png' + } + ] + }, + updateTime: 1673628969 + }, + { + mediaId: 'r6ryvl6LrxBU0miaST4Y-vdWrisK5EZbk4Y3tzh8P0PG0eEUbnQrh0BcsEb3WNP0', + content: { + newsItem: [ + { + title: 'tudou', + author: 'haha', + digest: '312', + content: '

132312

', + contentSourceUrl: 'http://www.iocoder.cn', + thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pgFtUNLu1foMSAMkoOsrQrTZ8EtTMssBLfTtzP0dfjG', + showCoverPic: 0, + needOpenComment: 0, + onlyFansCanComment: 0, + url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9qdkJ1ZjBoUmg2Uk9TS3RlQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNVg2aTJsaC1fMkU2eXNacUplN3VDTTZFZkhtMjhuTUZvWkxsNDBRSXExY2tiVXRHb09TaHgtREhzY3doZ0JYeC1TSTZ5eWZldXJsOWtfbV8yMi1aYkcyZ2pOY0haM0Ntb3VSWEtxUGVFRlNBfn4%3D&chksm=1f6354ba2814ddacf0184b24d310483641ef190b1faac098c285eb416c70017e2f54decfa1af#rd', + thumbUrl: + 'http://test.mes.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pgFtUNLu1foMSAMkoOsrQrTZ8EtTMssBLfTtzP0dfjG.png' + } + ] + }, + updateTime: 1673628760 + }, + { + mediaId: 'r6ryvl6LrxBU0miaST4Y-u9kTIm1DhWZDdXyxsxUVv2Z5DAB99IPxkIRTUUD206k', + content: { + newsItem: [ + { + title: '12', + author: '333', + digest: '123', + content: '123', + contentSourceUrl: 'https://www.iocoder.cn', + thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-jVixJGgnBnkBPRbuVptOW0CHYuQFyiOVNtamctS8xU8', + showCoverPic: 0, + needOpenComment: 0, + onlyFansCanComment: 0, + url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9qVVhpSDZUaFJWTzBBWWRVQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNWRnTDJWYmF2NER0clV1bThmQ0xUR3hqQnJkZ3BJSUNmNDJmc0lCZ1dadkVnZ3Z5bkN4YWtVUjhoaWZWYzZURUR4NnpMd0Y4Z3U5aUdib0lkMzI4Rjg3SG9JX2FycTMxbUctOHplaTlQVVhnfn4%3D&chksm=1f6354b62814dda076c778af33f06580165d8aa81f7798d55cfabb1886b5c74d9b2124a3535c#rd', + thumbUrl: + 'http://test.mes.iocoder.cn/r6ryvl6LrxBU0miaST4Y-jVixJGgnBnkBPRbuVptOW0CHYuQFyiOVNtamctS8xU8.jpg' + } + ] + }, + updateTime: 1673626494 + }, + { + mediaId: 'r6ryvl6LrxBU0miaST4Y-sO24upobaENDmeByfBTfaozB3aOqSMAV0lGy-UkHXE7', + content: { + newsItem: [ + { + title: '我是标题', + author: '我是作者', + digest: '我是摘要', + content: '我是内容', + contentSourceUrl: 'https://www.iocoder.cn', + thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn', + showCoverPic: 0, + needOpenComment: 0, + onlyFansCanComment: 0, + url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9LT2dqRnpMNUpsR0hjYWtBQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNGNmazZTdlE5WkxvU0tfX2V5cjV2WjJiR0xjQUhyREFSZWo2eWNrUW9EYVh6ZkpWRXBLR3FmTEV6YldBMno3Q2ZvVXBSdzlaVDc3aFhndEpQWUwzWmFMUWt0YVVURE1VZ1FsQTdPMlRtc3JBfn4%3D&chksm=1f6354aa2814ddbcc2637382f963a8742993ac38ebcebe6e3411df5ac82ac7bbdb391be6494a#rd', + thumbUrl: + 'http://test.mes.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png' + } + ] + }, + updateTime: 1673534279 + } + ], + total: 6 +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/freePublish/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/freePublish/index.vue new file mode 100644 index 00000000..2ed8ae77 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/freePublish/index.vue @@ -0,0 +1,336 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/hooks/useUpload.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/hooks/useUpload.ts new file mode 100644 index 00000000..b0e70531 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/hooks/useUpload.ts @@ -0,0 +1,50 @@ +import type { UploadRawFile } from 'element-plus' + +const message = useMessage() // 消息 + +enum UploadType { + Image = 'image', + Voice = 'voice', + Video = 'video' +} + +const useBeforeUpload = (type: UploadType, maxSizeMB: number) => { + const fn = (rawFile: UploadRawFile): boolean => { + let allowTypes: string[] = [] + let name = '' + + switch (type) { + case UploadType.Image: + allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg'] + maxSizeMB = 2 + name = '图片' + break + case UploadType.Voice: + allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr'] + maxSizeMB = 2 + name = '语音' + break + case UploadType.Video: + allowTypes = ['video/mp4'] + maxSizeMB = 10 + name = '视频' + break + } + // 格式不正确 + if (!allowTypes.includes(rawFile.type)) { + message.error(`上传${name}格式不对!`) + return false + } + // 大小不正确 + if (rawFile.size / 1024 / 1024 > maxSizeMB) { + message.error(`上传${name}大小不能超过${maxSizeMB}M!`) + return false + } + + return true + } + + return fn +} + +export { UploadType, useBeforeUpload } diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/ImageTable.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/ImageTable.vue new file mode 100644 index 00000000..52c608f6 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/ImageTable.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/UploadFile.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/UploadFile.vue new file mode 100644 index 00000000..276a798c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/UploadFile.vue @@ -0,0 +1,77 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/UploadVideo.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/UploadVideo.vue new file mode 100644 index 00000000..0eda1cef --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/UploadVideo.vue @@ -0,0 +1,129 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/VideoTable.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/VideoTable.vue new file mode 100644 index 00000000..cbaa9024 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/VideoTable.vue @@ -0,0 +1,59 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/VoiceTable.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/VoiceTable.vue new file mode 100644 index 00000000..76fab7af --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/VoiceTable.vue @@ -0,0 +1,51 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/upload.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/upload.ts new file mode 100644 index 00000000..e732fe70 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/components/upload.ts @@ -0,0 +1,32 @@ +import type { UploadProps, UploadRawFile } from 'element-plus' +import { getAccessToken } from '@/utils/auth' +import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload' + +const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 请求头 +const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 上传地址 + +interface UploadData { + type: UploadType + title: string + introduction: string + accountId: number +} + +const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => + useBeforeUpload(UploadType.Image, 2)(rawFile) + +const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => + useBeforeUpload(UploadType.Voice, 2)(rawFile) + +const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => + useBeforeUpload(UploadType.Video, 10)(rawFile) + +export { + HEADERS, + UPLOAD_URL, + UploadType, + UploadData, + beforeImageUpload, + beforeVoiceUpload, + beforeVideoUpload +} diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/material/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/index.vue new file mode 100644 index 00000000..de060429 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/material/index.vue @@ -0,0 +1,159 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/assets/iphone_backImg.png b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/assets/iphone_backImg.png new file mode 100644 index 00000000..bb09591a Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/assets/iphone_backImg.png differ diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/assets/menu_foot.png b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/assets/menu_foot.png new file mode 100644 index 00000000..4a89d4bd Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/assets/menu_foot.png differ diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/assets/menu_head.png b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/assets/menu_head.png new file mode 100644 index 00000000..248cfb76 Binary files /dev/null and b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/assets/menu_head.png differ diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/MenuEditor.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/MenuEditor.vue new file mode 100644 index 00000000..5df1785c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/MenuEditor.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/MenuPreviewer.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/MenuPreviewer.vue new file mode 100644 index 00000000..93a19800 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/MenuPreviewer.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/menuOptions.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/menuOptions.ts new file mode 100644 index 00000000..d86dd789 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/menuOptions.ts @@ -0,0 +1,42 @@ +export default [ + { + value: 'view', + label: '跳转网页' + }, + { + value: 'miniprogram', + label: '跳转小程序' + }, + { + value: 'click', + label: '点击回复' + }, + { + value: 'article_view_limited', + label: '跳转图文消息' + }, + { + value: 'scancode_push', + label: '扫码直接返回结果' + }, + { + value: 'scancode_waitmsg', + label: '扫码回复' + }, + { + value: 'pic_sysphoto', + label: '系统拍照发图' + }, + { + value: 'pic_photo_or_album', + label: '拍照或者相册' + }, + { + value: 'pic_weixin', + label: '微信相册' + }, + { + value: 'location_select', + label: '选择地理位置' + } +] diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/types.ts b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/types.ts new file mode 100644 index 00000000..b9f76597 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/components/types.ts @@ -0,0 +1,73 @@ +export interface Replay { + title: string + description: string + picUrl: string + url: string +} + +export type MenuType = + | '' + | 'click' + | 'view' + | 'scancode_waitmsg' + | 'scancode_push' + | 'pic_sysphoto' + | 'pic_photo_or_album' + | 'pic_weixin' + | 'location_select' + | 'article_view_limited' + +interface _RawMenu { + // db + id: number + parentId: number + accountId: number + appId: string + createTime: number + + // mp-native + name: string + menuKey: string + type: MenuType + url: string + miniProgramAppId: string + miniProgramPagePath: string + articleId: string + replyMessageType: string + replyContent: string + replyMediaId: string + replyMediaUrl: string + replyThumbMediaId: string + replyThumbMediaUrl: string + replyTitle: string + replyDescription: string + replyArticles: Replay + replyMusicUrl: string + replyHqMusicUrl: string +} + +export type RawMenu = Partial<_RawMenu> + +interface _Reply { + type: string + accountId: number + content: string + mediaId: string + url: string + thumbMediaId: string + thumbMediaUrl: string + title: string + description: string + articles: null | Replay[] + musicUrl: string + hqMusicUrl: string +} + +export type Reply = Partial<_Reply> + +interface _Menu extends RawMenu { + children: _Menu[] + reply: Reply +} + +export type Menu = Partial<_Menu> diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/index.vue new file mode 100644 index 00000000..8cc8f586 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/menu/index.vue @@ -0,0 +1,401 @@ + + + + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/message/MessageTable.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/message/MessageTable.vue new file mode 100644 index 00000000..ebc3d749 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/message/MessageTable.vue @@ -0,0 +1,145 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/message/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/message/index.vue new file mode 100644 index 00000000..adceec56 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/message/index.vue @@ -0,0 +1,152 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/statistics/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/statistics/index.vue new file mode 100644 index 00000000..37ca2a00 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/statistics/index.vue @@ -0,0 +1,368 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/tag/TagForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/tag/TagForm.vue new file mode 100644 index 00000000..9a85bec9 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/tag/TagForm.vue @@ -0,0 +1,98 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/tag/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/tag/index.vue new file mode 100644 index 00000000..df76ce98 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/tag/index.vue @@ -0,0 +1,154 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/user/UserForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/user/UserForm.vue new file mode 100644 index 00000000..818fdd83 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/user/UserForm.vue @@ -0,0 +1,102 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/mp/user/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/mp/user/index.vue new file mode 100644 index 00000000..6147351a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/mp/user/index.vue @@ -0,0 +1,181 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/AppForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/AppForm.vue new file mode 100644 index 00000000..b99766c1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/AppForm.vue @@ -0,0 +1,130 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/channel/AlipayChannelForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/channel/AlipayChannelForm.vue new file mode 100644 index 00000000..169ef8ea --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/channel/AlipayChannelForm.vue @@ -0,0 +1,326 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/channel/MockChannelForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/channel/MockChannelForm.vue new file mode 100644 index 00000000..49cb3abc --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/channel/MockChannelForm.vue @@ -0,0 +1,122 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/channel/WeixinChannelForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/channel/WeixinChannelForm.vue new file mode 100644 index 00000000..bafa4bfe --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/components/channel/WeixinChannelForm.vue @@ -0,0 +1,342 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/app/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/index.vue new file mode 100644 index 00000000..3531633f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/app/index.vue @@ -0,0 +1,424 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/cashier/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/cashier/index.vue new file mode 100644 index 00000000..12723dba --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/cashier/index.vue @@ -0,0 +1,482 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/demo/order/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/demo/order/index.vue new file mode 100644 index 00000000..374464eb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/demo/order/index.vue @@ -0,0 +1,240 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/demo/transfer/DemoTransferForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/demo/transfer/DemoTransferForm.vue new file mode 100644 index 00000000..e5448f10 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/demo/transfer/DemoTransferForm.vue @@ -0,0 +1,122 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/demo/transfer/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/demo/transfer/index.vue new file mode 100644 index 00000000..44d07b12 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/demo/transfer/index.vue @@ -0,0 +1,159 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/notify/NotifyDetail.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/notify/NotifyDetail.vue new file mode 100644 index 00000000..938a3eeb --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/notify/NotifyDetail.vue @@ -0,0 +1,86 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/notify/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/notify/index.vue new file mode 100644 index 00000000..5daf754f --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/notify/index.vue @@ -0,0 +1,224 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/order/OrderDetail.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/order/OrderDetail.vue new file mode 100644 index 00000000..4f05c14a --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/order/OrderDetail.vue @@ -0,0 +1,111 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/order/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/order/index.vue new file mode 100644 index 00000000..16026599 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/order/index.vue @@ -0,0 +1,273 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/refund/RefundDetail.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/refund/RefundDetail.vue new file mode 100644 index 00000000..72f7a8c1 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/refund/RefundDetail.vue @@ -0,0 +1,93 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/refund/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/refund/index.vue new file mode 100644 index 00000000..eaa17b4c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/refund/index.vue @@ -0,0 +1,298 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/transfer/CreatePayTransfer.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/transfer/CreatePayTransfer.vue new file mode 100644 index 00000000..31706504 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/transfer/CreatePayTransfer.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/transfer/TransferDetail.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/transfer/TransferDetail.vue new file mode 100644 index 00000000..ad769d2b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/transfer/TransferDetail.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/transfer/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/transfer/index.vue new file mode 100644 index 00000000..b901f34b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/transfer/index.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/balance/WalletForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/balance/WalletForm.vue new file mode 100644 index 00000000..8173e123 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/balance/WalletForm.vue @@ -0,0 +1,22 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/balance/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/balance/index.vue new file mode 100644 index 00000000..296b567b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/balance/index.vue @@ -0,0 +1,149 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/rechargePackage/WalletRechargePackageForm.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/rechargePackage/WalletRechargePackageForm.vue new file mode 100644 index 00000000..0153225e --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/rechargePackage/WalletRechargePackageForm.vue @@ -0,0 +1,122 @@ + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/rechargePackage/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/rechargePackage/index.vue new file mode 100644 index 00000000..f097577c --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/rechargePackage/index.vue @@ -0,0 +1,185 @@ + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/transaction/WalletTransactionList.vue b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/transaction/WalletTransactionList.vue new file mode 100644 index 00000000..c440778b --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/pay/wallet/transaction/WalletTransactionList.vue @@ -0,0 +1,68 @@ + + + + diff --git a/mes-ui/mes-ui-admin-vue3/src/views/report/goview/index.vue b/mes-ui/mes-ui-admin-vue3/src/views/report/goview/index.vue new file mode 100644 index 00000000..1bac2869 --- /dev/null +++ b/mes-ui/mes-ui-admin-vue3/src/views/report/goview/index.vue @@ -0,0 +1,10 @@ +