commit
c170f82b4f
2710 changed files with 200136 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,160 @@ |
|||
#!/bin/bash |
|||
set -e |
|||
|
|||
DATE=$(date +%Y%m%d%H%M) |
|||
# 基础路径 |
|||
BASE_PATH=/work/projects/win-server |
|||
# 编译后 jar 的地址。部署时,Jenkins 会上传 jar 包到该目录下 |
|||
SOURCE_PATH=$BASE_PATH/build |
|||
# 服务名称。同时约定部署服务的 jar 包名字也为它。 |
|||
SERVER_NAME=win-server |
|||
# 环境 |
|||
PROFILES_ACTIVE=development |
|||
# 健康检查 URL |
|||
HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/ |
|||
|
|||
# heapError 存放路径 |
|||
HEAP_ERROR_PATH=$BASE_PATH/heapError |
|||
# JVM 参数 |
|||
JAVA_OPS="-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$HEAP_ERROR_PATH" |
|||
|
|||
# SkyWalking Agent 配置 |
|||
#export SW_AGENT_NAME=$SERVER_NAME |
|||
#export SW_AGENT_COLLECTOR_BACKEND_SERVICES=192.168.0.84:11800 |
|||
#export SW_GRPC_LOG_SERVER_HOST=192.168.0.84 |
|||
#export SW_AGENT_TRACE_IGNORE_PATH="Redisson/PING,/actuator/**,/admin/**" |
|||
#export JAVA_AGENT=-javaagent:/work/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar |
|||
|
|||
# 备份 |
|||
function backup() { |
|||
# 如果不存在,则无需备份 |
|||
if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then |
|||
echo "[backup] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过备份" |
|||
# 如果存在,则备份到 backup 目录下,使用时间作为后缀 |
|||
else |
|||
echo "[backup] 开始备份 $SERVER_NAME ..." |
|||
cp $BASE_PATH/$SERVER_NAME.jar $BASE_PATH/backup/$SERVER_NAME-$DATE.jar |
|||
echo "[backup] 备份 $SERVER_NAME 完成" |
|||
fi |
|||
} |
|||
|
|||
# 最新构建代码 移动到项目环境 |
|||
function transfer() { |
|||
echo "[transfer] 开始转移 $SERVER_NAME.jar" |
|||
|
|||
# 删除原 jar 包 |
|||
if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then |
|||
echo "[transfer] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过删除" |
|||
else |
|||
echo "[transfer] 移除 $BASE_PATH/$SERVER_NAME.jar 完成" |
|||
rm $BASE_PATH/$SERVER_NAME.jar |
|||
fi |
|||
|
|||
# 复制新 jar 包 |
|||
echo "[transfer] 从 $SOURCE_PATH 中获取 $SERVER_NAME.jar 并迁移至 $BASE_PATH ...." |
|||
cp $SOURCE_PATH/$SERVER_NAME.jar $BASE_PATH |
|||
|
|||
echo "[transfer] 转移 $SERVER_NAME.jar 完成" |
|||
} |
|||
|
|||
# 停止:优雅关闭之前已经启动的服务 |
|||
function stop() { |
|||
echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME" |
|||
PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') |
|||
# 如果 Java 服务启动中,则进行关闭 |
|||
if [ -n "$PID" ]; then |
|||
# 正常关闭 |
|||
echo "[stop] $BASE_PATH/$SERVER_NAME 运行中,开始 kill [$PID]" |
|||
kill -15 $PID |
|||
# 等待最大 120 秒,直到关闭完成。 |
|||
for ((i = 0; i < 120; i++)) |
|||
do |
|||
sleep 1 |
|||
PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') |
|||
if [ -n "$PID" ]; then |
|||
echo -e ".\c" |
|||
else |
|||
echo '[stop] 停止 $BASE_PATH/$SERVER_NAME 成功' |
|||
break |
|||
fi |
|||
done |
|||
|
|||
# 如果正常关闭失败,那么进行强制 kill -9 进行关闭 |
|||
if [ -n "$PID" ]; then |
|||
echo "[stop] $BASE_PATH/$SERVER_NAME 失败,强制 kill -9 $PID" |
|||
kill -9 $PID |
|||
fi |
|||
# 如果 Java 服务未启动,则无需关闭 |
|||
else |
|||
echo "[stop] $BASE_PATH/$SERVER_NAME 未启动,无需停止" |
|||
fi |
|||
} |
|||
|
|||
# 启动:启动后端项目 |
|||
function start() { |
|||
# 开启启动前,打印启动参数 |
|||
echo "[start] 开始启动 $BASE_PATH/$SERVER_NAME" |
|||
echo "[start] JAVA_OPS: $JAVA_OPS" |
|||
echo "[start] JAVA_AGENT: $JAVA_AGENT" |
|||
echo "[start] PROFILES: $PROFILES_ACTIVE" |
|||
|
|||
# 开始启动 |
|||
BUILD_ID=dontKillMe nohup java -server $JAVA_OPS $JAVA_AGENT -jar $BASE_PATH/$SERVER_NAME.jar --spring.profiles.active=$PROFILES_ACTIVE & |
|||
echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成" |
|||
} |
|||
|
|||
# 健康检查:自动判断后端项目是否正常启动 |
|||
function healthCheck() { |
|||
# 如果配置健康检查,则进行健康检查 |
|||
if [ -n "$HEALTH_CHECK_URL" ]; then |
|||
# 健康检查最大 120 秒,直到健康检查通过 |
|||
echo "[healthCheck] 开始通过 $HEALTH_CHECK_URL 地址,进行健康检查"; |
|||
for ((i = 0; i < 120; i++)) |
|||
do |
|||
# 请求健康检查地址,只获取状态码。 |
|||
result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"` |
|||
# 如果状态码为 200,则说明健康检查通过 |
|||
if [ "$result" == "200" ]; then |
|||
echo "[healthCheck] 健康检查通过"; |
|||
break |
|||
# 如果状态码非 200,则说明未通过。sleep 1 秒后,继续重试 |
|||
else |
|||
echo -e ".\c" |
|||
sleep 1 |
|||
fi |
|||
done |
|||
|
|||
# 健康检查未通过,则异常退出 shell 脚本,不继续部署。 |
|||
if [ ! "$result" == "200" ]; then |
|||
echo "[healthCheck] 健康检查不通过,可能部署失败。查看日志,自行判断是否启动成功"; |
|||
tail -n 10 nohup.out |
|||
exit 1; |
|||
# 健康检查通过,打印最后 10 行日志,可能部署的人想看下日志。 |
|||
else |
|||
tail -n 10 nohup.out |
|||
fi |
|||
# 如果未配置健康检查,则 sleep 120 秒,人工看日志是否部署成功。 |
|||
else |
|||
echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 120 秒"; |
|||
sleep 120 |
|||
echo "[healthCheck] sleep 120 秒完成,查看日志,自行判断是否启动成功"; |
|||
tail -n 50 nohup.out |
|||
fi |
|||
} |
|||
|
|||
# 部署 |
|||
function deploy() { |
|||
cd $BASE_PATH |
|||
# 备份原 jar |
|||
backup |
|||
# 停止 Java 服务 |
|||
stop |
|||
# 部署新 jar |
|||
transfer |
|||
# 启动 Java 服务 |
|||
start |
|||
# 健康检查 |
|||
healthCheck |
|||
} |
|||
|
|||
deploy |
@ -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,147 @@ |
|||
<?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-member</module>--> |
|||
<module>win-module-system</module> |
|||
<module>win-module-infra</module> |
|||
<module>win-module-bpm</module> |
|||
<module>win-module-report</module> |
|||
<!-- <module>win-module-mp</module>--> |
|||
<!-- <module>win-module-pay</module>--> |
|||
<!-- <module>win-module-mall</module>--> |
|||
<!-- 示例项目 --> |
|||
<module>win-example</module> |
|||
</modules> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>芋道项目基础脚手架</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<properties> |
|||
<revision>1.8.1-snapshot</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); |
@ -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,685 @@ |
|||
<?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>1.8.1-snapshot</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.6.1</dynamic-datasource.version> |
|||
<mybatis-plus-join.version>1.4.6</mybatis-plus-join.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> |
|||
<justauth.version>1.0.4</justauth.version> |
|||
<jimureport.version>1.6.1</jimureport.version> |
|||
<xercesImpl.version>2.12.2</xercesImpl.version> |
|||
<weixin-java.version>4.5.0</weixin-java.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-weixin</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-social</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>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>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>com.xingyuv</groupId> |
|||
<artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) --> |
|||
<version>${justauth.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.github.binarywang</groupId> |
|||
<artifactId>weixin-java-pay</artifactId> |
|||
<version>${weixin-java.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.github.binarywang</groupId> |
|||
<artifactId>weixin-java-mp</artifactId> |
|||
<version>${weixin-java.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.github.binarywang</groupId> |
|||
<artifactId>wx-java-mp-spring-boot-starter</artifactId> |
|||
<version>${weixin-java.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.github.binarywang</groupId> |
|||
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId> |
|||
<version>${weixin-java.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 积木报表--> |
|||
<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> |
|||
</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,22 @@ |
|||
<?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> |
|||
|
|||
<!-- 由于方便大家拷贝,使用不使用 win 作为 Maven parent --> |
|||
|
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-example</artifactId> |
|||
<version>1.0.0-snapshot</version> |
|||
<packaging>pom</packaging> |
|||
<modules> |
|||
<module>win-sso-demo-by-code</module> |
|||
<module>win-sso-demo-by-password</module> |
|||
</modules> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>提供各种示例,例如说:SSO 单点登录</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
</project> |
@ -0,0 +1,65 @@ |
|||
<?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> |
|||
|
|||
<!-- 由于方便大家拷贝,使用不使用 win 作为 Maven parent --> |
|||
|
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-sso-demo-by-code</artifactId> |
|||
<version>1.0.0-snapshot</version> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>基于授权码模式,如何实现 SSO 单点登录?</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<properties> |
|||
<!-- Maven 相关 --> |
|||
<maven.compiler.source>8</maven.compiler.source> |
|||
<maven.compiler.target>8</maven.compiler.target> |
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
|||
<!-- 统一依赖管理 --> |
|||
<spring.boot.version>2.7.15</spring.boot.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> |
|||
</dependencies> |
|||
</dependencyManagement> |
|||
|
|||
<dependencies> |
|||
<!-- Web 相关 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-web</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-security</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>cn.hutool</groupId> |
|||
<artifactId>hutool-all</artifactId> |
|||
<version>5.8.21</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -0,0 +1,13 @@ |
|||
package com.win.ssodemo; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
|
|||
@SpringBootApplication |
|||
public class SSODemoApplication { |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(SSODemoApplication.class, args); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,157 @@ |
|||
package com.win.ssodemo.client; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; |
|||
import org.springframework.core.ParameterizedTypeReference; |
|||
import org.springframework.http.*; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.Assert; |
|||
import org.springframework.util.Base64Utils; |
|||
import org.springframework.util.LinkedMultiValueMap; |
|||
import org.springframework.util.MultiValueMap; |
|||
import org.springframework.web.client.RestTemplate; |
|||
|
|||
import java.nio.charset.StandardCharsets; |
|||
|
|||
/** |
|||
* OAuth 2.0 客户端 |
|||
* |
|||
* 对应调用 OAuth2OpenController 接口 |
|||
*/ |
|||
@Component |
|||
public class OAuth2Client { |
|||
|
|||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2"; |
|||
|
|||
/** |
|||
* 租户编号 |
|||
* |
|||
* 默认使用 1;如果使用别的租户,可以调整 |
|||
*/ |
|||
public static final Long TENANT_ID = 1L; |
|||
|
|||
private static final String CLIENT_ID = "win-sso-demo-by-code"; |
|||
private static final String CLIENT_SECRET = "test"; |
|||
|
|||
|
|||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
|||
private final RestTemplate restTemplate = new RestTemplate(); |
|||
|
|||
/** |
|||
* 使用 code 授权码,获得访问令牌 |
|||
* |
|||
* @param code 授权码 |
|||
* @param redirectUri 重定向 URI |
|||
* @return 访问令牌 |
|||
*/ |
|||
public CommonResult<OAuth2AccessTokenRespDTO> postAccessToken(String code, String redirectUri) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("grant_type", "authorization_code"); |
|||
body.add("code", code); |
|||
body.add("redirect_uri", redirectUri); |
|||
// body.add("state", ""); // 选填;填了会校验
|
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 校验访问令牌,并返回它的基本信息 |
|||
* |
|||
* @param token 访问令牌 |
|||
* @return 访问令牌的基本信息 |
|||
*/ |
|||
public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("token", token); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/check-token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 使用刷新令牌,获得(刷新)访问令牌 |
|||
* |
|||
* @param refreshToken 刷新令牌 |
|||
* @return 访问令牌 |
|||
*/ |
|||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("grant_type", "refresh_token"); |
|||
body.add("refresh_token", refreshToken); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 删除访问令牌 |
|||
* |
|||
* @param token 访问令牌 |
|||
* @return 成功 |
|||
*/ |
|||
public CommonResult<Boolean> revokeToken(String token) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("token", token); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.DELETE, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
private static void addClientHeader(HttpHeaders headers) { |
|||
// client 拼接,需要 BASE64 编码
|
|||
String client = CLIENT_ID + ":" + CLIENT_SECRET; |
|||
client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); |
|||
headers.add("Authorization", "Basic " + client); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,73 @@ |
|||
package com.win.ssodemo.client; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.user.UserInfoRespDTO; |
|||
import com.win.ssodemo.client.dto.user.UserUpdateReqDTO; |
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import org.springframework.core.ParameterizedTypeReference; |
|||
import org.springframework.http.*; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.Assert; |
|||
import org.springframework.util.LinkedMultiValueMap; |
|||
import org.springframework.util.MultiValueMap; |
|||
import org.springframework.web.client.RestTemplate; |
|||
|
|||
/** |
|||
* 用户 User 信息的客户端 |
|||
* |
|||
* 对应调用 OAuth2UserController 接口 |
|||
*/ |
|||
@Component |
|||
public class UserClient { |
|||
|
|||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user"; |
|||
|
|||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
|||
private final RestTemplate restTemplate = new RestTemplate(); |
|||
|
|||
public CommonResult<UserInfoRespDTO> getUser() { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); |
|||
addTokenHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/get", |
|||
HttpMethod.GET, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
public CommonResult<Boolean> updateUser(UserUpdateReqDTO updateReqDTO) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_JSON); |
|||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); |
|||
addTokenHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
// 使用 updateReqDTO 即可
|
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/update", |
|||
HttpMethod.PUT, |
|||
new HttpEntity<>(updateReqDTO, headers), |
|||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
|
|||
private static void addTokenHeader(HttpHeaders headers) { |
|||
LoginUser loginUser = SecurityUtils.getLoginUser(); |
|||
Assert.notNull(loginUser, "登录用户不能为空"); |
|||
headers.add("Authorization", "Bearer " + loginUser.getAccessToken()); |
|||
} |
|||
} |
@ -0,0 +1,28 @@ |
|||
package com.win.ssodemo.client.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 通用返回 |
|||
* |
|||
* @param <T> 数据泛型 |
|||
*/ |
|||
@Data |
|||
public class CommonResult<T> implements Serializable { |
|||
|
|||
/** |
|||
* 错误码 |
|||
*/ |
|||
private Integer code; |
|||
/** |
|||
* 返回数据 |
|||
*/ |
|||
private T data; |
|||
/** |
|||
* 错误提示,用户可阅读 |
|||
*/ |
|||
private String msg; |
|||
|
|||
} |
@ -0,0 +1,45 @@ |
|||
package com.win.ssodemo.client.dto.oauth2; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 访问令牌 Response DTO |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class OAuth2AccessTokenRespDTO { |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
*/ |
|||
@JsonProperty("refresh_token") |
|||
private String refreshToken; |
|||
|
|||
/** |
|||
* 令牌类型 |
|||
*/ |
|||
@JsonProperty("token_type") |
|||
private String tokenType; |
|||
|
|||
/** |
|||
* 过期时间;单位:秒 |
|||
*/ |
|||
@JsonProperty("expires_in") |
|||
private Long expiresIn; |
|||
|
|||
/** |
|||
* 授权范围;如果多个授权范围,使用空格分隔 |
|||
*/ |
|||
private String scope; |
|||
|
|||
} |
@ -0,0 +1,59 @@ |
|||
package com.win.ssodemo.client.dto.oauth2; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 校验令牌 Response DTO |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class OAuth2CheckTokenRespDTO { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
@JsonProperty("user_id") |
|||
private Long userId; |
|||
/** |
|||
* 用户类型 |
|||
*/ |
|||
@JsonProperty("user_type") |
|||
private Integer userType; |
|||
/** |
|||
* 租户编号 |
|||
*/ |
|||
@JsonProperty("tenant_id") |
|||
private Long tenantId; |
|||
|
|||
/** |
|||
* 客户端编号 |
|||
*/ |
|||
@JsonProperty("client_id") |
|||
private String clientId; |
|||
/** |
|||
* 授权范围 |
|||
*/ |
|||
private List<String> scopes; |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
|
|||
/** |
|||
* 过期时间 |
|||
* |
|||
* 时间戳 / 1000,即单位:秒 |
|||
*/ |
|||
private Long exp; |
|||
|
|||
} |
@ -0,0 +1,97 @@ |
|||
package com.win.ssodemo.client.dto.user; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 获得用户基本信息 Response dto |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class UserInfoRespDTO { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 用户账号 |
|||
*/ |
|||
private String username; |
|||
|
|||
/** |
|||
* 用户昵称 |
|||
*/ |
|||
private String nickname; |
|||
|
|||
/** |
|||
* 用户邮箱 |
|||
*/ |
|||
private String email; |
|||
/** |
|||
* 手机号码 |
|||
*/ |
|||
private String mobile; |
|||
|
|||
/** |
|||
* 用户性别 |
|||
*/ |
|||
private Integer sex; |
|||
|
|||
/** |
|||
* 用户头像 |
|||
*/ |
|||
private String avatar; |
|||
|
|||
/** |
|||
* 所在部门 |
|||
*/ |
|||
private Dept dept; |
|||
|
|||
/** |
|||
* 所属岗位数组 |
|||
*/ |
|||
private List<Post> posts; |
|||
|
|||
/** |
|||
* 部门 |
|||
*/ |
|||
@Data |
|||
public static class Dept { |
|||
|
|||
/** |
|||
* 部门编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 部门名称 |
|||
*/ |
|||
private String name; |
|||
|
|||
} |
|||
|
|||
/** |
|||
* 岗位 |
|||
*/ |
|||
@Data |
|||
public static class Post { |
|||
|
|||
/** |
|||
* 岗位编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 岗位名称 |
|||
*/ |
|||
private String name; |
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,35 @@ |
|||
package com.win.ssodemo.client.dto.user; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 更新用户基本信息 Request DTO |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class UserUpdateReqDTO { |
|||
|
|||
/** |
|||
* 用户昵称 |
|||
*/ |
|||
private String nickname; |
|||
|
|||
/** |
|||
* 用户邮箱 |
|||
*/ |
|||
private String email; |
|||
|
|||
/** |
|||
* 手机号码 |
|||
*/ |
|||
private String mobile; |
|||
|
|||
/** |
|||
* 用户性别 |
|||
*/ |
|||
private Integer sex; |
|||
|
|||
} |
@ -0,0 +1,63 @@ |
|||
package com.win.ssodemo.controller; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.win.ssodemo.client.OAuth2Client; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
|
|||
@RestController |
|||
@RequestMapping("/auth") |
|||
public class AuthController { |
|||
|
|||
@Resource |
|||
private OAuth2Client oauth2Client; |
|||
|
|||
/** |
|||
* 使用 code 访问令牌,获得访问令牌 |
|||
* |
|||
* @param code 授权码 |
|||
* @param redirectUri 重定向 URI |
|||
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 |
|||
*/ |
|||
@PostMapping("/login-by-code") |
|||
public CommonResult<OAuth2AccessTokenRespDTO> loginByCode(@RequestParam("code") String code, |
|||
@RequestParam("redirectUri") String redirectUri) { |
|||
return oauth2Client.postAccessToken(code, redirectUri); |
|||
} |
|||
|
|||
/** |
|||
* 使用刷新令牌,获得(刷新)访问令牌 |
|||
* |
|||
* @param refreshToken 刷新令牌 |
|||
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 |
|||
*/ |
|||
@PostMapping("/refresh-token") |
|||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) { |
|||
return oauth2Client.refreshToken(refreshToken); |
|||
} |
|||
|
|||
/** |
|||
* 退出登录 |
|||
* |
|||
* @param request 请求 |
|||
* @return 成功 |
|||
*/ |
|||
@PostMapping("/logout") |
|||
public CommonResult<Boolean> logout(HttpServletRequest request) { |
|||
String token = SecurityUtils.obtainAuthorization(request, "Authorization"); |
|||
if (StrUtil.isNotBlank(token)) { |
|||
return oauth2Client.revokeToken(token); |
|||
} |
|||
// 返回成功
|
|||
return new CommonResult<>(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,40 @@ |
|||
package com.win.ssodemo.controller; |
|||
|
|||
import com.win.ssodemo.client.UserClient; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.user.UserInfoRespDTO; |
|||
import com.win.ssodemo.client.dto.user.UserUpdateReqDTO; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.annotation.Resource; |
|||
|
|||
@RestController |
|||
@RequestMapping("/user") |
|||
public class UserController { |
|||
|
|||
@Resource |
|||
private UserClient userClient; |
|||
|
|||
/** |
|||
* 获得当前登录用户的基本信息 |
|||
* |
|||
* @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 |
|||
*/ |
|||
@GetMapping("/get") |
|||
public CommonResult<UserInfoRespDTO> getUser() { |
|||
return userClient.getUser(); |
|||
} |
|||
|
|||
/** |
|||
* 更新当前登录用户的昵称 |
|||
* |
|||
* @param nickname 昵称 |
|||
* @return 成功 |
|||
*/ |
|||
@PutMapping("/update") |
|||
public CommonResult<Boolean> updateUser(@RequestParam("nickname") String nickname) { |
|||
UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null); |
|||
return userClient.updateUser(updateReqDTO); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,52 @@ |
|||
package com.win.ssodemo.framework.config; |
|||
|
|||
import com.win.ssodemo.framework.core.filter.TokenAuthenticationFilter; |
|||
import com.win.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.http.HttpMethod; |
|||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
|||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
|||
import org.springframework.security.web.AuthenticationEntryPoint; |
|||
import org.springframework.security.web.SecurityFilterChain; |
|||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
|||
|
|||
import javax.annotation.Resource; |
|||
|
|||
@Configuration(proxyBeanMethods = false) |
|||
@EnableWebSecurity |
|||
public class SecurityConfiguration{ |
|||
|
|||
@Resource |
|||
private TokenAuthenticationFilter tokenAuthenticationFilter; |
|||
|
|||
@Resource |
|||
private AccessDeniedHandlerImpl accessDeniedHandler; |
|||
@Resource |
|||
private AuthenticationEntryPoint authenticationEntryPoint; |
|||
|
|||
@Bean |
|||
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { |
|||
// 设置 URL 安全权限
|
|||
httpSecurity.csrf().disable() // 禁用 CSRF 保护
|
|||
.authorizeRequests() |
|||
// 1. 静态资源,可匿名访问
|
|||
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() |
|||
// 2. 登录相关的接口,可匿名访问
|
|||
.antMatchers("/auth/login-by-code").permitAll() |
|||
.antMatchers("/auth/refresh-token").permitAll() |
|||
.antMatchers("/auth/logout").permitAll() |
|||
// last. 兜底规则,必须认证
|
|||
.and().authorizeRequests() |
|||
.anyRequest().authenticated(); |
|||
|
|||
// 设置处理器
|
|||
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler) |
|||
.authenticationEntryPoint(authenticationEntryPoint); |
|||
|
|||
// 添加 Token Filter
|
|||
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); |
|||
return httpSecurity.build(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,37 @@ |
|||
package com.win.ssodemo.framework.core; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 登录用户信息 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Data |
|||
public class LoginUser { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
private Long id; |
|||
/** |
|||
* 用户类型 |
|||
*/ |
|||
private Integer userType; |
|||
/** |
|||
* 租户编号 |
|||
*/ |
|||
private Long tenantId; |
|||
/** |
|||
* 授权范围 |
|||
*/ |
|||
private List<String> scopes; |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
private String accessToken; |
|||
|
|||
} |
@ -0,0 +1,66 @@ |
|||
package com.win.ssodemo.framework.core.filter; |
|||
|
|||
import com.win.ssodemo.client.OAuth2Client; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; |
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.StringUtils; |
|||
import org.springframework.web.filter.OncePerRequestFilter; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.servlet.FilterChain; |
|||
import javax.servlet.ServletException; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
|
|||
/** |
|||
* Token 过滤器,验证 token 的有效性 |
|||
* 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Component |
|||
public class TokenAuthenticationFilter extends OncePerRequestFilter { |
|||
|
|||
@Resource |
|||
private OAuth2Client oauth2Client; |
|||
|
|||
@Override |
|||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, |
|||
FilterChain filterChain) throws ServletException, IOException { |
|||
// 1. 获得访问令牌
|
|||
String token = SecurityUtils.obtainAuthorization(request, "Authorization"); |
|||
if (StringUtils.hasText(token)) { |
|||
// 2. 基于 token 构建登录用户
|
|||
LoginUser loginUser = buildLoginUserByToken(token); |
|||
// 3. 设置当前用户
|
|||
if (loginUser != null) { |
|||
SecurityUtils.setLoginUser(loginUser, request); |
|||
} |
|||
} |
|||
|
|||
// 继续过滤链
|
|||
filterChain.doFilter(request, response); |
|||
} |
|||
|
|||
private LoginUser buildLoginUserByToken(String token) { |
|||
try { |
|||
CommonResult<OAuth2CheckTokenRespDTO> accessTokenResult = oauth2Client.checkToken(token); |
|||
OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); |
|||
if (accessToken == null) { |
|||
return null; |
|||
} |
|||
// 构建登录用户
|
|||
return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) |
|||
.setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) |
|||
.setAccessToken(accessToken.getAccessToken()); |
|||
} catch (Exception exception) { |
|||
// 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
|
|||
return null; |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,44 @@ |
|||
package com.win.ssodemo.framework.core.handler; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import com.win.ssodemo.framework.core.util.ServletUtils; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.security.access.AccessDeniedException; |
|||
import org.springframework.security.web.access.AccessDeniedHandler; |
|||
import org.springframework.security.web.access.ExceptionTranslationFilter; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.servlet.FilterChain; |
|||
import javax.servlet.ServletException; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
|
|||
/** |
|||
* 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 |
|||
* |
|||
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Component |
|||
@SuppressWarnings("JavadocReference") |
|||
@Slf4j |
|||
public class AccessDeniedHandlerImpl implements AccessDeniedHandler { |
|||
|
|||
@Override |
|||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) |
|||
throws IOException, ServletException { |
|||
// 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
|
|||
log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), |
|||
SecurityUtils.getLoginUserId(), e); |
|||
// 返回 403
|
|||
CommonResult<Object> result = new CommonResult<>(); |
|||
result.setCode(HttpStatus.FORBIDDEN.value()); |
|||
result.setMsg("没有该操作权限"); |
|||
ServletUtils.writeJSON(response, result); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,36 @@ |
|||
package com.win.ssodemo.framework.core.handler; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.framework.core.util.ServletUtils; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.security.web.AuthenticationEntryPoint; |
|||
import org.springframework.security.web.access.ExceptionTranslationFilter; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.servlet.FilterChain; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
|
|||
/** |
|||
* 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 |
|||
* |
|||
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 |
|||
*/ |
|||
@Component |
|||
@Slf4j |
|||
@SuppressWarnings("JavadocReference") // 忽略文档引用报错
|
|||
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { |
|||
|
|||
@Override |
|||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { |
|||
log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); |
|||
// 返回 401
|
|||
CommonResult<Object> result = new CommonResult<>(); |
|||
result.setCode(HttpStatus.UNAUTHORIZED.value()); |
|||
result.setMsg("账号未登录"); |
|||
ServletUtils.writeJSON(response, result); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,103 @@ |
|||
package com.win.ssodemo.framework.core.util; |
|||
|
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import org.springframework.lang.Nullable; |
|||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.context.SecurityContext; |
|||
import org.springframework.security.core.context.SecurityContextHolder; |
|||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; |
|||
import org.springframework.util.StringUtils; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.util.Collections; |
|||
|
|||
/** |
|||
* 安全服务工具类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class SecurityUtils { |
|||
|
|||
public static final String AUTHORIZATION_BEARER = "Bearer"; |
|||
|
|||
private SecurityUtils() {} |
|||
|
|||
/** |
|||
* 从请求中,获得认证 Token |
|||
* |
|||
* @param request 请求 |
|||
* @param header 认证 Token 对应的 Header 名字 |
|||
* @return 认证 Token |
|||
*/ |
|||
public static String obtainAuthorization(HttpServletRequest request, String header) { |
|||
String authorization = request.getHeader(header); |
|||
if (!StringUtils.hasText(authorization)) { |
|||
return null; |
|||
} |
|||
int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); |
|||
if (index == -1) { // 未找到
|
|||
return null; |
|||
} |
|||
return authorization.substring(index + 7).trim(); |
|||
} |
|||
|
|||
/** |
|||
* 获得当前认证信息 |
|||
* |
|||
* @return 认证信息 |
|||
*/ |
|||
public static Authentication getAuthentication() { |
|||
SecurityContext context = SecurityContextHolder.getContext(); |
|||
if (context == null) { |
|||
return null; |
|||
} |
|||
return context.getAuthentication(); |
|||
} |
|||
|
|||
/** |
|||
* 获取当前用户 |
|||
* |
|||
* @return 当前用户 |
|||
*/ |
|||
@Nullable |
|||
public static LoginUser getLoginUser() { |
|||
Authentication authentication = getAuthentication(); |
|||
if (authentication == null) { |
|||
return null; |
|||
} |
|||
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; |
|||
} |
|||
|
|||
/** |
|||
* 获得当前用户的编号,从上下文中 |
|||
* |
|||
* @return 用户编号 |
|||
*/ |
|||
@Nullable |
|||
public static Long getLoginUserId() { |
|||
LoginUser loginUser = getLoginUser(); |
|||
return loginUser != null ? loginUser.getId() : null; |
|||
} |
|||
|
|||
/** |
|||
* 设置当前用户 |
|||
* |
|||
* @param loginUser 登录用户 |
|||
* @param request 请求 |
|||
*/ |
|||
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { |
|||
// 创建 Authentication,并设置到上下文
|
|||
Authentication authentication = buildAuthentication(loginUser, request); |
|||
SecurityContextHolder.getContext().setAuthentication(authentication); |
|||
} |
|||
|
|||
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { |
|||
// 创建 UsernamePasswordAuthenticationToken 对象
|
|||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( |
|||
loginUser, null, Collections.emptyList()); |
|||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); |
|||
return authenticationToken; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,32 @@ |
|||
package com.win.ssodemo.framework.core.util; |
|||
|
|||
import cn.hutool.extra.servlet.ServletUtil; |
|||
import cn.hutool.json.JSONUtil; |
|||
import org.springframework.http.MediaType; |
|||
|
|||
import javax.servlet.http.HttpServletResponse; |
|||
|
|||
/** |
|||
* 客户端工具类 |
|||
* |
|||
* @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 = JSONUtil.toJsonStr(object); |
|||
ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); |
|||
} |
|||
|
|||
public static void write(HttpServletResponse response, String text, String contentType) { |
|||
ServletUtil.write(response, text, contentType); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,2 @@ |
|||
server: |
|||
port: 18080 |
@ -0,0 +1,61 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>SSO 授权后的回调页</title> |
|||
<!-- jQuery:操作 dom、发起请求等 --> |
|||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> |
|||
<!-- 工具类 --> |
|||
<script type="application/javascript"> |
|||
(function ($) { |
|||
/** |
|||
* 获得 URL 的指定参数的值 |
|||
* |
|||
* @param name 参数名 |
|||
* @returns 参数值 |
|||
*/ |
|||
$.getUrlParam = function (name) { |
|||
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); |
|||
const r = window.location.search.substr(1).match(reg); |
|||
if (r != null) return unescape(r[2]); return null; |
|||
} |
|||
})(jQuery); |
|||
</script> |
|||
|
|||
<script type="application/javascript"> |
|||
$(function () { |
|||
// 获得 code 授权码 |
|||
const code = $.getUrlParam('code'); |
|||
if (!code) { |
|||
alert('获取不到 code 参数,请排查!') |
|||
return; |
|||
} |
|||
|
|||
// 提交 |
|||
const redirectUri = 'http://127.0.0.1:18080/callback.html'; // 需要修改成,你回调的地址,就是在 index.html 拼接的 redirectUri |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/login-by-code?code=" + code |
|||
+ '&redirectUri=' + redirectUri, |
|||
method: 'POST', |
|||
success: function( result ) { |
|||
if (result.code !== 0) { |
|||
alert('获得访问令牌失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('获得访问令牌成功!点击确认,跳转回首页') |
|||
|
|||
// 设置到 localStorage 中 |
|||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token); |
|||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); |
|||
|
|||
// 跳转回首页 |
|||
window.location.href = '/index.html'; |
|||
} |
|||
}) |
|||
}) |
|||
</script> |
|||
</head> |
|||
<body> |
|||
正在使用 code 授权码,进行 accessToken 访问令牌的获取 |
|||
</body> |
|||
</html> |
@ -0,0 +1,159 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>首页</title> |
|||
<!-- jQuery:操作 dom、发起请求等 --> |
|||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> |
|||
|
|||
<script type="application/javascript"> |
|||
|
|||
/** |
|||
* 跳转单点登录 |
|||
*/ |
|||
function ssoLogin() { |
|||
const clientId = 'win-sso-demo-by-code'; // 可以改写成,你的 clientId |
|||
const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址 |
|||
const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token |
|||
window.location.href = 'http://127.0.0.1:1024/sso?client_id=' + clientId |
|||
+ '&redirect_uri=' + redirectUri |
|||
+ '&response_type=' + responseType; |
|||
} |
|||
|
|||
/** |
|||
* 修改昵称 |
|||
*/ |
|||
function updateNickname() { |
|||
const nickname = prompt("请输入新的昵称", ""); |
|||
if (!nickname) { |
|||
return; |
|||
} |
|||
// 更新用户的昵称 |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/user/update?nickname=" + nickname, |
|||
method: 'PUT', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('更新昵称失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('更新昵称成功!'); |
|||
$('#nicknameSpan').html(nickname); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
*/ |
|||
function refreshToken() { |
|||
const refreshToken = localStorage.getItem('REFRESH-TOKEN'); |
|||
if (!refreshToken) { |
|||
alert("获取不到刷新令牌"); |
|||
return; |
|||
} |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken, |
|||
method: 'POST', |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('刷新访问令牌失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('更新访问令牌成功!'); |
|||
$('#accessTokenSpan').html(result.data.access_token); |
|||
|
|||
// 设置到 localStorage 中 |
|||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token); |
|||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 登出,删除访问令牌 |
|||
*/ |
|||
function logout() { |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
if (!accessToken) { |
|||
location.reload(); |
|||
return; |
|||
} |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/logout", |
|||
method: 'POST', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('退出登录失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('退出登录成功!'); |
|||
// 删除 localStorage 中 |
|||
localStorage.removeItem('ACCESS-TOKEN'); |
|||
localStorage.removeItem('REFRESH-TOKEN'); |
|||
|
|||
location.reload(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
$(function () { |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
// 情况一:未登录 |
|||
if (!accessToken) { |
|||
$('#noLoginDiv').css("display", "block"); |
|||
return; |
|||
} |
|||
|
|||
// 情况二:已登录 |
|||
$('#yesLoginDiv').css("display", "block"); |
|||
$('#accessTokenSpan').html(accessToken); |
|||
// 获得登录用户的信息 |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/user/get", |
|||
method: 'GET', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('获得个人信息失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
$('#nicknameSpan').html(result.data.nickname); |
|||
} |
|||
}); |
|||
}) |
|||
</script> |
|||
</head> |
|||
<body> |
|||
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 --> |
|||
<div id="noLoginDiv" style="display: none"> |
|||
您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录 |
|||
</div> |
|||
|
|||
<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 --> |
|||
<div id="yesLoginDiv" style="display: none"> |
|||
您已登录!<button onclick="logout()">退出登录</button> <br /> |
|||
昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br /> |
|||
访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br /> |
|||
</div> |
|||
</body> |
|||
<style> |
|||
body { /** 页面居中 */ |
|||
border-radius: 20px; |
|||
height: 350px; |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%,-50%); |
|||
} |
|||
</style> |
|||
</html> |
@ -0,0 +1,65 @@ |
|||
<?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> |
|||
|
|||
<!-- 由于方便大家拷贝,使用不使用 win 作为 Maven parent --> |
|||
|
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-sso-demo-by-password</artifactId> |
|||
<version>1.0.0-snapshot</version> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>基于密码模式,如何实现 SSO 单点登录?</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<properties> |
|||
<!-- Maven 相关 --> |
|||
<maven.compiler.source>8</maven.compiler.source> |
|||
<maven.compiler.target>8</maven.compiler.target> |
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
|||
<!-- 统一依赖管理 --> |
|||
<spring.boot.version>2.7.15</spring.boot.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> |
|||
</dependencies> |
|||
</dependencyManagement> |
|||
|
|||
<dependencies> |
|||
<!-- Web 相关 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-web</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-security</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>cn.hutool</groupId> |
|||
<artifactId>hutool-all</artifactId> |
|||
<version>5.8.21</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -0,0 +1,13 @@ |
|||
package com.win.ssodemo; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
|
|||
@SpringBootApplication |
|||
public class SSODemoApplication { |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(SSODemoApplication.class, args); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,127 @@ |
|||
package com.win.ssodemo.client; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; |
|||
import org.springframework.core.ParameterizedTypeReference; |
|||
import org.springframework.http.*; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.Assert; |
|||
import org.springframework.util.Base64Utils; |
|||
import org.springframework.util.LinkedMultiValueMap; |
|||
import org.springframework.util.MultiValueMap; |
|||
import org.springframework.web.client.RestTemplate; |
|||
|
|||
import java.nio.charset.StandardCharsets; |
|||
|
|||
/** |
|||
* OAuth 2.0 客户端 |
|||
* |
|||
* 对应调用 OAuth2OpenController 接口 |
|||
*/ |
|||
@Component |
|||
public class OAuth2Client { |
|||
|
|||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2"; |
|||
|
|||
/** |
|||
* 租户编号 |
|||
* |
|||
* 默认使用 1;如果使用别的租户,可以调整 |
|||
*/ |
|||
public static final Long TENANT_ID = 1L; |
|||
|
|||
private static final String CLIENT_ID = "win-sso-demo-by-password"; |
|||
private static final String CLIENT_SECRET = "test"; |
|||
|
|||
|
|||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
|||
private final RestTemplate restTemplate = new RestTemplate(); |
|||
|
|||
/** |
|||
* 校验访问令牌,并返回它的基本信息 |
|||
* |
|||
* @param token 访问令牌 |
|||
* @return 访问令牌的基本信息 |
|||
*/ |
|||
public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("token", token); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/check-token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 使用刷新令牌,获得(刷新)访问令牌 |
|||
* |
|||
* @param refreshToken 刷新令牌 |
|||
* @return 访问令牌 |
|||
*/ |
|||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("grant_type", "refresh_token"); |
|||
body.add("refresh_token", refreshToken); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 删除访问令牌 |
|||
* |
|||
* @param token 访问令牌 |
|||
* @return 成功 |
|||
*/ |
|||
public CommonResult<Boolean> revokeToken(String token) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("token", token); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.DELETE, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
private static void addClientHeader(HttpHeaders headers) { |
|||
// client 拼接,需要 BASE64 编码
|
|||
String client = CLIENT_ID + ":" + CLIENT_SECRET; |
|||
client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); |
|||
headers.add("Authorization", "Basic " + client); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,73 @@ |
|||
package com.win.ssodemo.client; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.user.UserInfoRespDTO; |
|||
import com.win.ssodemo.client.dto.user.UserUpdateReqDTO; |
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import org.springframework.core.ParameterizedTypeReference; |
|||
import org.springframework.http.*; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.Assert; |
|||
import org.springframework.util.LinkedMultiValueMap; |
|||
import org.springframework.util.MultiValueMap; |
|||
import org.springframework.web.client.RestTemplate; |
|||
|
|||
/** |
|||
* 用户 User 信息的客户端 |
|||
* |
|||
* 对应调用 OAuth2UserController 接口 |
|||
*/ |
|||
@Component |
|||
public class UserClient { |
|||
|
|||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user"; |
|||
|
|||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
|||
private final RestTemplate restTemplate = new RestTemplate(); |
|||
|
|||
public CommonResult<UserInfoRespDTO> getUser() { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); |
|||
addTokenHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/get", |
|||
HttpMethod.GET, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
public CommonResult<Boolean> updateUser(UserUpdateReqDTO updateReqDTO) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_JSON); |
|||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); |
|||
addTokenHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
// 使用 updateReqDTO 即可
|
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/update", |
|||
HttpMethod.PUT, |
|||
new HttpEntity<>(updateReqDTO, headers), |
|||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
|
|||
private static void addTokenHeader(HttpHeaders headers) { |
|||
LoginUser loginUser = SecurityUtils.getLoginUser(); |
|||
Assert.notNull(loginUser, "登录用户不能为空"); |
|||
headers.add("Authorization", "Bearer " + loginUser.getAccessToken()); |
|||
} |
|||
} |
@ -0,0 +1,28 @@ |
|||
package com.win.ssodemo.client.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 通用返回 |
|||
* |
|||
* @param <T> 数据泛型 |
|||
*/ |
|||
@Data |
|||
public class CommonResult<T> implements Serializable { |
|||
|
|||
/** |
|||
* 错误码 |
|||
*/ |
|||
private Integer code; |
|||
/** |
|||
* 返回数据 |
|||
*/ |
|||
private T data; |
|||
/** |
|||
* 错误提示,用户可阅读 |
|||
*/ |
|||
private String msg; |
|||
|
|||
} |
@ -0,0 +1,45 @@ |
|||
package com.win.ssodemo.client.dto.oauth2; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 访问令牌 Response DTO |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class OAuth2AccessTokenRespDTO { |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
*/ |
|||
@JsonProperty("refresh_token") |
|||
private String refreshToken; |
|||
|
|||
/** |
|||
* 令牌类型 |
|||
*/ |
|||
@JsonProperty("token_type") |
|||
private String tokenType; |
|||
|
|||
/** |
|||
* 过期时间;单位:秒 |
|||
*/ |
|||
@JsonProperty("expires_in") |
|||
private Long expiresIn; |
|||
|
|||
/** |
|||
* 授权范围;如果多个授权范围,使用空格分隔 |
|||
*/ |
|||
private String scope; |
|||
|
|||
} |
@ -0,0 +1,59 @@ |
|||
package com.win.ssodemo.client.dto.oauth2; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 校验令牌 Response DTO |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class OAuth2CheckTokenRespDTO { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
@JsonProperty("user_id") |
|||
private Long userId; |
|||
/** |
|||
* 用户类型 |
|||
*/ |
|||
@JsonProperty("user_type") |
|||
private Integer userType; |
|||
/** |
|||
* 租户编号 |
|||
*/ |
|||
@JsonProperty("tenant_id") |
|||
private Long tenantId; |
|||
|
|||
/** |
|||
* 客户端编号 |
|||
*/ |
|||
@JsonProperty("client_id") |
|||
private String clientId; |
|||
/** |
|||
* 授权范围 |
|||
*/ |
|||
private List<String> scopes; |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
|
|||
/** |
|||
* 过期时间 |
|||
* |
|||
* 时间戳 / 1000,即单位:秒 |
|||
*/ |
|||
private Long exp; |
|||
|
|||
} |
@ -0,0 +1,97 @@ |
|||
package com.win.ssodemo.client.dto.user; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 获得用户基本信息 Response dto |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class UserInfoRespDTO { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 用户账号 |
|||
*/ |
|||
private String username; |
|||
|
|||
/** |
|||
* 用户昵称 |
|||
*/ |
|||
private String nickname; |
|||
|
|||
/** |
|||
* 用户邮箱 |
|||
*/ |
|||
private String email; |
|||
/** |
|||
* 手机号码 |
|||
*/ |
|||
private String mobile; |
|||
|
|||
/** |
|||
* 用户性别 |
|||
*/ |
|||
private Integer sex; |
|||
|
|||
/** |
|||
* 用户头像 |
|||
*/ |
|||
private String avatar; |
|||
|
|||
/** |
|||
* 所在部门 |
|||
*/ |
|||
private Dept dept; |
|||
|
|||
/** |
|||
* 所属岗位数组 |
|||
*/ |
|||
private List<Post> posts; |
|||
|
|||
/** |
|||
* 部门 |
|||
*/ |
|||
@Data |
|||
public static class Dept { |
|||
|
|||
/** |
|||
* 部门编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 部门名称 |
|||
*/ |
|||
private String name; |
|||
|
|||
} |
|||
|
|||
/** |
|||
* 岗位 |
|||
*/ |
|||
@Data |
|||
public static class Post { |
|||
|
|||
/** |
|||
* 岗位编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 岗位名称 |
|||
*/ |
|||
private String name; |
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,35 @@ |
|||
package com.win.ssodemo.client.dto.user; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 更新用户基本信息 Request DTO |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class UserUpdateReqDTO { |
|||
|
|||
/** |
|||
* 用户昵称 |
|||
*/ |
|||
private String nickname; |
|||
|
|||
/** |
|||
* 用户邮箱 |
|||
*/ |
|||
private String email; |
|||
|
|||
/** |
|||
* 手机号码 |
|||
*/ |
|||
private String mobile; |
|||
|
|||
/** |
|||
* 用户性别 |
|||
*/ |
|||
private Integer sex; |
|||
|
|||
} |
@ -0,0 +1,50 @@ |
|||
package com.win.ssodemo.controller; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.win.ssodemo.client.OAuth2Client; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
|
|||
@RestController |
|||
@RequestMapping("/auth") |
|||
public class AuthController { |
|||
|
|||
@Resource |
|||
private OAuth2Client oauth2Client; |
|||
|
|||
/** |
|||
* 使用刷新令牌,获得(刷新)访问令牌 |
|||
* |
|||
* @param refreshToken 刷新令牌 |
|||
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 |
|||
*/ |
|||
@PostMapping("/refresh-token") |
|||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) { |
|||
return oauth2Client.refreshToken(refreshToken); |
|||
} |
|||
|
|||
/** |
|||
* 退出登录 |
|||
* |
|||
* @param request 请求 |
|||
* @return 成功 |
|||
*/ |
|||
@PostMapping("/logout") |
|||
public CommonResult<Boolean> logout(HttpServletRequest request) { |
|||
String token = SecurityUtils.obtainAuthorization(request, "Authorization"); |
|||
if (StrUtil.isNotBlank(token)) { |
|||
return oauth2Client.revokeToken(token); |
|||
} |
|||
// 返回成功
|
|||
return new CommonResult<>(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,40 @@ |
|||
package com.win.ssodemo.controller; |
|||
|
|||
import com.win.ssodemo.client.UserClient; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.user.UserInfoRespDTO; |
|||
import com.win.ssodemo.client.dto.user.UserUpdateReqDTO; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.annotation.Resource; |
|||
|
|||
@RestController |
|||
@RequestMapping("/user") |
|||
public class UserController { |
|||
|
|||
@Resource |
|||
private UserClient userClient; |
|||
|
|||
/** |
|||
* 获得当前登录用户的基本信息 |
|||
* |
|||
* @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 |
|||
*/ |
|||
@GetMapping("/get") |
|||
public CommonResult<UserInfoRespDTO> getUser() { |
|||
return userClient.getUser(); |
|||
} |
|||
|
|||
/** |
|||
* 更新当前登录用户的昵称 |
|||
* |
|||
* @param nickname 昵称 |
|||
* @return 成功 |
|||
*/ |
|||
@PutMapping("/update") |
|||
public CommonResult<Boolean> updateUser(@RequestParam("nickname") String nickname) { |
|||
UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null); |
|||
return userClient.updateUser(updateReqDTO); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,52 @@ |
|||
package com.win.ssodemo.framework.config; |
|||
|
|||
import com.win.ssodemo.framework.core.filter.TokenAuthenticationFilter; |
|||
import com.win.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.http.HttpMethod; |
|||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
|||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
|||
import org.springframework.security.web.AuthenticationEntryPoint; |
|||
import org.springframework.security.web.SecurityFilterChain; |
|||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
|||
|
|||
import javax.annotation.Resource; |
|||
|
|||
@Configuration(proxyBeanMethods = false) |
|||
@EnableWebSecurity |
|||
public class SecurityConfiguration { |
|||
|
|||
@Resource |
|||
private TokenAuthenticationFilter tokenAuthenticationFilter; |
|||
|
|||
@Resource |
|||
private AccessDeniedHandlerImpl accessDeniedHandler; |
|||
@Resource |
|||
private AuthenticationEntryPoint authenticationEntryPoint; |
|||
|
|||
@Bean |
|||
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { |
|||
// 设置 URL 安全权限
|
|||
httpSecurity.csrf().disable() // 禁用 CSRF 保护
|
|||
.authorizeRequests() |
|||
// 1. 静态资源,可匿名访问
|
|||
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() |
|||
// 2. 登录相关的接口,可匿名访问
|
|||
.antMatchers("/auth/login-by-code").permitAll() |
|||
.antMatchers("/auth/refresh-token").permitAll() |
|||
.antMatchers("/auth/logout").permitAll() |
|||
// last. 兜底规则,必须认证
|
|||
.and().authorizeRequests() |
|||
.anyRequest().authenticated(); |
|||
|
|||
// 设置处理器
|
|||
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler) |
|||
.authenticationEntryPoint(authenticationEntryPoint); |
|||
|
|||
// 添加 Token Filter
|
|||
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); |
|||
return httpSecurity.build(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,37 @@ |
|||
package com.win.ssodemo.framework.core; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 登录用户信息 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Data |
|||
public class LoginUser { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
private Long id; |
|||
/** |
|||
* 用户类型 |
|||
*/ |
|||
private Integer userType; |
|||
/** |
|||
* 租户编号 |
|||
*/ |
|||
private Long tenantId; |
|||
/** |
|||
* 授权范围 |
|||
*/ |
|||
private List<String> scopes; |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
private String accessToken; |
|||
|
|||
} |
@ -0,0 +1,66 @@ |
|||
package com.win.ssodemo.framework.core.filter; |
|||
|
|||
import com.win.ssodemo.client.OAuth2Client; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; |
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.StringUtils; |
|||
import org.springframework.web.filter.OncePerRequestFilter; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.servlet.FilterChain; |
|||
import javax.servlet.ServletException; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
|
|||
/** |
|||
* Token 过滤器,验证 token 的有效性 |
|||
* 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Component |
|||
public class TokenAuthenticationFilter extends OncePerRequestFilter { |
|||
|
|||
@Resource |
|||
private OAuth2Client oauth2Client; |
|||
|
|||
@Override |
|||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, |
|||
FilterChain filterChain) throws ServletException, IOException { |
|||
// 1. 获得访问令牌
|
|||
String token = SecurityUtils.obtainAuthorization(request, "Authorization"); |
|||
if (StringUtils.hasText(token)) { |
|||
// 2. 基于 token 构建登录用户
|
|||
LoginUser loginUser = buildLoginUserByToken(token); |
|||
// 3. 设置当前用户
|
|||
if (loginUser != null) { |
|||
SecurityUtils.setLoginUser(loginUser, request); |
|||
} |
|||
} |
|||
|
|||
// 继续过滤链
|
|||
filterChain.doFilter(request, response); |
|||
} |
|||
|
|||
private LoginUser buildLoginUserByToken(String token) { |
|||
try { |
|||
CommonResult<OAuth2CheckTokenRespDTO> accessTokenResult = oauth2Client.checkToken(token); |
|||
OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); |
|||
if (accessToken == null) { |
|||
return null; |
|||
} |
|||
// 构建登录用户
|
|||
return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) |
|||
.setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) |
|||
.setAccessToken(accessToken.getAccessToken()); |
|||
} catch (Exception exception) { |
|||
// 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
|
|||
return null; |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,44 @@ |
|||
package com.win.ssodemo.framework.core.handler; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import com.win.ssodemo.framework.core.util.ServletUtils; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.security.access.AccessDeniedException; |
|||
import org.springframework.security.web.access.AccessDeniedHandler; |
|||
import org.springframework.security.web.access.ExceptionTranslationFilter; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.servlet.FilterChain; |
|||
import javax.servlet.ServletException; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
|
|||
/** |
|||
* 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 |
|||
* |
|||
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Component |
|||
@SuppressWarnings("JavadocReference") |
|||
@Slf4j |
|||
public class AccessDeniedHandlerImpl implements AccessDeniedHandler { |
|||
|
|||
@Override |
|||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) |
|||
throws IOException, ServletException { |
|||
// 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
|
|||
log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), |
|||
SecurityUtils.getLoginUserId(), e); |
|||
// 返回 403
|
|||
CommonResult<Object> result = new CommonResult<>(); |
|||
result.setCode(HttpStatus.FORBIDDEN.value()); |
|||
result.setMsg("没有该操作权限"); |
|||
ServletUtils.writeJSON(response, result); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,36 @@ |
|||
package com.win.ssodemo.framework.core.handler; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.framework.core.util.ServletUtils; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.security.web.AuthenticationEntryPoint; |
|||
import org.springframework.security.web.access.ExceptionTranslationFilter; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.servlet.FilterChain; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
|
|||
/** |
|||
* 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 |
|||
* |
|||
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 |
|||
*/ |
|||
@Component |
|||
@Slf4j |
|||
@SuppressWarnings("JavadocReference") // 忽略文档引用报错
|
|||
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { |
|||
|
|||
@Override |
|||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { |
|||
log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); |
|||
// 返回 401
|
|||
CommonResult<Object> result = new CommonResult<>(); |
|||
result.setCode(HttpStatus.UNAUTHORIZED.value()); |
|||
result.setMsg("账号未登录"); |
|||
ServletUtils.writeJSON(response, result); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,103 @@ |
|||
package com.win.ssodemo.framework.core.util; |
|||
|
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import org.springframework.lang.Nullable; |
|||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.context.SecurityContext; |
|||
import org.springframework.security.core.context.SecurityContextHolder; |
|||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; |
|||
import org.springframework.util.StringUtils; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.util.Collections; |
|||
|
|||
/** |
|||
* 安全服务工具类 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
public class SecurityUtils { |
|||
|
|||
public static final String AUTHORIZATION_BEARER = "Bearer"; |
|||
|
|||
private SecurityUtils() {} |
|||
|
|||
/** |
|||
* 从请求中,获得认证 Token |
|||
* |
|||
* @param request 请求 |
|||
* @param header 认证 Token 对应的 Header 名字 |
|||
* @return 认证 Token |
|||
*/ |
|||
public static String obtainAuthorization(HttpServletRequest request, String header) { |
|||
String authorization = request.getHeader(header); |
|||
if (!StringUtils.hasText(authorization)) { |
|||
return null; |
|||
} |
|||
int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); |
|||
if (index == -1) { // 未找到
|
|||
return null; |
|||
} |
|||
return authorization.substring(index + 7).trim(); |
|||
} |
|||
|
|||
/** |
|||
* 获得当前认证信息 |
|||
* |
|||
* @return 认证信息 |
|||
*/ |
|||
public static Authentication getAuthentication() { |
|||
SecurityContext context = SecurityContextHolder.getContext(); |
|||
if (context == null) { |
|||
return null; |
|||
} |
|||
return context.getAuthentication(); |
|||
} |
|||
|
|||
/** |
|||
* 获取当前用户 |
|||
* |
|||
* @return 当前用户 |
|||
*/ |
|||
@Nullable |
|||
public static LoginUser getLoginUser() { |
|||
Authentication authentication = getAuthentication(); |
|||
if (authentication == null) { |
|||
return null; |
|||
} |
|||
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; |
|||
} |
|||
|
|||
/** |
|||
* 获得当前用户的编号,从上下文中 |
|||
* |
|||
* @return 用户编号 |
|||
*/ |
|||
@Nullable |
|||
public static Long getLoginUserId() { |
|||
LoginUser loginUser = getLoginUser(); |
|||
return loginUser != null ? loginUser.getId() : null; |
|||
} |
|||
|
|||
/** |
|||
* 设置当前用户 |
|||
* |
|||
* @param loginUser 登录用户 |
|||
* @param request 请求 |
|||
*/ |
|||
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { |
|||
// 创建 Authentication,并设置到上下文
|
|||
Authentication authentication = buildAuthentication(loginUser, request); |
|||
SecurityContextHolder.getContext().setAuthentication(authentication); |
|||
} |
|||
|
|||
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { |
|||
// 创建 UsernamePasswordAuthenticationToken 对象
|
|||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( |
|||
loginUser, null, Collections.emptyList()); |
|||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); |
|||
return authenticationToken; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,32 @@ |
|||
package com.win.ssodemo.framework.core.util; |
|||
|
|||
import cn.hutool.extra.servlet.ServletUtil; |
|||
import cn.hutool.json.JSONUtil; |
|||
import org.springframework.http.MediaType; |
|||
|
|||
import javax.servlet.http.HttpServletResponse; |
|||
|
|||
/** |
|||
* 客户端工具类 |
|||
* |
|||
* @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 = JSONUtil.toJsonStr(object); |
|||
ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); |
|||
} |
|||
|
|||
public static void write(HttpServletResponse response, String text, String contentType) { |
|||
ServletUtil.write(response, text, contentType); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,2 @@ |
|||
server: |
|||
port: 18080 |
@ -0,0 +1,154 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>首页</title> |
|||
<!-- jQuery:操作 dom、发起请求等 --> |
|||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> |
|||
|
|||
<script type="application/javascript"> |
|||
|
|||
/** |
|||
* 跳转单点登录 |
|||
*/ |
|||
function passwordLogin() { |
|||
window.location.href = '/login.html' |
|||
} |
|||
|
|||
/** |
|||
* 修改昵称 |
|||
*/ |
|||
function updateNickname() { |
|||
const nickname = prompt("请输入新的昵称", ""); |
|||
if (!nickname) { |
|||
return; |
|||
} |
|||
// 更新用户的昵称 |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/user/update?nickname=" + nickname, |
|||
method: 'PUT', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('更新昵称失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('更新昵称成功!'); |
|||
$('#nicknameSpan').html(nickname); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
*/ |
|||
function refreshToken() { |
|||
const refreshToken = localStorage.getItem('REFRESH-TOKEN'); |
|||
if (!refreshToken) { |
|||
alert("获取不到刷新令牌"); |
|||
return; |
|||
} |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken, |
|||
method: 'POST', |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('刷新访问令牌失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('更新访问令牌成功!'); |
|||
$('#accessTokenSpan').html(result.data.access_token); |
|||
|
|||
// 设置到 localStorage 中 |
|||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token); |
|||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 登出,删除访问令牌 |
|||
*/ |
|||
function logout() { |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
if (!accessToken) { |
|||
location.reload(); |
|||
return; |
|||
} |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/logout", |
|||
method: 'POST', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('退出登录失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('退出登录成功!'); |
|||
// 删除 localStorage 中 |
|||
localStorage.removeItem('ACCESS-TOKEN'); |
|||
localStorage.removeItem('REFRESH-TOKEN'); |
|||
|
|||
location.reload(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
$(function () { |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
// 情况一:未登录 |
|||
if (!accessToken) { |
|||
$('#noLoginDiv').css("display", "block"); |
|||
return; |
|||
} |
|||
|
|||
// 情况二:已登录 |
|||
$('#yesLoginDiv').css("display", "block"); |
|||
$('#accessTokenSpan').html(accessToken); |
|||
// 获得登录用户的信息 |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/user/get", |
|||
method: 'GET', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('获得个人信息失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
$('#nicknameSpan').html(result.data.nickname); |
|||
} |
|||
}); |
|||
}) |
|||
</script> |
|||
</head> |
|||
<body> |
|||
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 --> |
|||
<div id="noLoginDiv" style="display: none"> |
|||
您未登录,点击 <a href="#" onclick="passwordLogin()">跳转 </a> 账号密码登录 |
|||
</div> |
|||
|
|||
<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 --> |
|||
<div id="yesLoginDiv" style="display: none"> |
|||
您已登录!<button onclick="logout()">退出登录</button> <br /> |
|||
昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br /> |
|||
访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br /> |
|||
</div> |
|||
</body> |
|||
<style> |
|||
body { /** 页面居中 */ |
|||
border-radius: 20px; |
|||
height: 350px; |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%,-50%); |
|||
} |
|||
</style> |
|||
</html> |
@ -0,0 +1,74 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>登录</title> |
|||
<!-- jQuery:操作 dom、发起请求等 --> |
|||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> |
|||
|
|||
<script type="application/javascript"> |
|||
|
|||
/** |
|||
* 账号密码登录 |
|||
*/ |
|||
function login() { |
|||
const clientId = 'win-sso-demo-by-password'; // 可以改写成,你的 clientId |
|||
const clientSecret = 'test'; // 可以改写成,你的 clientSecret |
|||
const grantType = 'password'; // 密码模式 |
|||
|
|||
// 账号 + 密码 |
|||
const username = $('#username').val(); |
|||
const password = $('#password').val(); |
|||
if (username.length === 0 || password.length === 0) { |
|||
alert('账号或密码未输入'); |
|||
return; |
|||
} |
|||
|
|||
// 发起请求 |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:48080/admin-api/system/oauth2/token?" |
|||
// 客户端 |
|||
+ "client_id=" + clientId |
|||
+ "&client_secret=" + clientSecret |
|||
// 密码模式的参数 |
|||
+ "&grant_type=" + grantType |
|||
+ "&username=" + username |
|||
+ "&password=" + password |
|||
+ '&scope=user.read user.write', |
|||
method: 'POST', |
|||
headers: { |
|||
'tenant-id': '1', // 多租户编号,写死 |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('登录失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
// 设置到 localStorage 中 |
|||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token); |
|||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); |
|||
|
|||
// 提示登录成功 |
|||
alert('登录成功!点击确认,跳转回首页'); |
|||
window.location.href = '/index.html'; |
|||
} |
|||
}); |
|||
} |
|||
</script> |
|||
</head> |
|||
<body> |
|||
账号:<input id="username" value="admin" /> <br /> |
|||
密码:<input id="password" value="admin123" > <br /> |
|||
<button style="float: right; margin-top: 5px;" onclick="login()">登录</button> |
|||
</body> |
|||
<style> |
|||
body { /** 页面居中 */ |
|||
border-radius: 20px; |
|||
height: 350px; |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%,-50%); |
|||
} |
|||
</style> |
|||
</html> |
@ -0,0 +1,60 @@ |
|||
<?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-pay</module> |
|||
<module>win-spring-boot-starter-biz-weixin</module> |
|||
<module>win-spring-boot-starter-biz-social</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,37 @@ |
|||
package com.win.framework.common.enums; |
|||
|
|||
import com.win.framework.common.core.IntArrayValuable; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
|
|||
import java.util.Arrays; |
|||
|
|||
/** |
|||
* 通用状态枚举 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@Getter |
|||
@AllArgsConstructor |
|||
public enum CommonStatusEnum implements IntArrayValuable { |
|||
|
|||
ENABLE(0, "开启"), |
|||
DISABLE(1, "关闭"); |
|||
|
|||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray(); |
|||
|
|||
/** |
|||
* 状态值 |
|||
*/ |
|||
private final Integer status; |
|||
/** |
|||
* 状态名 |
|||
*/ |
|||
private final String name; |
|||
|
|||
@Override |
|||
public int[] array() { |
|||
return ARRAYS; |
|||
} |
|||
|
|||
} |
@ -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,40 @@ |
|||
package com.win.framework.common.enums; |
|||
|
|||
import com.win.framework.common.core.IntArrayValuable; |
|||
import lombok.Getter; |
|||
import lombok.RequiredArgsConstructor; |
|||
|
|||
import java.util.Arrays; |
|||
|
|||
/** |
|||
* 终端的枚举 |
|||
* |
|||
* @author 芋道源码 |
|||
*/ |
|||
@RequiredArgsConstructor |
|||
@Getter |
|||
public enum TerminalEnum implements IntArrayValuable { |
|||
|
|||
WECHAT_MINI_PROGRAM(10, "微信小程序"), |
|||
WECHAT_WAP(11, "微信公众号"), |
|||
H5(20, "H5 网页"), |
|||
IOS(31, "苹果 App"), |
|||
ANDROID(32, "安卓 App"), |
|||
; |
|||
|
|||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray(); |
|||
|
|||
/** |
|||
* 终端 |
|||
*/ |
|||
private final Integer terminal; |
|||
/** |
|||
* 终端名 |
|||
*/ |
|||
private final String name; |
|||
|
|||
@Override |
|||
public int[] array() { |
|||
return ARRAYS; |
|||
} |
|||
} |
@ -0,0 +1,39 @@ |
|||
package com.win.framework.common.enums; |
|||
|
|||
import cn.hutool.core.util.ArrayUtil; |
|||
import com.win.framework.common.core.IntArrayValuable; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
|
|||
import java.util.Arrays; |
|||
|
|||
/** |
|||
* 全局用户类型枚举 |
|||
*/ |
|||
@AllArgsConstructor |
|||
@Getter |
|||
public enum UserTypeEnum implements IntArrayValuable { |
|||
|
|||
MEMBER(1, "会员"), // 面向 c 端,普通用户
|
|||
ADMIN(2, "管理员"); // 面向 b 端,管理后台
|
|||
|
|||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray(); |
|||
|
|||
/** |
|||
* 类型 |
|||
*/ |
|||
private final Integer value; |
|||
/** |
|||
* 类型名 |
|||
*/ |
|||
private final String name; |
|||
|
|||
public static UserTypeEnum valueOf(Integer value) { |
|||
return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values()); |
|||
} |
|||
|
|||
@Override |
|||
public int[] array() { |
|||
return ARRAYS; |
|||
} |
|||
} |
@ -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,127 @@ |
|||
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 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, 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,6 @@ |
|||
/** |
|||
* 基础的通用类,和框架无关 |
|||
* |
|||
* 例如说,CommonResult 为通用返回 |
|||
*/ |
|||
package com.win.framework.common; |
@ -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,29 @@ |
|||
package com.win.framework.common.pojo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import javax.validation.constraints.Min; |
|||
import javax.validation.constraints.Max; |
|||
import javax.validation.constraints.NotNull; |
|||
import java.io.Serializable; |
|||
|
|||
@Schema(description="分页参数") |
|||
@Data |
|||
public class PageParam implements Serializable { |
|||
|
|||
private static final Integer PAGE_NO = 1; |
|||
private static final Integer PAGE_SIZE = 10; |
|||
|
|||
@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; |
|||
|
|||
} |
@ -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,37 @@ |
|||
package com.win.framework.common.pojo; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 排序字段 DTO |
|||
* |
|||
* 类名加了 ing 的原因是,避免和 ES SortField 重名。 |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class SortingField implements Serializable { |
|||
|
|||
/** |
|||
* 顺序 - 升序 |
|||
*/ |
|||
public static final String ORDER_ASC = "asc"; |
|||
/** |
|||
* 顺序 - 降序 |
|||
*/ |
|||
public static final String ORDER_DESC = "desc"; |
|||
|
|||
/** |
|||
* 字段 |
|||
*/ |
|||
private String field; |
|||
/** |
|||
* 顺序 |
|||
*/ |
|||
private String order; |
|||
|
|||
} |
@ -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,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; |
|||
} |
|||
|
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue