daikun1@bosssoft.com.cn
1 year ago
commit
80cb2bdbc2
1363 changed files with 112730 additions and 0 deletions
@ -0,0 +1 @@ |
|||
*.sql linguist-language=java |
@ -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 |
|||
|
|||
/win-ui-app/unpackage/ |
@ -0,0 +1,49 @@ |
|||
# Docker Build & Up |
|||
|
|||
目标: 快速部署体验系统,帮助了解系统之间的依赖关系。 |
|||
依赖:docker compose v2,删除`name: win-system`,降低`version`版本为`3.3`以下,支持`docker-compose`。 |
|||
|
|||
## 功能文件列表 |
|||
|
|||
```text |
|||
. |
|||
├── Docker-HOWTO.md |
|||
├── docker-compose.yml |
|||
├── docker.env <-- 提供docker-compose环境变量配置 |
|||
├── win-server |
|||
│ └── Dockerfile |
|||
└── win-ui-admin |
|||
├── .dockerignore |
|||
├── Dockerfile |
|||
└── nginx.conf <-- 提供基础配置,gzip压缩、api转发 |
|||
``` |
|||
|
|||
## 构建 jar 包 |
|||
|
|||
```shell |
|||
# 创建maven缓存volume |
|||
docker volume create --name win-maven-repo |
|||
|
|||
docker run -it --rm --name win-maven \ |
|||
-v win-maven-repo:/root/.m2 \ |
|||
-v $PWD:/usr/src/mymaven \ |
|||
-w /usr/src/mymaven \ |
|||
maven mvn clean install package '-Dmaven.test.skip=true' |
|||
``` |
|||
|
|||
## 构建启动服务 |
|||
|
|||
```shell |
|||
docker compose --env-file docker.env up -d |
|||
``` |
|||
|
|||
首次运行会自动构建容器。可以通过`docker compose build [service]`来手动构建所有或某个docker镜像 |
|||
|
|||
`--env-file docker.env`为可选参数,只是展示了通过`.env`文件配置容器启动的环境变量,`docker-compose.yml`本身已经提供足够的默认参数来正常运行系统。 |
|||
|
|||
## 服务器的宿主机端口映射 |
|||
|
|||
- admin ui: http://localhost:8080 |
|||
- api server: http://localhost:48080 |
|||
- mysql: root/123456, port: 3306 |
|||
- redis: port: 6379 |
@ -0,0 +1,60 @@ |
|||
#!groovy |
|||
pipeline { |
|||
|
|||
agent any |
|||
|
|||
parameters { |
|||
string(name: 'TAG_NAME', defaultValue: '', description: '') |
|||
} |
|||
|
|||
environment { |
|||
// DockerHub 凭证 ID(登录您的 DockerHub) |
|||
DOCKER_CREDENTIAL_ID = 'dockerhub-id' |
|||
// GitHub 凭证 ID (推送 tag 到 GitHub 仓库) |
|||
GITHUB_CREDENTIAL_ID = 'github-id' |
|||
// kubeconfig 凭证 ID (访问接入正在运行的 Kubernetes 集群) |
|||
KUBECONFIG_CREDENTIAL_ID = 'demo-kubeconfig' |
|||
// 镜像的推送 |
|||
REGISTRY = 'docker.io' |
|||
// DockerHub 账号名 |
|||
DOCKERHUB_NAMESPACE = 'docker_username' |
|||
// GitHub 账号名 |
|||
GITHUB_ACCOUNT = 'https://gitee.com/zhijiantianya/ruoyi-vue-pro' |
|||
// 应用名称 |
|||
APP_NAME = 'win-server' |
|||
// 应用部署路径 |
|||
APP_DEPLOY_BASE_DIR = '/media/pi/KINGTON/data/work/projects/' |
|||
} |
|||
|
|||
stages { |
|||
stage('检出') { |
|||
steps { |
|||
git url: "https://gitee.com/will-we/ruoyi-vue-pro.git", |
|||
branch: "devops" |
|||
} |
|||
} |
|||
|
|||
stage('构建') { |
|||
steps { |
|||
// TODO 解决多环境链接、密码不同配置临时方案 |
|||
sh 'if [ ! -d "' + "${env.HOME}" + '/resources" ];then\n' + |
|||
' echo "配置文件不存在无需修改"\n' + |
|||
'else\n' + |
|||
' cp -rf ' + "${env.HOME}" + '/resources/*.yaml ' + "${env.APP_NAME}" + '/src/main/resources\n' + |
|||
' echo "配置文件替换"\n' + |
|||
'fi' |
|||
sh 'mvn clean package -Dmaven.test.skip=true' |
|||
} |
|||
} |
|||
|
|||
stage('部署') { |
|||
steps { |
|||
sh 'cp -f ' + ' bin/deploy.sh ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" |
|||
sh 'cp -f ' + "${env.APP_NAME}" + '/target/*.jar ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" +'/build/' |
|||
archiveArtifacts "${env.APP_NAME}" + '/target/*.jar' |
|||
sh 'chmod +x ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh' |
|||
sh 'bash ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh' |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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. |
@ -0,0 +1,340 @@ |
|||
**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!!** |
|||
|
|||
**「我喜欢写代码,乐此不疲」** |
|||
**「我喜欢做开源,以此为乐」** |
|||
|
|||
我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。 |
|||
|
|||
如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。 |
|||
|
|||
## 🐶 新手必读 |
|||
|
|||
* 演示地址【Vue3 + element-plus】:<http://dashboard-vue3.win.iocoder.cn> |
|||
* 演示地址【Vue3 + vben(ant-design-vue)】:<http://dashboard-vben.win.iocoder.cn> |
|||
* 演示地址【Vue2 + element-ui】:<http://dashboard.win.iocoder.cn> |
|||
* 启动文档:<https://doc.iocoder.cn/quick-start/> |
|||
* 视频教程:<https://doc.iocoder.cn/video/> |
|||
|
|||
已支持 Spring Boot 3.X + JDK 17 版本,可见 [master-boot3](https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/README.md) 分支。 |
|||
|
|||
## 🐯 平台简介 |
|||
|
|||
**闻荫**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。 |
|||
|
|||
> 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。 |
|||
> |
|||
> 😜 给项目点点 Star 吧,这对我们真的很重要! |
|||
|
|||
![架构图](/.image/common/ruoyi-vue-pro-architecture.png) |
|||
|
|||
* 管理后台的电脑端:Vue3 提供 [element-plus](https://gitee.com/wincode/win-ui-admin-vue3)、[vben(ant-design-vue)](https://gitee.com/wincode/win-ui-admin-vben) 两个版本,Vue2 提供 [element-ui](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/win-ui-admin) 版本 |
|||
* 管理后台的移动端:采用 [uni-app](https://github.com/dcloudio/uni-app) 方案,一份代码多终端适配,同时支持 APP、小程序、H5! |
|||
* 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson |
|||
* 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等 |
|||
* 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统,支持 SSO 单点登录 |
|||
* 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能 |
|||
* 支持 SaaS 多租户,可自定义每个租户的权限,提供透明化的多租户底层封装 |
|||
* 工作流使用 Flowable,支持动态表单、在线设计流程、会签 / 或签、多种任务分配方式 |
|||
* 高效率开发,使用代码生成器可以一键生成前后端代码 + 单元测试 + Swagger 接口文档 + Validator 参数校验 |
|||
* 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款 |
|||
* 集成阿里云、腾讯云等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务 |
|||
* 集成报表设计器、大屏设计器,通过拖拽即可生成酷炫的报表与大屏 |
|||
|
|||
## 🐳 项目关系 |
|||
|
|||
![架构演进](https://static.iocoder.cn/win-roadmap.png?imageView2/2/format/webp) |
|||
|
|||
三个项目的功能对比,可见社区共同整理的 [国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn) 表格。 |
|||
|
|||
### 后端项目 |
|||
|
|||
|
|||
| 项目 | Star | 简介 | |
|||
|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| |
|||
| [ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro) | 基于 Spring Boot 多模块架构 | |
|||
| [win-cloud](https://gitee.com/zhijiantianya/win-cloud) | [![Gitee star](https://gitee.com/zhijiantianya/win-cloud/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/win-cloud) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/win-cloud.svg?style=social&label=Stars)](https://github.com/YunaiV/win-cloud) | 基于 Spring Cloud 微服务架构 | |
|||
| [Spring-Boot-Labs](https://gitee.com/wincode/SpringBoot-Labs) | [![Gitee star](https://gitee.com/wincode/SpringBoot-Labs/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/win-cloud) [![GitHub stars](https://img.shields.io/github/stars/wincode/SpringBoot-Labs.svg?style=social&label=Stars)](https://github.com/wincode/SpringBoot-Labs) | 系统学习 Spring Boot & Cloud 专栏 | |
|||
|
|||
### 前端项目 |
|||
|
|||
| 项目 | Star | 简介 | |
|||
|----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------| |
|||
| [win-ui-admin-vue3](https://gitee.com/wincode/win-ui-admin-vue3) | [![Gitee star](https://gitee.com/wincode/win-ui-admin-vue3/badge/star.svg?theme=white)](https://gitee.com/wincode/win-ui-admin-vue3) [![GitHub stars](https://img.shields.io/github/stars/wincode/win-ui-admin-vue3.svg?style=social&label=Stars)](https://github.com/wincode/win-ui-admin-vue3) | 基于 Vue3 + element-plus 实现的管理后台 | |
|||
| [win-ui-admin-vben](https://gitee.com/wincode/win-ui-admin-vben) | [![Gitee star](https://gitee.com/wincode/win-ui-admin-vben/badge/star.svg?theme=white)](https://gitee.com/wincode/win-ui-admin-vben) [![GitHub stars](https://img.shields.io/github/stars/wincode/win-ui-admin-vben.svg?style=social&label=Stars)](https://github.com/wincode/win-ui-admin-vben) | 基于 Vue3 + vben(ant-design-vue) 实现的管理后台 | |
|||
| [win-ui-admin](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/win-ui-admin) | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/win-ui-admin) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro/tree/master/win-ui-admin) | 基于 Vue2 + element-ui 实现的管理后台 | |
|||
| [win-ui-admin-uniapp](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/win-ui-admin-uniapp) | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/win-ui-admin-uniapp) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro/tree/master/win-ui-admin-uniapp) | 基于 uni-app + uni-ui 实现的管理后台的小程序 | |
|||
| [win-ui-go-view](https://gitee.com/wincode/win-ui-go-view) | [![Gitee star](https://gitee.com/wincode/win-ui-go-view/badge/star.svg?theme=white)](https://gitee.com/wincode/win-ui-go-view) [![GitHub stars](https://img.shields.io/github/stars/wincode/win-ui-go-view.svg?style=social&label=Stars)](https://github.com/wincode/win-ui-go-view) | 基于 Vue3 + naive-ui 实现的大屏报表 | |
|||
| [win-mall-uniapp](https://gitee.com/wincode/win-mall-uniapp) | [![Gitee star](https://gitee.com/wincode/win-mall-uniapp/badge/star.svg?theme=white)](https://gitee.com/wincode/win-mall-uniapp) [![GitHub stars](https://img.shields.io/github/stars/wincode/win-mall-uniapp.svg?style=social&label=Stars)](https://github.com/wincode/win-mall-uniapp) | 基于 uni-app 实现的商城小程序 | |
|||
|
|||
## 🐰 分支说明 |
|||
|
|||
| | JDK 8 完整版 | JDK 8 精简版 | JDK 17 完整版 | |
|||
|-------|-----------------------------------------------------------|--------------------------------------------------------------------|-----------------------------------------------------------------------------| |
|||
| 分支 | [`master`](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [`mini`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/mini/) | [`master-boot3`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master-boot3/) | |
|||
| 说明 | 包括所有功能 | 只保留核心功能 | 适配 Spring Boot 3.X | |
|||
| 系统功能 | √ | √ | √ | |
|||
| 基础设施 | √ | √ | √ | |
|||
| 会员中心 | √ | √ | √ | |
|||
| 工作流程 | √ | x | √ | |
|||
| 数据报表 | √ | x | 适配中 | |
|||
| 商城系统 | √ | x | √ | |
|||
| 微信公众号 | √ | x | √ | |
|||
|
|||
## 😎 开源协议 |
|||
|
|||
**为什么推荐使用本项目?** |
|||
|
|||
① 本项目采用比 Apache 2.0 更宽松的 [MIT License](https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/LICENSE) 开源协议,个人与企业可 100% 免费使用,不用保留类作者、Copyright 信息。 |
|||
|
|||
② 代码全部开源,不会像其他项目一样,只开源部分代码,让你无法了解整个项目的架构设计。[国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn) |
|||
|
|||
![开源项目对比](https://static.iocoder.cn/project-vs.png?imageView2/2/format/webp/w/1280) |
|||
|
|||
③ 代码整洁、架构整洁,遵循《阿里巴巴 Java 开发手册》规范,代码注释详细,57000 行 Java 代码,22000 行代码注释。 |
|||
|
|||
## 🤝 项目外包 |
|||
|
|||
我们也是接外包滴,如果你有项目想要外包,可以微信联系【**Aix9975**】。 |
|||
|
|||
团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。 |
|||
|
|||
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 聊天、微信公众号、微信小程序等等。 |
|||
|
|||
## 🐼 内置功能 |
|||
|
|||
系统内置多种多种业务功能,可以用于快速你的业务系统: |
|||
|
|||
![功能分层](/.image/common/ruoyi-vue-pro-biz.png) |
|||
|
|||
* 系统功能 |
|||
* 基础设施 |
|||
* 工作流程 |
|||
* 支付系统 |
|||
* 会员中心 |
|||
* 数据报表 |
|||
* 商城系统 |
|||
* 微信公众号 |
|||
|
|||
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。 |
|||
> |
|||
> * 额外新增的功能,我们使用 🚀 标记。 |
|||
> * 重新实现的功能,我们使用 ⭐️ 标记。 |
|||
|
|||
🙂 所有功能,都通过 **单元测试** 保证高质量。 |
|||
|
|||
### 系统功能 |
|||
|
|||
| | 功能 | 描述 | |
|||
|-----|-------|---------------------------------| |
|||
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 | |
|||
| ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 | |
|||
| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 | |
|||
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 | |
|||
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 | |
|||
| | 岗位管理 | 配置系统用户所属担任职务 | |
|||
| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 | |
|||
| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 | |
|||
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 | |
|||
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 | |
|||
| 🚀 | 邮件管理 | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台 | |
|||
| 🚀 | 站内信 | 系统内的消息通知,提供站内信模版、站内信消息 | |
|||
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 | |
|||
| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 | |
|||
| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 | |
|||
| | 通知公告 | 系统通知公告信息发布维护 | |
|||
| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 | |
|||
| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 | |
|||
| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 | |
|||
|
|||
### 工作流程 |
|||
|
|||
| | 功能 | 描述 | |
|||
|-----|-------|----------------------------------------| |
|||
| 🚀 | 流程模型 | 配置工作流的流程模型,支持文件导入与在线设计流程图,提供 7 种任务分配规则 | |
|||
| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 | |
|||
| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 | |
|||
| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 | |
|||
| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转发、委派、退回等操作 | |
|||
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 | |
|||
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 | |
|||
|
|||
### 支付系统 |
|||
|
|||
| | 功能 | 描述 | |
|||
|-----|------|---------------------------| |
|||
| 🚀 | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 | |
|||
| 🚀 | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单 | |
|||
| 🚀 | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单 | |
|||
| 🚀 | 回调通知 | 查看支付回调业务的【支付】【退款】的通知结果 | |
|||
| 🚀 | 接入示例 | 提供接入支付系统的【支付】【退款】的功能实战 | |
|||
|
|||
### 基础设施 |
|||
|
|||
| | 功能 | 描述 | |
|||
|-----|----------|----------------------------------------------| |
|||
| 🚀 | 代码生成 | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 | |
|||
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 | |
|||
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 | |
|||
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 | |
|||
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 | |
|||
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 | |
|||
| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 | |
|||
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 | |
|||
| | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 | |
|||
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 | |
|||
| 🚀 | 消息队列 | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 | |
|||
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 | |
|||
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 | |
|||
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 | |
|||
| 🚀 | 分布式锁 | 基于 Redis 实现分布式锁,满足并发场景 | |
|||
| 🚀 | 幂等组件 | 基于 Redis 实现幂等组件,解决重复请求问题 | |
|||
| 🚀 | 服务保障 | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能 | |
|||
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 | |
|||
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 | |
|||
|
|||
### 数据报表 |
|||
|
|||
| | 功能 | 描述 | |
|||
|-----|-------|--------------------| |
|||
| 🚀 | 报表设计器 | 支持数据报表、图形报表、打印设计等 | |
|||
| 🚀 | 大屏设计器 | 拖拽生成数据大屏,内置几十种图表组件 | |
|||
|
|||
### 微信公众号 |
|||
|
|||
| | 功能 | 描述 | |
|||
|-----|--------|-------------------------------| |
|||
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 | |
|||
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 | |
|||
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 | |
|||
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 | |
|||
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 | |
|||
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 | |
|||
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 | |
|||
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 | |
|||
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 | |
|||
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 | |
|||
|
|||
### 商城系统 |
|||
|
|||
![功能图](/.image/common/mall-feature.png) |
|||
|
|||
![功能图](/.image/common/mall-preview.png) |
|||
|
|||
_前端基于 crmeb uniapp 经过授权重构,优化代码实现,接入闻荫快速开发平台_ |
|||
|
|||
演示地址:<https://doc.iocoder.cn/mall-preview/> |
|||
|
|||
### 会员中心 |
|||
|
|||
| | 功能 | 描述 | |
|||
|-----|------|----------------------------------| |
|||
| 🚀 | 会员管理 | 会员是 C 端的消费者,该功能用于会员的搜索与管理 | |
|||
| 🚀 | 会员标签 | 对会员的标签进行创建、查询、修改、删除等操作 | |
|||
| 🚀 | 会员等级 | 对会员的等级、成长值进行管理,可用于订单折扣等会员权益 | |
|||
| 🚀 | 会员分组 | 对会员进行分组,用于用户画像、内容推送等运营手段 | |
|||
| 🚀 | 积分签到 | 回馈给签到、消费等行为的积分,会员可订单抵现、积分兑换等途径消耗 | |
|||
|
|||
## 🐨 技术栈 |
|||
|
|||
### 模块 |
|||
|
|||
| 项目 | 说明 | |
|||
|--------------------------------------------------------------------------|--------------------| |
|||
| `win-dependencies` | Maven 依赖版本管理 | |
|||
| `win-framework` | Java 框架拓展 | |
|||
| `win-server` | 管理后台 + 用户 APP 的服务端 | |
|||
| `win-module-system` | 系统功能的 Module 模块 | |
|||
| `win-module-member` | 会员中心的 Module 模块 | |
|||
| `win-module-infra` | 基础设施的 Module 模块 | |
|||
| `win-module-bpm` | 工作流程的 Module 模块 | |
|||
| `win-module-pay` | 支付系统的 Module 模块 | |
|||
| `win-module-mall` | 商城系统的 Module 模块 | |
|||
| `win-module-mp` | 微信公众号的 Module 模块 | |
|||
| `win-module-report` | 大屏报表 Module 模块 | |
|||
|
|||
### 框架 |
|||
|
|||
| 框架 | 说明 | 版本 | 学习指南 | |
|||
|---------------------------------------------------------------------------------------------|------------------|-------------|----------------------------------------------------------------| |
|||
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.7.15 | [文档](https://github.com/YunaiV/SpringBoot-Labs) | |
|||
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | | |
|||
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.19 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?win) | |
|||
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.3.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?win) | |
|||
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.6.1 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?win) | |
|||
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 / 6.0 | | |
|||
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.18.0 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?win) | |
|||
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.24 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?win) | |
|||
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.7.6 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?win) | |
|||
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.5 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?win) | |
|||
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.8.0 | [文档](https://doc.iocoder.cn/bpm/) | |
|||
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?win) | |
|||
| [Springdoc](https://springdoc.org/) | Swagger 文档 | 1.6.15 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?win) | |
|||
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?win) | |
|||
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.12.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?win) | |
|||
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.7.10 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?win) | |
|||
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | | |
|||
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.5.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?win) | |
|||
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.28 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?win) | |
|||
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - | |
|||
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.8.0 | - | |
|||
|
|||
## 🐷 演示图 |
|||
|
|||
### 系统功能 |
|||
|
|||
| 模块 | biu | biu | biu | |
|||
|----------|-----------------------------|---------------------------|--------------------------| |
|||
| 登录 & 首页 | ![登录](/.image/登录.jpg) | ![首页](/.image/首页.jpg) | ![个人中心](/.image/个人中心.jpg) | |
|||
| 用户 & 应用 | ![用户管理](/.image/用户管理.jpg) | ![令牌管理](/.image/令牌管理.jpg) | ![应用管理](/.image/应用管理.jpg) | |
|||
| 租户 & 套餐 | ![租户管理](/.image/租户管理.jpg) | ![租户套餐](/.image/租户套餐.png) | - | |
|||
| 部门 & 岗位 | ![部门管理](/.image/部门管理.jpg) | ![岗位管理](/.image/岗位管理.jpg) | - | |
|||
| 菜单 & 角色 | ![菜单管理](/.image/菜单管理.jpg) | ![角色管理](/.image/角色管理.jpg) | - | |
|||
| 审计日志 | ![操作日志](/.image/操作日志.jpg) | ![登录日志](/.image/登录日志.jpg) | - | |
|||
| 短信 | ![短信渠道](/.image/短信渠道.jpg) | ![短信模板](/.image/短信模板.jpg) | ![短信日志](/.image/短信日志.jpg) | |
|||
| 字典 & 敏感词 | ![字典类型](/.image/字典类型.jpg) | ![字典数据](/.image/字典数据.jpg) | ![敏感词](/.image/敏感词.jpg) | |
|||
| 错误码 & 通知 | ![错误码管理](/.image/错误码管理.jpg) | ![通知公告](/.image/通知公告.jpg) | - | |
|||
|
|||
### 工作流程 |
|||
|
|||
| 模块 | biu | biu | biu | |
|||
|---------|---------------------------------|---------------------------------|---------------------------------| |
|||
| 流程模型 | ![流程模型-列表](/.image/流程模型-列表.jpg) | ![流程模型-设计](/.image/流程模型-设计.jpg) | ![流程模型-定义](/.image/流程模型-定义.jpg) | |
|||
| 表单 & 分组 | ![流程表单](/.image/流程表单.jpg) | ![用户分组](/.image/用户分组.jpg) | - | |
|||
| 我的流程 | ![我的流程-列表](/.image/我的流程-列表.jpg) | ![我的流程-发起](/.image/我的流程-发起.jpg) | ![我的流程-详情](/.image/我的流程-详情.jpg) | |
|||
| 待办 & 已办 | ![任务列表-审批](/.image/任务列表-审批.jpg) | ![任务列表-待办](/.image/任务列表-待办.jpg) | ![任务列表-已办](/.image/任务列表-已办.jpg) | |
|||
| OA 请假 | ![OA请假-列表](/.image/OA请假-列表.jpg) | ![OA请假-发起](/.image/OA请假-发起.jpg) | ![OA请假-详情](/.image/OA请假-详情.jpg) | |
|||
|
|||
### 基础设施 |
|||
|
|||
| 模块 | biu | biu | biu | |
|||
|---------------|-------------------------------|-----------------------------|---------------------------| |
|||
| 代码生成 | ![代码生成](/.image/代码生成.jpg) | ![生成效果](/.image/生成效果.jpg) | - | |
|||
| 文档 | ![系统接口](/.image/系统接口.jpg) | ![数据库文档](/.image/数据库文档.jpg) | - | |
|||
| 文件 & 配置 | ![文件配置](/.image/文件配置.jpg) | ![文件管理](/.image/文件管理2.jpg) | ![配置管理](/.image/配置管理.jpg) | |
|||
| 定时任务 | ![定时任务](/.image/定时任务.jpg) | ![任务日志](/.image/任务日志.jpg) | - | |
|||
| API 日志 | ![访问日志](/.image/访问日志.jpg) | ![错误日志](/.image/错误日志.jpg) | - | |
|||
| MySQL & Redis | ![MySQL](/.image/MySQL.jpg) | ![Redis](/.image/Redis.jpg) | - | |
|||
| 监控平台 | ![Java监控](/.image/Java监控.jpg) | ![链路追踪](/.image/链路追踪.jpg) | ![日志中心](/.image/日志中心.jpg) | |
|||
|
|||
### 支付系统 |
|||
|
|||
| 模块 | biu | biu | biu | |
|||
|---------|---------------------------|---------------------------------|---------------------------------| |
|||
| 商家 & 应用 | ![商户信息](/.image/商户信息.jpg) | ![应用信息-列表](/.image/应用信息-列表.jpg) | ![应用信息-编辑](/.image/应用信息-编辑.jpg) | |
|||
| 支付 & 退款 | ![支付订单](/.image/支付订单.jpg) | ![退款订单](/.image/退款订单.jpg) | --- | |
|||
### 数据报表 |
|||
|
|||
| 模块 | biu | biu | biu | |
|||
|-------|---------------------------------|---------------------------------|---------------------------------------| |
|||
| 报表设计器 | ![数据报表](/.image/报表设计器-数据报表.jpg) | ![图形报表](/.image/报表设计器-图形报表.jpg) | ![报表设计器-打印设计](/.image/报表设计器-打印设计.jpg) | |
|||
| 大屏设计器 | ![大屏列表](/.image/大屏设计器-列表.jpg) | ![大屏预览](/.image/大屏设计器-预览.jpg) | ![大屏编辑](/.image/大屏设计器-编辑.jpg) | |
|||
|
|||
### 移动端(管理后台) |
|||
|
|||
| biu | biu | biu | |
|||
|----------------------------------|----------------------------------|----------------------------------| |
|||
| ![](/.image/admin-uniapp/01.png) | ![](/.image/admin-uniapp/02.png) | ![](/.image/admin-uniapp/03.png) | |
|||
| ![](/.image/admin-uniapp/04.png) | ![](/.image/admin-uniapp/05.png) | ![](/.image/admin-uniapp/06.png) | |
|||
| ![](/.image/admin-uniapp/07.png) | ![](/.image/admin-uniapp/08.png) | ![](/.image/admin-uniapp/09.png) | |
|||
|
|||
目前已经实现登录、我的、工作台、编辑资料、头像修改、密码修改、常见问题、关于我们等基础功能。 |
@ -0,0 +1,84 @@ |
|||
version: "3.4" |
|||
|
|||
name: win-system |
|||
|
|||
services: |
|||
mysql: |
|||
container_name: win-mysql |
|||
image: mysql:8 |
|||
restart: unless-stopped |
|||
tty: true |
|||
ports: |
|||
- "3306:3306" |
|||
environment: |
|||
MYSQL_DATABASE: ${MYSQL_DATABASE:-ruoyi-vue-pro} |
|||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456} |
|||
volumes: |
|||
- mysql:/var/lib/mysql/ |
|||
- ./sql/mysql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/ruoyi-vue-pro.sql:ro |
|||
|
|||
redis: |
|||
container_name: win-redis |
|||
image: redis:6-alpine |
|||
restart: unless-stopped |
|||
ports: |
|||
- "6379:6379" |
|||
volumes: |
|||
- redis:/data |
|||
|
|||
server: |
|||
container_name: win-server |
|||
build: |
|||
context: ./win-server/ |
|||
image: win-server |
|||
restart: unless-stopped |
|||
ports: |
|||
- "48080:48080" |
|||
environment: |
|||
# https://github.com/polovyivan/docker-pass-configs-to-container |
|||
SPRING_PROFILES_ACTIVE: local |
|||
JAVA_OPTS: |
|||
${JAVA_OPTS:- |
|||
-Xms512m |
|||
-Xmx512m |
|||
-Djava.security.egd=file:/dev/./urandom |
|||
} |
|||
ARGS: |
|||
--spring.datasource.dynamic.datasource.master.url=${MASTER_DATASOURCE_URL:-jdbc:mysql://win-mysql:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true} |
|||
--spring.datasource.dynamic.datasource.master.username=${MASTER_DATASOURCE_USERNAME:-root} |
|||
--spring.datasource.dynamic.datasource.master.password=${MASTER_DATASOURCE_PASSWORD:-123456} |
|||
--spring.datasource.dynamic.datasource.slave.url=${SLAVE_DATASOURCE_URL:-jdbc:mysql://win-mysql:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true} |
|||
--spring.datasource.dynamic.datasource.slave.username=${SLAVE_DATASOURCE_USERNAME:-root} |
|||
--spring.datasource.dynamic.datasource.slave.password=${SLAVE_DATASOURCE_PASSWORD:-123456} |
|||
--spring.redis.host=${REDIS_HOST:-win-redis} |
|||
depends_on: |
|||
- mysql |
|||
- redis |
|||
|
|||
admin: |
|||
container_name: win-admin |
|||
build: |
|||
context: ./win-ui-admin |
|||
args: |
|||
NODE_ENV: |
|||
ENV=${NODE_ENV:-production} |
|||
PUBLIC_PATH=${PUBLIC_PATH:-/} |
|||
VUE_APP_TITLE=${VUE_APP_TITLE:-闻荫管理系统} |
|||
VUE_APP_BASE_API=${VUE_APP_BASE_API:-/prod-api} |
|||
VUE_APP_APP_NAME=${VUE_APP_APP_NAME:-/} |
|||
VUE_APP_TENANT_ENABLE=${VUE_APP_TENANT_ENABLE:-true} |
|||
VUE_APP_CAPTCHA_ENABLE=${VUE_APP_CAPTCHA_ENABLE:-true} |
|||
VUE_APP_DOC_ENABLE=${VUE_APP_DOC_ENABLE:-true} |
|||
VUE_APP_BAIDU_CODE=${VUE_APP_BAIDU_CODE:-fadc1bd5db1a1d6f581df60a1807f8ab} |
|||
image: win-admin |
|||
restart: unless-stopped |
|||
ports: |
|||
- "8080:80" |
|||
depends_on: |
|||
- server |
|||
|
|||
volumes: |
|||
mysql: |
|||
driver: local |
|||
redis: |
|||
driver: local |
@ -0,0 +1,25 @@ |
|||
## mysql |
|||
MYSQL_DATABASE=ruoyi-vue-pro |
|||
MYSQL_ROOT_PASSWORD=123456 |
|||
|
|||
## server |
|||
JAVA_OPTS=-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom |
|||
|
|||
MASTER_DATASOURCE_URL=jdbc:mysql://win-mysql:3306/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true |
|||
MASTER_DATASOURCE_USERNAME=root |
|||
MASTER_DATASOURCE_PASSWORD=${MYSQL_ROOT_PASSWORD} |
|||
SLAVE_DATASOURCE_URL=${MASTER_DATASOURCE_URL} |
|||
SLAVE_DATASOURCE_USERNAME=${MASTER_DATASOURCE_USERNAME} |
|||
SLAVE_DATASOURCE_PASSWORD=${MASTER_DATASOURCE_PASSWORD} |
|||
REDIS_HOST=win-redis |
|||
|
|||
## admin |
|||
NODE_ENV=production |
|||
PUBLIC_PATH=/ |
|||
VUE_APP_TITLE=闻荫管理系统 |
|||
VUE_APP_BASE_API=/prod-api |
|||
VUE_APP_APP_NAME=/ |
|||
VUE_APP_TENANT_ENABLE=true |
|||
VUE_APP_CAPTCHA_ENABLE=true |
|||
VUE_APP_DOC_ENABLE=true |
|||
VUE_APP_BAIDU_CODE=fadc1bd5db1a1d6f581df60a1807f8ab |
@ -0,0 +1,20 @@ |
|||
{ |
|||
"local": { |
|||
"baseUrl": "http://127.0.0.1:48080/admin-api", |
|||
"token": "test1", |
|||
"adminTenentId": "1", |
|||
|
|||
"appApi": "http://127.0.0.1:48080/app-api", |
|||
"appToken": "test247", |
|||
"appTenentId": "1" |
|||
}, |
|||
"gateway": { |
|||
"baseUrl": "http://127.0.0.1:8888/admin-api", |
|||
"token": "test1", |
|||
"adminTenentId": "1", |
|||
|
|||
"appApi": "http://127.0.0.1:8888/app-api", |
|||
"appToken": "test1", |
|||
"appTenantId": "1" |
|||
} |
|||
} |
@ -0,0 +1,4 @@ |
|||
config.stopBubbling = true |
|||
lombok.tostring.callsuper=CALL |
|||
lombok.equalsandhashcode.callsuper=CALL |
|||
lombok.accessors.chain=true |
@ -0,0 +1,141 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win</artifactId> |
|||
<version>${revision}</version> |
|||
<packaging>pom</packaging> |
|||
<modules> |
|||
<module>win-dependencies</module> |
|||
<module>win-framework</module> |
|||
<!-- Server 主项目 --> |
|||
<module>win-server</module> |
|||
<!-- 各种 module 拓展 --> |
|||
<module>win-module-system</module> |
|||
<module>win-module-infra</module> |
|||
<module>win-module-bpm</module> |
|||
<module>win-module-report</module> |
|||
</modules> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>闻荫项目基础脚手架</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<properties> |
|||
<revision>3.0.0</revision> |
|||
<!-- Maven 相关 --> |
|||
<java.version>1.8</java.version> |
|||
<maven.compiler.source>${java.version}</maven.compiler.source> |
|||
<maven.compiler.target>${java.version}</maven.compiler.target> |
|||
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version> |
|||
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version> |
|||
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version> |
|||
<!-- 看看咋放到 bom 里 --> |
|||
<lombok.version>1.18.28</lombok.version> |
|||
<spring.boot.version>2.7.15</spring.boot.version> |
|||
<mapstruct.version>1.5.5.Final</mapstruct.version> |
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
|||
</properties> |
|||
|
|||
<dependencyManagement> |
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-dependencies</artifactId> |
|||
<version>${revision}</version> |
|||
<type>pom</type> |
|||
<scope>import</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
</dependencyManagement> |
|||
|
|||
<build> |
|||
<pluginManagement> |
|||
<plugins> |
|||
<!-- maven-surefire-plugin 插件,用于运行单元测试。 --> |
|||
<!-- 注意,需要使用 3.0.X+,因为要支持 Junit 5 版本 --> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-surefire-plugin</artifactId> |
|||
<version>${maven-surefire-plugin.version}</version> |
|||
</plugin> |
|||
<!-- maven-compiler-plugin 插件,解决 spring-boot-configuration-processor + Lombok + MapStruct 组合 --> |
|||
<!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada --> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-compiler-plugin</artifactId> |
|||
<version>${maven-compiler-plugin.version}</version> |
|||
<configuration> |
|||
<annotationProcessorPaths> |
|||
<path> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-configuration-processor</artifactId> |
|||
<version>${spring.boot.version}</version> |
|||
</path> |
|||
<path> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<version>${lombok.version}</version> |
|||
</path> |
|||
<path> |
|||
<groupId>org.mapstruct</groupId> |
|||
<artifactId>mapstruct-processor</artifactId> |
|||
<version>${mapstruct.version}</version> |
|||
</path> |
|||
</annotationProcessorPaths> |
|||
</configuration> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.codehaus.mojo</groupId> |
|||
<artifactId>flatten-maven-plugin</artifactId> |
|||
</plugin> |
|||
</plugins> |
|||
</pluginManagement> |
|||
|
|||
<plugins> |
|||
<!-- 统一 revision 版本 --> |
|||
<plugin> |
|||
<groupId>org.codehaus.mojo</groupId> |
|||
<artifactId>flatten-maven-plugin</artifactId> |
|||
<version>${flatten-maven-plugin.version}</version> |
|||
<configuration> |
|||
<flattenMode>resolveCiFriendliesOnly</flattenMode> |
|||
<updatePomFile>true</updatePomFile> |
|||
</configuration> |
|||
<executions> |
|||
<execution> |
|||
<goals> |
|||
<goal>flatten</goal> |
|||
</goals> |
|||
<id>flatten</id> |
|||
<phase>process-resources</phase> |
|||
</execution> |
|||
<execution> |
|||
<goals> |
|||
<goal>clean</goal> |
|||
</goals> |
|||
<id>flatten.clean</id> |
|||
<phase>clean</phase> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
|
|||
<!-- 使用 huawei / aliyun 的 Maven 源,提升下载速度 --> |
|||
<repositories> |
|||
<repository> |
|||
<id>huaweicloud</id> |
|||
<name>huawei</name> |
|||
<url>https://mirrors.huaweicloud.com/repository/maven/</url> |
|||
</repository> |
|||
<repository> |
|||
<id>aliyunmaven</id> |
|||
<name>aliyun</name> |
|||
<url>https://maven.aliyun.com/repository/public</url> |
|||
</repository> |
|||
</repositories> |
|||
|
|||
</project> |
@ -0,0 +1,3 @@ |
|||
暂未适配 IBM DB2 数据库,如果你有需要,可以微信联系 wangwenbin-server 一起建设。 |
|||
|
|||
你需要把表结构与数据导入到 DM 数据库,我来测试与适配代码。 |
@ -0,0 +1,3 @@ |
|||
暂未适配国产 DM 数据库,如果你有需要,可以微信联系 wangwenbin-server 一起建设。 |
|||
|
|||
你需要把表结构与数据导入到 DM 数据库,我来测试与适配代码。 |
File diff suppressed because it is too large
@ -0,0 +1,221 @@ |
|||
-- 增加配置表 |
|||
create table trade_config |
|||
( |
|||
id bigint auto_increment comment '自增主键' primary key, |
|||
brokerage_enabled bit default 1 not null comment '是否启用分佣', |
|||
brokerage_enabled_condition tinyint default 0 not null comment '分佣模式:1-人人分销 2-指定分销', |
|||
brokerage_bind_mode tinyint default 0 not null comment '分销关系绑定模式: 1-没有推广人,2-新用户, 3-扫码覆盖', |
|||
brokerage_post_urls varchar(2000) default '' null comment '分销海报图地址数组', |
|||
brokerage_first_percent int default 0 not null comment '一级返佣比例', |
|||
brokerage_second_percent int default 0 not null comment '二级返佣比例', |
|||
brokerage_withdraw_min_price int default 0 not null comment '用户提现最低金额', |
|||
brokerage_bank_names varchar(200) default '' not null comment '提现银行(字典类型=brokerage_bank_name)', |
|||
brokerage_frozen_days int default 7 not null comment '佣金冻结时间(天)', |
|||
brokerage_withdraw_type varchar(32) default '1,2,3,4' not null comment '提现方式:1-钱包;2-银行卡;3-微信;4-支付宝', |
|||
creator varchar(64) collate utf8mb4_unicode_ci default '' null comment '创建者', |
|||
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间', |
|||
updater varchar(64) collate utf8mb4_unicode_ci default '' null comment '更新者', |
|||
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', |
|||
deleted bit default b'0' not null comment '是否删除', |
|||
tenant_id bigint default 0 not null comment '租户编号' |
|||
) comment '交易中心配置'; |
|||
|
|||
-- 增加分销用户扩展表 |
|||
create table trade_brokerage_user |
|||
( |
|||
id bigint auto_increment comment '用户编号' primary key, |
|||
bind_user_id bigint null comment '推广员编号', |
|||
bind_user_time datetime null comment '推广员绑定时间', |
|||
brokerage_enabled bit default 1 not null comment '是否成为推广员', |
|||
brokerage_time datetime null comment '成为分销员时间', |
|||
price int default 0 not null comment '可用佣金', |
|||
frozen_price int default 0 not null comment '冻结佣金', |
|||
creator varchar(64) collate utf8mb4_unicode_ci default '' null comment '创建者', |
|||
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间', |
|||
updater varchar(64) collate utf8mb4_unicode_ci default '' null comment '更新者', |
|||
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', |
|||
deleted bit default b'0' not null comment '是否删除', |
|||
tenant_id bigint default 0 not null comment '租户编号' |
|||
) comment '分销用户'; |
|||
|
|||
create index idx_invite_user_id on trade_brokerage_user (bind_user_id) comment '推广员编号'; |
|||
create index idx_agent on trade_brokerage_user (brokerage_enabled) comment '是否成为推广员'; |
|||
|
|||
|
|||
create table trade_brokerage_record |
|||
( |
|||
id int auto_increment comment '编号' |
|||
primary key, |
|||
user_id bigint not null comment '用户编号', |
|||
biz_id varchar(64) default '' not null comment '业务编号', |
|||
biz_type tinyint default 0 not null comment '业务类型:0-订单,1-提现', |
|||
title varchar(64) default '' not null comment '标题', |
|||
price int default 0 not null comment '金额', |
|||
total_price int default 0 not null comment '当前总佣金', |
|||
description varchar(500) default '' not null comment '说明', |
|||
status tinyint default 0 not null comment '状态:0-待结算,1-已结算,2-已取消', |
|||
frozen_days int default 0 not null comment '冻结时间(天)', |
|||
unfreeze_time datetime null comment '解冻时间', |
|||
creator varchar(64) collate utf8mb4_general_ci default '' null comment '创建者', |
|||
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间', |
|||
updater varchar(64) collate utf8mb4_general_ci default '' null comment '更新者', |
|||
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', |
|||
deleted bit default b'0' not null comment '是否删除', |
|||
tenant_id bigint default 0 not null comment '租户编号' |
|||
) |
|||
comment '佣金记录'; |
|||
|
|||
create index idx_user_id on trade_brokerage_record (user_id) comment '用户编号'; |
|||
create index idx_biz on trade_brokerage_record (biz_type, biz_id) comment '业务'; |
|||
create index idx_status on trade_brokerage_record (status) comment '状态'; |
|||
|
|||
|
|||
create table trade_brokerage_withdraw |
|||
( |
|||
id int auto_increment comment '编号' |
|||
primary key, |
|||
user_id bigint not null comment '用户编号', |
|||
price int default 0 not null comment '提现金额', |
|||
fee_price int default 0 not null comment '提现手续费', |
|||
total_price int default 0 not null comment '当前总佣金', |
|||
type tinyint default 0 not null comment '提现类型:1-钱包;2-银行卡;3-微信;4-支付宝', |
|||
name varchar(64) null comment '真实姓名', |
|||
account_no varchar(64) null comment '账号', |
|||
bank_name varchar(100) null comment '银行名称', |
|||
bank_address varchar(200) null comment '开户地址', |
|||
account_qr_code_url varchar(512) null comment '收款码', |
|||
status tinyint(2) default 0 not null comment '状态:0-审核中,10-审核通过 20-审核不通过;预留:11 - 提现成功;21-提现失败', |
|||
audit_reason varchar(128) null comment '审核驳回原因', |
|||
audit_time datetime null comment '审核时间', |
|||
remark varchar(500) null comment '备注', |
|||
creator varchar(64) collate utf8mb4_general_ci default '' null comment '创建者', |
|||
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间', |
|||
updater varchar(64) collate utf8mb4_general_ci default '' null comment '更新者', |
|||
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', |
|||
deleted bit default b'0' not null comment '是否删除', |
|||
tenant_id bigint default 0 not null comment '租户编号' |
|||
) |
|||
comment '佣金提现'; |
|||
|
|||
create index idx_user_id on trade_brokerage_withdraw (user_id) comment '用户编号'; |
|||
create index idx_audit_status on trade_brokerage_withdraw (status) comment '状态'; |
|||
|
|||
-- 增加字典 |
|||
insert into system_dict_type(type, name) |
|||
values ('brokerage_enabled_condition', '分佣模式'); |
|||
insert into system_dict_data(dict_type, label, value, sort, remark) |
|||
values ('brokerage_enabled_condition', '人人分销', 1, 1, '所有用户都可以分销'), |
|||
('brokerage_enabled_condition', '指定分销', 2, 2, '仅可后台手动设置推广员'); |
|||
|
|||
insert into system_dict_type(type, name) |
|||
values ('brokerage_bind_mode', '分销关系绑定模式'); |
|||
insert into system_dict_data(dict_type, label, value, sort, remark) |
|||
values ('brokerage_bind_mode', '没有推广人', 1, 1, '只要用户没有推广人,随时都可以绑定推广关系'), |
|||
('brokerage_bind_mode', '新用户', 2, 2, '仅新用户注册时才能绑定推广关系'), |
|||
('brokerage_bind_mode', '扫码覆盖', 3, 3, '如果用户已经有推广人,推广人会被变更'); |
|||
|
|||
insert into system_dict_type(type, name) |
|||
values ('brokerage_withdraw_type', '佣金提现类型'); |
|||
insert into system_dict_data(dict_type, label, value, sort) |
|||
values ('brokerage_withdraw_type', '钱包', 1, 1), |
|||
('brokerage_withdraw_type', '银行卡', 2, 2), |
|||
('brokerage_withdraw_type', '微信', 3, 3), |
|||
('brokerage_withdraw_type', '支付宝', 4, 4); |
|||
|
|||
insert into system_dict_type(type, name) |
|||
values ('brokerage_record_biz_type', '佣金记录业务类型'); |
|||
insert into system_dict_data(dict_type, label, value, sort) |
|||
values ('brokerage_record_biz_type', '订单返佣', 1, 1), |
|||
('brokerage_record_biz_type', '申请提现', 2, 2); |
|||
|
|||
insert into system_dict_type(type, name) |
|||
values ('brokerage_record_status', '佣金记录状态'); |
|||
insert into system_dict_data(dict_type, label, value, sort) |
|||
values ('brokerage_record_status', '待结算', 0, 0), |
|||
('brokerage_record_status', '已结算', 1, 1), |
|||
('brokerage_record_status', '已取消', 2, 2); |
|||
|
|||
insert into system_dict_type(type, name) |
|||
values ('brokerage_withdraw_status', '佣金提现状态'); |
|||
insert into system_dict_data(dict_type, label, value, sort) |
|||
values ('brokerage_withdraw_status', '审核中', 0, 0), |
|||
('brokerage_withdraw_status', '审核通过', 10, 10), |
|||
('brokerage_withdraw_status', '提现成功', 11, 11), |
|||
('brokerage_withdraw_status', '审核不通过', 20, 20), |
|||
('brokerage_withdraw_status', '提现失败', 21, 21); |
|||
|
|||
insert into system_dict_type(type, name) |
|||
values ('brokerage_bank_name', '佣金提现银行'); |
|||
insert into system_dict_data(dict_type, label, value, sort) |
|||
values ('brokerage_bank_name', '工商银行', 0, 0), |
|||
('brokerage_bank_name', '建设银行', 1, 1), |
|||
('brokerage_bank_name', '农业银行', 2, 2), |
|||
('brokerage_bank_name', '中国银行', 3, 3), |
|||
('brokerage_bank_name', '交通银行', 4, 4), |
|||
('brokerage_bank_name', '招商银行', 5, 5); |
|||
|
|||
|
|||
-- 交易中心配置:菜单 SQL |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status, component_name) |
|||
VALUES ('交易中心配置', '', 2, 0, 2072, 'config', 'ep:setting', 'trade/config/index', 0, 'TradeConfig'); |
|||
-- 按钮父菜单ID |
|||
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码 |
|||
SELECT @parentId := LAST_INSERT_ID(); |
|||
-- 按钮 SQL |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('交易中心配置查询', 'trade:config:query', 3, 1, @parentId, '', '', '', 0); |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('交易中心配置保存', 'trade:config:save', 3, 2, @parentId, '', '', '', 0); |
|||
|
|||
|
|||
-- 增加菜单:分销 |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status, component_name) |
|||
VALUES ('分销', '', 1, 5, 2072, 'brokerage', 'fa-solid:project-diagram', '', 0, ''); |
|||
-- 按钮父菜单ID |
|||
SELECT @brokerageMenuId := LAST_INSERT_ID(); |
|||
|
|||
-- 增加菜单:分销员 |
|||
-- 菜单 SQL |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status, component_name) |
|||
VALUES ('分销用户', '', 2, 0, @brokerageMenuId, 'brokerage-user', 'fa-solid:user-tie', 'trade/brokerage/user/index', 0, |
|||
'TradeBrokerageUser'); |
|||
-- 按钮父菜单ID |
|||
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码 |
|||
SELECT @parentId := LAST_INSERT_ID(); |
|||
-- 按钮 SQL |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('分销用户查询', 'trade:brokerage-user:query', 3, 1, @parentId, '', '', '', 0); |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('分销用户推广人查询', 'trade:brokerage-user:user-query', 3, 2, @parentId, '', '', '', 0); |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('分销用户推广订单查询', 'trade:brokerage-user:order-query', 3, 3, @parentId, '', '', '', 0); |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('分销用户修改推广资格', 'trade:brokerage-user:update-brokerage-enable', 3, 4, @parentId, '', '', '', 0); |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('分销用户修改推广员', 'trade:brokerage-user:update-brokerage-user', 3, 5, @parentId, '', '', '', 0); |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('分销用户清除推广员', 'trade:brokerage-user:clear-brokerage-user', 3, 6, @parentId, '', '', '', 0); |
|||
|
|||
-- 增加菜单:佣金记录 |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status, component_name) |
|||
VALUES ('佣金记录', '', 2, 1, @brokerageMenuId, 'brokerage-record', 'fa:money', 'trade/brokerage/record/index', 0, |
|||
'TradeBrokerageRecord'); |
|||
-- 按钮父菜单ID |
|||
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码 |
|||
SELECT @parentId := LAST_INSERT_ID(); |
|||
-- 按钮 SQL |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('佣金记录查询', 'trade:brokerage-record:query', 3, 1, @parentId, '', '', '', 0); |
|||
|
|||
-- 增加菜单:佣金提现 |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status, component_name) |
|||
VALUES ('佣金提现', '', 2, 2, @brokerageMenuId, 'brokerage-withdraw', 'fa:credit-card', |
|||
'trade/brokerage/withdraw/index', 0, 'TradeBrokerageWithdraw'); |
|||
-- 按钮父菜单ID |
|||
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码 |
|||
SELECT @parentId := LAST_INSERT_ID(); |
|||
-- 按钮 SQL |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('佣金提现查询', 'trade:brokerage-withdraw:query', 3, 1, @parentId, '', '', '', 0); |
|||
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status) |
|||
VALUES ('佣金提现审核', 'trade:brokerage-withdraw:audit', 3, 2, @parentId, '', '', '', 0); |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,3 @@ |
|||
ALTER TABLE `ruoyi-vue-pro`.`trade_after_sale_log` |
|||
ADD COLUMN `before_status` int NOT NULL COMMENT '售前状态' AFTER `id`, |
|||
ADD COLUMN `after_status` int NOT NULL COMMENT '售后状态' AFTER `before_status`; |
@ -0,0 +1,43 @@ |
|||
-- ---------------------------- |
|||
-- 会员钱包表 |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `pay_wallet`; |
|||
CREATE TABLE `pay_wallet` |
|||
( |
|||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', |
|||
`user_id` bigint NOT NULL COMMENT '用户编号', |
|||
`user_type` tinyint NOT NULL DEFAULT 0 COMMENT '用户类型', |
|||
`balance` int NOT NULL DEFAULT 0 COMMENT '余额,单位分', |
|||
`total_expense` int NOT NULL DEFAULT 0 COMMENT '累计支出,单位分', |
|||
`total_recharge` int NOT NULL DEFAULT 0 COMMENT '累计充值,单位分', |
|||
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', |
|||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
|||
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', |
|||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', |
|||
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', |
|||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', |
|||
PRIMARY KEY (`id`) USING BTREE |
|||
) ENGINE=InnoDB COMMENT='会员钱包表'; |
|||
|
|||
-- ---------------------------- |
|||
-- 会员钱包流水表 |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `pay_wallet_transaction`; |
|||
CREATE TABLE `pay_wallet_transaction` |
|||
( |
|||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', |
|||
`wallet_id` bigint NOT NULL COMMENT '会员钱包 id', |
|||
`biz_type` tinyint NOT NULL COMMENT '关联类型', |
|||
`biz_id` varchar(64) NOT NULL COMMENT '关联业务编号', |
|||
`no` varchar(64) NOT NULL COMMENT '流水号', |
|||
`title` varchar(128) NOT NULL COMMENT '流水标题', |
|||
`price` int NOT NULL COMMENT '交易金额, 单位分', |
|||
`balance` int NOT NULL COMMENT '余额, 单位分', |
|||
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', |
|||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
|||
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', |
|||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', |
|||
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', |
|||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', |
|||
PRIMARY KEY (`id`) USING BTREE |
|||
) ENGINE=InnoDB COMMENT='会员钱包流水表'; |
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,673 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-dependencies</artifactId> |
|||
<version>${revision}</version> |
|||
<packaging>pom</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>基础 bom 文件,管理整个项目的依赖版本</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<properties> |
|||
<revision>3.0.0</revision> |
|||
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version> |
|||
<!-- 统一依赖管理 --> |
|||
<spring.boot.version>2.7.15</spring.boot.version> |
|||
<!-- Web 相关 --> |
|||
<springdoc.version>1.7.0</springdoc.version> |
|||
<knife4j.version>4.3.0</knife4j.version> |
|||
<servlet.versoin>2.5</servlet.versoin> |
|||
<!-- DB 相关 --> |
|||
<druid.version>1.2.19</druid.version> |
|||
<mybatis-plus.version>3.5.3.2</mybatis-plus.version> |
|||
<mybatis-plus-generator.version>3.5.3.2</mybatis-plus-generator.version> |
|||
<dynamic-datasource.version>3.3.2</dynamic-datasource.version> |
|||
<mybatis-plus-join.version>1.4.6</mybatis-plus-join.version> |
|||
<shardingsphere.version>5.1.1</shardingsphere.version> |
|||
<redisson.version>3.18.0</redisson.version> |
|||
<dm8.jdbc.version>8.1.2.141</dm8.jdbc.version> |
|||
<!-- 服务保障相关 --> |
|||
<lock4j.version>2.2.3</lock4j.version> |
|||
<resilience4j.version>1.7.1</resilience4j.version> |
|||
<!-- 监控相关 --> |
|||
<skywalking.version>8.12.0</skywalking.version> |
|||
<spring-boot-admin.version>2.7.10</spring-boot-admin.version> |
|||
<opentracing.version>0.33.0</opentracing.version> |
|||
<!-- Test 测试相关 --> |
|||
<podam.version>7.2.11.RELEASE</podam.version> |
|||
<jedis-mock.version>1.0.7</jedis-mock.version> |
|||
<mockito-inline.version>4.11.0</mockito-inline.version> |
|||
<!-- Bpm 工作流相关 --> |
|||
<flowable.version>6.8.0</flowable.version> |
|||
<!-- 工具类相关 --> |
|||
<captcha-plus.version>1.0.7</captcha-plus.version> |
|||
<jsoup.version>1.15.4</jsoup.version> |
|||
<lombok.version>1.18.28</lombok.version> |
|||
<mapstruct.version>1.5.5.Final</mapstruct.version> |
|||
<hutool.version>5.8.21</hutool.version> |
|||
<easyexcel.verion>3.3.2</easyexcel.verion> |
|||
<velocity.version>2.3</velocity.version> |
|||
<screw.version>1.0.5</screw.version> |
|||
<fastjson.version>1.2.83</fastjson.version> |
|||
<guava.version>32.0.1-jre</guava.version> |
|||
<guice.version>5.1.0</guice.version> |
|||
<transmittable-thread-local.version>2.14.2</transmittable-thread-local.version> |
|||
<commons-net.version>3.9.0</commons-net.version> |
|||
<jsch.version>0.1.55</jsch.version> |
|||
<tika-core.version>2.7.0</tika-core.version> |
|||
<ip2region.version>2.7.0</ip2region.version> |
|||
<!-- 三方云服务相关 --> |
|||
<okio.version>3.0.0</okio.version> |
|||
<okhttp3.version>4.10.0</okhttp3.version> |
|||
<commons-io.version>2.11.0</commons-io.version> |
|||
<minio.version>8.5.5</minio.version> |
|||
<aliyun-java-sdk-core.version>4.6.3</aliyun-java-sdk-core.version> |
|||
<aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version> |
|||
<tencentcloud-sdk-java.version>3.1.758</tencentcloud-sdk-java.version> |
|||
<jimureport.version>1.6.1</jimureport.version> |
|||
<xercesImpl.version>2.12.2</xercesImpl.version> |
|||
<kaptcha.version>2.3.3</kaptcha.version> |
|||
<magic-api.version>2.1.1</magic-api.version> |
|||
</properties> |
|||
|
|||
<dependencyManagement> |
|||
<dependencies> |
|||
<!-- 统一依赖管理 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-dependencies</artifactId> |
|||
<version>${spring.boot.version}</version> |
|||
<type>pom</type> |
|||
<scope>import</scope> |
|||
</dependency> |
|||
|
|||
<!-- 业务组件 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-banner</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-operatelog</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-trade</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-dict</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-sms</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-pay</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-tenant</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-data-permission</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-error-code</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-ip</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-captcha</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-desensitize</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<!-- Spring 核心 --> |
|||
<dependency> |
|||
<!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 --> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-configuration-processor</artifactId> |
|||
<version>${spring.boot.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- Web 相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-web</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-security</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.github.xiaoymin</groupId> |
|||
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId> |
|||
<version>${knife4j.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.springdoc</groupId> |
|||
<artifactId>springdoc-openapi-ui</artifactId> |
|||
<version>${springdoc.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- DB 相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-mybatis</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>druid-spring-boot-starter</artifactId> |
|||
<version>${druid.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.baomidou</groupId> |
|||
<artifactId>mybatis-plus-boot-starter</artifactId> |
|||
<version>${mybatis-plus.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.baomidou</groupId> |
|||
<artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 --> |
|||
<version>${mybatis-plus-generator.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.baomidou</groupId> |
|||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 --> |
|||
<version>${dynamic-datasource.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.apache.shardingsphere</groupId> |
|||
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> |
|||
<version>${shardingsphere.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.github.yulichang</groupId> |
|||
<artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 --> |
|||
<version>${mybatis-plus-join.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-redis</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.redisson</groupId> |
|||
<artifactId>redisson-spring-boot-starter</artifactId> |
|||
<version>${redisson.version}</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-actuator</artifactId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.dameng</groupId> |
|||
<artifactId>DmJdbcDriver18</artifactId> |
|||
<version>${dm8.jdbc.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- Job 定时任务相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-job</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<!-- 消息队列相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-mq</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<!-- 服务保障相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-protection</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.baomidou</groupId> |
|||
<artifactId>lock4j-redisson-spring-boot-starter</artifactId> |
|||
<version>${lock4j.version}</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<artifactId>redisson-spring-boot-starter</artifactId> |
|||
<groupId>org.redisson</groupId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>io.github.resilience4j</groupId> |
|||
<artifactId>resilience4j-ratelimiter</artifactId> |
|||
<version>${resilience4j.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>io.github.resilience4j</groupId> |
|||
<artifactId>resilience4j-spring-boot2</artifactId> |
|||
<version>${resilience4j.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 监控相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-monitor</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.apache.skywalking</groupId> |
|||
<artifactId>apm-toolkit-trace</artifactId> |
|||
<version>${skywalking.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.apache.skywalking</groupId> |
|||
<artifactId>apm-toolkit-logback-1.x</artifactId> |
|||
<version>${skywalking.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.apache.skywalking</groupId> |
|||
<artifactId>apm-toolkit-opentracing</artifactId> |
|||
<version>${skywalking.version}</version> |
|||
<!-- <exclusions>--> |
|||
<!-- <exclusion>--> |
|||
<!-- <artifactId>opentracing-api</artifactId>--> |
|||
<!-- <groupId>io.opentracing</groupId>--> |
|||
<!-- </exclusion>--> |
|||
<!-- <exclusion>--> |
|||
<!-- <artifactId>opentracing-util</artifactId>--> |
|||
<!-- <groupId>io.opentracing</groupId>--> |
|||
<!-- </exclusion>--> |
|||
<!-- </exclusions>--> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>io.opentracing</groupId> |
|||
<artifactId>opentracing-api</artifactId> |
|||
<version>${opentracing.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>io.opentracing</groupId> |
|||
<artifactId>opentracing-util</artifactId> |
|||
<version>${opentracing.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>io.opentracing</groupId> |
|||
<artifactId>opentracing-noop</artifactId> |
|||
<version>${opentracing.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>de.codecentric</groupId> |
|||
<artifactId>spring-boot-admin-starter-server</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 --> |
|||
<version>${spring-boot-admin.version}</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<groupId>de.codecentric</groupId> |
|||
<artifactId>spring-boot-admin-server-cloud</artifactId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>de.codecentric</groupId> |
|||
<artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 --> |
|||
<version>${spring-boot-admin.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- Test 测试相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-test</artifactId> |
|||
<version>${revision}</version> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.mockito</groupId> |
|||
<artifactId>mockito-inline</artifactId> |
|||
<version>${mockito-inline.version}</version> <!-- 支持 Mockito 的 final 类与 static 方法的 mock --> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-test</artifactId> |
|||
<version>${spring.boot.version}</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<artifactId>asm</artifactId> |
|||
<groupId>org.ow2.asm</groupId> |
|||
</exclusion> |
|||
<exclusion> |
|||
<groupId>org.mockito</groupId> |
|||
<artifactId>mockito-core</artifactId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 --> |
|||
<artifactId>jedis-mock</artifactId> |
|||
<version>${jedis-mock.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 --> |
|||
<artifactId>podam</artifactId> |
|||
<version>${podam.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 工作流相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-flowable</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.flowable</groupId> |
|||
<artifactId>flowable-spring-boot-starter-process</artifactId> |
|||
<version>${flowable.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.flowable</groupId> |
|||
<artifactId>flowable-spring-boot-starter-actuator</artifactId> |
|||
<version>${flowable.version}</version> |
|||
</dependency> |
|||
<!-- 工作流相关结束 --> |
|||
|
|||
<!-- 工具类相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-common</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-excel</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<version>${lombok.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.mapstruct</groupId> |
|||
<artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher --> |
|||
<version>${mapstruct.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.mapstruct</groupId> |
|||
<artifactId>mapstruct-jdk8</artifactId> |
|||
<version>${mapstruct.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.mapstruct</groupId> |
|||
<artifactId>mapstruct-processor</artifactId> |
|||
<version>${mapstruct.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>cn.hutool</groupId> |
|||
<artifactId>hutool-all</artifactId> |
|||
<version>${hutool.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>easyexcel</artifactId> |
|||
<version>${easyexcel.verion}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>commons-io</groupId> |
|||
<artifactId>commons-io</artifactId> |
|||
<version>${commons-io.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.apache.tika</groupId> |
|||
<artifactId>tika-core</artifactId> <!-- 文件类型的识别 --> |
|||
<version>${tika-core.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.apache.velocity</groupId> |
|||
<artifactId>velocity-engine-core</artifactId> |
|||
<version>${velocity.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>fastjson</artifactId> |
|||
<version>${fastjson.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>cn.smallbun.screw</groupId> |
|||
<artifactId>screw-core</artifactId> <!-- 实现数据库文档 --> |
|||
<version>${screw.version}</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<groupId>org.freemarker</groupId> |
|||
<artifactId>freemarker</artifactId> <!-- 移除 Freemarker 依赖,采用 Velocity 作为模板引擎 --> |
|||
</exclusion> |
|||
<exclusion> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>fastjson</artifactId> <!-- 最新版screw-core1.0.5依赖fastjson1.2.73存在漏洞,移除。 --> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.google.guava</groupId> |
|||
<artifactId>guava</artifactId> |
|||
<version>${guava.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.google.inject</groupId> |
|||
<artifactId>guice</artifactId> |
|||
<version>${guice.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 --> |
|||
<version>${transmittable-thread-local.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>commons-net</groupId> |
|||
<artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 --> |
|||
<version>${commons-net.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.jcraft</groupId> |
|||
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 --> |
|||
<version>${jsch.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.xingyuv</groupId> |
|||
<artifactId>spring-boot-starter-captcha-plus</artifactId> |
|||
<version>${captcha-plus.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.lionsoul</groupId> |
|||
<artifactId>ip2region</artifactId> |
|||
<version>${ip2region.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.jsoup</groupId> |
|||
<artifactId>jsoup</artifactId> |
|||
<version>${jsoup.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 验证码 --> |
|||
<dependency> |
|||
<groupId>pro.fessional</groupId> |
|||
<artifactId>kaptcha</artifactId> |
|||
<version>${kaptcha.version}</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<groupId>javax.servlet</groupId> |
|||
<artifactId>servlet-api</artifactId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
|
|||
<!-- 三方云服务相关 --> |
|||
<dependency> |
|||
<groupId>com.squareup.okio</groupId> |
|||
<artifactId>okio</artifactId> |
|||
<version>${okio.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.squareup.okhttp3</groupId> |
|||
<artifactId>okhttp</artifactId> |
|||
<version>${okhttp3.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-file</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>io.minio</groupId> |
|||
<artifactId>minio</artifactId> |
|||
<version>${minio.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- SMS SDK begin --> |
|||
<dependency> |
|||
<groupId>com.aliyun</groupId> |
|||
<artifactId>aliyun-java-sdk-core</artifactId> |
|||
<version>${aliyun-java-sdk-core.version}</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<artifactId>opentracing-api</artifactId> |
|||
<groupId>io.opentracing</groupId> |
|||
</exclusion> |
|||
<exclusion> |
|||
<artifactId>opentracing-util</artifactId> |
|||
<groupId>io.opentracing</groupId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.aliyun</groupId> |
|||
<artifactId>aliyun-java-sdk-dysmsapi</artifactId> |
|||
<version>${aliyun-java-sdk-dysmsapi.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.tencentcloudapi</groupId> |
|||
<artifactId>tencentcloud-sdk-java-sms</artifactId> |
|||
<version>${tencentcloud-sdk-java.version}</version> |
|||
</dependency> |
|||
<!-- SMS SDK end --> |
|||
|
|||
<!-- 积木报表--> |
|||
<dependency> |
|||
<groupId>org.jeecgframework.jimureport</groupId> |
|||
<artifactId>jimureport-spring-boot-starter</artifactId> |
|||
<version>${jimureport.version}</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>druid</artifactId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>xerces</groupId> |
|||
<artifactId>xercesImpl</artifactId> |
|||
<version>${xercesImpl.version}</version> |
|||
</dependency> |
|||
<!-- SpringBoot Websocket --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-websocket</artifactId> |
|||
<version>${spring.boot.version}</version> |
|||
</dependency> |
|||
<!-- magic-api --> |
|||
<dependency> |
|||
<groupId>org.ssssssss</groupId> |
|||
<artifactId>magic-api-spring-boot-starter</artifactId> |
|||
<version>${magic-api.version}</version> |
|||
</dependency> |
|||
</dependencies> |
|||
</dependencyManagement> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<!-- 统一 revision 版本 --> |
|||
<plugin> |
|||
<groupId>org.codehaus.mojo</groupId> |
|||
<artifactId>flatten-maven-plugin</artifactId> |
|||
<version>${flatten-maven-plugin.version}</version> |
|||
<configuration> |
|||
<flattenMode>resolveCiFriendliesOnly</flattenMode> |
|||
<updatePomFile>true</updatePomFile> |
|||
</configuration> |
|||
<executions> |
|||
<execution> |
|||
<goals> |
|||
<goal>flatten</goal> |
|||
</goals> |
|||
<id>flatten</id> |
|||
<phase>process-resources</phase> |
|||
</execution> |
|||
<execution> |
|||
<goals> |
|||
<goal>clean</goal> |
|||
</goals> |
|||
<id>flatten.clean</id> |
|||
<phase>clean</phase> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
|
|||
</project> |
@ -0,0 +1,51 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<parent> |
|||
<artifactId>win</artifactId> |
|||
<groupId>com.win</groupId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<packaging>pom</packaging> |
|||
<modules> |
|||
<module>win-common</module> |
|||
<module>win-spring-boot-starter-banner</module> |
|||
<module>win-spring-boot-starter-mybatis</module> |
|||
<module>win-spring-boot-starter-redis</module> |
|||
<module>win-spring-boot-starter-web</module> |
|||
<module>win-spring-boot-starter-security</module> |
|||
<module>win-spring-boot-starter-file</module> |
|||
<module>win-spring-boot-starter-monitor</module> |
|||
<module>win-spring-boot-starter-protection</module> |
|||
<module>win-spring-boot-starter-job</module> |
|||
<module>win-spring-boot-starter-mq</module> |
|||
<module>win-spring-boot-starter-excel</module> |
|||
<module>win-spring-boot-starter-test</module> |
|||
<module>win-spring-boot-starter-biz-operatelog</module> |
|||
<module>win-spring-boot-starter-biz-dict</module> |
|||
<module>win-spring-boot-starter-biz-sms</module> |
|||
<module>win-spring-boot-starter-biz-tenant</module> |
|||
<module>win-spring-boot-starter-biz-data-permission</module> |
|||
<module>win-spring-boot-starter-biz-error-code</module> |
|||
<module>win-spring-boot-starter-biz-ip</module> |
|||
<module>win-spring-boot-starter-flowable</module> |
|||
<module>win-spring-boot-starter-captcha</module> |
|||
<module>win-spring-boot-starter-websocket</module> |
|||
<module>win-spring-boot-starter-desensitize</module> |
|||
</modules> |
|||
|
|||
<artifactId>win-framework</artifactId> |
|||
<description> |
|||
该包是技术组件,每个子包,代表一个组件。每个组件包括两部分: |
|||
1. core 包:是该组件的核心封装 |
|||
2. config 包:是该组件基于 Spring 的配置 |
|||
技术组件,也分成两类: |
|||
1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展 |
|||
2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。 |
|||
如果是业务组件,Maven 名字会包含 biz |
|||
</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
</project> |
@ -0,0 +1,144 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-framework</artifactId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>win-common</artifactId> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>定义基础 pojo 类、枚举、工具类等等</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<dependencies> |
|||
<!-- Spring 核心 --> |
|||
<dependency> |
|||
<groupId>org.springframework</groupId> |
|||
<artifactId>spring-core</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.springframework</groupId> |
|||
<artifactId>spring-expression</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.springframework</groupId> |
|||
<artifactId>spring-aop</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.aspectj</groupId> |
|||
<artifactId>aspectjweaver</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 --> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-configuration-processor</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
|
|||
<!-- Web 相关 --> |
|||
<dependency> |
|||
<groupId>org.springframework</groupId> |
|||
<artifactId>spring-web</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>jakarta.servlet</groupId> |
|||
<artifactId>jakarta.servlet-api</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springdoc</groupId> |
|||
<artifactId>springdoc-openapi-ui</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 --> |
|||
</dependency> |
|||
|
|||
<!-- 监控相关 --> |
|||
<dependency> |
|||
<groupId>org.apache.skywalking</groupId> |
|||
<artifactId>apm-toolkit-trace</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 工具类相关 --> |
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.mapstruct</groupId> |
|||
<artifactId>mapstruct</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.mapstruct</groupId> |
|||
<artifactId>mapstruct-jdk8</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher --> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.mapstruct</groupId> |
|||
<artifactId>mapstruct-processor</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.google.guava</groupId> |
|||
<artifactId>guava</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.fasterxml.jackson.core</groupId> |
|||
<artifactId>jackson-databind</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.fasterxml.jackson.core</groupId> |
|||
<artifactId>jackson-core</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.fasterxml.jackson.datatype</groupId> |
|||
<artifactId>jackson-datatype-jsr310</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.slf4j</groupId> |
|||
<artifactId>slf4j-api</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>jakarta.validation</groupId> |
|||
<artifactId>jakarta.validation-api</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 --> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>cn.hutool</groupId> |
|||
<artifactId>hutool-all</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>transmittable-thread-local</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Test 测试相关 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-test</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -0,0 +1,15 @@ |
|||
package com.win.framework.common.core; |
|||
|
|||
/** |
|||
* 可生成 Int 数组的接口 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
public interface IntArrayValuable { |
|||
|
|||
/** |
|||
* @return int 数组 |
|||
*/ |
|||
int[] array(); |
|||
|
|||
} |
@ -0,0 +1,20 @@ |
|||
package com.win.framework.common.core; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* Key Value 的键值对 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class KeyValue<K, V> { |
|||
|
|||
private K key; |
|||
private V value; |
|||
|
|||
} |
@ -0,0 +1,56 @@ |
|||
package com.win.framework.common.enums; |
|||
|
|||
import com.win.framework.common.core.IntArrayValuable; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
|
|||
import java.util.Arrays; |
|||
import java.util.stream.Stream; |
|||
|
|||
/** |
|||
* 通用状态枚举 |
|||
* |
|||
* @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 CommonStatusEnum convert(Integer value) { |
|||
return Stream.of(values()) |
|||
.filter(bean -> bean.status.equals(value)) |
|||
.findAny() |
|||
.orElse(DISABLE); |
|||
} |
|||
|
|||
public static CommonStatusEnum convert(String description) { |
|||
return Stream.of(values()) |
|||
.filter(bean -> bean.name.equals(description)) |
|||
.findAny() |
|||
.orElse(DISABLE); |
|||
} |
|||
|
|||
public static String[] getStatusNameArray() { |
|||
return Stream.of(values()).map(CommonStatusEnum::getName).toArray(String[]::new); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,21 @@ |
|||
package com.win.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; |
|||
|
|||
} |
@ -0,0 +1,34 @@ |
|||
package com.win.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; |
|||
|
|||
} |
@ -0,0 +1,32 @@ |
|||
package com.win.framework.common.exception; |
|||
|
|||
import com.win.framework.common.exception.enums.GlobalErrorCodeConstants; |
|||
import com.win.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; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,60 @@ |
|||
package com.win.framework.common.exception; |
|||
|
|||
import com.win.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; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,60 @@ |
|||
package com.win.framework.common.exception; |
|||
|
|||
import com.win.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; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,40 @@ |
|||
package com.win.framework.common.exception.enums; |
|||
|
|||
import com.win.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 REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
|
|||
ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); |
|||
|
|||
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); |
|||
|
|||
} |
@ -0,0 +1,43 @@ |
|||
package com.win.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)
|
|||
// 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
|
|||
// 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
|
|||
// 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
|
|||
// 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
|
|||
|
|||
} |
@ -0,0 +1,128 @@ |
|||
package com.win.framework.common.exception.util; |
|||
|
|||
import com.win.framework.common.exception.ErrorCode; |
|||
import com.win.framework.common.exception.ServiceException; |
|||
import com.win.framework.common.exception.enums.GlobalErrorCodeConstants; |
|||
import com.google.common.annotations.VisibleForTesting; |
|||
import com.win.framework.common.util.i18n.MessageUtil; |
|||
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<Integer, String> MESSAGES = new ConcurrentHashMap<>(); |
|||
|
|||
public static void putAll(Map<Integer, String> 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, MessageUtil.message(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(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,112 @@ |
|||
package com.win.framework.common.pojo; |
|||
|
|||
import com.win.framework.common.exception.ErrorCode; |
|||
import com.win.framework.common.exception.ServiceException; |
|||
import com.win.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 <T> 数据泛型 |
|||
*/ |
|||
@Data |
|||
public class CommonResult<T> 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 <T> 返回的泛型 |
|||
* @return 新的 CommonResult 对象 |
|||
*/ |
|||
public static <T> CommonResult<T> error(CommonResult<?> result) { |
|||
return error(result.getCode(), result.getMsg()); |
|||
} |
|||
|
|||
public static <T> CommonResult<T> error(Integer code, String message) { |
|||
Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); |
|||
CommonResult<T> result = new CommonResult<>(); |
|||
result.code = code; |
|||
result.msg = message; |
|||
return result; |
|||
} |
|||
|
|||
public static <T> CommonResult<T> error(ErrorCode errorCode) { |
|||
return error(errorCode.getCode(), errorCode.getMsg()); |
|||
} |
|||
|
|||
public static <T> CommonResult<T> success(T data) { |
|||
CommonResult<T> 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 <T> CommonResult<T> error(ServiceException serviceException) { |
|||
return error(serviceException.getCode(), serviceException.getMessage()); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,32 @@ |
|||
package com.win.framework.common.pojo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Data |
|||
public class CustomConditions extends PageParam { |
|||
|
|||
/** |
|||
* 自定义条件 |
|||
*/ |
|||
private List<Condition> filters; |
|||
|
|||
@Data |
|||
public static class Condition { |
|||
|
|||
/** |
|||
* 类型,==,!=,>,<,>=,<=,like,in,notIn,betweeen,isNull,isNotNull |
|||
*/ |
|||
private String action; |
|||
/** |
|||
* 字段 |
|||
*/ |
|||
private String column; |
|||
/** |
|||
* 值 |
|||
*/ |
|||
private String value; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,64 @@ |
|||
package com.win.framework.common.pojo; |
|||
|
|||
import com.google.common.base.CaseFormat; |
|||
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; |
|||
|
|||
@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; |
|||
|
|||
/** |
|||
* 顺序 - 升序 |
|||
*/ |
|||
public static final String ORDER_ASC = "ASC"; |
|||
|
|||
/** |
|||
* 顺序 - 降序 |
|||
*/ |
|||
public static final String ORDER_DESC = "DESC"; |
|||
|
|||
/** |
|||
* 字段 |
|||
*/ |
|||
@Schema(description = "排序属性", requiredMode = Schema.RequiredMode.REQUIRED, example = "userName") |
|||
private String sort; |
|||
|
|||
/** |
|||
* 顺序 |
|||
*/ |
|||
@Schema(description = "排序类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "DESC") |
|||
private String by; |
|||
|
|||
public void setSort(String sort) { |
|||
if(sort != null) { |
|||
this.sort = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, sort); |
|||
} |
|||
} |
|||
|
|||
public void setBy(String by) { |
|||
if(by != null) { |
|||
this.by = by.toUpperCase(); |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,41 @@ |
|||
package com.win.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<T> implements Serializable { |
|||
|
|||
@Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private List<T> list; |
|||
|
|||
@Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private Long total; |
|||
|
|||
public PageResult() { |
|||
} |
|||
|
|||
public PageResult(List<T> list, Long total) { |
|||
this.list = list; |
|||
this.total = total; |
|||
} |
|||
|
|||
public PageResult(Long total) { |
|||
this.list = new ArrayList<>(); |
|||
this.total = total; |
|||
} |
|||
|
|||
public static <T> PageResult<T> empty() { |
|||
return new PageResult<>(0L); |
|||
} |
|||
|
|||
public static <T> PageResult<T> empty(Long total) { |
|||
return new PageResult<>(total); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,25 @@ |
|||
package com.win.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 <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) { |
|||
return CacheBuilder.newBuilder() |
|||
// 只阻塞当前数据加载线程,其他线程返回旧值
|
|||
.refreshAfterWrite(duration) |
|||
// 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
|
|||
.build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,58 @@ |
|||
package com.win.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.win.framework.common.util.collection.CollectionUtils.convertList; |
|||
|
|||
/** |
|||
* Array 工具类 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
public class ArrayUtils { |
|||
|
|||
/** |
|||
* 将 object 和 newElements 合并成一个数组 |
|||
* |
|||
* @param object 对象 |
|||
* @param newElements 数组 |
|||
* @param <T> 泛型 |
|||
* @return 结果数组 |
|||
*/ |
|||
@SafeVarargs |
|||
public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) { |
|||
if (object == null) { |
|||
return newElements; |
|||
} |
|||
Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); |
|||
result[0] = object; |
|||
System.arraycopy(newElements, 0, result, 1, newElements.length); |
|||
return result; |
|||
} |
|||
|
|||
public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) { |
|||
return toArray(convertList(from, mapper)); |
|||
} |
|||
|
|||
@SuppressWarnings("unchecked") |
|||
public static <T> T[] toArray(Collection<T> from) { |
|||
if (CollectionUtil.isEmpty(from)) { |
|||
return (T[]) (new Object[0]); |
|||
} |
|||
return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator())); |
|||
} |
|||
|
|||
public static <T> T get(T[] array, int index) { |
|||
if (null == array || index >= array.length) { |
|||
return null; |
|||
} |
|||
return array[index]; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,247 @@ |
|||
package com.win.framework.common.util.collection; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.collection.CollectionUtil; |
|||
import com.google.common.collect.ImmutableMap; |
|||
|
|||
import java.util.*; |
|||
import java.util.function.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
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 <T> boolean anyMatch(Collection<T> from, Predicate<T> predicate) { |
|||
return from.stream().anyMatch(predicate); |
|||
} |
|||
|
|||
public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new ArrayList<>(); |
|||
} |
|||
return from.stream().filter(predicate).collect(Collectors.toList()); |
|||
} |
|||
|
|||
public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new ArrayList<>(); |
|||
} |
|||
return distinct(from, keyMapper, (t1, t2) -> t1); |
|||
} |
|||
|
|||
public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new ArrayList<>(); |
|||
} |
|||
return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); |
|||
} |
|||
|
|||
public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new ArrayList<>(); |
|||
} |
|||
return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); |
|||
} |
|||
|
|||
public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new ArrayList<>(); |
|||
} |
|||
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); |
|||
} |
|||
|
|||
public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new HashSet<>(); |
|||
} |
|||
return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); |
|||
} |
|||
|
|||
public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new HashSet<>(); |
|||
} |
|||
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); |
|||
} |
|||
|
|||
public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new HashMap<>(); |
|||
} |
|||
return convertMap(from, keyFunc, Function.identity()); |
|||
} |
|||
|
|||
public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return supplier.get(); |
|||
} |
|||
return convertMap(from, keyFunc, Function.identity(), supplier); |
|||
} |
|||
|
|||
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new HashMap<>(); |
|||
} |
|||
return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); |
|||
} |
|||
|
|||
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new HashMap<>(); |
|||
} |
|||
return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); |
|||
} |
|||
|
|||
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return supplier.get(); |
|||
} |
|||
return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); |
|||
} |
|||
|
|||
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new HashMap<>(); |
|||
} |
|||
return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); |
|||
} |
|||
|
|||
public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new HashMap<>(); |
|||
} |
|||
return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); |
|||
} |
|||
|
|||
public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new HashMap<>(); |
|||
} |
|||
return from.stream() |
|||
.collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); |
|||
} |
|||
|
|||
// 暂时没想好名字,先以 2 结尾噶
|
|||
public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return new HashMap<>(); |
|||
} |
|||
return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); |
|||
} |
|||
|
|||
public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return Collections.emptyMap(); |
|||
} |
|||
ImmutableMap.Builder<K, T> 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 <T> List<List<T>> diffList(Collection<T> oldList, Collection<T> newList, |
|||
BiFunction<T, T, Boolean> sameFunc) { |
|||
List<T> createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除
|
|||
List<T> updateList = new ArrayList<>(); |
|||
List<T> deleteList = new ArrayList<>(); |
|||
|
|||
// 通过以 oldList 为主遍历,找出 updateList 和 deleteList
|
|||
for (T oldObj : oldList) { |
|||
// 1. 寻找是否有匹配的
|
|||
T foundObj = null; |
|||
for (Iterator<T> 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> T getFirst(List<T> from) { |
|||
return !CollectionUtil.isEmpty(from) ? from.get(0) : null; |
|||
} |
|||
|
|||
public static <T> T findFirst(List<T> from, Predicate<T> predicate) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return null; |
|||
} |
|||
return from.stream().filter(predicate).findFirst().orElse(null); |
|||
} |
|||
|
|||
public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return null; |
|||
} |
|||
assert from.size() > 0; // 断言,避免告警
|
|||
T t = from.stream().max(Comparator.comparing(valueFunc)).get(); |
|||
return valueFunc.apply(t); |
|||
} |
|||
|
|||
public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return null; |
|||
} |
|||
assert from.size() > 0; // 断言,避免告警
|
|||
T t = from.stream().min(Comparator.comparing(valueFunc)).get(); |
|||
return valueFunc.apply(t); |
|||
} |
|||
|
|||
public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc, BinaryOperator<V> accumulator) { |
|||
if (CollUtil.isEmpty(from)) { |
|||
return null; |
|||
} |
|||
assert from.size() > 0; // 断言,避免告警
|
|||
return from.stream().map(valueFunc).reduce(accumulator).get(); |
|||
} |
|||
|
|||
public static <T> void addIfNotNull(Collection<T> coll, T item) { |
|||
if (item == null) { |
|||
return; |
|||
} |
|||
coll.add(item); |
|||
} |
|||
|
|||
public static <T> Collection<T> singleton(T deptId) { |
|||
return deptId == null ? Collections.emptyList() : Collections.singleton(deptId); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,66 @@ |
|||
package com.win.framework.common.util.collection; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.collection.CollectionUtil; |
|||
import com.win.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 <K, V> List<V> getList(Multimap<K, V> multimap, Collection<K> keys) { |
|||
List<V> result = new ArrayList<>(); |
|||
keys.forEach(k -> { |
|||
Collection<V> 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 <K, V> void findAndThen(Map<K, V> map, K key, Consumer<V> consumer) { |
|||
if (CollUtil.isEmpty(map)) { |
|||
return; |
|||
} |
|||
V value = map.get(key); |
|||
if (value == null) { |
|||
return; |
|||
} |
|||
consumer.accept(value); |
|||
} |
|||
|
|||
public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) { |
|||
Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); |
|||
keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); |
|||
return map; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,19 @@ |
|||
package com.win.framework.common.util.collection; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
|
|||
import java.util.Set; |
|||
|
|||
/** |
|||
* Set 工具类 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
public class SetUtils { |
|||
|
|||
@SafeVarargs |
|||
public static <T> Set<T> asSet(T... objs) { |
|||
return CollUtil.newHashSet(objs); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,180 @@ |
|||
package com.win.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 日历字段.<br/>eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,<br/>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()); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,80 @@ |
|||
package com.win.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; |
|||
|
|||
/** |
|||
* 时间工具类,用于 {@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 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 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)); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,126 @@ |
|||
package com.win.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<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>) |
|||
ReflectUtil.getFieldValue(builder.getQuery(), "query"); |
|||
query.remove(key); |
|||
// 后添加
|
|||
builder.addQuery(key, value); |
|||
return builder.build(); |
|||
} |
|||
|
|||
private String append(String base, Map<String, ?> 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<String, ?> query, Map<String, String> 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; |
|||
} |
|||
|
|||
|
|||
} |
@ -0,0 +1,26 @@ |
|||
package com.win.framework.common.util.i18n; |
|||
|
|||
import cn.hutool.extra.spring.SpringUtil; |
|||
import org.springframework.context.MessageSource; |
|||
import org.springframework.context.NoSuchMessageException; |
|||
import org.springframework.context.i18n.LocaleContextHolder; |
|||
|
|||
public class MessageUtil { |
|||
|
|||
/** |
|||
* 根据消息键和参数 获取消息 委托给spring messageSource,获取不到则返回code |
|||
* |
|||
* @param code 消息键 |
|||
* @param args 参数 |
|||
* @return 获取国际化翻译值 |
|||
*/ |
|||
public static String message(String code, Object... args) { |
|||
MessageSource messageSource = SpringUtil.getBean(MessageSource.class); |
|||
try { |
|||
return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); |
|||
} catch (NoSuchMessageException ignored) { |
|||
return code; |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,84 @@ |
|||
package com.win.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)); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,28 @@ |
|||
package com.win.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)); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,157 @@ |
|||
package com.win.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.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.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
|
|||
} |
|||
|
|||
/** |
|||
* 初始化 objectMapper 属性 |
|||
* <p> |
|||
* 通过这样的方式,使用 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> T parseObject(String text, Class<T> 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> 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> T parseObject2(String text, Class<T> clazz) { |
|||
if (StrUtil.isEmpty(text)) { |
|||
return null; |
|||
} |
|||
return JSONUtil.toBean(text, clazz); |
|||
} |
|||
|
|||
public static <T> T parseObject(byte[] bytes, Class<T> 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> T parseObject(String text, TypeReference<T> typeReference) { |
|||
try { |
|||
return objectMapper.readValue(text, typeReference); |
|||
} catch (IOException e) { |
|||
log.error("json parse err,json:{}", text, e); |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
|
|||
public static <T> List<T> parseArray(String text, Class<T> 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 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); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,30 @@ |
|||
package com.win.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(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,50 @@ |
|||
package com.win.framework.common.util.number; |
|||
|
|||
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
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,16 @@ |
|||
package com.win.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; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,63 @@ |
|||
package com.win.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> T cloneIgnoreId(T object, Consumer<T> 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 extends Comparable<T>> 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> T defaultIfNull(T... array) { |
|||
for (T item : array) { |
|||
if (item != null) { |
|||
return item; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
@SafeVarargs |
|||
public static <T> boolean equalsAny(T obj, T... array) { |
|||
return Arrays.asList(array).contains(obj); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,16 @@ |
|||
package com.win.framework.common.util.object; |
|||
|
|||
import com.win.framework.common.pojo.PageParam; |
|||
|
|||
/** |
|||
* {@link com.win.framework.common.pojo.PageParam} 工具类 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
public class PageUtils { |
|||
|
|||
public static int getStart(PageParam pageParam) { |
|||
return (pageParam.getPageNo() - 1) * pageParam.getPageSize(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,110 @@ |
|||
package com.win.framework.common.util.servlet; |
|||
|
|||
import cn.hutool.core.io.IoUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.hutool.extra.servlet.ServletUtil; |
|||
import com.win.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); |
|||
} |
|||
|
|||
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<String, String> getParamMap(HttpServletRequest request) { |
|||
return ServletUtil.getParamMap(request); |
|||
} |
|||
} |
@ -0,0 +1,46 @@ |
|||
package com.win.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(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,133 @@ |
|||
package com.win.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.ProceedingJoinPoint; |
|||
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 { |
|||
|
|||
/** |
|||
* spel表达式解析器 |
|||
*/ |
|||
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(ProceedingJoinPoint joinPoint, String expressionString) { |
|||
Map<String, Object> result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); |
|||
return result.get(expressionString); |
|||
} |
|||
|
|||
/** |
|||
* 从切面中,批量解析 EL 表达式的结果 |
|||
* |
|||
* @param joinPoint 切面点 |
|||
* @param expressionStrings EL 表达式数组 |
|||
* @return 结果,key 为表达式,value 为对应值 |
|||
*/ |
|||
public static Map<String, Object> parseExpressions(ProceedingJoinPoint joinPoint, List<String> 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<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true); |
|||
expressionStrings.forEach(key -> { |
|||
Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); |
|||
result.put(key, value); |
|||
}); |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* JoinPoint 切面 批量解析 EL 表达式,转换 jspl参数 |
|||
* |
|||
* @param joinPoint 切面点 |
|||
* @param info 返回值 |
|||
* @param expressionStrings EL 表达式数组 |
|||
* @return Map<String, Object> 结果 |
|||
* @author 陈賝 |
|||
* @since 2023/6/18 11:20 |
|||
*/ |
|||
// TODO @chenchen: 这个方法,和 parseExpressions 比较接近,是不是可以合并下;
|
|||
public static Map<String, Object> parseExpression(JoinPoint joinPoint, Object info, List<String> expressionStrings) { |
|||
// 如果为空,则不进行解析
|
|||
if (CollUtil.isEmpty(expressionStrings)) { |
|||
return MapUtil.newHashMap(); |
|||
} |
|||
|
|||
// 第一步,构建解析的上下文 EvaluationContext
|
|||
// 通过 joinPoint 获取被注解方法
|
|||
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); |
|||
Method method = signature.getMethod(); |
|||
// 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
|
|||
String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); |
|||
// Spring 的表达式上下文对象
|
|||
EvaluationContext context = new StandardEvaluationContext(); |
|||
if (ArrayUtil.isNotEmpty(parameterNames)) { |
|||
//获取方法参数值
|
|||
Object[] args = joinPoint.getArgs(); |
|||
for (int i = 0; i < args.length; i++) { |
|||
// 替换 SP EL 里的变量值为实际值, 比如 #user --> user对象
|
|||
context.setVariable(parameterNames[i], args[i]); |
|||
} |
|||
context.setVariable("info", info); |
|||
} |
|||
// 第二步,逐个参数解析
|
|||
Map<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true); |
|||
expressionStrings.forEach(key -> { |
|||
Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); |
|||
result.put(key, value); |
|||
}); |
|||
return result; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,53 @@ |
|||
package com.win.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<String> 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<Long> splitToLong(String value, CharSequence separator) { |
|||
long[] longs = StrUtil.splitToLong(value, separator); |
|||
return Arrays.stream(longs).boxed().collect(Collectors.toList()); |
|||
} |
|||
|
|||
public static List<Integer> splitToInteger(String value, CharSequence separator) { |
|||
int[] integers = StrUtil.splitToInt(value, separator); |
|||
return Arrays.stream(integers).boxed().collect(Collectors.toList()); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,55 @@ |
|||
package com.win.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<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); |
|||
if (CollUtil.isNotEmpty(constraintViolations)) { |
|||
throw new ConstraintViolationException(constraintViolations); |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,35 @@ |
|||
package com.win.framework.common.validation; |
|||
|
|||
import com.win.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<? extends IntArrayValuable> value(); |
|||
|
|||
String message() default "必须在指定范围 {value}"; |
|||
|
|||
Class<?>[] groups() default {}; |
|||
|
|||
Class<? extends Payload>[] payload() default {}; |
|||
|
|||
} |
@ -0,0 +1,42 @@ |
|||
package com.win.framework.common.validation; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import com.win.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<InEnum, Collection<Integer>> { |
|||
|
|||
private List<Integer> 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<Integer> 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; |
|||
} |
|||
|
|||
} |
|||
|
@ -0,0 +1,44 @@ |
|||
package com.win.framework.common.validation; |
|||
|
|||
import com.win.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<InEnum, Integer> { |
|||
|
|||
private List<Integer> 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; |
|||
} |
|||
|
|||
} |
|||
|
@ -0,0 +1,28 @@ |
|||
package com.win.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<? extends Payload>[] payload() default {}; |
|||
|
|||
} |
@ -0,0 +1,25 @@ |
|||
package com.win.framework.common.validation; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.win.framework.common.util.validation.ValidationUtils; |
|||
|
|||
import javax.validation.ConstraintValidator; |
|||
import javax.validation.ConstraintValidatorContext; |
|||
|
|||
public class MobileValidator implements ConstraintValidator<Mobile, String> { |
|||
|
|||
@Override |
|||
public void initialize(Mobile annotation) { |
|||
} |
|||
|
|||
@Override |
|||
public boolean isValid(String value, ConstraintValidatorContext context) { |
|||
// 如果手机号为空,默认不校验,即校验通过
|
|||
if (StrUtil.isEmpty(value)) { |
|||
return true; |
|||
} |
|||
// 校验手机
|
|||
return ValidationUtils.isMobile(value); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,4 @@ |
|||
/** |
|||
* 使用 Hibernate Validator 实现参数校验 |
|||
*/ |
|||
package com.win.framework.common.validation; |
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<artifactId>win-framework</artifactId> |
|||
<groupId>com.win</groupId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>win-spring-boot-starter-banner</artifactId> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>Banner 用于在 console 控制台,打印开发文档、接口文档等</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-common</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter</artifactId> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -0,0 +1,20 @@ |
|||
package com.win.framework.banner.config; |
|||
|
|||
import com.win.framework.banner.core.BannerApplicationRunner; |
|||
import org.springframework.boot.autoconfigure.AutoConfiguration; |
|||
import org.springframework.context.annotation.Bean; |
|||
|
|||
/** |
|||
* Banner 的自动配置类 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
@AutoConfiguration |
|||
public class WinBannerAutoConfiguration { |
|||
|
|||
@Bean |
|||
public BannerApplicationRunner bannerApplicationRunner() { |
|||
return new BannerApplicationRunner(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,41 @@ |
|||
package com.win.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----------------------------------------------------------"); |
|||
// 数据报表
|
|||
if (isNotPresent("com.win.module.report.framework.security.config.SecurityConfiguration")) { |
|||
System.out.println("[报表模块 win-module-report - 已禁用]"); |
|||
} |
|||
// 工作流
|
|||
if (isNotPresent("com.win.framework.flowable.config.WinFlowableConfiguration")) { |
|||
System.out.println("[工作流模块 win-module-bpm - 已禁用]"); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private static boolean isNotPresent(String className) { |
|||
return !ClassUtils.isPresent(className, ClassUtils.getDefaultClassLoader()); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,6 @@ |
|||
/** |
|||
* Banner 用于在 console 控制台,打印开发文档、接口文档等 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
package com.win.framework.banner; |
@ -0,0 +1 @@ |
|||
com.win.framework.banner.config.WinBannerAutoConfiguration |
@ -0,0 +1,17 @@ |
|||
闻荫源码 http://www.iocoder.cn |
|||
Application Version: ${win.info.version} |
|||
Spring Boot Version: ${spring-boot.version} |
|||
|
|||
.__ __. ______ .______ __ __ _______ |
|||
| \ | | / __ \ | _ \ | | | | / _____| |
|||
| \| | | | | | | |_) | | | | | | | __ |
|||
| . ` | | | | | | _ < | | | | | | |_ | |
|||
| |\ | | `--' | | |_) | | `--' | | |__| | |
|||
|__| \__| \______/ |______/ \______/ \______| |
|||
|
|||
███╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ |
|||
████╗ ██║██╔═══██╗ ██╔══██╗██║ ██║██╔════╝ |
|||
██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║██║ ███╗ |
|||
██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║██║ ██║ |
|||
██║ ╚████║╚██████╔╝ ██████╔╝╚██████╔╝╚██████╔╝ |
|||
╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ |
@ -0,0 +1,52 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-framework</artifactId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>win-spring-boot-starter-biz-data-permission</artifactId> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>数据权限</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-common</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Web 相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-security</artifactId> |
|||
<optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 --> |
|||
</dependency> |
|||
|
|||
<!-- DB 相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-mybatis</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 业务组件 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 --> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<!-- Test 测试相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-test</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -0,0 +1,44 @@ |
|||
package com.win.framework.datapermission.config; |
|||
|
|||
import com.win.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; |
|||
import com.win.framework.datapermission.core.db.DataPermissionDatabaseInterceptor; |
|||
import com.win.framework.datapermission.core.rule.DataPermissionRule; |
|||
import com.win.framework.datapermission.core.rule.DataPermissionRuleFactory; |
|||
import com.win.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; |
|||
import com.win.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 WinDataPermissionAutoConfiguration { |
|||
|
|||
@Bean |
|||
public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> 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(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,34 @@ |
|||
package com.win.framework.datapermission.config; |
|||
|
|||
import com.win.framework.datapermission.core.rule.dept.DeptDataPermissionRule; |
|||
import com.win.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; |
|||
import com.win.framework.security.core.LoginUser; |
|||
import com.win.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 WinDeptDataPermissionAutoConfiguration { |
|||
|
|||
@Bean |
|||
public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi, |
|||
List<DeptDataPermissionRuleCustomizer> customizers) { |
|||
// 创建 DeptDataPermissionRule 对象
|
|||
DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); |
|||
// 补全表配置
|
|||
customizers.forEach(customizer -> customizer.customize(rule)); |
|||
return rule; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,35 @@ |
|||
package com.win.framework.datapermission.core.annotation; |
|||
|
|||
import com.win.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<? extends DataPermissionRule>[] includeRules() default {}; |
|||
|
|||
/** |
|||
* 排除的数据权限规则数组,优先级最低 |
|||
*/ |
|||
Class<? extends DataPermissionRule>[] excludeRules() default {}; |
|||
|
|||
} |
@ -0,0 +1,36 @@ |
|||
package com.win.framework.datapermission.core.aop; |
|||
|
|||
import com.win.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.win.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); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,72 @@ |
|||
package com.win.framework.datapermission.core.aop; |
|||
|
|||
import com.win.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<MethodClassKey, DataPermission> 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; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,72 @@ |
|||
package com.win.framework.datapermission.core.aop; |
|||
|
|||
import com.win.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<LinkedList<DataPermission>> 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<DataPermission> getAll() { |
|||
return DATA_PERMISSIONS.get(); |
|||
} |
|||
|
|||
/** |
|||
* 清空上下文 |
|||
* |
|||
* 目前仅仅用于单测 |
|||
*/ |
|||
public static void clear() { |
|||
DATA_PERMISSIONS.remove(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,641 @@ |
|||
package com.win.framework.datapermission.core.db; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import com.win.framework.common.util.collection.SetUtils; |
|||
import com.win.framework.datapermission.core.rule.DataPermissionRule; |
|||
import com.win.framework.datapermission.core.rule.DataPermissionRuleFactory; |
|||
import com.win.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<DataPermissionRule> 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<DataPermissionRule> 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<WithItem> 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<SelectBody> selectBodyList = operationList.getSelects(); |
|||
if (CollectionUtils.isNotEmpty(selectBodyList)) { |
|||
selectBodyList.forEach(this::processSelectBody); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理 PlainSelect |
|||
*/ |
|||
protected void processPlainSelect(PlainSelect plainSelect) { |
|||
//#3087 github
|
|||
List<SelectItem> selectItems = plainSelect.getSelectItems(); |
|||
if (CollectionUtils.isNotEmpty(selectItems)) { |
|||
selectItems.forEach(this::processSelectItem); |
|||
} |
|||
|
|||
// 处理 where 中的子查询
|
|||
Expression where = plainSelect.getWhere(); |
|||
processWhereSubSelect(where); |
|||
|
|||
// 处理 fromItem
|
|||
FromItem fromItem = plainSelect.getFromItem(); |
|||
List<Table> list = processFromItem(fromItem); |
|||
List<Table> mainTables = new ArrayList<>(list); |
|||
|
|||
// 处理 join
|
|||
List<Join> joins = plainSelect.getJoins(); |
|||
if (CollectionUtils.isNotEmpty(joins)) { |
|||
mainTables = processJoins(mainTables, joins); |
|||
} |
|||
|
|||
// 当有 mainTable 时,进行 where 条件追加
|
|||
if (CollectionUtils.isNotEmpty(mainTables)) { |
|||
plainSelect.setWhere(builderExpression(where, mainTables)); |
|||
} |
|||
} |
|||
|
|||
private List<Table> processFromItem(FromItem fromItem) { |
|||
// 处理括号括起来的表达式
|
|||
while (fromItem instanceof ParenthesisFromItem) { |
|||
fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); |
|||
} |
|||
|
|||
List<Table> mainTables = new ArrayList<>(); |
|||
// 无 join 时的处理逻辑
|
|||
if (fromItem instanceof Table) { |
|||
Table fromTable = (Table) fromItem; |
|||
mainTables.add(fromTable); |
|||
} else if (fromItem instanceof SubJoin) { |
|||
// SubJoin 类型则还需要添加上 where 条件
|
|||
List<Table> tables = processSubJoin((SubJoin) fromItem); |
|||
mainTables.addAll(tables); |
|||
} else { |
|||
// 处理下 fromItem
|
|||
processOtherFromItem(fromItem); |
|||
} |
|||
return mainTables; |
|||
} |
|||
|
|||
/** |
|||
* 处理where条件内的子查询 |
|||
* <p> |
|||
* 支持如下: |
|||
* 1. in |
|||
* 2. = |
|||
* 3. > |
|||
* 4. < |
|||
* 5. >= |
|||
* 6. <= |
|||
* 7. <> |
|||
* 8. EXISTS |
|||
* 9. NOT EXISTS |
|||
* <p> |
|||
* 前提条件: |
|||
* 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()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理函数 |
|||
* <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)<p> |
|||
* <p> fixed gitee pulls/141</p> |
|||
* |
|||
* @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<Table> processSubJoin(SubJoin subJoin) { |
|||
List<Table> mainTables = new ArrayList<>(); |
|||
if (subJoin.getJoinList() != null) { |
|||
List<Table> list = processFromItem(subJoin.getLeft()); |
|||
mainTables.addAll(list); |
|||
mainTables = processJoins(mainTables, subJoin.getJoinList()); |
|||
} |
|||
return mainTables; |
|||
} |
|||
|
|||
/** |
|||
* 处理 joins |
|||
* |
|||
* @param mainTables 可以为 null |
|||
* @param joins join 集合 |
|||
* @return List<Table> 右连接查询的 Table 列表 |
|||
*/ |
|||
private List<Table> processJoins(List<Table> mainTables, List<Join> 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<List<Table>> onTableDeque = new LinkedList<>(); |
|||
for (Join join : joins) { |
|||
// 处理 on 表达式
|
|||
FromItem joinItem = join.getRightItem(); |
|||
|
|||
// 获取当前 join 的表,subJoint 可以看作是一张表
|
|||
List<Table> 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<Table> 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<Expression> originOnExpressions = join.getOnExpressions(); |
|||
// 正常 join on 表达式只有一个,立刻处理
|
|||
if (originOnExpressions.size() == 1 && onTables != null) { |
|||
List<Expression> 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<Expression> onExpressions = new LinkedList<>(); |
|||
for (Expression originOnExpression : originOnExpressions) { |
|||
List<Table> 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<Table> 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<List<DataPermissionRule>> RULES = ThreadLocal.withInitial(Collections::emptyList); |
|||
/** |
|||
* SQL 是否进行重写 |
|||
*/ |
|||
private static final ThreadLocal<Boolean> REWRITE = ThreadLocal.withInitial(() -> Boolean.FALSE); |
|||
|
|||
public static void init(List<DataPermissionRule> 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<DataPermissionRule> 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<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements = new ConcurrentHashMap<>(); |
|||
|
|||
/** |
|||
* 判断是否无需重写 |
|||
* ps:虽然有点中文式英语,但是容易读懂即可 |
|||
* |
|||
* @param ms MappedStatement |
|||
* @param rules 数据权限规则数组 |
|||
* @return 是否无需重写 |
|||
*/ |
|||
public boolean noRewritable(MappedStatement ms, List<DataPermissionRule> rules) { |
|||
// 如果规则为空,说明无需重写
|
|||
if (CollUtil.isEmpty(rules)) { |
|||
return true; |
|||
} |
|||
// 任一规则不在 noRewritableMap 中,则说明可能需要重写
|
|||
for (DataPermissionRule rule : rules) { |
|||
Set<String> 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<DataPermissionRule> rules) { |
|||
for (DataPermissionRule rule : rules) { |
|||
Set<String> 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(); |
|||
} |
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,36 @@ |
|||
package com.win.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<String> getTableNames(); |
|||
|
|||
/** |
|||
* 根据表名和别名,生成对应的 WHERE / OR 过滤条件 |
|||
* |
|||
* @param tableName 表名 |
|||
* @param tableAlias 别名,可能为空 |
|||
* @return 过滤条件 Expression 表达式 |
|||
*/ |
|||
Expression getExpression(String tableName, Alias tableAlias); |
|||
|
|||
} |
@ -0,0 +1,28 @@ |
|||
package com.win.framework.datapermission.core.rule; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* {@link DataPermissionRule} 工厂接口 |
|||
* 作为 {@link DataPermissionRule} 的容器,提供管理能力 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
public interface DataPermissionRuleFactory { |
|||
|
|||
/** |
|||
* 获得所有数据权限规则数组 |
|||
* |
|||
* @return 数据权限规则数组 |
|||
*/ |
|||
List<DataPermissionRule> getDataPermissionRules(); |
|||
|
|||
/** |
|||
* 获得指定 Mapper 的数据权限规则数组 |
|||
* |
|||
* @param mappedStatementId 指定 Mapper 的编号 |
|||
* @return 数据权限规则数组 |
|||
*/ |
|||
List<DataPermissionRule> getDataPermissionRule(String mappedStatementId); |
|||
|
|||
} |
@ -0,0 +1,62 @@ |
|||
package com.win.framework.datapermission.core.rule; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.util.ArrayUtil; |
|||
import com.win.framework.datapermission.core.annotation.DataPermission; |
|||
import com.win.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<DataPermissionRule> rules; |
|||
|
|||
@Override |
|||
public List<DataPermissionRule> getDataPermissionRules() { |
|||
return rules; |
|||
} |
|||
|
|||
@Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
|
|||
public List<DataPermissionRule> 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; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,199 @@ |
|||
package com.win.framework.datapermission.core.rule.dept; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
|||
import com.win.framework.common.util.collection.CollectionUtils; |
|||
import com.win.framework.common.util.json.JsonUtils; |
|||
import com.win.framework.datapermission.core.rule.DataPermissionRule; |
|||
import com.win.framework.mybatis.core.dataobject.BaseDO; |
|||
import com.win.framework.mybatis.core.util.MyBatisUtils; |
|||
import com.win.framework.security.core.LoginUser; |
|||
import com.win.framework.security.core.util.SecurityFrameworkUtils; |
|||
import com.win.module.system.api.permission.PermissionApi; |
|||
import com.win.module.system.api.permission.dto.DeptDataPermissionRespDTO; |
|||
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 不进行修改,则会导致用户看不到之前的数据。【win-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<String, String> deptColumns = new HashMap<>(); |
|||
/** |
|||
* 基于用户的表字段配置 |
|||
* 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 |
|||
* |
|||
* key:表名 |
|||
* value:字段名 |
|||
*/ |
|||
private final Map<String, String> userColumns = new HashMap<>(); |
|||
/** |
|||
* 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 |
|||
*/ |
|||
private final Set<String> TABLE_NAMES = new HashSet<>(); |
|||
|
|||
@Override |
|||
public Set<String> getTableNames() { |
|||
return TABLE_NAMES; |
|||
} |
|||
|
|||
@Override |
|||
public Expression getExpression(String tableName, Alias tableAlias) { |
|||
// 只有有登陆用户的情况下,才进行数据权限的处理
|
|||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); |
|||
if (loginUser == null) { |
|||
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<Long> 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<? extends BaseDO> entityClass) { |
|||
addDeptColumn(entityClass, DEPT_COLUMN_NAME); |
|||
} |
|||
|
|||
public void addDeptColumn(Class<? extends BaseDO> 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<? extends BaseDO> entityClass) { |
|||
addUserColumn(entityClass, USER_COLUMN_NAME); |
|||
} |
|||
|
|||
public void addUserColumn(Class<? extends BaseDO> 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); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,20 @@ |
|||
package com.win.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); |
|||
|
|||
} |
@ -0,0 +1,6 @@ |
|||
/** |
|||
* 基于部门的数据权限规则 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
package com.win.framework.datapermission.core.rule.dept; |
@ -0,0 +1,43 @@ |
|||
package com.win.framework.datapermission.core.util; |
|||
|
|||
import com.win.framework.datapermission.core.annotation.DataPermission; |
|||
import com.win.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(); |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,4 @@ |
|||
/** |
|||
* 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 |
|||
*/ |
|||
package com.win.framework.datapermission; |
@ -0,0 +1,2 @@ |
|||
com.win.framework.datapermission.config.WinDataPermissionAutoConfiguration |
|||
com.win.framework.datapermission.config.WinDeptDataPermissionAutoConfiguration |
@ -0,0 +1,50 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-framework</artifactId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>win-spring-boot-starter-biz-dict</artifactId> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>字典类型、数据</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-common</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Spring 核心 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 业务组件 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 --> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<!-- 工具类相关 --> |
|||
<dependency> |
|||
<groupId>com.google.guava</groupId> |
|||
<artifactId>guava</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Test 测试相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-test</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
</project> |
@ -0,0 +1,18 @@ |
|||
package com.win.framework.dict.config; |
|||
|
|||
import com.win.framework.dict.core.util.DictFrameworkUtils; |
|||
import com.win.module.system.api.dict.DictDataApi; |
|||
import org.springframework.boot.autoconfigure.AutoConfiguration; |
|||
import org.springframework.context.annotation.Bean; |
|||
|
|||
@AutoConfiguration |
|||
public class WinDictAutoConfiguration { |
|||
|
|||
@Bean |
|||
@SuppressWarnings("InstantiationOfUtilityClass") |
|||
public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) { |
|||
DictFrameworkUtils.init(dictDataApi); |
|||
return new DictFrameworkUtils(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,4 @@ |
|||
/** |
|||
* 占位 |
|||
*/ |
|||
package com.win.framework.dict.core; |
@ -0,0 +1,94 @@ |
|||
package com.win.framework.dict.core.util; |
|||
|
|||
import cn.hutool.core.util.ObjectUtil; |
|||
import com.win.framework.common.core.KeyValue; |
|||
import com.win.framework.common.util.cache.CacheUtils; |
|||
import com.win.module.system.api.dict.DictDataApi; |
|||
import com.win.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<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( |
|||
Duration.ofMinutes(1L), // 过期时间 1 分钟
|
|||
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() { |
|||
|
|||
@Override |
|||
public DictDataRespDTO load(KeyValue<String, String> key) { |
|||
return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()), DICT_DATA_NULL); |
|||
} |
|||
|
|||
}); |
|||
|
|||
/** |
|||
* 针对 {@link #parseDictDataValue(String, String)} 的缓存 |
|||
*/ |
|||
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( |
|||
Duration.ofMinutes(1L), // 过期时间 1 分钟
|
|||
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() { |
|||
|
|||
@Override |
|||
public DictDataRespDTO load(KeyValue<String, String> key) { |
|||
return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()), DICT_DATA_NULL); |
|||
} |
|||
|
|||
}); |
|||
|
|||
/** |
|||
* 针对 {@link #parseDictDataValue(String, String)} 的缓存 |
|||
*/ |
|||
private static final LoadingCache<KeyValue<String, String>, String[]> DICT_TYPE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( |
|||
Duration.ofMinutes(1L), // 过期时间 1 分钟
|
|||
new CacheLoader<KeyValue<String, String>, String[]>() { |
|||
|
|||
@Override |
|||
public String[] load(KeyValue<String, String> key) { |
|||
return ObjectUtil.defaultIfNull(dictDataApi.getDictDataByType(key.getKey()), new String[0]); |
|||
} |
|||
|
|||
}); |
|||
|
|||
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(); |
|||
} |
|||
|
|||
@SneakyThrows |
|||
public static String[] dictTypeDictDataValue(String dictType) { |
|||
return DICT_TYPE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, null)); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,6 @@ |
|||
/** |
|||
* 字典数据模块,提供 {@link com.win.framework.dict.core.util.DictFrameworkUtils} 工具类 |
|||
* |
|||
* 通过将字典缓存在内存中,保证性能 |
|||
*/ |
|||
package com.win.framework.dict; |
@ -0,0 +1 @@ |
|||
com.win.framework.dict.config.WinDictAutoConfiguration |
@ -0,0 +1,49 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-framework</artifactId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>win-spring-boot-starter-biz-error-code</artifactId> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description> |
|||
错误码 ErrorCode 的自动配置功能,提供如下功能: |
|||
1. 远程读取:项目启动时,从 system-server 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置; |
|||
2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-server 服务加载最新的 ErrorCode 错误码; |
|||
3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑; |
|||
</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-common</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Spring 核心 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 业务组件 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-system-api</artifactId> <!-- 需要使用它,进行操作日志的记录 --> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>jakarta.validation</groupId> |
|||
<artifactId>jakarta.validation-api</artifactId> |
|||
<scope>provided</scope> <!-- 设置为 provided,主要是 ErrorCodeProperties 使用到 --> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -0,0 +1,30 @@ |
|||
package com.win.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("win.error-code") |
|||
@Data |
|||
@Validated |
|||
public class ErrorCodeProperties { |
|||
|
|||
/** |
|||
* 是否开启 |
|||
*/ |
|||
private Boolean enable = true; |
|||
/** |
|||
* 错误码枚举类 |
|||
*/ |
|||
@NotNull(message = "错误码枚举类不能为空") |
|||
private List<String> constantsClassList; |
|||
|
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue