commit 1b577e55fea5ceffb2bb7fe419872652178692ab Author: alex_q <914269@qq.com> Date: Fri Jun 12 01:09:25 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39b8817 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Created by .ignore support plugin (hsz.mobi) +### Example sysUserDetails template template +### Example sysUserDetails template + +# IntelliJ project files +.idea +*.iml +out +gen +target +*.log +logs +.history +lib +sql \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f8d91c4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,111 @@ +# AGENTS.md + +> 本文件用于帮助后续 OpenCode 会话快速了解本仓库,避免常见错误。 + +## 项目概况 + +- **技术栈**:Java 17 + Spring Boot 3.3.2 + Spring Security 6 + MyBatis-Plus 3.5.7 +- **构建工具**:Maven(单模块 POM) +- **应用入口**:`com.ichangzuo.iczApplication` +- **默认端口**:8989(dev/test)/ 9999(prod) +- **接口文档**:启动后访问 `http://localhost:8989/doc.html`(Knife4j) + +## 运行环境依赖 + +- **MySQL**:数据库名 `music`,开发配置在 `application-dev.yml` +- **Redis**:端口 16378,开发环境密码 `CZwy168` +- **运行时 Profile**: + - `dev`(默认):本地开发 + - `test`:测试环境(数据库 `192.168.5.2`) + - `prod`:生产环境 + +> 注意:`application.yml` 中默认激活的是 `prod`,但 Maven 的 `dev` profile 是默认激活的。 + +## 常用命令 + +```bash +# 开发模式启动(使用 dev profile) +mvn spring-boot:run -P dev + +# 打包(默认 dev) +mvn clean package + +# 测试环境打包 +mvn clean package -P test + +# 生产环境打包 +mvn clean package -P prod + +# 运行测试 +mvn test + +# 跳过测试打包 +mvn clean package -DskipTests +``` + +## 项目结构 + +``` +src/main/java/com/ichangzuo/ + iczApplication.java # 启动类(注意不是 YouLaiApplication) + common/ # 公共模块:注解、常量、枚举、异常、工具类 + config/ # 自动装配配置(CORS、Redis、Security、Swagger 等) + core/ # 核心功能:切面(日志、防重提交)、过滤器、Security + module/ # 业务模块 + auth/ # 认证模块 + codegen/ # 代码生成器 + file/ # 文件模块 + mail/ # 邮件模块 + sms/ # 短信模块 + websocket/ # WebSocket 模块 + system/ # 系统管理模块:用户、角色、菜单、部门、字典等 + controller/ + converter/ # MapStruct 转换器 + event/ # 事件处理 + handler/ + listener/ + model/ # bo, dto, entity, form, query, vo + mapper/ + service/ +``` + +## 关键配置与约定 + +### 数据库与 ORM + +- **MyBatis-Plus**: + - 逻辑删除字段:`is_deleted`(删除=1,未删除=0) + - 主键 ID 类型:`none` + - 驼峰下划线自动转换已开启 +- **数据源**:Druid 连接池 + +### 安全与认证 + +- 基于 Spring Security + JWT 的无状态认证 +- JWT 密钥配置在 `application.yml` 的 `security.jwt.key` +- 部分接口在白名单中(如 `/api/v1/auth/**`、`/client/**` 等),无需认证 + +### 文件存储 + +- 支持 `minio` 和 `aliyun` 两种类型,通过 `oss.type` 切换 +- 当前测试/生产环境使用 `minio` + +### 缓存 + +- Spring Cache + Redis,但当前 `spring.cache.enabled=false` + +### 定时任务 + +- 集成 XXL-JOB,但默认 `enabled: false` + +### 支付 + +- 集成支付宝和微信支付,配置在 `pay.ali` 和 `pay.wx` 下 + +## 开发注意事项 + +1. **启动类名称**:实际启动类是 `iczApplication`,不是 README 中提到的 `YouLaiApplication.java` +2. **MapStruct**:项目使用 MapStruct 进行实体转换,配合 Lombok 使用,需确保 `lombok-mapstruct-binding` 依赖存在 +3. **Undertow 替代 Tomcat**:`spring-boot-starter-tomcat` 被排除,使用 `spring-boot-starter-undertow` +4. **打包配置**:`spring-boot-maven-plugin` 配置了 `ZIP` layout,并排除了 Lombok +5. **Maven Profile 优先级**:`dev` profile 默认激活,但 Spring 的 `application.yml` 默认激活 `prod`。实际运行时以 Spring 的 `spring.profiles.active` 为准 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..85c30ab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +# 2.7.1 (2024/4/18) +### 🐛 fix +- 修复用户名或者密码错误时,返回的错误信息不正确问题 +### 🛠️ refactor +- JWT 解析和验证代码优化重构 +- 优化代码结构和完善注释,提高代码可读性 + +# 2.7.0 (2024/4/13) +### ✨ feat +- 集成 Mybatis-Plus generator 代码生成器 + +# 2.6.0 (2024/3/6) + +### ✨ feat +- 黑名单方式实现 JWT 主动注销过期 +### 🛠️ refactor +- 角色权限重构 + + +# 2.5.0 (2023/12/6) +### ✨ feat +- [集成 Spring Cache 和 Redis 缓存,路由缓存](https://blog.csdn.net/u013737132/article/details/134789862) +### 🛠️ refactor +- 权限判断逻辑调整,用户绑定权限调整为角色绑定权限 +### fix +- [接口无请求权限,Spring Security 自定义异常无效问题修复](https://youlai.blog.csdn.net/article/details/134718249) + + +# 2.4.1 (2023/11/7) +### ✂️ refactor +- 项目目录结构优化 +### ⬆️ chore +- 升级 SpringBoot 版本 `3.1.4` → `3.1.5` + + +# 2.2.1 (2023/5/25) + +### 🐛 fix + +- 修复多级路由的组件路径错误导致页面404问题 + +# 2.2.0 (2023/5/21) + +### ✨ feat +- 菜单、角色、字典、部门添加接口权限控制 + +### 🐛 fix + +- 用户登录权限缓存键值不一致导致获取用户数据权限错误问题修复 + +### ✂️ refactor + +- 递归获取菜单、部门属性列表代码重构优化 + +### ⬆️ chore +- 升级 SpringBoot 版本 `3.0.6` → `3.1.0` + +### 📝 docs +- SQL 脚本更新,sys_menu 新增 `tree_path` 字段 (升级需更新SQL脚本) + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..587ad95 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# 基础镜像 +FROM openjdk:17-jdk-alpine + +# 维护者信息 +MAINTAINER youlai + +# 设置国内镜像源(中国科技大学镜像源),修改容器时区(alpine镜像需安装tzdata来设置时区),安装字体库(验证码) +RUN echo -e https://mirrors.ustc.edu.cn/alpine/v3.7/main/ > /etc/apk/repositories \ + && apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone \ + && apk --no-cache add ttf-dejavu fontconfig + +# 在运行时自动挂载 /tmp 目录为匿名卷,提高可移植性 +VOLUME /tmp + +# 将构建的 Spring Boot 可执行 JAR 复制到容器中,重命名为 app.jar +ADD target/youlai-boot.jar app.jar + +# 指定容器启动时执行的命令 +CMD java \ + -Djava.security.egd=file:/dev/./urandom \ + -jar /app.jar + +# 暴露容器的端口 +EXPOSE 8989 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..090c4ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 有来开源组织 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb2896d --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ + +
+

server-java

+ Java + SpringBoot + license +
+ +基于 JDK 17、Spring Boot 3.3.2、Spring Security 6、JWT、Redis、MyBatis-Plus、Knife4j 构建的前后端分离单体业务系统。 + +## 项目概况 + +- **开发框架**:Spring Boot 3.3.2 + Spring Security 6 + MyBatis-Plus 3.5.7 +- **安全认证**:Spring Security + JWT 无状态认证,部分接口白名单免登录 +- **权限管理**:基于 RBAC 模型,细粒度控制到接口方法和按钮级别 +- **功能模块**:用户管理、角色管理、菜单管理、部门管理、字典管理、代码生成、文件管理、邮件、短信、支付(支付宝/微信)等 +- **接口文档**:Knife4j 自动生成,支持在线调试 + +## 项目目录 + +``` +src/main/java/com/ichangzuo/ + iczApplication.java # 启动类 + common/ # 公共模块:注解、常量、枚举、异常、工具类 + config/ # 自动装配配置(CORS、Redis、Security、Swagger 等) + core/ # 核心功能:切面(日志、防重提交)、过滤器、Security + module/ # 业务模块 + auth/ # 认证模块 + codegen/ # 代码生成器 + file/ # 文件模块 + mail/ # 邮件模块 + sms/ # 短信模块 + websocket/ # WebSocket 模块 + system/ # 系统管理模块:用户、角色、菜单、部门、字典等 + controller/ + converter/ # MapStruct 转换器 + event/ # 事件处理 + handler/ + listener/ + model/ # bo, dto, entity, form, query, vo + mapper/ + service/ +``` + +## 技术选型 + +| 技术 | 版本 | 说明 | +|------|------|------| +| JDK | 17 | | +| Spring Boot | 3.3.2 | 开发框架 | +| Spring Security | 6 | 安全认证 | +| MyBatis-Plus | 3.5.7 | ORM 框架 | +| Knife4j | 4.5.0 | 接口文档 | +| MapStruct | 1.5.5.Final | 对象映射 | +| Druid | 1.2.23 | 数据库连接池 | +| XXL-JOB | 2.4.2 | 定时任务 | +| Redisson | 3.30.0 | 分布式锁 | + +## 环境依赖 + +- **MySQL**:数据库名 `music`,开发配置在 `application-dev.yml` +- **Redis**:端口 16378 +- **运行时 Profile**: + - `dev`(默认):本地开发 + - `test`:测试环境 + - `prod`:生产环境 + +## 快速开始 + +### 1. 配置数据库 + +修改 `src/main/resources/application-dev.yml` 中的 MySQL、Redis 连接配置。 + +### 2. 启动项目 + +```bash +# 开发模式启动(使用 dev profile) +mvn spring-boot:run -P dev +``` + +### 3. 验证启动 + +访问接口文档地址 [http://localhost:8989/doc.html](http://localhost:8989/doc.html) 验证项目启动是否成功。 + +## 常用命令 + +```bash +# 开发模式启动 +mvn spring-boot:run -P dev + +# 打包(默认 dev) +mvn clean package + +# 测试环境打包 +mvn clean package -P test + +# 生产环境打包 +mvn clean package -P prod + +# 运行测试 +mvn test + +# 跳过测试打包 +mvn clean package -DskipTests +``` + +## 接口文档 + +- `knife4j` 接口文档:[http://localhost:8989/doc.html](http://localhost:8989/doc.html) +- `swagger` 接口文档:[http://localhost:8989/swagger-ui/index.html](http://localhost:8989/swagger-ui/index.html) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..10744c9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,323 @@ + + + 4.0.0 + + com.ichangzuo + server-java + 1.9 + 基于 Java 17 + SpringBoot 3 + Spring Security 构建的唱作网系统。 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + + + 17 + 17 + + 5.8.27 + + 8.0.28 + 1.2.23 + 3.5.7 + + 4.5.0 + + 1.5.5.Final + 0.2.0 + + 2.4.2 + + 3.2.1 + + + 8.5.10 + 4.8.1 + + 3.16.3 + + + 3.30.0 + + + 3.5.6 + 2.3 + + + 2.7.0 + + + 4.6.4 + 2.2.1 + + + + + org.projectlombok + lombok + + provided + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + provided + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.springframework.boot + spring-boot-starter-cache + + + + org.springframework.boot + spring-boot-starter-aop + + + + mysql + mysql-connector-java + ${mysql.version} + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + com.xuxueli + xxl-job-core + ${xxl-job.version} + + + + com.alibaba + easyexcel + ${easyexcel.version} + + + + org.springframework.boot + spring-boot-starter-validation + + + + + + + + + + + + + + + + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + + + + + + + + + + + + + + + + + + + + + org.lionsoul + ip2region + ${ip2region.version} + + + + org.springframework.boot + spring-boot-starter-mail + + + + com.aliyun + aliyun-java-sdk-core + ${aliyun.java.sdk.core.version} + + + + com.aliyun + aliyun-java-sdk-dysmsapi + ${aliyun.java.sdk.dysmsapi.version} + + + + com.alipay.sdk + alipay-sdk-java + 4.40.8.ALL + + + + com.github.wechatpay-apiv3 + wechatpay-java + 0.2.15 + + + + org.apache.httpcomponents.client5 + httpclient5 + 5.1 + + + + + + dev + + + dev + + + + true + + + ${project.artifactId}-${project.version}-dev + + + + prod + + prod + + + ${project.artifactId}-${project.version} + + + + test + + test + + + ${project.artifactId}-${project.version}-test + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + org.springframework.boot + spring-boot-maven-plugin + + ZIP + + + nothing + nothing + + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/common/annotation/DataPermission.java b/src/main/java/com/ichangzuo/common/annotation/DataPermission.java new file mode 100644 index 0000000..0a71af1 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/annotation/DataPermission.java @@ -0,0 +1,28 @@ +package com.ichangzuo.common.annotation; + +import java.lang.annotation.*; + +/** + * 数据权限注解 + * + * @author zc + * @since 2.0.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface DataPermission { + + /** + * 数据权限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor} + */ + String deptAlias() default ""; + + String deptIdColumnName() default "dept_id"; + + String userAlias() default ""; + + String userIdColumnName() default "create_by"; + +} + diff --git a/src/main/java/com/ichangzuo/common/annotation/Log.java b/src/main/java/com/ichangzuo/common/annotation/Log.java new file mode 100644 index 0000000..a9a9269 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/annotation/Log.java @@ -0,0 +1,23 @@ +package com.ichangzuo.common.annotation; + +import com.ichangzuo.common.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * 日志注解 + * + * @author Ray + * @since 2024/6/25 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + String value() default ""; + + LogModuleEnum module() ; + + +} \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/common/annotation/RepeatSubmit.java b/src/main/java/com/ichangzuo/common/annotation/RepeatSubmit.java new file mode 100644 index 0000000..d4f10b0 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/annotation/RepeatSubmit.java @@ -0,0 +1,28 @@ +package com.ichangzuo.common.annotation; + + +import java.lang.annotation.*; + +/** + * 防止重复提交注解 + *

+ * 该注解用于方法上,防止在指定时间内的重复提交。 + * 默认时间为5秒。 + * + * @author haoxr + * @since 2.3.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface RepeatSubmit { + + /** + * 锁过期时间(秒) + *

+ * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} diff --git a/src/main/java/com/ichangzuo/common/base/BaseAnalysisEventListener.java b/src/main/java/com/ichangzuo/common/base/BaseAnalysisEventListener.java new file mode 100644 index 0000000..95c3026 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/base/BaseAnalysisEventListener.java @@ -0,0 +1,15 @@ +package com.ichangzuo.common.base; + +import com.alibaba.excel.event.AnalysisEventListener; + +/** + * 自定义解析结果监听器 + * + * @author haoxr + * @since 2023/03/01 + */ +public abstract class BaseAnalysisEventListener extends AnalysisEventListener { + + private String msg; + public abstract String getMsg(); +} diff --git a/src/main/java/com/ichangzuo/common/base/BaseEntity.java b/src/main/java/com/ichangzuo/common/base/BaseEntity.java new file mode 100644 index 0000000..a38c0a1 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/base/BaseEntity.java @@ -0,0 +1,42 @@ +package com.ichangzuo.common.base; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 基础实体类 + * + *

实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等

+ * + * @author Ray + * @since 2024/6/23 + */ +@Data +public class BaseEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + +} diff --git a/src/main/java/com/ichangzuo/common/base/BasePageQuery.java b/src/main/java/com/ichangzuo/common/base/BasePageQuery.java new file mode 100644 index 0000000..88d8c5a --- /dev/null +++ b/src/main/java/com/ichangzuo/common/base/BasePageQuery.java @@ -0,0 +1,22 @@ +package com.ichangzuo.common.base; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 基础分页请求对象 + * + * @author haoxr + * @since 2021/2/28 + */ +@Data +@Schema +public class BasePageQuery { + + @Schema(description = "页码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private int pageNum = 1; + + @Schema(description = "每页记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private int pageSize = 10; +} diff --git a/src/main/java/com/ichangzuo/common/base/BaseVO.java b/src/main/java/com/ichangzuo/common/base/BaseVO.java new file mode 100644 index 0000000..bf462ac --- /dev/null +++ b/src/main/java/com/ichangzuo/common/base/BaseVO.java @@ -0,0 +1,21 @@ +package com.ichangzuo.common.base; + +import lombok.Data; +import lombok.ToString; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 视图对象基类 + * + * @author haoxr + * @since 2022/10/22 + */ +@Data +@ToString +public class BaseVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/com/ichangzuo/common/base/IBaseEnum.java b/src/main/java/com/ichangzuo/common/base/IBaseEnum.java new file mode 100644 index 0000000..1077694 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/base/IBaseEnum.java @@ -0,0 +1,88 @@ +package com.ichangzuo.common.base; + + +import cn.hutool.core.util.ObjectUtil; + +import java.util.EnumSet; +import java.util.Objects; + +/** + * 枚举通用接口 + * + * @author haoxr + * @since 2022/3/27 12:06 + */ +public interface IBaseEnum { + + T getValue(); + + String getLabel(); + + /** + * 根据值获取枚举 + * + * @param value + * @param clazz + * @param 枚举 + * @return + */ + static & IBaseEnum> E getEnumByValue(Object value, Class clazz) { + Objects.requireNonNull(value); + EnumSet allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举 + E matchEnum = allEnums.stream() + .filter(e -> ObjectUtil.equal(e.getValue(), value)) + .findFirst() + .orElse(null); + return matchEnum; + } + + /** + * 根据文本标签获取值 + * + * @param value + * @param clazz + * @param + * @return + */ + static & IBaseEnum> String getLabelByValue(Object value, Class clazz) { + Objects.requireNonNull(value); + EnumSet allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举 + E matchEnum = allEnums.stream() + .filter(e -> ObjectUtil.equal(e.getValue(), value)) + .findFirst() + .orElse(null); + + String label = null; + if (matchEnum != null) { + label = matchEnum.getLabel(); + } + return label; + } + + + /** + * 根据文本标签获取值 + * + * @param label + * @param clazz + * @param + * @return + */ + static & IBaseEnum> Object getValueByLabel(String label, Class clazz) { + Objects.requireNonNull(label); + EnumSet allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举 + String finalLabel = label; + E matchEnum = allEnums.stream() + .filter(e -> ObjectUtil.equal(e.getLabel(), finalLabel)) + .findFirst() + .orElse(null); + + Object value = null; + if (matchEnum != null) { + value = matchEnum.getValue(); + } + return value; + } + + +} diff --git a/src/main/java/com/ichangzuo/common/constant/JwtClaimConstants.java b/src/main/java/com/ichangzuo/common/constant/JwtClaimConstants.java new file mode 100644 index 0000000..14bb922 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/constant/JwtClaimConstants.java @@ -0,0 +1,33 @@ +package com.ichangzuo.common.constant; + +/** + * JWT Claims声明常量 + *

+ * JWT Claims 属于 Payload 的一部分,包含了一些实体(通常指的用户)的状态和额外的元数据。 + * + * @author haoxr + * @since 2023/11/24 + */ +public interface JwtClaimConstants { + + /** + * 用户ID + */ + String USER_ID = "userId"; + + /** + * 部门ID + */ + String DEPT_ID = "deptId"; + + /** + * 数据权限 + */ + String DATA_SCOPE = "dataScope"; + + /** + * 权限(角色Code)集合 + */ + String AUTHORITIES = "authorities"; + +} diff --git a/src/main/java/com/ichangzuo/common/constant/RedisConstants.java b/src/main/java/com/ichangzuo/common/constant/RedisConstants.java new file mode 100644 index 0000000..05b09b0 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/constant/RedisConstants.java @@ -0,0 +1,43 @@ +package com.ichangzuo.common.constant; + +/** + * Redis Key常量 + * + * @author Theo + * @since 2024-7-29 11:46:08 + */ +public interface RedisConstants { + + /** + * 系统配置Redis-key + */ + String SYSTEM_CONFIG_KEY = "system:config"; + + /** + * IP限流Redis-key + */ + String IP_RATE_LIMITER_KEY = "ip:rate:limiter:"; + + /** + * 防重复提交Redis-key + */ + String RESUBMIT_LOCK_PREFIX = "resubmit:lock:"; + + /** + * 单个IP请求的最大每秒查询数(QPS)阈值Key + */ + String IP_QPS_THRESHOLD_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT"; + + /** + * 手机验证码缓存前缀 + */ + + String MOBILE_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:MOBILE:"; + + + /** + * 邮箱验证码缓存前缀 + */ + String EMAIL_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:EMAIL:"; + +} diff --git a/src/main/java/com/ichangzuo/common/constant/SecurityConstants.java b/src/main/java/com/ichangzuo/common/constant/SecurityConstants.java new file mode 100644 index 0000000..38c3b62 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/constant/SecurityConstants.java @@ -0,0 +1,40 @@ +package com.ichangzuo.common.constant; + +/** + * 缓存常量 + * + * @author haoxr + * @since 2023/11/24 + */ +public interface SecurityConstants { + + /** + * 验证码缓存前缀 + */ + String CAPTCHA_CODE_PREFIX = "captcha_code:"; + + /** + * 角色和权限缓存前缀 + */ + String ROLE_PERMS_PREFIX = "role_perms:"; + + /** + * 黑名单Token缓存前缀 + */ + String BLACKLIST_TOKEN_PREFIX = "token:blacklist:"; + + + /** + * 登录路径 + */ + String LOGIN_PATH = "/api/v1/auth/login"; + /** + * JWT Token 前缀 + */ + String JWT_TOKEN_PREFIX = "Bearer "; + + String REGISTER_PATH = "/api/v1/auth/register"; + String AUTOLOGIN_PATH = "/api/v1/auth/autoLogin"; + String SEND_REG_CODE = "/api/v1/auth/sendRegCode"; + String SEND_BIND_CODE = "api/v1/users/sendBindCode"; +} diff --git a/src/main/java/com/ichangzuo/common/constant/SymbolConstant.java b/src/main/java/com/ichangzuo/common/constant/SymbolConstant.java new file mode 100644 index 0000000..fd96ed9 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/constant/SymbolConstant.java @@ -0,0 +1,120 @@ +package com.ichangzuo.common.constant; + +/** + * 符号和特殊符号常用类 + * + * @author Theo + * @since 2024-7-29 11:46:08 + */ +public interface SymbolConstant { + + /** + * 符号:点 + */ + String SPOT = "."; + + /** + * 符号:双斜杠 + */ + String DOUBLE_BACKSLASH = "\\"; + + /** + * 符号:冒号 + */ + String COLON = ":"; + + /** + * 符号:逗号 + */ + String COMMA = ","; + + /** + * 符号:左花括号 { + */ + String LEFT_CURLY_BRACKET = "{"; + + /** + * 符号:右花括号 } + */ + String RIGHT_CURLY_BRACKET = "}"; + + /** + * 符号:井号 # + */ + String WELL_NUMBER = "#"; + + /** + * 符号:单斜杠 + */ + String SINGLE_SLASH = "/"; + + /** + * 符号:双斜杠 + */ + String DOUBLE_SLASH = "//"; + + /** + * 符号:感叹号 + */ + String EXCLAMATORY_MARK = "!"; + + /** + * 符号:下划线 + */ + String UNDERLINE = "_"; + + /** + * 符号:单引号 + */ + String SINGLE_QUOTATION_MARK = "'"; + + /** + * 符号:星号 + */ + String ASTERISK = "*"; + + /** + * 符号:百分号 + */ + String PERCENT_SIGN = "%"; + + /** + * 符号:美元 $ + */ + String DOLLAR = "$"; + + /** + * 符号:和 & + */ + String AND = "&"; + + /** + * 符号:../ + */ + String SPOT_SINGLE_SLASH = "../"; + + /** + * 符号:..\\ + */ + String SPOT_DOUBLE_BACKSLASH = "..\\"; + + /** + * 系统变量前缀 #{ + */ + String SYS_VAR_PREFIX = "#{"; + + /** + * 符号 {{ + */ + String DOUBLE_LEFT_CURLY_BRACKET = "{{"; + + /** + * 符号:[ + */ + String SQUARE_BRACKETS_LEFT = "["; + + /** + * 符号:] + */ + String SQUARE_BRACKETS_RIGHT = "]"; +} diff --git a/src/main/java/com/ichangzuo/common/constant/SystemConstants.java b/src/main/java/com/ichangzuo/common/constant/SystemConstants.java new file mode 100644 index 0000000..4c535e6 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/constant/SystemConstants.java @@ -0,0 +1,29 @@ +package com.ichangzuo.common.constant; + +/** + * 系统常量 + * + * @author haoxr + * @since 1.0.0 + */ +public interface SystemConstants { + + /** + * 根节点ID + */ + Long ROOT_NODE_ID = 0L; + + /** + * 系统默认密码 + */ + String DEFAULT_PASSWORD = "123456"; + + /** + * 超级管理员角色编码 + */ + String ROOT_ROLE_CODE = "ROOT"; + + String REGREX_EMAIL = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$"; + String REGREX_PHONE = "^$|^1(3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$";// "^1[3-9]\\d{9}$"; + String TEMPLATE_MAIL_SENDCODE = "

您此次 ${sub} 的验证码是:

${code}

验证码将于此电子邮件发出 15 分钟后过期。

"; +} diff --git a/src/main/java/com/ichangzuo/common/enums/CaptchaTypeEnum.java b/src/main/java/com/ichangzuo/common/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000..ee64d4c --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package com.ichangzuo.common.enums; + +/** + * EasyCaptcha 验证码类型枚举 + * + * @author haoxr + * @since 2.5.1 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/src/main/java/com/ichangzuo/common/enums/ContactType.java b/src/main/java/com/ichangzuo/common/enums/ContactType.java new file mode 100644 index 0000000..d32e8a5 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/ContactType.java @@ -0,0 +1,19 @@ +package com.ichangzuo.common.enums; + +/** + * 联系方式类型 + * + * @author Ray + * @since 2.10.0 + */ +public enum ContactType { + /** + * 手机 + */ + MOBILE, + + /** + * 邮箱 + */ + EMAIL +} diff --git a/src/main/java/com/ichangzuo/common/enums/DataScopeEnum.java b/src/main/java/com/ichangzuo/common/enums/DataScopeEnum.java new file mode 100644 index 0000000..b852194 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/DataScopeEnum.java @@ -0,0 +1,31 @@ +package com.ichangzuo.common.enums; + +import com.ichangzuo.common.base.IBaseEnum; +import lombok.Getter; + +/** + * 数据权限枚举 + * + * @author haoxr + * @since 2.3.0 + */ +@Getter +public enum DataScopeEnum implements IBaseEnum { + + /** + * value 越小,数据权限范围越大 + */ + ALL(0, "所有数据"), + DEPT_AND_SUB(1, "部门及子部门数据"), + DEPT(2, "本部门数据"), + SELF(3, "本人数据"); + + private final Integer value; + + private final String label; + + DataScopeEnum(Integer value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/src/main/java/com/ichangzuo/common/enums/EnvEnum.java b/src/main/java/com/ichangzuo/common/enums/EnvEnum.java new file mode 100644 index 0000000..4050ede --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/EnvEnum.java @@ -0,0 +1,26 @@ +package com.ichangzuo.common.enums; + +import com.ichangzuo.common.base.IBaseEnum; +import lombok.Getter; + +/** + * 环境枚举 + * + * @author Ray + * @since 4.0.0 + */ +@Getter +public enum EnvEnum implements IBaseEnum { + + DEV("dev", "开发环境"), + PROD("prod", "生产环境"); + + private final String value; + + private final String label; + + EnvEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/src/main/java/com/ichangzuo/common/enums/FormTypeEnum.java b/src/main/java/com/ichangzuo/common/enums/FormTypeEnum.java new file mode 100644 index 0000000..31ef90c --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/FormTypeEnum.java @@ -0,0 +1,84 @@ +package com.ichangzuo.common.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.ichangzuo.common.base.IBaseEnum; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 表单类型枚举 + * + * @author Ray + * @since 2.10.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/src/main/java/com/ichangzuo/common/enums/GenderEnum.java b/src/main/java/com/ichangzuo/common/enums/GenderEnum.java new file mode 100644 index 0000000..d346cc8 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/GenderEnum.java @@ -0,0 +1,32 @@ +package com.ichangzuo.common.enums; + +import com.ichangzuo.common.base.IBaseEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +/** + * 性别枚举 + * + * @author haoxr + * @since 2022/10/14 + */ +@Getter +@Schema(enumAsRef = true) +public enum GenderEnum implements IBaseEnum { + + UNSET(0, ""), + MALE(1, "男"), + FEMALE (2, "女"), + COMB(3,"组合"), + OTHER(4,"其他"), + UNKNOWN(5,"未知"); + + private final Integer value; + + private final String label; + + GenderEnum(Integer value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/src/main/java/com/ichangzuo/common/enums/JavaTypeEnum.java b/src/main/java/com/ichangzuo/common/enums/JavaTypeEnum.java new file mode 100644 index 0000000..d4b1421 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package com.ichangzuo.common.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * 表单类型枚举 + * + * @author Ray + * @since 2.10.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "bigint"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/src/main/java/com/ichangzuo/common/enums/LogModuleEnum.java b/src/main/java/com/ichangzuo/common/enums/LogModuleEnum.java new file mode 100644 index 0000000..2f4dd4d --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/LogModuleEnum.java @@ -0,0 +1,32 @@ +package com.ichangzuo.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +/** + * 日志模块枚举 + * + * @author Ray + * @since 2.10.0 + */ +@Schema(enumAsRef = true) +@Getter +public enum LogModuleEnum { + + LOGIN("登录"), + USER("用户"), + DEPT("部门"), + ROLE("角色"), + MENU("菜单"), + DICT("字典"), + OTHER("其他") + ; + + @JsonValue + private final String moduleName; + + LogModuleEnum(String moduleName) { + this.moduleName = moduleName; + } +} \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/common/enums/MenuTypeEnum.java b/src/main/java/com/ichangzuo/common/enums/MenuTypeEnum.java new file mode 100644 index 0000000..3dd8a41 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/MenuTypeEnum.java @@ -0,0 +1,34 @@ +package com.ichangzuo.common.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.ichangzuo.common.base.IBaseEnum; +import lombok.Getter; + +/** + * 菜单类型枚举 + * + * @author haoxr + * @since 2022/4/23 9:36 + */ +@Getter +public enum MenuTypeEnum implements IBaseEnum { + + NULL(0, null), + MENU(1, "菜单"), + CATALOG(2, "目录"), + EXTLINK(3, "外链"), + BUTTON(4, "按钮"); + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + MenuTypeEnum(Integer value, String label) { + this.value = value; + this.label = label; + } + +} diff --git a/src/main/java/com/ichangzuo/common/enums/QueryTypeEnum.java b/src/main/java/com/ichangzuo/common/enums/QueryTypeEnum.java new file mode 100644 index 0000000..de313be --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package com.ichangzuo.common.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.ichangzuo.common.base.IBaseEnum; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 查询类型枚举 + * + * @author Ray + * @since 2.10.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/src/main/java/com/ichangzuo/common/enums/StatusEnum.java b/src/main/java/com/ichangzuo/common/enums/StatusEnum.java new file mode 100644 index 0000000..673e006 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/enums/StatusEnum.java @@ -0,0 +1,27 @@ +package com.ichangzuo.common.enums; + +import com.ichangzuo.common.base.IBaseEnum; +import lombok.Getter; + +/** + * 状态枚举 + * + * @author haoxr + * @since 2022/10/14 + */ +public enum StatusEnum implements IBaseEnum { + + ENABLE(1, "启用"), + DISABLE (0, "禁用"); + + @Getter + private Integer value; + + @Getter + private String label; + + StatusEnum(Integer value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/src/main/java/com/ichangzuo/common/exception/AccountDeletedException.java b/src/main/java/com/ichangzuo/common/exception/AccountDeletedException.java new file mode 100644 index 0000000..69e9fd0 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/exception/AccountDeletedException.java @@ -0,0 +1,9 @@ +package com.ichangzuo.common.exception; + +import org.springframework.security.core.AuthenticationException; + +public class AccountDeletedException extends AuthenticationException { + public AccountDeletedException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/ichangzuo/common/exception/AccountLockedException.java b/src/main/java/com/ichangzuo/common/exception/AccountLockedException.java new file mode 100644 index 0000000..fb98df0 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/exception/AccountLockedException.java @@ -0,0 +1,9 @@ +package com.ichangzuo.common.exception; + +import org.springframework.security.core.AuthenticationException; + +public class AccountLockedException extends AuthenticationException { + public AccountLockedException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/ichangzuo/common/exception/BusinessException.java b/src/main/java/com/ichangzuo/common/exception/BusinessException.java new file mode 100644 index 0000000..71f0bc4 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/exception/BusinessException.java @@ -0,0 +1,38 @@ +package com.ichangzuo.common.exception; + +import com.ichangzuo.common.result.IResultCode; +import lombok.Getter; +import org.slf4j.helpers.MessageFormatter; + +/** + * 自定义业务异常 + * + * @author Ray + * @since 2022/7/31 + */ +@Getter +public class BusinessException extends RuntimeException { + + public IResultCode resultCode; + + public BusinessException(IResultCode errorCode) { + super(errorCode.getMsg()); + this.resultCode = errorCode; + } + + public BusinessException(String message, Throwable cause) { + super(message, cause); + } + + public BusinessException(Throwable cause) { + super(cause); + } + + public BusinessException(String message, Object... args) { + super(formatMessage(message, args)); + } + + private static String formatMessage(String message, Object... args) { + return MessageFormatter.arrayFormat(message, args).getMessage(); + } +} diff --git a/src/main/java/com/ichangzuo/common/exception/GlobalExceptionHandler.java b/src/main/java/com/ichangzuo/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..46f59f8 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,219 @@ +package com.ichangzuo.common.exception; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.common.result.ResultCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import jakarta.servlet.ServletException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; + +import java.sql.SQLSyntaxErrorException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * 全局系统异常处理器 + *

+ * 调整异常处理的HTTP状态码,丰富异常处理类型 + * + * @author Gadfly + * @since 2020-02-25 13:54 + **/ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(BindException e) { + log.error("BindException:{}", e.getMessage()); + String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.PARAM_ERROR, msg); + } + + /** + * RequestParam参数的校验 + * + * @param e + * @param + * @return + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ConstraintViolationException e) { + log.error("ConstraintViolationException:{}", e.getMessage()); + String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.PARAM_ERROR, msg); + } + + /** + * RequestBody参数的校验 + * + * @param e + * @param + * @return + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException:{}", e.getMessage()); + String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.PARAM_ERROR, msg); + } + + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Result processException(NoHandlerFoundException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.RESOURCE_NOT_FOUND); + } + + /** + * MissingServletRequestParameterException + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MissingServletRequestParameterException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAM_IS_NULL); + } + + /** + * MethodArgumentTypeMismatchException + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentTypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAM_ERROR, "类型错误"); + } + + /** + * ServletException + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * HttpMessageNotReadableException + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.FORBIDDEN_OPERATION); + } else { + return Result.failed(e.getMessage()); + } + } + + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception: {}", e.getMessage()); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode()); + } + return Result.failed(e.getMessage()); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception{ + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception: {}", e.getMessage()); + e.printStackTrace(); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/src/main/java/com/ichangzuo/common/model/KeyValue.java b/src/main/java/com/ichangzuo/common/model/KeyValue.java new file mode 100644 index 0000000..f1f7087 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/model/KeyValue.java @@ -0,0 +1,32 @@ +package com.ichangzuo.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 下拉选项对象 + * + * @author haoxr + * @since 2024/5/25 + */ +@Schema(description ="键值对") +@Data +@NoArgsConstructor +public class KeyValue{ + + public KeyValue(String key, String value) { + this.key = key; + this.value = value; + } + + @Schema(description="选项的值") + private String key; + + @Schema(description="选项的标签") + private String value; + +} \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/common/model/Option.java b/src/main/java/com/ichangzuo/common/model/Option.java new file mode 100644 index 0000000..be69699 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/model/Option.java @@ -0,0 +1,42 @@ +package com.ichangzuo.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 下拉选项对象 + * + * @author haoxr + * @since 2022/1/22 + */ +@Schema(description ="下拉选项对象") +@Data +@NoArgsConstructor +public class Option { + + public Option(T value, String label) { + this.value = value; + this.label = label; + } + + public Option(T value, String label, List> children) { + this.value = value; + this.label = label; + this.children= children; + } + + @Schema(description="选项的值") + private T value; + + @Schema(description="选项的标签") + private String label; + + @Schema(description="子选项列表") + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private List> children; + +} \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/common/result/AjaxResult.java b/src/main/java/com/ichangzuo/common/result/AjaxResult.java new file mode 100644 index 0000000..2bed14d --- /dev/null +++ b/src/main/java/com/ichangzuo/common/result/AjaxResult.java @@ -0,0 +1,162 @@ +package com.ichangzuo.common.result; + +import org.springframework.http.HttpStatus; + +import java.util.HashMap; + +/** + * 操作消息提醒 + * + * @author ruoyi + */ +public class AjaxResult extends HashMap +{ + private static final long serialVersionUID = 1L; + + /** 状态码 */ + public static final String CODE_TAG = "code"; + + /** 返回内容 */ + public static final String MSG_TAG = "msg"; + + /** 数据对象 */ + public static final String DATA_TAG = "data"; + + /** + * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 + */ + public AjaxResult() + { + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (data!=null) + { + super.put(DATA_TAG, data); + } + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() + { + return AjaxResult.success("操作成功"); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data) + { + return AjaxResult.success("操作成功", data); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @return 成功消息 + */ + public static AjaxResult success(String msg) + { + return AjaxResult.success(msg, null); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, Object data) + { + return new AjaxResult(HttpStatus.OK.value(), msg, data); + } + + /** + * 返回错误消息 + * + * @return + */ + public static AjaxResult error() + { + return AjaxResult.error("操作失败"); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult error(String msg) + { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 警告消息 + */ + public static AjaxResult error(String msg, Object data) + { + return new AjaxResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult error(int code, String msg) + { + return new AjaxResult(code, msg, null); + } + + /** + * 方便链式调用 + * + * @param key 键 + * @param value 值 + * @return 数据对象 + */ + @Override + public AjaxResult put(String key, Object value) + { + super.put(key, value); + return this; + } +} diff --git a/src/main/java/com/ichangzuo/common/result/IResultCode.java b/src/main/java/com/ichangzuo/common/result/IResultCode.java new file mode 100644 index 0000000..931b02c --- /dev/null +++ b/src/main/java/com/ichangzuo/common/result/IResultCode.java @@ -0,0 +1,15 @@ +package com.ichangzuo.common.result; + +/** + * 响应码接口 + * + * @author Ray + * @since 2022/2/18 + **/ +public interface IResultCode { + + String getCode(); + + String getMsg(); + +} diff --git a/src/main/java/com/ichangzuo/common/result/PageResult.java b/src/main/java/com/ichangzuo/common/result/PageResult.java new file mode 100644 index 0000000..795013e --- /dev/null +++ b/src/main/java/com/ichangzuo/common/result/PageResult.java @@ -0,0 +1,46 @@ +package com.ichangzuo.common.result; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 分页响应结构体 + * + * @author Ray + * @since 2022/2/18 + */ +@Data +public class PageResult implements Serializable { + + private String code; + + private Data data; + + private String msg; + + public static PageResult success(IPage page) { + PageResult result = new PageResult<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + + Data data = new Data(); + data.setList(page.getRecords()); + data.setTotal(page.getTotal()); + + result.setData(data); + result.setMsg(ResultCode.SUCCESS.getMsg()); + return result; + } + + @lombok.Data + public static class Data { + + private List list; + + private long total; + + } + +} diff --git a/src/main/java/com/ichangzuo/common/result/Result.java b/src/main/java/com/ichangzuo/common/result/Result.java new file mode 100644 index 0000000..9b438e4 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/result/Result.java @@ -0,0 +1,73 @@ +package com.ichangzuo.common.result; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 统一响应结构体 + * + * @author Ray + * @since 2022/1/30 + **/ +@Data +public class Result implements Serializable { + + private String code; + + private T data; + + private String msg; + + public static Result success() { + return success(null); + } + + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMsg(ResultCode.SUCCESS.getMsg()); + result.setData(data); + return result; + } + + public static Result failed() { + return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), ResultCode.SYSTEM_EXECUTION_ERROR.getMsg(), null); + } + + public static Result failed(String msg) { + return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), msg, null); + } + + public static Result judge(boolean status) { + if (status) { + return success(); + } else { + return failed(); + } + } + + public static Result failed(IResultCode resultCode) { + return result(resultCode.getCode(), resultCode.getMsg(), null); + } + + public static Result failed(IResultCode resultCode, String msg) { + return result(resultCode.getCode(), msg, null); + } + + private static Result result(IResultCode resultCode, T data) { + return result(resultCode.getCode(), resultCode.getMsg(), data); + } + + private static Result result(String code, String msg, T data) { + Result result = new Result<>(); + result.setCode(code); + result.setData(data); + result.setMsg(msg); + return result; + } + + public static boolean isSuccess(Result result) { + return result != null && ResultCode.SUCCESS.getCode().equals(result.getCode()); + } +} diff --git a/src/main/java/com/ichangzuo/common/result/ResultCode.java b/src/main/java/com/ichangzuo/common/result/ResultCode.java new file mode 100644 index 0000000..9a2e3ca --- /dev/null +++ b/src/main/java/com/ichangzuo/common/result/ResultCode.java @@ -0,0 +1,125 @@ +package com.ichangzuo.common.result; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 响应码枚举 + *

+ * 参考阿里巴巴开发手册响应码规范 + * + * @author Ray + * @since 2020/6/23 + **/ +@AllArgsConstructor +@NoArgsConstructor +public enum ResultCode implements IResultCode, Serializable { + + SUCCESS("00000", "一切ok"), + + USER_ERROR("A0001", "用户端错误"), + REPEAT_SUBMIT_ERROR("A0002", "您的请求已提交,请不要重复提交或等待片刻再尝试。"), + + USER_LOGIN_ERROR("A0200", "用户登录异常"), + + USER_NOT_EXIST("A0201", "用户不存在"), + USER_ACCOUNT_LOCKED("A0202", "该账户已被冻结"), + USER_ACCOUNT_INVALID("A0203", "账户无效"), + USER_ACCOUNT_SHORT("A0204", "用户音符余额不足"), + USER_ACCOUNT_NOREALNAME("A0205", "用户未通过实名认证"), + USER_NAME_EXISTED("A0206", "该账号已被注册"), + USER_ACCOUNT_DELETED("A0207", "该账户已被注销"), + + USERNAME_OR_PASSWORD_ERROR("A0210", "用户名或密码错误"), + PASSWORD_ENTER_EXCEED_LIMIT("A0211", "用户输入密码次数超限"), + CLIENT_AUTHENTICATION_FAILED("A0212", "客户端认证失败"), + VERIFY_CODE_TIMEOUT("A0213", "验证码已过期"), + VERIFY_CODE_ERROR("A0214", "验证码错误"), + + USER_EMAIL_UNBIND("A0220", "用户邮箱未绑定"), + USER_EMAIL_INVALID("A0221", "用户邮箱不正确"), + USER_PHONE_UNBIND("A0222", "用户手机未绑定"), + USER_PHONE_INVALID("A0223", "用户手机不正确"), + + TOKEN_INVALID("A0230", "token无效或已过期"), + TOKEN_ACCESS_FORBIDDEN("A0231", "token已被禁止访问"), + + AUTHORIZED_ERROR("A0300", "访问权限异常"), + ACCESS_UNAUTHORIZED("A0301", "访问未授权"), + FORBIDDEN_OPERATION("A0302", "演示环境禁止新增、修改和删除数据,请本地部署后测试"), + + PARAM_ERROR("A0400", "用户请求参数错误"), + RESOURCE_NOT_FOUND("A0401", "请求资源不存在"), + PARAM_IS_NULL("A0410", "请求必填参数为空"), + + PARAM_VERSION_ERROR("A0420", "版本号错误"), + + USER_UPLOAD_FILE_ERROR("A0700", "用户上传文件异常"), + USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701", "用户上传文件类型不匹配"), + USER_UPLOAD_FILE_SIZE_EXCEEDS("A0702", "用户上传文件太大"), + USER_UPLOAD_IMAGE_SIZE_EXCEEDS("A0703", "用户上传图片太大"), + + SYSTEM_EXECUTION_ERROR("B0001", "系统执行出错"), + SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"), + SYSTEM_ORDER_PROCESSING_TIMEOUT("B0100", "系统订单处理超时"), + + SYSTEM_DISASTER_RECOVERY_TRIGGER("B0200", "系统容灾功能被触发"), + FLOW_LIMITING("B0210", "系统限流,请稍后再试"), + DEGRADATION("B0220", "系统功能降级"), + + SYSTEM_RESOURCE_ERROR("B0300", "系统资源异常"), + SYSTEM_RESOURCE_EXHAUSTION("B0310", "系统资源耗尽"), + SYSTEM_RESOURCE_ACCESS_ERROR("B0320", "系统资源访问异常"), + SYSTEM_READ_DISK_FILE_ERROR("B0321", "系统读取磁盘文件失败"), + + CALL_THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"), + MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"), + INTERFACE_NOT_EXIST("C0113", "接口不存在"), + + MESSAGE_SERVICE_ERROR("C0120", "消息服务出错"), + MESSAGE_DELIVERY_ERROR("C0121", "消息投递出错"), + MESSAGE_CONSUMPTION_ERROR("C0122", "消息消费出错"), + MESSAGE_SUBSCRIPTION_ERROR("C0123", "消息订阅出错"), + MESSAGE_GROUP_NOT_FOUND("C0124", "消息分组未查到"), + + DATABASE_ERROR("C0300", "数据库服务出错"), + DATABASE_TABLE_NOT_EXIST("C0311", "表不存在"), + DATABASE_COLUMN_NOT_EXIST("C0312", "列不存在"), + DATABASE_DUPLICATE_COLUMN_NAME("C0321", "多表关联中存在多个相同名称的列"), + DATABASE_DEADLOCK("C0331", "数据库死锁"), + DATABASE_PRIMARY_KEY_CONFLICT("C0341", "主键冲突"); + + @Override + public String getCode() { + return code; + } + + @Override + public String getMsg() { + return msg; + } + + private String code; + + private String msg; + + @Override + public String toString() { + return "{" + + "\"code\":\"" + code + '\"' + + ", \"msg\":\"" + msg + '\"' + + '}'; + } + + + public static ResultCode getValue(String code) { + for (ResultCode value : values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return SYSTEM_EXECUTION_ERROR; // 默认系统执行错误 + } +} diff --git a/src/main/java/com/ichangzuo/common/util/DateUtils.java b/src/main/java/com/ichangzuo/common/util/DateUtils.java new file mode 100644 index 0000000..e868174 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/util/DateUtils.java @@ -0,0 +1,61 @@ + +package com.ichangzuo.common.util; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * 日期工具类 + * + * @author haoxr + * @since 2.4.2 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + *

+ * eg:2021-01-01 → 2021-01-01 00:00:00 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} diff --git a/src/main/java/com/ichangzuo/common/util/ExcelUtils.java b/src/main/java/com/ichangzuo/common/util/ExcelUtils.java new file mode 100644 index 0000000..906561a --- /dev/null +++ b/src/main/java/com/ichangzuo/common/util/ExcelUtils.java @@ -0,0 +1,20 @@ +package com.ichangzuo.common.util; + +import com.alibaba.excel.EasyExcel; +import com.ichangzuo.common.base.BaseAnalysisEventListener; + +import java.io.InputStream; + +/** + * Excel 工具类 + * + * @author haoxr + * @since 2023/03/01 + */ +public class ExcelUtils { + + public static String importExcel(InputStream is, Class clazz, BaseAnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + return listener.getMsg(); + } +} diff --git a/src/main/java/com/ichangzuo/common/util/IPUtils.java b/src/main/java/com/ichangzuo/common/util/IPUtils.java new file mode 100644 index 0000000..7d7bea2 --- /dev/null +++ b/src/main/java/com/ichangzuo/common/util/IPUtils.java @@ -0,0 +1,139 @@ +package com.ichangzuo.common.util; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * IP工具类 + *

+ * 获取客户端IP地址和IP地址对应的地理位置信息 + *

+ * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + *

+ * + * @author Ray + * @since 2.10.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/data/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/com/ichangzuo/common/util/ResponseUtils.java b/src/main/java/com/ichangzuo/common/util/ResponseUtils.java new file mode 100644 index 0000000..beabd0e --- /dev/null +++ b/src/main/java/com/ichangzuo/common/util/ResponseUtils.java @@ -0,0 +1,52 @@ +package com.ichangzuo.common.util; + +import cn.hutool.json.JSONUtil; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.common.result.ResultCode; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * 响应工具类 + * + * @author Ray Hao + * @since 2.0.0 + */ +@Slf4j +public class ResponseUtils { + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + // 根据不同的结果码设置HTTP状态 + int status = switch (resultCode) { + case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value(); + case VERIFY_CODE_TIMEOUT, VERIFY_CODE_ERROR -> HttpStatus.OK.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + +} diff --git a/src/main/java/com/ichangzuo/config/AlipayConfig.java b/src/main/java/com/ichangzuo/config/AlipayConfig.java new file mode 100644 index 0000000..f727e6d --- /dev/null +++ b/src/main/java/com/ichangzuo/config/AlipayConfig.java @@ -0,0 +1,161 @@ +package com.ichangzuo.config; + +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayClient; +import com.alipay.api.AlipayConstants; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.internal.util.AlipaySignature; +import com.ichangzuo.config.property.AliPayProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +@Configuration +@EnableConfigurationProperties(AliPayProperties.class) +public class AlipayConfig { + private static AliPayProperties aliPayProperties; + + public AlipayConfig(AliPayProperties properties) { + aliPayProperties = properties; + } + + public static final String APP_ID_APP = "2016041901311447"; + public static final String APP_ID_WEB = "2021005104641778"; + public static final String APP_ID_SANDBOX = "9021000138603610"; + + public static final int WEB = 0; + public static final int APP = 1; +// public static final int WAP = 2; + //支付宝私钥 + private static final String APP_PRIVATE_KEY = """ + MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDKIIMWunigVf65\ + 5us9PWlMKebBlDwU8mkJHLWqSj35HWugWEq4lQkkCQOegUIHCvVGaQIyha9WpWdi\ + wxGQZTfry+VxGtA/TiFKD1K5hGQw7Wnf5yMqM/iFZGyORdWdtE1ajESrxHYqp05h\ + qqfk922qRgKtlFdHxU/52MCXhauhmvARrHHrFpe3QIXpFh5U05WyWqs2VhTGNihM\ + 83sOvu6D8uEru412cMnbe0iu5Rc+nc0Uzy+YaJEVKtDBueu8IMZmTsJenK8eizrU\ + 6gRZrgAB+bCJc4l1ZtA00/ygI6IawJf3S67fwb4+hL4fDyuhyCWU4lRGW8+mM8DP\ + tFaI9ciPAgMBAAECggEBAJ134MinM2yuMknARgoqusr0ZervwjMLe5r2u+zT9B4M\ + tplvz56ntTxWrXQh/T+hYN6e1nBnx+b362h80kUtJfjUm4aXPh/jjXm2IFuZcbjQ\ + bUhbOQEbBgVd7FiWvUokepMNbj5nSywFtcHPAwgVX2FlM5bpV2n5pUnffINPRxwZ\ + RverEmpcqZf1L5yPG8+axAh3RJdsM5PYp4D8erkD9bp0vU1Tq7hQ+MU9HHM/i9D8\ + L3kGSz/xah3l68QvI91MiyF3Qg1EMHyz4tqxq0Db63Err6s8v0Y0n8F9SCrzP2d5\ + d3Xh8MMuVNPtQnIEAG084IpK1HLy2pL9CDb47cRsyWkCgYEA7/E0ga9CFzMt6kAc\ + 2qolXbX1jopwjVFpmfdWjyrrrDgke+eC19bFP/BR7MpLEKanTY8azKKU3VInIHFy\ + uddp/ZJqrMNHV0BtaiRWqJMw5VnuQTgTo1BNJ128bfLDep6F4I103csQncu9I6/S\ + Am2GSKPRvKu1CrzOJpige0jQx9UCgYEA16dwtnrw4vKeFvsTC2LOkiQ9YC/EFKkP\ + EKPhtHuTXeaxBQ1scZgBVTr65pfz3cl3VKbIfxFM5TflSl5K72KtxGPn/2wUh0fV\ + ud+E5LM76mGSe5pBjoPyFY2i/Qyic+AA8L8vfHQwKqFo1OqfDBEJkmrsXgjQBtyX\ + 1Oi3zj9axNMCgYEA4J7FsMII9P8MdMcgO/QcluXIw3AGfcVBPsm1VsGvbsIAJZ5N\ + dxGwBnNLvoiCTUw2Qv088WUiRy6pQk3yQNfQeXmgM6t8FcpSo5LxLU7d71eJG7UL\ + bU+3aqrtw2AIb7oHSngid5+qJo6cudPWnj85/radmiqEiVDHDIrFcaRxDyECgYBW\ + TUDTFiIegH95rOKzNMh8PZp+Sr9KkVlhDGR/6NBRzMdcwUF7uBwYcrED5R2HzV8+\ + 9jvYdiDyvkq5V0DfyfrGVED8u9D/TmUerG+vYncA1ilb46CGmxEfRP5MDGlau/NE\ + ZQ5o3MqF1PBx/K7Hkm3lNXsAKsCtbkwovTUJidsWVwKBgQC9BED3+SJIKmGnP107\ + piz0hCvT/5HkfOXYgP8K0IAATW26h81k5QFEGfWHGSKqam9oB+Jz2YToh7gwuS6E\ + Zo6zc0xzYPR5QJvbZC/YYOMmfjz0F9Z7uFC2Tq+143bCkg9XlDwY8KKTxAfb80t7\ + m9zGkwDF59l1GEV9QNmh1q7jKA=="""; + //支付宝公钥 + public static final String ALIPAY_PUBLIC_KEY = """ + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi2w/yQ9QCkAeKD1mQDkN\ + tZ5GY83FQ2x+mEVmVMF1c2a4eb8btYd62WOzaCFb0Is8BP2up1B8GElTmoDEO44V\ + tuC+2elHYezSRXhiBcrnnRjZebFh+k5SG4pgIBXVsSR/2fL94p8reYadTmV18wLd\ + W7Mqi54S16WNlw+PXkg8o4bnYxvJj6K8/lhgZg5As5cfsWK4WMQ1osnlPkSUb4uj\ + ye4KB2RWkVla+cZmswu8gWA1M9ygtcjCpvTyT2oVIoFQkWIU3IO9CxVPWhF7l8nP\ + OJW2fj9qIWWISPNA4PJCeTvGwN7lwE4oASGnag0CSJxpgz8SmKKAFtOm4rB08aCG\ + 4QIDAQAB"""; + + public static final String ALIPAY_PUBLIC_KEY_SANDBOX = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvMfJ4G5X9JtJ5JwIiYpZ8WHKnbBaAHNjdS/Pvy7O51KBuBC/3fiQRKMy8P9HhfikAuoDrAJo3LKc3vQ2oSLcg5P+6/phL/y1fLudu3rX45hfSuB2pm+eImgViylK2oXZxMu4KJCuK+iwt0NDABz5tGKc1veuoLJQKOLPgV0NXOP/QMKa598T98NqlM5qGBvZEykKI6f6E8GOytzY+deTtUPhoNAVmTWz4KgLWN1f9pGRT9qDKPai+TmVgqXPwpsBYeXoC9usCxpc20x+HF+CmL0nG1p7Wb43LDL17pbnL/ZFgfWWoa/mvbLOFP1zmeMsYNU3Rs5V8YjgWf0u5y+rhQIDAQAB"; + private static final String APP_PRIVATE_KEY_SANDBOX = + "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDhVzjbtde5cTroYrZ2aqVZ85Lwx6M7iVehoOHgW6Hq3mYspA7m4Axs/uCjaeORNsMoG6P0t5XDBkDzQ9XZyzUGdcsXKviIKPMwdzqA3lOGpItRipo4aOqrD0xnAhYknWa2UQSfm4ch0SOEofpD8rO3N1E0wj3RJd7Zlb4+juUfOf8wJ7Dp/QJiEo+LHH3O12to4/YBrFNY8OcJ1uS4Y4u8aAKIkzP9AWH45cKlOY/sjU4cMW3DJ4xAhh6LFIEqo7SkQ7jz6cfvOg8+IrPvcwOz4FiZyjM+qEqF4Pt+fa5iPxInQBkQk2+RhCh5sKVPGSFKFPA4h1OPG3lsqvHCvuU5AgMBAAECggEBANpmDOd1ANXslmlwcuOmTQg3eK0A8IXdgR9XrFQku3PFhUWy4/aEI8bn6JS5JiQx7UfMMkYWQII6Z2zezD9AIe5W8DVOIn7lIX5RsFQCJvYSOC0ZposRtI+CTkxvy0JFg60kLNT8iiYOatw7mTGN4vyqEnyN3hf9GlXMOgyrtglh/MM0xJZF0CEWa1ipA1Modzk349gcUEQTL9Mw6vm2AYhL/tlJTKmrzz7hyjDMeUgeQ7xiHUr6aGj54g9I9RdpV+Uoo+ts3YR9H0oUkz2rjcm/wwPYdF2i1E2yCG5d6Rug7YW/MXQwYSQTGphf3Vibe1xz6/yU3snbvT7FgDp/hJkCgYEA+Npru6u+g61FEtac8gMdcKxr0tKf43oifbp2MaLWWgHNtLhKeZg4lcATvYyHZBhZ9rLUmslg5luufCa/XbwXFdGmS4UdwKIm886uAGE0CKPNLyy7Szc0tHk5FbXk+bOv6aalM49lzINyun40Q1lD6ePrk3Gi5PXGSwnWhpLCPfcCgYEA58/vuYRlFHZnDMcd9IUTQh+Dx+eCWPXSYr0zrgkgQ9DgWt3l5V6VcoUFqykpTNGpy0w5RxgFXN/cEEVRpYe4nEgU287L/hL8kgDnYDDZ+WOT2RIF0jNJ8+wdCQgMjF9EEW4N80AnOSrsp2rBB101RqZy15BLLzTk6k+y3bKm6k8CgYEAsSEHPfXpDWDvoZEQ9VIySljwBofVNt1gX42xQ3Ncj4RpHxFuMU6gODcX1fuJAz4yCt8PZX2hc1YexE/wNpAC/ozTiT2fB9ZjU3bxc2O83cl56vIz/j21TiBa+ZEXAoVb2Tu8qw6OrxuBNi1OlgGbzYdlzvU7tS0bj53ZDMC5cdECgYBlo59piSpEqZGPYbK5pquF/4lpWhGl7cqcLnb9ZNT3xxrH3KlTQ4BlYPvWS0rnerpm//nROTAIw8Kag7pDyNlh9Jzor6hzs2F4ptrMKz83gLivoZ5ZxtEzGSC1+AiAd7jBp66ILGXGRBLVaRJPp0eXvZ129LZycU+5iM5VNGLJMwKBgQCZig65wd33dZdtt/oYdfZqFOZHdVk7QaQygqxWNh8N2TraWW571kfGFqlUJ8+IIuSx8+CAMNRSCB7MruuSVLvkouywcVt3lSsQvhPRriY7UUXI4mYDCSihk7aP7e11OfVH3ex3/mELHPEG0UgfaIa/Co9SlOYDnMyKZz3aL5xV4w=="; + + public static final String SIGNTYPE = "RSA2"; + //支付宝回调的接口 + public static final String SERVERURL = "https://openapi.alipay.com/gateway.do"; + public static final String SERVERURL_SANDBOX = "https://openapi-sandbox.dl.alipaydev.com/gateway.do"; + private static AlipayClient alipayAppClient = null; + private static AlipayClient alipayWebClient = null; + + //因为支付宝alipayClient本身是线程安全的,因此只用创建一个,创建成单例的模式 +// public static AlipayClient getAlipayAppClient() { +// synchronized (AlipayConfig.class) { +// if (null == alipayAppClient) { +// alipayAppClient = new DefaultAlipayClient(SERVERURL, APP_ID_APP, APP_PRIVATE_KEY, +// AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8, +// ALIPAY_PUBLIC_KEY, SIGNTYPE); +// } +// } +// return alipayAppClient; +// } +// +// public static AlipayClient getAlipayWebClient() { +// synchronized (AlipayConfig.class) { +// if (null == alipayWebClient) { +// alipayWebClient = new DefaultAlipayClient(SERVERURL, APP_ID_WEB, APP_PRIVATE_KEY, +// AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8, +// ALIPAY_PUBLIC_KEY, SIGNTYPE); +// } +// } +// return alipayWebClient; +// } +// +// public static AlipayClient getAlipaySandboxClient() { +// synchronized (AlipayConfig.class) { +// if (null == alipayWebClient) { +// alipayWebClient = new DefaultAlipayClient(SERVERURL_SANDBOX, APP_ID_SANDBOX, APP_PRIVATE_KEY_SANDBOX, +// AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8, +// ALIPAY_PUBLIC_KEY_SANDBOX, SIGNTYPE); +// } +// } +// return alipayWebClient; +// } + + public static String notifyUrl(){ + return aliPayProperties.getNotifyUrl(); + } + + public static String returnUrl(){ + return aliPayProperties.getReturnUrl(); + } + + //因为支付宝alipayClient本身是线程安全的,因此只用创建一个,创建成单例的模式 + public static AlipayClient getClient(int type){ + synchronized (AlipayConfig.class) { + AlipayClient client = type==WEB ? alipayWebClient : alipayAppClient; + if(null == client) { + if (aliPayProperties.isSandBox()) { + client = new DefaultAlipayClient(SERVERURL_SANDBOX, APP_ID_SANDBOX, APP_PRIVATE_KEY_SANDBOX, + AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8, + ALIPAY_PUBLIC_KEY_SANDBOX, SIGNTYPE); + } else if (type == WEB) { + client = new DefaultAlipayClient(SERVERURL, APP_ID_WEB, APP_PRIVATE_KEY, + AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8, + ALIPAY_PUBLIC_KEY, SIGNTYPE); + } else if (type == APP) { + client = new DefaultAlipayClient(SERVERURL, APP_ID_APP, APP_PRIVATE_KEY, + AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8, + ALIPAY_PUBLIC_KEY, SIGNTYPE); + } + + if(type==WEB) + alipayWebClient = client; + else + alipayAppClient = client; + } + } + + return type==WEB ? alipayWebClient : alipayAppClient; + } + + public static Boolean checkSignature(Map params) throws AlipayApiException { + if(aliPayProperties.isSandBox()){ + return AlipaySignature.rsaCheckV1(params, AlipayConfig.ALIPAY_PUBLIC_KEY_SANDBOX, + AlipayConstants.CHARSET_UTF8,AlipayConfig.SIGNTYPE) ; + } + else{ + return AlipaySignature.rsaCheckV1(params, AlipayConfig.ALIPAY_PUBLIC_KEY, + AlipayConstants.CHARSET_UTF8,AlipayConfig.SIGNTYPE) ; + } + } +} diff --git a/src/main/java/com/ichangzuo/config/CaptchaConfig.java b/src/main/java/com/ichangzuo/config/CaptchaConfig.java new file mode 100644 index 0000000..b0db61f --- /dev/null +++ b/src/main/java/com/ichangzuo/config/CaptchaConfig.java @@ -0,0 +1,57 @@ +package com.ichangzuo.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import com.ichangzuo.config.property.CaptchaProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.awt.*; + +/** + * 验证码自动装配配置 + * + * @author haoxr + * @since 2023/11/24 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else if ("number".equalsIgnoreCase(codeType)) { + return new RandomGenerator("0123456789", codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/src/main/java/com/ichangzuo/config/CorsConfig.java b/src/main/java/com/ichangzuo/config/CorsConfig.java new file mode 100644 index 0000000..0e3415d --- /dev/null +++ b/src/main/java/com/ichangzuo/config/CorsConfig.java @@ -0,0 +1,42 @@ +package com.ichangzuo.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * CORS 资源共享配置 + * + * @author haoxr + * @since 2023/4/17 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/config/MailConfig.java b/src/main/java/com/ichangzuo/config/MailConfig.java new file mode 100644 index 0000000..4ae2c87 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/MailConfig.java @@ -0,0 +1,66 @@ +package com.ichangzuo.config; + +import com.ichangzuo.config.property.MailProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +/** + * MailConfig 配置类,用于手动配置和注入 JavaMailSender。 + * 通过读取 MailProperties 类中配置的邮件相关属性来初始化 JavaMailSender。 + *

+ * 手动注入的原因是为了避免在使用 application-dev.yml 或其他非 application.yml 配置文件时, + * IDEA 提示无法找到 JavaMailSender 的 bean。 + * + * @author Ray + * @since 2024/8/17 + */ +@Configuration +@EnableConfigurationProperties(MailProperties.class) +public class MailConfig { + + private final MailProperties mailProperties; + + public MailConfig(MailProperties mailProperties) { + this.mailProperties = mailProperties; + } + + /** + * 创建并配置 JavaMailSender bean。 + * + * @return 配置好的 JavaMailSender 实例 + */ + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(mailProperties.getHost()); + mailSender.setPort(mailProperties.getPort()); + mailSender.setUsername(mailProperties.getUsername()); + mailSender.setPassword(mailProperties.getPassword()); + mailSender.setProtocol(mailProperties.getProtocol()); + + Properties properties = mailSender.getJavaMailProperties(); + properties.put("mail.smtp.auth", mailProperties.getProperties().getMail().getSmtp().isAuth()); + properties.put("mail.smtp.starttls.enable", mailProperties.getProperties().getMail().getSmtp().getStarttls().isEnable()); + +// javaMailSender.setHost("smtp.qq.com"); // 设置邮箱服务器 +// javaMailSender.setPort(465); // 设置端口 +// javaMailSender.setUsername("747692844@qq.com"); // 设置用户名 +// javaMailSender.setPassword("<你的密码/授权码>"); // 设置密码(记得替换为你实际的密码、授权码) +// javaMailSender.setProtocol("smtps"); // 设置协议 +// +// Properties properties = new Properties(); // 配置项 + properties.put("mail.smtp.connectiontimeout", 5000); + properties.put("mail.smtp.timeout", 3000); + properties.put("mail.smtp.writetimeout", "5000"); +// properties.put("mail.smtp.auth", true); +// properties.put("mail.smtp.starttls.enable", true); +// properties.put("mail.smtp.starttls.required", true); + + return mailSender; + } +} diff --git a/src/main/java/com/ichangzuo/config/MybatisConfig.java b/src/main/java/com/ichangzuo/config/MybatisConfig.java new file mode 100644 index 0000000..fd7c790 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/MybatisConfig.java @@ -0,0 +1,49 @@ +package com.ichangzuo.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.ichangzuo.core.handler.MyDataPermissionHandler; +import com.ichangzuo.core.handler.MyMetaObjectHandler; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * mybatis-plus 自动配置类 + * + * @author haoxr + * @since 2022/7/2 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new MyMetaObjectHandler()); + return globalConfig; + } + +} diff --git a/src/main/java/com/ichangzuo/config/RedisCacheConfig.java b/src/main/java/com/ichangzuo/config/RedisCacheConfig.java new file mode 100644 index 0000000..07fbe82 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/RedisCacheConfig.java @@ -0,0 +1,74 @@ +package com.ichangzuo.config; + +import org.springframework.boot.autoconfigure.cache.CacheProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * Redis 缓存配置 + * + * @author Ray + * @since 2023/12/4 + */ +@EnableCaching +@EnableConfigurationProperties(CacheProperties.class) +@Configuration +@ConditionalOnProperty(name = "spring.cache.enabled") // xxl.job.enabled = true 才会自动装配 +public class RedisCacheConfig { + + /** + * 自定义 RedisCacheManager + *

+ * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer + * + * @param redisConnectionFactory {@link RedisConnectionFactory} + * @param cacheProperties {@link CacheProperties} + * @return {@link RedisCacheManager} + */ + @Bean + public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties cacheProperties){ + return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)) + .cacheDefaults(redisCacheConfiguration(cacheProperties)) + .build(); + } + + /** + * 自定义 RedisCacheConfiguration + * + * @param cacheProperties {@link CacheProperties} + * @return {@link RedisCacheConfiguration} + */ + @Bean + RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { + + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); + + config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())); + config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json())); + + CacheProperties.Redis redisProperties = cacheProperties.getRedis(); + + if (redisProperties.getTimeToLive() != null) { + config = config.entryTtl(redisProperties.getTimeToLive()); + } + if (!redisProperties.isCacheNullValues()) { + config = config.disableCachingNullValues(); + } + if (!redisProperties.isUseKeyPrefix()) { + config = config.disableKeyPrefix(); + } + // 覆盖默认key双冒号 CacheKeyPrefix#prefixed + config = config.computePrefixWith(name -> name + ":"); + return config; + } + +} diff --git a/src/main/java/com/ichangzuo/config/RedisConfig.java b/src/main/java/com/ichangzuo/config/RedisConfig.java new file mode 100644 index 0000000..824367f --- /dev/null +++ b/src/main/java/com/ichangzuo/config/RedisConfig.java @@ -0,0 +1,42 @@ +package com.ichangzuo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * Redis 配置 + * + * @author Ray + * @since 2023/5/15 + */ +@Configuration +public class RedisConfig { + + /** + * 自定义 RedisTemplate + *

+ * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer + * + * @param redisConnectionFactory {@link RedisConnectionFactory} + * @return {@link RedisTemplate} + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + + redisTemplate.setKeySerializer(RedisSerializer.string()); + redisTemplate.setValueSerializer(RedisSerializer.json()); + + redisTemplate.setHashKeySerializer(RedisSerializer.string()); + redisTemplate.setHashValueSerializer(RedisSerializer.json()); + + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } + +} diff --git a/src/main/java/com/ichangzuo/config/SecurityConfig.java b/src/main/java/com/ichangzuo/config/SecurityConfig.java new file mode 100644 index 0000000..79ebda4 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/SecurityConfig.java @@ -0,0 +1,132 @@ +package com.ichangzuo.config; + +import com.ichangzuo.core.security.service.SysUserDetailsService; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.ichangzuo.config.property.SecurityProperties; +import com.ichangzuo.common.constant.SecurityConstants; +import com.ichangzuo.core.filter.RateLimiterFilter; +//import com.ichangzuo.core.security.authentication.PreAuthenticationProvider; +import com.ichangzuo.core.security.exception.MyAccessDeniedHandler; +import com.ichangzuo.core.security.exception.MyAuthenticationEntryPoint; +import com.ichangzuo.core.security.filter.JwtValidationFilter; +import com.ichangzuo.core.security.filter.CaptchaValidationFilter; +import com.ichangzuo.system.service.ConfigService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.security.MessageDigest; + +/** + * Spring Security 权限配置 + * + * @author Ray + * @since 2023/2/17 + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final MyAuthenticationEntryPoint authenticationEntryPoint; + private final MyAccessDeniedHandler accessDeniedHandler; + private final RedisTemplate redisTemplate; + private final CodeGenerator codeGenerator; + private final SecurityProperties securityProperties; + private final ConfigService configService; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + + .authorizeHttpRequests(requestMatcherRegistry -> + requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH).permitAll() + .requestMatchers(SecurityConstants.REGISTER_PATH).permitAll() + .requestMatchers(SecurityConstants.AUTOLOGIN_PATH).permitAll() + .requestMatchers(SecurityConstants.SEND_REG_CODE).permitAll() + .requestMatchers(SecurityConstants.SEND_BIND_CODE).permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> + httpSecurityExceptionHandlingConfigurer + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler) + ) +// .authenticationProvider(preAuthenticationProvider) + .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .csrf(AbstractHttpConfigurer::disable) + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) + + ; + // 限流过滤器 + http.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class); + // 验证码校验过滤器 + http.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class); + // JWT 校验过滤器 + http.addFilterBefore(new JwtValidationFilter(redisTemplate,securityProperties.getJwt().getKey()), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + /** + * 不走过滤器链的放行配置 + */ + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> { + if (CollectionUtil.isNotEmpty(securityProperties.getIgnoreUrls())) { + web.ignoring().requestMatchers(securityProperties.getIgnoreUrls().toArray(new String[0])); + } + }; + } + + // + public static class MyPasswordEncoder implements PasswordEncoder { + @Override + public String encode(CharSequence charSequence) { + if(charSequence.length()>20) + return charSequence.toString(); + return DigestUtil.sha256Hex(DigestUtil.sha256Hex((String)charSequence)); + } + + @Override + public boolean matches(CharSequence charSequence, String s) { + return s.equals(encode(charSequence)); + } + } + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new MyPasswordEncoder(); //new BCryptPasswordEncoder(); + } + + /** + * AuthenticationManager 手动注入 + * + * @param authenticationConfiguration 认证配置 + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/ichangzuo/config/SwaggerConfig.java b/src/main/java/com/ichangzuo/config/SwaggerConfig.java new file mode 100644 index 0000000..d9b7bd0 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/SwaggerConfig.java @@ -0,0 +1,84 @@ +package com.ichangzuo.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; + +/** + * Swagger 配置 + *

+ * + * @author Ray + * @see knife4j 快速开始 + * @since 2023/2/17 + */ +@Configuration +@Slf4j +@RequiredArgsConstructor +public class SwaggerConfig { + + private final Environment environment; + + /** + * 接口信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("系统接口文档") + .version(appVersion) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + *

+ * 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。 + * 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加鉴权参数 + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((s, pathItem) -> { + // 登录接口/验证码不需要添加鉴权参数 + if ("/api/v1/auth/login".equals(s) || "/api/v1/auth/captcha".equals(s)) { + return; + } + // 接口添加鉴权参数 + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} diff --git a/src/main/java/com/ichangzuo/config/WebMvcConfig.java b/src/main/java/com/ichangzuo/config/WebMvcConfig.java new file mode 100644 index 0000000..278e864 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/WebMvcConfig.java @@ -0,0 +1,67 @@ +package com.ichangzuo.config; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.util.List; + +/** + * WebMvc 自动装配配置 + * + * @author Ray + * @since 2020/10/16 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + @Override + public void configureMessageConverters(List> converters) { + converters.add(new StringHttpMessageConverter()); + + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + + // 后台Long值传递给前端精度丢失问题(JS最大精度整数是Math.pow(2,53)) + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(jackson2HttpMessageConverter); + } + + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 不校验所有参数,只要出现校验失败情况直接返回,不再进行后续参数校验 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory(); + + return validatorFactory.getValidator(); + } +} diff --git a/src/main/java/com/ichangzuo/config/WebSocketConfig.java b/src/main/java/com/ichangzuo/config/WebSocketConfig.java new file mode 100644 index 0000000..5d3797f --- /dev/null +++ b/src/main/java/com/ichangzuo/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +//package com.ichangzuo.config; +// +//import cn.hutool.core.util.StrUtil; +//import cn.hutool.jwt.JWTPayload; +//import cn.hutool.jwt.JWTUtil; +//import com.ichangzuo.common.constant.SecurityConstants; +//import com.ichangzuo.system.event.UserConnectionEvent; +//import lombok.extern.slf4j.Slf4j; +//import org.jetbrains.annotations.NotNull; +//import org.springframework.context.ApplicationEventPublisher; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.http.HttpHeaders; +//import org.springframework.messaging.Message; +//import org.springframework.messaging.MessageChannel; +//import org.springframework.messaging.simp.config.ChannelRegistration; +//import org.springframework.messaging.simp.config.MessageBrokerRegistry; +//import org.springframework.messaging.simp.stomp.StompCommand; +//import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +//import org.springframework.messaging.support.ChannelInterceptor; +//import org.springframework.messaging.support.MessageHeaderAccessor; +//import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +//import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +//import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +// +///** +// * WebSocket 自动配置类 +// * +// * @author haoxr +// * @since 2.4.0 +// */ +//// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +//@EnableWebSocketMessageBroker +//@Configuration +//@Slf4j +//public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { +// +// private final ApplicationEventPublisher eventPublisher; +// +// public WebSocketConfig(ApplicationEventPublisher eventPublisher) { +// this.eventPublisher = eventPublisher; +// } +// /** +// * 注册一个端点,客户端通过这个端点进行连接 +// */ +// @Override +// public void registerStompEndpoints(StompEndpointRegistry registry) { +// registry +// // 注册 /ws 的端点 +// .addEndpoint("/ws") +// // 允许跨域 +// .setAllowedOriginPatterns("*"); +// } +// +// +// /** +// * 配置消息代理 +// */ +// @Override +// public void configureMessageBroker(MessageBrokerRegistry registry) { +// // 客户端发送消息的请求前缀 +// registry.setApplicationDestinationPrefixes("/app"); +// +// // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 +// registry.enableSimpleBroker("/topic", "/queue"); +// +// // 服务端通知客户端的前缀,可以不设置,默认为user +// registry.setUserDestinationPrefix("/user"); +// } +// +// +// /** +// * 配置客户端入站通道拦截器 +// *

+// * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 +// * +// * @param registration 通道注册器 +// */ +// @Override +// public void configureClientInboundChannel(ChannelRegistration registration) { +// registration.interceptors(new ChannelInterceptor() { +// @Override +// public Message preSend(@NotNull Message message, @NotNull MessageChannel channel) { +// StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); +// if (accessor != null) { +// if (StompCommand.CONNECT.equals(accessor.getCommand())) { +// String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); +// if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { +// bearerToken = bearerToken.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); +// String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); +// if (StrUtil.isNotBlank(username)) { +// accessor.setUser(() -> username); +// eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); +// } +// } +// } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { +// if (accessor.getUser() != null) { +// String username = accessor.getUser().getName(); +// eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); +// } +// } +// } +// return ChannelInterceptor.super.preSend(message, channel); +// } +// }); +// } +// +//} diff --git a/src/main/java/com/ichangzuo/config/WxpayConfig.java b/src/main/java/com/ichangzuo/config/WxpayConfig.java new file mode 100644 index 0000000..805423e --- /dev/null +++ b/src/main/java/com/ichangzuo/config/WxpayConfig.java @@ -0,0 +1,78 @@ +package com.ichangzuo.config; + +import com.ichangzuo.config.property.AliPayProperties; +import com.ichangzuo.config.property.WxPayProperties; +import com.wechat.pay.java.core.Config; +import com.wechat.pay.java.core.RSAAutoCertificateConfig; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(WxPayProperties.class) +public class WxpayConfig { + private static WxPayProperties wxPayProperties; + + public WxpayConfig(WxPayProperties properties) { + wxPayProperties = properties; + } + + public static final int WX_APP = 0; + public static final int WX_WEB = 1; + /** App号 */ + public static String appAppId = "wx8f83a7f750d63431"; + /** 商户号 */ + public static String appMerchantId = "1266239101"; + /** 商户API私钥路径 */ + /** 商户证书序列号 */ + public static String appMerchantSerialNumber = "18650196CADB6AB05753AFF90B5C60EBFDDB9858"; + + /** App号 */ + public static String webAppId = "wx0243718acff3873f";// + /** 商户号 */ + public static String webMerchantId = "1250765101";// + /** 商户API私钥路径 */ + /** 商户证书序列号 */ + public static String webMerchantSerialNumber = "6D24B656FFCD5DFADAC7CDFFDFEC60FDA07B8654";// + + /** 商户APIV3密钥 */ + public static String apiV3Key = "aS1yC4sS3xM8hI3oF1qD2uZ7kH8mO3uB";// + //回调地址 + //Wxpayconfig + private static Config wxpayAppConfig = null; + private static Config wxpayWebConfig = null; + + public static Config getWxpayConfig(int type) { + Config config = type == WX_APP ? wxpayAppConfig : wxpayWebConfig; + synchronized (WxpayConfig.class) { + + if (null == config) { + if(type==WX_APP) { + config = new RSAAutoCertificateConfig.Builder() + .merchantId(appMerchantId) + .privateKeyFromPath(wxPayProperties.getAppPrivateKeyPath() + "apiclient_key.pem") + .merchantSerialNumber(appMerchantSerialNumber) + .apiV3Key(apiV3Key) + .build(); + } + else if(type==WX_WEB){ + config = new RSAAutoCertificateConfig.Builder() + .merchantId(webMerchantId) + .privateKeyFromPath(wxPayProperties.getWebPrivateKeyPath() + "apiclient_key.pem") + .merchantSerialNumber(webMerchantSerialNumber) + .apiV3Key(apiV3Key) + .build(); + } + } + } + return config; + } + + public static String notifyUrl(int type){ + if( type==WX_APP ) + return wxPayProperties.getAppNotifyUrl(); + else if( type==WX_WEB ) + return wxPayProperties.getWebNotifyUrl(); + + return null; + } +} diff --git a/src/main/java/com/ichangzuo/config/XxlJobConfig.java b/src/main/java/com/ichangzuo/config/XxlJobConfig.java new file mode 100644 index 0000000..2328116 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/XxlJobConfig.java @@ -0,0 +1,61 @@ +package com.ichangzuo.config; + +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * xxl-job config + * + * @author xuxueli 2017-04-28 + */ +@Configuration +@ConditionalOnProperty(name = "xxl.job.enabled") // xxl.job.enabled = true 才会自动装配 +@Slf4j +public class XxlJobConfig { + + @Value("${xxl.job.admin.addresses}") + private String adminAddresses; + + @Value("${xxl.job.accessToken}") + private String accessToken; + + @Value("${xxl.job.executor.appname}") + private String appname; + + @Value("${xxl.job.executor.address}") + private String address; + + @Value("${xxl.job.executor.ip}") + private String ip; + + @Value("${xxl.job.executor.port}") + private int port; + + @Value("${xxl.job.executor.logpath}") + private String logPath; + + @Value("${xxl.job.executor.logretentiondays}") + private int logRetentionDays; + + + @Bean + public XxlJobSpringExecutor xxlJobExecutor() { + log.info(">>>>>>>>>>> xxl-job config init."); + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + xxlJobSpringExecutor.setAdminAddresses(adminAddresses); + xxlJobSpringExecutor.setAppname(appname); + xxlJobSpringExecutor.setAddress(address); + xxlJobSpringExecutor.setIp(ip); + xxlJobSpringExecutor.setPort(port); + xxlJobSpringExecutor.setAccessToken(accessToken); + xxlJobSpringExecutor.setLogPath(logPath); + xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); + + return xxlJobSpringExecutor; + } + +} diff --git a/src/main/java/com/ichangzuo/config/property/AliPayProperties.java b/src/main/java/com/ichangzuo/config/property/AliPayProperties.java new file mode 100644 index 0000000..b367508 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/property/AliPayProperties.java @@ -0,0 +1,12 @@ +package com.ichangzuo.config.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "pay.ali") +@Data +public class AliPayProperties { + private boolean sandBox; + private String notifyUrl; + private String returnUrl; +} \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/config/property/AliyunSmsProperties.java b/src/main/java/com/ichangzuo/config/property/AliyunSmsProperties.java new file mode 100644 index 0000000..fa21737 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/property/AliyunSmsProperties.java @@ -0,0 +1,50 @@ +package com.ichangzuo.config.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +/** + * 阿里云短信配置 + * + * @author Ray + * @since 2024/8/17 + */ +@Configuration +@ConfigurationProperties(prefix = "sms.aliyun") +@Data +public class AliyunSmsProperties { + + /** + * 阿里云账户的Access Key ID,用于API请求认证 + */ + private String accessKeyId; + + /** + *阿里云账户的Access Key Secret,用于API请求认证 + */ + private String accessKeySecret; + + /** + * 阿里云短信服务API的域名 eg: dysmsapi.aliyuncs.com + */ + private String domain; + + /** + * 阿里云服务的区域ID,如cn-shanghai + */ + private String regionId; + + /** + * 短信签名,必须是已经在阿里云短信服务中注册并通过审核的 + */ + private String signName; + + /** + * 模板编码 + */ + private Map templateCodes; + +} diff --git a/src/main/java/com/ichangzuo/config/property/CaptchaProperties.java b/src/main/java/com/ichangzuo/config/property/CaptchaProperties.java new file mode 100644 index 0000000..f11a160 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/property/CaptchaProperties.java @@ -0,0 +1,92 @@ +package com.ichangzuo.config.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 验证码 属性配置 + * + * @author haoxr + * @since 2023/11/24 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/src/main/java/com/ichangzuo/config/property/CodegenProperties.java b/src/main/java/com/ichangzuo/config/property/CodegenProperties.java new file mode 100644 index 0000000..1603306 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/property/CodegenProperties.java @@ -0,0 +1,96 @@ +//package com.ichangzuo.config.property; +// +//import cn.hutool.core.io.file.FileNameUtil; +//import cn.hutool.core.map.MapUtil; +//import lombok.Data; +//import org.springframework.boot.context.properties.ConfigurationProperties; +//import org.springframework.stereotype.Component; +// +//import java.util.List; +//import java.util.Map; +// +///** +// * 代码生成配置属性 +// * +// * @author Ray +// * @since 2.11.0 +// */ +//@Component +//@ConfigurationProperties(prefix = "codegen") +//@Data +//public class CodegenProperties { +// +// +// /** +// * 默认配置 +// */ +// private DefaultConfig defaultConfig ; +// +// /** +// * 模板配置 +// */ +// private Map templateConfigs = MapUtil.newHashMap(true); +// +// /** +// * 后端应用名 +// */ +// private String backendAppName; +// +// /** +// * 前端应用名 +// */ +// private String frontendAppName; +// +// /** +// * 下载文件名 +// */ +// private String downloadFileName; +// +// /** +// * 排除数据表 +// */ +// private List excludeTables; +// +// /** +// * 模板配置 +// */ +// @Data +// public static class TemplateConfig { +// +// /** +// * 模板路径 (e.g. /templates/codegen/controller.java.vm) +// */ +// private String templatePath; +// +// /** +// * 子包名 (e.g. controller/service/mapper/model) +// */ +// private String subpackageName; +// +// /** +// * 文件扩展名,如 .java +// */ +// private String extension = FileNameUtil.EXT_JAVA; +// +// } +// +// /** +// * 默认配置 +// */ +// @Data +// public static class DefaultConfig { +// +// /** +// * 作者 (e.g. Ray) +// */ +// private String author; +// +// /** +// * 默认模块名(e.g. system) +// */ +// private String moduleName; +// +// } +// +// +//} diff --git a/src/main/java/com/ichangzuo/config/property/MailProperties.java b/src/main/java/com/ichangzuo/config/property/MailProperties.java new file mode 100644 index 0000000..975c5cc --- /dev/null +++ b/src/main/java/com/ichangzuo/config/property/MailProperties.java @@ -0,0 +1,99 @@ +package com.ichangzuo.config.property; + +import cn.hutool.extra.mail.Mail; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 邮件配置类,用于接收和存储邮件相关的配置属性。 + * + * @author Ray + * @since 2024/8/17 + */ +@ConfigurationProperties(prefix = "spring.mail") +@Data +public class MailProperties { + + /** + * 邮件服务器主机名或 IP 地址。 + * 例如:smtp.example.com + */ + private String host; + + /** + * 邮件服务器端口号。 + * 例如:587 + */ + private int port; + + /** + * 用于连接邮件服务器的用户名。 + * 例如:your_email@example.com + */ + private String username; + + /** + * 用于连接邮件服务器的密码。 + * 该密码应安全存储,不应在代码中硬编码。 + */ + private String password; + + /** + * 用于发送邮件的协议 + */ + private String protocol; + + /** + * 邮件发送者地址。 + */ + private String from; + + /** + * 邮件服务器的其他属性配置。 + * 这些配置通常用于进一步定制邮件发送行为。 + */ + private Properties properties = new Properties(); + + /** + * 内部类,用于封装邮件服务器的详细配置。 + * 包含 SMTP 相关的配置选项。 + */ + @Data + public static class Properties { + private Mail mail = new Mail(); + + @Data + public static class Mail { + /** + * SMTP 配置选项类。 + * 包含认证、加密等与 SMTP 协议相关的配置。 + */ + private Smtp smtp = new Smtp(); + + @Data + public static class Smtp { + + /** + * 是否启用 SMTP 认证。 + * 如果为 `true`,则需要提供有效的用户名和密码进行认证。 + */ + private boolean auth; + + /** + * STARTTLS 加密配置选项。 + */ + private StartTls starttls = new StartTls(); + + @Data + public static class StartTls { + + /** + * 是否启用 STARTTLS 加密。 + * 如果为 `true`,在发送邮件时将启用 STARTTLS 协议进行加密传输。 + */ + private boolean enable; + } + } + } + } +} diff --git a/src/main/java/com/ichangzuo/config/property/SecurityProperties.java b/src/main/java/com/ichangzuo/config/property/SecurityProperties.java new file mode 100644 index 0000000..aa3f714 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/property/SecurityProperties.java @@ -0,0 +1,44 @@ +package com.ichangzuo.config.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +/** + * @author haoxr + * @since 2024/4/18 + */ +@Data +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 白名单 URL 集合 + */ + private List ignoreUrls; + + /** + * JWT 配置 + */ + private JwtProperty jwt; + + + /** + * JWT 配置 + */ + @Data + public static class JwtProperty { + + /** + * JWT 密钥 + */ + private String key; + + /** + * JWT 过期时间 + */ + private Long ttl; + + } +} diff --git a/src/main/java/com/ichangzuo/config/property/WxPayProperties.java b/src/main/java/com/ichangzuo/config/property/WxPayProperties.java new file mode 100644 index 0000000..bcadb74 --- /dev/null +++ b/src/main/java/com/ichangzuo/config/property/WxPayProperties.java @@ -0,0 +1,13 @@ +package com.ichangzuo.config.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "pay.wx") +@Data +public class WxPayProperties { + private String appNotifyUrl; + private String webNotifyUrl; + private String appPrivateKeyPath; + private String webPrivateKeyPath; +} diff --git a/src/main/java/com/ichangzuo/core/aspect/LogAspect.java b/src/main/java/com/ichangzuo/core/aspect/LogAspect.java new file mode 100644 index 0000000..30715d8 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/aspect/LogAspect.java @@ -0,0 +1,95 @@ +package com.ichangzuo.core.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import com.ichangzuo.common.annotation.Log; +import com.ichangzuo.common.constant.SecurityConstants; +import com.ichangzuo.common.util.IPUtils; +import com.ichangzuo.core.security.util.SecurityUtils; +import com.ichangzuo.system.service.LogService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; + +/** + * 日志切面 + * + * @author Ray + * @since 2024/6/25 + */ +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class LogAspect { + + private final LogService logService; + private final HttpServletRequest request; + + @Pointcut("@annotation(com.ichangzuo.common.annotation.Log)") + public void logPointcut() { + } + + @Around("logPointcut() && @annotation(logAnnotation)") + public Object logExecutionTime(ProceedingJoinPoint joinPoint, Log logAnnotation) throws Throwable { + String requestURI = request.getRequestURI(); + + Long userId = null; + // 非登录请求获取用户ID,登录请求在登录成功后(joinPoint.proceed())获取用户ID + if (!SecurityConstants.LOGIN_PATH.equals(requestURI)) { + userId = SecurityUtils.getUserId(); + } + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + Object proceed = joinPoint.proceed(); + long executionTime = timer.interval(); + + // 创建日志记录 + com.ichangzuo.system.model.entity.Log log = new com.ichangzuo.system.model.entity.Log(); + log.setModule(logAnnotation.module()); + log.setContent(logAnnotation.value()); + log.setRequestUri(requestURI); + // 登录方法需要在登录成功后获取用户ID + if (userId == null) { + userId = SecurityUtils.getUserId(); + } + log.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + log.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + log.setProvince(regionArray[2]); + log.setCity(regionArray[3]); + } + } + } + log.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = UserAgentUtil.parse(userAgentString); + // 系统信息 + log.setOs(userAgent.getOs().getName()); + // 浏览器信息 + log.setBrowser(userAgent.getBrowser().getName()); + log.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + // 保存日志到数据库 + logService.save(log); + + return proceed; + } + + +} diff --git a/src/main/java/com/ichangzuo/core/aspect/RepeatSubmitAspect.java b/src/main/java/com/ichangzuo/core/aspect/RepeatSubmitAspect.java new file mode 100644 index 0000000..968499b --- /dev/null +++ b/src/main/java/com/ichangzuo/core/aspect/RepeatSubmitAspect.java @@ -0,0 +1,82 @@ +package com.ichangzuo.core.aspect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTUtil; +import cn.hutool.jwt.RegisteredPayload; +import com.ichangzuo.common.annotation.RepeatSubmit; +import com.ichangzuo.common.constant.RedisConstants; +import com.ichangzuo.common.constant.SecurityConstants; +import com.ichangzuo.common.exception.BusinessException; +import com.ichangzuo.common.result.ResultCode; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.concurrent.TimeUnit; + +/** + * 处理重复提交的切面 + * + * @author haoxr + * @since 2.3.0 + */ +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class RepeatSubmitAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(repeatSubmit)") + public void preventDuplicateSubmitPointCut(RepeatSubmit repeatSubmit) { + log.info("定义防重复提交切点"); + } + + @Around("preventDuplicateSubmitPointCut(repeatSubmit)") + public Object doAround(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable { + + String resubmitLockKey = generateResubmitLockKey(); + if (resubmitLockKey != null) { + int expire = repeatSubmit.expire(); // 防重提交锁过期时间 + RLock lock = redissonClient.getLock(resubmitLockKey); + boolean lockResult = lock.tryLock(0, expire, TimeUnit.SECONDS); // 获取锁失败,直接返回 false + if (!lockResult) { + throw new BusinessException(ResultCode.REPEAT_SUBMIT_ERROR); // 抛出重复提交提示信息 + } + } + return pjp.proceed(); + } + + + /** + * 获取重复提交锁的 key + */ + private String generateResubmitLockKey() { + String resubmitLockKey = null; + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); + // 从 JWT Token 中获取 jti + String jti = (String) JWTUtil.parseToken(token).getPayload(RegisteredPayload.JWT_ID); + resubmitLockKey = RedisConstants.RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI(); + } + return resubmitLockKey; + } + +} diff --git a/src/main/java/com/ichangzuo/core/filter/RateLimiterFilter.java b/src/main/java/com/ichangzuo/core/filter/RateLimiterFilter.java new file mode 100644 index 0000000..e6107c8 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/filter/RateLimiterFilter.java @@ -0,0 +1,81 @@ +package com.ichangzuo.core.filter; + +import com.ichangzuo.common.constant.RedisConstants; +import com.ichangzuo.common.result.ResultCode; +import com.ichangzuo.common.util.IPUtils; +import com.ichangzuo.common.util.ResponseUtils; +import com.ichangzuo.system.service.ConfigService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * IP限流过滤器 + * + * @author Theo + * @since 2024/08/10 14:38 + */ +@Slf4j +public class RateLimiterFilter extends OncePerRequestFilter { + + private final RedisTemplate redisTemplate; + private final ConfigService configService; + + public RateLimiterFilter(RedisTemplate redisTemplate, ConfigService configService) { + this.redisTemplate = redisTemplate; + this.configService = configService; + } + + /** + * 确认是否限流方法 + * 默认情况下:限制同一个IP的QPS最大为10,可以通过修改系统配置进行调整 + * 这里也可以进行扩展,比如redis记录同一个ip每天出发限流的上限次数,记录在redis中,达到某个阈值后,进行永久封禁这个ip + * + * @param ip ip地址 + * @return 是否限流 + */ + public boolean rateLimit(String ip) { + String key = RedisConstants.IP_RATE_LIMITER_KEY + ip; + Long count = redisTemplate.opsForValue().increment(key); + if (count == null || count == 1) { + redisTemplate.expire(key,1, TimeUnit.SECONDS); + } + Object systemConfig = configService.getSystemConfig(RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY); + long limit = 10; + if(systemConfig != null){ + limit = Long.parseLong(systemConfig.toString()); + }else{ +// log.warn("[RedisRateLimiterFilter.rateLimit]系统配置中未配置IP请求限制QPS阈值配置,使用默认值:{},请检查配置项:{}", +// limit,RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY); + } + return count != null && count > limit; + } + + /** + * IP限流过滤器 + * 默认情况下:限制同一个IP在一分钟内只能访问10次,可以通过修改系统配置进行调整 + * + * @param request 请求体 + * @param response 响应体 + * @param filterChain 过滤器链 + */ + @Override + protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, + @NotNull FilterChain filterChain) throws ServletException, IOException { + String ip = IPUtils.getIpAddr(request); + if (rateLimit(ip)) { + ResponseUtils.writeErrMsg(response, ResultCode.FLOW_LIMITING); + return; + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/ichangzuo/core/filter/RequestLogFilter.java b/src/main/java/com/ichangzuo/core/filter/RequestLogFilter.java new file mode 100644 index 0000000..ae07c70 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/filter/RequestLogFilter.java @@ -0,0 +1,36 @@ +package com.ichangzuo.core.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; + +/** + * 请求日志打印过滤器 + * + * @author haoxr + * @since 2023/03/03 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + log.info("request uri: {}", requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} diff --git a/src/main/java/com/ichangzuo/core/handler/MyDataPermissionHandler.java b/src/main/java/com/ichangzuo/core/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000..d924eaa --- /dev/null +++ b/src/main/java/com/ichangzuo/core/handler/MyDataPermissionHandler.java @@ -0,0 +1,101 @@ +package com.ichangzuo.core.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import com.ichangzuo.common.annotation.DataPermission; +import com.ichangzuo.common.base.IBaseEnum; +import com.ichangzuo.common.enums.DataScopeEnum; +import com.ichangzuo.core.security.util.SecurityUtils; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; + +import java.lang.reflect.Method; + +/** + * 数据权限控制器 + * + * @author zc + * @since 2021-12-10 13:28 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + + Class clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 如果没有注解或者是超级管理员,直接返回 + if (annotation == null || SecurityUtils.isRoot() ) { + return where; + } + return dataScopeFilter(/*annotation.deptAlias(), annotation.deptIdColumnName(), */annotation.userAlias(), annotation.userIdColumnName(), where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(/*String deptAlias, String deptIdColumnName,*/ String userAlias, String userIdColumnName, Expression where) { + + +// String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { +// case ALL: +// return where; +// case DEPT: +// deptId = SecurityUtils.getDeptId(); +// appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; +// break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: +// deptId = SecurityUtils.getDeptId(); +// appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; +// break; + return where; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/src/main/java/com/ichangzuo/core/handler/MyMetaObjectHandler.java b/src/main/java/com/ichangzuo/core/handler/MyMetaObjectHandler.java new file mode 100644 index 0000000..30e55f2 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/handler/MyMetaObjectHandler.java @@ -0,0 +1,39 @@ +package com.ichangzuo.core.handler; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * mybatis-plus 字段自动填充 + * + * @author haoxr + * @since 2022/10/14 + */ +@Component +public class MyMetaObjectHandler implements MetaObjectHandler { + + /** + * 新增填充创建时间 + * + * @param metaObject 元数据 + */ + @Override + public void insertFill(MetaObject metaObject) { + this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class); + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); + } + + /** + * 更新填充更新时间 + * + * @param metaObject 元数据 + */ + @Override + public void updateFill(MetaObject metaObject) { + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); + } + +} diff --git a/src/main/java/com/ichangzuo/core/security/exception/MyAccessDeniedHandler.java b/src/main/java/com/ichangzuo/core/security/exception/MyAccessDeniedHandler.java new file mode 100644 index 0000000..56e6974 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/security/exception/MyAccessDeniedHandler.java @@ -0,0 +1,25 @@ +package com.ichangzuo.core.security.exception; + +import com.ichangzuo.common.result.ResultCode; +import com.ichangzuo.common.util.ResponseUtils; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Spring Security访问异常处理器 + * + * @author haoxr + * @since 2022/10/18 + */ +@Component +public class MyAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED); + } +} diff --git a/src/main/java/com/ichangzuo/core/security/exception/MyAuthenticationEntryPoint.java b/src/main/java/com/ichangzuo/core/security/exception/MyAuthenticationEntryPoint.java new file mode 100644 index 0000000..8df36b3 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/security/exception/MyAuthenticationEntryPoint.java @@ -0,0 +1,51 @@ +package com.ichangzuo.core.security.exception; + +import com.ichangzuo.common.exception.AccountDeletedException; +import com.ichangzuo.common.exception.AccountLockedException; +import com.ichangzuo.common.result.ResultCode; +import com.ichangzuo.common.util.ResponseUtils; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +/** + * 认证异常处理 + * + * @author haoxr + * @since 2.0.0 + */ +@Component +public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + int status = response.getStatus(); + if (status == HttpServletResponse.SC_NOT_FOUND) { + // 资源不存在 + ResponseUtils.writeErrMsg(response, ResultCode.RESOURCE_NOT_FOUND); + } else { + if(authException instanceof BadCredentialsException){ + // 用户名或密码错误 + ResponseUtils.writeErrMsg(response, ResultCode.USERNAME_OR_PASSWORD_ERROR); + } + else if(authException.getCause() instanceof AccountLockedException){ + // 账户被冻结 + ResponseUtils.writeErrMsg(response, ResultCode.USER_ACCOUNT_LOCKED); + } + else if(authException.getCause() instanceof AccountDeletedException){ + // 账户已注销 + ResponseUtils.writeErrMsg(response, ResultCode.USER_ACCOUNT_DELETED); + } + else { + // 未认证或者token过期 + ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); + } + } + } +} diff --git a/src/main/java/com/ichangzuo/core/security/filter/CaptchaValidationFilter.java b/src/main/java/com/ichangzuo/core/security/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000..b53ac4a --- /dev/null +++ b/src/main/java/com/ichangzuo/core/security/filter/CaptchaValidationFilter.java @@ -0,0 +1,74 @@ +package com.ichangzuo.core.security.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import com.ichangzuo.common.constant.SecurityConstants; +import com.ichangzuo.common.result.ResultCode; +import com.ichangzuo.common.util.ResponseUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + + +/** + * 图形验证码校验过滤器 + * + * @author haoxr + * @since 2022/10/1 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST"); + private static final AntPathRequestMatcher SEND_REG_CODE_MATCHER = new AntPathRequestMatcher(SecurityConstants.SEND_REG_CODE, "POST"); + private static final AntPathRequestMatcher SEND_BIND_CODE_MATCHER = new AntPathRequestMatcher(SecurityConstants.SEND_BIND_CODE, "POST"); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request) || SEND_REG_CODE_MATCHER.matches(request) + || SEND_BIND_CODE_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get(SecurityConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/src/main/java/com/ichangzuo/core/security/filter/JwtValidationFilter.java b/src/main/java/com/ichangzuo/core/security/filter/JwtValidationFilter.java new file mode 100644 index 0000000..2ef01e6 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/security/filter/JwtValidationFilter.java @@ -0,0 +1,83 @@ +package com.ichangzuo.core.security.filter; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import com.ichangzuo.common.constant.SecurityConstants; +import com.ichangzuo.common.result.ResultCode; +import com.ichangzuo.common.util.ResponseUtils; +import com.ichangzuo.core.security.util.JwtUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * JWT token 校验过滤器 + * + * @author Ray Hao + * @since 2023/9/13 + */ +public class JwtValidationFilter extends OncePerRequestFilter { + + private final RedisTemplate redisTemplate; + + private final byte[] secretKey; + + public JwtValidationFilter(RedisTemplate redisTemplate, String secretKey) { + this.redisTemplate = redisTemplate; + this.secretKey = secretKey.getBytes(); + } + + + /** + * 从请求中获取 JWT Token,校验 JWT Token 是否合法 + *

+ * 如果合法则将 Authentication 设置到 Spring Security Context 上下文中 + * 如果不合法则清空 Spring Security Context 上下文,并直接返回响应 + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + try { + if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { + // 去除 Bearer 前缀 + token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); + // 解析 Token + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValidate = jwt.setKey(secretKey).validate(0); + if (!isValidate) { + ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); + return; + } + // 检查 Token 是否已被加入黑名单(注销) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + boolean isTokenBlacklisted = Boolean.TRUE.equals(redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti)); + if (isTokenBlacklisted) { + ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); + return; + } + // Token 有效将其解析为 Authentication 对象,并设置到 Spring Security 上下文中 + Authentication authentication = JwtUtils.getAuthentication(payloads); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + SecurityContextHolder.clearContext(); + ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); + return; + } + // Token有效或无Token时继续执行过滤链 + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/ichangzuo/core/security/model/SysUserDetails.java b/src/main/java/com/ichangzuo/core/security/model/SysUserDetails.java new file mode 100644 index 0000000..0d0c386 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/security/model/SysUserDetails.java @@ -0,0 +1,102 @@ +package com.ichangzuo.core.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.ichangzuo.system.model.dto.UserAuthInfo; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Spring Security 用户对象 + * + * @author haoxr + * @since 3.0.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + @Getter + private Long userId; + + private String username; + + private String password; + +// private Boolean enabled; +// private Boolean locked; + + private Collection authorities; + + private Set perms; + +// private Long deptId; + + private Integer dataScope; + + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + Set roles = user.getRoles(); + Set authorities; + if (CollectionUtil.isNotEmpty(roles)) { + authorities = roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // 标识角色 + .collect(Collectors.toSet()); + } else { + authorities = Collections.EMPTY_SET; + } + this.authorities = authorities; + this.username = user.getUsername(); + this.password = user.getPassword(); +// this.enabled = user.getStatus()>1; +// this.locked = user.getStatus()==1; + this.perms = user.getPerms(); +// this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + } + + + @Override + public Collection getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/ichangzuo/core/security/service/PermissionService.java b/src/main/java/com/ichangzuo/core/security/service/PermissionService.java new file mode 100644 index 0000000..bbbdba1 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package com.ichangzuo.core.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.ichangzuo.common.constant.SecurityConstants; +import com.ichangzuo.core.security.util.SecurityUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; + +import java.util.*; + +/** + * SpringSecurity 权限校验 + * + * @author haoxr + * @since 2022/2/22 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限"); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(SecurityConstants.ROLE_PERMS_PREFIX, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/src/main/java/com/ichangzuo/core/security/service/SysUserDetailsService.java b/src/main/java/com/ichangzuo/core/security/service/SysUserDetailsService.java new file mode 100644 index 0000000..0460fa9 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/security/service/SysUserDetailsService.java @@ -0,0 +1,59 @@ +package com.ichangzuo.core.security.service; + +import com.ichangzuo.common.exception.AccountDeletedException; +import com.ichangzuo.common.exception.AccountLockedException; +import com.ichangzuo.common.exception.BusinessException; +import com.ichangzuo.common.result.ResultCode; +import com.ichangzuo.core.security.model.SysUserDetails; +import com.ichangzuo.system.model.dto.UserAuthInfo; +import com.ichangzuo.system.service.UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +/** + * 系统用户认证 DetailsService + * + * @author Ray + * @since 2021/10/19 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class SysUserDetailsService implements UserDetailsService { + + private final UserService userService; + + /** + * 根据用户名获取用户信息 + * + * @param username 用户名 + * @return 用户信息 + * @throws UsernameNotFoundException 用户名未找到异常 + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + try { + UserAuthInfo userAuthInfo = userService.getUserAuthInfo(username); + if (userAuthInfo == null) { + throw new UsernameNotFoundException(username); + } + else if(userAuthInfo.getStatus()==0){ + throw new AccountDeletedException(username); + } + else if(userAuthInfo.getStatus()==1){ + throw new AccountLockedException(username); + } + return new SysUserDetails(userAuthInfo); + } catch (Exception e) { + // 记录异常日志 + log.error("认证异常:{}", e.getMessage()); + // 抛出异常 + throw e; + } + } +} diff --git a/src/main/java/com/ichangzuo/core/security/util/JwtUtils.java b/src/main/java/com/ichangzuo/core/security/util/JwtUtils.java new file mode 100644 index 0000000..0a4363d --- /dev/null +++ b/src/main/java/com/ichangzuo/core/security/util/JwtUtils.java @@ -0,0 +1,108 @@ +package com.ichangzuo.core.security.util; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import com.ichangzuo.common.constant.JwtClaimConstants; +import com.ichangzuo.core.security.model.SysUserDetails; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * JWT Token 工具类 + * + * @author Ray Hao + * @since 2.6.0 + */ +@Component +public class JwtUtils { + + /** + * JWT 加解密使用的密钥 + */ + private static byte[] key; + + + /** + * JWT Token 的有效时间(单位:秒) + */ + private static int ttl; + + + @Value("${security.jwt.key}") + public void setKey(String key) { + JwtUtils.key = key.getBytes(); + } + + @Value("${security.jwt.ttl}") + public void setTtl(Integer ttl) { + JwtUtils.ttl = ttl; + } + + /** + * 生成 JWT Token + * + * @param authentication 用户认证信息 + * @return Token 字符串 + */ + public static String createToken(Authentication authentication) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID +// payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + + Date now = new Date(); + Date expiration = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.ISSUED_AT, now); + payload.put(JWTPayload.EXPIRES_AT, expiration); + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, key); + } + + + /** + * 从 JWT Token 中解析 Authentication 用户认证信息 + * + * @param payloads JWT 载体 + * @return 用户认证信息 + */ + public static UsernamePasswordAuthenticationToken getAuthentication(JSONObject payloads) { + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID +// userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + +} diff --git a/src/main/java/com/ichangzuo/core/security/util/SecurityUtils.java b/src/main/java/com/ichangzuo/core/security/util/SecurityUtils.java new file mode 100644 index 0000000..8c82368 --- /dev/null +++ b/src/main/java/com/ichangzuo/core/security/util/SecurityUtils.java @@ -0,0 +1,109 @@ +package com.ichangzuo.core.security.util; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.ichangzuo.common.constant.SystemConstants; +import com.ichangzuo.core.security.model.SysUserDetails; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Spring Security 工具类 + * + * @author Ray + * @since 2021/1/10 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ +// public static Long getDeptId() { +// return getUser().map(SysUserDetails::getDeptId).orElse(null); +// } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取用户角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Collection authorities = authentication.getAuthorities(); + if (CollectionUtil.isNotEmpty(authorities)) { + return authorities.stream().filter(item -> item.getAuthority().startsWith("ROLE_")) + .map(item -> StrUtil.removePrefix(item.getAuthority(), "ROLE_")) + .collect(Collectors.toSet()); + } + } + return Collections.EMPTY_SET; + } + + /** + * 是否超级管理员 + *

+ * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + +} diff --git a/src/main/java/com/ichangzuo/iczApplication.java b/src/main/java/com/ichangzuo/iczApplication.java new file mode 100644 index 0000000..468c3ed --- /dev/null +++ b/src/main/java/com/ichangzuo/iczApplication.java @@ -0,0 +1,24 @@ +package com.ichangzuo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * 应用启动类 + * + * @author Ray + * @since 0.0.1 + */ +@SpringBootApplication +@ConfigurationPropertiesScan +@EnableScheduling +public class iczApplication { + public static void main(String[] args) { + SpringApplication.run(iczApplication.class, args); + } +} diff --git a/src/main/java/com/ichangzuo/module/auth/controller/AuthController.java b/src/main/java/com/ichangzuo/module/auth/controller/AuthController.java new file mode 100644 index 0000000..e37299a --- /dev/null +++ b/src/main/java/com/ichangzuo/module/auth/controller/AuthController.java @@ -0,0 +1,93 @@ +package com.ichangzuo.module.auth.controller; + +import com.ichangzuo.module.auth.service.AuthService; +import com.ichangzuo.common.enums.LogModuleEnum; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.system.model.dto.CaptchaResult; +import com.ichangzuo.system.model.dto.LoginResult; +import com.ichangzuo.common.annotation.Log; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +/** + * 认证控制层 + * + * @author Ray + * @since 2022/10/16 + */ +@Tag(name = "01.认证中心") +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +@Slf4j +public class AuthController { + + private final AuthService authService; + + @Operation(summary = "登录") + @PostMapping("/login") + @Log(value = "登录", module = LogModuleEnum.LOGIN) + public Result login( + @Parameter(description = "用户名", example = "admin") @RequestParam String username, + @Parameter(description = "密码", example = "123456") @RequestParam String password + ) { + LoginResult loginResult = authService.login(username, password); + return Result.success(loginResult); + } + + @Operation(summary = "注销") + @DeleteMapping("/logout") + @Log(value = "注销", module = LogModuleEnum.LOGIN) + public Result logout() { + authService.logout(); + return Result.success(); + } + + @Operation(summary = "获取验证码") + @GetMapping("/captcha") + public Result getCaptcha() { + CaptchaResult captcha = authService.getCaptcha(); + return Result.success(captcha); + } + + @Operation(summary = "注册") + @PostMapping("/register") + public Result register( + @Parameter(description = "用户名", example = "admin") @RequestParam String username, + @Parameter(description = "密码", example = "123456") @RequestParam String password, + @Parameter(description = "验证码", example = "123456") @RequestParam String verifyCode + ) { + return authService.register(username,password,verifyCode); + } + + @Operation(summary = "发送注册验证码") + @PostMapping("/sendRegCode") + public Result sendRegCode( + @Parameter(description = "联系方式(手机号码或邮箱地址)", required = true) @RequestParam String contact + ) { + return authService.sendRegCode(contact); + } + + @Operation(summary = "核验注册验证码") + @PostMapping("/verifyRegCode") + public Result verifyRegCode( + @Parameter(description = "用户名", example = "admin") @RequestParam String username, + @Parameter(description = "验证码", example = "123456") @RequestParam String verifyCode + ) { + return authService.verifyRegCode(username, verifyCode); + } + + @Operation(summary = "自动登录") + @PostMapping("/autoLogin") + @Log(value = "自动登录", module = LogModuleEnum.LOGIN) + public Result autoLogin( + @Parameter(description = "用户名", example = "admin") @RequestParam String username, + @Parameter(description = "密码", example = "123456") @RequestParam String password + ){ + return authService.autoLogin(username, password); + } +} diff --git a/src/main/java/com/ichangzuo/module/auth/service/AuthService.java b/src/main/java/com/ichangzuo/module/auth/service/AuthService.java new file mode 100644 index 0000000..108a7b7 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/auth/service/AuthService.java @@ -0,0 +1,44 @@ +package com.ichangzuo.module.auth.service; + +import com.ichangzuo.common.enums.ContactType; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.system.model.dto.CaptchaResult; +import com.ichangzuo.system.model.dto.LoginResult; + +/** + * 认证服务接口 + * + * @author haoxr + * @since 2.4.0 + */ +public interface AuthService { + + /** + * 登录 + * + * @param username 用户名 + * @param password 密码 + * @return 登录结果 + */ + LoginResult login(String username, String password); + + /** + * 登出 + */ + void logout(); + + /** + * 获取验证码 + * + * @return 验证码 + */ + CaptchaResult getCaptcha(); + + Result register(String username, String password, String verifyCode); + + Result sendRegCode(String contact); + + Result verifyRegCode(String username, String verifyCode); + + Result autoLogin(String username, String password); +} diff --git a/src/main/java/com/ichangzuo/module/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/ichangzuo/module/auth/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..9babee7 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/auth/service/impl/AuthServiceImpl.java @@ -0,0 +1,218 @@ +package com.ichangzuo.module.auth.service.impl; + +import cn.hutool.captcha.AbstractCaptcha; +import cn.hutool.captcha.CaptchaUtil; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.common.result.ResultCode; +import com.ichangzuo.module.auth.service.AuthService; +import com.ichangzuo.common.constant.SecurityConstants; +import com.ichangzuo.common.enums.CaptchaTypeEnum; +import com.ichangzuo.system.model.dto.CaptchaResult; +import com.ichangzuo.system.model.dto.LoginResult; +import com.ichangzuo.config.property.CaptchaProperties; +import com.ichangzuo.core.security.util.JwtUtils; +import com.ichangzuo.system.service.UserService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.awt.*; +import java.util.concurrent.TimeUnit; + +/** + * 认证服务实现类 + * + * @author haoxr + * @since 2.4.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class AuthServiceImpl implements AuthService { + + private final AuthenticationManager authenticationManager; + private final RedisTemplate redisTemplate; + private final CodeGenerator codeGenerator; + private final Font captchaFont; + private final CaptchaProperties captchaProperties; + private final UserService userService; + + /** + * 登录 + * + * @param username 用户名 + * @param password 密码 + * @return 登录结果 + */ + @Override + public LoginResult login(String username, String password) { + // 创建认证令牌对象 + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(username.toLowerCase().trim(), password); + // 执行用户认证 + Authentication authentication = authenticationManager.authenticate(authenticationToken); + // 认证成功后生成JWT令牌 + String accessToken = JwtUtils.createToken(authentication); + // 将认证信息存入Security上下文,便于在AOP(如日志记录)中获取当前用户信息 + SecurityContextHolder.getContext().setAuthentication(authentication); + + userService.afterUserLogon(); + // 返回包含JWT令牌的登录结果 + return LoginResult.builder() + .tokenType("Bearer") + .accessToken(accessToken) + .build(); + } + + /** + * 注销 + */ + @Override + public void logout() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); + // 解析Token以获取有效载荷(payload) + JSONObject payloads = JWTUtil.parseToken(token).getPayloads(); + // 解析 Token 获取 jti(JWT ID) 和 exp(过期时间) + String jti = payloads.getStr(JWTPayload.JWT_ID); + Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); // 过期时间(秒) + // 如果exp存在,则计算Token剩余有效时间 + if (expiration != null) { + long currentTimeSeconds = System.currentTimeMillis() / 1000; + if (expiration < currentTimeSeconds) { + // Token已过期,不再加入黑名单 + return; + } + // 将Token的jti加入黑名单,并设置剩余有效时间,使其在过期后自动从黑名单移除 + long ttl = expiration - currentTimeSeconds; + redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS); + } else { + // 如果exp不存在,说明Token永不过期,则永久加入黑名单 + redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null); + } + } + // 清空Spring Security上下文 + SecurityContextHolder.clearContext(); + } + + /** + * 获取验证码 + * + * @return 验证码 + */ + @Override + public CaptchaResult getCaptcha() { + + String captchaType = captchaProperties.getType(); + int width = captchaProperties.getWidth(); + int height = captchaProperties.getHeight(); + int interfereCount = captchaProperties.getInterfereCount(); + int codeLength = captchaProperties.getCode().getLength(); + String codeType = captchaProperties.getCode().getType(); + + AbstractCaptcha captcha; + if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) { + captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount); + } else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) { + captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength); + } else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) { + captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount); + } else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) { + captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount); + } else { + throw new IllegalArgumentException("Invalid captcha type: " + captchaType); + } + + captcha.setGenerator(codeGenerator); + captcha.setTextAlpha(captchaProperties.getTextAlpha()); + captcha.setFont(captchaFont); + + String captchaCode = captcha.getCode(); + String imageBase64Data = captcha.getImageBase64Data(); + + // 验证码文本缓存至Redis,用于登录校验 + String captchaKey = IdUtil.fastSimpleUUID(); + redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode, + captchaProperties.getExpireSeconds(), TimeUnit.SECONDS); + + return CaptchaResult.builder() + .captchaKey(captchaKey) + .captchaBase64(imageBase64Data) + .build(); + } + + @Override + public Result register(String username, String password, String verifyCode){ + if(userService.existUsername(username)) + return Result.failed(ResultCode.USER_NAME_EXISTED); + + int verify = userService.checkVerificationCode(username, verifyCode, "注册新用户"); + if(verify==-1) + return Result.failed(ResultCode.VERIFY_CODE_ERROR); + else if(verify==-2) + return Result.failed(ResultCode.VERIFY_CODE_TIMEOUT); + else if(verify==-3) + return Result.failed(ResultCode.PARAM_ERROR); + + if( !userService.register(username, password) ) + return Result.failed(ResultCode.SYSTEM_EXECUTION_ERROR); + + LoginResult result = login(username, password); + if(result==null) + return Result.failed(ResultCode.SYSTEM_EXECUTION_ERROR); + + userService.setNewAccount(); + + return Result.success(result); + } + + @Override + public Result sendRegCode(String contact){ + if(userService.existUsername(contact)) + return Result.failed(ResultCode.USER_NAME_EXISTED); + + String subject = "注册新用户"; + boolean result = userService.sendVerificationCode(contact, subject); + return Result.judge(result); + } + + @Override + public Result verifyRegCode(String username, String verifyCode) { + int verify = userService.checkVerificationCode(username, verifyCode, "注册新用户"); + if(verify==-1) + return Result.failed(ResultCode.VERIFY_CODE_ERROR); + else if(verify==-2) + return Result.failed(ResultCode.VERIFY_CODE_TIMEOUT); + else if(verify==-3) + return Result.failed(ResultCode.PARAM_ERROR); + + return Result.success(); + } + + @Override + public Result autoLogin(String username, String password){ + LoginResult result = login(username, password); + if(result==null) + return Result.failed(ResultCode.SYSTEM_EXECUTION_ERROR); + + return Result.success(result); + } +} diff --git a/src/main/java/com/ichangzuo/module/codegen/controller/CodegenController.java b/src/main/java/com/ichangzuo/module/codegen/controller/CodegenController.java new file mode 100644 index 0000000..cb38e92 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/controller/CodegenController.java @@ -0,0 +1,109 @@ +//package com.ichangzuo.module.codegen.controller; +// +//import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +//import com.ichangzuo.module.codegen.service.CodegenService; +//import com.ichangzuo.common.result.PageResult; +//import com.ichangzuo.common.result.Result; +//import com.ichangzuo.config.property.CodegenProperties; +//import com.ichangzuo.common.enums.LogModuleEnum; +//import com.ichangzuo.module.codegen.model.form.GenConfigForm; +//import com.ichangzuo.module.codegen.model.query.TablePageQuery; +//import com.ichangzuo.module.codegen.model.vo.CodegenPreviewVO; +//import com.ichangzuo.module.codegen.model.vo.TablePageVO; +//import com.ichangzuo.common.annotation.Log; +//import com.ichangzuo.module.codegen.service.GenConfigService; +//import io.swagger.v3.oas.annotations.Operation; +//import io.swagger.v3.oas.annotations.Parameter; +//import io.swagger.v3.oas.annotations.tags.Tag; +//import jakarta.servlet.ServletOutputStream; +//import jakarta.servlet.http.HttpServletResponse; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.web.bind.annotation.*; +// +//import java.io.IOException; +//import java.net.URLEncoder; +//import java.nio.charset.StandardCharsets; +//import java.util.List; +// +///** +// * 代码生成器控制层 +// * +// * @author Ray +// * @since 2.10.0 +// */ +//@Tag(name = "09.代码生成") +//@RestController +//@RequestMapping("/api/v1/codegen") +//@RequiredArgsConstructor +//@Slf4j +//public class CodegenController { +// +// private final CodegenService codegenService; +// private final GenConfigService genConfigService; +// private final CodegenProperties codegenProperties; +// +// @Operation(summary = "获取数据表分页列表") +// @GetMapping("/table/page") +// @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) +// public PageResult getTablePage( +// TablePageQuery queryParams +// ) { +// Page result = codegenService.getTablePage(queryParams); +// return PageResult.success(result); +// } +// +// @Operation(summary = "获取代码生成配置") +// @GetMapping("/{tableName}/config") +// public Result getGenConfigFormData( +// @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName +// ) { +// GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); +// return Result.success(formData); +// } +// +// @Operation(summary = "保存代码生成配置") +// @PostMapping("/{tableName}/config") +// @Log(value = "生成代码", module = LogModuleEnum.OTHER) +// public Result saveGenConfig(@RequestBody GenConfigForm formData) { +// genConfigService.saveGenConfig(formData); +// return Result.success(); +// } +// +// @Operation(summary = "删除代码生成配置") +// @DeleteMapping("/{tableName}/config") +// public Result deleteGenConfig( +// @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName +// ) { +// genConfigService.deleteGenConfig(tableName); +// return Result.success(); +// } +// +// @Operation(summary = "获取预览生成代码") +// @GetMapping("/{tableName}/preview") +// @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) +// public Result> getTablePreviewData(@PathVariable String tableName) { +// List list = codegenService.getCodegenPreviewData(tableName); +// return Result.success(list); +// } +// +// @Operation(summary = "下载代码") +// @GetMapping("/{tableName}/download") +// @Log(value = "下载代码", module = LogModuleEnum.OTHER) +// public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { +// String[] tableNames = tableName.split(","); +// byte[] data = codegenService.downloadCode(tableNames); +// +// response.reset(); +// response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); +// response.setContentType("application/octet-stream; charset=UTF-8"); +// +// try (ServletOutputStream outputStream = response.getOutputStream()) { +// outputStream.write(data); +// outputStream.flush(); +// } catch (IOException e) { +// log.error("Error while writing the zip file to response", e); +// throw new RuntimeException("Failed to write the zip file to response", e); +// } +// } +//} diff --git a/src/main/java/com/ichangzuo/module/codegen/converter/CodegenConverter.java b/src/main/java/com/ichangzuo/module/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000..3a0e3ea --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package com.ichangzuo.module.codegen.converter; + +import com.ichangzuo.module.codegen.model.entity.GenConfig; +import com.ichangzuo.module.codegen.model.entity.GenFieldConfig; +import com.ichangzuo.module.codegen.model.form.GenConfigForm; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.List; + +/** + * 代码生成配置转换器 + * + * @author Ray + * @since 2.10.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/module/codegen/mapper/DatabaseMapper.java b/src/main/java/com/ichangzuo/module/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000..e3cc1ed --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,23 @@ +package com.ichangzuo.module.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ichangzuo.module.codegen.model.query.TablePageQuery; +import com.ichangzuo.module.codegen.model.bo.ColumnMetaData; +import com.ichangzuo.module.codegen.model.bo.TableMetaData; +import com.ichangzuo.module.codegen.model.vo.TablePageVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + + +@Mapper +public interface DatabaseMapper extends BaseMapper { + + + Page getTablePage(Page page, TablePageQuery queryParams); + + List getTableColumns(String tableName); + + TableMetaData getTableMetadata(String tableName); +} diff --git a/src/main/java/com/ichangzuo/module/codegen/mapper/GenConfigMapper.java b/src/main/java/com/ichangzuo/module/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000..7255114 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,20 @@ +package com.ichangzuo.module.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ichangzuo.module.codegen.model.entity.GenConfig; +import org.apache.ibatis.annotations.Mapper; + +/** + * 代码生成基础配置访问层 + * + * @author Ray + * @since 2.10.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/ichangzuo/module/codegen/mapper/GenFieldConfigMapper.java b/src/main/java/com/ichangzuo/module/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000..9614004 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,20 @@ +package com.ichangzuo.module.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ichangzuo.module.codegen.model.entity.GenFieldConfig; +import org.apache.ibatis.annotations.Mapper; + +/** + * 代码生成字段配置访问层 + * + * @author Ray + * @since 2.10.0 + */ +@Mapper +public interface GenFieldConfigMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/ichangzuo/module/codegen/model/bo/ColumnMetaData.java b/src/main/java/com/ichangzuo/module/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000..e2998ae --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,50 @@ +package com.ichangzuo.module.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Integer characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} diff --git a/src/main/java/com/ichangzuo/module/codegen/model/bo/TableMetaData.java b/src/main/java/com/ichangzuo/module/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000..b52b9a0 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/model/bo/TableMetaData.java @@ -0,0 +1,45 @@ +package com.ichangzuo.module.codegen.model.bo; + +import lombok.Data; + + +/** + * 数据表元数据 + * + * @author Ray + * @since 2.10.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/src/main/java/com/ichangzuo/module/codegen/model/entity/GenConfig.java b/src/main/java/com/ichangzuo/module/codegen/model/entity/GenConfig.java new file mode 100644 index 0000000..efe15d2 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/model/entity/GenConfig.java @@ -0,0 +1,54 @@ +package com.ichangzuo.module.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.*; + +import com.ichangzuo.common.base.BaseEntity; +import lombok.Getter; +import lombok.Setter; + +/** + * 代码生成基础配置 + * + * @author Ray + * @since 2.10.0 + */ +@TableName(value = "gen_config") +@Getter +@Setter +public class GenConfig extends BaseEntity { + + /** + * 表名 + */ + private String tableName; + + /** + * 包名 + */ + private String packageName; + + /** + * 模块名 + */ + private String moduleName; + + /** + * 实体类名 + */ + private String entityName; + + /** + * 业务名 + */ + private String businessName; + + /** + * 父菜单ID + */ + private Long parentMenuId; + + /** + * 作者 + */ + private String author; +} \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/module/codegen/model/entity/GenFieldConfig.java b/src/main/java/com/ichangzuo/module/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000..21b5a04 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,106 @@ +package com.ichangzuo.module.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.ichangzuo.common.base.BaseEntity; +import com.ichangzuo.common.enums.FormTypeEnum; +import com.ichangzuo.common.enums.QueryTypeEnum; +import lombok.Getter; +import lombok.Setter; + +/** + * 字段生成配置实体 + * + * @author Ray + * @since 2.10.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Integer maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} \ No newline at end of file diff --git a/src/main/java/com/ichangzuo/module/codegen/model/form/GenConfigForm.java b/src/main/java/com/ichangzuo/module/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000..13b3544 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/model/form/GenConfigForm.java @@ -0,0 +1,103 @@ +package com.ichangzuo.module.codegen.model.form; + +import com.ichangzuo.common.enums.FormTypeEnum; +import com.ichangzuo.common.enums.QueryTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 代码生成配置表单 + * + * @author Ray + * @since 2.10.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} diff --git a/src/main/java/com/ichangzuo/module/codegen/model/query/TablePageQuery.java b/src/main/java/com/ichangzuo/module/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000..aac7e3a --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package com.ichangzuo.module.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.ichangzuo.common.base.BasePageQuery; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 数据表分页查询对象 + * + * @author Ray + * @since 2.10.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} diff --git a/src/main/java/com/ichangzuo/module/codegen/model/vo/CodegenPreviewVO.java b/src/main/java/com/ichangzuo/module/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000..354b1b0 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,19 @@ +package com.ichangzuo.module.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/src/main/java/com/ichangzuo/module/codegen/model/vo/TablePageVO.java b/src/main/java/com/ichangzuo/module/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000..cc1b6ee --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/model/vo/TablePageVO.java @@ -0,0 +1,32 @@ +package com.ichangzuo.module.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + + +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/src/main/java/com/ichangzuo/module/codegen/service/CodegenService.java b/src/main/java/com/ichangzuo/module/codegen/service/CodegenService.java new file mode 100644 index 0000000..4ae3d92 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/service/CodegenService.java @@ -0,0 +1,40 @@ +package com.ichangzuo.module.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ichangzuo.module.codegen.model.query.TablePageQuery; +import com.ichangzuo.module.codegen.model.vo.CodegenPreviewVO; +import com.ichangzuo.module.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * 代码生成配置接口 + * + * @author Ray + * @since 2.10.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} diff --git a/src/main/java/com/ichangzuo/module/codegen/service/GenConfigService.java b/src/main/java/com/ichangzuo/module/codegen/service/GenConfigService.java new file mode 100644 index 0000000..26654a5 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package com.ichangzuo.module.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ichangzuo.module.codegen.model.entity.GenConfig; +import com.ichangzuo.module.codegen.model.form.GenConfigForm; + +/** + * 代码生成配置接口 + * + * @author Ray + * @since 2.10.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} diff --git a/src/main/java/com/ichangzuo/module/codegen/service/GenFieldConfigService.java b/src/main/java/com/ichangzuo/module/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000..d66b2d0 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package com.ichangzuo.module.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ichangzuo.module.codegen.model.entity.GenFieldConfig; + +/** + * 代码生成配置接口 + * + * @author Ray + * @since 2.10.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/src/main/java/com/ichangzuo/module/codegen/service/impl/CodegenServiceImpl.java b/src/main/java/com/ichangzuo/module/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000..ef5765d --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,313 @@ +//package com.ichangzuo.module.codegen.service.impl; +// +//import cn.hutool.core.collection.CollectionUtil; +//import cn.hutool.core.date.DateUtil; +//import cn.hutool.core.util.ObjectUtil; +//import cn.hutool.core.util.StrUtil; +//import cn.hutool.extra.template.Template; +//import cn.hutool.extra.template.TemplateConfig; +//import cn.hutool.extra.template.TemplateEngine; +//import cn.hutool.extra.template.TemplateUtil; +//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +//import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +//import com.ichangzuo.module.codegen.service.CodegenService; +//import com.ichangzuo.common.enums.JavaTypeEnum; +//import com.ichangzuo.config.property.CodegenProperties; +//import com.ichangzuo.module.codegen.service.GenConfigService; +//import com.ichangzuo.module.codegen.service.GenFieldConfigService; +//import com.ichangzuo.common.exception.BusinessException; +//import com.ichangzuo.module.codegen.mapper.DatabaseMapper; +//import com.ichangzuo.module.codegen.model.entity.GenConfig; +//import com.ichangzuo.module.codegen.model.entity.GenFieldConfig; +//import com.ichangzuo.module.codegen.model.query.TablePageQuery; +//import com.ichangzuo.module.codegen.model.vo.CodegenPreviewVO; +//import com.ichangzuo.module.codegen.model.vo.TablePageVO; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.stereotype.Service; +// +//import java.io.ByteArrayOutputStream; +//import java.io.File; +//import java.io.IOException; +//import java.nio.charset.StandardCharsets; +//import java.util.*; +//import java.util.zip.ZipEntry; +//import java.util.zip.ZipOutputStream; +// +///** +// * 数据库服务实现类 +// * +// * @author Ray +// * @since 2.10.0 +// */ +//@Service +//@RequiredArgsConstructor +//@Slf4j +//public class CodegenServiceImpl implements CodegenService { +// +// private final DatabaseMapper databaseMapper; +// private final CodegenProperties codegenProperties; +// private final GenConfigService genConfigService; +// private final GenFieldConfigService genFieldConfigService; +// +// /** +// * 数据表分页列表 +// * +// * @param queryParams 查询参数 +// * @return 分页结果 +// */ +// public Page getTablePage(TablePageQuery queryParams) { +// Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); +// // 设置排除的表 +// List excludeTables = codegenProperties.getExcludeTables(); +// queryParams.setExcludeTables(excludeTables); +// +// return databaseMapper.getTablePage(page, queryParams); +// } +// +// /** +// * 获取预览生成代码 +// * +// * @param tableName 表名 +// * @return 预览数据 +// */ +// @Override +// public List getCodegenPreviewData(String tableName) { +// +// List list = new ArrayList<>(); +// +// GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() +// .eq(GenConfig::getTableName, tableName) +// ); +// if (genConfig == null) { +// throw new BusinessException("未找到表生成配置"); +// } +// +// List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() +// .eq(GenFieldConfig::getConfigId, genConfig.getId()) +// .orderByAsc(GenFieldConfig::getFieldSort) +// +// ); +// if (CollectionUtil.isEmpty(fieldConfigs)) { +// throw new BusinessException("未找到字段生成配置"); +// } +// +// // 遍历模板配置 +// Map templateConfigs = codegenProperties.getTemplateConfigs(); +// for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { +// CodegenPreviewVO previewVO = new CodegenPreviewVO(); +// +// CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); +// +// /* 1. 生成文件名 UserController */ +// // User Role Menu Dept +// String entityName = genConfig.getEntityName(); +// // Controller Service Mapper Entity +// String templateName = templateConfigEntry.getKey(); +// // .java .ts .vue +// String extension = templateConfig.getExtension(); +// +// // 文件名 UserController.java +// String fileName = getFileName(entityName, templateName, extension); +// previewVO.setFileName(fileName); +// +// /* 2. 生成文件路径 */ +// // 包名:com.ichangzuo +// String packageName = genConfig.getPackageName(); +// // 模块名:system +// String moduleName = genConfig.getModuleName(); +// // 子包名:controller +// String subpackageName = templateConfig.getSubpackageName(); +// // 组合成文件路径:src/main/java/com/youlai/boot/system/controller +// String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); +// previewVO.setPath(filePath); +// +// /* 3. 生成文件内容 */ +// // 将模板文件中的变量替换为具体的值 生成代码内容 +// String content = getCodeContent(templateConfig, genConfig, fieldConfigs); +// previewVO.setContent(content); +// +// list.add(previewVO); +// } +// return list; +// } +// +// /** +// * 生成文件名 +// * +// * @param entityName 实体类名 UserController +// * @param templateName 模板名 Entity +// * @param extension 文件后缀 .java +// * @return 文件名 +// */ +// private String getFileName(String entityName, String templateName, String extension) { +// if ("Entity".equals(templateName)) { +// return entityName + extension; +// } else if ("MapperXml".equals(templateName)) { +// return entityName + "Mapper" + extension; +// } else if ("API".equals(templateName)) { +// return StrUtil.toSymbolCase(entityName, '-') + extension; +// } else if ("VIEW".equals(templateName)) { +// return "index.vue"; +// } +// return entityName + templateName + extension; +// } +// +// /** +// * 生成文件路径 +// * +// * @param templateName 模板名 Entity +// * @param moduleName 模块名 system +// * @param packageName 包名 com.youlai +// * @param subPackageName 子包名 controller +// * @param entityName 实体类名 UserController +// * @return 文件路径 src/main/java/com/youlai/system/controller +// */ +// private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { +// String path; +// if ("MapperXml".equals(templateName)) { +// path = (codegenProperties.getBackendAppName() +// + File.separator +// + "src" + File.separator + "main" + File.separator + "resources" +// + File.separator + subPackageName +// ); +// } else if ("API".equals(templateName)) { +// path = (codegenProperties.getFrontendAppName() +// + File.separator +// + "src" + File.separator + subPackageName +// ); +// } else if ("VIEW".equals(templateName)) { +// path = (codegenProperties.getFrontendAppName() +// + File.separator + "src" +// + File.separator + subPackageName +// + File.separator + moduleName +// + File.separator + StrUtil.toSymbolCase(entityName, '-') +// ); +// } else { +// path = (codegenProperties.getBackendAppName() +// + File.separator +// + "src" + File.separator + "main" + File.separator + "java" +// + File.separator + packageName +// + File.separator + moduleName +// + File.separator + subPackageName +// ); +// } +// +// // subPackageName = model.entity => model/entity +// path = path.replace(".", File.separator); +// +// return path; +// } +// +// /** +// * 生成代码内容 +// * +// * @param templateConfig 模板配置 +// * @param genConfig 生成配置 +// * @param fieldConfigs 字段配置 +// * @return 代码内容 +// */ +// private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { +// +// Map bindMap = new HashMap<>(); +// +// String entityName = genConfig.getEntityName(); +// +// bindMap.put("packageName", genConfig.getPackageName()); +// bindMap.put("moduleName", genConfig.getModuleName()); +// bindMap.put("subpackageName", templateConfig.getSubpackageName()); +// bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); +// bindMap.put("entityName", entityName); +// bindMap.put("tableName", genConfig.getTableName()); +// bindMap.put("author", genConfig.getAuthor()); +// bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest +// bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-test +// bindMap.put("businessName", genConfig.getBusinessName()); +// bindMap.put("fieldConfigs", fieldConfigs); +// +// boolean hasLocalDateTime = false; +// boolean hasBigDecimal = false; +// boolean hasRequiredField = false; +// +// for (GenFieldConfig fieldConfig : fieldConfigs) { +// +// if ("LocalDateTime".equals(fieldConfig.getFieldType())) { +// hasLocalDateTime = true; +// } +// if ("BigDecimal".equals(fieldConfig.getFieldType())) { +// hasBigDecimal = true; +// } +// if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { +// hasRequiredField = true; +// } +// fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); +// +// +// } +// +// bindMap.put("hasLocalDateTime", hasLocalDateTime); +// bindMap.put("hasBigDecimal", hasBigDecimal); +// bindMap.put("hasRequiredField", hasRequiredField); +// +// TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); +// Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); +// +// return template.render(bindMap); +// } +// +// /** +// * 下载代码 +// * +// * @param tableNames 表名数组,支持多张表。 +// * @return 压缩文件字节数组 +// */ +// @Override +// public byte[] downloadCode(String[] tableNames) { +// try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); +// ZipOutputStream zip = new ZipOutputStream(outputStream)) { +// +// // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 +// for (String tableName : tableNames) { +// generateAndZipCode(tableName, zip); +// } +// +// return outputStream.toByteArray(); +// +// } catch (IOException e) { +// log.error("Error while generating zip for code download", e); +// throw new RuntimeException("Failed to generate code zip file", e); +// } +// } +// +// /** +// * 根据表名生成代码并压缩到zip文件中 +// * +// * @param tableName 表名 +// * @param zip 压缩文件输出流 +// */ +// private void generateAndZipCode(String tableName, ZipOutputStream zip) { +// List codePreviewList = getCodegenPreviewData(tableName); +// +// for (CodegenPreviewVO codePreview : codePreviewList) { +// String fileName = codePreview.getFileName(); +// String content = codePreview.getContent(); +// String path = codePreview.getPath(); +// +// try { +// // 创建压缩条目 +// ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); +// zip.putNextEntry(zipEntry); +// +// // 写入文件内容 +// zip.write(content.getBytes(StandardCharsets.UTF_8)); +// +// // 关闭当前压缩条目 +// zip.closeEntry(); +// +// } catch (IOException e) { +// log.error("Error while adding file {} to zip", fileName, e); +// } +// } +// } +// +//} diff --git a/src/main/java/com/ichangzuo/module/codegen/service/impl/GenConfigServiceImpl.java b/src/main/java/com/ichangzuo/module/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000..e52394d --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,221 @@ +//package com.ichangzuo.module.codegen.service.impl; +// +//import cn.hutool.core.collection.CollectionUtil; +//import cn.hutool.core.lang.Assert; +//import cn.hutool.core.util.StrUtil; +//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +//import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +//import com.ichangzuo.module.codegen.converter.CodegenConverter; +//import com.ichangzuo.iczApplication; +//import com.ichangzuo.common.enums.EnvEnum; +//import com.ichangzuo.common.enums.FormTypeEnum; +//import com.ichangzuo.common.enums.JavaTypeEnum; +//import com.ichangzuo.common.enums.QueryTypeEnum; +//import com.ichangzuo.common.exception.BusinessException; +//import com.ichangzuo.config.property.CodegenProperties; +//import com.ichangzuo.module.codegen.mapper.DatabaseMapper; +//import com.ichangzuo.module.codegen.mapper.GenConfigMapper; +//import com.ichangzuo.module.codegen.model.bo.ColumnMetaData; +//import com.ichangzuo.module.codegen.model.bo.TableMetaData; +//import com.ichangzuo.module.codegen.model.entity.GenConfig; +//import com.ichangzuo.module.codegen.model.entity.GenFieldConfig; +//import com.ichangzuo.module.codegen.model.form.GenConfigForm; +//import com.ichangzuo.module.codegen.service.GenConfigService; +//import com.ichangzuo.module.codegen.service.GenFieldConfigService; +//import com.ichangzuo.system.service.MenuService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.stereotype.Service; +// +//import java.util.ArrayList; +//import java.util.Comparator; +//import java.util.List; +//import java.util.Objects; +// +///** +// * 数据库服务实现类 +// * +// * @author Ray +// * @since 2.10.0 +// */ +//@Service +//@RequiredArgsConstructor +//public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { +// +// private final DatabaseMapper databaseMapper; +// private final CodegenProperties codegenProperties; +// private final GenFieldConfigService genFieldConfigService; +// private final CodegenConverter codegenConverter; +// +// @Value("${spring.profiles.active}") +// private String springProfilesActive; +// +// private final MenuService menuService; +// +// /** +// * 获取代码生成配置 +// * +// * @param tableName 表名 eg: sys_user +// * @return 代码生成配置 +// */ +// @Override +// public GenConfigForm getGenConfigFormData(String tableName) { +// // 查询表生成配置 +// GenConfig genConfig = this.getOne( +// new LambdaQueryWrapper<>(GenConfig.class) +// .eq(GenConfig::getTableName, tableName) +// .last("LIMIT 1") +// ); +// +// // 是否有代码生成配置 +// boolean hasGenConfig = genConfig != null; +// +// // 如果没有代码生成配置,则根据表的元数据生成默认配置 +// if (genConfig == null) { +// TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); +// Assert.isTrue(tableMetadata != null, "未找到表元数据"); +// +// genConfig = new GenConfig(); +// genConfig.setTableName(tableName); +// +// String tableComment = tableMetadata.getTableComment(); +// if (StrUtil.isNotBlank(tableComment)) { +// genConfig.setBusinessName(tableComment.replace("表", "")); +// } +// // 实体类名 = 表名去掉前缀后转驼峰,前缀默认为下划线分割的第一个元素 +// String entityName = StrUtil.toCamelCase(StrUtil.removePrefix(tableName, tableName.split("_")[0])); +// genConfig.setEntityName(entityName); +// +// genConfig.setPackageName(iczApplication.class.getPackageName()); +// genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 +// genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); +// } +// +// // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 +// List genFieldConfigs = new ArrayList<>(); +// +// // 获取表的列 +// List tableColumns = databaseMapper.getTableColumns(tableName); +// if (CollectionUtil.isNotEmpty(tableColumns)) { +// // 查询字段生成配置 +// List fieldConfigList = genFieldConfigService.list( +// new LambdaQueryWrapper() +// .eq(GenFieldConfig::getConfigId, genConfig.getId()) +// .orderByAsc(GenFieldConfig::getFieldSort) +// ); +// Integer maxSort = fieldConfigList.stream() +// .map(GenFieldConfig::getFieldSort) +// .filter(Objects::nonNull) // 过滤掉空值 +// .max(Integer::compareTo) +// .orElse(0); +// for (ColumnMetaData tableColumn : tableColumns) { +// // 根据列名获取字段生成配置 +// String columnName = tableColumn.getColumnName(); +// GenFieldConfig fieldConfig = fieldConfigList.stream() +// .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) +// .findFirst() +// .orElseGet(() -> createDefaultFieldConfig(tableColumn)); +// if (fieldConfig.getFieldSort() == null) { +// fieldConfig.setFieldSort(++maxSort); +// } +// // 根据列类型设置字段类型 +// String fieldType = fieldConfig.getFieldType(); +// if (StrUtil.isBlank(fieldType)) { +// String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); +// fieldConfig.setFieldType(javaType); +// } +// // 如果没有代码生成配置,则默认展示在列表和表单 +// if (!hasGenConfig) { +// fieldConfig.setIsShowInList(1); +// fieldConfig.setIsShowInForm(1); +// } +// genFieldConfigs.add(fieldConfig); +// } +// } +// //对genFieldConfigs按照fieldSort排序 +// genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); +// GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); +// +// genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); +// genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); +// return genConfigForm; +// } +// +// +// /** +// * 创建默认字段配置 +// * +// * @param columnMetaData 表字段元数据 +// * @return +// */ +// private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { +// GenFieldConfig fieldConfig = new GenFieldConfig(); +// fieldConfig.setColumnName(columnMetaData.getColumnName()); +// fieldConfig.setColumnType(columnMetaData.getDataType()); +// fieldConfig.setFieldComment(columnMetaData.getColumnComment()); +// fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); +// fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 1 : 0); +// +// if (fieldConfig.getColumnType().equals("date")) { +// fieldConfig.setFormType(FormTypeEnum.DATE); +// } else if (fieldConfig.getColumnType().equals("datetime")) { +// fieldConfig.setFormType(FormTypeEnum.DATE_TIME); +// } else { +// fieldConfig.setFormType(FormTypeEnum.INPUT); +// } +// +// fieldConfig.setQueryType(QueryTypeEnum.EQ); +// fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); +// return fieldConfig; +// } +// +// /** +// * 保存代码生成配置 +// * +// * @param formData 代码生成配置表单 +// */ +// @Override +// public void saveGenConfig(GenConfigForm formData) { +// GenConfig genConfig = codegenConverter.toGenConfig(formData); +// this.saveOrUpdate(genConfig); +// +// // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 +// Long parentMenuId = formData.getParentMenuId(); +// if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { +// menuService.addMenuForCodegen(parentMenuId, genConfig); +// } +// +// List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); +// +// if (CollectionUtil.isEmpty(genFieldConfigs)) { +// throw new BusinessException("字段配置不能为空"); +// } +// genFieldConfigs.forEach(genFieldConfig -> { +// genFieldConfig.setConfigId(genConfig.getId()); +// }); +// genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); +// } +// +// /** +// * 删除代码生成配置 +// * +// * @param tableName 表名 +// */ +// @Override +// public void deleteGenConfig(String tableName) { +// GenConfig genConfig = this.getOne(new LambdaQueryWrapper() +// .eq(GenConfig::getTableName, tableName)); +// +// boolean result = this.remove(new LambdaQueryWrapper() +// .eq(GenConfig::getTableName, tableName) +// ); +// if (result) { +// genFieldConfigService.remove(new LambdaQueryWrapper() +// .eq(GenFieldConfig::getConfigId, genConfig.getId()) +// ); +// } +// } +// +// +// +//} diff --git a/src/main/java/com/ichangzuo/module/codegen/service/impl/GenFieldConfigServiceImpl.java b/src/main/java/com/ichangzuo/module/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000..1f53cad --- /dev/null +++ b/src/main/java/com/ichangzuo/module/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,21 @@ +package com.ichangzuo.module.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ichangzuo.module.codegen.mapper.GenFieldConfigMapper; +import com.ichangzuo.module.codegen.model.entity.GenFieldConfig; +import com.ichangzuo.module.codegen.service.GenFieldConfigService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 代码生成字段配置服务实现类 + * + * @author Ray + * @since 2.10.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + + +} diff --git a/src/main/java/com/ichangzuo/module/file/controller/FileController.java b/src/main/java/com/ichangzuo/module/file/controller/FileController.java new file mode 100644 index 0000000..2ddbb05 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/file/controller/FileController.java @@ -0,0 +1,55 @@ +package com.ichangzuo.module.file.controller; + +import com.ichangzuo.common.result.Result; +import com.ichangzuo.module.file.service.FileService; +import com.ichangzuo.system.model.dto.FileInfo; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +/** + * 文件控制层 + * + * @author Ray + * @since 2022/10/16 + */ +@Tag(name = "07.文件接口") +@RestController +@RequestMapping("/api/v1/files") +@RequiredArgsConstructor +public class FileController { + + private final FileService fileService; + + @PostMapping + @Operation(summary = "文件上传") + public Result uploadFile( + @Parameter( + name = "file", + description = "表单文件对象", + required = true, + in = ParameterIn.DEFAULT, + schema = @Schema(name = "file", format = "binary") + ) + @RequestPart(value = "file") MultipartFile file, @RequestParam(value = "type") String type + ) { + FileInfo fileInfo = fileService.uploadFile(file, type); + return Result.success(fileInfo); + } + + @DeleteMapping + @Operation(summary = "文件删除") + @SneakyThrows + public Result deleteFile( + @Parameter(description = "文件路径") @RequestParam String filePath + ) { + boolean result = fileService.deleteFile(filePath); + return Result.judge(result); + } +} diff --git a/src/main/java/com/ichangzuo/module/file/service/FileService.java b/src/main/java/com/ichangzuo/module/file/service/FileService.java new file mode 100644 index 0000000..c29443a --- /dev/null +++ b/src/main/java/com/ichangzuo/module/file/service/FileService.java @@ -0,0 +1,31 @@ +package com.ichangzuo.module.file.service; + +import com.ichangzuo.system.model.dto.FileInfo; +import org.springframework.web.multipart.MultipartFile; + +/** + * 对象存储服务接口层 + * + * @author haoxr + * @since 2022/11/19 + */ +public interface FileService { + + /** + * 上传文件 + * @param file 表单文件对象 + * @param type 表单文件类型 + * @return 文件信息 + */ + FileInfo uploadFile(MultipartFile file, String type); + + /** + * 删除文件 + * + * @param filePath 文件完整URL + * @return 删除结果 + */ + boolean deleteFile(String filePath); + + +} diff --git a/src/main/java/com/ichangzuo/module/file/service/impl/AliyunFileService.java b/src/main/java/com/ichangzuo/module/file/service/impl/AliyunFileService.java new file mode 100644 index 0000000..f8b077e --- /dev/null +++ b/src/main/java/com/ichangzuo/module/file/service/impl/AliyunFileService.java @@ -0,0 +1,98 @@ +//package com.ichangzuo.module.file.service.impl; +// +//import cn.hutool.core.date.DateUtil; +//import cn.hutool.core.io.FileUtil; +//import cn.hutool.core.lang.Assert; +//import cn.hutool.core.util.IdUtil; +//import com.aliyun.oss.OSS; +//import com.aliyun.oss.OSSClientBuilder; +//import com.aliyun.oss.model.ObjectMetadata; +//import com.aliyun.oss.model.PutObjectRequest; +//import com.ichangzuo.module.file.service.FileService; +//import com.ichangzuo.system.model.dto.FileInfo; +//import jakarta.annotation.PostConstruct; +//import lombok.Data; +//import lombok.RequiredArgsConstructor; +//import lombok.SneakyThrows; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +//import org.springframework.boot.context.properties.ConfigurationProperties; +//import org.springframework.stereotype.Component; +//import org.springframework.web.multipart.MultipartFile; +// +//import java.io.InputStream; +//import java.time.LocalDateTime; +// +///** +// * Aliyun 对象存储服务类 +// * +// * @author haoxr +// * @since 2.3.0 +// */ +//@Component +//@ConditionalOnProperty(value = "oss.type", havingValue = "aliyun") +//@ConfigurationProperties(prefix = "oss.aliyun") +//@RequiredArgsConstructor +//@Data +//public class AliyunFileService implements FileService { +// /** +// * 服务Endpoint +// */ +// private String endpoint; +// /** +// * 访问凭据 +// */ +// private String accessKeyId; +// /** +// * 凭据密钥 +// */ +// private String accessKeySecret; +// /** +// * 存储桶名称 +// */ +// private String bucketName; +// +// private OSS aliyunOssClient; +// +// @PostConstruct +// public void init() { +// aliyunOssClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); +// } +// +// @Override +// @SneakyThrows +// public FileInfo uploadFile(MultipartFile file) { +// +// // 生成文件名(日期文件夹) +// String suffix = FileUtil.getSuffix(file.getOriginalFilename()); +// String uuid = IdUtil.simpleUUID(); +// String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + uuid + "." + suffix; +// // try-with-resource 语法糖自动释放流 +// try (InputStream inputStream = file.getInputStream()) { +// +// // 设置上传文件的元信息,例如Content-Type +// ObjectMetadata metadata = new ObjectMetadata(); +// metadata.setContentType(file.getContentType()); +// // 创建PutObjectRequest对象,指定Bucket名称、对象名称和输入流 +// PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream, metadata); +// // 上传文件 +// aliyunOssClient.putObject(putObjectRequest); +// } catch (Exception e) { +// throw new RuntimeException("文件上传失败"); +// } +// // 获取文件访问路径 +// String fileUrl = "https://" + bucketName + "." + endpoint + "/" + fileName; +// FileInfo fileInfo = new FileInfo(); +// fileInfo.setName(fileName); +// fileInfo.setUrl(fileUrl); +// return fileInfo; +// } +// +// @Override +// public boolean deleteFile(String filePath) { +// Assert.notBlank(filePath, "删除文件路径不能为空"); +// String fileHost = "https://" + bucketName + "." + endpoint; // 文件主机域名 +// String fileName = filePath.substring(fileHost.length() + 1); // +1 是/占一个字符,截断左闭右开 +// aliyunOssClient.deleteObject(bucketName, fileName); +// return true; +// } +//} diff --git a/src/main/java/com/ichangzuo/module/file/service/impl/FileServiceImpl.java b/src/main/java/com/ichangzuo/module/file/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..e4a205f --- /dev/null +++ b/src/main/java/com/ichangzuo/module/file/service/impl/FileServiceImpl.java @@ -0,0 +1,106 @@ +package com.ichangzuo.module.file.service.impl; + +import cn.hutool.core.io.FileUtil; +import com.ichangzuo.core.security.util.SecurityUtils; +import com.ichangzuo.module.file.service.FileService; +import com.ichangzuo.system.model.dto.FileInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Objects; + +@Component +public class FileServiceImpl implements FileService { + @Autowired + private Environment env; + + @Override + public FileInfo uploadFile(MultipartFile file, String type) { + if (file != null) { + String uploadPath = env.getProperty("web.resource-path"); + String suffix = FileUtil.getSuffix(file.getOriginalFilename()); + + Date date = new Date(); + SimpleDateFormat fmtYM = new SimpleDateFormat("yyyyMM/"); + String subdir = ""; + + String dir = "", filename = ""; + if ("user-name".equals(type)) { + dir = "User/Name/"; + subdir = fmtYM.format(date); + + Long userId = SecurityUtils.getUserId(); + filename = userId.toString() + "_" + date.getTime() + "." + suffix; + + } + else if("rich-edit".equals(type)){ + dir = "RichEditor/"; + subdir = fmtYM.format(date); + filename = date.getTime() + "." + suffix; + } + else if(type.contains("software")){ + dir = "Software/App/"; + + String[] arrTemp = type.split(","); + if("ac_win".equals(arrTemp[1])) { + subdir = "Ac/Pc/"; + filename = "Ac_Windows_" + arrTemp[2] + ".exe"; + } + else if("ac_android".equals(arrTemp[1])) { + subdir = "Ac/Android/"; + filename = "Ac_Android_" + arrTemp[2] + ".apk"; + } + else if("cz_android".equals(arrTemp[1])) { + subdir = "Icz/Android/"; + filename = "Icz_Android_" + arrTemp[2] + ".apk"; + } + else if("pro_win".equals(arrTemp[1])) { + subdir = "Ac_Pro/Pc/"; + filename = "Ac_Pro_" + arrTemp[2] + ".exe"; + } + else if("styleTool".equals(arrTemp[1])) { + subdir = "Tools/Pc/"; + filename = "StyleTools_" + arrTemp[2] + ".exe"; + } + else + return null; + } + + try { + String upload_file_dir = uploadPath + dir + subdir; + File upload_file_dir_file = new File(upload_file_dir); + if (!upload_file_dir_file.exists()) { + if (!upload_file_dir_file.mkdirs()) + return null; + } + + File targetFile = new File(upload_file_dir_file, filename); + file.transferTo(targetFile); + + if( !targetFile.setReadable(true,false) ) + return null; + + FileInfo fileInfo = new FileInfo(); + fileInfo.setName(filename); + fileInfo.setUrl(subdir + filename); + return fileInfo; + + } catch (Exception e) { + e.printStackTrace(); + } + } + + return null; + } + + @Override + public boolean deleteFile(String filePath) { + String uploadPathImg = env.getProperty("web.resource-path"); + return false; + } +} diff --git a/src/main/java/com/ichangzuo/module/file/service/impl/MinioFileService.java b/src/main/java/com/ichangzuo/module/file/service/impl/MinioFileService.java new file mode 100644 index 0000000..08ae7c7 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/file/service/impl/MinioFileService.java @@ -0,0 +1,209 @@ +//package com.ichangzuo.module.file.service.impl; +// +//import cn.hutool.core.date.DateUtil; +//import cn.hutool.core.io.FileUtil; +//import cn.hutool.core.lang.Assert; +//import cn.hutool.core.util.IdUtil; +//import cn.hutool.core.util.StrUtil; +//import com.ichangzuo.module.file.service.FileService; +//import com.ichangzuo.system.model.dto.FileInfo; +//import io.minio.*; +//import io.minio.errors.*; +//import io.minio.http.Method; +//import jakarta.annotation.PostConstruct; +//import lombok.Data; +//import lombok.RequiredArgsConstructor; +//import lombok.SneakyThrows; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +//import org.springframework.boot.context.properties.ConfigurationProperties; +//import org.springframework.stereotype.Component; +//import org.springframework.web.multipart.MultipartFile; +// +//import java.io.IOException; +//import java.io.InputStream; +//import java.security.InvalidKeyException; +//import java.security.NoSuchAlgorithmException; +//import java.time.LocalDateTime; +// +///** +// * MinIO 文件上传服务类 +// * +// * @author haoxr +// * @since 2023/6/2 +// */ +//@Component +//@ConditionalOnProperty(value = "oss.type", havingValue = "minio") +//@ConfigurationProperties(prefix = "oss.minio") +//@RequiredArgsConstructor +//@Data +//public class MinioFileService implements FileService { +// +// /** +// * 服务Endpoint +// */ +// private String endpoint; +// /** +// * 访问凭据 +// */ +// private String accessKey; +// /** +// * 凭据密钥 +// */ +// private String secretKey; +// /** +// * 存储桶名称 +// */ +// private String bucketName; +// /** +// * 自定义域名 +// */ +// private String customDomain; +// +// private MinioClient minioClient; +// +// // 依赖注入完成之后执行初始化 +// @PostConstruct +// public void init() { +// minioClient = MinioClient.builder() +// .endpoint(endpoint) +// .credentials(accessKey, secretKey) +// .build(); +// // 创建存储桶(存储桶不存在) +// // createBucketIfAbsent(bucketName); +// } +// +// +// /** +// * 上传文件 +// * +// * @param file 表单文件对象 +// * @return +// */ +// @Override +// public FileInfo uploadFile(MultipartFile file) { +// +// // 创建存储桶(存储桶不存在),如果有搭建好的minio服务,建议放在init方法中 +// createBucketIfAbsent(bucketName); +// +// // 生成文件名(日期文件夹) +// String suffix = FileUtil.getSuffix(file.getOriginalFilename()); +// String uuid = IdUtil.simpleUUID(); +// String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + uuid + "." + suffix; +// // try-with-resource 语法糖自动释放流 +// try (InputStream inputStream = file.getInputStream()) { +// // 文件上传 +// PutObjectArgs putObjectArgs = PutObjectArgs.builder() +// .bucket(bucketName) +// .object(fileName) +// .contentType(file.getContentType()) +// .stream(inputStream, inputStream.available(), -1) +// .build(); +// minioClient.putObject(putObjectArgs); +// +// // 返回文件路径 +// String fileUrl; +// // 未配置自定义域名 +// if (StrUtil.isBlank(customDomain)) { +// GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder() +// .bucket(bucketName).object(fileName) +// .method(Method.GET) +// .build(); +// +// fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs); +// fileUrl = fileUrl.substring(0, fileUrl.indexOf("?")); +// } else { // 配置自定义文件路径域名 +// fileUrl = customDomain + '/' + bucketName + "/" + fileName; +// } +// +// FileInfo fileInfo = new FileInfo(); +// fileInfo.setName(fileName); +// fileInfo.setUrl(fileUrl); +// return fileInfo; +// } catch (Exception e) { +// throw new RuntimeException("文件上传失败"); +// } +// } +// +// +// /** +// * 删除文件 +// * +// * @param filePath 文件路径 +// * https://oss.youlai.tech/default/20221120/test.jpg +// * @return +// */ +// @Override +// public boolean deleteFile(String filePath) { +// Assert.notBlank(filePath, "删除文件路径不能为空"); +// try { +// String fileName; +// if (StrUtil.isNotBlank(customDomain)) { +// // https://oss.youlai.tech/default/20221120/test.jpg → 20221120/test.jpg +// fileName = filePath.substring(customDomain.length() + 1 + bucketName.length() + 1); // 两个/占了2个字符长度 +// } else { +// // http://localhost:9000/default/20221120/test.jpg → 20221120/test.jpg +// fileName = filePath.substring(endpoint.length() + 1 + bucketName.length() + 1); +// } +// RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder() +// .bucket(bucketName) +// .object(fileName) +// .build(); +// +// minioClient.removeObject(removeObjectArgs); +// return true; +// } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | +// InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | +// XmlParserException e) { +// throw new RuntimeException(e); +// } +// } +// +// +// /** +// * PUBLIC桶策略 +// * 如果不配置,则新建的存储桶默认是PRIVATE,则存储桶文件会拒绝访问 Access Denied +// * +// * @param bucketName +// * @return +// */ +// private static String publicBucketPolicy(String bucketName) { +// /** +// * AWS的S3存储桶策略 +// * Principal: 生效用户对象 +// * Resource: 指定存储桶 +// * Action: 操作行为 +// */ +// +// return "{\"Version\":\"2012-10-17\"," +// + "\"Statement\":[{\"Effect\":\"Allow\"," +// + "\"Principal\":{\"AWS\":[\"*\"]}," +// + "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"]," +// + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]}," +// + "{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]}," +// + "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"]," +// + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}"; +// } +// +// /** +// * 创建存储桶(存储桶不存在) +// * +// * @param bucketName +// */ +// @SneakyThrows +// private void createBucketIfAbsent(String bucketName) { +// BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build(); +// if (!minioClient.bucketExists(bucketExistsArgs)) { +// MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build(); +// +// minioClient.makeBucket(makeBucketArgs); +// +// // 设置存储桶访问权限为PUBLIC, 如果不配置,则新建的存储桶默认是PRIVATE,则存储桶文件会拒绝访问 Access Denied +// SetBucketPolicyArgs setBucketPolicyArgs = SetBucketPolicyArgs +// .builder() +// .bucket(bucketName) +// .config(publicBucketPolicy(bucketName)) +// .build(); +// minioClient.setBucketPolicy(setBucketPolicyArgs); +// } +// } +//} diff --git a/src/main/java/com/ichangzuo/module/mail/controller/MailController.java b/src/main/java/com/ichangzuo/module/mail/controller/MailController.java new file mode 100644 index 0000000..03045af --- /dev/null +++ b/src/main/java/com/ichangzuo/module/mail/controller/MailController.java @@ -0,0 +1,14 @@ +package com.ichangzuo.module.mail.controller; + +import org.springframework.web.bind.annotation.*; + +/** + * 邮件控制层 + * + * @author Ray + * @since 2.10.0 + */ +@RestController +public class MailController { + +} diff --git a/src/main/java/com/ichangzuo/module/mail/service/MailService.java b/src/main/java/com/ichangzuo/module/mail/service/MailService.java new file mode 100644 index 0000000..e21aacc --- /dev/null +++ b/src/main/java/com/ichangzuo/module/mail/service/MailService.java @@ -0,0 +1,32 @@ +package com.ichangzuo.module.mail.service; + +/** + * 邮件服务接口层 + * + * @author Ray + * @since 2024/8/17 + */ +public interface MailService { + + + /** + * 发送简单文本邮件 + * + * @param to 收件人地址 + * @param subject 邮件主题 + * @param text 邮件内容 + */ + boolean sendMail(String to, String subject, String text); + + boolean sendHtmlMail(String to, String subject, String text); + + /** + * 发送带附件的邮件 + * + * @param to 收件人地址 + * @param subject 邮件主题 + * @param text 邮件内容 + * @param filePath 附件路径 + */ + boolean sendMailWithAttachment(String to, String subject, String text, String filePath); +} diff --git a/src/main/java/com/ichangzuo/module/mail/service/impl/MailServiceImpl.java b/src/main/java/com/ichangzuo/module/mail/service/impl/MailServiceImpl.java new file mode 100644 index 0000000..c075ae1 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/mail/service/impl/MailServiceImpl.java @@ -0,0 +1,147 @@ +package com.ichangzuo.module.mail.service.impl; + +import com.ichangzuo.config.property.MailProperties; +import com.ichangzuo.module.mail.service.MailService; +import groovy.lang.GString; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mail.MailException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.util.Properties; + +/** + * 邮件服务实现类 + * + * @author Ray + * @since 2024/8/17 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class MailServiceImpl implements MailService { + private final JavaMailSender mailSender; + private final MailProperties mailProperties; + + /** + * 发送简单文本邮件 + * + * @param to 收件人地址 + * @param subject 邮件主题 + * @param text 邮件内容 + */ + @Override + public boolean sendMail(String to, String subject, String text) { + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(mailProperties.getFrom()); + message.setTo(to); + message.setSubject(subject); + message.setText(text); +// + + // 直接创建 JavaMailSenderImpl 实现类 +// JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); +// +// mailSender.setDefaultEncoding("utf-8"); +// +// mailSender.setHost("smtp.qq.com"); // 设置邮箱服务器 +// mailSender.setPort(465); // 设置端口 +// mailSender.setUsername("747692844@qq.com"); // 设置用户名 +// mailSender.setPassword("<你的密码/授权码>"); // 设置密码(记得替换为你实际的密码、授权码) +// mailSender.setProtocol("smtps"); // 设置协议 +// +// Properties properties = new Properties(); // 配置项 +// properties.put("mail.smtp.connectiontimeout", 5000); +// properties.put("mail.smtp.timeout", 3000); +// properties.put("mail.smtp.writetimeout", "5000"); +// properties.put("mail.smtp.auth", true); +// properties.put("mail.smtp.starttls.enable", true); +// properties.put("mail.smtp.starttls.required", true); + +// mailSender.setJavaMailProperties(properties); // 设置配置项 +// +// // 创建一个邮件消息 +// MimeMessage message = mailSender.createMimeMessage(); +// +// // 创建 MimeMessageHelper +// MimeMessageHelper helper = new MimeMessageHelper(message, false); +// +// // 发件人邮箱和名称 +// helper.setFrom(mailProperties.getFrom()); +// // 收件人邮箱 +// helper.setTo(to); +// // 邮件标题 +// helper.setSubject(subject); +// // 邮件正文,第二个参数表示是否是HTML正文 +// helper.setText(text, true); +// + mailSender.send(message); + + } catch (MailException e) { + log.error("发送邮件失败{}", e.getMessage()); + return false; + } + + return true; + } + + @Override + public boolean sendHtmlMail(String to, String subject, String text) { + MimeMessage message = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, "UTF-8"); + helper.setFrom(mailProperties.getFrom()); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(text, true); // true表示支持HTML内容 + + mailSender.send(message); + } catch (MessagingException e) { + log.error("发送邮件失败{}", e.getMessage()); + + return false; + } + + return true; + } + + /** + * 发送带附件的邮件 + * + * @param to 收件人地址 + * @param subject 邮件主题 + * @param text 邮件内容 + * @param filePath 附件路径 + */ + @Override + public boolean sendMailWithAttachment(String to, String subject, String text, String filePath) { + MimeMessage message = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(mailProperties.getFrom()); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(text, true); // true表示支持HTML内容 + + FileSystemResource file = new FileSystemResource(new File(filePath)); + helper.addAttachment(file.getFilename(), file); + + mailSender.send(message); + } catch (MessagingException e) { + log.error("发送邮件失败{}", e.getMessage()); + + return false; + } + + return true; + } +} diff --git a/src/main/java/com/ichangzuo/module/sms/controller/SmsController.java b/src/main/java/com/ichangzuo/module/sms/controller/SmsController.java new file mode 100644 index 0000000..f1c9f1b --- /dev/null +++ b/src/main/java/com/ichangzuo/module/sms/controller/SmsController.java @@ -0,0 +1,13 @@ +package com.ichangzuo.module.sms.controller; + +/** + * 短信控制层 + * + * @author Ray + * @since 2.10.0 + */ +public class SmsController { + + + +} diff --git a/src/main/java/com/ichangzuo/module/sms/service/SmsService.java b/src/main/java/com/ichangzuo/module/sms/service/SmsService.java new file mode 100644 index 0000000..ee295ec --- /dev/null +++ b/src/main/java/com/ichangzuo/module/sms/service/SmsService.java @@ -0,0 +1,22 @@ +package com.ichangzuo.module.sms.service; + +/** + * 短信服务接口层 + *

+ * SMS = Short Message Service 短信服务 + * + * @author Ray + * @since 2024/8/17 + */ +public interface SmsService { + + /** + * 发送短信 + * + * @param mobile 手机号 13388886666 + * @param templateCode 短信模板 SMS_194640010 + * @param templateParam 模板参数 "[{"code":"123456"}]" + * @return boolean 是否发送成功 + */ + boolean sendSms(String mobile, String templateCode, String templateParam); +} diff --git a/src/main/java/com/ichangzuo/module/sms/service/impl/AliyunSmsService.java b/src/main/java/com/ichangzuo/module/sms/service/impl/AliyunSmsService.java new file mode 100644 index 0000000..a4ebb94 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/sms/service/impl/AliyunSmsService.java @@ -0,0 +1,75 @@ +package com.ichangzuo.module.sms.service.impl; + +import com.aliyuncs.CommonRequest; +import com.aliyuncs.CommonResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.exceptions.ServerException; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.profile.DefaultProfile; +import com.google.gson.Gson; +import com.ichangzuo.module.sms.service.SmsService; +import com.ichangzuo.config.property.AliyunSmsProperties; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** + * 阿里云短信业务类 + * + * @author Ray + * @since 2024/8/17 + */ +@Service +@RequiredArgsConstructor +public class AliyunSmsService implements SmsService { + + private static final Logger log = LoggerFactory.getLogger(AliyunSmsService.class); + private final AliyunSmsProperties aliyunSmsProperties; + + /** + * 发送短信验证码 + * + * @param mobile 手机号 13388886666 + * @param templateCode 短信模板 SMS_194640010 + * @param templateParam 模板参数 "[{"code":"123456"}]" + * + * @return boolean 是否发送成功 + */ + @Override + public boolean sendSms(String mobile,String templateCode,String templateParam) { + if(aliyunSmsProperties.getAccessKeyId().equals("123456")) //测试版本 + return true; + + DefaultProfile profile = DefaultProfile.getProfile(aliyunSmsProperties.getRegionId(), + aliyunSmsProperties.getAccessKeyId(), aliyunSmsProperties.getAccessKeySecret()); + IAcsClient client = new DefaultAcsClient(profile); + + SendSmsRequest request = new SendSmsRequest(); + request.setSignName(aliyunSmsProperties.getSignName()); + request.setTemplateCode(templateCode); + request.setPhoneNumbers(mobile); + request.setTemplateParam(templateParam); + + try { + SendSmsResponse response = client.getAcsResponse(request); + log.info("Send sms response: " + response.getMessage()); + return response.getCode().equals("OK"); + } catch (ServerException e) { + e.printStackTrace(); + } catch (ClientException e) { + log.error(e.getErrMsg()); +// System.out.println("ErrCode:" + e.getErrCode()); +// System.out.println("ErrMsg:" + e.getErrMsg()); +// System.out.println("RequestId:" + e.getRequestId()); + } + + return false; + } + + +} diff --git a/src/main/java/com/ichangzuo/module/websocket/controller/WebsocketController.java b/src/main/java/com/ichangzuo/module/websocket/controller/WebsocketController.java new file mode 100644 index 0000000..a0cecd3 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/websocket/controller/WebsocketController.java @@ -0,0 +1,61 @@ +//package com.ichangzuo.module.websocket.controller; +// +//import com.ichangzuo.system.model.dto.ChatMessage; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.messaging.handler.annotation.DestinationVariable; +//import org.springframework.messaging.handler.annotation.MessageMapping; +//import org.springframework.messaging.handler.annotation.SendTo; +//import org.springframework.messaging.simp.SimpMessagingTemplate; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RestController; +// +//import java.security.Principal; +// +///** +// * WebSocket 测试控制层 +// * +// * @author Ray +// * @since 2.3.0 +// */ +//@RestController +//@RequestMapping("/websocket") +//@RequiredArgsConstructor +//@Slf4j +//public class WebsocketController { +// +// private final SimpMessagingTemplate messagingTemplate; +// +// +// /** +// * 广播发送消息 +// * +// * @param message 消息内容 +// */ +// @MessageMapping("/sendToAll") +// @SendTo("/topic/notice") +// public String sendToAll(String message) { +// return "服务端通知: " + message; +// } +// +// /** +// * 点对点发送消息 +// *

+// * 模拟 张三 给 李四 发送消息场景 +// * +// * @param principal 当前用户 +// * @param username 接收消息的用户 +// * @param message 消息内容 +// */ +// @MessageMapping("/sendToUser/{username}") +// public void sendToUser(Principal principal, @DestinationVariable String username, String message) { +// +// String sender = principal.getName(); // 发送人 +// String receiver = username; // 接收人 +// +// log.info("发送人:{}; 接收人:{}", sender, receiver); +// // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting +// messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); +// } +// +//} diff --git a/src/main/java/com/ichangzuo/module/websocket/service/WebsocketService.java b/src/main/java/com/ichangzuo/module/websocket/service/WebsocketService.java new file mode 100644 index 0000000..350bedc --- /dev/null +++ b/src/main/java/com/ichangzuo/module/websocket/service/WebsocketService.java @@ -0,0 +1,9 @@ +package com.ichangzuo.module.websocket.service; + +public interface WebsocketService { + + void addUser(String username); + + void removeUser(String username) ; + +} diff --git a/src/main/java/com/ichangzuo/module/websocket/service/impl/WebsocketServiceImpl.java b/src/main/java/com/ichangzuo/module/websocket/service/impl/WebsocketServiceImpl.java new file mode 100644 index 0000000..d6f4914 --- /dev/null +++ b/src/main/java/com/ichangzuo/module/websocket/service/impl/WebsocketServiceImpl.java @@ -0,0 +1,52 @@ +//package com.ichangzuo.module.websocket.service.impl; +// +//import com.ichangzuo.module.websocket.service.WebsocketService; +//import com.ichangzuo.system.event.UserConnectionEvent; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.context.event.EventListener; +//import org.springframework.messaging.simp.SimpMessagingTemplate; +//import org.springframework.scheduling.annotation.Scheduled; +//import org.springframework.stereotype.Service; +// +//import java.util.Set; +//import java.util.concurrent.ConcurrentHashMap; +// +//@Service +//@Slf4j +//@RequiredArgsConstructor +//public class WebsocketServiceImpl implements WebsocketService { +// +// private final SimpMessagingTemplate messagingTemplate; +// +// private final Set onlineUsers = ConcurrentHashMap.newKeySet(); +// +// @Override +// public void addUser(String username) { +// onlineUsers.add(username); +// } +// +// @Override +// public void removeUser(String username) { +// onlineUsers.remove(username); +// } +// +// @EventListener +// public void handleUserConnectionEvent(UserConnectionEvent event) { +// String username = event.getUsername(); +// if (event.isConnected()) { +// onlineUsers.add(username); +// log.info("User connected: {}", username); +// } else { +// onlineUsers.remove(username); +// log.info("User disconnected: {}", username); +// } +// // 推送在线用户人数 +// messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUsers.size()); +// } +// +// @Scheduled(fixedRate = 5000) +// public void sendOnlineUserCount() { +// messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUsers.size()); +// } +//} diff --git a/src/main/java/com/ichangzuo/system/controller/BankController.java b/src/main/java/com/ichangzuo/system/controller/BankController.java new file mode 100644 index 0000000..2689ce5 --- /dev/null +++ b/src/main/java/com/ichangzuo/system/controller/BankController.java @@ -0,0 +1,44 @@ +package com.ichangzuo.system.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.system.model.entity.Bank; +import com.ichangzuo.system.model.entity.UserBank; +import com.ichangzuo.system.service.impl.BankServiceImpl; +import com.ichangzuo.system.service.impl.UserBankServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/bank") +@RequiredArgsConstructor +public class BankController { + private final BankServiceImpl bankService; + private final UserBankServiceImpl userBankService; + + @GetMapping("/hotList") + public Result> listHot(){ + List bankList = bankService.list(new LambdaQueryWrapper() + .eq(Bank::getIsHot, 1) + .eq(Bank::getState, 1) + .orderByDesc(Bank::getBankId)); + + List retList = new ArrayList<>(); + bankList.forEach((item)->{ + retList.add(item.getName()); + }); + + return Result.success(retList); + } + + @GetMapping("/me") + public Result getBankInfo(){ + UserBank bank = userBankService.getCurrentBankInfo(); + return Result.success(bank); + } +} diff --git a/src/main/java/com/ichangzuo/system/controller/ClientController.java b/src/main/java/com/ichangzuo/system/controller/ClientController.java new file mode 100644 index 0000000..54bb53a --- /dev/null +++ b/src/main/java/com/ichangzuo/system/controller/ClientController.java @@ -0,0 +1,209 @@ +package com.ichangzuo.system.controller; + + +import cn.hutool.json.JSONString; +import com.alibaba.fastjson.JSONObject; +import com.ichangzuo.common.result.AjaxResult; +import com.ichangzuo.config.WxpayConfig; +import com.ichangzuo.system.model.entity.User; +import com.ichangzuo.system.service.PayService; +import com.ichangzuo.system.service.UserService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Objects; + +@RestController +@RequestMapping("/client") +@RequiredArgsConstructor +public class ClientController { + private final PayService payService; + private final UserService userService; + + /** + * 苹果及老版本请求订单 + * @param user + * @param password + * @param type + * @param money + * @return + */ + @PostMapping(value = "/orderPlace", produces = MediaType.TEXT_HTML_VALUE) + public String orderPlace(@RequestParam String user, @RequestParam String password, @RequestParam String type, @RequestParam String money) { + return payService.getAppOrderInfo(user, password, type, money).toJSONString(); + } + +// /** +// * 苹果支付验签 PHP网站调用 +// * @param param +// * @return +// */ +// @PostMapping("/verifyAppleReceipt") +// public AjaxResult verifyAppleReceipt(@RequestBody String param) { +//// System.out.println(param); +// return payService.verifyAppleReceipt(param, false); +// } + + /** + * 苹果支付结果查询 + * @param receipt + * @param isSandbox + * @param user + * @param password + * @param order + * @return + */ + @PostMapping(value = "/appleOrderFinish", produces = MediaType.TEXT_HTML_VALUE) + public String appleOrderFinis(@RequestParam String receipt, + @RequestParam String isSandbox, + @RequestParam String user, + @RequestParam String password, + @RequestParam String order) { + return payService.appleOrderFinis(receipt, isSandbox, user, password, order).toJSONString(); + } + + @PostMapping(value = "/appleOrderCheck") + public JSONObject appleOrderCheck(@RequestParam String receipt, + @RequestParam String isSandbox, + @RequestParam String user, + @RequestParam String password, + @RequestParam String order) { + return payService.appleOrderFinis(receipt, isSandbox, user, password, order); + } + +// /** +// * 支付宝/微信预约订单 php网站调用 +// * @param order +// * @param money +// * @param type +// * @return +// */ +// @PostMapping("/getPrepayInfo") +// public AjaxResult getPrepayInfo(@RequestParam String order, @RequestParam String money, @RequestParam String type) { +// System.out.println("getPrepayInfo: " + order + " " + money + " " + type); +// +// String result = null; +// if(Objects.equals(type, "ali_ad")) +// result = payService.getAliPayAppInfo(money, order); +// else if(Objects.equals(type, "wx_ad")) +// result = payService.getWxPayAppInfo(money, order); +// else +// return AjaxResult.error("无效的支付方式"); +// +// if(result==null) +// return AjaxResult.error("支付接口调用失败"); +// +// return AjaxResult.success(result); +// } + +// /** +// * alipay验签 php网站调用 +// * @param request +// * @return +// */ +// @PostMapping("/notifyAlipayOrder") +// public AjaxResult notifyAlipayOrder(HttpServletRequest request) { +// return payService.handleAlipayAppNotify(request); +// } + +// /** +// * wxpay验签 php网站调用 +// * @param request +// * @return +// */ +// @PostMapping("/notifyWxpayOrder") +// public AjaxResult notifyWxpayOrder(HttpServletRequest request) { +// return payService.handleWxpayNotify(request); +// } + + /** + * 删除账户 + * @param user + * @param password + * @return + */ + @PostMapping(value = "/deleteUser", produces = MediaType.TEXT_HTML_VALUE) + public String deleteUser(@RequestParam String user, @RequestParam String password) { + JSONObject result = new JSONObject(); + result.put("result", userService.deleteUser(user, password)); + return result.toJSONString(); + } + + @PostMapping(value = "/delAccount") + public JSONObject delAccount(@RequestParam String user, @RequestParam String password) { + JSONObject result = new JSONObject(); + result.put("result", userService.deleteUser(user, password)); + return result; + } + + /** + * 创建订单 + * @param user + * @param password + * @param type + * @param money + * @return + */ + @PostMapping("/getOrderInfo") + public JSONObject getOrderInfo(@RequestParam String user, @RequestParam String password, + @RequestParam String type, @RequestParam String money) { + return payService.getAppOrderInfo(user, password, type, money); + } + + /** + * 订单查询 + * @param user + * @param password + * @param order + * @return + */ + @PostMapping("/orderCheck") + public JSONObject orderCheck(@RequestParam String user, @RequestParam String password, @RequestParam String order) { + return payService.orderCheck(user, password, order); + } + + /** + * 支付宝异步通知 + * @param request + * @return + */ + @PostMapping("/alipayOrderNotify") + public String alipayOrderNotify(HttpServletRequest request) { + AjaxResult result = payService.handleAlipayAppNotify(request); + + Integer code = (Integer) result.get(AjaxResult.CODE_TAG); + if(code==200){ + String orderId = (String) result.get(AjaxResult.DATA_TAG); + payService.orderFinished(orderId, 1.0); + + return "success"; + } + + return "fail"; + } + + /** + * 微信WEB异步通知 + * @param request + * @return + */ + @PostMapping("/wxpayWebOrderNotify") + public void wxpayPageOrderNotify(HttpServletRequest request, HttpServletResponse response) { + payService.handleWxpayNotify(request, response, WxpayConfig.WX_WEB); + } + + /** + * 微信APP异步通知 + * @param request + * @return + */ + @PostMapping("/wxpayAppOrderNotify") + public void wxpayAppOrderNotify(HttpServletRequest request, HttpServletResponse response) { + payService.handleWxpayNotify(request, response, WxpayConfig.WX_APP ); + } +} diff --git a/src/main/java/com/ichangzuo/system/controller/ConfigController.java b/src/main/java/com/ichangzuo/system/controller/ConfigController.java new file mode 100644 index 0000000..d3b81ec --- /dev/null +++ b/src/main/java/com/ichangzuo/system/controller/ConfigController.java @@ -0,0 +1,82 @@ +package com.ichangzuo.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.ichangzuo.common.result.PageResult; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.system.model.form.ConfigForm; +import com.ichangzuo.system.model.query.ConfigPageQuery; +import com.ichangzuo.system.model.vo.ConfigVO; +import com.ichangzuo.system.service.ConfigService; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.security.access.prepost.PreAuthorize; + + +/** + * 系统配置前端控制层 + * + * @author Theo + * @since 2024-07-30 11:25 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @GetMapping("/page") + @Operation(summary = "系统配置分页列表") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + public Result save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PatchMapping + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @PutMapping(value = "/{id}") + @Operation(summary = "修改系统配置") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + public Result update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除系统配置") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + public Result delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/src/main/java/com/ichangzuo/system/controller/DeptController.java b/src/main/java/com/ichangzuo/system/controller/DeptController.java new file mode 100644 index 0000000..325bfa6 --- /dev/null +++ b/src/main/java/com/ichangzuo/system/controller/DeptController.java @@ -0,0 +1,94 @@ +package com.ichangzuo.system.controller; + +import com.ichangzuo.common.annotation.Log; +import com.ichangzuo.common.annotation.RepeatSubmit; +import com.ichangzuo.common.enums.LogModuleEnum; +import com.ichangzuo.common.model.Option; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.system.model.form.DeptForm; +import com.ichangzuo.system.model.query.DeptQuery; +import com.ichangzuo.system.model.vo.DeptVO; +import com.ichangzuo.system.service.DeptService; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; + +/** + * 部门控制器 + * + * @author haoxr + * @since 2020/11/6 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @RepeatSubmit + public Result saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} diff --git a/src/main/java/com/ichangzuo/system/controller/DictController.java b/src/main/java/com/ichangzuo/system/controller/DictController.java new file mode 100644 index 0000000..9abc662 --- /dev/null +++ b/src/main/java/com/ichangzuo/system/controller/DictController.java @@ -0,0 +1,103 @@ +package com.ichangzuo.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ichangzuo.common.annotation.Log; +import com.ichangzuo.common.annotation.RepeatSubmit; +import com.ichangzuo.common.enums.LogModuleEnum; +import com.ichangzuo.common.model.Option; +import com.ichangzuo.common.result.PageResult; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.system.model.query.DictPageQuery; +import com.ichangzuo.system.model.vo.DictPageVO; +import com.ichangzuo.system.model.form.DictForm; +import com.ichangzuo.system.service.DictService; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 字典控制层 + * + * @author Ray + * @since 2.9.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "字典列表") + @GetMapping("/list") + public Result>> getDictList() { + List> list = dictService.getDictList(); + return Result.success(list); + } + + @Operation(summary = "字典数据项列表") + @GetMapping("/{code}/options") + public Result>> getDictOptions( + @Parameter(description = "字典编码") @PathVariable String code + ) { + List> options = dictService.listDictItemsByCode(code); + return Result.success(options); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @RepeatSubmit + public Result saveDict(@RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(ids); + return Result.success(); + } + + +} diff --git a/src/main/java/com/ichangzuo/system/controller/HelperController.java b/src/main/java/com/ichangzuo/system/controller/HelperController.java new file mode 100644 index 0000000..9b32bbd --- /dev/null +++ b/src/main/java/com/ichangzuo/system/controller/HelperController.java @@ -0,0 +1,49 @@ +package com.ichangzuo.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.ichangzuo.common.result.PageResult; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.system.model.bo.QnABO; +import com.ichangzuo.system.model.bo.QnAFullBO; +import com.ichangzuo.system.model.bo.QnAUpdateBO; +import com.ichangzuo.system.model.query.QnAPageQuery; +import com.ichangzuo.system.service.impl.QnAServiceImpl; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/helper") +@RequiredArgsConstructor +public class HelperController { + private final QnAServiceImpl qnaService; + + @GetMapping("/listQnA/{id}") + public Result> listQnA(@Parameter(description = "问答ID") @PathVariable Integer id){ + List listBo = qnaService.listByType(id); + return Result.success(listBo); + } + + @GetMapping("/pageQnA") + public PageResult pageQnA(QnAPageQuery queryParams){ + IPage page = qnaService.listPage(queryParams); + return PageResult.success(page); + } + + @PutMapping(value = "/updateQnA") + public Result updateQnA( @RequestBody QnAUpdateBO qnaBo ) { + boolean result = qnaService.updateOrSave(qnaBo); + return Result.judge(result); + } + + @PutMapping(value = "/setQnAByIds/{ids}") + public Result setByIds( + @Parameter(description = "QnAID,多个以英文逗号(,)分割") @PathVariable String ids, + @Parameter(description = "设置状态参数") @RequestParam Integer state + ) { + boolean result = qnaService.setByIds(ids,state); + return Result.judge(result); + } +} diff --git a/src/main/java/com/ichangzuo/system/controller/IntegralController.java b/src/main/java/com/ichangzuo/system/controller/IntegralController.java new file mode 100644 index 0000000..9a30a2e --- /dev/null +++ b/src/main/java/com/ichangzuo/system/controller/IntegralController.java @@ -0,0 +1,44 @@ +package com.ichangzuo.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ichangzuo.common.result.PageResult; +import com.ichangzuo.core.security.util.SecurityUtils; +import com.ichangzuo.system.model.query.IntegralPageQuery; +import com.ichangzuo.system.model.vo.IntegralAdminPageVO; +import com.ichangzuo.system.model.vo.IntegralUserPageVO; +import com.ichangzuo.system.service.impl.IntegralServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/integral") +@RequiredArgsConstructor +public class IntegralController { + private final IntegralServiceImpl integralService; + + @GetMapping("/pageAdmin") + public PageResult listAdminPage( + IntegralPageQuery queryParams + ){ + IPage page = integralService.listPagedAdmin(queryParams); + return PageResult.success(page); + } + + @GetMapping("/pageUser") + public PageResult listUserPage( + IntegralPageQuery queryParams + ){ + Long userId = SecurityUtils.getUserId(); + if(queryParams.getUserId()==null){ + queryParams.setUserId(userId); + } + else if(!userId.equals(queryParams.getUserId())) { + return PageResult.success(new Page<>()); + } + + return PageResult.success(integralService.listPagedUser(queryParams)); + } +} diff --git a/src/main/java/com/ichangzuo/system/controller/LogController.java b/src/main/java/com/ichangzuo/system/controller/LogController.java new file mode 100644 index 0000000..fe967db --- /dev/null +++ b/src/main/java/com/ichangzuo/system/controller/LogController.java @@ -0,0 +1,63 @@ +package com.ichangzuo.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ichangzuo.common.result.PageResult; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.system.model.query.LogPageQuery; +import com.ichangzuo.system.model.vo.LogPageVO; +import com.ichangzuo.system.model.vo.VisitStatsVO; +import com.ichangzuo.system.model.vo.VisitTrendVO; +import com.ichangzuo.system.service.LogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + + +/** + * 日志控制层 + * + * @author Ray + * @since 2.10.0 + */ +@Tag(name = "08.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult listPagedLogs( + LogPageQuery queryParams + ) { + Page result = logService.listPagedLogs(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取统计数据") + @GetMapping("/visit-stats") + public Result> getVisitStats() { + List list = logService.getVisitStats(); + return Result.success(list); + } + +} diff --git a/src/main/java/com/ichangzuo/system/controller/MenuController.java b/src/main/java/com/ichangzuo/system/controller/MenuController.java new file mode 100644 index 0000000..8d4b82f --- /dev/null +++ b/src/main/java/com/ichangzuo/system/controller/MenuController.java @@ -0,0 +1,116 @@ +package com.ichangzuo.system.controller; + +import com.ichangzuo.common.annotation.Log; +import com.ichangzuo.common.annotation.RepeatSubmit; +import com.ichangzuo.common.enums.LogModuleEnum; +import com.ichangzuo.common.model.Option; +import com.ichangzuo.common.result.Result; +import com.ichangzuo.core.security.util.SecurityUtils; +import com.ichangzuo.system.model.form.MenuForm; +import com.ichangzuo.system.model.query.MenuQuery; +import com.ichangzuo.system.model.vo.MenuVO; +import com.ichangzuo.system.model.vo.RouteVO; +import com.ichangzuo.system.service.MenuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Set; + +/** + * 菜单控制层 + * + * @author Ray + * @since 2020/11/06 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List