Jelajahi Sumber

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

 Conflicts:
	yudao-module-mall/yudao-module-trade-biz/pom.xml
YunaiV 2 tahun lalu
induk
melakukan
2bd2cb56a1
100 mengubah file dengan 2824 tambahan dan 208 penghapusan
  1. 37 32
      README.md
  2. 5 3
      pom.xml
  3. 23 23
      yudao-dependencies/pom.xml
  4. 22 0
      yudao-example/pom.xml
  5. 65 0
      yudao-example/yudao-sso-demo-by-code/pom.xml
  6. 13 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java
  7. 157 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java
  8. 73 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java
  9. 28 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java
  10. 45 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java
  11. 59 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java
  12. 97 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java
  13. 35 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java
  14. 63 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java
  15. 40 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java
  16. 48 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java
  17. 37 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java
  18. 66 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java
  19. 44 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java
  20. 36 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java
  21. 103 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java
  22. 28 0
      yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java
  23. 2 0
      yudao-example/yudao-sso-demo-by-code/src/main/resources/application.yaml
  24. 61 0
      yudao-example/yudao-sso-demo-by-code/src/main/resources/static/callback.html
  25. 159 0
      yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html
  26. 65 0
      yudao-example/yudao-sso-demo-by-password/pom.xml
  27. 13 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java
  28. 127 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java
  29. 73 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java
  30. 28 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java
  31. 45 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java
  32. 59 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java
  33. 97 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java
  34. 35 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java
  35. 50 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java
  36. 40 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java
  37. 48 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java
  38. 37 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java
  39. 66 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java
  40. 44 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java
  41. 36 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java
  42. 103 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java
  43. 28 0
      yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java
  44. 2 0
      yudao-example/yudao-sso-demo-by-password/src/main/resources/application.yaml
  45. 154 0
      yudao-example/yudao-sso-demo-by-password/src/main/resources/static/index.html
  46. 74 0
      yudao-example/yudao-sso-demo-by-password/src/main/resources/static/login.html
  47. 2 2
      yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml
  48. 2 2
      yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml
  49. 4 3
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java
  50. 5 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/message/BpmMessageConvert.java
  51. 22 16
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm
  52. 8 6
      yudao-module-mall/yudao-module-coupon-biz/pom.xml
  53. 0 1
      yudao-module-mall/yudao-module-coupon-biz/src/main/java/cn/iocoder/yudao/module/coupon/service/coupon/CouponServiceImpl.java
  54. 10 4
      yudao-module-mall/yudao-module-market-biz/pom.xml
  55. 2 3
      yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/controller/app/banner/AppBannerController.java
  56. 6 5
      yudao-module-mall/yudao-module-product-biz/pom.xml
  57. 9 7
      yudao-module-mall/yudao-module-trade-biz/pom.xml
  58. 0 1
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java
  59. 3 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java
  60. 1 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java
  61. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java
  62. 0 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java
  63. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantPackageDO.java
  64. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java
  65. 7 7
      yudao-server/src/main/resources/application-local.yaml
  66. 10 10
      yudao-ui-admin-vue3/README.md
  67. 41 41
      yudao-ui-admin-vue3/package.json
  68. 2 0
      yudao-ui-admin-vue3/src/api/system/tenant/types.ts
  69. 2 1
      yudao-ui-admin-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue
  70. 2 1
      yudao-ui-admin-vue3/src/components/ContextMenu/src/ContextMenu.vue
  71. 1 1
      yudao-ui-admin-vue3/src/components/CountTo/src/CountTo.vue
  72. 1 1
      yudao-ui-admin-vue3/src/components/Crontab/src/Crontab.vue
  73. 1 0
      yudao-ui-admin-vue3/src/components/Descriptions/src/Descriptions.vue
  74. 1 1
      yudao-ui-admin-vue3/src/components/Dialog/src/Dialog.vue
  75. 1 1
      yudao-ui-admin-vue3/src/components/Editor/src/Editor.vue
  76. 1 0
      yudao-ui-admin-vue3/src/components/Error/src/Error.vue
  77. 1 0
      yudao-ui-admin-vue3/src/components/Form/index.ts
  78. 1 0
      yudao-ui-admin-vue3/src/components/Form/src/Form.vue
  79. 1 0
      yudao-ui-admin-vue3/src/components/Form/src/componentMap.ts
  80. 1 0
      yudao-ui-admin-vue3/src/components/Form/src/components/useRenderCheckbox.tsx
  81. 1 0
      yudao-ui-admin-vue3/src/components/Form/src/components/useRenderRadio.tsx
  82. 10 1
      yudao-ui-admin-vue3/src/components/Form/src/components/useRenderSelect.tsx
  83. 2 0
      yudao-ui-admin-vue3/src/components/Form/src/helper.ts
  84. 2 0
      yudao-ui-admin-vue3/src/components/Form/src/types.ts
  85. 1 1
      yudao-ui-admin-vue3/src/components/Icon/src/IconSelect.vue
  86. 3 2
      yudao-ui-admin-vue3/src/components/Infotip/src/Infotip.vue
  87. 1 1
      yudao-ui-admin-vue3/src/components/InputPassword/src/InputPassword.vue
  88. 1 1
      yudao-ui-admin-vue3/src/components/Menu/src/Menu.vue
  89. 2 1
      yudao-ui-admin-vue3/src/components/Qrcode/src/Qrcode.vue
  90. 2 1
      yudao-ui-admin-vue3/src/components/Search/src/Search.vue
  91. 1 1
      yudao-ui-admin-vue3/src/components/Setting/src/Setting.vue
  92. 1 1
      yudao-ui-admin-vue3/src/components/Setting/src/components/ColorRadioPicker.vue
  93. 26 1
      yudao-ui-admin-vue3/src/components/Setting/src/components/InterfaceDisplay.vue
  94. 2 1
      yudao-ui-admin-vue3/src/components/SizeDropdown/src/SizeDropdown.vue
  95. 36 9
      yudao-ui-admin-vue3/src/components/TabMenu/src/TabMenu.vue
  96. 4 4
      yudao-ui-admin-vue3/src/components/TabMenu/src/helper.ts
  97. 3 0
      yudao-ui-admin-vue3/src/components/Table/index.ts
  98. 1 0
      yudao-ui-admin-vue3/src/components/Table/src/Table.vue
  99. 2 0
      yudao-ui-admin-vue3/src/components/Table/src/types.ts
  100. 2 2
      yudao-ui-admin-vue3/src/components/UserInfo/src/UserInfo.vue

+ 37 - 32
README.md

@@ -23,6 +23,8 @@
 >
 > 😜 给项目点点 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
