Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/notify

# Conflicts:
#	sql/mysql/ruoyi-vue-pro.sql
#	yudao-ui-admin/src/utils/dict.js
This commit is contained in:
YunaiV 2023-01-27 23:25:08 +08:00
commit 143035d798
2233 changed files with 107040 additions and 43011 deletions

117
README.md
View File

@ -1,6 +1,4 @@
**严肃声明:现在、未来都不会有商业版本,所有功能全部开源!**
**拒绝虚假开源,售卖商业版,程序员不骗程序员!!**
**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!**
**「我喜欢写代码,乐此不疲」**
**「我喜欢做开源,以此为乐」**
@ -23,9 +21,11 @@
>
> 😜 给项目点点 Star 吧,这对我们真的很重要!
![架构图](https://static.iocoder.cn/ruoyi-vue-pro-architecture.png)
* 管理后台的 Vue3 版本采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) Vue2 版本采用 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
* 管理后台的移动端采用 [uni-app](https://github.com/dcloudio/uni-app) 方案,一份代码多终端适配,同时支持 APP、小程序、H5
* 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson
* 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson
* 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等
* 权限认证使用 Spring Security & Token & Redis支持多终端、多种用户的认证系统支持 SSO 单点登录
* 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能
@ -33,25 +33,42 @@
* 工作流使用 Flowable支持动态表单、在线设计流程、会签 / 或签、多种任务分配方式
* 高效率开发,使用代码生成器可以一键生成前后端代码 + 单元测试 + Swagger 接口文档 + Validator 参数校验
* 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款
* 集成阿里云、腾讯云、云片等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务
* 集成阿里云、腾讯云等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务
* 集成报表设计器,支持数据报表、图形报表、打印设计等
| 项目名 | 说明 | 传说门 |
|--------------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| 项目名 | 说明 | 传送门 |
|----------------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| `ruoyi-vue-pro` | Spring Boot 多模块 | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro)**     [Github](https://github.com/YunaiV/ruoyi-vue-pro) |
| `yudao-cloud` | Spring Cloud 微服务 | **[Gitee](https://gitee.com/zhijiantianya/yudao-cloud)**     [Github](https://github.com/YunaiV/yudao-cloud) |
| `Spring-Boot-Labs` | Spring Boot & Cloud 入门 | **[Gitee](https://gitee.com/zhijiantianya/SpringBoot-Labs)**     [Github](https://github.com/YunaiV/SpringBoot-Labs) |
| `ruoyi-vue-pro-mini` | 精简版:移除工作流、支付等模块 | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/mini)** |
## 😎 开源协议
**为什么推荐使用本项目?**
① 本项目采用比 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 行代码注释。
## 🐼 内置功能
分成多种内置功能:
系统内置多种多种业务功能,可以用于快速你的业务系统:
![功能分层](https://static.iocoder.cn/ruoyi-vue-pro-biz.png)
* 系统功能
* 基础设施
* 工作流程
* 支付系统
* 商城系统
* 会员中心
* 数据报表
* 商城系统
* 微信公众号
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
>
@ -73,13 +90,15 @@
| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 |
| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 |
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、云片等主流短信平台 |
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 |
| 🚀 | 邮件管理 | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台 |
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 |
| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 |
| | 通知公告 | 系统通知公告信息发布维护 |
| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 |
| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 |
### 工作流程
@ -135,6 +154,21 @@ ps核心功能已经实现正在对接微信小程序中...
| 🚀 | 报表设计器 | 支持数据报表、图形报表、打印设计等 |
| 🚀 | 大屏设计器 | 建设中... 拖拽式实现可视化数据大屏 |
### 微信公众号
| | 功能 | 描述 |
|-----|--------|-------------------------------|
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
### 商城系统
建设中...
@ -152,7 +186,7 @@ ps核心功能已经实现正在对接微信小程序中...
## 🐨 技术栈
| 项目 | 说明 |
|-----------------------|--------------------|
|------------------------------|--------------------|
| `yudao-dependencies` | Maven 依赖版本管理 |
| `yudao-framework` | Java 框架拓展 |
| `yudao-server` | 管理后台 + 用户 APP 的服务端 |
@ -163,60 +197,61 @@ ps核心功能已经实现正在对接微信小程序中...
| `yudao-module-system` | 系统功能的 Module 模块 |
| `yudao-module-member` | 会员中心的 Module 模块 |
| `yudao-module-infra` | 基础设施的 Module 模块 |
| `yudao-module-tool` | 研发工具的 Module 模块 |
| `yudao-module-bpm` | 工作流程的 Module 模块 |
| `yudao-module-pay` | 支付系统的 Module 模块 |
| `yudao-module-mall` | 商城系统的 Module 模块 |
| `yudao-module-mp` | 微信公众号的 Module 模块 |
| `yudao-module-visualization` | 大屏报表 Module 模块 |
### 后端
| 框架 | 说明 | 版本 | 学习指南 |
|---------------------------------------------------------------------------------------------|-----------------------|-----------|----------------------------------------------------------------|
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.6.10 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 | |
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.11 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 | |
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.17.4 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.20 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.6.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.3 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.7.0 | [文档](https://doc.iocoder.cn/bpm/) |
|---------------------------------------------------------------------------------------------|------------------|-------------|----------------------------------------------------------------|
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.7.7 | [文档](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.15 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.3.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.6.1 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [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/?yudao) |
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.24 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.7.6 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.5 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
| [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/?yudao) |
| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.6.7 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.12.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.7.10 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | |
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.3.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.24 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - |
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.0.0 | - |
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.8.0 | - |
### [管理后台 Vue2 前端](./yudao-ui-admin)
| 框架 | 说明 | 版本 |
|------------------------------------------------------------------------------|---------------|--------|
| [Vue](https://cn.vuejs.org/index.html) | JavaScript 框架 | 2.6.12 |
| [Vue](https://cn.vuejs.org/index.html) | JavaScript 框架 | 2.7.14 |
| [Vue Element Admin](https://panjiachen.github.io/vue-element-admin-site/zh/) | 后台前端解决方案 | - |
### [管理后台 Vue3 前端](./yudao-ui-admin-vue3)
| 框架 | 说明 | 版本 |
|----------------------------------------------------------------------|------------------|--------|
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.37 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.0.4 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.12 |
| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.7.4 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.17 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.0 |
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.1 |
|----------------------------------------------------------------------|:------------:|:------:|
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.45 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.0.4 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.28 |
| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.9.4 |
| [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.28 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
| [vxe-table](https://vxetable.cn/) | vue最强表单 | 4.3.9 |
### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp)
| 框架 | 说明 | 版本 |
|----------------------------------------------------------------------|------------------|--------|
|-------------------------------------------------|--------------------|--------|
| [uni-app](hhttps://github.com/dcloudio/uni-app) | 跨平台框架 | 2.0.0 |
| [uni-ui](https://github.com/dcloudio/uni-ui) | 基于 uni-app 的 UI 框架 | 1.4.20 |
@ -225,7 +260,7 @@ ps核心功能已经实现正在对接微信小程序中...
### 系统功能
| 模块 | biu | biu | biu |
|----------|--------------------------------------------------------------------|------------------------------------------------------------------|------------------------------------------------------------------|
|------------|--------------------------------------------------------------------|------------------------------------------------------------------|------------------------------------------------------------------|
| 登录 & 首页 | ![登录](https://static.iocoder.cn/images/ruoyi-vue-pro/登录.jpg?imageView2/2/format/webp/w/1280) | ![首页](https://static.iocoder.cn/images/ruoyi-vue-pro/首页.jpg?imageView2/2/format/webp/w/1280) | ![个人中心](https://static.iocoder.cn/images/ruoyi-vue-pro/个人中心.jpg?imageView2/2/format/webp/w/1280) |
| 用户 & 应用 | ![用户管理](https://static.iocoder.cn/images/ruoyi-vue-pro/用户管理.jpg?imageView2/2/format/webp/w/1280) | ![令牌管理](https://static.iocoder.cn/images/ruoyi-vue-pro/令牌管理.jpg?imageView2/2/format/webp/w/1280) | ![应用管理](https://static.iocoder.cn/images/ruoyi-vue-pro/应用管理.jpg?imageView2/2/format/webp/w/1280) |
| 租户 & 套餐 | ![租户管理](https://static.iocoder.cn/images/ruoyi-vue-pro/租户管理.jpg?imageView2/2/format/webp/w/1280) | ![租户套餐](https://static.iocoder.cn/images/ruoyi-vue-pro/租户套餐.png) | - |

View File

@ -5,7 +5,7 @@
"adminTenentId": "1",
"appApi": "http://127.0.0.1:48080/app-api",
"appToken": "test1",
"appToken": "test247",
"appTenentId": "1"
},
"gateway": {
@ -15,6 +15,6 @@
"appApi": "http://127.0.0.1:8888/app-api",
"appToken": "test1",
"appTenentId": "1"
"appTenantId": "1"
}
}

26
pom.xml
View File

@ -14,12 +14,15 @@
<module>yudao-server</module>
<!-- 各种 module 拓展 -->
<module>yudao-module-member</module>
<module>yudao-module-bpm</module>
<module>yudao-module-system</module>
<module>yudao-module-infra</module>
<module>yudao-module-pay</module>
<module>yudao-module-mall</module>
<module>yudao-module-visualization</module>
<!-- <module>yudao-module-bpm</module>-->
<!-- <module>yudao-module-visualization</module>-->
<!-- <module>yudao-module-mp</module>-->
<!-- <module>yudao-module-mall</module>-->
<!-- 示例项目 -->
<module>yudao-example</module>
</modules>
<name>${project.artifactId}</name>
@ -27,16 +30,17 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>1.6.3-snapshot</revision>
<revision>1.6.6-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.0</maven-compiler-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<!-- 看看咋放到 bom 里 -->
<lombok.version>1.18.20</lombok.version>
<mapstruct.version>1.4.1.Final</mapstruct.version>
<lombok.version>1.18.24</lombok.version>
<spring.boot.version>2.7.7</spring.boot.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@ -62,13 +66,19 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
<!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 -->
<!-- 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>

View File

@ -418,17 +418,19 @@ CREATE TABLE `jimu_report_data_source` (
`update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新日期',
`connect_times` int(1) UNSIGNED NULL DEFAULT 0 COMMENT '连接失败次数',
`tenant_id` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '多租户标识',
`type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '类型(report:报表;drag:仪表盘)',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_jmdatasource_report_id`(`report_id`) USING BTREE,
INDEX `idx_jmdatasource_code`(`code`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of jimu_report_data_source
-- ----------------------------
INSERT INTO `jimu_report_data_source` VALUES ('1324261983692902402', 'jeewx', '1324261770294071296', '', NULL, 'MYSQL', 'com.mysql.jdbc.Driver', 'jdbc:mysql://127.0.0.1:3306/jeewx-boot?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8', 'root', 'root', 'jeecg', '2020-11-05 16:07:15', NULL, '2020-11-05 16:07:15', 0);
INSERT INTO `jimu_report_data_source` VALUES ('26d21fe4f27920d2f56abc8d90a8e527', 'oracle', '1308645288868712448', '', NULL, 'ORACLE', 'oracle.jdbc.OracleDriver', 'jdbc:oracle:thin:@192.168.1.199:1521:helowin', 'jeecgbootbpm', 'jeecg196283', 'admin', '2021-01-05 19:26:24', NULL, '2021-01-05 19:26:24', 1);
INSERT INTO `jimu_report_data_source` VALUES ('8f90daf47d15d35ca6cf420748b8b9ba', 'localhost', '1316944968992034816', '', NULL, 'MYSQL5.7', 'com.mysql.cj.jdbc.Driver', 'jdbc:mysql://127.0.0.1:3306/jeecg-boot?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8', 'root', 'root', 'admin', '2021-01-13 14:34:00', NULL, '2021-01-13 14:34:00', 0);
INSERT INTO `jimu_report_data_source` VALUES ('1324261983692902402', 'jeewx', '1324261770294071296', '', NULL, 'MYSQL', 'com.mysql.jdbc.Driver', 'jdbc:mysql://127.0.0.1:3306/jeewx-boot?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8', 'root', 'root', 'jeecg', '2020-11-05 16:07:15', NULL, '2020-11-05 16:07:15', 0, NULL, 'report');
INSERT INTO `jimu_report_data_source` VALUES ('26d21fe4f27920d2f56abc8d90a8e527', 'oracle', '1308645288868712448', '', NULL, 'ORACLE', 'oracle.jdbc.OracleDriver', 'jdbc:oracle:thin:@192.168.1.199:1521:helowin', 'jeecgbootbpm', 'jeecg196283', 'admin', '2021-01-05 19:26:24', NULL, '2021-01-05 19:26:24', 1, NULL, 'report');
INSERT INTO `jimu_report_data_source` VALUES ('8f90daf47d15d35ca6cf420748b8b9ba', 'localhost', '1316944968992034816', '', NULL, 'MYSQL5.7', 'com.mysql.cj.jdbc.Driver', 'jdbc:mysql://127.0.0.1:3306/jeecg-boot?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8', 'root', 'root', 'admin', '2021-01-13 14:34:00', NULL, '2021-01-13 14:34:00', 0, NULL, 'report');
-- ----------------------------
-- Table structure for jimu_report_db
@ -1342,6 +1344,7 @@ CREATE TABLE `jimu_report_share` (
`last_update_time` datetime NULL DEFAULT NULL COMMENT '最后更新时间',
`term_of_validity` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '有效期(0:永久有效1:1天2:7天)',
`status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '是否过期(0未过期1已过期)',
`preview_lock_status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码锁状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '积木报表预览权限表' ROW_FORMAT = Dynamic;

274
sql/mysql/optional/mp.sql Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,9 @@
-- ----------------------------
-- Table structure for system_menu
-- icon 不兼容
-- 注意只有在使用 yudao-ui-admin-vue3 才进行导入
-- 注意只有在使用 yudao-ui-admin-vue3 才进行导入
-- 注意只有在使用 yudao-ui-admin-vue3 才进行导入
-- ----------------------------
DROP TABLE IF EXISTS `system_menu`;
CREATE TABLE `system_menu` (
@ -257,7 +260,8 @@ INSERT INTO `system_menu` VALUES (1264, '客户端查询', 'system:oauth2-client
INSERT INTO `system_menu` VALUES (1265, '客户端创建', 'system:oauth2-client:create', 3, 2, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:23', b'0');
INSERT INTO `system_menu` VALUES (1266, '客户端更新', 'system:oauth2-client:update', 3, 3, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:28', b'0');
INSERT INTO `system_menu` VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'ep:histogram', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'ep:histogram', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0');
INSERT INTO `system_menu` VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'ep:histogram', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0');
INSERT INTO `system_menu` VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'ep:histogram', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0');
INSERT INTO `system_menu` VALUES (1283, 'webSocket连接', '', 2, 14, 2, 'webSocket', 'ep:turn-off', 'infra/webSocket/index', 0, b'1', b'1', '1', '2023-01-01 11:43:04', '1', '2023-01-01 11:43:04', b'0');
SET FOREIGN_KEY_CHECKS = 1;

File diff suppressed because it is too large Load Diff

View File

@ -1,287 +0,0 @@
/*
Navicat Premium Data Transfer
Source Server : 127.0.0.1
Source Server Type : MySQL
Source Server Version : 80026
Source Host : localhost:3306
Source Schema : ruoyi-vue-pro
Target Server Type : MySQL
Target Server Version : 80026
File Encoding : 65001
Date: 05/02/2022 00:50:30
*/
SET
FOREIGN_KEY_CHECKS = 0;
SET NAMES utf8mb4;
-- ----------------------------
-- Table structure for product_category
-- ----------------------------
DROP TABLE IF EXISTS `product_category`;
CREATE TABLE `product_category`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '分类编号',
`parent_id` bigint NOT NULL COMMENT '父分类编号',
`name` varchar(255) NOT NULL COMMENT '分类名称',
`icon` varchar(100) NOT NULL DEFAULT '#' COMMENT '分类图标',
`banner_url` varchar(255) NOT NULL COMMENT '分类图片',
`sort` int DEFAULT '0' COMMENT '分类排序',
`description` varchar(1024) DEFAULT NULL COMMENT '分类描述',
`status` tinyint NOT NULL COMMENT '开启状态',
`creator` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) 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='商品分类';
-- ----------------------------
-- Table structure for product_brand
-- ----------------------------
DROP TABLE IF EXISTS `product_brand`;
CREATE TABLE `product_brand`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '品牌编号',
`category_id` bigint NOT NULL COMMENT '分类编号',
`name` varchar(255) NOT NULL COMMENT '品牌名称',
`banner_url` varchar(255) NOT NULL COMMENT '品牌图片',
`sort` int DEFAULT '0' COMMENT '品牌排序',
`description` varchar(1024) DEFAULT NULL COMMENT '品牌描述',
`status` tinyint NOT NULL COMMENT '状态',
`creator` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) 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='品牌';
-- TODO 父级菜单的 id 处理: 2000 2001
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES (2000, '商城', '', 1, 1, 0, '/mall', 'merchant', NULL, 0);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES (2001, '商品', '', 1, 1, 2000, 'product', 'dict', NULL, 0);
-- 商品分类 菜单 SQL
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('分类管理', '', 2, 0, 2001, 'category', '', 'mall/product/category/index', 0);
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('分类查询', 'product:category:query', 3, 1, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('分类创建', 'product:category:create', 3, 2, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('分类更新', 'product:category:update', 3, 3, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('分类删除', 'product:category:delete', 3, 4, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('分类导出', 'product:category:export', 3, 5, @parentId, '', '', '', 0);
-- 品牌管理 菜单 SQL
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('品牌管理', '', 2, 1, 2001, 'brand', '', 'mall/product/brand/index', 0);
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('品牌查询', 'product:brand:query', 3, 1, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('品牌创建', 'product:brand:create', 3, 2, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('品牌更新', 'product:brand:update', 3, 3, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('品牌删除', 'product:brand:delete', 3, 4, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('品牌导出', 'product:brand:export', 3, 5, @parentId, '', '', '', 0);
-- ----------------------------
-- Table structure for market_activity
-- ----------------------------
DROP TABLE IF EXISTS `market_activity`;
CREATE TABLE `market_activity`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '活动编号',
`title` varchar(50) NOT NULL DEFAULT '' COMMENT '活动标题',
`activity_type` tinyint(4) NOT NULL COMMENT '活动类型',
`status` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '活动状态',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`invalid_time` datetime DEFAULT NULL COMMENT '失效时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`time_limited_discount` varchar(2000) DEFAULT NULL COMMENT '限制折扣字符串使用 JSON 序列化成字符串存储',
`full_privilege` varchar(2000) DEFAULT NULL COMMENT '限制折扣字符串使用 JSON 序列化成字符串存储',
`creator` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) 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 DEFAULT CHARSET=utf8mb4 COMMENT='促销活动';
-- 规格菜单 SQL
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('规格管理', '', 2, 3, 2001, 'property', '', 'mall/product/property/index', 0);
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('规格查询', 'product:property:query', 3, 1, @parentId, '', '', '', 0);
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('规格创建', 'product:property:create', 3, 2, @parentId, '', '', '', 0);
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('规格更新', 'product:property:update', 3, 3, @parentId, '', '', '', 0);
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('规格删除', 'product:property:delete', 3, 4, @parentId, '', '', '', 0);
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('规格导出', 'product:property:export', 3, 5, @parentId, '', '', '', 0);
-- 商品菜单 SQL
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('商品管理', '', 2, 2, 2001, 'spu', '', 'mall/product/spu/index', 0);
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('商品查询', 'product:spu:query', 3, 1, @parentId, '', '', '', 0);
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('商品创建', 'product:spu:create', 3, 2, @parentId, '', '', '', 0);
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('商品更新', 'product:spu:update', 3, 3, @parentId, '', '', '', 0);
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('商品删除', 'product:spu:delete', 3, 4, @parentId, '', '', '', 0);
INSERT INTO system_menu(name, permission, type, sort, parent_id, path, icon, component, status)
VALUES ('商品导出', 'product:spu:export', 3, 5, @parentId, '', '', '', 0);
-- 规格名称表
drop table if exists product_property;
create table product_property
(
id bigint NOT NULL AUTO_INCREMENT comment '主键',
name varchar(64) comment '规格名称',
status tinyint comment '状态 0 开启 1 禁用',
create_time datetime default current_timestamp comment '创建时间',
update_time datetime default current_timestamp on update current_timestamp comment '更新时间',
creator varchar(64) comment '创建人',
updater varchar(64) comment '更新人',
tenant_id bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
primary key (id),
key idx_name ( name (32)) comment '规格名称索引'
) comment '规格名称' character set utf8mb4
collate utf8mb4_general_ci;
-- 规格值表
drop table if exists product_property_value;
create table product_property_value
(
id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
property_id bigint comment '规格键id',
name varchar(128) comment '规格值名字',
status tinyint comment '状态 1 开启 2 禁用',
create_time datetime default current_timestamp comment '创建时间',
update_time datetime default current_timestamp on update current_timestamp comment '更新时间',
creator varchar(64) comment '创建人',
updater varchar(64) comment '更新人',
tenant_id bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
primary key (id)
) comment '规格值' character set utf8mb4
collate utf8mb4_general_ci;
-- spu
drop table if exists product_spu;
create table product_spu
(
id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
name varchar(128) comment '商品名称',
sell_point varchar(128) not null comment '卖点',
description text not null comment '描述',
category_id bigint not null comment '分类id',
pic_urls varchar(1024) not null default '' comment '商品主图地址\n *\n * 数组以逗号分隔\n 最多上传15张',
sort int not null default 0 comment '排序字段',
like_count int comment '点赞初始人数',
price int comment '价格 单位使用',
quantity int comment '库存数量',
status bit(1) comment '上下架状态 0 上架开启 1 下架禁用',
create_time datetime default current_timestamp comment '创建时间',
update_time datetime default current_timestamp on update current_timestamp comment '更新时间',
creator varchar(64) comment '创建人',
updater varchar(64) comment '更新人',
tenant_id bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
primary key (id)
) comment '商品spu' character set utf8mb4
collate utf8mb4_general_ci;
-- sku
drop table if exists product_sku;
create table product_sku
(
id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
spu_id bigint not null comment 'spu编号',
properties varchar(64) not null comment '规格值数组-json格式 [{propertId: , valueId: }, {propertId: , valueId: }]',
price int not null DEFAULT -1 comment '销售价格单位',
original_price int not null DEFAULT -1 comment '原价 单位 ',
cost_price int not null DEFAULT -1 comment '成本价单位 ',
bar_code varchar(64) not null comment '条形码',
pic_url VARCHAR(128) not null comment '图片地址',
status tinyint comment '状态 0-正常 1-禁用',
create_time datetime default current_timestamp comment '创建时间',
update_time datetime default current_timestamp on update current_timestamp comment '更新时间',
creator varchar(64) comment '创建人',
updater varchar(64) comment '更新人',
tenant_id bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
primary key (id)
) comment '商品sku' character set utf8mb4
collate utf8mb4_general_ci;
---Market-Banner管理SQL
drop table if exists market_banner;
CREATE TABLE `market_banner` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Banner编号',
`title` varchar(64) NOT NULL DEFAULT '' COMMENT 'Banner标题',
`pic_url` varchar(255) NOT NULL COMMENT '图片URL',
`status` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '活动状态',
`url` varchar(255) NOT NULL COMMENT '跳转地址',
`creator` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) 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(20) NOT NULL DEFAULT '0' COMMENT '租户编号',
`sort` tinyint(4) DEFAULT NULL COMMENT '排序',
`memo` varchar(255) DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='Banner管理';
-- 菜单 SQL
INSERT INTO `system_menu`(`id`,`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES (2002, 'Banner管理', '', 2, 1, 2000, 'brand', '', 'mall/market/banner/index', 0);
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('Banner查询', 'market:banner:query', 3, 1, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('Banner创建', 'market:banner:create', 3, 2, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('Banner更新', 'market:banner:update', 3, 3, @parentId, '', '', '', 0);
INSERT INTO `system_menu`(`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`)
VALUES ('Banner删除', 'market:banner:delete', 3, 4, @parentId, '', '', '', 0);

View File

@ -2454,7 +2454,6 @@ INSERT INTO "SYSTEM_DICT_DATA" ("ID", "SORT", "LABEL", "VALUE", "DICT_TYPE", "ST
INSERT INTO "SYSTEM_DICT_DATA" ("ID", "SORT", "LABEL", "VALUE", "DICT_TYPE", "STATUS", "COLOR_TYPE", "CSS_CLASS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('62', '0', '未处理', '0', 'infra_api_error_log_process_status', '0', 'primary', NULL, NULL, NULL, TO_DATE('2021-02-26 07:07:19', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-02-16 20:14:17', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_DATA" ("ID", "SORT", "LABEL", "VALUE", "DICT_TYPE", "STATUS", "COLOR_TYPE", "CSS_CLASS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('63', '1', '已处理', '1', 'infra_api_error_log_process_status', '0', 'success', NULL, NULL, NULL, TO_DATE('2021-02-26 07:07:26', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-02-16 20:14:08', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_DATA" ("ID", "SORT", "LABEL", "VALUE", "DICT_TYPE", "STATUS", "COLOR_TYPE", "CSS_CLASS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('64', '2', '已忽略', '2', 'infra_api_error_log_process_status', '0', 'danger', NULL, NULL, NULL, TO_DATE('2021-02-26 07:07:34', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-02-16 20:14:14', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_DATA" ("ID", "SORT", "LABEL", "VALUE", "DICT_TYPE", "STATUS", "COLOR_TYPE", "CSS_CLASS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('65', '1', '云片', 'YUN_PIAN', 'system_sms_channel_code', '0', 'success', NULL, NULL, '1', TO_DATE('2021-04-05 01:05:14', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-02-16 10:09:55', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_DATA" ("ID", "SORT", "LABEL", "VALUE", "DICT_TYPE", "STATUS", "COLOR_TYPE", "CSS_CLASS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('66', '2', '阿里云', 'ALIYUN', 'system_sms_channel_code', '0', 'primary', NULL, NULL, '1', TO_DATE('2021-04-05 01:05:26', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-02-16 10:09:52', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_DATA" ("ID", "SORT", "LABEL", "VALUE", "DICT_TYPE", "STATUS", "COLOR_TYPE", "CSS_CLASS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('67', '1', '验证码', '1', 'system_sms_template_type', '0', 'warning', NULL, NULL, '1', TO_DATE('2021-04-05 21:50:57', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-02-16 12:48:30', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_DATA" ("ID", "SORT", "LABEL", "VALUE", "DICT_TYPE", "STATUS", "COLOR_TYPE", "CSS_CLASS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('68', '2', '通知', '2', 'system_sms_template_type', '0', 'primary', NULL, NULL, '1', TO_DATE('2021-04-05 21:51:08', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-02-16 12:48:27', 'SYYYY-MM-DD HH24:MI:SS'), '0');
@ -2475,7 +2474,7 @@ COMMIT;
-- Table structure for SYSTEM_DICT_TYPE
-- ----------------------------
DROP TABLE "SYSTEM_DICT_TYPE";
CREATE TABLE "SYSTEM_DICT_TYPE" (
CREATE TABLE "SYSTEM_DICT_TYPE"(
"ID" NUMBER(20,0) NOT NULL,
"NAME" NVARCHAR2(100),
"TYPE" NVARCHAR2(100),
@ -2485,6 +2484,7 @@ CREATE TABLE "SYSTEM_DICT_TYPE" (
"CREATE_TIME" DATE NOT NULL,
"UPDATER" NVARCHAR2(64),
"UPDATE_TIME" DATE NOT NULL,
"DELETED_TIME" DATE,
"DELETED" NUMBER(1,0) DEFAULT 0 NOT NULL
)
LOGGING
@ -2504,25 +2504,54 @@ PARALLEL 1
NOCACHE
DISABLE ROW MOVEMENT
;
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."ID" IS '字典主键';
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."NAME" IS '字典名称';
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."TYPE" IS '字典类型';
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."STATUS" IS '状态0正常 1停用';
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."REMARK" IS '备注';
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."CREATOR" IS '创建者';
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."CREATE_TIME" IS '创建时间';
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."UPDATER" IS '更新者';
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."UPDATE_TIME" IS '更新时间';
COMMENT ON COLUMN "SYSTEM_DICT_TYPE"."DELETED" IS '是否删除';
COMMENT ON TABLE "SYSTEM_DICT_TYPE" IS '字典类型表';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."ID" IS '字典主键';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."NAME" IS '字典名称';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."TYPE" IS '字典类型';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."STATUS" IS '状态0正常 1停用';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."REMARK" IS '备注';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."CREATOR" IS '创建者';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."CREATE_TIME" IS '创建时间';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."UPDATER" IS '更新者';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."UPDATE_TIME" IS '更新时间';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."DELETED_TIME" IS '删除时间';
COMMENT
ON COLUMN "SYSTEM_DICT_TYPE"."DELETED" IS '是否删除';
COMMENT
ON TABLE "SYSTEM_DICT_TYPE" IS '字典类型表';
-- ----------------------------
-- Records of SYSTEM_DICT_TYPE
-- ----------------------------
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('1', '用户性别', 'system_user_sex', '0', NULL, 'admin', TO_DATE('2021-01-05 17:03:48', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-05-01 12:55:56', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('6', '参数类型', 'infra_config_type', '0', NULL, 'admin', TO_DATE('2021-01-05 17:03:48', 'SYYYY-MM-DD HH24:MI:SS'), NULL, TO_DATE('2022-02-01 16:36:54', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('7', '通知类型', 'system_notice_type', '0', NULL, 'admin', TO_DATE('2021-01-05 17:03:48', 'SYYYY-MM-DD HH24:MI:SS'), NULL, TO_DATE('2022-02-01 16:35:26', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('9', '操作类型', 'system_operate_type', '0', NULL, 'admin', TO_DATE('2021-01-05 17:03:48', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-02-16 09:32:21', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER",
"UPDATE_TIME", "DELETED")
VALUES ('1', '用户性别', 'system_user_sex', '0', NULL, 'admin',
TO_DATE('2021-01-05 17:03:48', 'SYYYY-MM-DD HH24:MI:SS'), '1',
TO_DATE('2022-05-01 12:55:56', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER",
"UPDATE_TIME", "DELETED")
VALUES ('6', '参数类型', 'infra_config_type', '0', NULL, 'admin',
TO_DATE('2021-01-05 17:03:48', 'SYYYY-MM-DD HH24:MI:SS'), NULL,
TO_DATE('2022-02-01 16:36:54', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER",
"UPDATE_TIME", "DELETED")
VALUES ('7', '通知类型', 'system_notice_type', '0', NULL, 'admin',
TO_DATE('2021-01-05 17:03:48', 'SYYYY-MM-DD HH24:MI:SS'), NULL,
TO_DATE('2022-02-01 16:35:26', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER",
"UPDATE_TIME", "DELETED")
VALUES ('9', '操作类型', 'system_operate_type', '0', NULL, 'admin',
TO_DATE('2021-01-05 17:03:48', 'SYYYY-MM-DD HH24:MI:SS'), '1',
TO_DATE('2022-02-16 09:32:21', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('10', '系统状态', 'common_status', '0', NULL, 'admin', TO_DATE('2021-01-05 17:03:48', 'SYYYY-MM-DD HH24:MI:SS'), NULL, TO_DATE('2022-02-01 16:21:28', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('11', 'Boolean 是否类型', 'infra_boolean_string', '0', 'boolean 转是否', NULL, TO_DATE('2021-01-19 03:20:08', 'SYYYY-MM-DD HH24:MI:SS'), NULL, TO_DATE('2022-02-01 16:37:10', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_DICT_TYPE" ("ID", "NAME", "TYPE", "STATUS", "REMARK", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('104', '登陆结果', 'system_login_result', '0', '登陆结果', NULL, TO_DATE('2021-01-18 06:17:11', 'SYYYY-MM-DD HH24:MI:SS'), NULL, TO_DATE('2022-02-01 16:36:00', 'SYYYY-MM-DD HH24:MI:SS'), '0');
@ -3845,7 +3874,6 @@ COMMENT ON TABLE "SYSTEM_SMS_CHANNEL" IS '短信渠道';
-- ----------------------------
-- Records of SYSTEM_SMS_CHANNEL
-- ----------------------------
INSERT INTO "SYSTEM_SMS_CHANNEL" ("ID", "SIGNATURE", "CODE", "STATUS", "REMARK", "API_KEY", "API_SECRET", "CALLBACK_URL", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('1', '芋道', 'YUN_PIAN', '0', '呵呵呵哒', '1555a14277cb8a608cf45a9e6a80d510', NULL, 'http://vdwapu.natappfree.cc/admin-api/system/sms/callback/yunpian', NULL, TO_DATE('2021-03-31 06:12:20', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2022-02-23 16:48:44', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_SMS_CHANNEL" ("ID", "SIGNATURE", "CODE", "STATUS", "REMARK", "API_KEY", "API_SECRET", "CALLBACK_URL", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('2', 'Ballcat', 'ALIYUN', '0', '啦啦啦', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, NULL, TO_DATE('2021-03-31 11:53:10', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2021-04-14 00:08:37', 'SYYYY-MM-DD HH24:MI:SS'), '0');
INSERT INTO "SYSTEM_SMS_CHANNEL" ("ID", "SIGNATURE", "CODE", "STATUS", "REMARK", "API_KEY", "API_SECRET", "CALLBACK_URL", "CREATOR", "CREATE_TIME", "UPDATER", "UPDATE_TIME", "DELETED") VALUES ('4', '测试渠道', 'DEBUG_DING_TALK', '0', '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', TO_DATE('2021-04-13 00:23:14', 'SYYYY-MM-DD HH24:MI:SS'), '1', TO_DATE('2021-04-14 00:07:10', 'SYYYY-MM-DD HH24:MI:SS'), '0');
COMMIT;

View File

@ -2267,7 +2267,6 @@ INSERT INTO "system_dict_data" ("id", "sort", "label", "value", "dict_type", "st
INSERT INTO "system_dict_data" ("id", "sort", "label", "value", "dict_type", "status", "color_type", "css_class", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (62, 0, '未处理', '0', 'infra_api_error_log_process_status', 0, 'primary', '', NULL, '', '2021-02-26 07:07:19', '1', '2022-02-16 20:14:17', 0);
INSERT INTO "system_dict_data" ("id", "sort", "label", "value", "dict_type", "status", "color_type", "css_class", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (63, 1, '已处理', '1', 'infra_api_error_log_process_status', 0, 'success', '', NULL, '', '2021-02-26 07:07:26', '1', '2022-02-16 20:14:08', 0);
INSERT INTO "system_dict_data" ("id", "sort", "label", "value", "dict_type", "status", "color_type", "css_class", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (64, 2, '已忽略', '2', 'infra_api_error_log_process_status', 0, 'danger', '', NULL, '', '2021-02-26 07:07:34', '1', '2022-02-16 20:14:14', 0);
INSERT INTO "system_dict_data" ("id", "sort", "label", "value", "dict_type", "status", "color_type", "css_class", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (65, 1, '云片', 'YUN_PIAN', 'system_sms_channel_code', 0, 'success', '', NULL, '1', '2021-04-05 01:05:14', '1', '2022-02-16 10:09:55', 0);
INSERT INTO "system_dict_data" ("id", "sort", "label", "value", "dict_type", "status", "color_type", "css_class", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (66, 2, '阿里云', 'ALIYUN', 'system_sms_channel_code', 0, 'primary', '', NULL, '1', '2021-04-05 01:05:26', '1', '2022-02-16 10:09:52', 0);
INSERT INTO "system_dict_data" ("id", "sort", "label", "value", "dict_type", "status", "color_type", "css_class", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (67, 1, '验证码', '1', 'system_sms_template_type', 0, 'warning', '', NULL, '1', '2021-04-05 21:50:57', '1', '2022-02-16 12:48:30', 0);
INSERT INTO "system_dict_data" ("id", "sort", "label", "value", "dict_type", "status", "color_type", "css_class", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (68, 2, '通知', '2', 'system_sms_template_type', 0, 'primary', '', NULL, '1', '2021-04-05 21:51:08', '1', '2022-02-16 12:48:27', 0);
@ -2358,7 +2357,7 @@ COMMIT;
-- Table structure for system_dict_type
-- ----------------------------
DROP TABLE IF EXISTS "system_dict_type";
CREATE TABLE "system_dict_type" (
CREATE TABLE "system_dict_type"(
"id" int8 NOT NULL,
"name" varchar(100) COLLATE "pg_catalog"."default" NOT NULL,
"type" varchar(100) COLLATE "pg_catalog"."default" NOT NULL,
@ -2368,28 +2367,48 @@ CREATE TABLE "system_dict_type" (
"create_time" timestamp(6) NOT NULL,
"updater" varchar(64) COLLATE "pg_catalog"."default",
"update_time" timestamp(6) NOT NULL,
"deleted_time" timestamp(6),
"deleted" int2 NOT NULL DEFAULT 0
)
;
COMMENT ON COLUMN "system_dict_type"."id" IS '字典主键';
COMMENT ON COLUMN "system_dict_type"."name" IS '字典名称';
COMMENT ON COLUMN "system_dict_type"."type" IS '字典类型';
COMMENT ON COLUMN "system_dict_type"."status" IS '状态0正常 1停用';
COMMENT ON COLUMN "system_dict_type"."remark" IS '备注';
COMMENT ON COLUMN "system_dict_type"."creator" IS '创建者';
COMMENT ON COLUMN "system_dict_type"."create_time" IS '创建时间';
COMMENT ON COLUMN "system_dict_type"."updater" IS '更新者';
COMMENT ON COLUMN "system_dict_type"."update_time" IS '更新时间';
COMMENT ON COLUMN "system_dict_type"."deleted" IS '是否删除';
COMMENT ON TABLE "system_dict_type" IS '字典类型表';
COMMENT
ON COLUMN "system_dict_type"."id" IS '字典主键';
COMMENT
ON COLUMN "system_dict_type"."name" IS '字典名称';
COMMENT
ON COLUMN "system_dict_type"."type" IS '字典类型';
COMMENT
ON COLUMN "system_dict_type"."status" IS '状态0正常 1停用';
COMMENT
ON COLUMN "system_dict_type"."remark" IS '备注';
COMMENT
ON COLUMN "system_dict_type"."creator" IS '创建者';
COMMENT
ON COLUMN "system_dict_type"."create_time" IS '创建时间';
COMMENT
ON COLUMN "system_dict_type"."updater" IS '更新者';
COMMENT
ON COLUMN "system_dict_type"."update_time" IS '更新时间';
COMMENT
ON COLUMN "system_dict_type"."deleted_time" IS '删除时间';
COMMENT
ON COLUMN "system_dict_type"."deleted" IS '是否删除';
COMMENT
ON TABLE "system_dict_type" IS '字典类型表';
-- ----------------------------
-- Records of system_dict_type
-- ----------------------------
BEGIN;
INSERT INTO "system_dict_type" ("id", "name", "type", "status", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (1, '用户性别', 'system_user_sex', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:30:31', 0);
INSERT INTO "system_dict_type" ("id", "name", "type", "status", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (6, '参数类型', 'infra_config_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:36:54', 0);
INSERT INTO "system_dict_type" ("id", "name", "type", "status", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (7, '通知类型', 'system_notice_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:35:26', 0);
INSERT INTO "system_dict_type" ("id", "name", "type", "status", "remark", "creator", "create_time", "updater",
"update_time", "deleted")
VALUES (1, '用户性别', 'system_user_sex', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:30:31', 0);
INSERT INTO "system_dict_type" ("id", "name", "type", "status", "remark", "creator", "create_time", "updater",
"update_time", "deleted")
VALUES (6, '参数类型', 'infra_config_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:36:54', 0);
INSERT INTO "system_dict_type" ("id", "name", "type", "status", "remark", "creator", "create_time", "updater",
"update_time", "deleted")
VALUES (7, '通知类型', 'system_notice_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:35:26', 0);
INSERT INTO "system_dict_type" ("id", "name", "type", "status", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (9, '操作类型', 'system_operate_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:32:21', 0);
INSERT INTO "system_dict_type" ("id", "name", "type", "status", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (10, '系统状态', 'common_status', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:21:28', 0);
INSERT INTO "system_dict_type" ("id", "name", "type", "status", "remark", "creator", "create_time", "updater", "update_time", "deleted") VALUES (11, 'Boolean 是否类型', 'infra_boolean_string', 0, 'boolean 转是否', '', '2021-01-19 03:20:08', '', '2022-02-01 16:37:10', 0);
@ -3519,7 +3538,6 @@ COMMENT ON TABLE "system_sms_channel" IS '短信渠道';
-- Records of system_sms_channel
-- ----------------------------
BEGIN;
INSERT INTO "system_sms_channel" ("id", "signature", "code", "status", "remark", "api_key", "api_secret", "callback_url", "creator", "create_time", "updater", "update_time", "deleted") VALUES (1, '芋道', 'YUN_PIAN', 0, '呵呵呵哒', '1555a14277cb8a608cf45a9e6a80d510', NULL, 'http://vdwapu.natappfree.cc/admin-api/system/sms/callback/yunpian', '', '2021-03-31 06:12:20', '1', '2022-02-23 16:48:44', 0);
INSERT INTO "system_sms_channel" ("id", "signature", "code", "status", "remark", "api_key", "api_secret", "callback_url", "creator", "create_time", "updater", "update_time", "deleted") VALUES (2, 'Ballcat', 'ALIYUN', 0, '啦啦啦', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2021-04-14 00:08:37', 0);
INSERT INTO "system_sms_channel" ("id", "signature", "code", "status", "remark", "api_key", "api_secret", "callback_url", "creator", "create_time", "updater", "update_time", "deleted") VALUES (4, '测试渠道', 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', 0);
INSERT INTO "system_sms_channel" ("id", "signature", "code", "status", "remark", "api_key", "api_secret", "callback_url", "creator", "create_time", "updater", "update_time", "deleted") VALUES (6, '测试演示', 'DEBUG_DING_TALK', 0, NULL, '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2022-04-10 23:07:59', '1', '2022-04-10 23:07:59', 0);

View File

@ -5545,11 +5545,7 @@ GO
INSERT INTO [dbo].[system_dict_data] ([id], [sort], [label], [value], [dict_type], [status], [color_type], [css_class], [remark], [creator], [create_time], [updater], [update_time], [deleted]) VALUES (N'63', N'1', N'已处理', N'1', N'infra_api_error_log_process_status', N'0', N'success', N'', NULL, N'', N'2021-02-26 07:07:26.0000000', N'1', N'2022-02-16 20:14:08.0000000', N'0')
GO
INSERT INTO [dbo].[system_dict_data] ([id], [sort], [label], [value], [dict_type], [status], [color_type], [css_class], [remark], [creator], [create_time], [updater], [update_time], [deleted]) VALUES (N'64', N'2', N'已忽略', N'2', N'infra_api_error_log_process_status', N'0', N'danger', N'', NULL, N'', N'2021-02-26 07:07:34.0000000', N'1', N'2022-02-16 20:14:14.0000000', N'0')
GO
INSERT INTO [dbo].[system_dict_data] ([id], [sort], [label], [value], [dict_type], [status], [color_type], [css_class], [remark], [creator], [create_time], [updater], [update_time], [deleted]) VALUES (N'65', N'1', N'云片', N'YUN_PIAN', N'system_sms_channel_code', N'0', N'success', N'', NULL, N'1', N'2021-04-05 01:05:14.0000000', N'1', N'2022-02-16 10:09:55.0000000', N'0')
GO
INSERT INTO [dbo].[system_dict_data] ([id], [sort], [label], [value], [dict_type], [status], [color_type], [css_class], [remark], [creator], [create_time], [updater], [update_time], [deleted]) VALUES (N'66', N'2', N'阿里云', N'ALIYUN', N'system_sms_channel_code', N'0', N'primary', N'', NULL, N'1', N'2021-04-05 01:05:26.0000000', N'1', N'2022-02-16 10:09:52.0000000', N'0')
GO
@ -5817,18 +5813,51 @@ IF EXISTS (SELECT * FROM sys.all_objects WHERE object_id = OBJECT_ID(N'[dbo].[sy
DROP TABLE [dbo].[system_dict_type]
GO
CREATE TABLE [dbo].[system_dict_type] (
[id] bigint IDENTITY(1,1) NOT NULL,
[name] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[type] nvarchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
CREATE TABLE [dbo].[system_dict_type]
(
[
id]
bigint
IDENTITY
(
1,
1
) NOT NULL,
[name] nvarchar
(
100
) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[type] nvarchar
(
100
) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[status] tinyint NOT NULL,
[remark] nvarchar(500) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[creator] nvarchar(64) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[create_time] datetime2(7) NOT NULL,
[updater] nvarchar(64) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[update_time] datetime2(7) NOT NULL,
[remark] nvarchar
(
500
) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[creator] nvarchar
(
64
) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[create_time] datetime2
(
7
) NOT NULL,
[updater] nvarchar
(
64
) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[update_time] datetime2
(
7
) NOT NULL,
[deleted_time] datetime2
(
7
),
[deleted] bit DEFAULT 0 NOT NULL
)
)
GO
ALTER TABLE [dbo].[system_dict_type] SET (LOCK_ESCALATION = TABLE)
@ -5886,26 +5915,29 @@ GO
EXEC sp_addextendedproperty
'MS_Description', N'更新者',
'SCHEMA', N'dbo',
'TABLE', N'system_dict_type',
'COLUMN', N'updater'
GO
EXEC sp_addextendedproperty
'MS_Description', N'更新时间',
'SCHEMA', N'dbo',
'TABLE', N'system_dict_type',
'COLUMN', N'update_time'
GO
EXEC sp_addextendedproperty
'MS_Description', N'是否删除',
'SCHEMA', N'dbo',
'TABLE', N'system_dict_type',
'COLUMN', N'deleted'
GO
EXEC sp_addextendedproperty
'MS_Description', N'字典类型表',
'TABLE', N'system_dict_type',
'COLUMN', N'updater'
GO
EXEC sp_addextendedproperty
'MS_Description', N'更新时间',
'SCHEMA', N'dbo',
'TABLE', N'system_dict_type',
'COLUMN', N'update_time'
GO
EXEC sp_addextendedproperty
'MS_Description', N'删除时间',
'SCHEMA', N'dbo',
'TABLE', N'system_dict_type',
'COLUMN', N'deleted_time'
GO
EXEC sp_addextendedproperty
'MS_Description', N'是否删除',
'SCHEMA', N'dbo',
'TABLE', N'system_dict_type',
'COLUMN', N'deleted'
GO
EXEC sp_addextendedproperty
'MS_Description', N'字典类型表',
'SCHEMA', N'dbo',
'TABLE', N'system_dict_type'
GO
@ -9623,9 +9655,6 @@ GO
SET IDENTITY_INSERT [dbo].[system_sms_channel] ON
GO
INSERT INTO [dbo].[system_sms_channel] ([id], [signature], [code], [status], [remark], [api_key], [api_secret], [callback_url], [creator], [create_time], [updater], [update_time], [deleted]) VALUES (N'1', N'芋道', N'YUN_PIAN', N'0', N'呵呵呵哒', N'1555a14277cb8a608cf45a9e6a80d510', NULL, N'http://vdwapu.natappfree.cc/admin-api/system/sms/callback/yunpian', N'', N'2021-03-31 06:12:20.0000000', N'1', N'2022-02-23 16:48:44.0000000', N'0')
GO
INSERT INTO [dbo].[system_sms_channel] ([id], [signature], [code], [status], [remark], [api_key], [api_secret], [callback_url], [creator], [create_time], [updater], [update_time], [deleted]) VALUES (N'2', N'Ballcat', N'ALIYUN', N'0', N'啦啦啦', N'LTAI5tCnKso2uG3kJ5gRav88', N'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, N'', N'2021-03-31 11:53:10.0000000', N'1', N'2021-04-14 00:08:37.0000000', N'0')
GO

View File

@ -14,57 +14,61 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>1.6.3-snapshot</revision>
<revision>1.6.6-snapshot</revision>
<!-- 统一依赖管理 -->
<spring.boot.version>2.6.10</spring.boot.version>
<spring.boot.version>2.7.7</spring.boot.version>
<!-- Web 相关 -->
<knife4j.version>3.0.3</knife4j.version>
<swagger-annotations.version>1.6.6</swagger-annotations.version>
<knife4j.version>4.0.0</knife4j.version>
<swagger-annotations.version>1.6.8</swagger-annotations.version>
<servlet.versoin>2.5</servlet.versoin>
<!-- DB 相关 -->
<druid.version>1.2.11</druid.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
<dynamic-datasource.version>3.5.0</dynamic-datasource.version>
<redisson.version>3.17.4</redisson.version>
<!-- Config 配置中心相关 -->
<apollo.version>1.9.2</apollo.version>
<druid.version>1.2.15</druid.version>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.3.1</mybatis-plus-generator.version>
<dynamic-datasource.version>3.6.1</dynamic-datasource.version>
<redisson.version>3.18.0</redisson.version>
<!-- 服务保障相关 -->
<lock4j.version>2.2.0</lock4j.version>
<lock4j.version>2.2.3</lock4j.version>
<resilience4j.version>1.7.1</resilience4j.version>
<!-- 监控相关 -->
<skywalking.version>8.7.0</skywalking.version>
<spring-boot-admin.version>2.6.7</spring-boot-admin.version>
<opentracing.version>0.31.0</opentracing.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.6.RELEASE</podam.version>
<jedis-mock.version>0.1.16</jedis-mock.version>
<mockito-inline.version>4.0.0</mockito-inline.version>
<podam.version>7.2.11.RELEASE</podam.version>
<jedis-mock.version>1.0.5</jedis-mock.version>
<mockito-inline.version>4.11.0</mockito-inline.version>
<!-- Bpm 工作流相关 -->
<flowable.version>6.7.0</flowable.version>
<flowable.version>6.8.0</flowable.version>
<!-- 工具类相关 -->
<jasypt-spring-boot-starter.version>3.0.4</jasypt-spring-boot-starter.version>
<lombok.version>1.18.20</lombok.version>
<mapstruct.version>1.4.1.Final</mapstruct.version>
<hutool.version>5.7.22</hutool.version>
<easyexcel.verion>3.1.1</easyexcel.verion>
<velocity.version>2.2</velocity.version>
<captcha-plus.version>1.0.1</captcha-plus.version>
<jsoup.version>1.15.3</jsoup.version>
<lombok.version>1.18.24</lombok.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
<hutool.version>5.8.11</hutool.version>
<easyexcel.verion>3.1.5</easyexcel.verion>
<velocity.version>2.3</velocity.version>
<screw.version>1.0.5</screw.version>
<fastjson.version>1.2.83</fastjson.version>
<guava.version>30.1.1-jre</guava.version>
<guava.version>31.1-jre</guava.version>
<guice.version>5.1.0</guice.version>
<transmittable-thread-local.version>2.12.2</transmittable-thread-local.version>
<transmittable-thread-local.version>2.14.2</transmittable-thread-local.version>
<commons-net.version>3.8.0</commons-net.version>
<jsch.version>0.1.55</jsch.version>
<tika-core.version>2.4.1</tika-core.version>
<tika-core.version>2.6.0</tika-core.version>
<netty-all.version>4.1.86.Final</netty-all.version>
<ip2region.version>2.6.6</ip2region.version>
<!-- 三方云服务相关 -->
<minio.version>8.2.2</minio.version>
<aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version>
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
<tencentcloud-sdk-java.version>3.1.471</tencentcloud-sdk-java.version>
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
<okio.version>3.0.0</okio.version>
<okhttp3.version>4.10.0</okhttp3.version>
<minio.version>8.5.1</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.676</tencentcloud-sdk-java.version>
<justauth.version>1.4.0</justauth.version>
<jimureport.version>1.5.2</jimureport.version>
<jimureport.version>1.5.6</jimureport.version>
<xercesImpl.version>2.12.2</xercesImpl.version>
<wx-java-mp.version>4.3.0</wx-java-mp.version>
</properties>
<dependencyManagement>
@ -129,6 +133,21 @@
<artifactId>yudao-spring-boot-starter-biz-error-code</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-captcha</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-desensitize</artifactId>
<version>${revision}</version>
</dependency>
<!-- Spring 核心 -->
<dependency>
@ -153,7 +172,7 @@
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
<exclusions>
<exclusion>
@ -217,17 +236,6 @@
</dependency>
<!-- Config 配置中心相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-config</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId> <!-- 引入 Apollo Client 库,实现内嵌的配置中心 -->
<version>${apollo.version}</version>
</dependency>
<!-- Job 定时任务相关 -->
<dependency>
@ -399,12 +407,6 @@
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId> <!-- 加解密 -->
<version>${jasypt-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId>
@ -502,13 +504,48 @@
<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>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty-all.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>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-file</artifactId>
@ -521,11 +558,6 @@
</dependency>
<!-- SMS SDK begin -->
<dependency>
<groupId>com.yunpian.sdk</groupId>
<artifactId>yunpian-java-sdk</artifactId>
<version>${yunpian-java-sdk.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
@ -548,7 +580,7 @@
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<version>${tencentcloud-sdk-java.version}</version>
</dependency>
<!-- SMS SDK end -->
@ -559,11 +591,34 @@
<version>${justauth.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>${wx-java-mp.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>

22
yudao-example/pom.xml Normal file
View File

@ -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>
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-example</artifactId>
<version>1.0.0-snapshot</version>
<packaging>pom</packaging>
<modules>
<module>yudao-sso-demo-by-code</module>
<module>yudao-sso-demo-by-password</module>
</modules>
<name>${project.artifactId}</name>
<description>提供各种示例例如说SSO 单点登录</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
</project>

View File

@ -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>
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-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.7</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.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,13 @@
package cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,157 @@
package cn.iocoder.yudao.ssodemo.client;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
import cn.iocoder.yudao.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 = "yudao-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);
}
}

View File

@ -0,0 +1,73 @@
package cn.iocoder.yudao.ssodemo.client;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO;
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
import cn.iocoder.yudao.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());
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,45 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,59 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,97 @@
package cn.iocoder.yudao.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;
}
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,63 @@
package cn.iocoder.yudao.ssodemo.controller;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
import cn.iocoder.yudao.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<>();
}
}

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.ssodemo.controller;
import cn.iocoder.yudao.ssodemo.client.UserClient;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
import cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.ssodemo.framework.config;
import cn.iocoder.yudao.ssodemo.framework.core.filter.TokenAuthenticationFilter;
import cn.iocoder.yudao.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();
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,66 @@
package cn.iocoder.yudao.ssodemo.framework.core.filter;
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
import cn.iocoder.yudao.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;
}
}
}

View File

@ -0,0 +1,44 @@
package cn.iocoder.yudao.ssodemo.framework.core.handler;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
import cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.ssodemo.framework.core.handler;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,103 @@
package cn.iocoder.yudao.ssodemo.framework.core.util;
import cn.iocoder.yudao.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;
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,2 @@
server:
port: 18080

View File

@ -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>

View File

@ -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 = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId
const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址
const responseType = 'code'; // 1授权码模式对应 code2简化模式对应 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>

View File

@ -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>
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-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.7</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.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,13 @@
package cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,127 @@
package cn.iocoder.yudao.ssodemo.client;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
import cn.iocoder.yudao.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 = "yudao-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);
}
}

View File

@ -0,0 +1,73 @@
package cn.iocoder.yudao.ssodemo.client;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO;
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
import cn.iocoder.yudao.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());
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,45 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,59 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,97 @@
package cn.iocoder.yudao.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;
}
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,50 @@
package cn.iocoder.yudao.ssodemo.controller;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
import cn.iocoder.yudao.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<>();
}
}

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.ssodemo.controller;
import cn.iocoder.yudao.ssodemo.client.UserClient;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
import cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.ssodemo.framework.config;
import cn.iocoder.yudao.ssodemo.framework.core.filter.TokenAuthenticationFilter;
import cn.iocoder.yudao.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();
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.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;
}

View File

@ -0,0 +1,66 @@
package cn.iocoder.yudao.ssodemo.framework.core.filter;
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
import cn.iocoder.yudao.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;
}
}
}

View File

@ -0,0 +1,44 @@
package cn.iocoder.yudao.ssodemo.framework.core.handler;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
import cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.ssodemo.framework.core.handler;
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
import cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,103 @@
package cn.iocoder.yudao.ssodemo.framework.core.util;
import cn.iocoder.yudao.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;
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.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);
}
}

View File

@ -0,0 +1,2 @@
server:
port: 18080

View File

@ -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>

View File

@ -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 = 'yudao-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>

View File

@ -20,7 +20,6 @@
<module>yudao-spring-boot-starter-file</module>
<module>yudao-spring-boot-starter-monitor</module>
<module>yudao-spring-boot-starter-protection</module>
<module>yudao-spring-boot-starter-config</module>
<module>yudao-spring-boot-starter-job</module>
<module>yudao-spring-boot-starter-mq</module>
@ -37,8 +36,12 @@
<module>yudao-spring-boot-starter-biz-tenant</module>
<module>yudao-spring-boot-starter-biz-data-permission</module>
<module>yudao-spring-boot-starter-biz-error-code</module>
<module>yudao-spring-boot-starter-biz-ip</module>
<module>yudao-spring-boot-starter-flowable</module>
<module>yudao-spring-boot-starter-captcha</module>
<module>yudao-spring-boot-starter-websocket</module>
<module>yudao-spring-boot-starter-desensitize</module>
</modules>
<artifactId>yudao-framework</artifactId>

View File

@ -105,6 +105,11 @@
<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>

View File

@ -20,7 +20,6 @@ public enum CommonStatusEnum implements IntArrayValuable {
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray();
/**
* 状态值
*/

View File

@ -12,7 +12,8 @@ import lombok.Getter;
@AllArgsConstructor
public enum DocumentEnum {
REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档");
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;

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.framework.common.enums;
import cn.iocoder.yudao.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;
}
}

View File

@ -29,6 +29,7 @@ public interface GlobalErrorCodeConstants {
// ========== 服务端错误段 ==========
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
// ========== 自定义错误段 ==========
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.common.util.collection;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.util.ArrayUtil;
import java.util.Collection;
@ -44,7 +45,7 @@ public class ArrayUtils {
if (CollectionUtil.isEmpty(from)) {
return (T[]) (new Object[0]);
}
return ArrayUtil.toArray(from, (Class<T>) CollectionUtil.getElementType(from.iterator()));
return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator()));
}
public static <T> T get(T[] array, int index) {

View File

@ -173,6 +173,23 @@ public class CollectionUtils {
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;

View File

@ -11,6 +11,7 @@ import java.util.Set;
*/
public class SetUtils {
@SafeVarargs
public static <T> Set<T> asSet(T... objs) {
return new HashSet<>(Arrays.asList(objs));
}

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.framework.common.util.date;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import java.time.Duration;
import java.time.*;
import java.util.Calendar;
import java.util.Date;
@ -25,6 +25,37 @@ public class DateUtils {
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) {
// 将此日期时间与时区相结合以创建 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) {
// 转为时间戳
Instant instant = date.toInstant();
// UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
}
@Deprecated
public static Date addTime(Duration duration) {
return new Date(System.currentTimeMillis() + duration.toMillis());
}
@ -33,6 +64,11 @@ public class DateUtils {
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();
}
@ -83,12 +119,14 @@ public class DateUtils {
return a.compareTo(b) > 0 ? a : b;
}
public static boolean beforeNow(Date date) {
return date.getTime() < System.currentTimeMillis();
public static LocalDateTime max(LocalDateTime a, LocalDateTime b) {
if (a == null) {
return b;
}
public static boolean afterNow(Date date) {
return date.getTime() >= System.currentTimeMillis();
if (b == null) {
return a;
}
return a.isAfter(b) ? a : b;
}
/**
@ -128,11 +166,8 @@ public class DateUtils {
* @param date 日期
* @return 是否
*/
public static boolean isToday(Date date) {
if (date == null) {
return false;
}
return DateUtil.isSameDay(date, new Date());
public static boolean isToday(LocalDateTime date) {
return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now());
}
}

View File

@ -0,0 +1,63 @@
package cn.iocoder.yudao.framework.common.util.date;
import cn.hutool.core.date.LocalDateTimeUtil;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 时间工具类用于 {@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);
}
}

View File

@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
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.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
@ -28,6 +29,7 @@ public class JsonUtils {
static {
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
}
/**

View File

@ -1,12 +1,10 @@
package cn.iocoder.yudao.framework.common.util.object;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Consumer;
/**
@ -47,6 +45,7 @@ public class ObjectUtils {
return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
}
@SafeVarargs
public static <T> T defaultIfNull(T... array) {
for (T item : array) {
if (item != null) {
@ -56,6 +55,7 @@ public class ObjectUtils {
return null;
}
@SafeVarargs
public static <T> boolean equalsAny(T obj, T... array) {
return Arrays.asList(array).contains(obj);
}

View File

@ -3,7 +3,10 @@ package cn.iocoder.yudao.framework.common.util.string;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* 字符串工具类
@ -37,4 +40,9 @@ public class StrUtils {
return false;
}
public static List<Long> splitToLong(String value, CharSequence separator) {
long[] longs = StrUtil.splitToLong(value, separator);
return Arrays.stream(longs).boxed().collect(Collectors.toList());
}
}

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.framework.common.util.validation;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintViolation;
@ -17,16 +16,15 @@ import java.util.regex.Pattern;
*/
public class ValidationUtils {
private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[189]))\\d{8}$");
private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
public static boolean isMobile(String mobile) {
if (StrUtil.length(mobile) != 11) {
return false;
}
// TODO 芋艿后面完善手机校验
return true;
return StringUtils.hasText(mobile)
&& PATTERN_MOBILE.matcher(mobile).matches();
}
public static boolean isURL(String url) {

View File

@ -1,15 +1,15 @@
package cn.iocoder.yudao.framework.banner.config;
import cn.iocoder.yudao.framework.banner.core.BannerApplicationRunner;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Banner 的自动配置类
*
* @author 芋道源码
*/
@Configuration
@AutoConfiguration
public class YudaoBannerAutoConfiguration {
@Bean

View File

@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration

View File

@ -0,0 +1 @@
cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration

View File

@ -7,8 +7,8 @@ import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFac
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@ -17,7 +17,7 @@ import java.util.List;
*
* @author 芋道源码
*/
@Configuration
@AutoConfiguration
public class YudaoDataPermissionAutoConfiguration {
@Bean
@ -27,9 +27,8 @@ public class YudaoDataPermissionAutoConfiguration {
@Bean
public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor,
List<DataPermissionRule> rules) {
DataPermissionRuleFactory ruleFactory) {
// 创建 DataPermissionDatabaseInterceptor 拦截器
DataPermissionRuleFactory ruleFactory = dataPermissionRuleFactory(rules);
DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory);
// 添加到 interceptor
// 需要加在首个主要是为了在分页插件前面这个是 MyBatis Plus 的规定

View File

@ -4,10 +4,10 @@ import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissi
import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@ -16,7 +16,7 @@ import java.util.List;
*
* @author 芋道源码
*/
@Configuration
@AutoConfiguration
@ConditionalOnClass(LoginUser.class)
@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class})
public class YudaoDeptDataPermissionAutoConfiguration {

View File

@ -18,7 +18,6 @@ import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.*;
@ -37,7 +36,7 @@ import java.util.concurrent.ConcurrentHashMap;
/**
* 数据权限拦截器通过 {@link DataPermissionRule} 数据权限规则重写 SQL 的方式来实现
* 主要的 SQL 重写方法可见 {@link #builderExpression(Expression, Table)} 方法
* 主要的 SQL 重写方法可见 {@link #builderExpression(Expression, List)} 方法
*
* 整体的代码实现上参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现
* 所以每次 MyBatis Plus 升级时需要 Review 下其具体的实现是否有变更
@ -53,8 +52,7 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
private final MappedStatementCache mappedStatementCache = new MappedStatementCache();
@Override // SELECT 场景
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 获得 Mapper 对应的数据权限的规则
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId());
if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写则跳过
@ -68,12 +66,14 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
// 处理 SQL
mpBs.sql(parserSingle(mpBs.sql(), null));
} finally {
// 添加是否需要重写的缓存
addMappedStatementCache(ms);
// 清空上下文
ContextHolder.clear();
}
}
@Override // 只处理 UPDATE / DELETE 场景不处理 INSERT 场景
@Override // 只处理 UPDATE / DELETE 场景不处理 INSERT 场景因为 INSERT 不需要数据权限)
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
@ -92,7 +92,9 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
// 处理 SQL
mpBs.sql(parserMulti(mpBs.sql(), null));
} finally {
// 添加是否需要重写的缓存
addMappedStatementCache(ms);
// 清空上下文
ContextHolder.clear();
}
}
@ -107,24 +109,6 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
}
}
protected void processSelectBody(SelectBody selectBody) {
if (selectBody == null) {
return;
}
if (selectBody instanceof PlainSelect) {
processPlainSelect((PlainSelect) selectBody);
} else if (selectBody instanceof WithItem) {
WithItem withItem = (WithItem) selectBody;
processSelectBody(withItem.getSubSelect().getSelectBody());
} else {
SetOperationList operationList = (SetOperationList) selectBody;
List<SelectBody> selectBodys = operationList.getSelects();
if (CollectionUtils.isNotEmpty(selectBodys)) {
selectBodys.forEach(this::processSelectBody);
}
}
}
/**
* update 语句处理
*/
@ -142,28 +126,77 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable()));
}
// ========== TenantLineInnerInterceptor 一致的逻辑 ==========
protected void processSelectBody(SelectBody selectBody) {
if (selectBody == null) {
return;
}
if (selectBody instanceof PlainSelect) {
processPlainSelect((PlainSelect) selectBody);
} else if (selectBody instanceof WithItem) {
WithItem withItem = (WithItem) selectBody;
processSelectBody(withItem.getSubSelect().getSelectBody());
} else {
SetOperationList operationList = (SetOperationList) selectBody;
List<SelectBody> selectBodyList = operationList.getSelects();
if (CollectionUtils.isNotEmpty(selectBodyList)) {
selectBodyList.forEach(this::processSelectBody);
}
}
}
/**
* 处理 PlainSelect
*/
protected void processPlainSelect(PlainSelect plainSelect) {
FromItem fromItem = plainSelect.getFromItem();
Expression where = plainSelect.getWhere();
processWhereSubSelect(where);
if (fromItem instanceof Table) {
Table fromTable = (Table) fromItem;
plainSelect.setWhere(builderExpression(where, fromTable));
} else {
processFromItem(fromItem);
}
//#3087 github
List<SelectItem> selectItems = plainSelect.getSelectItems();
if (CollectionUtils.isNotEmpty(selectItems)) {
selectItems.forEach(this::processSelectItem);
}
// 处理 where 中的子查询
Expression where = plainSelect.getWhere();
processWhereSubSelect(where);
// 处理 fromItem
FromItem fromItem = plainSelect.getFromItem();
List<Table> list = processFromItem(fromItem);
List<Table> mainTables = new ArrayList<>(list);
// 处理 join
List<Join> joins = plainSelect.getJoins();
if (CollectionUtils.isNotEmpty(joins)) {
processJoins(joins);
mainTables = processJoins(mainTables, joins);
}
// 当有 mainTable 进行 where 条件追加
if (CollectionUtils.isNotEmpty(mainTables)) {
plainSelect.setWhere(builderExpression(where, mainTables));
}
}
private List<Table> processFromItem(FromItem fromItem) {
// 处理括号括起来的表达式
while (fromItem instanceof ParenthesisFromItem) {
fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
}
List<Table> mainTables = new ArrayList<>();
// join 时的处理逻辑
if (fromItem instanceof Table) {
Table fromTable = (Table) fromItem;
mainTables.add(fromTable);
} else if (fromItem instanceof SubJoin) {
// SubJoin 类型则还需要添加上 where 条件
List<Table> tables = processSubJoin((SubJoin) fromItem);
mainTables.addAll(tables);
} else {
// 处理下 fromItem
processOtherFromItem(fromItem);
}
return mainTables;
}
/**
@ -191,7 +224,7 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
return;
}
if (where instanceof FromItem) {
processFromItem((FromItem) where);
processOtherFromItem((FromItem) where);
return;
}
if (where.toString().indexOf("SELECT") > 0) {
@ -204,9 +237,9 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
} else if (where instanceof InExpression) {
// in
InExpression expression = (InExpression) where;
ItemsList itemsList = expression.getRightItemsList();
if (itemsList instanceof SubSelect) {
processSelectBody(((SubSelect) itemsList).getSelectBody());
Expression inExpression = expression.getRightExpression();
if (inExpression instanceof SubSelect) {
processSelectBody(((SubSelect) inExpression).getSelectBody());
}
} else if (where instanceof ExistsExpression) {
// exists
@ -239,7 +272,7 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
* <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)<p>
* <p> fixed gitee pulls/141</p>
*
* @param function 函数
* @param function
*/
protected void processFunction(Function function) {
ExpressionList parameters = function.getParameters();
@ -257,22 +290,19 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
/**
* 处理子查询等
*/
protected void processFromItem(FromItem fromItem) {
if (fromItem instanceof SubJoin) {
SubJoin subJoin = (SubJoin) fromItem;
if (subJoin.getJoinList() != null) {
processJoins(subJoin.getJoinList());
protected void processOtherFromItem(FromItem fromItem) {
// 去除括号
while (fromItem instanceof ParenthesisFromItem) {
fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
}
if (subJoin.getLeft() != null) {
processFromItem(subJoin.getLeft());
}
} else if (fromItem instanceof SubSelect) {
if (fromItem instanceof SubSelect) {
SubSelect subSelect = (SubSelect) fromItem;
if (subSelect.getSelectBody() != null) {
processSelectBody(subSelect.getSelectBody());
}
} else if (fromItem instanceof ValuesList) {
logger.debug("Perform a subquery, if you do not give us feedback");
logger.debug("Perform a subQuery, if you do not give us feedback");
} else if (fromItem instanceof LateralSubSelect) {
LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
if (lateralSubSelect.getSubSelect() != null) {
@ -284,75 +314,176 @@ public class DataPermissionDatabaseInterceptor extends JsqlParserSupport impleme
}
}
/**
* 处理 sub join
*
* @param subJoin subJoin
* @return Table subJoin 中的主表
*/
private List<Table> processSubJoin(SubJoin subJoin) {
List<Table> mainTables = new ArrayList<>();
if (subJoin.getJoinList() != null) {
List<Table> list = processFromItem(subJoin.getLeft());
mainTables.addAll(list);
mainTables = processJoins(mainTables, subJoin.getJoinList());
}
return mainTables;
}
/**
* 处理 joins
*
* @param mainTables 可以为 null
* @param joins join 集合
* @return List<Table> 右连接查询的 Table 列表
*/
private void processJoins(List<Join> joins) {
private List<Table> processJoins(List<Table> mainTables, List<Join> joins) {
// join 表达式中最终的主表
Table mainTable = null;
// 当前 join 的左表
Table leftTable = null;
if (mainTables == null) {
mainTables = new ArrayList<>();
} else if (mainTables.size() == 1) {
mainTable = mainTables.get(0);
leftTable = mainTable;
}
//对于 on 表达式写在最后的 join需要记录下前面多个 on 的表名
Deque<Table> tables = new LinkedList<>();
Deque<List<Table>> onTableDeque = new LinkedList<>();
for (Join join : joins) {
// 处理 on 表达式
FromItem fromItem = join.getRightItem();
if (fromItem instanceof Table) {
Table fromTable = (Table) fromItem;
FromItem joinItem = join.getRightItem();
// 获取当前 join 的表subJoint 可以看作是一张表
List<Table> joinTables = null;
if (joinItem instanceof Table) {
joinTables = new ArrayList<>();
joinTables.add((Table) joinItem);
} else if (joinItem instanceof SubJoin) {
joinTables = processSubJoin((SubJoin) joinItem);
}
if (joinTables != null) {
// 如果是隐式内连接
if (join.isSimple()) {
mainTables.addAll(joinTables);
continue;
}
// 当前表是否忽略
Table joinTable = joinTables.get(0);
List<Table> onTables = null;
// 如果不要忽略且是右连接则记录下当前表
if (join.isRight()) {
mainTable = joinTable;
if (leftTable != null) {
onTables = Collections.singletonList(leftTable);
}
} else if (join.isLeft()) {
onTables = Collections.singletonList(joinTable);
} else if (join.isInner()) {
if (mainTable == null) {
onTables = Collections.singletonList(joinTable);
} else {
onTables = Arrays.asList(mainTable, joinTable);
}
mainTable = null;
}
mainTables = new ArrayList<>();
if (mainTable != null) {
mainTables.add(mainTable);
}
// 获取 join 尾缀的 on 表达式列表
Collection<Expression> originOnExpressions = join.getOnExpressions();
// 正常 join on 表达式只有一个立刻处理
if (originOnExpressions.size() == 1) {
processJoin(join);
if (originOnExpressions.size() == 1 && onTables != null) {
List<Expression> onExpressions = new LinkedList<>();
onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables));
join.setOnExpressions(onExpressions);
leftTable = joinTable;
continue;
}
tables.push(fromTable);
// 表名压栈忽略的表压入 null以便后续不处理
onTableDeque.push(onTables);
// 尾缀多个 on 表达式的时候统一处理
if (originOnExpressions.size() > 1) {
Collection<Expression> onExpressions = new LinkedList<>();
for (Expression originOnExpression : originOnExpressions) {
Table currentTable = tables.poll();
onExpressions.add(builderExpression(originOnExpression, currentTable));
List<Table> currentTableList = onTableDeque.poll();
if (CollectionUtils.isEmpty(currentTableList)) {
onExpressions.add(originOnExpression);
} else {
onExpressions.add(builderExpression(originOnExpression, currentTableList));
}
}
join.setOnExpressions(onExpressions);
}
leftTable = joinTable;
} else {
// 处理右边连接的子表达式
processFromItem(fromItem);
}
processOtherFromItem(joinItem);
leftTable = null;
}
}
/**
* 处理联接语句
*/
protected void processJoin(Join join) {
if (join.getRightItem() instanceof Table) {
Table fromTable = (Table) join.getRightItem();
Expression originOnExpression = CollUtil.getFirst(join.getOnExpressions());
originOnExpression = builderExpression(originOnExpression, fromTable);
join.setOnExpressions(CollUtil.newArrayList(originOnExpression));
return mainTables;
}
// ========== TenantLineInnerInterceptor 存在差异的逻辑关键实现权限条件的拼接 ==========
/**
* 处理条件
*
* @param currentExpression 当前 where 条件
* @param table 单个表
*/
protected Expression builderExpression(Expression currentExpression, Table table) {
return this.builderExpression(currentExpression, Collections.singletonList(table));
}
/**
* 处理条件
*
* @param currentExpression 当前 where 条件
* @param tables 多个表
*/
protected Expression builderExpression(Expression currentExpression, Table table) {
// 获得 Table 对应的数据权限条件
Expression equalsTo = buildDataPermissionExpression(table);
if (equalsTo == null) { // 如果没条件则返回 currentExpression 默认
protected Expression builderExpression(Expression currentExpression, List<Table> tables) {
// 没有表需要处理直接返回
if (CollectionUtils.isEmpty(tables)) {
return currentExpression;
}
// 表达式为空则直接返回 equalsTo
// 第一步获得 Table 对应的数据权限条件
Expression dataPermissionExpression = null;
for (Table table : tables) {
// 构建每个表的权限 Expression 条件
Expression expression = buildDataPermissionExpression(table);
if (expression == null) {
continue;
}
// 合并到 dataPermissionExpression
dataPermissionExpression = dataPermissionExpression == null ? expression
: new AndExpression(dataPermissionExpression, expression);
}
// 第二步合并多个 Expression 条件
if (dataPermissionExpression == null) {
return currentExpression;
}
if (currentExpression == null) {
return equalsTo;
return dataPermissionExpression;
}
// 如果表达式为 Or则需要 (currentExpression) AND equalsTo
// 如果表达式为 Or则需要 (currentExpression) AND dataPermissionExpression
if (currentExpression instanceof OrExpression) {
return new AndExpression(new Parenthesis(currentExpression), equalsTo);
return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression);
}
// 如果表达式为 And则直接返回 currentExpression AND equalsTo
return new AndExpression(currentExpression, equalsTo);
// 如果表达式为 And则直接返回 where AND dataPermissionExpression
return new AndExpression(currentExpression, dataPermissionExpression);
}
/**

View File

@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
import cn.iocoder.yudao.framework.expression.OrExpressionX;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.framework.security.core.LoginUser;
@ -20,7 +21,6 @@ import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
@ -143,8 +143,8 @@ public class DeptDataPermissionRule implements DataPermissionRule {
if (userExpression == null) {
return deptExpression;
}
// 目前如果有指定部门 + 可查看自己采用 OR 条件WHERE dept_id IN ? OR user_id = ?
return new OrExpression(deptExpression, userExpression);
// 目前如果有指定部门 + 可查看自己采用 OR 条件WHERE (dept_id IN ? OR user_id = ?)
return new OrExpressionX(deptExpression, userExpression);
}
private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) {

View File

@ -1,3 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration,\
cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration

View File

@ -0,0 +1,2 @@
cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration
cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration

View File

@ -87,7 +87,7 @@ public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest {
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
// 断言
verify(mpBs, times(1)).sql(
eq("SELECT * FROM t_user WHERE id = 1 AND dept_id = 100"));
eq("SELECT * FROM t_user WHERE id = 1 AND t_user.dept_id = 100"));
// 断言缓存
assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
}

View File

@ -46,7 +46,7 @@ public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest
@Override
public Set<String> getTableNames() {
return asSet("entity", "entity1", "entity2", "t1", "t2", // 支持 MyBatis Plus 的单元测试
return asSet("entity", "entity1", "entity2", "entity3", "t1", "t2", "sys_dict_item", // 支持 MyBatis Plus 的单元测试
"t_user", "t_role"); // 满足自己的单元测试
}
@ -84,30 +84,30 @@ public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest
@Test
void delete() {
assertSql("delete from entity where id = ?",
"DELETE FROM entity WHERE id = ? AND tenant_id = 1");
"DELETE FROM entity WHERE id = ? AND entity.tenant_id = 1");
}
@Test
void update() {
assertSql("update entity set name = ? where id = ?",
"UPDATE entity SET name = ? WHERE id = ? AND tenant_id = 1");
"UPDATE entity SET name = ? WHERE id = ? AND entity.tenant_id = 1");
}
@Test
void selectSingle() {
// 单表
assertSql("select * from entity where id = ?",
"SELECT * FROM entity WHERE id = ? AND tenant_id = 1");
"SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1");
assertSql("select * from entity where id = ? or name = ?",
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1");
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1");
assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)",
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1");
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1");
/* not */
assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)",
"SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND tenant_id = 1");
"SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND entity.tenant_id = 1");
}
@Test
@ -167,10 +167,12 @@ public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest
assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)",
"SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
/* <= */
assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)",
"SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
/* <> */
assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)",
"SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
@ -204,6 +206,14 @@ public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest
"SELECT * FROM entity e " +
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
assertSql("SELECT * FROM entity e " +
"left join entity1 e1 on e1.id = e.id " +
"left join entity2 e2 on e1.id = e2.id",
"SELECT * FROM entity e " +
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " +
"WHERE e.tenant_id = 1");
}
@Test
@ -212,17 +222,125 @@ public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest
assertSql("SELECT * FROM entity e " +
"right join entity1 e1 on e1.id = e.id",
"SELECT * FROM entity e " +
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"WHERE e.tenant_id = 1");
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
"WHERE e1.tenant_id = 1");
assertSql("SELECT * FROM with_as_1 e " +
"right join entity1 e1 on e1.id = e.id",
"SELECT * FROM with_as_1 e " +
"RIGHT JOIN entity1 e1 ON e1.id = e.id " +
"WHERE e1.tenant_id = 1");
assertSql("SELECT * FROM entity e " +
"right join entity1 e1 on e1.id = e.id " +
"WHERE e.id = ? OR e.name = ?",
"SELECT * FROM entity e " +
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
assertSql("SELECT * FROM entity e " +
"right join entity1 e1 on e1.id = e.id " +
"right join entity2 e2 on e1.id = e2.id ",
"SELECT * FROM entity e " +
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
"RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " +
"WHERE e2.tenant_id = 1");
}
@Test
void selectMixJoin() {
assertSql("SELECT * FROM entity e " +
"right join entity1 e1 on e1.id = e.id " +
"left join entity2 e2 on e1.id = e2.id",
"SELECT * FROM entity e " +
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " +
"LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " +
"WHERE e1.tenant_id = 1");
assertSql("SELECT * FROM entity e " +
"left join entity1 e1 on e1.id = e.id " +
"right join entity2 e2 on e1.id = e2.id",
"SELECT * FROM entity e " +
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " +
"WHERE e2.tenant_id = 1");
assertSql("SELECT * FROM entity e " +
"left join entity1 e1 on e1.id = e.id " +
"inner join entity2 e2 on e1.id = e2.id",
"SELECT * FROM entity e " +
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"INNER JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 AND e2.tenant_id = 1");
}
@Test
void selectJoinSubSelect() {
assertSql("select * from (select * from entity) e1 " +
"left join entity2 e2 on e1.id = e2.id",
"SELECT * FROM (SELECT * FROM entity WHERE entity.tenant_id = 1) e1 " +
"LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1");
assertSql("select * from entity1 e1 " +
"left join (select * from entity2) e2 " +
"on e1.id = e2.id",
"SELECT * FROM entity1 e1 " +
"LEFT JOIN (SELECT * FROM entity2 WHERE entity2.tenant_id = 1) e2 " +
"ON e1.id = e2.id " +
"WHERE e1.tenant_id = 1");
}
@Test
void selectSubJoin() {
assertSql("select * FROM " +
"(entity1 e1 right JOIN entity2 e2 ON e1.id = e2.id)",
"SELECT * FROM " +
"(entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " +
"WHERE e2.tenant_id = 1");
assertSql("select * FROM " +
"(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id)",
"SELECT * FROM " +
"(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
"WHERE e1.tenant_id = 1");
assertSql("select * FROM " +
"(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id) " +
"right join entity3 e3 on e1.id = e3.id",
"SELECT * FROM " +
"(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
"RIGHT JOIN entity3 e3 ON e1.id = e3.id AND e1.tenant_id = 1 " +
"WHERE e3.tenant_id = 1");
assertSql("select * FROM entity e " +
"LEFT JOIN (entity1 e1 right join entity2 e2 ON e1.id = e2.id) " +
"on e.id = e2.id",
"SELECT * FROM entity e " +
"LEFT JOIN (entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " +
"ON e.id = e2.id AND e2.tenant_id = 1 " +
"WHERE e.tenant_id = 1");
assertSql("select * FROM entity e " +
"LEFT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " +
"on e.id = e2.id",
"SELECT * FROM entity e " +
"LEFT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
"ON e.id = e2.id AND e1.tenant_id = 1 " +
"WHERE e.tenant_id = 1");
assertSql("select * FROM entity e " +
"RIGHT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " +
"on e.id = e2.id",
"SELECT * FROM entity e " +
"RIGHT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " +
"ON e.id = e2.id AND e.tenant_id = 1 " +
"WHERE e1.tenant_id = 1");
}
@Test
void selectLeftJoinMultipleTrailingOn() {
// 多个 on 尾缀的
@ -256,51 +374,97 @@ public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest
"inner join entity1 e1 on e1.id = e.id " +
"WHERE e.id = ? OR e.name = ?",
"SELECT * FROM entity e " +
"INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
"INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " +
"WHERE e.id = ? OR e.name = ?");
assertSql("SELECT * FROM entity e " +
"inner join entity1 e1 on e1.id = e.id " +
"WHERE (e.id = ? OR e.name = ?)",
"SELECT * FROM entity e " +
"INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
"INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?)");
// 隐式内连接
assertSql("SELECT * FROM entity,entity1 " +
"WHERE entity.id = entity1.id",
"SELECT * FROM entity, entity1 " +
"WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
// 隐式内连接
assertSql("SELECT * FROM entity a, with_as_entity1 b " +
"WHERE a.id = b.id",
"SELECT * FROM entity a, with_as_entity1 b " +
"WHERE a.id = b.id AND a.tenant_id = 1");
assertSql("SELECT * FROM with_as_entity a, with_as_entity1 b " +
"WHERE a.id = b.id",
"SELECT * FROM with_as_entity a, with_as_entity1 b " +
"WHERE a.id = b.id");
// SubJoin with 隐式内连接
assertSql("SELECT * FROM (entity,entity1) " +
"WHERE entity.id = entity1.id",
"SELECT * FROM (entity, entity1) " +
"WHERE entity.id = entity1.id " +
"AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
assertSql("SELECT * FROM ((entity,entity1),entity2) " +
"WHERE entity.id = entity1.id and entity.id = entity2.id",
"SELECT * FROM ((entity, entity1), entity2) " +
"WHERE entity.id = entity1.id AND entity.id = entity2.id " +
"AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1");
assertSql("SELECT * FROM (entity,(entity1,entity2)) " +
"WHERE entity.id = entity1.id and entity.id = entity2.id",
"SELECT * FROM (entity, (entity1, entity2)) " +
"WHERE entity.id = entity1.id AND entity.id = entity2.id " +
"AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1");
// 沙雕的括号写法
assertSql("SELECT * FROM (((entity,entity1))) " +
"WHERE entity.id = entity1.id",
"SELECT * FROM (((entity, entity1))) " +
"WHERE entity.id = entity1.id " +
"AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
// 垃圾 inner join todo
// assertSql("SELECT * FROM entity,entity1 " +
// "WHERE entity.id = entity1.id",
// "SELECT * FROM entity e " +
// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
}
@Test
void selectWithAs() {
assertSql("with with_as_A as (select * from entity) select * from with_as_A",
"WITH with_as_A AS (SELECT * FROM entity WHERE tenant_id = 1) SELECT * FROM with_as_A");
"WITH with_as_A AS (SELECT * FROM entity WHERE entity.tenant_id = 1) SELECT * FROM with_as_A");
}
@Test
void selectIgnoreTable() {
assertSql(" SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)",
"SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id AND item.tenant_id = 1 WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)");
}
private void assertSql(String sql, String targetSql) {
assertEquals(targetSql, interceptor.parserSingle(sql, null));
}
// ========== 额外的测试 ==========
@Test
public void testSelectSingle() {
// 单表
assertSql("select * from t_user where id = ?",
"SELECT * FROM t_user WHERE id = ? AND tenant_id = 1 AND dept_id IN (10, 20)");
"SELECT * FROM t_user WHERE id = ? AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
assertSql("select * from t_user where id = ? or name = ?",
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)");
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)",
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)");
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
/* not */
assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)",
"SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)");
"SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)");
}
@Test
@ -329,16 +493,16 @@ public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest
"right join t_role e1 on e1.id = e.id " +
"WHERE e.id = ? OR e.name = ?",
"SELECT * FROM t_user e " +
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " +
"WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
// 条件 e.id = ? OR e.name = ? 带括号
assertSql("SELECT * FROM t_user e " +
"right join t_role e1 on e1.id = e.id " +
"WHERE (e.id = ? OR e.name = ?)",
"SELECT * FROM t_user e " +
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " +
"WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1");
}
@Test
@ -348,23 +512,22 @@ public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest
"inner join entity1 e1 on e1.id = e.id " +
"WHERE e.id = ? OR e.name = ?",
"SELECT * FROM t_user e " +
"INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
"INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " +
"WHERE e.id = ? OR e.name = ?");
// 条件 e.id = ? OR e.name = ? 带括号
assertSql("SELECT * FROM t_user e " +
"inner join t_role e1 on e1.id = e.id " +
"inner join entity1 e1 on e1.id = e.id " +
"WHERE (e.id = ? OR e.name = ?)",
"SELECT * FROM t_user e " +
"INNER JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
"INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " +
"WHERE (e.id = ? OR e.name = ?)");
// 垃圾 inner join todo
// assertSql("SELECT * FROM entity,entity1 " +
// "WHERE entity.id = entity1.id",
// "SELECT * FROM entity e " +
// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
// 没有 On inner join
assertSql("SELECT * FROM entity,entity1 " +
"WHERE entity.id = entity1.id",
"SELECT * FROM entity, entity1 " +
"WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1");
}
}

View File

@ -227,7 +227,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
// 调用
Expression expression = rule.getExpression(tableName, tableAlias);
// 断言
assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString());
assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString());
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
}
}

View File

@ -2,10 +2,10 @@ package cn.iocoder.yudao.framework.dict.config;
import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils;
import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@AutoConfiguration
public class YudaoDictAutoConfiguration {
@Bean

View File

@ -27,7 +27,7 @@ public class DictFrameworkUtils {
/**
* 针对 {@link #getDictDataLabel(String, String)} 的缓存
*/
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> getDictDataCache = CacheUtils.buildAsyncReloadingCache(
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
@ -41,7 +41,7 @@ public class DictFrameworkUtils {
/**
* 针对 {@link #parseDictDataValue(String, String)} 的缓存
*/
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> parseDictDataCache = CacheUtils.buildAsyncReloadingCache(
private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache(
Duration.ofMinutes(1L), // 过期时间 1 分钟
new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() {
@ -59,12 +59,12 @@ public class DictFrameworkUtils {
@SneakyThrows
public static String getDictDataLabel(String dictType, String value) {
return getDictDataCache.get(new KeyValue<>(dictType, value)).getLabel();
return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel();
}
@SneakyThrows
public static String parseDictDataValue(String dictType, String label) {
return parseDictDataCache.get(new KeyValue<>(dictType, label)).getValue();
return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue();
}
}

View File

@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.dict.config.YudaoDictAutoConfiguration

View File

@ -0,0 +1 @@
cn.iocoder.yudao.framework.dict.config.YudaoDictAutoConfiguration

View File

@ -6,10 +6,10 @@ import cn.iocoder.yudao.framework.errorcode.core.loader.ErrorCodeLoader;
import cn.iocoder.yudao.framework.errorcode.core.loader.ErrorCodeLoaderImpl;
import cn.iocoder.yudao.module.system.api.errorcode.ErrorCodeApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
@ -17,7 +17,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
*
* @author 芋道源码
*/
@Configuration
@AutoConfiguration
@ConditionalOnProperty(prefix = "yudao.error-code", value = "enable", matchIfMissing = true) // 允许使用 yudao.error-code.enable=false 禁用访问日志
@EnableConfigurationProperties(ErrorCodeProperties.class)
@EnableScheduling // 开启调度任务的功能因为 ErrorCodeRemoteLoader 通过定时刷新错误码

View File

@ -10,7 +10,7 @@ import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.Date;
import java.time.LocalDateTime;
import java.util.List;
/**
@ -41,7 +41,7 @@ public class ErrorCodeLoaderImpl implements ErrorCodeLoader {
/**
* 缓存错误码的最大更新时间用于后续的增量轮询判断是否有更新
*/
private Date maxUpdateTime;
private LocalDateTime maxUpdateTime;
@EventListener(ApplicationReadyEvent.class)
public void loadErrorCodes() {

View File

@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.errorcode.config.YudaoErrorCodeConfiguration

View File

@ -0,0 +1 @@
cn.iocoder.yudao.framework.errorcode.config.YudaoErrorCodeConfiguration

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-framework</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>IP 拓展,支持如下功能:
1. IP 功能:查询 IP 对应的城市信息
基于 https://gitee.com/lionsoul/ip2region 实现
2. 城市功能:查询城市编码对应的城市信息
基于 https://github.com/modood/Administrative-divisions-of-China 实现
</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<ip2region.version>2.6.6</ip2region.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- IP地址检索 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.framework.ip.core;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 区域节点包括国家省份城市地区等信息
*
* 数据可见 resources/area.csv 文件
*
* @author 芋道源码
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Area {
/**
* 编号 - 全球即根目录
*/
public static final Integer ID_GLOBAL = 0;
/**
* 编号 - 中国
*/
public static final Integer ID_CHINA = 1;
/**
* 编号
*/
private Integer id;
/**
* 名字
*/
private String name;
/**
* 类型
*
* 枚举 {@link AreaTypeEnum}
*/
private Integer type;
/**
* 父节点
*/
private Area parent;
/**
* 子节点
*/
private List<Area> children;
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.framework.ip.core.enums;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 区域类型枚举
*
* @author 芋道源码
*/
@AllArgsConstructor
@Getter
public enum AreaTypeEnum implements IntArrayValuable {
COUNTRY(1, "国家"),
PROVINCE(2, "省份"),
CITY(3, "城市"),
DISTRICT(4, "地区"), // 区等
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AreaTypeEnum::getType).toArray();
/**
* 类型
*/
private final Integer type;
/**
* 名字
*/
private final String name;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,119 @@
package cn.iocoder.yudao.framework.ip.core.utils;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.csv.CsvRow;
import cn.hutool.core.text.csv.CsvUtil;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 区域工具类
*
* @author 芋道源码
*/
@Slf4j
public class AreaUtils {
/**
* 初始化 SEARCHER
*/
@SuppressWarnings("InstantiationOfUtilityClass")
private final static AreaUtils INSTANCE = new AreaUtils();
/**
* Area 内存缓存提升访问速度
*/
private static Map<Integer, Area> areas;
private AreaUtils() {
long now = System.currentTimeMillis();
areas = new HashMap<>();
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0,
null, new ArrayList<>()));
// csv 中加载数据
List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows();
rows.remove(0); // 删除 header
for (CsvRow row : rows) {
// 创建 Area 对象
Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)),
null, new ArrayList<>());
// 添加到 areas
areas.put(area.getId(), area);
}
// 构建父子关系因为 Area 中没有 parentId 字段所以需要重复读取
for (CsvRow row : rows) {
Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
Area parent = areas.get(Integer.valueOf(row.get(3))); //
Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
area.setParent(parent);
parent.getChildren().add(area);
}
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
}
/**
* 获得指定编号对应的区域
*
* @param id 区域编号
* @return 区域
*/
public static Area getArea(Integer id) {
return areas.get(id);
}
/**
* 格式化区域
*
* @param id 区域编号
* @return 格式化后的区域
*/
public static String format(Integer id) {
return format(id, " ");
}
/**
* 格式化区域
*
* 例如说
* 1. id = 静安区上海 上海市 静安区
* 2. id = 上海市上海 上海市
* 3. id = 上海上海
* 4. id = 美国美国
* 当区域在中国时默认不显示中国
*
* @param id 区域编号
* @param separator 分隔符
* @return 格式化后的区域
*/
public static String format(Integer id, String separator) {
// 获得区域
Area area = areas.get(id);
if (area == null) {
return null;
}
// 格式化
StringBuilder sb = new StringBuilder();
for (int i = 0; i < AreaTypeEnum.values().length; i++) { // 避免死循环
sb.insert(0, area.getName());
// 递归父节点
area = area.getParent();
if (area == null
|| ObjectUtils.equalsAny(area.getId(), Area.ID_GLOBAL, Area.ID_CHINA)) { // 跳过父节点为中国的情况
break;
}
sb.insert(0, separator);
}
return sb.toString();
}
}

View File

@ -0,0 +1,87 @@
package cn.iocoder.yudao.framework.ip.core.utils;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.iocoder.yudao.framework.ip.core.Area;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.IOException;
/**
* IP 工具类
*
* IP 数据源来自 ip2region.xdb 精简版基于 <a href="https://gitee.com/zhijiantianya/ip2region"/> 项目
*
* @author wanglhup
*/
@Slf4j
public class IPUtils {
/**
* 初始化 SEARCHER
*/
@SuppressWarnings("InstantiationOfUtilityClass")
private final static IPUtils INSTANCE = new IPUtils();
/**
* IP 查询器启动加载到内存中
*/
private static Searcher SEARCHER;
/**
* 私有化构造
*/
private IPUtils() {
try {
long now = System.currentTimeMillis();
byte[] bytes = ResourceUtil.readBytes("ip2region.xdb");
SEARCHER = Searcher.newWithBuffer(bytes);
log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} catch (IOException e) {
log.error("启动加载 IPUtils 失败", e);
}
}
/**
* 查询 IP 对应的地区编号
*
* @param ip IP 地址格式为 127.0.0.1
* @return 地区id
*/
@SneakyThrows
public static Integer getAreaId(String ip) {
return Integer.parseInt(SEARCHER.search(ip));
}
/**
* 查询 IP 对应的地区编号
*
* @param ip IP 地址的时间戳格式参考{@link Searcher#checkIP(String)} 的返回
* @return 地区编号
*/
@SneakyThrows
public static Integer getAreaId(long ip) {
return Integer.parseInt(SEARCHER.search(ip));
}
/**
* 查询 IP 对应的地区
*
* @param ip IP 地址格式为 127.0.0.1
* @return 地区
*/
public static Area getArea(String ip) {
return AreaUtils.getArea(getAreaId(ip));
}
/**
* 查询 IP 对应的地区
*
* @param ip IP 地址的时间戳格式参考{@link Searcher#checkIP(String)} 的返回
* @return 地区
*/
public static Area getArea(long ip) {
return AreaUtils.getArea(getAreaId(ip));
}
}

View File

@ -0,0 +1,11 @@
/**
* IP 拓展支持如下功能
*
* 1. IP 功能查询 IP 对应的城市信息
* 基于 https://gitee.com/lionsoul/ip2region 实现
* 2. 城市功能查询城市编码对应的城市信息
* 基于 https://github.com/modood/Administrative-divisions-of-China 实现
*
* @author 芋道源码
*/
package cn.iocoder.yudao.framework.ip;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.framework.ip.core.utils;
import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link AreaUtils} 的单元测试
*
* @author 芋道源码
*/
public class AreaUtilsTest {
@Test
public void testGetArea() {
// 调用北京
Area area = AreaUtils.getArea(110100);
// 断言
assertEquals(area.getId(), 110100);
assertEquals(area.getName(), "北京市");
assertEquals(area.getType(), AreaTypeEnum.CITY.getType());
assertEquals(area.getParent().getId(), 110000);
assertEquals(area.getChildren().size(), 16);
}
@Test
public void testFormat() {
assertEquals(AreaUtils.format(110105), "北京 北京市 朝阳区");
assertEquals(AreaUtils.format(1), "中国");
assertEquals(AreaUtils.format(2), "蒙古");
}
}

View File

@ -0,0 +1,47 @@
package cn.iocoder.yudao.framework.ip.core.utils;
import cn.iocoder.yudao.framework.ip.core.Area;
import org.junit.jupiter.api.Test;
import org.lionsoul.ip2region.xdb.Searcher;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link IPUtils} 的单元测试
*
* @author wanglhup
*/
public class IPUtilsTest {
@Test
public void testGetAreaId_string() {
// 120.202.4.0|120.202.4.255|420600
Integer areaId = IPUtils.getAreaId("120.202.4.50");
assertEquals(420600, areaId);
}
@Test
public void testGetAreaId_long() throws Exception {
// 120.203.123.0|120.203.133.255|360900
long ip = Searcher.checkIP("120.203.123.250");
Integer areaId = IPUtils.getAreaId(ip);
assertEquals(360900, areaId);
}
@Test
public void testGetArea_string() {
// 120.202.4.0|120.202.4.255|420600
Area area = IPUtils.getArea("120.202.4.50");
assertEquals("襄阳市", area.getName());
}
@Test
public void testGetArea_long() throws Exception {
// 120.203.123.0|120.203.133.255|360900
long ip = Searcher.checkIP("120.203.123.252");
Area area = IPUtils.getArea(ip);
assertEquals("宜春市", area.getName());
}
}

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