@@ -42,16 +44,19 @@
 | `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) |
 
-
 ## 🐼 内置功能
 
-分成多种内置功能:
+系统内置多种多种业务功能,可以用于快速你的业务系统:
+
+![功能分层](https://static.iocoder.cn/ruoyi-vue-pro-biz.png)
+
 * 系统功能
 * 基础设施
 * 工作流程
 * 支付系统
-* 商城系统
+* 会员中心
 * 数据报表
+* 商城系统
 
 > 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
 >
@@ -169,29 +174,29 @@ ps:核心功能已经实现,正在对接微信小程序中...
 
 ### 后端
 
-| 框架                                                                                         | 说明                   | 版本      | 学习指南                                                           |
-|---------------------------------------------------------------------------------------------|-----------------------|---------|----------------------------------------------------------------|
-| [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.2   | [文档](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)           |
-| [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)          |
-| [JUnit](https://junit.org/junit5/)                                                          | Java 单元测试框架        | 5.8.2   | -                                                              |
-| [Mockito](https://github.com/mockito/mockito)                                               | Java Mock 框架         | 4.0.0   | -                                                              |
+| 框架                                                                                         | 说明                   | 版本               | 学习指南                                                           |
+|---------------------------------------------------------------------------------------------|-----------------------|------------------|----------------------------------------------------------------|
+| [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架             | 2.6.12           | [文档](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.13-SNSAPSHOT | [文档](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.2            | [文档](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.7           | [文档](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.2            | [文档](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.9            | [文档](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.5.2.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.8.0            | -                                                              |
 
 ### [管理后台 Vue2 前端](./yudao-ui-admin)
 
@@ -204,14 +209,14 @@ ps:核心功能已经实现,正在对接微信小程序中...
 
 | 框架                                                                  | 说明              | 版本     |
 |----------------------------------------------------------------------|-----------------|--------|
-| [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/)                                    | vuex5           | 2.0.17 |
+| [Vue](https://staging-cn.vuejs.org/)                                 | Vue 框架          | 3.2.41 |
+| [Vite](https://cn.vitejs.dev//)                                      | 开发与构建工具         | 3.1.8  |
+| [Element Plus](https://element-plus.org/zh-CN/)                      | Element Plus    | 2.2.18 |
+| [TypeScript](https://www.typescriptlang.org/docs/)                   | TypeScript      | 4.8.4  |
+| [pinia](https://pinia.vuejs.org/)                                    | vuex5           | 2.0.23 |
 | [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化             | 9.2.2  |
 | [windicss](https://cn.windicss.org/)                                 | 下一代工具优先的 CSS 框架 | 3.5.6  |
-| [iconify](https://icon-sets.iconify.design/)                         | 在线图标库           | 2.2.1  |
+| [iconify](https://icon-sets.iconify.design/)                         | 在线图标库           | 3.0.0  |
 
 ### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp)
 

+ 5 - 3
pom.xml

@@ -12,7 +12,7 @@
         <module>yudao-framework</module>
         <!-- Server 主项目 -->
         <module>yudao-server</module>
-        <!--  各种 module 拓展 -->
+        <!-- 各种 module 拓展 -->
         <module>yudao-module-member</module>
         <module>yudao-module-bpm</module>
         <module>yudao-module-system</module>
@@ -20,6 +20,8 @@
         <module>yudao-module-pay</module>
         <module>yudao-module-mall</module>
         <module>yudao-module-visualization</module>
+        <!-- 示例项目 -->
+        <module>yudao-example</module>
     </modules>
 
     <name>${project.artifactId}</name>
@@ -35,8 +37,8 @@
         <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
         <maven-compiler-plugin.version>3.8.0</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>
+        <mapstruct.version>1.5.2.Final</mapstruct.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
 

+ 23 - 23
yudao-dependencies/pom.xml

@@ -16,58 +16,58 @@
     <properties>
         <revision>1.6.4-snapshot</revision>
         <!-- 统一依赖管理 -->
-        <spring.boot.version>2.6.10</spring.boot.version>
+        <spring.boot.version>2.6.12</spring.boot.version>
         <!-- Web 相关 -->
         <knife4j.version>3.0.3</knife4j.version>
-        <swagger-annotations.version>1.6.6</swagger-annotations.version>
+        <swagger-annotations.version>1.6.7</swagger-annotations.version>
         <servlet.versoin>2.5</servlet.versoin>
         <!-- DB 相关 -->
-        <druid.version>1.2.11</druid.version>
+        <druid.version>1.2.13-SNSAPSHOT</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>
+        <dynamic-datasource.version>3.5.2</dynamic-datasource.version>
+        <redisson.version>3.17.7</redisson.version>
         <!-- Config 配置中心相关 -->
-        <apollo.version>1.9.2</apollo.version>
+        <apollo.version>2.0.1</apollo.version>
         <!-- 服务保障相关 -->
-        <lock4j.version>2.2.0</lock4j.version>
+        <lock4j.version>2.2.2</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.6.9</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.9.RELEASE</podam.version>
+        <jedis-mock.version>1.0.4</jedis-mock.version>
+        <mockito-inline.version>4.8.0</mockito-inline.version>
         <!-- Bpm 工作流相关 -->
         <flowable.version>6.7.2</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.8.5</hutool.version>
+        <lombok.version>1.18.24</lombok.version>
+        <mapstruct.version>1.5.2.Final</mapstruct.version>
+        <hutool.version>5.8.8</hutool.version>
         <easyexcel.verion>3.1.1</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>
+        <fastjson.version>1.2.83</fastjson.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.0</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.5.0</tika-core.version>
         <aj-captcha.version>1.3.0</aj-captcha.version>
         <netty-all.version>4.1.82.Final</netty-all.version>
         <!-- 三方云服务相关 -->
         <minio.version>8.2.2</minio.version>
-        <aliyun-java-sdk-core.version>4.6.0</aliyun-java-sdk-core.version>
+        <aliyun-java-sdk-core.version>4.6.2</aliyun-java-sdk-core.version>
         <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
         <tencentcloud-sdk-java.version>3.1.561</tencentcloud-sdk-java.version>
         <yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
         <justauth.version>1.4.0</justauth.version>
-        <jimureport.version>1.5.2</jimureport.version>
-        <xercesImpl.version>2.12.0</xercesImpl.version>
+        <jimureport.version>1.5.3</jimureport.version>
+        <xercesImpl.version>2.12.2</xercesImpl.version>
     </properties>
 
     <dependencyManagement>

+ 22 - 0
yudao-example/pom.xml

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

+ 65 - 0
yudao-example/yudao-sso-demo-by-code/pom.xml

@@ -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.6.10</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.8</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+
+</project>

+ 13 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java

@@ -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);
+    }
+
+}

+ 157 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java

@@ -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);
+    }
+
+}

+ 73 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java

@@ -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());
+    }
+}

+ 28 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java

@@ -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;
+
+}

+ 45 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java

@@ -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;
+
+}

+ 59 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java

@@ -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;
+
+}

+ 97 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java

@@ -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;
+
+    }
+
+}

+ 35 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java

@@ -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;
+
+}

+ 63 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java

@@ -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<>();
+    }
+
+}

+ 40 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java

@@ -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);
+    }
+
+}

+ 48 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java

@@ -0,0 +1,48 @@
+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.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+import javax.annotation.Resource;
+
+@Configuration
+public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+    @Resource
+    private TokenAuthenticationFilter tokenAuthenticationFilter;
+
+    @Resource
+    private AccessDeniedHandlerImpl accessDeniedHandler;
+    @Resource
+    private AuthenticationEntryPoint authenticationEntryPoint;
+
+    @Override
+    protected void configure(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);
+    }
+
+}

+ 37 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java

@@ -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;
+
+}

+ 66 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java

@@ -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;
+        }
+    }
+
+}

+ 44 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java

@@ -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);
+    }
+
+}

+ 36 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java

@@ -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);
+    }
+
+}

+ 103 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java

@@ -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;
+    }
+
+}

+ 28 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java

@@ -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);
+    }
+
+}

+ 2 - 0
yudao-example/yudao-sso-demo-by-code/src/main/resources/application.yaml

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

+ 61 - 0
yudao-example/yudao-sso-demo-by-code/src/main/resources/static/callback.html

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

+ 159 - 0
yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html

@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="UTF-8">
+	<title>首页</title>
+	<!-- jQuery:操作 dom、发起请求等 -->
+	<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
+
+	<script type="application/javascript">
+
+    /**
+     * 跳转单点登录
+     */
+    function ssoLogin() {
+      const clientId = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId
+      const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址
+      const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token
+      window.location.href = 'http://127.0.0.1:1024/sso?client_id=' + clientId
+        + '&redirect_uri=' + redirectUri
+        + '&response_type=' + responseType;
+    }
+
+    /**
+     * 修改昵称
+     */
+    function updateNickname() {
+      const nickname = prompt("请输入新的昵称", "");
+      if (!nickname) {
+        return;
+      }
+      // 更新用户的昵称
+      const accessToken = localStorage.getItem('ACCESS-TOKEN');
+      $.ajax({
+        url: "http://127.0.0.1:18080/user/update?nickname=" + nickname,
+        method: 'PUT',
+        headers: {
+          'Authorization': 'Bearer ' + accessToken
+        },
+        success: function (result) {
+          if (result.code !== 0) {
+            alert('更新昵称失败,原因:' + result.msg)
+            return;
+          }
+          alert('更新昵称成功!');
+          $('#nicknameSpan').html(nickname);
+        }
+      });
+    }
+
+    /**
+     * 刷新令牌
+     */
+    function refreshToken() {
+      const refreshToken = localStorage.getItem('REFRESH-TOKEN');
+      if (!refreshToken) {
+        alert("获取不到刷新令牌");
+        return;
+      }
+      $.ajax({
+        url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken,
+        method: 'POST',
+        success: function (result) {
+          if (result.code !== 0) {
+            alert('刷新访问令牌失败,原因:' + result.msg)
+            return;
+          }
+          alert('更新访问令牌成功!');
+          $('#accessTokenSpan').html(result.data.access_token);
+
+          // 设置到 localStorage 中
+          localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
+          localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
+        }
+      });
+    }
+
+    /**
+     * 登出,删除访问令牌
+     */
+    function logout() {
+      const accessToken = localStorage.getItem('ACCESS-TOKEN');
+      if (!accessToken) {
+        location.reload();
+        return;
+      }
+      $.ajax({
+        url: "http://127.0.0.1:18080/auth/logout",
+        method: 'POST',
+        headers: {
+          'Authorization': 'Bearer ' + accessToken
+        },
+        success: function (result) {
+          if (result.code !== 0) {
+            alert('退出登录失败,原因:' + result.msg)
+            return;
+          }
+          alert('退出登录成功!');
+          // 删除 localStorage 中
+          localStorage.removeItem('ACCESS-TOKEN');
+          localStorage.removeItem('REFRESH-TOKEN');
+
+          location.reload();
+        }
+      });
+    }
+
+    $(function () {
+      const accessToken = localStorage.getItem('ACCESS-TOKEN');
+      // 情况一:未登录
+      if (!accessToken) {
+        $('#noLoginDiv').css("display", "block");
+        return;
+      }
+
+      // 情况二:已登录
+      $('#yesLoginDiv').css("display", "block");
+      $('#accessTokenSpan').html(accessToken);
+      // 获得登录用户的信息
+      $.ajax({
+        url: "http://127.0.0.1:18080/user/get",
+        method: 'GET',
+        headers: {
+          'Authorization': 'Bearer ' + accessToken
+        },
+        success: function (result) {
+          if (result.code !== 0) {
+            alert('获得个人信息失败,原因:' + result.msg)
+            return;
+          }
+          $('#nicknameSpan').html(result.data.nickname);
+        }
+      });
+    })
+	</script>
+</head>
+<body>
+<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
+<div id="noLoginDiv" style="display: none">
+	您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录
+</div>
+
+<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
+<div id="yesLoginDiv" style="display: none">
+	您已登录!<button onclick="logout()">退出登录</button> <br />
+	昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br />
+	访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br />
+</div>
+</body>
+<style>
+    body { /** 页面居中 */
+        border-radius: 20px;
+        height: 350px;
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%,-50%);
+    }
+</style>
+</html>

+ 65 - 0
yudao-example/yudao-sso-demo-by-password/pom.xml

@@ -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.6.10</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.8</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+
+</project>

+ 13 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java

@@ -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);
+    }
+
+}

+ 127 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java

@@ -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);
+    }
+
+}

+ 73 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java

@@ -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());
+    }
+}

+ 28 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java

@@ -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;
+
+}

+ 45 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java

@@ -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;
+
+}

+ 59 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java

@@ -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;
+
+}

+ 97 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java

@@ -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;
+
+    }
+
+}

+ 35 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java

@@ -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;
+
+}

+ 50 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java

@@ -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<>();
+    }
+
+}

+ 40 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java

@@ -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);
+    }
+
+}

+ 48 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java

@@ -0,0 +1,48 @@
+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.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+import javax.annotation.Resource;
+
+@Configuration
+public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+    @Resource
+    private TokenAuthenticationFilter tokenAuthenticationFilter;
+
+    @Resource
+    private AccessDeniedHandlerImpl accessDeniedHandler;
+    @Resource
+    private AuthenticationEntryPoint authenticationEntryPoint;
+
+    @Override
+    protected void configure(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);
+    }
+
+}

+ 37 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java

@@ -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;
+
+}

+ 66 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java

@@ -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;
+        }
+    }
+
+}

+ 44 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java

@@ -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);
+    }
+
+}

+ 36 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java

@@ -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);
+    }
+
+}

+ 103 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java

@@ -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;
+    }
+
+}

+ 28 - 0
yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java

@@ -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);
+    }
+
+}

+ 2 - 0
yudao-example/yudao-sso-demo-by-password/src/main/resources/application.yaml

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

+ 154 - 0
yudao-example/yudao-sso-demo-by-password/src/main/resources/static/index.html

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

+ 74 - 0
yudao-example/yudao-sso-demo-by-password/src/main/resources/static/login.html

@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="UTF-8">
+	<title>登录</title>
+	<!-- jQuery:操作 dom、发起请求等 -->
+	<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
+
+	<script type="application/javascript">
+
+    /**
+     * 账号密码登录
+     */
+    function login() {
+      const clientId = '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>

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml

@@ -52,7 +52,7 @@
         <dependency>
             <groupId>com.alipay.sdk</groupId>
             <artifactId>alipay-sdk-java</artifactId>
-            <version>4.31.72.ALL</version>
+            <version>4.33.44.ALL</version>
             <exclusions>
                 <exclusion>
                     <groupId>org.bouncycastle</groupId>
@@ -63,7 +63,7 @@
         <dependency>
             <groupId>com.github.binarywang</groupId>
             <artifactId>weixin-java-pay</artifactId>
-            <version>4.3.8.B</version>
+            <version>4.4.0</version>
         </dependency>
         <!-- TODO 芋艿:清理 -->
 

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml

@@ -35,12 +35,12 @@
         <dependency>
             <groupId>com.github.binarywang</groupId>
             <artifactId>wx-java-mp-spring-boot-starter</artifactId>
-            <version>4.3.8.B</version>
+            <version>4.4.0</version>
         </dependency>
         <dependency>
             <groupId>com.github.binarywang</groupId>
             <artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
-            <version>4.3.8.B</version>
+            <version>4.4.0</version>
         </dependency>
         <!-- TODO 芋艿:清理 -->
     </dependencies>

+ 4 - 3
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java

@@ -5,6 +5,8 @@ import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstant
 import com.baomidou.lock.LockFailureStrategy;
 import lombok.extern.slf4j.Slf4j;
 
+import java.lang.reflect.Method;
+
 /**
  * 自定义获取锁失败策略,抛出 {@link ServiceException} 异常
  */
@@ -12,9 +14,8 @@ import lombok.extern.slf4j.Slf4j;
 public class DefaultLockFailureStrategy implements LockFailureStrategy {
 
     @Override
-    public void onLockFailure(String key, long acquireTimeout, int acquireCount) {
-        log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取超时时长:{} ms]", Thread.currentThread().getName(), key, acquireTimeout);
+    public void onLockFailure(String key, Method method, Object[] arguments) {
+        log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments);
         throw new ServiceException(GlobalErrorCodeConstants.LOCKED);
     }
-
 }

+ 5 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/message/BpmMessageConvert.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.convert.message;
 
 import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
 import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 
 import java.util.Map;
@@ -11,6 +12,10 @@ public interface BpmMessageConvert {
 
     BpmMessageConvert INSTANCE = Mappers.getMapper(BpmMessageConvert.class);
 
+    @Mapping(target = "mobile", ignore = true)
+    @Mapping(source = "userId", target = "userId")
+    @Mapping(source = "templateCode", target = "templateCode")
+    @Mapping(source = "templateParams", target = "templateParams")
     SmsSendSingleToUserReqDTO convert(Long userId, String templateCode, Map<String, Object> templateParams);
 
 }

+ 22 - 16
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm

@@ -50,23 +50,29 @@
 
   // 提交按钮
   const submitForm = async () => {
-    actionLoading.value = true
-    // 提交请求
-    try {
-      const data = unref(formRef)?.formModel as ${simpleClassName}VO
-      if (actionType.value === 'create') {
-        await ${simpleClassName}Api.create${simpleClassName}Api(data)
-        ElMessage.success(t('common.createSuccess'))
-      } else {
-        await ${simpleClassName}Api.update${simpleClassName}Api(data)
-        ElMessage.success(t('common.updateSuccess'))
+    const elForm = unref(formRef)?.getElFormRef()
+    if (!elForm) return
+    elForm.validate(async (valid) => {
+      if (valid) {
+        actionLoading.value = true
+        // 提交请求
+        try {
+          const data = unref(formRef)?.formModel as ${simpleClassName}VO
+          if (actionType.value === 'create') {
+            await ${simpleClassName}Api.create${simpleClassName}Api(data)
+            ElMessage.success(t('common.createSuccess'))
+          } else {
+            await ${simpleClassName}Api.update${simpleClassName}Api(data)
+            ElMessage.success(t('common.updateSuccess'))
+          }
+          // 操作成功,重新加载列表
+          dialogVisible.value = false
+          await getList()
+        } finally {
+          actionLoading.value = false
+        }
       }
-      // 操作成功,重新加载列表
-      dialogVisible.value = false
-      await getList()
-    } finally {
-      actionLoading.value = false
-    }
+    })
   }
 
   // ========== 详情相关 ==========

+ 8 - 6
yudao-module-mall/yudao-module-coupon-biz/pom.xml

@@ -30,10 +30,6 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
         </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
-        </dependency>
 
         <!-- Web 相关 -->
         <dependency>
@@ -42,7 +38,7 @@
         </dependency>
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
         </dependency>
 
         <!-- DB 相关 -->
@@ -57,6 +53,12 @@
             <artifactId>yudao-spring-boot-starter-test</artifactId>
         </dependency>
 
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
+
     </dependencies>
 
-</project>
+</project>

+ 0 - 1
yudao-module-mall/yudao-module-coupon-biz/src/main/java/cn/iocoder/yudao/module/coupon/service/coupon/CouponServiceImpl.java

@@ -4,7 +4,6 @@ import cn.hutool.core.bean.BeanUtil;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.module.coupon.dal.dataobject.CouponTemplete.CouponTempleteDO;
 import cn.iocoder.yudao.module.coupon.dal.mysql.CouponTemplete.CouponTempleteMapper;
-import io.micrometer.core.instrument.Counter;
 import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import org.springframework.validation.annotation.Validated;

+ 10 - 4
yudao-module-mall/yudao-module-market-biz/pom.xml

@@ -39,16 +39,16 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
         </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
-        </dependency>
 
         <!-- Web 相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-web</artifactId>
         </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
 
         <!-- DB 相关 -->
         <dependency>
@@ -61,6 +61,12 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-test</artifactId>
         </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 2 - 3
yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/controller/app/banner/AppBannerController.java

@@ -7,13 +7,13 @@ import cn.iocoder.yudao.module.market.dal.dataobject.banner.BannerDO;
 import cn.iocoder.yudao.module.market.service.banner.BannerService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import javax.annotation.Resource;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -27,8 +27,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 @Validated
 public class AppBannerController {
 
-    // TODO @xia:使用 @Resource 哈
-    @Autowired
+    @Resource
     private BannerService bannerService;
 
     // TODO @xia:新建一个 AppBannerRespVO,只返回必要的字段。status 要过滤下。然后 sort 下结果

+ 6 - 5
yudao-module-mall/yudao-module-product-biz/pom.xml

@@ -29,10 +29,6 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
         </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
-        </dependency>
 
         <!-- Web 相关 -->
         <dependency>
@@ -41,7 +37,7 @@
         </dependency>
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
         </dependency>
 
         <!-- DB 相关 -->
@@ -56,6 +52,11 @@
             <artifactId>yudao-spring-boot-starter-test</artifactId>
         </dependency>
 
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 9 - 7
yudao-module-mall/yudao-module-trade-biz/pom.xml

@@ -47,10 +47,6 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
         </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
-        </dependency>
 
         <!-- Web 相关 -->
         <dependency>
@@ -60,13 +56,13 @@
 
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
         </dependency>
 
-        <!-- DB 相关 -->
+        <!-- TODO 芋艿:引入依赖要优化下: -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+            <artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
         </dependency>
 
         <!-- DB 相关 -->
@@ -80,6 +76,12 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-test</artifactId>
         </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 0 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java

@@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.validation.Mobile;
 import lombok.Data;
 
 import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
 import java.util.Map;
 
 /**

+ 3 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java

@@ -106,7 +106,7 @@ public class OAuth2OpenController {
                                                                      @RequestParam(value = "scope", required = false) String scope, // 密码模式
                                                                      @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式
         List<String> scopes = OAuth2Utils.buildScopes(scope);
-        // 授权类型
+        // 1.1 校验授权类型
         OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
         if (grantTypeEnum == null) {
             throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType));
@@ -115,12 +115,12 @@ public class OAuth2OpenController {
             throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式");
         }
 
-        // 校验客户端
+        // 1.2 校验客户端
         String[] clientIdAndSecret = obtainBasicAuthorization(request);
         OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
                 grantType, scopes, redirectUri);
 
-        // 根据授权模式,获取访问令牌
+        // 2. 根据授权模式,获取访问令牌
         OAuth2AccessTokenDO accessTokenDO;
         switch (grantTypeEnum) {
             case AUTHORIZATION_CODE:

+ 1 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java

@@ -26,6 +26,7 @@ public class OAuth2OpenCheckTokenRespVO {
     private Long tenantId;
 
     @ApiModelProperty(value = "客户端编号", required = true, example = "car")
+    @JsonProperty("client_id")
     private String clientId;
     @ApiModelProperty(value = "授权范围", required = true, example = "user_info")
     private List<String> scopes;

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java

@@ -17,7 +17,7 @@ public class OAuth2UserInfoRespVO {
     @ApiModelProperty(value = "用户编号", required = true, example = "1")
     private Long id;
 
-    @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
+    @ApiModelProperty(value = "用户账号", required = true, example = "芋艿")
     private String username;
 
     @ApiModelProperty(value = "用户昵称", required = true, example = "芋道")

+ 0 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java

@@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.*;
 import cn.iocoder.yudao.module.system.convert.permission.MenuConvert;
 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
 import cn.iocoder.yudao.module.system.service.permission.MenuService;
-import cn.iocoder.yudao.module.system.service.tenant.TenantService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiOperation;
@@ -29,8 +28,6 @@ public class MenuController {
 
     @Resource
     private MenuService menuService;
-    @Resource
-    private TenantService tenantService;
 
     @PostMapping("/create")
     @ApiOperation("创建菜单")

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantPackageDO.java

@@ -34,7 +34,7 @@ public class TenantPackageDO extends BaseDO {
      */
     private String name;
     /**
-     * 租户状态
+     * 租户套餐状态
      *
      * 枚举 {@link CommonStatusEnum}
      */

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java

@@ -12,7 +12,7 @@ import java.util.List;
  * 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。
  *
  * 问题:为什么自身也作为一个第三方应用,也走这套流程呢?
- * 回复:当然可以这么做,采用 Implicit 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。
+ * 回复:当然可以这么做,采用 password 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。
  *
  * @author 芋道源码
  */

+ 7 - 7
yudao-server/src/main/resources/application-local.yaml

@@ -45,33 +45,33 @@ spring:
       datasource:
         master:
           name: ruoyi-vue-pro
-          url: jdbc:mysql://139.9.196.247:3306/${spring.datasource.dynamic.datasource.master.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
+          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
           #          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
           #          url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例
           #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
           #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例
           username: root
-          password: ${RUOYI_VUE_PRO}
+          password: 123456
         #          username: sa
         #          password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
         slave: # 模拟从库,可根据自己需要修改
           name: ruoyi-vue-pro
-          url: jdbc:mysql://139.9.196.247:3306/${spring.datasource.dynamic.datasource.slave.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
+          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
           #          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
           #          url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例
           #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
           #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例
           username: root
-          password: ${RUOYI_VUE_PRO}
+          password: 123456
   #          username: sa
   #          password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
 
   # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
   redis:
-    host: 139.9.196.247 # 地址
+    host: 127.0.0.1 # 地址
     port: 6379 # 端口
     database: 0 # 数据库索引
-    password: 123456 # 密码,建议生产环境开启
+#    password: 123456 # 密码,建议生产环境开启
 
 jasypt:
   encryptor:
@@ -198,7 +198,7 @@ wx:
 # 芋道配置项,设置当前项目所有自定义的配置
 yudao:
   captcha:
-    enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试
+    enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试
   security:
     mock-enable: true
   xss:

+ 10 - 10
yudao-ui-admin-vue3/README.md

@@ -27,17 +27,17 @@
 
 | 框架 | 说明 | 版本     |
 | --- | --- |--------|
-| [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/) | JavaScript 的超集 | 4.7.4 |
-| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.17 |
-| [vueuse](https://vueuse.org//) | 常用工具集 | 9.0.2 |
-| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.0 |
-| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.3 |
+| [Vue](https://staging-cn.vuejs.org/) | vue 框架 | 3.2.41 |
+| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.1.8 |
+| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.18 |
+| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.8.4 |
+| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.23 |
+| [vueuse](https://vueuse.org//) | 常用工具集 | 9.3.1 |
+| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
+| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.5 |
 | [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
-| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.1 |
-| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.14 |
+| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.0.0 |
+| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.21 |
 
 ### 推荐 VScode 开发,插件如下
 

+ 41 - 41
yudao-ui-admin-vue3/package.json

@@ -1,6 +1,6 @@
 {
   "name": "ruoyi-vue-pro-vue3",
-  "version": "1.6.3.1611",
+  "version": "1.6.4.1811",
   "description": "基于vue3、element-plus、typesScript、vite3",
   "author": "xingyu",
   "private": false,
@@ -10,7 +10,7 @@
     "ts:check": "vue-tsc --noEmit",
     "build:pro": "vite build --mode pro",
     "build:dev": "vite build --mode dev",
-    "build:test": "pnpm run ts:check && vite build --mode test",
+    "build:test": "npm run ts:check && vite build --mode test",
     "serve:pro": "vite preview --mode pro",
     "serve:dev": "vite preview --mode dev",
     "serve:test": "vite preview --mode test",
@@ -25,84 +25,84 @@
     "p": "plop"
   },
   "dependencies": {
-    "@iconify/iconify": "^2.2.1",
-    "@vueuse/core": "^9.1.0",
-    "@wangeditor/editor": "^5.1.14",
+    "@iconify/iconify": "^3.0.0",
+    "@vueuse/core": "^9.3.1",
+    "@wangeditor/editor": "^5.1.21",
     "@wangeditor/editor-for-vue": "^5.1.10",
-    "@zxcvbn-ts/core": "^2.0.4",
+    "@zxcvbn-ts/core": "^2.0.5",
     "animate.css": "^4.1.1",
     "axios": "^0.27.2",
     "crypto-js": "^4.1.1",
-    "dayjs": "^1.11.4",
-    "echarts": "^5.3.3",
+    "dayjs": "^1.11.5",
+    "echarts": "^5.4.0",
     "echarts-wordcloud": "^2.0.0",
-    "element-plus": "2.2.12",
+    "element-plus": "2.2.18",
     "intro.js": "^6.0.0",
     "jsencrypt": "^3.2.1",
     "lodash-es": "^4.17.21",
     "mitt": "^3.0.0",
     "nprogress": "^0.2.0",
-    "pinia": "^2.0.17",
-    "pinia-plugin-persist": "^1.0.0",
+    "pinia": "^2.0.23",
     "qrcode": "^1.5.1",
     "qs": "^6.11.0",
     "url": "^0.11.0",
-    "vue": "3.2.37",
+    "vue": "3.2.41",
     "vue-cropper": "^1.0.3",
     "vue-i18n": "9.2.2",
-    "vue-router": "^4.1.3",
+    "vue-router": "^4.1.5",
     "vue-types": "^4.2.1",
     "web-storage-cache": "^1.1.1"
   },
   "devDependencies": {
-    "@commitlint/cli": "^17.0.3",
-    "@commitlint/config-conventional": "^17.0.3",
-    "@iconify/json": "^2.1.89",
-    "@intlify/vite-plugin-vue-i18n": "^6.0.0",
+    "@commitlint/cli": "^17.1.2",
+    "@commitlint/config-conventional": "^17.1.0",
+    "@iconify/json": "^2.1.122",
+    "@intlify/vite-plugin-vue-i18n": "^6.0.3",
     "@purge-icons/generated": "^0.9.0",
     "@types/intro.js": "^5.1.0",
     "@types/lodash-es": "^4.17.6",
-    "@types/node": "^18.6.5",
+    "@types/node": "^18.11.0",
     "@types/nprogress": "^0.2.0",
-    "@types/qrcode": "^1.4.2",
+    "@types/qrcode": "^1.5.0",
     "@types/qs": "^6.9.7",
-    "@typescript-eslint/eslint-plugin": "^5.33.0",
-    "@typescript-eslint/parser": "^5.33.0",
-    "@vitejs/plugin-vue": "^3.0.1",
-    "@vitejs/plugin-vue-jsx": "^2.0.0",
-    "autoprefixer": "^10.4.8",
-    "eslint": "^8.21.0",
+    "@typescript-eslint/eslint-plugin": "^5.40.0",
+    "@typescript-eslint/parser": "^5.40.0",
+    "@vitejs/plugin-vue": "^3.1.2",
+    "@vitejs/plugin-vue-jsx": "^2.0.1",
+    "autoprefixer": "^10.4.12",
+    "eslint": "^8.25.0",
     "eslint-config-prettier": "^8.5.0",
-    "eslint-define-config": "^1.6.0",
+    "eslint-define-config": "^1.7.0",
     "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-vue": "^9.3.0",
+    "eslint-plugin-vue": "^9.6.0",
     "less": "^4.1.3",
     "lint-staged": "^13.0.3",
     "plop": "^3.1.1",
-    "postcss": "^8.4.16",
+    "postcss": "^8.4.18",
     "postcss-html": "^1.5.0",
     "postcss-less": "^6.0.0",
     "prettier": "^2.7.1",
     "rimraf": "^3.0.2",
-    "rollup": "^2.77.2",
-    "stylelint": "^14.9.1",
+    "rollup": "^2.79.1",
+    "stylelint": "^14.14.0",
     "stylelint-config-html": "^1.1.0",
     "stylelint-config-prettier": "^9.0.3",
-    "stylelint-config-recommended": "^8.0.0",
-    "stylelint-config-standard": "^26.0.0",
+    "stylelint-config-recommended": "^9.0.0",
+    "stylelint-config-standard": "^29.0.0",
     "stylelint-order": "^5.0.0",
-    "typescript": "4.7.4",
-    "unplugin-vue-define-options": "^0.7.3",
-    "vite": "3.0.5",
+    "typescript": "4.8.4",
+    "unplugin-vue-macros": "^0.13.2",
+    "vite": "3.1.8",
     "vite-plugin-compression": "^0.5.1",
-    "vite-plugin-eslint": "^1.7.0",
+    "vite-plugin-eslint": "^1.8.1",
     "vite-plugin-html": "^3.2.0",
-    "vite-plugin-purge-icons": "^0.9.0",
-    "vite-plugin-style-import": "^2.0.0",
+    "vite-plugin-purge-icons": "^0.9.1",
+    "vite-plugin-style-import": "2.0.0",
     "vite-plugin-svg-icons": "^2.0.1",
-    "vite-plugin-windicss": "^1.8.7",
-    "vue-tsc": "^0.39.5",
-    "windicss": "^3.5.6"
+    "vite-plugin-windicss": "^1.8.8",
+    "vue-tsc": "^1.0.8",
+    "windicss": "^3.5.6",
+    "windicss-analysis": "^0.3.5"
   },
   "engines": {
     "node": ">= 14.18.0"

+ 2 - 0
yudao-ui-admin-vue3/src/api/system/tenant/types.ts

@@ -4,6 +4,8 @@ export type TenantVO = {
   packageId: number
   contactName: string
   contactMobile: string
+  username: string
+  password: string
   accountCount: number
   expireTime: string
   domain: string

+ 2 - 1
yudao-ui-admin-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue

@@ -7,13 +7,14 @@ import { useWindowSize } from '@vueuse/core'
 import { useAppStore } from '@/store/modules/app'
 import { setCssVar } from '@/utils'
 import { useDesign } from '@/hooks/web/useDesign'
+import { ElementPlusSize } from '@/types/elementPlus'
 
 const { variables } = useDesign()
 
 const appStore = useAppStore()
 
 const props = defineProps({
-  size: propTypes.oneOf<ElememtPlusSize[]>(['default', 'small', 'large']).def('default')
+  size: propTypes.oneOf<ElementPlusSize[]>(['default', 'small', 'large']).def('default')
 })
 
 provide('configGlobal', props)

+ 2 - 1
yudao-ui-admin-vue3/src/components/ContextMenu/src/ContextMenu.vue

@@ -1,9 +1,10 @@
 <script setup lang="ts">
 import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
-import { PropType, ref } from 'vue'
+import { PropType, ref, defineEmits } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useDesign } from '@/hooks/web/useDesign'
 import type { RouteLocationNormalizedLoaded } from 'vue-router'
+import { contextMenuSchema } from '../../../types/contextMenu'
 
 const { getPrefixCls } = useDesign()
 

+ 1 - 1
yudao-ui-admin-vue3/src/components/CountTo/src/CountTo.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { reactive, computed, watch, onMounted, unref, toRef, PropType } from 'vue'
+import { reactive, computed, watch, onMounted, unref, toRef, PropType, defineEmits } from 'vue'
 import { isNumber } from '@/utils/is'
 import { propTypes } from '@/utils/propTypes'
 import { useDesign } from '@/hooks/web/useDesign'

+ 1 - 1
yudao-ui-admin-vue3/src/components/Crontab/src/Crontab.vue

@@ -24,7 +24,7 @@ import {
   ElRadioButton,
   ElMessage
 } from 'element-plus'
-import { ref, computed, onMounted, reactive, watch, PropType } from 'vue'
+import { ref, computed, onMounted, reactive, watch, PropType, defineEmits } from 'vue'
 interface shortcutsType {
   text: string
   value: string

+ 1 - 0
yudao-ui-admin-vue3/src/components/Descriptions/src/Descriptions.vue

@@ -4,6 +4,7 @@ import { useDesign } from '@/hooks/web/useDesign'
 import { propTypes } from '@/utils/propTypes'
 import { ref, unref, PropType, computed, useAttrs } from 'vue'
 import { useAppStore } from '@/store/modules/app'
+import { DescriptionsSchema } from '@/types/descriptions'
 
 const appStore = useAppStore()
 

+ 1 - 1
yudao-ui-admin-vue3/src/components/Dialog/src/Dialog.vue

@@ -74,7 +74,7 @@ const dialogStyle = computed(() => {
         </slot>
         <Icon
           v-if="fullscreen"
-          class="mr-22px cursor-pointer is-hover mt-2px"
+          class="mr-22px cursor-pointer is-hover mt-2px z-10"
           :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
           color="var(--el-color-info)"
           @click="toggleFull"

+ 1 - 1
yudao-ui-admin-vue3/src/components/Editor/src/Editor.vue

@@ -177,7 +177,7 @@ defineExpose({
 </script>
 
 <template>
-  <div class="border-1 border-solid border-[var(--tags-view-border-color)]">
+  <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-3000">
     <!-- 工具栏 -->
     <Toolbar
       :editor="editorRef"

+ 1 - 0
yudao-ui-admin-vue3/src/components/Error/src/Error.vue

@@ -4,6 +4,7 @@ import networkError from '@/assets/svgs/500.svg'
 import noPermission from '@/assets/svgs/403.svg'
 import { propTypes } from '@/utils/propTypes'
 import { useI18n } from '@/hooks/web/useI18n'
+import { defineEmits } from 'vue'
 
 interface ErrorMap {
   url: string

+ 1 - 0
yudao-ui-admin-vue3/src/components/Form/index.ts

@@ -1,5 +1,6 @@
 import Form from './src/Form.vue'
 import { ElForm } from 'element-plus'
+import { FormSchema, FormSetPropsType } from '@/types/form'
 
 export interface FormExpose {
   setValues: (data: Recordable) => void

+ 1 - 0
yudao-ui-admin-vue3/src/components/Form/src/Form.vue

@@ -20,6 +20,7 @@ import { findIndex } from '@/utils'
 import { set } from 'lodash-es'
 import { FormProps } from './types'
 import { Icon } from '@/components/Icon'
+import { FormSchema, FormSetPropsType } from '@/types/form'
 
 const { getPrefixCls } = useDesign()
 

+ 1 - 0
yudao-ui-admin-vue3/src/components/Form/src/componentMap.ts

@@ -20,6 +20,7 @@ import {
 } from 'element-plus'
 import { InputPassword } from '@/components/InputPassword'
 import { Editor } from '@/components/Editor'
+import { ComponentName } from '@/types/components'
 
 const componentMap: Recordable<Component, ComponentName> = {
   Radio: ElRadioGroup,

+ 1 - 0
yudao-ui-admin-vue3/src/components/Form/src/components/useRenderCheckbox.tsx

@@ -1,3 +1,4 @@
+import { FormSchema } from '@/types/form'
 import { ElCheckbox, ElCheckboxButton } from 'element-plus'
 import { defineComponent } from 'vue'
 

+ 1 - 0
yudao-ui-admin-vue3/src/components/Form/src/components/useRenderRadio.tsx

@@ -1,3 +1,4 @@
+import { FormSchema } from '@/types/form'
 import { ElRadio, ElRadioButton } from 'element-plus'
 import { defineComponent } from 'vue'
 

+ 10 - 1
yudao-ui-admin-vue3/src/components/Form/src/components/useRenderSelect.tsx

@@ -1,3 +1,5 @@
+import { FormSchema } from '@/types/form'
+import { ComponentOptions } from '@/types/components'
 import { ElOption, ElOptionGroup } from 'element-plus'
 import { getSlot } from '@/utils/tsxHelper'
 import { Slots } from 'vue'
@@ -29,8 +31,15 @@ export const useRenderSelect = (slots: Slots) => {
     // 如果有别名,就取别名
     const labelAlias = item?.componentProps?.optionsAlias?.labelField
     const valueAlias = item?.componentProps?.optionsAlias?.valueField
+
+    const { label, value, ...other } = option
+
     return (
-      <ElOption label={option[labelAlias || 'label']} value={option[valueAlias || 'value']}>
+      <ElOption
+        label={labelAlias ? option[labelAlias] : label}
+        value={valueAlias ? option[valueAlias] : value}
+        {...other}
+      >
         {{
           default: () =>
             // option 插槽名规则,{field}-option

+ 2 - 0
yudao-ui-admin-vue3/src/components/Form/src/helper.ts

@@ -2,6 +2,8 @@ import { useI18n } from '@/hooks/web/useI18n'
 import type { Slots } from 'vue'
 import { getSlot } from '@/utils/tsxHelper'
 import { PlaceholderMoel } from './types'
+import { FormSchema } from '@/types/form'
+import { ColProps } from '@/types/components'
 
 /**
  *

+ 2 - 0
yudao-ui-admin-vue3/src/components/Form/src/types.ts

@@ -1,3 +1,5 @@
+import { FormSchema } from '@/types/form'
+
 export interface PlaceholderMoel {
   placeholder?: string
   startPlaceholder?: string

+ 1 - 1
yudao-ui-admin-vue3/src/components/Icon/src/IconSelect.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import { cloneDeep } from 'lodash-es'
-import { ref, computed, CSSProperties, toRef, watch } from 'vue'
+import { ref, computed, CSSProperties, toRef, watch, defineEmits } from 'vue'
 import {
   ElInput,
   ElPopover,

+ 3 - 2
yudao-ui-admin-vue3/src/components/Infotip/src/Infotip.vue

@@ -1,8 +1,9 @@
 <script setup lang="ts">
-import { PropType } from 'vue'
-import { Highlight } from '@//components/Highlight'
+import { PropType, defineEmits } from 'vue'
+import { Highlight } from '@/components/Highlight'
 import { useDesign } from '@/hooks/web/useDesign'
 import { propTypes } from '@/utils/propTypes'
+import { TipSchema } from '@/types/infoTip'
 
 const { getPrefixCls } = useDesign()
 

+ 1 - 1
yudao-ui-admin-vue3/src/components/InputPassword/src/InputPassword.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, unref, computed, watch } from 'vue'
+import { ref, unref, computed, watch, defineEmits } from 'vue'
 import { ElInput } from 'element-plus'
 import { propTypes } from '@/utils/propTypes'
 import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'

+ 1 - 1
yudao-ui-admin-vue3/src/components/Menu/src/Menu.vue

@@ -3,11 +3,11 @@ import { computed, defineComponent, unref, PropType } from 'vue'
 import { ElMenu, ElScrollbar } from 'element-plus'
 import { useAppStore } from '@/store/modules/app'
 import { usePermissionStore } from '@/store/modules/permission'
-import type { LayoutType } from '@/config/app'
 import { useRenderMenuItem } from './components/useRenderMenuItem'
 import { useRouter } from 'vue-router'
 import { isUrl } from '@/utils/is'
 import { useDesign } from '@/hooks/web/useDesign'
+import { LayoutType } from '@/types/layout'
 
 const { getPrefixCls } = useDesign()
 

+ 2 - 1
yudao-ui-admin-vue3/src/components/Qrcode/src/Qrcode.vue

@@ -1,11 +1,12 @@
 <script setup lang="ts">
-import { PropType, nextTick, ref, watch, computed, unref } from 'vue'
+import { PropType, nextTick, ref, watch, computed, unref, defineEmits } from 'vue'
 import QRCode from 'qrcode'
 import { QRCodeRenderersOptions } from 'qrcode'
 import { cloneDeep } from 'lodash-es'
 import { propTypes } from '@/utils/propTypes'
 import { useDesign } from '@/hooks/web/useDesign'
 import { isString } from '@/utils/is'
+import { QrcodeLogo } from '@/types/qrcode'
 
 const props = defineProps({
   // img 或者 canvas,img不支持logo嵌套

+ 2 - 1
yudao-ui-admin-vue3/src/components/Search/src/Search.vue

@@ -1,12 +1,13 @@
 <script setup lang="ts">
 import { Form } from '@/components/Form'
-import { PropType, computed, unref, ref } from 'vue'
+import { PropType, computed, unref, ref, defineEmits } from 'vue'
 import { propTypes } from '@/utils/propTypes'
 import { ElButton } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useForm } from '@/hooks/web/useForm'
 import { findIndex } from '@/utils'
 import { cloneDeep } from 'lodash-es'
+import { FormSchema } from '@/types/form'
 
 const { t } = useI18n()
 

+ 1 - 1
yudao-ui-admin-vue3/src/components/Setting/src/Setting.vue

@@ -208,7 +208,7 @@ const clear = () => {
     <Icon icon="ep:setting" color="#fff" />
   </div>
 
-  <ElDrawer v-model="drawer" direction="rtl" size="350px">
+  <ElDrawer v-model="drawer" direction="rtl" size="350px" :z-index="4000">
     <template #header>
       <span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
     </template>

+ 1 - 1
yudao-ui-admin-vue3/src/components/Setting/src/components/ColorRadioPicker.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { PropType, watch, unref, ref } from 'vue'
+import { PropType, watch, unref, ref, defineEmits } from 'vue'
 import { propTypes } from '@/utils/propTypes'
 import { useDesign } from '@/hooks/web/useDesign'
 

+ 26 - 1
yudao-ui-admin-vue3/src/components/Setting/src/components/InterfaceDisplay.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ElSwitch } from 'element-plus'
+import { ElSwitch, ElMessage } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useAppStore } from '@/store/modules/app'
 import { computed, ref, watch } from 'vue'
@@ -107,6 +107,21 @@ const greyModeChange = (show: boolean) => {
   appStore.setGreyMode(show)
 }
 
+// 动态路由
+const dynamicRouter = ref(appStore.getDynamicRouter)
+
+const dynamicRouterChange = (show: boolean) => {
+  ElMessage.info(t('setting.reExperienced'))
+  appStore.setDynamicRouter(show)
+}
+
+// 固定菜单
+const fixedMenu = ref(appStore.getFixedMenu)
+
+const fixedMenuChange = (show: boolean) => {
+  appStore.setFixedMenu(show)
+}
+
 const layout = computed(() => appStore.getLayout)
 
 watch(
@@ -185,5 +200,15 @@ watch(
       <span class="text-14px">{{ t('setting.greyMode') }}</span>
       <ElSwitch v-model="greyMode" @change="greyModeChange" />
     </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.dynamicRouter') }}</span>
+      <ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.fixedMenu') }}</span>
+      <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
+    </div>
   </div>
 </template>

+ 2 - 1
yudao-ui-admin-vue3/src/components/SizeDropdown/src/SizeDropdown.vue

@@ -5,6 +5,7 @@ import { useAppStore } from '@/store/modules/app'
 import { useI18n } from '@/hooks/web/useI18n'
 import { propTypes } from '@/utils/propTypes'
 import { useDesign } from '@/hooks/web/useDesign'
+import { ElementPlusSize } from '@/types/elementPlus'
 
 const { getPrefixCls } = useDesign()
 
@@ -20,7 +21,7 @@ const appStore = useAppStore()
 
 const sizeMap = computed(() => appStore.sizeMap)
 
-const setCurrentSize = (size: ElememtPlusSize) => {
+const setCurrentSize = (size: ElementPlusSize) => {
   appStore.setCurrentSize(size)
 }
 </script>

+ 36 - 9
yudao-ui-admin-vue3/src/components/TabMenu/src/TabMenu.vue

@@ -1,7 +1,7 @@
 <script lang="tsx">
 import { usePermissionStore } from '@/store/modules/permission'
 import { useAppStore } from '@/store/modules/app'
-import { computed, unref, defineComponent, watch, ref } from 'vue'
+import { computed, unref, defineComponent, watch, ref, onMounted } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { ElScrollbar } from 'element-plus'
 import { Icon } from '@/components/Icon'
@@ -28,6 +28,8 @@ export default defineComponent({
 
     const collapse = computed(() => appStore.getCollapse)
 
+    const fixedMenu = computed(() => appStore.getFixedMenu)
+
     const permissionStore = usePermissionStore()
 
     const routers = computed(() => permissionStore.getRouters)
@@ -38,6 +40,27 @@ export default defineComponent({
       appStore.setCollapse(!unref(collapse))
     }
 
+    onMounted(() => {
+      if (unref(fixedMenu)) {
+        const path = `/${unref(currentRoute).path.split('/')[1]}`
+        const children = unref(tabRouters).find(
+          (v) =>
+            (v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)) &&
+            v.path === path
+        )?.children
+
+        tabActive.value = path
+        if (children) {
+          permissionStore.setMenuTabRouters(
+            cloneDeep(children).map((v) => {
+              v.path = pathResolve(unref(tabActive), v.path)
+              return v
+            })
+          )
+        }
+      }
+    })
+
     watch(
       () => routers.value,
       (routers: AppRouteRecordRaw[]) => {
@@ -66,7 +89,7 @@ export default defineComponent({
     )
 
     // 是否显示菜单
-    const showMenu = ref(false)
+    const showMenu = ref(unref(fixedMenu) ? true : false)
 
     // tab高亮
     const tabActive = ref('')
@@ -77,9 +100,13 @@ export default defineComponent({
         window.open(item.path)
         return
       }
+      const newPath = item.children ? item.path : item.path.split('/')[0]
+      const oldPath = unref(tabActive)
       tabActive.value = item.children ? item.path : item.path.split('/')[0]
       if (item.children) {
-        showMenu.value = !unref(showMenu)
+        if (newPath === oldPath || !unref(showMenu)) {
+          showMenu.value = unref(fixedMenu) ? true : !unref(showMenu)
+        }
         if (unref(showMenu)) {
           permissionStore.setMenuTabRouters(
             cloneDeep(item.children).map((v) => {
@@ -96,7 +123,7 @@ export default defineComponent({
     }
 
     // 设置高亮
-    const isActice = (currentPath: string) => {
+    const isActive = (currentPath: string) => {
       const { path } = unref(currentRoute)
       if (tabPathMap[currentPath].includes(path)) {
         return true
@@ -105,7 +132,7 @@ export default defineComponent({
     }
 
     const mouseleave = () => {
-      if (!unref(showMenu)) return
+      if (!unref(showMenu) || unref(fixedMenu)) return
       showMenu.value = false
     }
 
@@ -114,7 +141,7 @@ export default defineComponent({
         id={`${variables.namespace}-menu`}
         class={[
           prefixCls,
-          'relative bg-[var(--left-menu-bg-color)] top-1px z-999',
+          'relative bg-[var(--left-menu-bg-color)] top-1px z-3000',
           {
             'w-[var(--tab-menu-max-width)]': !unref(collapse),
             'w-[var(--tab-menu-min-width)]': unref(collapse)
@@ -140,7 +167,7 @@ export default defineComponent({
                       `${prefixCls}__item`,
                       'text-center text-12px relative py-12px cursor-pointer',
                       {
-                        'is-active': isActice(v.path)
+                        'is-active': isActive(v.path)
                       }
                     ]}
                     onClick={() => {
@@ -174,8 +201,8 @@ export default defineComponent({
             {
               '!left-[var(--tab-menu-min-width)]': unref(collapse),
               '!left-[var(--tab-menu-max-width)]': !unref(collapse),
-              '!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu),
-              '!w-0': !unref(showMenu)
+              '!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu),
+              '!w-0': !unref(showMenu) && !unref(fixedMenu)
             }
           ]}
           style="transition: width var(--transition-time-02), left var(--transition-time-02);"

+ 4 - 4
yudao-ui-admin-vue3/src/components/TabMenu/src/helper.ts

@@ -28,9 +28,9 @@ export const filterMenusPath = (
     let data: Nullable<AppRouteRecordRaw> = null
     const meta = (v.meta ?? {}) as RouteMeta
     if (!meta.hidden || meta.canTo) {
-      const allParentPaht = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
+      const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
 
-      const fullPath = isUrl(v.path) ? v.path : allParentPaht.join('/')
+      const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/')
 
       data = cloneDeep(v)
       data.path = fullPath
@@ -42,8 +42,8 @@ export const filterMenusPath = (
         res.push(data)
       }
 
-      if (allParentPaht.length && Reflect.has(tabPathMap, allParentPaht[0])) {
-        tabPathMap[allParentPaht[0]].push(fullPath)
+      if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) {
+        tabPathMap[allParentPath[0]].push(fullPath)
       }
     }
   }

+ 3 - 0
yudao-ui-admin-vue3/src/components/Table/index.ts

@@ -1,9 +1,12 @@
 import Table from './src/Table.vue'
+import { ElTable } from 'element-plus'
+import { TableSetPropsType } from '@/types/table'
 
 export interface TableExpose {
   setProps: (props: Recordable) => void
   setColumn: (columnProps: TableSetPropsType[]) => void
   selections: Recordable[]
+  elTableRef: ComponentRef<typeof ElTable>
 }
 
 export { Table }

+ 1 - 0
yudao-ui-admin-vue3/src/components/Table/src/Table.vue

@@ -6,6 +6,7 @@ import { setIndex } from './helper'
 import { getSlot } from '@/utils/tsxHelper'
 import type { TableProps } from './types'
 import { set } from 'lodash-es'
+import { Pagination, TableColumn, TableSetPropsType, TableSlotDefault } from '@/types/table'
 
 export default defineComponent({
   name: 'Table',

+ 2 - 0
yudao-ui-admin-vue3/src/components/Table/src/types.ts

@@ -1,3 +1,5 @@
+import { Pagination, TableColumn } from '@/types/table'
+
 export type TableProps = {
   pageSize?: number
   currentPage?: number

+ 2 - 2
yudao-ui-admin-vue3/src/components/UserInfo/src/UserInfo.vue

@@ -36,8 +36,8 @@ const loginOut = () => {
   })
     .then(async () => {
       userStore.loginOut()
-      tagsViewStore.delAllViews
-      replace('/login')
+      tagsViewStore.delAllViews()
+      replace('/login?redirect=/index')
     })
     .catch(() => {})
 }

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini