first commit

This commit is contained in:
蔡浩珊_信息数字化部
2026-05-23 17:20:26 +08:00
commit f053037227
152 changed files with 10574 additions and 0 deletions

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
.fastRequest/
application-prod.yml

97
README.md Normal file
View File

@@ -0,0 +1,97 @@
# miku-framework
私有开发框架基于springboot3.0
### 项目结构
```markdown
miku-framework目录结构
└─main
├─java
│ └─com
│ └─mikufufu
│ ├─common # 全局共享层(跨模块复用)
│ │ ├─annotation # 自定义注解(如@Log@Permission
│ │ ├─constant # 全局常量(错误码/正则表达式/通用配置项)
│ │ ├─enums # 通用枚举(业务状态/类型标记)
│ │ └─exception # 全局异常类(如参数校验异常)
│ │
│ ├─config # 应用配置类如Swagger/线程池配置)
│ │
│ ├─core # 核心技术组件(与业务解耦)
│ │ ├─cache # 缓存抽象层Redis/Memcached操作封装
│ │ ├─serializer # 序列化协议JSON/ProtoBuf自定义逻辑
│ │ └─utils # 核心工具类(加解密/反射工具等)
│ │
│ ├─modules # 业务模块(按功能垂直拆分)
│ │ │
│ │ ├─api # 对外接口模块
│ │ │ ├─controller # 聚合API入口如第三方回调接口
│ │ │ └─model
│ │ │ └─vo # API专用视图对象避免污染内部模型
│ │ │
│ │ ├─auth # 认证授权模块
│ │ │ ├─controller # 登录/登出/令牌管理接口
│ │ │ ├─model
│ │ │ │ ├─dto # 认证传输对象如LoginDTO
│ │ │ │ └─entity # 认证实体如UserToken表映射
│ │ │ ├─security # 安全子模块
│ │ │ │ ├─filter # 安全过滤器如JWT校验
│ │ │ │ └─handler # 认证处理器(如登录成功返回处理)
│ │ │ ├─service # 认证服务接口
│ │ │ │ └─impl # 接口实现如JWT认证服务
│ │ │ └─utils # 模块专用工具如Token生成器
│ │ │
│ │ ├─storage # 文件存储模块
│ │ │ ├─controller # 文件上传/下载接口
│ │ │ ├─enums # 存储枚举如文件类型FileType
│ │ │ ├─model
│ │ │ │ └─entity # 存储实体如FileMetadata表映射
│ │ │ ├─service # 存储服务接口
│ │ │ │ └─impl # 实现类如OSS/Local存储实现
│ │ │ └─strategy # 存储策略模式
│ │ │ └─mode
│ │ │ └─impl # 策略实现(如分片上传策略)
│ │ │
│ │ └─system # 系统管理模块
│ │ ├─controller # 用户/角色/权限管理接口
│ │ ├─exception # 模块专属异常如UserNotFoundException
│ │ ├─model
│ │ │ ├─entity # 系统实体如User表映射
│ │ │ └─vo # 视图对象如UserVO
│ │ └─service # 系统服务接口
│ │ └─impl # 实现类如UserServiceImpl
│ │
│ ├─mapper # MyBatis数据访问接口统一管理
│ │
│ ├─task # 定时任务调度如XXL-JOB处理器
│ │
│ └─utils # 自定义工具类
└─resources
├─mapper # MyBatis XML文件
└─static # 静态资源
└─css # CSS样式文件
```
### 部署方式
执行下列命令
```
mvn package -P prod
```
在打包好的jar文件所在的文件夹下新建config文件夹
将代码中的`application.yml``application-prod.yml`复制到config文件夹中即可如需修改只需要修改prod文件即可
执行下列命令运行:
```
jar -jar [jar包名].jar
```
Ps修改yml文件后需要重启才可生效

305
pom.xml Normal file
View File

@@ -0,0 +1,305 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.13</version>
<relativePath/>
</parent>
<groupId>com.mikufufu</groupId>
<artifactId>miku-framework</artifactId>
<version>0.0.1</version>
<name>miku-framework</name>
<description>私有开发框架基于springboot3.0</description>
<!-- 打包方式默认jar <packaging>pom</packaging>-->
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!--核心依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Spring Boot的安全启动器依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<!--redis相关的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!--mybatis-plus分页插件依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.9</version>
</dependency>
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.21</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.48</version>
</dependency>
<!--阿里云oss依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.18.3</version>
</dependency>
<!--minio对象存储服务-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.6.0</version>
<exclusions>
<exclusion>
<artifactId>commons-io</artifactId>
<groupId>commons-io</groupId>
</exclusion>
<exclusion>
<artifactId>okhttp</artifactId>
<groupId>com.squareup.okhttp3</groupId>
</exclusion>
</exclusions>
</dependency>
<!--okhttp依赖-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- springdoc -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.9</version>
</dependency>
<!--springboot配置依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 数据验证依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--aop依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--邮件依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<exclusions>
<exclusion>
<groupId>org.eclipse.angus</groupId>
<artifactId>jakarta.mail</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.angus</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.4</version> <!-- 使用已修复漏洞的版本 -->
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- io常用工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.16.1</version>
</dependency>
<!--excel解析器-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.3</version>
</dependency>
<!--mybatis-plus生成器依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.9</version>
</dependency>
<!-- freemarker代码生成器依赖-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.34</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.2.14</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.2.14</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.2.14</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>6.2.14</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>10.1.49</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec</artifactId>
<version>4.1.128.Final</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
</dependency>
<!-- 修复 commons-compress 漏洞 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.0</version>
</dependency>
<!-- 修复 poi-ooxml 漏洞 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.4.1</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<!-- 配置编译时处理器 -->
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- 关键:配置 JAR 打包时排除 YAML -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<excludes>
<!-- 排除所有 YAML 文件 -->
<exclude>application*.yml</exclude> <!-- 排除所有YAML配置文件 -->
<exclude>application*.properties</exclude> <!-- 排除所有properties配置文件 -->
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>dev</id> <!--给每个环境一个唯一的id-->
<properties><!--在这个标签中配置你的自定义变量下面的env就是我自己定义的-->
<active>dev</active>
</properties>
<activation>
<activeByDefault>true</activeByDefault><!--默认激活的环境-->
</activation>
</profile>
<profile>
<id>local</id>
<properties>
<active>local</active>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<active>test</active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<active>prod</active>
</properties>
</profile>
</profiles>
</project>

234
sql/miku.sql Normal file
View File

@@ -0,0 +1,234 @@
CREATE DATABASE IF NOT EXISTS miku
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
use miku;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for m_menu
-- ----------------------------
DROP TABLE IF EXISTS `m_menu`;
CREATE TABLE `m_menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '菜单id',
`menu_name` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '菜单的名字',
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '菜单的路由地址',
`component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '菜单对应组件地址',
`parent_id` int NULL DEFAULT 0 COMMENT '父菜单id',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '菜单图标',
`status` int UNSIGNED NULL DEFAULT 0 COMMENT '是否隐藏0.显示1.隐藏)',
`sort` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '排序',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_menu
-- ----------------------------
-- ----------------------------
-- Table structure for m_operate_log
-- ----------------------------
DROP TABLE IF EXISTS `m_operate_log`;
CREATE TABLE `m_operate_log` (
`id` int NOT NULL AUTO_INCREMENT,
`module` int NULL DEFAULT NULL COMMENT '功能模块',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '接口地址',
`params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '请求参数',
`operate_type` int NULL DEFAULT NULL COMMENT '操作类型',
`method_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '方法名',
`operator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作人',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'ip地址',
`operate_time` datetime NULL DEFAULT NULL COMMENT '操作时间',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '附加信息',
`status` int NULL DEFAULT NULL COMMENT '状态1.成功0.失败)',
`error_message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '错误信息',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '操作日志表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_operate_log
-- ----------------------------
-- ----------------------------
-- Table structure for m_permission
-- ----------------------------
DROP TABLE IF EXISTS `m_permission`;
CREATE TABLE `m_permission` (
`id` int NOT NULL AUTO_INCREMENT COMMENT ' 权限主键',
`method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '请求的方法',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '接口名称',
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '接口地址',
`sign` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '接口签名',
`status` int NOT NULL DEFAULT 0 COMMENT '接口状态0.启用1.停用)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`) USING BTREE,
INDEX `url_path`(`path` ASC) USING BTREE COMMENT '权限连接',
INDEX `idx_deleted` (`is_deleted`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '权限表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_permission
-- ----------------------------
-- ----------------------------
-- Table structure for m_role
-- ----------------------------
DROP TABLE IF EXISTS `m_role`;
CREATE TABLE `m_role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '角色主键',
`role_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名称',
`role_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色编号',
`status` int NOT NULL DEFAULT 0 COMMENT '角色状态0.启用1.停用)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_deleted` (`is_deleted`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_role
-- ----------------------------
INSERT INTO `m_role` VALUES (1, '超级管理员', 'system_admin', 0, '2025-04-23 22:31:03', 'admin', '2025-04-23 22:31:03',null, 0);
INSERT INTO `m_role` VALUES (2, '管理员', 'admin', 0, '2025-04-23 22:31:03', 'admin', '2025-04-23 22:31:03',null, 0);
INSERT INTO `m_role` VALUES (3, '用户', 'user', 0, '2025-04-23 22:31:03', 'admin', '2025-04-23 22:31:03',null, 0);
INSERT INTO `m_role` VALUES (4, '游客', 'visitor', 0, '2025-04-23 22:31:03', 'admin', '2025-04-23 22:31:03',null, 0);
-- ----------------------------
-- Table structure for m_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `m_role_menu`;
CREATE TABLE `m_role_menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` int NOT NULL COMMENT '角色id',
`menu_id` int NOT NULL COMMENT '导航id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`) USING BTREE,
INDEX idx_r_m (role_id, menu_id, is_deleted) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色导航关系表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_role_menu
-- ----------------------------
-- ----------------------------
-- Table structure for m_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `m_role_permission`;
CREATE TABLE `m_role_permission` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`permission_id` int NOT NULL COMMENT '权限id',
`role_id` int NOT NULL COMMENT '角色id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`) USING BTREE,
INDEX idx_r_p (role_id, permission_id, is_deleted) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色权限关系表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_role_permission
-- ----------------------------
-- ----------------------------
-- Table structure for m_storage
-- ----------------------------
DROP TABLE IF EXISTS `m_storage`;
CREATE TABLE `m_storage` (
`id` int NOT NULL AUTO_INCREMENT,
`storage_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '对象存储服务商目前支持阿里云、minio和本地',
`storage_type` int NULL DEFAULT NULL COMMENT '对象存储服务的类型0.local1.minio2.oss',
`storage_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '对象存储服务商名称',
`host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '外链访问地址',
`endpoint` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'API访问地址',
`access_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '账号或者用户识别码',
`secret_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密钥',
`bucket_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '存储桶名称',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'icon的url链接',
`forced_path_mode` int UNSIGNED NULL DEFAULT 0 COMMENT '是否强制使用路径模式1.使用2.不使用-默认)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_deleted` (`is_deleted`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '存储桶表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_storage
-- ----------------------------
-- ----------------------------
-- Table structure for m_setting
-- ----------------------------
DROP TABLE IF EXISTS `m_setting`;
CREATE TABLE `m_setting` (
`id` int NOT NULL AUTO_INCREMENT,
`code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '配置编号',
`config_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '配置名称',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '配置描述',
`value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '配置的值',
`status` int NULL DEFAULT NULL DEFAULT 0 COMMENT '是否启用0.启用1.停用)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_deleted` (`is_deleted`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统设置表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_setting
-- ----------------------------
-- ----------------------------
-- Table structure for m_user
-- ----------------------------
DROP TABLE IF EXISTS `m_user`;
CREATE TABLE `m_user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户昵称',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户头像',
`role` int NOT NULL DEFAULT 1 COMMENT '角色',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
`phone_number` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '电话号码',
`gender` tinyint NULL DEFAULT NULL COMMENT '性别(0.保密1.男2女)',
`status` int NOT NULL DEFAULT 0 COMMENT '用户状态0.正常1.封号)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`) USING BTREE,
INDEX `user_relo`(`role` ASC) USING BTREE,
INDEX `idx_deleted` (`is_deleted`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of m_user
-- ----------------------------
INSERT INTO `m_user` VALUES (1, 'admin', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918', '超级管理员', 'https://oss.moxiaoli.cn/image/avatar.jpg', 1, '1414212942@qq.com', '12345678901', 1, 0, '2025-04-23 22:31:03', 'admin', '2025-04-23 22:31:03',null, 0);
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,18 @@
package com.mikufufu;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
@MapperScan("com.mikufufu.mapper")
public class AdminApplication {
public static void main(String[] args) {
System.out.println("项目启动!!");
SpringApplication.run(AdminApplication.class, args);
}
}

View File

@@ -0,0 +1,10 @@
package com.mikufufu.common.annotation;
import java.lang.annotation.*;
// 注解位置设置为类成员变量或方法参数上
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Account {
}

View File

@@ -0,0 +1,21 @@
package com.mikufufu.common.annotation;
import java.lang.annotation.*;
/**
* 一个用于方法级别的注解,标识该方法具有匿名特性。
* 这个注解适用于那些在运行时希望被识别为“匿名”的方法。
* </br>
* {@code @Target} 指定此注解可以用于什么地方,这里限定为方法(ElementType.METHOD)。
* {@code @Retention} 指定此注解的生命周期,这里为运行时(RetentionPolicy.RUNTIME)。
* {@code @Documented} 指定是否将此注解包含在JavaDoc中默认为false。
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AnonymousApi {
String description() default "";
}

View File

@@ -0,0 +1,28 @@
package com.mikufufu.common.annotation;
import com.mikufufu.common.enums.ModuleType;
import com.mikufufu.common.enums.OperationType;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
/**
* 功能模块名称
*/
ModuleType module() default ModuleType.OTHER;
/**
* 操作类型 {@link OperationType} 默认 插入
*/
OperationType type() default OperationType.INSERT;
/**
* 操作描述
*/
String description() default "";
}

View File

@@ -0,0 +1,30 @@
package com.mikufufu.common.constant;
public interface Constant {
Integer YES = 1;
Integer NO = 0;
/**
* 网站标识的常量键名
*/
String WEB_SITE_CODE = "website";
/**
* 存储设置标识的常量键名
*/
String STORAGE_SETTING_CODE = "storage";
/**
* 系统邮件设置标识的常量键名
*/
String SYS_EMAIL_SETTING_CODE = "sys_email";
/**
* 顶级父导航id
*/
Integer TOP_PARENT_ID = 0;
}

View File

@@ -0,0 +1,10 @@
package com.mikufufu.common.constant;
public interface HttpMethod {
String GET = "GET";
String POST = "POST";
String PUT = "PUT";
String DELETE = "DELETE";
String OPTIONS = "OPTIONS";
}

View File

@@ -0,0 +1,53 @@
package com.mikufufu.common.constant;
/**
* Redis常量
*
*/
public interface RedisKey {
/**
* 系统设置缓存前缀
*/
String SETTING = "setting:";
/**
* 用户缓存前缀
*/
String USER = "user:";
/**
* 角色缓存前缀
*/
String ROLE = "role:";
/**
* 权限缓存前缀
*/
String PERMISSION = "permission:";
/**
* 存储服务商缓存前缀
*/
String STORAGE = "storage:";
/**
* 菜单缓存前缀
*/
String MENU = "menu:";
/**
* token缓存前缀
*/
String TOKEN = "token:";
/**
* 邮件验证码缓存前缀
*/
String EMAIL_CODE = "mail:code:";
/**
* 邮件验证码过期时间缓存前缀
*/
String EMAIL_CODE_EXPIRATION_TIME = EMAIL_CODE + "expiration:time:";
}

View File

@@ -0,0 +1,50 @@
package com.mikufufu.common.constant;
/**
* RegexStr接口定义了一系列常用的正则表达式字符串
* 用于匹配不同的数据格式例如手机号、电子邮件、URL和密码。
*/
public interface RegexStr {
/**
* 手机号正则表达式仅匹配中国手机号格式为13x-18x开头的11位数字
*/
String MOBILE = "^1[3|4|5|7|8][0-9]\\d{8}$";
/**
* 电子邮件正则表达式,匹配标准电子邮件格式
*/
String EMAIL = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
/**
* 密码正则表达式匹配8-16位字母、数字和下划线、英文句号、斜线组成的密码
*/
String PASSWORD = "^[a-zA-Z0-9\\._/]{8,16}$";
/**
* 邮政编码正则表达式匹配6位数字
*/
String POST_CODE = "^[1-9]\\d{5}$";
/**
* 身份证正则表达式匹配15位或18位数字或字母
*/
String ID_CARD = "^[1-9]\\d{5}(?:\\d|[xX])$";
/**
* URL正则表达式匹配http、ftp或file协议的URL地址
*/
String URL = "^(https?|ftp|file):/**[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]";
/**
* 定义QQ号的正则表达式QQ号必须以非零数字开头长度在5到11位之间
*/
String QQ = "^[1-9][0-9]{4,10}$";
/**
* 域名
*/
String DOMAIN = "^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}$";
}

View File

@@ -0,0 +1,166 @@
package com.mikufufu.common.entity;
import com.mikufufu.common.enums.ResultCode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 通用请求消息结果封装类
*
*/
@Schema(description = "通用请求消息结果封装类")
@Data
public class AjaxResult<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
@Schema(description = "状态码")
private Integer code;
/**
* 返回消息
*/
@Schema(description = "返回消息")
private String msg;
/**
* 返回数据
*/
private T data;
public AjaxResult(Integer code,String msg){
this.code = code;
this.msg = msg;
}
public AjaxResult(Integer code,String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public AjaxResult(ResultCode resultCode) {
this.code = resultCode.getCode();
this.msg = resultCode.getMessage();
}
/**
* 返回成功消息
* @return 成功消息
*/
public static <T> AjaxResult<T> success(){
return new AjaxResult<>(ResultCode.SUCCESS);
}
/**
* 返回成功消息
* @param msg 返回内容
* @return 成功消息
*/
public static <T> AjaxResult<T> success(String msg){
return new AjaxResult<>(ResultCode.SUCCESS.getCode(),msg);
}
/**
* 返回成功消息
* @param data 数据对象
* @return 成功消息
*/
public static <T> AjaxResult<T> data(T data){
return AjaxResult.data(ResultCode.SUCCESS.getMessage(),data);
}
/**
* 返回成功消息
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static <T> AjaxResult<T> data(String msg, T data){
return AjaxResult.data(ResultCode.SUCCESS.getCode(),msg,data);
}
/**
* 返回成功消息
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static <T> AjaxResult<T> data(Integer code,String msg, T data){
return new AjaxResult<>(code,data == null ? ResultCode.NOT_DATA.getMessage(): msg,data);
}
/**
* 返回错误消息
* @return 错误消息
*/
public static <T> AjaxResult<T> error(){
return new AjaxResult<>(ResultCode.FAIL);
}
/**
* 返回错误消息
* @param msg 返回内容
* @return 错误消息
*/
public static <T> AjaxResult<T> error(Integer code,String msg){
return new AjaxResult<>(code,msg);
}
/**
* 返回错误消息
* @param msg 返回内容
* @return 错误消息
*/
public static <T> AjaxResult<T> error(String msg){
return AjaxResult.error(ResultCode.FAIL.getCode(),msg);
}
/**
* 返回错误消息
* @param code 错误码
* @param msg 错误内容
* @param data 数据对象
* @return 错误消息
*/
public static <T> AjaxResult<T> error(Integer code,String msg, T data){
return new AjaxResult<>(code,msg,data);
}
/**
* 返回错误消息
*
* @param msg 错误内容
* @param data 数据对象
* @return 错误消息
*/
public static <T> AjaxResult<T> error(String msg, T data){
return AjaxResult.error(ResultCode.FAIL.getCode(),msg,data);
}
/**
* 返回成功或者失败消息的通用方法
* @param flag 返回内容
* @return 成功与失败状态消息
*/
public static <T> AjaxResult<T> status(Boolean flag){
return flag? AjaxResult.success() : AjaxResult.error();
}
/**
* 返回成功或者失败消息的通用方法
*
* @param flag 返回内容
* @return 成功与失败状态消息
*/
public static <T> AjaxResult<T> status(Boolean flag,String success,String error){
return flag? AjaxResult.success(success) : AjaxResult.error(error);
}
}

View File

@@ -0,0 +1,64 @@
package com.mikufufu.common.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 实体类基础字段定义
*
* 包含数据库表中共用的审计字段和逻辑删除标识,
* 使用MyBatis-Plus注解实现字段自动填充和逻辑删除功能
*/
@Data
public class BaseEntity {
/**
* 创建时间
* {@code @TableField} 配置:
* value = "create_time" 映射数据库字段名
* fill = FieldFill.INSERT 仅在INSERT操作时自动填充
*/
@TableField(value = "create_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
/**
* 创建人
* {@code @TableField} 配置:
* fill = FieldFill.INSERT 仅在INSERT操作时自动填充
*/
@TableField(value = "create_by")
private String createBy;
/**
* 最后更新时间
* {@code @TableField} 配置:
* fill = FieldFill.INSERT_UPDATE 在INSERT和UPDATE操作时自动填充
*/
@TableField(value = "update_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
/**
* 最后更新人
* {@code @TableField} 配置:
* fill = FieldFill.INSERT_UPDATE 在INSERT和UPDATE操作时自动填充
*/
@TableField(value = "update_by")
private String updateBy;
/**
* 逻辑删除标识
* {@code @TableLogic} 配置逻辑删除规则:
* value = "0" 表示未删除状态
* delval = "1" 表示已删除状态
* {@code @TableField} 显式映射数据库字段名为is_deleted
*/
@TableLogic(value = "0", delval = "1")
@TableField("is_deleted")
private Integer isDeleted;
}

View File

@@ -0,0 +1,19 @@
package com.mikufufu.common.entity;
import com.mikufufu.modules.system.model.entity.OperateLog;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* 定义日志事件
*/
@Getter
public class LogEvent extends ApplicationEvent {
private final OperateLog operateLog;
public LogEvent(OperateLog operateLog){
super(operateLog);
this.operateLog = operateLog;
}
}

View File

@@ -0,0 +1,54 @@
package com.mikufufu.common.entity;
import lombok.Data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 一个改进后的Node类提供了更安全、更易于维护的结构。
* 对于泛型T和E建议子类在使用时确保类型安全。
* 这个类是线程不安全的,如果需要在多线程环境中使用,请在子类中实现适当的同步机制。
*
*
* @param <T> 节点类型标识,用于区分不同类型的节点
* @param <E> 父节点标识类型,确保与节点类型区分,避免数据一致性问题
*/
@Data
public abstract class Node<T, E> {
/**
* 节点ID
*/
private E id;
/**
* 父节点ID
*/
private E parentId;
/**
* 子节点列表
*/
private List<Node<T, E>> children = Collections.synchronizedList(new ArrayList<>());
/**
* 添加一个子节点。
* @param child 要添加的子节点。
* @return 如果添加成功返回true如果子节点已存在返回false。
*/
public boolean addChild(Node<T, E> child) {
return children.add(child);
}
/**
* 移除一个子节点。
* @param child 要移除的子节点。
* @return 如果成功移除返回true如果子节点不存在返回false。
*/
public boolean removeChild(Node<T, E> child) {
return children.remove(child);
}
}

View File

@@ -0,0 +1,27 @@
package com.mikufufu.common.entity;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
/**
* 分页查询通用实体类
*
*/
@Data
public class Query {
/**
* 当前页码
*/
private Integer current = 1;
/**
* 每页条数
*/
private Integer size =10;
public <T> IPage<T> getPage() {
return new Page<>(this.current, this.size);
}
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum Channel {
WEB(1, "用户端"),
ADMIN(2, "管理端")
;
private final int type;
private final String name;
}

View File

@@ -0,0 +1,26 @@
package com.mikufufu.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum HtmlTemplate {
EMAIL_CODE("email_code", "email_code.html", "邮件验证码");
/**
* 模板名称
*/
private final String template;
/**
* 模板文件名称
*/
private final String fileName;
/**
* 描述
*/
private final String subject;
}

View File

@@ -0,0 +1,31 @@
package com.mikufufu.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 功能模块类型
*/
@Getter
@AllArgsConstructor
public enum ModuleType {
/**
* 用户模块
*/
USER(1, "用户模块"),
/**
* 系统模块
*/
SYSTEM(2, "系统模块"),
/**
* 其他模块
*/
OTHER(0, "其他");
private final int type;
private final String name;
}

View File

@@ -0,0 +1,48 @@
package com.mikufufu.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum OperationType {
/**
* 插入
*/
INSERT(1,"插入"),
/**
* 更新
*/
UPDATE(2,"更新"),
/**
* 删除
*/
DELETE(3,"删除"),
/**
* 登录
*/
LOGIN(4,"登录"),
/**
* 注册
*/
REGISTER(5,"注册"),
/**
* 发送邮件
*/
SEND_EMAIL(6,"发送邮件"),
/**
* 其他
*/
OTHER(0,"其他"),
;
private final int type;
private final String name;
}

View File

@@ -0,0 +1,21 @@
package com.mikufufu.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ResultCode {
SUCCESS(200, "操作成功"),
FAIL(500, "操作失败"),
NOT_FOUND(404, "资源不存在"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
BAD_REQUEST(400, "请求参数错误"),
NOT_DATA(501, "暂无数据")
;
private final int code;
private final String message;
}

View File

@@ -0,0 +1,64 @@
package com.mikufufu.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 系统角色枚举
*
*/
@Getter
@AllArgsConstructor
public enum RoleCode {
/**
* 超级管理员
*/
ROLE_SYSTEM_ADMIN("system_admin", 1, "超级管理员"),
/**
* 管理员
*/
ROLE_ADMIN("admin",2, "管理员"),
/**
* 普通用户
*/
ROLE_USER("user",3, "普通用户"),
/**
* 游客
*/
ROLE_VISITOR("visitor", 4, "游客");
/**
* 角色编码
*/
private final String code;
/**
* 角色值
*/
private final Integer value;
/**
* 描述
*/
private final String desc;
/**
* 根据角色编码获取枚举
* @param code 角色编码
* @return 枚举
*/
public static RoleCode getRoleCodeByCode(String code) {
return Arrays.stream(RoleCode.values())
.filter(role -> role.getCode().equals(code))
.findFirst()
.orElse(null);
}
public static RoleCode getRoleCodeByValue(Integer value) {
return Arrays.stream(RoleCode.values())
.filter(role -> role.getValue().equals(value))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,43 @@
package com.mikufufu.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
@Getter
@AllArgsConstructor
public enum SexType {
UNKNOWN(0,"未知"),
MAN(1,""),
WOMAN(2,"");
private final Integer type;
private final String name;
/**
* 根据type获取sexEnum
* @param type 类型
* @return sexEnum {@link SexType}
*/
public static SexType getByType(Integer type) {
return Arrays.stream(SexType.values())
.filter(sex -> sex.getType().equals(type))
.findFirst()
.orElse(UNKNOWN);
}
/**
* 根据type获取name
* @param type 类型
* @return 类型名称
*/
public static String getNameByType(Integer type) {
return Arrays.stream(SexType.values())
.filter(sex -> sex.getType().equals(type))
.findFirst()
.orElse(UNKNOWN).getName();
}
}

View File

@@ -0,0 +1,32 @@
package com.mikufufu.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 文件类型枚举类
*
*/
@Getter
@AllArgsConstructor
public enum UploadFileType {
/**
* 图片
*/
IMAGE("image"),
/**
* 视频
*/
VIDEO("video"),
/**
* 音频
*/
AUDIO("music"),
/**
* 文件
*/
FILE("file");
private final String path;
}

View File

@@ -0,0 +1,33 @@
package com.mikufufu.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum UserStatus {
NORMAL(0, "正常"),
DISABLED(1, "禁用"),
LOCKED(2, "锁定"),
;
private final Integer code;
private final String name;
/**
* 根据code获取枚举
*
* @param code code
* @return 枚举
*/
public static String getName(int code) {
for (UserStatus value : UserStatus.values()) {
if (value.code == code) {
return value.name;
}
}
return null;
}
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.common.exception;
/**
* 认证异常
*
*/
public class AuthException extends RuntimeException{
/**
* 权限认证异常
* @param message 异常消息
*/
public AuthException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,133 @@
package com.mikufufu.common.permission;
import com.mikufufu.common.annotation.AnonymousApi;
import com.mikufufu.core.utils.SpringUtils;
import com.mikufufu.modules.system.model.entity.Permission;
import com.mikufufu.modules.system.model.vo.PermissionRoleVO;
import com.mikufufu.modules.system.service.IPermissionService;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
import java.util.*;
/**
* 权限处理类
*/
@Slf4j
public class PermissionHandle {
/**
* 存储权限角色列表的静态变量
*/
@Getter
private static List<PermissionRoleVO> permissionRoleList;
/**
* 存储匿名权限列表的静态变量
*/
@Getter
private static List<Permission> anonymousPermissionList = null;
public static void init(){
initAnonymousPermissionList();
loadDataSource();
}
/**
* 检查权限资源与权限的关系是否为空
* @return true:为空 false:不为空
*/
public static boolean permissionRoleListIsNull(){
return permissionRoleList == null || permissionRoleList.isEmpty();
}
/**
* 检查给定的URL和HTTP方法是否有权限访问。
* @param url 请求的URL
* @param method 请求的方法包括GET和POST等
* @return 如果请求的URL和方法有权限则返回true否则返回false。
*/
public static boolean hasAnonymousPermission(String url,String method){
log.info("检查权限资源与权限的关系:{} {}",url,method);
// Spring提供的用于匹配路径的匹配器
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 检查是否匹配任一匿名权限列表中的权限
return anonymousPermissionList.stream().anyMatch(permission -> antPathMatcher.match(permission.getPath(), url) && permission.getMethod().equalsIgnoreCase(method));
}
/**
* 初始化匿名权限列表。
* 遍历所有映射的请求处理方法,检查是否有@Anonymous注解若有则添加到匿名权限列表中。
* {@code @PostConstruct}注解,在类实例化后执行
*/
private static void initAnonymousPermissionList(){
List<Permission> permissionList = new ArrayList<>();
// 获取所有映射的处理方法
// @Qualifier("requestMappingHandlerMapping")
// 注入请求映射处理器映射
Map<RequestMappingInfo, HandlerMethod> handlerMethods = SpringUtils.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
// 遍历处理方法解析URL和请求方法并检查是否有@Anonymous注解
handlerMethods.forEach((k, v) -> {
Permission permission = new Permission();
// 解析URL
PathPatternsRequestCondition pathPatternsCondition = k.getPathPatternsCondition();
if (pathPatternsCondition == null){
return;
}
// 当前patterns中只有一个pattern所以直接取第一个即可
pathPatternsCondition.getPatterns().forEach(path -> permission.setPath(path.getPatternString()));
RequestMethodsRequestCondition methodsCondition = k.getMethodsCondition();
if (methodsCondition.getMethods().isEmpty()){
return;
}
// 解析请求方法
methodsCondition.getMethods().forEach(method -> permission.setMethod(method.name()));
Method method = v.getMethod();
// 检查是否有@Anonymous注解
if (method.isAnnotationPresent(AnonymousApi.class)){
permissionList.add(permission);
}
});
anonymousPermissionList = permissionList;
log.info("匿名权限列表:{}",anonymousPermissionList);
}
/**
* 加载权限资源与权限的关系
* {@code @PostConstruct} 注解用于在类被实例化后执行
*/
public static void loadDataSource() {
// 获取权限的Service
IPermissionService permissionService = SpringUtils.getBean(IPermissionService.class);
// 从permissionService获取角色权限列表
permissionRoleList = permissionService.getPermissionRoleList();
if (permissionRoleListIsNull()){
// 如果角色权限列表为空则调用permissionService的saveAllPermissionOfController方法写入所有权限
Boolean flag = permissionService.saveAllPermissionOfController();
// 如果写入权限成功则调用permissionService的grantAnonymousPermission方法写入web权限
if(flag){
permissionService.grantAnonymousPermission();
permissionRoleList = permissionService.getPermissionRoleList();
}
if (permissionRoleListIsNull()){
permissionRoleList = Collections.emptyList();
}
}
log.info("权限资源与权限的关系:{}", permissionRoleList);
}
/**
* 清除权限资源与权限的关系
*/
public static void clearDataSource() {
log.info("清除权限资源与权限的关系");
permissionRoleList = null;
}
}

View File

@@ -0,0 +1,24 @@
package com.mikufufu.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("logTaskExecutor")
public Executor logTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(100); // 队列容量
executor.setThreadNamePrefix("Log-Async-");
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,45 @@
package com.mikufufu.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.mikufufu.core.utils.AesUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
@Slf4j
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password; // 存储的是加密后的密码
@Bean
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
try {
dataSource.setUsername(AesUtils.decrypt(username));
}catch (Exception e){
log.error("数据库用户解密失败:{}",username);
dataSource.setUsername(username);
}
try {
dataSource.setPassword(AesUtils.decrypt(password)); // 解密密码
}catch (Exception e){
log.error("数据库密码解密失败:{}",password);
dataSource.setPassword(password);
}
return dataSource;
}
}

View File

@@ -0,0 +1,22 @@
package com.mikufufu.config;
import com.mikufufu.core.cache.SettingCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Configuration;
/**
* 初始化配置
* 修复Redis相关依赖和服务可能还未完全初始化完成导致初始化失败。
*/
@Slf4j
@Configuration
public class InitConfig implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
log.debug("初始化设置信息");
SettingCache.init();
}
}

View File

@@ -0,0 +1,45 @@
package com.mikufufu.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatisPlus配置类
*
*/
@Configuration
public class MyBatisPlusConfig {
/**
* 创建并配置MybatisPlusInterceptor用于分页拦截。
*
* @return MybatisPlusInterceptor 分页拦截器实例
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor(){
// 初始化MybatisPlusInterceptor
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 创建分页内部拦截器并配置
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 设置是否允许页数溢出,默认false意思是不允许
paginationInnerInterceptor.setOverflow(false);
// 设置最大限制条数为不限制
paginationInnerInterceptor.setMaxLimit(-1L);
// 设置数据库类型
paginationInnerInterceptor.setDbType(DbType.MYSQL);
// 将分页内部拦截器添加到MybatisPlusInterceptor中
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
// 添加阻止攻击拦截器
mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return mybatisPlusInterceptor;
}
}

View File

@@ -0,0 +1,53 @@
package com.mikufufu.config;
import com.mikufufu.core.serializer.FastJson2JsonRedisSerializer;
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.StringRedisSerializer;
/**
* redis配置
*
*/
@Configuration
public class RedisConfig{
/**
* 配置RedisTemplate
* <p>
* redisTemplate方法是一个@Bean方法@Bean注解括号中的参数用于指定RedisTemplate的名称。
* 用于创建一个{@link RedisTemplate}对象并配置它。
* 它接受一个{@link RedisConnectionFactory}参数作为Redis连接工厂。
* 在方法内部,它创建了一个{@link FastJson2JsonRedisSerializer}对象用于序列化和反序列化Redis的value值
* 并将其设置为RedisTemplate的值序列化器。
* 同时,它还使用{@link StringRedisSerializer}来序列化和反序列化Redis的key值和Hash的key值。
* 最后它调用afterPropertiesSet方法来初始化{@link RedisTemplate}对象,并返回它。
* </p>
* @param connectionFactory Redis连接工厂
* @return RedisTemplate对象
*/
@Bean("redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 创建一个RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置Redis连接工厂
template.setConnectionFactory(connectionFactory);
// 创建一个FastJson2JsonRedisSerializer对象用于序列化和反序列化Redis的value值
FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
// afterPropertiesSet方法来初始化RedisTemplate对象并返回它
template.afterPropertiesSet();
return template;
}
}

View File

@@ -0,0 +1,165 @@
package com.mikufufu.config;
import com.mikufufu.modules.auth.security.filter.IllegalRequestFilter;
import com.mikufufu.modules.auth.security.filter.TokenFilter;
import com.mikufufu.modules.auth.security.handler.AuthorizationManagerImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authorization.AuthorizationManager;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Stream;
/**
* Spring Security配置类
*
*/
@Slf4j
@Configuration // 表示这是一个配置类
@EnableWebSecurity // 启用Spring Web安全功能
@EnableMethodSecurity
public class SecurityConfig {
/**
* 认证失败处理类:用于处理认证失败的情况。
*/
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
/**
* 权限拒绝处理类:用于处理没有权限访问的情况。
*/
@Autowired
private AccessDeniedHandler accessDeniedHandler;
// 解决循环依赖的问题
@Bean
public AuthorizationManager<RequestAuthorizationContext> authorizationManager(){
return new AuthorizationManagerImpl();
}
/**
* 获取匿名访问的权限列表。
* 匿名权限即不需要用户登录即可访问的资源路径。
*
* @return 返回一个包含所有匿名权限路径的列表。
*/
private String[] anonymousGet(){
// 初始化匿名权限路径列表包括默认权限、Swagger文档权限
String[] defaultPermission = {"/cache/**","/logout"};
String[] swaggerPermission = {"/doc.html","/swagger-resources/**","/webjars/**","/v3/api-docs/**","/swagger-ui/**"};
// 合并权限列表,去除重复项
return Stream.of(defaultPermission, swaggerPermission)
.flatMap(Arrays::stream).distinct()
.toArray(String[]::new);
}
/**
* 静态资源路径
* 解决 ** 路径无法匹配的问题
* @return AntPathRequestMatcher[]
*/
private AntPathRequestMatcher[] staticResources(){
String[] staticPath = {"/","/*.html","/**/*.html","/**/*.js","/**/*.css","/favicon.ico","/image/**"};
// 创建一个AntPathRequestMatcher数组用于匹配静态资源路径
AntPathRequestMatcher[] antPathRequestMatchers = new AntPathRequestMatcher[staticPath.length];
// 遍历静态资源路径数组为每个路径创建一个AntPathRequestMatcher对象
for (int i = 0; i < staticPath.length; i++) {
antPathRequestMatchers[i] = new AntPathRequestMatcher(staticPath[i]);
}
// 返回AntPathRequestMatcher数组
return antPathRequestMatchers;
}
/**
* 配置CORS跨源资源共享设置以允许来自特定源的Web请求访问应用程序。
* 这个方法返回一个CorsConfigurationSource实例其中包含了对跨域请求的配置。
* @return UrlBasedCorsConfigurationSource 一个基于URL的CORS配置源用于指定哪些URL路径应用此CORS配置。
*/
@Bean
public CorsConfigurationSource configurationSource(){
// 创建一个新的CORS配置对象
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 设置允许的所有请求头
corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
// 设置允许的所有请求方法
corsConfiguration.setAllowedMethods(Collections.singletonList("*"));
// 设置允许的请求来源,这里指定了两个来源:本地开发环境和一个示例域名
// corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8080","https://www.mikufufu.com"));
corsConfiguration.addAllowedOrigin("*");
// 设置预检请求OPTIONS请求的缓存时间单位为秒
corsConfiguration.setMaxAge(3600L);
// 创建一个新的URL基于的CORS配置源
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 将之前配置的CORS配置应用到所有URL路径
source.registerCorsConfiguration("/**",corsConfiguration);
// 返回配置源
return source;
}
/**
* 配置安全过滤链
* @param http 用于配置HttpSecurity的bean
* @return 返回配置好的SecurityFilterChain
* @throws Exception 配置过程中可能抛出的异常
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置CORS跨域资源共享
.cors(cors -> cors.configurationSource(configurationSource()))
// 禁用CSRF保护
.csrf(AbstractHttpConfigurer::disable)
// 禁用会话创建
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 禁用security默认的注销接口以解决重定向到login页面时因为没有登录所以会重定向到登录页面但是登录页面没有权限所以会抛出异常。
.logout(AbstractHttpConfigurer::disable)
// 配置请求过滤规则
.authorizeHttpRequests(authorize -> authorize
// 放行静态资源
.requestMatchers(staticResources()).permitAll()
// 允许GET请求和POST请求
.requestMatchers(HttpMethod.GET, anonymousGet()).permitAll()
.requestMatchers(HttpMethod.POST, "/login","/admin/login","/register").permitAll()
// 使用自定义的权限管理器进行权限检查
.anyRequest().access(authorizationManager())
)
.addFilterBefore(new TokenFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new IllegalRequestFilter(),TokenFilter.class);
// 允许iframe访问
http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable));
http.exceptionHandling(exceptionHandling ->
exceptionHandling
// 认证失败处理
.authenticationEntryPoint(authenticationEntryPoint)
// 权限不足处理
.accessDeniedHandler(accessDeniedHandler));
// 构建并返回安全过滤链
return http.build();
}
}

View File

@@ -0,0 +1,169 @@
package com.mikufufu.core.aop;
import com.alibaba.fastjson2.JSON;
import com.mikufufu.common.annotation.Account;
import com.mikufufu.common.annotation.OperationLog;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.common.entity.LogEvent;
import com.mikufufu.common.enums.OperationType;
import com.mikufufu.core.utils.IpUtil;
import com.mikufufu.modules.auth.utils.AuthUtils;
import com.mikufufu.modules.system.model.entity.OperateLog;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Slf4j
@Aspect
@Component
public class LogAspect {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Pointcut("@annotation(com.mikufufu.common.annotation.OperationLog)")
public void operationLogPointCut() {
}
@AfterReturning(value = "operationLogPointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String[] paramNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();
OperationLog operationLog = method.getAnnotation(OperationLog.class);
OperateLog operateLog = new OperateLog();
setOperateLog(operationLog,method,paramNames,args,operateLog);
if (Objects.nonNull(result) && result instanceof AjaxResult){
AjaxResult<?> ajaxResult = (AjaxResult<?>) result;
if (ajaxResult.getCode() == 200){
operateLog.setStatus(1);
}else{
OperationType type = operationLog.type();
if (type == OperationType.LOGIN){
return;
}
operateLog.setStatus(0);
operateLog.setErrorMessage(ajaxResult.getMsg());
}
}
eventPublisher.publishEvent(new LogEvent(operateLog));
}
/**
* 设置操作日志
* @param operateLog 操作日志
*/
private void setOperateLog(OperationLog operationLog, Method method, String[] paramNames,Object[] args,OperateLog operateLog) {
HttpServletRequest request = getRequest();
if (request == null){
throw new RuntimeException("获取请求失败");
}
OperationType type = operationLog.type();
operateLog.setModule(operationLog.module().getType());
operateLog.setUrl(request.getRequestURI());
setParam(operateLog, method,paramNames,args);
if (OperationType.LOGIN.equals(type)){
setLoginLog(operateLog,args);
}
operateLog.setOperator(AuthUtils.getUsername());
operateLog.setIp(IpUtil.getIpAddress(request));
operateLog.setOperateType(type.getType());
operateLog.setOperateTime(LocalDateTime.now());
operateLog.setRemark(operationLog.description());
}
/**
* 设置操作日志中的登录用户信息
*
* @param operateLog 操作日志对象,用于设置操作员信息
* @param args 方法参数数组从中查找带有Account注解的字段来获取操作员信息
*/
private void setLoginLog(OperateLog operateLog, Object[] args) {
// 遍历方法参数查找带有Account注解的字段
for(Object arg : args){
Field[] fields = arg.getClass().getDeclaredFields();
for (Field field : fields) {
// 判断字段是否带有Account注解
if (field.isAnnotationPresent(Account.class)){
try {
field.setAccessible(true);
operateLog.setOperator((String) field.get(arg));
return;
}catch (Exception e){
log.error("获取参数失败",e);
operateLog.setOperator("");
return;
}
}
}
}
}
/**
* 设置参数
* @param operateLog 操作日志
* @param method 方法
* @param paramNames 参数名
* @param args 参数值
*/
private void setParam(OperateLog operateLog,Method method,String[] paramNames, Object[] args) {
// 如果参数为空,则直接返回
if (args.length == 0){
return;
}
// 如果只有一个参数且是RequestBody注解则直接返回
if (args.length == 1 && AnnotationUtils.findAnnotation(method.getParameters()[0], RequestBody.class) != null){
operateLog.setParams(JSON.toJSONString(args[0]));
return;
}
// 遍历参数将参数名和参数值存入map中
Map<String, Object> params = new HashMap<>();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof MultipartFile) {
continue;
}
if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse) {
continue;
}
if (args[i] == null || args[i] instanceof byte[]) {
continue;
}
params.put(paramNames[i], args[i]);
}
operateLog.setParams(JSON.toJSONString(params));
}
private HttpServletRequest getRequest() {
// 从当前请求中获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
return ((ServletRequestAttributes) requestAttributes).getRequest();
}
return null;
}
}

View File

@@ -0,0 +1,82 @@
package com.mikufufu.core.aop;
import com.alibaba.fastjson2.JSON;
import com.mikufufu.common.entity.AjaxResult;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Aspect
@Component
@Order(1) // 数值越小优先级越高
public class WebAspect {
@Pointcut("execution(* com.mikufufu.modules..controller.*.*(..))")
public void controllerLog() {
}
@Around("controllerLog()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 获取方法签名
MethodSignature signature = (MethodSignature) point.getSignature();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return point.proceed();
}
HttpServletRequest request = requestAttributes.getRequest();
String url = request.getRequestURI();
log.info("请求{} :{}", request.getMethod(),url);
try {
// 获取参数类型数组Class类型
Class<?>[] paramTypes = signature.getParameterTypes();
// 获取参数值数组
Object[] args = point.getArgs();
String[] paramNames = signature.getParameterNames();
for (int i = 0; i < args.length; i++) {
// 如果是MultipartFile类型则打印文件名
if (args[i] instanceof MultipartFile file) {
log.info("上传文件 ({}){} 文件名{}", paramTypes[i].getSimpleName(), paramNames[i], file.getOriginalFilename());
continue;
}
// 忽略HttpServletRequest和HttpServletResponse
if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse) {
continue;
}
// 忽略null和byte[]
if (args[i] == null || args[i] instanceof byte[]) {
log.info("入参值为空或者是字节流 ({}){}", paramTypes[i].getSimpleName(), paramNames[i]);
continue;
}
log.info("入参 ({}){}{}", paramTypes[i].getSimpleName(), paramNames[i], JSON.toJSONString(args[i]));
}
} catch (Exception e) {
log.error("接口{}入参解析失败", url, e);
}
long startTime = System.currentTimeMillis();
Object result = point.proceed();
long endTime = System.currentTimeMillis();
try {
if (result instanceof AjaxResult<?>) {
log.info("通用接口{}出参:{}", url, JSON.toJSONString(result));
}
} catch (Exception e) {
log.error("接口{}出参解析失败", url, e);
}
log.info("接口{}执行结束,耗时{}ms", url, endTime - startTime);
return result;
}
}

View File

@@ -0,0 +1,322 @@
package com.mikufufu.core.cache;
import com.alibaba.fastjson2.JSON;
import com.mikufufu.core.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* RedisCache类用于提供通用的Redis缓存功能。
* 该类封装了与Redis缓存服务器交互的基本操作以便于在应用程序中方便地使用Redis作为缓存机制。
*
*/
@Slf4j
public class RedisCache {
/**
* 将指定的键值对存储到Redis中。
* @param key 键
* @param value 值
*/
public static <T> void set(String key, T value){
log.info("新增缓存 key={}, value={}", key, JSON.toJSONString(value));
RedisUtils.set(key, value);
}
/**
* 将指定的键值对存储到Redis中并设置过期时间。
* @param key 键
* @param value 值
* @param time 过期时间 单位秒
*/
public static <T> void set(String key, T value, long time){
log.info("新增缓存 key={}, value={}, time={}", key, JSON.toJSONString(value), time);
RedisUtils.set(key, value, time);
}
/**
* 将指定的键值对存储到Redis中并设置过期时间单位可指定
* @param key 键
* @param value 值
* @param time 过期时间
* @param timeUnit 时间单位
*/
public static <T> void set(String key, T value, long time, TimeUnit timeUnit){
log.info("新增缓存 key={}, value={}, time={}, timeUnit={}", key, JSON.toJSONString(value), time, timeUnit);
RedisUtils.set(key, value, time, timeUnit);
}
/**
* 将列表类型的价值存储到指定的键。
* @param key 键
* @param value 值列表
*/
public static <T> long setList(String key, List<T> value){
log.info("新增缓存 key={}, value={}", key, JSON.toJSONString(value));
return RedisUtils.setList(key, value);
}
/**
* 将一个Set集合存储到缓存中。
*
* @param key 缓存的键类型为String。
* @param value 要存储的Set集合集合元素的类型为泛型T。
* 通过日志记录存储操作将key和value序列化后记录在日志中。
* 使用RedisUtils的setSet方法将key-value对存储到Redis的Set中。
*/
public static <T> void setSet(String key, Set<T> value){
log.info("新增缓存 key={}, value={}", key, JSON.toJSONString(value));
RedisUtils.setSet(key, value);
}
/**
* 向已存在的Set缓存中添加一个元素。
*
* @param key 缓存的键类型为String。
* @param value 要添加到Set中的元素元素的类型为泛型T。
* @return 返回操作的结果类型为long。如果添加成功返回新的Set集合大小如果添加失败返回-1。
* 通过日志记录添加操作将key和value序列化后记录在日志中。
* 使用RedisUtils的addSet方法向Redis的Set中添加元素并返回新的Set大小。
*/
public static <T> long addSet(String key, T value){
log.info("新增缓存 key={}, value={}", key, JSON.toJSONString(value));
return RedisUtils.addSet(key, value);
}
/**
* 通过键获取存储的值,需要指定值的类型。
* @param key 键
* @param clazz 值的类型
* @return 类型转换后的值
*/
public static <T> T get(String key, Class<T> clazz){
log.info("获取缓存 key={}, clazz={}", key, clazz.getName());
return RedisUtils.get(key, clazz);
}
/**
* 通过键获取存储的值,返回原生类型。
* @param key 键
* @return 存储的值
*/
public static Object get(String key){
log.info("获取缓存 key={}", key);
return RedisUtils.get(key);
}
/**
* 通过键获取存储的字符串类型值。
* @param key 键
* @return 字符串类型的值
*/
public static String getString(String key){
log.info("获取缓存 key={}", key);
return RedisUtils.get(key, String.class);
}
/**
* 通过键获取存储的整数类型值。
* @param key 键
* @return 整数类型的值
*/
public static Integer getInteger(String key){
log.info("获取缓存 key={}", key);
return RedisUtils.get(key, Integer.class);
}
/**
* 通过键获取存储的长整数类型值。
* @param key 键
* @return 长整数类型的值
*/
public static Long getLong(String key){
log.info("获取缓存 key={}", key);
return RedisUtils.get(key, Long.class);
}
/**
* 通过键获取存储的列表类型值。
* @param key 键
* @return 列表类型的值
*/
public static <T> List<T> getList(String key){
log.info("获取缓存 key={}", key);
return RedisUtils.getList(key);
}
public static <T> Set<T> getSet(String key){
log.info("获取缓存 key={}", key);
return RedisUtils.getSet(key);
}
/**
* 删除指定的键及其对应的值。
* @param key 键
* @return 删除操作是否成功
*/
public static Boolean delete(String key){
log.info("删除缓存 key={}", key);
return RedisUtils.delete(key);
}
/**
* 批量删除指定的键。
* @param keys 键集合
* @return 删除操作是否成功
*/
public static Boolean delete(Collection<String> keys){
log.info("删除缓存 keys={}", keys);
return RedisUtils.delete(keys);
}
/**
* 检查指定的键是否存在。
* @param key 键
* @return 键是否存在
*/
public static Boolean exists(String key){
log.info("检查缓存 key={}", key);
return RedisUtils.exists(key);
}
/**
* 获取当前数据库中的所有键。
* @return 键列表
*/
public static List<String> keys(){
log.info("获取缓存所有键");
return RedisUtils.keys();
}
/**
* 根据模式匹配获取键列表。
* @param pattern 模式
* @return 符合模式的键列表
*/
public static List<String> keys(String pattern){
log.info("获取缓存键列表 pattern={}", pattern);
return RedisUtils.keys(pattern);
}
/**
* 根据前缀删除键。
* @param prefix 键的前缀
* @return 删除操作是否成功
*/
public static Boolean deleteByPrefix(String prefix){
log.info("删除缓存前缀 prefix={}", prefix);
return RedisUtils.deleteByPrefix(prefix);
}
/**
* 清空Redis缓存。
* @return 清空操作是否成功
*/
public static Boolean flushAll(){
log.info("清空缓存");
return RedisUtils.deleteCache();
}
/**
* 在Redis中放入哈希表数据
*
* @param key 哈希表的键
* @param hashKey 哈希表中的字段键
* @param value 哈希表中的字段值
* @param <T> 字段值的类型
*/
public static <T> void putHash(String key, String hashKey, T value) {
RedisUtils.putHash(key, hashKey, value);
}
/**
* 在Redis中放入哈希表数据
*
* @param key 哈希表的键
* @param map 包含多个字段键值对的映射
* @param <T> 字段值的类型
*/
public static <T> void putHash(String key, Map<String, T> map) {
RedisUtils.putHash(key, map);
}
/**
* 从Redis中获取哈希表中的特定字段值
*
* @param key 哈希表的键
* @param hashKey 哈希表中的字段键
* @param <T> 字段值的类型
* @return 字段值如果不存在则返回null
*/
public static <T> T getHash(String key, String hashKey) {
return RedisUtils.getHash(key, hashKey);
}
/**
* 从Redis中获取整个哈希表的数据
*
* @param key 哈希表的键
* @param <T> 字段值的类型
* @return 包含所有字段键值对的映射
*/
public static <T> Map<String, T> getHash(String key) {
return RedisUtils.getHash(key);
}
/**
* 获取哈希表中的所有字段键
*
* @param key 哈希表的键
* @return 包含所有字段键的集合
*/
public static Set<String> getAllHash(String key){
return RedisUtils.getAllHash(key);
}
/**
* 获取哈希表中的所有字段值
*
* @param key 哈希表的键
* @param <T> 字段值的类型
* @return 包含所有字段值的列表
*/
public static <T> List<T> getAllHashValue(String key) {
return RedisUtils.getAllHashValue(key);
}
/**
* 删除Redis中的哈希表
*
* @param key 哈希表的键
* @return 如果哈希表存在且删除成功返回true否则返回false
*/
public static Boolean deleteHash(String key) {
return RedisUtils.deleteHash(key);
}
/**
* 删除哈希表中的一个或多个字段
*
* @param key 哈希表的键
* @param hashKeys 要删除的字段键数组
* @return 如果字段存在且删除成功返回true否则返回false
*/
public static Boolean deleteHash(String key, String... hashKeys) {
return RedisUtils.deleteHash(key, hashKeys);
}
/**
* 发送消息到Redis消息队列
* @param topic Redis消息队列的Topic
* @param msg 要发送的消息
*/
public static <T> void sendMsg(String topic, T msg) {
RedisUtils.sendMsg(topic,msg);
}
}

View File

@@ -0,0 +1,57 @@
package com.mikufufu.core.cache;
import com.mikufufu.common.constant.RedisKey;
import com.mikufufu.core.utils.RedisUtils;
import com.mikufufu.core.utils.SpringUtils;
import com.mikufufu.modules.system.model.entity.Setting;
import com.mikufufu.modules.system.service.ISettingService;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
@Slf4j
public class SettingCache {
private static final String SETTING_CACHE_KEY = RedisKey.SETTING;
public static void init(){
log.info("初始化系统设置缓存");
List<Setting> list = SpringUtils.getBean(ISettingService.class).list();
log.info("系统设置缓存初始化完成,共有 {} 条数据",list.size());
if (list.isEmpty()){
log.warn("系统设置缓存为空,请检查数据库");
return;
}
list.forEach(setting -> {
setSetting(setting.getCode(),setting.getValue());
});
}
public static void setSetting(String key,String value){
log.info("设置系统设置缓存:{} = {}",key,value);
RedisUtils.putHash(SETTING_CACHE_KEY,key,value);
}
public static String getSetting(String key){
log.info("获取系统设置缓存:{}",key);
return RedisUtils.getHash(SETTING_CACHE_KEY,key);
}
public static String getSetting(String key,String defaultValue){
log.info("获取系统设置缓存:{}",key);
if (RedisUtils.hasHashKey(SETTING_CACHE_KEY,key)){
return getSetting(key);
}
return defaultValue;
}
public static void flush(){
log.info("刷新系统设置缓存");
Map<String, String> cachedSettings = RedisUtils.getAllHashEntity(SETTING_CACHE_KEY);
if (!cachedSettings.isEmpty()) {
log.info("准备将 {} 条缓存数据写入数据库", cachedSettings.size());
SpringUtils.getBean(ISettingService.class).saveOrUpdateSetting(cachedSettings);
}
}
}

View File

@@ -0,0 +1,51 @@
package com.mikufufu.core.cache;
import com.alibaba.fastjson2.JSON;
import com.mikufufu.common.constant.RedisKey;
import com.mikufufu.core.utils.RedisUtils;
import com.mikufufu.core.utils.SpringUtils;
import com.mikufufu.modules.system.model.entity.User;
import com.mikufufu.modules.system.service.IUserService;
import lombok.extern.slf4j.Slf4j;
/**
* 用户缓存
*
*/
@Slf4j
public class UserCache {
private static final String USER_CACHE_KEY = RedisKey.USER;
public static void setUser(User user){
log.info("设置用户缓存={}", JSON.toJSONString(user));
RedisUtils.set(USER_CACHE_KEY + user.getId(), user);
}
public static User getUser(Integer userId) {
log.info("获取用户缓存={}", userId);
String key = USER_CACHE_KEY + userId;
if (RedisUtils.exists(key)) {
return RedisUtils.get(key, User.class);
}else {
log.info("用户缓存不存在,从数据库获取");
User user = SpringUtils.getBean(IUserService.class).getById(userId);
if (user == null) {
log.info("用户不存在");
throw new RuntimeException("用户不存在");
}
setUser(user);
return user;
}
}
public static Boolean delete(Integer userId) {
log.info("删除指定id的用户缓存={}", userId);
return RedisUtils.delete(USER_CACHE_KEY + userId);
}
public static Boolean flushAll() {
log.info("清空用户缓存");
return RedisUtils.deleteByPrefix(USER_CACHE_KEY);
}
}

View File

@@ -0,0 +1,172 @@
package com.mikufufu.core.exception;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.common.enums.ResultCode;
import com.mikufufu.common.exception.AuthException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
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.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* 全局异常处理
*
* <br>
* {@code @RestControllerAdvice}等价于{@code @ControllerAdvice}和{@code @ResponseBody}
* {@code @ControllerAdvice}用于全局异常处理,{@code @ResponseBody}返回json数据
*
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 全局异常处理
* @param request 请求对象
* @param e 异常对象
* @return 错误信息
*/
@ExceptionHandler(value = {Exception.class})
public AjaxResult<Void> exceptionHandler(HttpServletRequest request, Exception e) {
log.error("未知异常,请求地址:{}", request.getRequestURI(), e);
return AjaxResult.error(e.getMessage());
}
@ExceptionHandler(value = {AuthException.class})
public AjaxResult<Void> authExceptionHandler(HttpServletRequest request, AuthException e) {
log.error("权限异常,请求地址:{},错误信息:{}", request.getRequestURI(), e.getMessage());
return AjaxResult.error(ResultCode.FORBIDDEN.getCode(),e.getMessage());
}
@ExceptionHandler(value = {NullPointerException.class})
public AjaxResult<Void> nullPointExceptionHandler(HttpServletRequest request, NullPointerException e) {
log.error("空指针异常,请求地址:{}", request.getRequestURI(), e);
return AjaxResult.error("空指针异常");
}
/**
* 参数解析失败异常处理 如{@code @RequestBody}
* @param ex 异常对象
* @return 错误信息
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public AjaxResult<Void> handleHttpMessageNotReadableException(HttpServletRequest request,HttpMessageNotReadableException ex) {
log.error("参数解析失败异常,请求地址:{},错误信息:{}", request.getRequestURI(), ex.getMessage());
return AjaxResult.error("参数解析失败异常: " + ex.getMessage());
}
/**
* 参数校验异常处理
* @param e 异常对象
* @return 错误信息
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public AjaxResult<Map<String, Object>> handleValidationExceptions(HttpServletRequest request,MethodArgumentNotValidException e) {
String requestUri = request.getRequestURI();
log.error("参数校验失败异常, 请求地址: {}, 错误详情: {}", requestUri, e.getMessage());
Map<String, Object> errors = new HashMap<>();
e.getBindingResult().getAllErrors().forEach(error -> {
String errorMessage = error.getDefaultMessage();
String errorType = error.getClass().getSimpleName();
Map<String, String> fieldError = new HashMap<>();
fieldError.put("message", errorMessage);
fieldError.put("type", errorType);
if (error instanceof FieldError) {
String fieldName = ((FieldError) error).getField();
errors.put(fieldName, fieldError);
} else {
String objectName = error.getObjectName();
errors.put(objectName, fieldError);
}
});
return AjaxResult.error("参数校验异常处理",errors);
}
/**
* 参数验证失败异常处理 如{@code @Validated}
* @param request 请求对象
* @param e 错误对象
* @return 错误信息
*/
@ExceptionHandler(value = {BindException.class, ConstraintViolationException.class})
public AjaxResult<Void> handleMethodArgumentNotValidException(HttpServletRequest request, Exception e) {
String requestUri = request.getRequestURI();
log.error("参数验证失败异常, 请求地址: {}, 错误详情: {}", requestUri, e.getMessage(), e);
String message = "未知参数错误";
if (e instanceof BindException) {
BindingResult bindingResult = ((BindException) e).getBindingResult();
if (bindingResult.hasErrors()) {
message = Optional.ofNullable(bindingResult.getFieldError())
.map(FieldError::getDefaultMessage)
.orElse("未知参数错误");
}
} else if (e instanceof ConstraintViolationException) {
Set<ConstraintViolation<?>> violations = ((ConstraintViolationException) e).getConstraintViolations();
if (!violations.isEmpty()) {
message = violations.iterator().next().getMessage();
}
}
return AjaxResult.error("参数验证失败异常: " + message);
}
/**
* 缺少请求参数异常处理 如{@code @RequestParam}
* @param request 请求对象
* @param e 错误对象
* @return 错误信息
*/
@ExceptionHandler(value = {MissingServletRequestParameterException.class})
public AjaxResult<Void> missingServletRequestParameterExceptionHandler(HttpServletRequest request, MissingServletRequestParameterException e) {
log.error("缺少请求参数异常,请求地址:{},错误信息:{}", request.getRequestURI(), e.getMessage());
return AjaxResult.error("缺少请求参数异常: " + e.getMessage());
}
/**
* 请求方法不支持异常处理 如{@code @RequestMapping(method = RequestMethod.POST)}
* @param request 请求对象
* @param e 错误对象
* @return 错误信息
*/
@ExceptionHandler(value = {HttpRequestMethodNotSupportedException.class})
public AjaxResult<Void> httpRequestMethodNotSupportedExceptionHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException e) {
log.error("请求方法不支持异常,请求地址:{},错误信息:{}", request.getRequestURI(), e.getMessage());
return AjaxResult.error("请求方法不支持异常: " + e.getMessage());
}
/**
* 请求路径不存在异常处理
* @param request 请求对象
* @param e 错误对象
* @return 错误信息
*/
@ExceptionHandler(value = {NoHandlerFoundException.class})
public AjaxResult<Void> noHandlerFoundExceptionHandler(HttpServletRequest request, NoHandlerFoundException e) {
log.error("请求路径不存在异常,请求地址:{},错误信息:{}", request.getRequestURI(), e.getMessage());
return AjaxResult.error("请求路径不存在异常: " + e.getMessage());
}
@ExceptionHandler(RequestRejectedException.class)
public AjaxResult<Void> handleRequestRejectedException(RequestRejectedException ex, HttpServletRequest request) {
log.warn("Security firewall blocked request: {}", request.getRequestURI());
return AjaxResult.error("Security firewall blocked request: " + ex.getMessage());
}
}

View File

@@ -0,0 +1,23 @@
package com.mikufufu.core.listener;
import com.mikufufu.common.entity.LogEvent;
import com.mikufufu.modules.system.service.IOperateLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class LogEvenListener {
@Autowired
private IOperateLogService operateLogService;
@Async("logTaskExecutor") // 指定线程池
@EventListener
public void onApplicationEvent(LogEvent event) {
operateLogService.save(event.getOperateLog());
}
}

View File

@@ -0,0 +1,59 @@
package com.mikufufu.core.serializer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.StandardCharsets;
/**
* Redis使用FastJson序列化
*
*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
private final Class<T> clazz;
private static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(
"com.mikufufu.model.",
"com.mikufufu.modules.",
"com.mikufufu.common."
);
public FastJson2JsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
// 如果对象为空,则返回空字节数组
if (t == null)
{
return new byte[0];
}
// 使用FastJson将对象序列化为JSON字符串并转换为字节数组
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(StandardCharsets.UTF_8);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
// 如果字节数组为空或长度为0则返回空对象
if (bytes == null || bytes.length <= 0)
{
return null;
}
// 将字节数组转换为字符串
String str = new String(bytes, StandardCharsets.UTF_8);
// 使用FastJson将字符串反序列化为对象
try {
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER, JSONReader.Feature.FieldBased, JSONReader.Feature.SupportClassForName);
} catch (JSONException e) {
throw new SerializationException("将JSON字符串反序列化为对象时出错", e);
}
}
}

View File

@@ -0,0 +1,95 @@
package com.mikufufu.core.utils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AesUtils {
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String KEY = "MIKUFUFU1010MENG";
private static final String IV = "MOXIAOLIMIKUFUFU";
/**
* 使用默认密钥和IV加密明文数据
*
* @param plaintext 待加密的原始文本UTF-8编码
* @return Base64编码的加密结果字符串
* @throws Exception 当加密过程失败时抛出异常
*/
public static String encrypt(String plaintext) throws Exception {
return encrypt(plaintext, KEY, IV);
}
/**
* 使用默认密钥和IV解密密文数据
*
* @param ciphertext Base64编码的加密结果字符串
* @return 解密后的原始文本UTF-8编码
* @throws Exception 当解密过程失败时抛出异常
*/
public static String decrypt(String ciphertext) throws Exception {
return decrypt(ciphertext, KEY, IV);
}
/**
* 使用AES算法加密明文数据
*
* @param plaintext 待加密的原始文本UTF-8编码
* @param key AES密钥长度必须为16/24/32字节对应128/192/256位
* @param iv 初始化向量(IV)长度必须与算法块大小一致通常16字节
* @return Base64编码的加密结果字符串
* @throws Exception 当密钥不符合规范或加密过程失败时抛出异常
*/
public static String encrypt(String plaintext, String key, String iv) throws Exception {
validateKey(key);
// 初始化密钥材料和加密参数
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
// 配置加密器并执行加密操作
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* 解密AES加密的Base64字符串
*
* @param ciphertext Base64编码的加密字符串
* @param key AES密钥必须与加密时使用的密钥一致
* @param iv 初始化向量必须与加密时使用的IV一致
* @return 解密后的原始文本UTF-8编码
* @throws Exception 当密钥不符合规范或解密过程失败时抛出异常
*/
public static String decrypt(String ciphertext, String key, String iv) throws Exception {
validateKey(key);
// 准备解密所需的密钥材料和参数
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
// 配置解密器并执行解密流程
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] decoded = Base64.getDecoder().decode(ciphertext);
return new String(cipher.doFinal(decoded), StandardCharsets.UTF_8);
}
/**
* 验证AES密钥长度的有效性
* @param key 待验证的密钥字符串
* @throws IllegalArgumentException 当密钥长度不符合AES标准时抛出
*/
private static void validateKey(String key) {
if (key.length() != 16 && key.length() != 24 && key.length() != 32) {
throw new IllegalArgumentException("密钥长度必须为16/24/32字节");
}
}
}

View File

@@ -0,0 +1,52 @@
package com.mikufufu.core.utils;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.stream.Collectors;
/**
* Bean工具类用于复制对象属性值到目标对象。
*
*/
public class BeanUtil {
/**
* 复制对象属性值到目标对象。
*
* @param source 源对象,其属性值将被复制。
* @param target 目标对象,将接收源对象的属性值。
* @param <T> 目标对象的类型。
* @return 返回经过属性值复制后的目标对象。
*/
public static <T> T copy(Object source, T target) {
// 使用BeanUtils工具类的copyProperties方法将source对象的属性值复制到target对象
BeanUtils.copyProperties(source, target);
return target;
}
/**
* 将源对象复制到目标类型的新对象中。
*
* @param source 源对象,需要被复制的内容。
* @param target 目标对象的类型,用于创建新对象并复制源对象的内容。
* @return 复制后的新对象其类型为参数target指定的类型。
* @param <T> 目标对象的类型。
*/
public static <T> T copy(Object source, Class<T> target) {
return copy(source, BeanUtils.instantiateClass(target));
}
/**
* 将源对象列表复制到目标类型的新列表中。
*
* @param source 源对象列表,每个对象都需要被复制到目标类型的新对象中。
* @param target 目标对象的类型,用于创建新对象并复制源对象的内容。
* @return 复制后的新对象列表其中每个对象的类型为参数target指定的类型。
* @param <T> 目标对象的类型。
*/
public static <T> List<T> copyList(List<?> source, Class<T> target) {
// 通过流处理源对象列表,对每个对象进行复制,并收集到新的列表中
return source.stream().map(s -> copy(s, target)).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,64 @@
package com.mikufufu.core.utils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class CollUtils {
/**
* 转为一个可变 ListArrays.asList 不可变)
*/
public static <T> List<T> asList(T[] ary) {
if (ary == null) {
return null;
} else {
List<T> list = new ArrayList<>(ary.length);
Collections.addAll(list, ary);
return list;
}
}
/**
* 创建一个可变 List
* <code>@SafeVarargs</code>注解用于抑制编译器对可变参数数组的警告。由于Java泛型的类型擦除机制
* 使用泛型可变参数时编译器会警告可能存在类型安全问题。该注解告诉编译器这个方法内部不会对可变参数数组进行不安全的操作,可以安全地抑制相关警告。
*/
@SafeVarargs
public static <E> List<E> newArrayList(E... elements) {
if (elements == null) {
throw new NullPointerException();
} else {
List<E> list = new ArrayList<>(elements.length);
Collections.addAll(list, elements);
return list;
}
}
/**
* 创建一个新的ArrayList实例包含指定可迭代元素的所有元素
*
* @param elements 要添加到新列表中的可迭代元素可以为null
* @param <E> 列表元素的类型
* @return 包含所有指定元素的新ArrayList实例如果输入为null则返回null
*/
public static <E> List<E> newArrayList(Iterable<? extends E> elements) {
if (elements == null) {
throw new NullPointerException();
} else {
// 如果输入是Collection类型直接使用Collection构造函数创建ArrayList
if (elements instanceof Collection) {
return new ArrayList<>((Collection<? extends E>) elements);
} else {
// 如果输入是Iterable但不是Collection逐个添加元素
List<E> list = new ArrayList<>();
for (E e : elements) {
list.add(e);
}
return list;
}
}
}
}

View File

@@ -0,0 +1,162 @@
package com.mikufufu.core.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.function.Consumer;
public class EasyExcelExportUtil {
/**
* 通用 Excel 导出方法(适用于 Web 下载)
* @param response HttpServletResponse 对象
* @param fileName 文件名(无需后缀)
* @param data 数据集合
* @param head 表头定义List<List<String>> 结构)
* @param customStyle 自定义样式处理可为null
*/
public static <T> void exportToWeb(HttpServletResponse response,
String fileName,
List<T> data,
List<List<String>> head,
Consumer<ExcelWriter> customStyle) throws IOException {
configureResponse(response, fileName);
try (OutputStream outputStream = response.getOutputStream()) {
ExcelWriter excelWriter = EasyExcel.write(outputStream)
.head(head)
.registerWriteHandler(createDefaultStyleStrategy())
.build();
if (customStyle != null) {
customStyle.accept(excelWriter);
}
WriteSheet writeSheet = EasyExcel.writerSheet("Sheet1").build();
excelWriter.write(data, writeSheet);
excelWriter.finish();
}
}
/**
* 基于实体类注解的导出(自动生成表头)
* @param response HttpServletResponse
* @param fileName 文件名
* @param data 数据集合
* @param clazz 实体类类型
*/
public static <T> void exportByAnnotation(HttpServletResponse response,
String fileName,
List<T> data,
Class<T> clazz) throws IOException {
configureResponse(response, fileName);
EasyExcel.write(response.getOutputStream(), clazz)
.registerWriteHandler(createDefaultStyleStrategy())
.sheet("Sheet1")
.doWrite(data);
}
/**
* 写入本地文件(服务端生成文件)
* @param filePath 完整文件路径(包含后缀)
* @param data 数据集合
* @param head 表头定义
*/
public static <T> void exportToFile(String filePath,
List<T> data,
List<List<String>> head) {
EasyExcel.write(filePath)
.head(head)
.registerWriteHandler(createDefaultStyleStrategy())
.sheet("Sheet1")
.doWrite(data);
}
/**
* 带模板的导出(复杂报表场景)
* @param templatePath 模板路径
* @param outputStream 输出流
* @param data 填充数据
*/
public static void exportWithTemplate(String templatePath,
OutputStream outputStream,
Object data) {
EasyExcel.write(outputStream)
.withTemplate(templatePath)
.registerWriteHandler(createDefaultStyleStrategy())
.sheet()
.doFill(data);
}
// 创建默认样式策略(表头居中,内容左对齐)
private static HorizontalCellStyleStrategy createDefaultStyleStrategy() {
WriteCellStyle headStyle = new WriteCellStyle();
headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
WriteCellStyle contentStyle = new WriteCellStyle();
contentStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
return new HorizontalCellStyleStrategy(headStyle, contentStyle);
}
// 配置响应头信息
private static void configureResponse(HttpServletResponse response,
String fileName) throws IOException {
String encodedFileName = URLEncoder.encode(fileName, "UTF-8")
.replaceAll("\\+", "%20");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition",
"attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
}
/**
* 并发安全的大数据量导出(分页查询模式)
* @param response HttpServletResponse
* @param fileName 文件名
* @param head 表头
* @param pageSize 每页大小
* @param dataSupplier 分页数据提供函数(接收页码,返回数据列表)
*/
public static <T> void exportLargeData(HttpServletResponse response,
String fileName,
List<List<String>> head,
int pageSize,
PageDataSupplier<T> dataSupplier) throws IOException {
configureResponse(response, fileName);
try (OutputStream outputStream = response.getOutputStream()) {
ExcelWriter excelWriter = EasyExcel.write(outputStream)
.head(head)
.registerWriteHandler(createDefaultStyleStrategy())
.build();
WriteSheet writeSheet = EasyExcel.writerSheet("Sheet1").build();
int pageNo = 1;
while (true) {
List<T> data = dataSupplier.getPage(pageNo, pageSize);
if (data == null || data.isEmpty()) {
break;
}
excelWriter.write(data, writeSheet);
pageNo++;
}
excelWriter.finish();
}
}
@FunctionalInterface
public interface PageDataSupplier<T> {
List<T> getPage(int pageNo, int pageSize);
}
}

View File

@@ -0,0 +1,95 @@
package com.mikufufu.core.utils;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 加密工具类
*
*/
@Slf4j
public class EncryptionUtils {
/**
* MD5加密
* @param source 需要加密的字符串
* @return 结果 String 加密后的字符串
*/
public static String md5(String source){
try {
// 获取 MD5 算法实例对象
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] md5 = messageDigest.digest(source.getBytes(StandardCharsets.UTF_8));
// 将处理后的字节转成 16 进制,得到最终 32 个字符
StringBuilder stringBuilder = new StringBuilder();
for (byte b : md5) {
stringBuilder.append(String.format("%02x", b));
}
return stringBuilder.toString();
} catch (NoSuchAlgorithmException e) {
log.error("MD5 加密失败", e);
}
return null;
}
/**
* MD5加密
* @param source 需要加密的字符串
* @param salt 盐
* @return 结果 String 加密后的字符串
*/
public static String md5(String source, String salt) {
return md5(source + salt);
}
/**
* 检查给定的源字符串和盐值经过MD5加密后是否与给定的编码字符串匹配。
*
* @param sourceStr 源字符串,即将被加密的字符串。
* @param salt 盐值,用于加强加密的字符串。
* @param encodedStr 加密字符串,用于比对的已编码字符串。
* @return boolean 如果给定的源字符串和盐值加密后与编码字符串匹配则返回true否则返回false。
*/
public static boolean md5Matches(String sourceStr, String salt, String encodedStr) {
// 根据源字符串和盐值计算MD5值并与给定的编码字符串进行比较
return encodedStr.equals(md5(sourceStr, salt));
}
/**
* SHA-256加密
* @param source 需要加密的字符串
* @return 结果 String 加密后的字符串
*/
public static String sha256(String source){
try {
// 获取 SHA-256 算法实例对象
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] sha256 = messageDigest.digest(source.getBytes(StandardCharsets.UTF_8));
// 将处理后的字节转成 16 进制,得到最终 32 个字符
StringBuilder stringBuilder = new StringBuilder();
for (byte b : sha256) {
stringBuilder.append(String.format("%02x", b));
}
return stringBuilder.toString();
} catch (NoSuchAlgorithmException e) {
log.error("SHA-256 加密失败", e);
}
return null;
}
/**
* 判断是否与加密值相同 SHA-256
* @param sourceStr 源字符串
* @param encodedStr 加密后字符
* @return 结果
*/
public static boolean sha256Matches(String sourceStr, String encodedStr) {
return encodedStr.equals(sha256(sourceStr));
}
}

View File

@@ -0,0 +1,136 @@
package com.mikufufu.core.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.FileNameMap;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
/**
* 文件工具类
* 用于文件操作
* 比如文件大小转换等
*
*/
@Slf4j
public class FileUtils {
private static final FileNameMap mimeMap = URLConnection.getFileNameMap();
// 使用ThreadLocal来存储每个线程独有的DecimalFormat实例避免多线程下的并发问题
private static final ThreadLocal<DecimalFormat> DECIMAL_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
// 初始化DecimalFormat使用美国 Locale 以确保小数分隔符为点
DecimalFormat decimalFormat = new DecimalFormat("#.##", new DecimalFormatSymbols(Locale.US));
// 设置不使用千位分隔符
decimalFormat.setGroupingUsed(false);
return decimalFormat;
});
/**
* 将字节大小转换为更易读的文件大小格式
* 此方法根据文件大小选择合适的单位BKBMBGB并格式化为两位小数
* 注意此方法不支持超过1TB的文件大小转换
*
* @param size 字节大小
* @return {@link String} 转换后的文件大小
* @throws IllegalArgumentException 如果文件大小超过1TB
*/
public static String convertFileSize(long size) {
// 检查文件大小是否超过1TB如果超过则抛出异常
if (size > 1024L * 1024 * 1024 * 1024) {
throw new IllegalArgumentException("文件超过1TB");
}
// 获取当前线程的DecimalFormat实例
DecimalFormat decimalFormat = DECIMAL_FORMAT_THREAD_LOCAL.get();
// 根据文件大小,选择合适的单位进行转换
if (size < 1024) {
return size + "B";
} else if (size < 1024 * 1024) {
return decimalFormat.format(size / 1024.0) + "KB";
} else if (size < 1024 * 1024 * 1024) {
return decimalFormat.format(size / (1024.0 * 1024)) + "MB";
} else {
return decimalFormat.format(size / (1024.0 * 1024 * 1024)) + "GB";
}
}
/**
* 将输入流内容转换为字符串
*
* @param inputStream 输入流对象,方法执行完毕后会自动关闭
* @param charset 用于解码字节数据的字符集
* @return 使用指定字符集解码后的字符串
* @throws IOException 当发生I/O错误时抛出
* 实现逻辑:
* 1. 使用try-with-resources自动管理输入流
* 2. 通过缓冲区循环读取输入流内容
* 3. 将读取的字节数据转换为字符串
*/
public static String readFileToString(InputStream inputStream, Charset charset) throws IOException {
try (InputStream is = inputStream) { // 使用 try-with-resources
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
// 循环读取数据直到流结束
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 将缓存的字节数据转换为字符串
return new String(outputStream.toByteArray(), charset);
} // 自动关闭 InputStream
}
/**
* 使用UTF-8编码将输入流内容转换为字符串
*
* @param inputStream 输入流对象,方法执行完毕后会自动关闭
* @return 使用UTF-8字符集解码后的字符串
* @throws IOException 当发生I/O错误时抛出
*/
public static String readFileToString(InputStream inputStream) throws IOException {
return readFileToString(inputStream, StandardCharsets.UTF_8);
}
/**
* 读取文件内容为字符串(默认 UTF-8 编码)
*/
public static String readFileToString(Path path) throws IOException {
return readFileToString(path, StandardCharsets.UTF_8);
}
/**
* 读取文件内容为字符串(支持自定义编码)
*/
public static String readFileToString(Path path, Charset charset) throws IOException {
byte[] bytes = Files.readAllBytes(path);
return new String(bytes, charset);
}
/**
* 获取MIME
*
* @param fileName 文件名
*/
public static String mime(String fileName) {
String tmp = mimeMap.getContentTypeFor(fileName);
if (tmp == null) {
return "application/octet-stream";
} else {
return tmp;
}
}
}

View File

@@ -0,0 +1,116 @@
package com.mikufufu.core.utils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.*;
@Slf4j
@Component
public class IpUtil {
private static Method method;
/**
* 从HttpServletRequest中获取客户端真实IP地址
*
* 方法按优先级尝试从多个代理服务器标准头字段中获取IP地址处理多层代理和本地访问场景。
* 注意当存在多个IP时会截取第一个有效IP按逗号分隔
*
* @param request HTTP请求对象用于获取请求头信息和远程地址
* @return 处理后的客户端IP地址字符串可能去除端口和多余代理信息
*/
public static String getIpAddress(HttpServletRequest request) {
String ipAddress = request.getHeader("X-Real-IP");
// 按优先级检查代理服务器头信息
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 处理x-forwarded-for头信息,常用在Nginx代理服务器上
ipAddress = request.getHeader("x-forwarded-for");
}
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 处理Proxy-Client-IP头信息,常用于Apache代理服务器上
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 处理WL-Proxy-Client-IP头信息,常用于WebLogic代理服务器上
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 处理HTTP_CLIENT_IP头信息,用于Apache代理服务器上
ipAddress = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 处理HTTP_X_FORWARDED_FOR头信息,用于Nginx代理服务器上
ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
}
// 当所有代理头都不存在时,使用远程地址并处理本地访问场景
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if ("127.0.0.1".equals(ipAddress) || "0:0:0:0:0:0:0:1".equals(ipAddress)) {
// 处理本地回环地址尝试获取本机实际网络IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.error("getIpAddress exception:", e);
}
assert inet != null;
ipAddress = inet.getHostAddress();
}
}
// 处理可能存在的多个IP地址如x-forwarded-for的多值情况
return StringUtils.substringBefore(ipAddress, ",");
}
/**
* 从HttpServletRequest中获取客户端真实IP地址
*
* 方法按优先级尝试从多个代理服务器标准头字段中获取IP地址处理多层代理和本地访问场景。
* 注意当存在多个IP时会截取第一个有效IP按逗号分隔
* @return 处理后的客户端IP地址字符串可能去除端口和多余代理信息
*/
public static String getIpAddress() {
// 从当前请求中获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
return getIpAddress(((ServletRequestAttributes) requestAttributes).getRequest());
}
return "";
}
/**
* Ping 一个地址
*
* @param address 192.168.1.1 或 192.168.1.1:8080
*/
public static boolean ping(String address) throws Exception {
if (address.contains(":")) {
String host = address.split(":")[0];
int port = Integer.parseInt(address.split(":")[1]);
try (Socket socket = new Socket()) {
SocketAddress addr = new InetSocketAddress(host, port);
socket.connect(addr, 3000);
return true;
} catch (IOException e) {
return false;
}
} else {
return InetAddress.getByName(address).isReachable(3000);
}
}
public static String getUserAgent(HttpServletRequest request) {
return request.getHeader("User-Agent");
}
}

View File

@@ -0,0 +1,182 @@
package com.mikufufu.core.utils;
import jakarta.mail.*;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.angus.mail.util.MailSSLSocketFactory;
import jakarta.validation.constraints.NotNull;
import java.security.GeneralSecurityException;
import java.util.Date;
import java.util.List;
import java.util.Properties;
/**
* Java邮件发送工具类支持SSL/TLS加密
* 使用示例:
* {@code MailUtils.sendEmail("smtp.qq.com", 465, true,"发件邮箱@qq.com", "授权码","邮件标题", "<h1>HTML内容</h1>",Lists.newArrayList("xxx@qq.com"),Lists.newArrayList("xxx@qq.com"),Lists.newArrayList("xxx@qq.com"));}
*/
@Slf4j
public class MailUtils {
/**
* 发送邮件核心方法
* @param host SMTP服务器地址如smtp.qq.com
* @param port SMTP端口465/587
* @param sslEnabled 是否启用SSL加密
* @param username 发件邮箱(完整地址)
* @param password 邮箱密码/授权码
* @param subject 邮件标题
* @param content 邮件内容支持HTML
* @param recipients 收件人列表
* @param ccRecipients 抄送人列表
* @param bccRecipients 密送人列表
*/
public static void sendEmail(String host, int port, boolean sslEnabled,
String username, String password,
String subject, String content,
List<String> recipients,
List<String> ccRecipients,
List<String> bccRecipients
) throws MessagingException {
Properties props = getProps(host, port, sslEnabled);
sendEmail(props, username, password, subject, content, recipients, ccRecipients, bccRecipients);
}
/**
* 构建邮件内容
* @param props 邮件配置
* @param username 发件邮箱(完整地址)
* @param password 邮箱密码/授权码
* @param subject 邮件标题
* @param content 邮件内容支持HTML
* @param recipients 收件人列表
* @param ccRecipients 抄送人列表
* @param bccRecipients 密送人列表
*/
public static void sendEmail(Properties props,
final String username,final String password,
String subject, String content,
List<String> recipients,
List<String> ccRecipients,
List<String> bccRecipients) throws MessagingException{
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
// 构建邮件内容
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(username));
message.setSubject(subject, "UTF-8");
message.setSentDate(new Date());
// 设置收件人(支持多个)
InternetAddress[] addresses = recipients.stream()
.map(email -> {
try {
return new InternetAddress(email);
} catch (AddressException e) {
throw new RuntimeException("邮箱地址格式错误: " + email);
}
})
.toArray(InternetAddress[]::new);
message.setRecipients(Message.RecipientType.TO, addresses);
// 抄送(支持多个)
if (ccRecipients != null && !ccRecipients.isEmpty()) {
InternetAddress[] ccAddresses = ccRecipients.stream()
.map(email -> {
try {
return new InternetAddress(email);
} catch (AddressException e) {
throw new RuntimeException("邮箱地址格式错误: " + email);
}
})
.toArray(InternetAddress[]::new);
message.setRecipients(Message.RecipientType.CC, ccAddresses);
}
// 密送(支持多个)
if (bccRecipients != null && !bccRecipients.isEmpty()) {
InternetAddress[] bccAddresses = bccRecipients.stream()
.map(email -> {
try {
return new InternetAddress(email);
} catch (AddressException e) {
throw new RuntimeException("邮箱地址格式错误: " + email);
}
})
.toArray(InternetAddress[]::new);
message.setRecipients(Message.RecipientType.BCC, bccAddresses);
}
// 设置HTML内容
message.setContent(content, "text/html;charset=UTF-8");
// 发送邮件
Transport.send(message);
}
/**
* 发送邮件核心方法
* @param host SMTP服务器地址如smtp.qq.com
* @param port SMTP端口465/587
* @param sslEnabled 是否启用SSL加密
* @param username 发件邮箱(完整地址)
* @param password 邮箱密码/授权码
* @param subject 邮件标题
* @param content 邮件内容支持HTML
* @param recipients 收件人列表
*/
public static void sendEmail(String host, int port, boolean sslEnabled,
String username, String password,
String subject, String content,
List<String> recipients) throws MessagingException {
sendEmail(host, port, sslEnabled, username, password, subject, content, recipients, null, null);
}
/**
* 获取邮件配置
* @param host SMTP服务器地址如smtp.qq.com
* @param port SMTP端口465/587
* @param sslEnabled 是否启用SSL加密
* @return 邮件配置
*/
@NotNull
private static Properties getProps(String host, int port, boolean sslEnabled) {
Properties props = new Properties();
// 基础配置
props.put("mail.smtp.host", host);
props.put("mail.smtp.port", port);
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.connectiontimeout", 5000);
props.put("mail.smtp.timeout", 5000);
// SSL加密配置
if (sslEnabled) {
props.put("mail.smtp.ssl.enable", "true");
// props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
try {
MailSSLSocketFactory sf = new MailSSLSocketFactory();
sf.setTrustAllHosts(true);
props.put("mail.smtp.ssl.socketFactory", sf);
} catch (GeneralSecurityException e) {
log.error("创建MailSSLSocketFactory失败", e);
}
props.put("mail.smtp.socketFactory.port", port);
}
// TLS加密配置
if (!sslEnabled) {
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.starttls.required", "true");
}
return props;
}
}

View File

@@ -0,0 +1,423 @@
package com.mikufufu.core.utils;
import com.alibaba.fastjson2.JSON;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类提供对Redis简单操作的方法封装。
* <p>
* 含{@code @SuppressWarnings}注解的代码段。value 该参数指定了要抑制的警告类型,可以是多个类型。
* 例如,{@code "unchecked"}用于抑制未检查类型转换的警告,{@code "rawtypes"}用于抑制使用原始类型而不是泛型的警告。
* 该注释的目的是解释为什么需要抑制这些特定的警告,以及在没有其他更好解决方法的情况下,这种做法的合理性。
* 如果不希望抑制这些警告,可以使用其他方法来避免它们。
* </p>
*
*/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public class RedisUtils {
// RedisTemplate实例用于执行Redis操作。
private final static RedisTemplate redisTemplate = (RedisTemplate) SpringUtils.getBean("redisTemplate");
/**
* 将给定的值设置到指定的键上。
* @param key 键名
* @param value 要设置的值
*/
public static <T> void set(String key, T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 将给定的值设置到指定的键上,并设定过期时间。
* @param key 键名
* @param value 要设置的值
* @param time 过期时间, 单位为秒
*/
public static <T> void set(String key, T value, long time) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
/**
* 将给定的值设置到指定的键上,并设定过期时间。
* @param key 键名
* @param value 要设置的值
* @param time 过期时间 long
* @param timeUnit 过期时间单位
*/
public static <T> void set(String key, T value, long time, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, time, timeUnit);
}
/**
* 获取指定键的值。
* @param key 键名
* @return 键对应的值
*/
public static Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 获取指定键的值,并将其转换为指定类型。
* @param key 键名
* @param clazz 需要转换成的目标类型
* @param <T> 目标类型
* @return 键对应的值转换后的类型为clazz指定的类型
*/
public static <T> T get(String key, Class<T> clazz) {
return StringUtils.convert(redisTemplate.opsForValue().get(key), clazz);
}
/**
* 将指定列表值添加到Redis列表的末尾(右侧)。
*
* @param key Redis中列表的键名。
* @param value 要添加到列表中的值,该值是一个泛型列表。
* @return 添加成功后列表的长度如果添加前列表不存在则返回0。
*/
public static <T> long setList(String key, List<T> value) {
// 使用RedisTemplate向指定key的列表末尾添加所有value的元素
Long count = redisTemplate.opsForList().rightPushAll(key, value);
// 如果count为null表示添加前列表不存在返回0否则返回添加后的列表长度
return count == null ? 0: count;
}
/**
* 将指定列表值添加到Redis列表的末尾(右侧)。
*
* @param key Redis中列表的键名。
* @param value 要添加到列表中的值,该值是一个泛型列表。
* @param time 添加后过期时间,单位为秒。
* @return 添加成功后列表的长度如果添加前列表不存在则返回0。
*/
public static <T> long setList(String key, List<T> value, Long time) {
// 使用RedisTemplate向指定key的列表末尾添加所有value的元素
Long count = redisTemplate.opsForList().rightPushAll(key, value);
if (time != null && time > 0) {
// 如果time大于0则设置过期时间
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
// 如果count为null表示添加前列表不存在返回0否则返回添加后的列表长度
return count == null ? 0: count;
}
/**
* 将指定列表值添加到Redis列表的末尾(右侧)。
*
* @param key Redis中列表的键名。
* @param value 要添加到列表中的值,该值是一个泛型列表。
* @param time 添加后过期时间,单位为秒。
* @param timeUnit 添加后过期时间单位。
* @return 添加成功后列表的长度如果添加前列表不存在则返回0。
*/
public static <T> long setList(String key, List<T> value, Long time, TimeUnit timeUnit) {
// 使用RedisTemplate向指定key的列表末尾添加所有value的元素
Long count = redisTemplate.opsForList().rightPushAll(key, value);
if (time != null && time > 0) {
// 如果time大于0则设置过期时间
redisTemplate.expire(key, time, timeUnit);
}
// 如果count为null表示添加前列表不存在返回0否则返回添加后的列表长度
return count == null ? 0: count;
}
/**
* 向Redis的列表类型数据中添加元素。
*
* @param key Redis中列表的键名。
* @param value 要添加到列表中的元素。
* @return 如果添加前列表不存在返回0否则返回添加后的列表长度。
* @param <T> 列表中元素的类型。
*/
public static <T> long addList(String key, T value) {
// 向指定key的列表中添加value元素
Long count = redisTemplate.opsForList().rightPush(key, value);
// 判断添加结果若为null表示列表之前不存在返回0否则返回添加的元素数量
return count == null ? 0: count;
}
/**
* 向Redis的Set类型数据中添加元素。
*
* @param key Redis中Set的键名。
* @param value 要添加到Set中的元素集合。
* @return 如果添加前Set不存在返回0否则返回添加后的Set长度。
* @param <T> Set中元素的类型。
*/
public static <T> long setSet(String key, Set<T> value) {
// 向指定key的Set中添加所有value的元素
Long count = redisTemplate.opsForSet().add(key, value.toArray());
// 判断添加结果若为null表示Set之前不存在返回0否则返回添加的元素数量
return count == null ? 0: count;
}
public static <T> long addSet(String key, T value) {
// 向指定key的Set中添加value元素
Long count = redisTemplate.opsForSet().add(key, value);
// 判断添加结果若为null表示Set之前不存在返回0否则返回添加的元素数量
return count == null ? 0: count;
}
/**
* 从Redis列表中获取指定键的所有元素。
*
* @param key 列表的键名对应Redis中的键。
* @return 返回一个包含列表所有元素的List类型为泛型T。
*/
public static <T> List<T> getList(String key) {
// 通过redisTemplate操作Redis列表获取key对应列表的所有元素
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 根据给定的键从Redis中获取集合并返回。
*
* @param key 用于获取集合的Redis键。
* @return 返回与给定键关联的集合。如果键不存在,返回空集合。
* @param <T> 集合元素的类型。
*/
public static <T> Set<T> getSet(String key) {
// 从Redis中获取指定key的集合成员
return redisTemplate.opsForSet().members(key);
}
/**
* 删除指定键的值。
* @param key 键名
*/
public static Boolean delete(String key) {
return redisTemplate.delete(key);
}
/**
* 批量删除Redis中的键值对。
*
* @param keys 要删除的键的集合键的类型为String。
* @return 返回一个布尔值如果删除成功即删除的键数量大于0则返回true否则返回false。
*/
public static Boolean delete(Collection<String> keys) {
// 从Redis中删除指定的键并返回删除的键的数量
Long count = redisTemplate.delete(keys);
// 判断删除的键的数量是否大于0若大于0则返回true否则返回false
return count > 0;
}
/**
* 获取所有匹配给定模式的键名。
*
* @param pattern 键名模式,可以使用通配符,例如"*"表示所有键。
* @return 匹配模式的键名列表。
*/
public static List<String> keys(String pattern) {
// 通过RedisTemplate获取所有匹配pattern的键名转换为ArrayList并返回
return new ArrayList<>(Objects.requireNonNull(redisTemplate.keys(pattern)));
}
/**
* 获取所有键名。
*
* @return 所有键名的列表。
*/
public static List<String> keys() {
// 调用keys方法传入"*"作为模式,以获取所有键名
return keys("*");
}
/**
* 在Redis中放入哈希表数据
*
* @param key 哈希表的键
* @param hashKey 哈希表中的字段键
* @param value 哈希表中的字段值
* @param <T> 字段值的类型
*/
public static <T> void putHash(String key, String hashKey, T value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
/**
* 在Redis中放入哈希表数据
*
* @param key 哈希表的键
* @param map 包含多个字段键值对的映射
* @param <T> 字段值的类型
*/
public static <T> void putHash(String key, Map<String, T> map) {
redisTemplate.opsForHash().putAll(key, map);
}
/**
* 从Redis中获取哈希表中的特定字段值
*
* @param key 哈希表的键
* @param hashKey 哈希表中的字段键
* @param <T> 字段值的类型
* @return 字段值如果不存在则返回null
*/
public static <T> T getHash(String key, String hashKey) {
return (T) redisTemplate.opsForHash().get(key, hashKey);
}
/**
* 从Redis中获取整个哈希表的数据
*
* @param key 哈希表的键
* @param <T> 字段值的类型
* @return 包含所有字段键值对的映射
*/
public static <T> Map<String, T> getHash(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 获取哈希表中的所有字段键
*
* @param key 哈希表的键
* @return 包含所有字段键的集合
*/
public static Set<String> getAllHash(String key){
return redisTemplate.opsForHash().keys(key);
}
/**
* 获取哈希表中的所有字段值
*
* @param key 哈希表的键
* @param <T> 字段值的类型
* @return 包含所有字段值的列表
*/
public static <T> List<T> getAllHashValue(String key) {
return redisTemplate.opsForHash().values(key);
}
/**
* 获取哈希表中的所有字段键值对
*
* @param key 哈希表的键
* @param <T> 字段值的类型
* @return 包含所有字段键值对的映射
*/
public static <T> Map<String, T> getAllHashEntity(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 删除Redis中的哈希表
*
* @param key 哈希表的键
* @return 如果哈希表存在且删除成功返回true否则返回false
*/
public static Boolean deleteHash(String key) {
return redisTemplate.opsForHash().delete(key) > 0;
}
/**
* 删除哈希表中的一个或多个字段
*
* @param key 哈希表的键
* @param hashKeys 要删除的字段键数组
* @return 如果字段存在且删除成功返回true否则返回false
*/
public static Boolean deleteHash(String key, String... hashKeys) {
return redisTemplate.opsForHash().delete(key, (Object) hashKeys) > 0;
}
/**
* 发送消息到Redis消息队列
*
* @param topic Redis消息队列的Topic
* @param msg 要发送的消息
* @param <T> 消息的类型
*/
public static <T> void sendMsg(String topic, T msg) {
redisTemplate.convertAndSend(topic, JSON.toJSONString(msg));
}
/**
* 根据指定前缀删除键值对
* @param prefix 键名的前缀
* @return 如果删除成功则返回true否则返回false
*/
public static Boolean deleteByPrefix(String prefix) {
// 获取所有匹配指定前缀的键名
List<String> keys = keys(prefix + "*");
return delete(keys);
}
/**
* 删除使用的Redis缓存。
* @return true 删除成功 false删除失败
*/
public static boolean deleteCache() {
// 获取所有键名
List<String> keys = keys("*");
// 删除所有键值对
return delete(keys);
}
/**
* 设置Redis键的过期时间
*
* @param key Redis的键
* @param time 过期时间,单位为秒
* @return 返回是否成功设置过期时间
*/
public static Boolean setExpirationTime(String key, long time){
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
/**
* 设置Redis键的过期时间
*
* @param key Redis的键
* @param time 过期时间,单位为秒
* @return 返回是否成功设置过期时间
*/
public static Boolean setExpirationTime(String key, long time, TimeUnit timeUnit){
return redisTemplate.expire(key, time, timeUnit);
}
/**
* 获取Redis键的剩余过期时间
*
* @param key Redis的键
* @return 剩余过期时间,单位为秒,如果键不存在或没有设置过期时间,则返回-2如果键设置了过期时间则返回剩余过期时间。
*/
public static Long getExpirationTime(String key){
return redisTemplate.getExpire(key);
}
/**
* 获取Redis键的剩余过期时间
*
* @param key Redis的键
* @param timeUnit 时间单位
* @return 剩余过期时间,单位为秒,如果键不存在或没有设置过期时间,则返回-2如果键设置了过期时间则返回剩余过期时间。
*/
public static Long getExpirationTime(String key, TimeUnit timeUnit){
return redisTemplate.getExpire(key,timeUnit);
}
/**
* 判断缓存中是否有对应的value
* @return true 存在 false不存在
*/
public static boolean exists(String key) {
return redisTemplate.hasKey(key);
}
/**
* 判断缓存中是否有对应的value
* @param key 缓存的key
* @param hashKey 哈希key
* @return true 存在 false不存在
*/
public static boolean hasHashKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
}

View File

@@ -0,0 +1,145 @@
package com.mikufufu.core.utils;
import com.mikufufu.common.constant.RegexStr;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexUtils {
// 使用 ConcurrentHashMap 缓存编译后的正则表达式对象,以提高性能
private static final ConcurrentHashMap<String, Pattern> patternCache = new ConcurrentHashMap<>();
/**
* 检查字符串是否与给定的正则表达式匹配
*
* @param str 要匹配的字符串
* @param regex 正则表达式
* @return 如果字符串与正则表达式匹配,则返回 true否则返回 false
* @throws IllegalArgumentException 如果正则表达式无效
*/
public static boolean match(String str, String regex) {
// 检查输入参数是否为空,为空则直接返回 false
if (StringUtils.isBlank(str) || StringUtils.isBlank(regex)) {
return false;
}
// 简单的正则表达式验证,防止注入攻击
if (!regex.matches("^[a-zA-Z0-9.*?+@_{}()|\\[\\]^$\\\\,%-]*$")) {
throw new IllegalArgumentException("Invalid regex pattern");
}
// 从缓存中获取或编译正则表达式对象
Pattern pattern = getPattern(regex);
// 使用正则表达式对象匹配字符串,并返回匹配结果
return pattern.matcher(str).matches();
}
/**
* 替换标识符的值
*
* @param destinationString 目标字符串
* @param valueMap 标识符和值映射
* @return 替换后的字符串
*/
public static String replaceIdentifierValue(String destinationString, Map<String, String> valueMap) {
// 检查输入参数是否为空,为空则直接返回
if (StringUtils.isEmpty(destinationString) || valueMap == null || valueMap.isEmpty()) {
return destinationString;
}
// 使用正则表达式进行匹配和替换
Pattern pattern = getPattern("\\$\\{([^}]+)}");
Matcher matcher = pattern.matcher(destinationString);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
String key = matcher.group(1);
String replacement = valueMap.getOrDefault(key, "");
matcher.appendReplacement(result, replacement);
}
matcher.appendTail(result);
return result.toString();
}
public static String findFirstMatch(String regex, String input) {
Pattern pattern = getPattern(regex);
Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
return matcher.group();
}
return null;
}
public static String[] findAllMatch(String regex, String input) {
Pattern pattern = getPattern(regex);
Matcher matcher = pattern.matcher(input);
return matcher.results().map(MatchResult::group).toArray(String[]::new);
}
/**
* 从缓存中获取或编译正则表达式对象
*
* @param regex 正则表达式
* @return 编译后的正则表达式对象
*/
private static Pattern getPattern(String regex) {
// 如果缓存中不存在该正则表达式,则编译并添加到缓存中
return patternCache.computeIfAbsent(regex, Pattern::compile);
}
/**
* 验证手机号码是否合法
* @param mobile 待验证的手机号码
* @return 如果手机号码合法返回true否则返回false。
*/
public static boolean isMobile(String mobile) {
// 使用正则表达式进行手机号码的匹配
return match(mobile, RegexStr.MOBILE);
}
/**
* 验证邮箱地址是否合法
* @param email 待验证的邮箱地址
* @return 如果邮箱地址合法返回true否则返回false。
*/
public static boolean isEmail(String email) {
// 使用正则表达式进行邮箱的匹配
return match(email, RegexStr.EMAIL);
}
/**
* 验证密码是否合法
* @param password 待验证的密码
* @return 如果密码合法返回true否则返回false。
*/
public static boolean isPassword(String password) {
return match(password, RegexStr.PASSWORD);
}
/**
* 验证URL地址是否合法
* @param url 待验证的URL地址
* @return 如果URL地址合法返回true否则返回false。
*/
public static boolean isUrl(String url) {
// 使用正则表达式进行URL的匹配
return match(url, RegexStr.URL);
}
/**
* 验证QQ号码是否合法
* @param qq 待验证的QQ号码
* @return 如果QQ号码合法返回true否则返回false。
*/
public static boolean isQQ(String qq) {
// 使用正则表达式进行QQ号的匹配
return match(qq, RegexStr.QQ);
}
}

View File

@@ -0,0 +1,76 @@
package com.mikufufu.core.utils;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RsaUtils {
// 密钥算法类型
private static final String ALGORITHM = "RSA";
// 加密填充方式
private static final String TRANSFORMATION = "RSA/ECB/PKCS1Padding";
// 密钥长度推荐2048位
private static final int KEY_SIZE = 2048;
/**
* 生成RSA密钥对
* @return 包含Base64编码公钥私钥的Map
*/
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(ALGORITHM);
keyPairGen.initialize(KEY_SIZE, new SecureRandom());
return keyPairGen.generateKeyPair();
}
/**
* 公钥加密
* @param plainText 明文
* @param publicKey Base64编码的公钥字符串
* @return Base64编码的密文
*/
public static String encrypt(String plainText, String publicKey) throws Exception {
byte[] keyBytes = java.util.Base64.getDecoder().decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
PublicKey pubKey = KeyFactory.getInstance(ALGORITHM).generatePublic(keySpec);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
return java.util.Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes()));
}
/**
* 私钥解密
* @param cipherText Base64编码的密文
* @param privateKey Base64编码的私钥字符串
* @return 解密后的明文
*/
public static String decrypt(String cipherText, String privateKey) throws Exception {
byte[] keyBytes = java.util.Base64.getDecoder().decode(privateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
PrivateKey priKey = KeyFactory.getInstance(ALGORITHM).generatePrivate(keySpec);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, priKey);
byte[] decrypted = cipher.doFinal(java.util.Base64.getDecoder().decode(cipherText));
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* 获取Base64编码的公钥字符串
*/
public static String getPublicKeyString(KeyPair keyPair) {
return java.util.Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
}
/**
* 获取Base64编码的私钥字符串
*/
public static String getPrivateKeyString(KeyPair keyPair) {
return Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
}
}

View File

@@ -0,0 +1,129 @@
package com.mikufufu.core.utils;
import jakarta.validation.constraints.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Map;
/**
* 这个类用于提供一个全局的获取Spring ApplicationContext的方法
*
*/
@Component // 标记为Spring组件
public class SpringUtils implements ApplicationContextAware {
// 静态的ApplicationContext引用
private static ApplicationContext context;
// 提供一个全局的获取ApplicationContext的方法
public static ApplicationContext getApplicationContext() {
return context == null ? null : context;
}
// 实现ApplicationContextAware接口的方法Spring会自动调用这个方法传入ApplicationContext
@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
// 将ApplicationContext保存到静态变量中
context = applicationContext;
}
/**
* 根据bean的class获取bean
* @param clazz bean的class
* @return bean
*/
public static <T> T getBean(Class<T> clazz) {
return clazz == null ? null : context.getBean(clazz);
}
/**
* 根据bean的id获取bean
* @param beanId bean的id
* @return bean
*/
public static Object getBean(String beanId) {
return beanId == null ? null : context.getBean(beanId);
}
/**
* 根据bean的id和class获取bean
* @param beanName bean的id
* @param clazz bean的class
* @return bean
*/
public static <T> T getBean(String beanName, Class<T> clazz) {
if (null != beanName && !beanName.trim().isEmpty()) {
return clazz == null ? null : context.getBean(beanName, clazz);
} else {
return null;
}
}
/**
* 获取包含指定注解的所有Bean的映射。
*
* @param clazz 指定的注解类用于查询包含该注解的所有Bean。
* @return 返回一个映射其中键是Bean的名称值是Bean的实例。如果指定的注解类为null则返回null。
*/
public static Map<String, Object> getBeansWithAnnotation(Class<? extends Annotation> clazz) {
// 判断注解类是否为null为null则直接返回null否则查询并返回包含该注解的所有Bean
return clazz == null ? null : context.getBeansWithAnnotation(clazz);
}
/**
* 获取当前环境
* @return 当前环境
*/
public static String getActiveProfile() {
return context.getEnvironment().getActiveProfiles()[0];
}
/**
* 获取当前属性名对应的属性值
* @param key 属性名
* @return 属性值
*/
public static String getProperty(String key) {
return context.getEnvironment().getProperty(key);
}
/**
* 获取当前的环境对象。
*
* @return Environment 返回当前的环境对象,该对象包含了系统的环境配置信息。
*/
public static Environment getEnvironment() {
// 从上下文中获取环境对象
return context.getEnvironment();
}
/**
* 获取当前环境的ip
* @return 当前ip
*/
public static String getHost(){
try {
return InetAddress.getLocalHost().getHostAddress() + ":" + getProperty("server.port");
} catch (UnknownHostException e) {
throw new RuntimeException("获取当前环境的ip失败");
}
}
/**
* 获取系统启动时间
* @return 返回系统启动的时间戳,单位为毫秒。
*/
public static Date getStartupTime() {
return new Date(context.getStartupDate());
}
}

View File

@@ -0,0 +1,415 @@
package com.mikufufu.core.utils;
import jakarta.validation.constraints.NotNull;
import org.springframework.lang.Nullable;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 字符串工具类
*
*/
public class StringUtils {
/**
* 判断给定对象是否为空。
* @param object 待检查的对象可以是null或者以下类型之一String、Collection、Map、CharSequence、数组。
* @return 如果对象为null或者为空字符串String、空集合Collection、空映射Map、空字符序列CharSequence、空数组返回true否则返回false。
*/
public static boolean isEmpty(@Nullable Object object) {
if (object == null) {
// 对象为null时认为为空
return true;
} else if (object instanceof String) {
// 对象为String类型时使用String的isEmpty()方法判断
return ((String) object).isEmpty();
} else if (object instanceof Collection) {
// 对象为Collection类型时使用Collection的isEmpty()方法判断
return ((Collection<?>) object).isEmpty();
} else if (object instanceof Map) {
// 对象为Map类型时使用Map的isEmpty()方法判断
return ((Map<?, ?>) object).isEmpty();
} else if (object instanceof CharSequence) {
// 对象为CharSequence类型时判断其长度是否为0
return ((CharSequence) object).isEmpty();
} else if (object.getClass().isArray()){
// 对象为数组时判断数组长度是否为0
return ((Object[])object).length == 0;
} else {
// 对于其他类型,均不认为为空
return false;
}
}
/**
* 判断实体类是否不为空
* @param object 实体类
* @return true=实体类不为空false=实体类为空
*/
public static boolean isNotEmpty(@Nullable Object object) {
return !isEmpty(object);
}
/**
* 判断字符串是否为空白
* @param str 字符串
* @return true=字符串为空白false=字符串不为空白
*/
public static boolean isBlank(String str) {
// 此处的str.trim().isEmpty()的写法等同于str.trim().length() == 0
return str == null || str.trim().isEmpty();
}
/**
* 判断字符串是否不为空白
* @param str 字符串
* @return true=字符串不为空白false=字符串为空白
*/
public static boolean isNotBlank(String str) {
return !isBlank(str);
}
/**
* 将list列表转换成字符串以逗号分隔
* @param list 列表
* @return 字符串
*/
public static String join(List<?> list) {
return join(list, ",");
}
/**
* 将list列表转换成字符串
* @param list 列表
* @param separator 分隔符
* @return 字符串
*/
public static String join(List<?> list, String separator) {
return join(list, separator, null,null);
}
/**
* 将list列表转换成字符串
* @param list 列表
* @param separator 分隔符
* @param prefix 前缀
* @param suffix 后缀
* @return 字符串
*/
public static String join(List<?> list, String separator, String prefix, String suffix) {
StringBuilder sb = new StringBuilder();
if (isNotEmpty(prefix)) {
sb.append(prefix);
}
if (isNotEmpty(list)) {
for (Object obj : list) {
sb.append(obj);
sb.append(separator);
}
// 删除最后一个分隔符
sb.delete(sb.length() - separator.length(), sb.length());
}
if (isNotEmpty(suffix)) {
sb.append(suffix);
}
return sb.toString();
}
/**
* 将字符串形式的列表转换成字符串列表。
* @param listStr 表示列表的字符串,各个元素通过","分隔。
* @return 转换后的字符串列表。
*/
public static List<String> toStrList(String listStr) {
return toStrList(listStr,",");
}
/**
* 将字符串形式的列表转换成字符串列表。
* @param listStr 表示列表的字符串各个元素通过separator分隔。
* @param separator 用于分割字符串列表元素的字符。
* @return 转换后的字符串列表。
*/
public static List<String> toStrList(String listStr, String separator) {
return toList(listStr, separator,String.class);
}
/**
* 将字符串形式的列表转换成指定类型的实体列表。
* @param listStr 表示列表的字符串各个元素通过separator分隔。
* @param clazz 要转换成的实体类型。
* @return 转换后的实体列表。
*/
public static <T> List<T> toList(String listStr, Class<T> clazz) {
return toList(listStr, ",",clazz);
}
/**
* 将字符串形式的列表转换成指定类型的实体列表。
* @param listStr 表示列表的字符串各个元素通过separator分隔。
* @param separator 用于分割字符串列表元素的字符。
* @param clazz 要转换成的实体类型。
* @return 转换后的实体列表。
*/
public static <T> List<T> toList(String listStr, String separator,Class<T> clazz) {
return toList(listStr, separator,null,null,clazz);
}
/**
* 将字符串形式的列表转换成指定类型的实体列表。
* @param listStr 表示列表的字符串各个元素通过separator分隔。
* @param separator 用于分割字符串列表元素的字符。
* @param prefix 每个元素前可能存在的前缀。
* @param suffix 每个元素后可能存在的后缀。
* @param clazz 要转换成的实体类型。
* @return 转换后的实体列表。
*/
public static <T> List<T> toList(String listStr,String separator, String prefix, String suffix ,Class<T> clazz) {
List<T> list = new ArrayList<>();
// 如果输入的字符串不为空,则进行分割和转换
if (isNotEmpty(listStr)) {
String[] strArray = listStr.split(separator);
for (String str : strArray) {
// 如果存在前缀,则移除
if (isNotEmpty(prefix) && str.length() >= prefix.length()) {
str = str.substring(prefix.length());
}
// 如果存在后缀,则移除
if (isNotEmpty(suffix) && str.length() >= suffix.length()) {
str = str.substring(0, str.length() - suffix.length());
}
// 将字符串转换成指定类型并添加到列表中
list.add(convert(str,clazz));
}
}
return list;
}
/**
* 将源对象转换为指定类型。
*
* @param source 源对象,需要转换的对象。
* @param clazz 目标类型,指定要将源对象转换成的类型。
* @return 转换后的对象。如果源对象不为null则返回转换后的对象如果源对象为null则返回null。
*/
public static <T> T convert(Object source, Class<T> clazz){
if(source != null){
// 源对象不为null时尝试将其转换为指定类型并返回
return clazz.cast(source);
}else {
// 源对象为null时直接返回null
return null;
}
}
/**
* 将字符串形式的列表转换成长整型列表。
* @param listStr 表示列表的字符串,各个元素通过","分隔。
* @return 转换后的字符串列表。
*/
public static List<Long> toLongList(String listStr) {
return toLongList(listStr,",");
}
/**
* 将字符串形式的列表转换成长整型列表。
* @param listStr 表示列表的字符串各个元素通过separator分隔。
* @param separator 用于分割字符串列表元素的字符。
* @return 转换后的字符串列表。
*/
public static List<Long> toLongList(String listStr, String separator) {
return toList(listStr, separator,Long.class);
}
/**
* 将字符串形式的列表转换成长整型列表。
* @param listStr 表示列表的字符串,各个元素通过","分隔。
* @return 转换后的字符串列表。
*/
public static List<Integer> toIntegerList(String listStr) {
return toIntegerList(listStr,",");
}
/**
* 将字符串形式的列表转换成长整型列表。
* @param listStr 表示列表的字符串各个元素通过separator分隔。
* @param separator 用于分割字符串列表元素的字符。
* @return 转换后的字符串列表。
*/
public static List<Integer> toIntegerList(String listStr, String separator) {
return toList(listStr, separator,Integer.class);
}
/**
* 将标识符替换成对应的值
* @param destinationString 目标字符串
* @param valueMap 存储标识符和对应值的映射关系的Map
* @return 替换标识符后的结果字符串
*/
public static String replaceIdentifierValue(String destinationString, Map<String, String> valueMap){
//正则表达式匹配规则的字符串,这里用来匹配${}字符串
String regex = "\\$\\{(.+?)}";
//匹配模式 符合regex中正则表达式匹配规则的匹配模式
Pattern pattern = Pattern.compile(regex);
//匹配器 对目标字符串destinationString使用pattern匹配模式得到的匹配器
Matcher matcher = pattern.matcher(destinationString);
//缓存字符串
StringBuffer stringBuffer = new StringBuffer();
while (matcher.find()){
// 获取匹配到的标识符 group(0)就是指的整个串group(1)指的是第一个括号里的东西,以此类推
String key = matcher.group(1);
// 根据标识符从valueMap中获取对应的值
String value = valueMap.get(key);
if(value == null){
matcher.appendReplacement(stringBuffer,"");
}else {
value.replaceAll("\\$","RDS_CHAR_DOLLAR");
// 将匹配到的标识符替换为对应的值并将结果追加到stringBuffer中
matcher.appendReplacement(stringBuffer,value);
}
}
// 将匹配到的剩余部分追加到stringBuffer中
matcher.appendTail(stringBuffer);
return stringBuffer.toString().replaceAll("RDS_CHAR_DOLLAR","\\$");
}
/**
* 将字符串转换成驼峰命名法
* @param str 字符串
* @return 驼峰命名法
*/
public static String toCamelCase(String str) {
if (str == null) {
return null;
}
// 将字符串中的下划线替换成驼峰命名法
StringBuilder sb = new StringBuilder();
// 用于标记是否需要将下一个字符转换成大写
boolean flag = false;
return getCamelCaseString(str, sb, flag);
}
/**
* 将字符串转换成下划线命名法
* @param str 字符串
* @return 下划线命名法
*/
public static String toUnderlineCase(String str) {
if (str == null) {
return null;
}
// 将字符串中的驼峰命名法替换成下划线命名法
StringBuilder sb = new StringBuilder();
for (char c : str.toCharArray()) {
if (Character.isUpperCase(c)) {
sb.append('_');
sb.append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* 将字符串转换成首字母大写的驼峰命名法
* @param str 字符串
* @return 首字母大写的驼峰命名法
*/
public static String toCapitalizeCamelCase(String str) {
if (str == null) {
return null;
}
// 将字符串中的下划线替换成驼峰命名法
StringBuilder sb = new StringBuilder();
// 用于标记是否需要将下一个字符转换成大写
boolean flag = true;
return getCamelCaseString(str, sb, flag);
}
/**
* 将字符串转换成驼峰命名法
* @param str 字符串
* @param sb StringBuilder
* @param flag 用于标记是否需要将下一个字符转换成大写 true=需要false=不需要
* @return 驼峰命名法
*/
@NotNull
private static String getCamelCaseString(String str, StringBuilder sb, boolean flag) {
for (char c : str.toCharArray()) {
if (c == '_') {
flag = true;
} else {
if (flag) {
sb.append(Character.toUpperCase(c));
flag = false;
} else {
sb.append(c);
}
}
}
return sb.toString();
}
/**
* 随机生成6位数验证码
* @return 生成的6位数验证码
*/
public static String randomCode() {
// 生成6位数的随机数
return String.valueOf((int) ((Math.random() * 9 + 1) * 100000));
}
/**
* 获取随机数 普通短信验证码 性能好,线程安全
* @return 随机数
*/
public static String threadLocalRandom(){
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
return String.valueOf(threadLocalRandom.nextInt(100_000, 1_000_000));
}
/**
* 获取随机数 密码学安全随机 性能一般,线程安全
* 支付/安全验证
* @return 随机数
*/
public static String secureRandom() {
SecureRandom secureRandom = new SecureRandom();
return String.format("%06d", secureRandom.nextInt(1_000_000));
}
/**
* 从字符串中截取指定分隔符之前的子字符串。
*
* @param str 原始字符串
* @param separator 分隔符
* @return 截取后的子字符串,如果没有找到分隔符,则返回原始字符串
*/
public static String substringBefore(String str, String separator) {
if (isEmpty(str) || separator == null) {
return str;
}
if (separator.isEmpty()) {
return "";
}
int pos = str.indexOf(separator);
// 如果没有找到分隔符,则返回原始字符串
if (pos == -1) {
return str;
}
return str.substring(0, pos);
}
}

View File

@@ -0,0 +1,45 @@
package com.mikufufu.core.utils;
import com.mikufufu.common.entity.Node;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 树形工具类
*
*/
public class TreeUtils {
public static <T, E> Node<T, E> buildTree(Node<T, E> root, Node<T, E> node) {
if (node.getParentId().equals(root.getId())) {
root.addChild(node);
} else {
for (Node<T, E> child : root.getChildren()) {
buildTree(child, node);
}
}
return root;
}
public static <T, E> List<Node<T,E>> listToTree(List<Node<T, E>> list,E rootId) {
// 检查输入列表是否为空,若为空则直接返回空列表
if (StringUtils.isEmpty(list)) {
return Collections.emptyList();
}
// 遍历列表
Map<E, List<Node<T, E>>> nodeMap = list.stream().collect(Collectors.groupingBy(Node<T, E>::getParentId));
// 返回根节点
return list.stream().peek(node -> {
List<Node<T,E>> children = nodeMap.get(node.getId());
if (StringUtils.isNotEmpty(children)) {
node.setChildren(children);
}
}).filter(node -> node.getParentId().equals(rootId)).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.mapper;
import com.mikufufu.modules.system.model.entity.Menu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 菜单信息表 Mapper 接口
* </p>
*
*
* @since 2024-12-16
*/
public interface MenuMapper extends BaseMapper<Menu> {
}

View File

@@ -0,0 +1,12 @@
package com.mikufufu.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.mikufufu.modules.system.model.dto.LogDto;
import com.mikufufu.modules.system.model.entity.OperateLog;
import org.apache.ibatis.annotations.Param;
public interface OperateLogMapper extends BaseMapper<OperateLog> {
IPage<OperateLog> getPage(@Param("logDto") LogDto logDto, IPage<OperateLog> page);
}

View File

@@ -0,0 +1,24 @@
package com.mikufufu.mapper;
import com.mikufufu.modules.system.model.entity.Permission;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mikufufu.modules.system.model.vo.PermissionRoleVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
* 权限表 Mapper 接口
* </p>
*
*
* @since 2024-12-16
*/
public interface PermissionMapper extends BaseMapper<Permission> {
List<Permission> getPermission(@Param("roleId") Integer roleId);
List<PermissionRoleVO> getPermissionRoleList();
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.mapper;
import com.mikufufu.modules.system.model.entity.Role;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 角色表 Mapper 接口
* </p>
*
*
* @since 2024-12-06
*/
public interface RoleMapper extends BaseMapper<Role> {
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.mapper;
import com.mikufufu.modules.system.model.entity.RoleMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 角色导航关系表 Mapper 接口
* </p>
*
*
* @since 2024-12-16
*/
public interface RoleMenuMapper extends BaseMapper<RoleMenu> {
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.mapper;
import com.mikufufu.modules.system.model.entity.RolePermission;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 角色权限关系表 Mapper 接口
* </p>
*
*
* @since 2024-12-16
*/
public interface RolePermissionMapper extends BaseMapper<RolePermission> {
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.mapper;
import com.mikufufu.modules.system.model.entity.Setting;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 系统设置表 Mapper 接口
* </p>
*
*
* @since 2024-12-16
*/
public interface SettingMapper extends BaseMapper<Setting> {
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.mapper;
import com.mikufufu.modules.storage.model.entity.Storage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 存储桶表 Mapper 接口
* </p>
*
*
* @since 2024-12-16
*/
public interface StorageMapper extends BaseMapper<Storage> {
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.mapper;
import com.mikufufu.modules.system.model.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 用户表 Mapper 接口
* </p>
*
*
* @since 2024-12-06
*/
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,121 @@
package com.mikufufu.modules.auth.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mikufufu.common.annotation.AnonymousApi;
import com.mikufufu.common.annotation.OperationLog;
import com.mikufufu.common.constant.RedisKey;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.common.enums.*;
import com.mikufufu.core.cache.RedisCache;
import com.mikufufu.core.utils.StringUtils;
import com.mikufufu.modules.auth.model.dto.ForgetPasswordParam;
import com.mikufufu.modules.auth.model.dto.LoginEmailParam;
import com.mikufufu.modules.auth.model.dto.LoginParam;
import com.mikufufu.modules.auth.model.dto.RegisterParam;
import com.mikufufu.modules.system.model.entity.User;
import com.mikufufu.modules.system.service.EmailService;
import com.mikufufu.modules.system.service.IUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Email;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 登录注册
*/
@Tag(name = "登录注册",description = "登录注册接口")
@RestController
@Validated
public class LoginController {
@Autowired
private IUserService userService;
@Autowired
private EmailService emailService;
@Operation(summary = "c端注册")
@PostMapping("/register")
@OperationLog(module = ModuleType.USER,type = OperationType.INSERT,description = "c端用户注册")
public AjaxResult<String> register(@RequestBody @Validated RegisterParam registerParam) {
return AjaxResult.data(userService.register(registerParam));
}
@Operation(summary = "c端登录")
@PostMapping("/login")
@OperationLog(module = ModuleType.USER,type = OperationType.LOGIN,description = "c端用户登录")
public AjaxResult<String> login(@RequestBody @Validated LoginParam loginParam) {
return AjaxResult.data(userService.login(loginParam, Channel.WEB));
}
@Operation(summary = "管理端登录")
@PostMapping("/admin/login")
@OperationLog(module = ModuleType.USER,type = OperationType.LOGIN,description = "管理端用户登录")
public AjaxResult<String> adminLogin(@RequestBody @Validated LoginParam loginParam) {
return AjaxResult.data(userService.login(loginParam,Channel.ADMIN));
}
@PostMapping("/admin/loginByEmail")
@Operation(summary = "管理端登录")
@OperationLog(module = ModuleType.USER,type = OperationType.LOGIN, description = "管理端登录")
public AjaxResult<String> adminLoginByEmail(@RequestBody LoginEmailParam loginEmailParam) {
return AjaxResult.data(userService.loginEmail(loginEmailParam,Channel.ADMIN));
}
@Operation(summary = "退出登录")
@GetMapping("/logout")
public AjaxResult<Boolean> logout() {
return AjaxResult.status(userService.logout());
}
@AnonymousApi
@GetMapping("/sendMailCode")
@Operation(summary = "发送邮箱验证码")
@OperationLog(module = ModuleType.SYSTEM,type = OperationType.SEND_EMAIL, description = "发送邮箱验证码")
public AjaxResult<Void> sendMailCode(@Email(message = "请输入正确的邮箱地址") String email) {
// 判断是否发送过验证码
if (RedisCache.exists(RedisKey.EMAIL_CODE_EXPIRATION_TIME + email)){
return AjaxResult.error("请勿频繁发送验证码");
}else if (RedisCache.exists(RedisKey.EMAIL_CODE + email)) {
RedisCache.delete(RedisKey.EMAIL_CODE + email);
}
if(!userService.exists(new LambdaQueryWrapper<User>().select(User::getId).eq(User::getEmail, email).eq(User::getStatus, UserStatus.NORMAL))){
return AjaxResult.error("该邮箱未注册");
}
// 生成验证码
String code = StringUtils.threadLocalRandom();
// 保存验证码到redis中
RedisCache.set(RedisKey.EMAIL_CODE + email, code, 6, TimeUnit.MINUTES);
RedisCache.set(RedisKey.EMAIL_CODE_EXPIRATION_TIME + email, 1, 1, TimeUnit.MINUTES);
// 构造邮件内容
Map<String, String> values = new HashMap<>();
values.put("code", code);
values.put("expire", "6");
values.put("time", LocalDateTime.now().plusMinutes(6).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
emailService.sendTemplateEmail("小缡", email, HtmlTemplate.EMAIL_CODE, values);
return AjaxResult.success();
}
@AnonymousApi
@PostMapping("/forgetPassword")
@Operation(summary = "忘记密码")
public AjaxResult<Boolean> forgetPassword(@RequestBody ForgetPasswordParam forgetPasswordParam) {
return AjaxResult.status(userService.forgetPassword(forgetPasswordParam));
}
}

View File

@@ -0,0 +1,33 @@
package com.mikufufu.modules.auth.model.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ForgetPasswordParam {
/**
* 邮箱
*/
@Email(message = "请输入正确的邮箱地址")
private String email;
/**
* 验证码
*/
@NotBlank( message = "验证码不能为空")
private String code;
/**
* 新密码
*/
@NotBlank( message = "新密码不能为空")
private String newPassword;
/**
* 确认密码
*/
@NotBlank( message = "确认密码不能为空")
private String confirmPassword;
}

View File

@@ -0,0 +1,18 @@
package com.mikufufu.modules.auth.model.dto;
import com.mikufufu.common.annotation.Account;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginEmailParam {
@Email(message = "请输入正确的邮箱地址")
@Account
private String email;
@NotBlank(message = "验证码不能为空")
private String code;
}

View File

@@ -0,0 +1,29 @@
package com.mikufufu.modules.auth.model.dto;
import com.mikufufu.common.annotation.Account;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 登录参数
*
*/
@Schema(description = "登录参数")
@Data
public class LoginParam {
/**
* 账号
*/
@Schema(description = "账号")
@NotBlank(message = "账号不能为空")
@Account
private String account;
/**
* 密码
*/
@Schema(description = "密码")
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@@ -0,0 +1,54 @@
package com.mikufufu.modules.auth.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 注册参数
*
*/
@Schema(description = "注册参数")
@Data
public class RegisterParam {
/**
* 账号
*/
@Schema(description = "账号")
@NotBlank(message = "账号不能为空")
private String account;
/**
* 昵称
*/
@Schema(description = "昵称")
@NotBlank(message = "昵称不能为空")
private String nickname;
/**
* 邮箱
*/
@Schema(description = "邮箱")
private String email;
/**
* 密码
*/
@Schema(description = "密码")
@NotBlank(message = "密码不能为空")
private String password;
/**
* 用户头像
*/
@Schema(description = "用户头像")
private String avatar;
/**
* 邀请码
*/
@Schema(description = "邀请码")
@NotBlank(message = "邀请码不能为空")
private String inviteCode;
}

View File

@@ -0,0 +1,110 @@
package com.mikufufu.modules.auth.model.entity;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serial;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
/**
* 用户鉴权实体类
*
*/
@Data
public class SysUser implements UserDetails {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
private Integer userId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 角色id
*/
private Integer roleId;
/**
* 角色
*/
private String role;
/**
* 权限列表
*/
private Set<String> permissions;
public SysUser(Integer userId, String username, String password, Integer roleId, String role, Set<String> permissions) {
this.userId = userId;
this.username = username;
this.password = password;
this.roleId = roleId;
this.role = role;
this.permissions = permissions;
}
/**
* Authorities 权限集合 用于存储用户权限信息
* @return 权限集合
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + role);
authorities.add(authority);
return authorities;
}
/**
* 判断帐号是否已经过期
* @return boolean
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 判断帐号是否已被锁定
* @return boolean
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 判断用户凭证是否已经过期
* @return boolean
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 判断用户帐号是否已启用
* @return boolean
*/
@Override
public boolean isEnabled() {
return true;
}
}

View File

@@ -0,0 +1,99 @@
package com.mikufufu.modules.auth.security;
import com.mikufufu.core.cache.RedisCache;
import com.mikufufu.common.constant.RedisKey;
import com.mikufufu.modules.auth.model.entity.SysUser;
import com.mikufufu.common.enums.UserStatus;
import com.mikufufu.common.exception.AuthException;
import com.mikufufu.core.utils.SpringUtils;
import com.mikufufu.core.utils.StringUtils;
import com.mikufufu.modules.system.model.entity.Permission;
import com.mikufufu.modules.system.model.entity.User;
import com.mikufufu.modules.system.service.IPermissionService;
import com.mikufufu.modules.system.service.IRoleService;
import lombok.extern.slf4j.Slf4j;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Token存储 如果有第三方也在这里实现
*
*/
@Slf4j
public class TokenStore {
private final static String TOKEN_PREFIX = RedisKey.USER + RedisKey.TOKEN ;
/**
* 生成用户token。
*
* @param user 用户对象
* @return 生成的token字符串
*/
public static String generateToken(User user) {
// 生成token
String token = UUID.randomUUID().toString().replace("-", "");
log.info("生成token: {}", token);
// 将token和对应的用户ID存储到Redis缓存中
RedisCache.set(TOKEN_PREFIX + token, user, 1, TimeUnit.DAYS);
return token;
}
/**
* 根据token获取用户对象。
*
* @param token 用户的token
* @return 用户对象
* @throws RuntimeException 如果token无效则抛出异常
*/
public static User getUserInToken(String token) {
// 从Redis缓存中根据token获取用户ID
User user = RedisCache.get(TOKEN_PREFIX + token,User.class);
if (StringUtils.isEmpty(user)) {
log.info("token无效");
return null;
}
// 通过用户ID从缓存中获取用户对象
return user;
}
/**
* 根据token获取系统用户对象包括权限信息。
*
* @param token 用户的token
* @return 系统用户对象如果用户不存在则返回null
*/
public static SysUser getSysUserInToken(String token) {
// 根据token获取用户对象
User user = getUserInToken(token);
if (StringUtils.isEmpty(user)) {
log.info("用户不存在");
return null;
}
if(UserStatus.DISABLED.getCode().equals(user.getStatus())){
throw new AuthException("用户已封号");
}
// 获取用户权限集合
Set<String> permissions = SpringUtils.getBean(IPermissionService.class).getPermissionList(user.getRole()).stream().map(Permission::getSign).collect(Collectors.toSet());
Integer roleId = user.getRole();
// 根据用户角色获取角色代码
String roleCode = SpringUtils.getBean(IRoleService.class).getCodeById(roleId);
// 构建并返回系统用户对象
return new SysUser(user.getId(), user.getUsername(), user.getPassword(),roleId, roleCode, permissions);
}
/**
* 删除指定的token。
*
* @param token 要删除的token
*/
public static void deleteToken(String token) {
log.info("删除token: {}", token);
// 从Redis缓存中删除指定的token
RedisCache.delete(TOKEN_PREFIX + token);
}
}

View File

@@ -0,0 +1,38 @@
package com.mikufufu.modules.auth.security.filter;
import com.mikufufu.core.utils.IpUtil;
import com.mikufufu.core.utils.StringUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 非发请求过滤器
* 请求示例:
* 假设 X-miku-verifyCode123456
* 则 X-miku-source123456-127.0.0.1-miku
*/
@Slf4j
public class IllegalRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String ip = IpUtil.getIpAddress(request);
if (StringUtils.isBlank(ip) || StringUtils.isBlank(request.getHeader("X-miku-verifyCode"))){
log.error("非法请求url:{},method:{},ip:{}", request.getRequestURI(), request.getMethod(), ip);
response.setStatus(404);
}
String verifyCode = request.getHeader("X-miku-verifyCode") + "-" + ip + "-miku";
if (StringUtils.isBlank(request.getHeader("X-miku-source")) || !request.getHeader("X-miku-source").equals(verifyCode)){
log.error("非法请求url:{},method:{},ip == {}", request.getRequestURI(), request.getMethod(), ip);
response.setStatus(404);
return;
}
// 继续执行过滤器链
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,63 @@
package com.mikufufu.modules.auth.security.filter;
import com.alibaba.fastjson2.JSON;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.common.enums.ResultCode;
import com.mikufufu.modules.auth.security.TokenStore;
import com.mikufufu.modules.auth.model.entity.SysUser;
import com.mikufufu.core.utils.StringUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
/**
* Token过滤器
*
*/
@Slf4j
public class TokenFilter extends OncePerRequestFilter {
private final String[] loginUrl = {"/login","/admin/login","/register"};
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) throws ServletException, IOException {
// 放行登录接口
if (Arrays.stream(loginUrl).anyMatch(url -> url.contains(request.getRequestURI()))){
chain.doFilter(request, response);
return;
}
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
chain.doFilter(request, response);
// 放行后不在执行后续过滤器
return;
}
// 从请求头中的token获取用户信息
SysUser user = TokenStore.getSysUserInToken(token);
if (StringUtils.isNotEmpty(user)) {
// 将认证对象设置到Security上下文中
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(user,token,user.getAuthorities())
);
log.info("用户{}认证成功",user.getUsername());
// 继续执行过滤器链
chain.doFilter(request, response);
}else {
response.setContentType("application/json;charset=UTF-8");
log.info("token无效");
// 将认证失败的信息封装为JSON格式并写入响应体中
response.getWriter().write(JSON.toJSONString(AjaxResult.error(ResultCode.UNAUTHORIZED.getCode(), "token无效!!")));
}
}
}

View File

@@ -0,0 +1,40 @@
package com.mikufufu.modules.auth.security.handler;
import com.alibaba.fastjson2.JSON;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.common.enums.ResultCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 权限不足处理器类,用于处理用户权限不足时的访问限制。
*
*/
@Slf4j // 使用Lombok的日志注解方便记录日志
@Component // 标记为Spring组件使得Spring容器能够自动发现和装配它
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
/**
* 处理权限不足的异常。
* @param request HttpServletRequest对象代表客户端的HTTP请求。
* @param response HttpServletResponse对象用于向客户端发送响应。
* @param accessDeniedException 访问被拒绝的异常实例。
* @throws IOException 如果发生I/O错误。
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
// 设置响应的内容类型为JSON支持UTF-8编码
response.setContentType("application/json;charset=UTF-8");
// 记录权限不足的错误日志
log.error("权限不足,无法访问{}", request.getRequestURI());
log.error("异常信息:{}", accessDeniedException.getMessage());
// 将错误信息封装为JSON格式并写入响应体中返回给客户端
response.getWriter().write(JSON.toJSONString(AjaxResult.error(ResultCode.FORBIDDEN.getCode(), accessDeniedException.getMessage())));
}
}

View File

@@ -0,0 +1,42 @@
package com.mikufufu.modules.auth.security.handler;
import com.alibaba.fastjson2.JSON;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.common.enums.ResultCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 认证失败处理类,用于处理未认证的请求,返回未授权的响应。
*
*/
@Slf4j
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
/**
* 当访问受保护的资源时,如果用户未通过认证,会调用此方法。
* @param request HttpServletRequest对象代表客户端的HTTP请求。
* @param response HttpServletResponse对象用于向客户端发送响应。
* @param authException 认证异常,封装了认证失败的详细信息。
* @throws IOException 如果在写入响应时发生IO错误。
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
// 设置响应内容类型为JSON并使用UTF-8编码
response.setContentType("application/json;charset=UTF-8");
// 记录认证失败的日志
log.error("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
// 将认证失败的信息封装为JSON格式并写入响应体中
response.getWriter().write(JSON.toJSONString(AjaxResult.error(ResultCode.UNAUTHORIZED.getCode(), "认证失败,无法访问系统资源")));
}
}

View File

@@ -0,0 +1,81 @@
package com.mikufufu.modules.auth.security.handler;
import com.mikufufu.common.permission.PermissionHandle;
import com.mikufufu.modules.auth.model.entity.SysUser;
import com.mikufufu.common.enums.RoleCode;
import com.mikufufu.modules.system.model.vo.PermissionRoleVO;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.util.AntPathMatcher;
import java.util.function.Supplier;
@Slf4j // 使用Lombok的日志注解方便记录日志
public class AuthorizationManagerImpl implements AuthorizationManager<RequestAuthorizationContext> {
/**
* Spring提供的用于匹配路径的匹配器
*/
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@PostConstruct
public void init() {
PermissionHandle.init();
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
// 获取请求的URL和HTTP方法
String url = context.getRequest().getRequestURI();
String method = context.getRequest().getMethod();
// 检查匿名权限
if (PermissionHandle.hasAnonymousPermission(url,method)){
return new AuthorizationDecision(true);
}
if (authentication.get() == null){
// 用户未登录,拒绝访问
return new AuthorizationDecision(false);
}
// 超管可以访问所有资源
if (authentication.get().getPrincipal() instanceof UserDetails){
SysUser user = (SysUser) authentication.get().getPrincipal();
// 获取用户角色
String roleCode = user.getRole();
if (RoleCode.ROLE_SYSTEM_ADMIN.getCode().equals(roleCode)){
return new AuthorizationDecision(true);
}
}
if (PermissionHandle.permissionRoleListIsNull()){
log.info("权限资源与权限的关系为空,重新加载");
PermissionHandle.loadDataSource();
}
// 获取权限与角色的关系
PermissionRoleVO permissionRoleVO = PermissionHandle.getPermissionRoleList().stream().filter(permission -> antPathMatcher.match(permission.getPath(), url) && permission.getMethod().equalsIgnoreCase(method)).findFirst().orElse(null);
log.info("权限权限与角色的关系:{}", permissionRoleVO);
if (permissionRoleVO == null || permissionRoleVO.getRoleList() == null || permissionRoleVO.getRoleList().isEmpty()){
// 没有权限
return new AuthorizationDecision(false);
}
// 放行数据库配置的匿名接口
if (permissionRoleVO.getRoleList().contains(RoleCode.ROLE_VISITOR.getCode())){
return new AuthorizationDecision(true);
}
if (authentication.get().getPrincipal() instanceof UserDetails){
// 非匿名用户,获取用户角色信息
SysUser user = (SysUser) authentication.get().getPrincipal();
// 获取用户角色
String roleCode = user.getRole();
// 判断用户角色是否包含权限的角色
if (permissionRoleVO.getRoleList().contains(roleCode)){
return new AuthorizationDecision(true);
}
}
return new AuthorizationDecision(false);
}
}

View File

@@ -0,0 +1,16 @@
package com.mikufufu.modules.auth.service;
/**
* 权限服务类
*
*/
public interface SecurityService{
/**
* 检查当前用户是否具有指定权限。
*
* @param permission 需要检查的权限字符串。如果为空或null则认为没有权限。
* @return boolean 如果当前用户具有指定权限则返回true否则返回false。
*/
boolean hasPermission(String permission);
}

View File

@@ -0,0 +1,31 @@
package com.mikufufu.modules.auth.service.impl;
import com.mikufufu.modules.auth.model.entity.SysUser;
import com.mikufufu.modules.auth.service.SecurityService;
import com.mikufufu.modules.auth.utils.AuthUtils;
import com.mikufufu.core.utils.StringUtils;
import org.springframework.stereotype.Service;
/**
* 权限服务实现类
*
*/
@Service("ss")
public class SecurityServiceImpl implements SecurityService {
@Override
public boolean hasPermission(String permission){
// 获取当前登录的用户对象
SysUser user = AuthUtils.getUser();
// 检查用户对象是否为空
if (StringUtils.isEmpty(user)) {
return false;
}
// 检查用户权限集合是否为空
if (StringUtils.isEmpty(user.getPermissions())){
return false;
}
// 检查用户权限集合中是否包含指定的权限
return user.getPermissions().contains(permission);
}
}

View File

@@ -0,0 +1,85 @@
package com.mikufufu.modules.auth.utils;
import com.mikufufu.modules.auth.model.entity.SysUser;
import com.mikufufu.common.exception.AuthException;
import com.mikufufu.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
/**
* 权限相关工具类
*
*/
@Slf4j
public class AuthUtils {
/**
* 获取当前认证的用户对象
* @return SysUser 当前认证的用户对象
*/
public static SysUser getUser() {
Authentication authentication = getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new AuthException("无法获取身份信息!!");
}
Object principal = authentication.getPrincipal();
// 假设SysUser实现了UserDetails接口
if (principal instanceof UserDetails) {
return (SysUser) principal;
}
return null;
}
/**
* 获取当前用户的用户名
* @return String 当前用户的用户名
*/
public static String getUsername() {
SysUser user = getUser();
if (StringUtils.isEmpty(user)) {
// throw new AuthException("用户对象为空,无法获取用户名");
// 修复因为获取用户对象为空,导致的报错
return "";
}
return user.getUsername();
}
/**
* 获取当前用户的用户ID
* @return Integer 当前用户的用户ID
*/
public static Integer getUserId() {
SysUser user = getUser();
if (StringUtils.isEmpty(user)) {
throw new AuthException("用户对象为空无法获取用户ID");
}
return user.getUserId();
}
/**
* 获取当前用户的token
* @return String 当前用户的token
*/
public static String getToken() {
Authentication authentication = getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new AuthException("无法获取身份信息!!");
}
Object credentials = authentication.getCredentials();
// 假设credentials是一个字符串
if (credentials instanceof String) {
return (String) credentials;
}
return null;
}
/**
* 获取当前认证的Authentication对象
* @return Authentication 当前认证的Authentication对象
*/
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
}

View File

@@ -0,0 +1,103 @@
package com.mikufufu.modules.storage.controller;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.core.utils.StringUtils;
import com.mikufufu.modules.storage.service.UploadService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterStyle;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@Tag(name = "文件上传", description = "用户上传文件")
@RequestMapping("/files")
public class FilesController {
@Autowired
private UploadService uploadService;
/**
* 上传文件 <br/>
* <p> @RequestPart("file") 是为了解决上传文件的时候报错也能让swagger正常使用上传文件的功能</p>
* @param file 文件
* @param pathName 文件路径
* @return 文件路径
*/
@Operation(summary = "上传文件")
@Parameters({
@Parameter(name = "file",description = "文件",style = ParameterStyle.FORM),
@Parameter(name = "pathName",description = "文件路径",style = ParameterStyle.FORM)
})
@PostMapping("/upload")
public AjaxResult<Map<String,String>> upload(@RequestPart("file") MultipartFile file, String pathName){
// 如果文件为空,则返回错误信息
if (file.isEmpty()){
return AjaxResult.error("文件不能为空");
}
// 如果文件路径为空,则使用默认路径
if (StringUtils.isBlank(pathName)){
return AjaxResult.data(uploadService.upload(file));
}
return AjaxResult.data(uploadService.upload(file,pathName));
}
@Operation(summary = "删除文件")
@Parameter(name = "fileName",description = "文件名",style = ParameterStyle.FORM,required = true)
@DeleteMapping("/delete")
public AjaxResult<Boolean> delete(String fileName){
return AjaxResult.status(uploadService.delete(fileName),"文件下载失败","文件下载失败");
}
@Operation(summary = "下载文件")
@Parameter(name = "fileName",description = "文件名",style = ParameterStyle.FORM,required = true)
@GetMapping("/download")
public AjaxResult<String> download(String fileName){
return AjaxResult.data(uploadService.download(fileName));
}
@Operation(summary = "下载图片")
@Parameter(name = "fileName",description = "文件名",required = true)
@GetMapping("/downloadImage")
public void downloadImage(String fileName, HttpServletResponse response){
try {
if (StringUtils.isBlank(fileName)){
throw new RuntimeException("文件名不能为空");
}
uploadService.downloadImage(fileName,response.getOutputStream());
}catch (RuntimeException | IOException e){
log.error("文件下载失败",e);
}
}
@Operation(summary = "查询文件列表")
@Parameter(name = "path",description = "文件路径")
@GetMapping("/listNotSubDir")
public AjaxResult<List<Map<String,String>>> listNotSubDir(String path){
return AjaxResult.data(uploadService.listObjects(path,false));
}
@Operation(summary = "查询文件列表")
@Parameter(name = "path",description = "文件路径")
@GetMapping("/listAndSubDir")
public AjaxResult<List<Map<String,String>>> listAndSubDir(String path){
return AjaxResult.data(uploadService.listObjects(path,true));
}
@Operation(summary = "查询文件列表")
@Parameter(name = "prefix",description = "文件前缀")
@GetMapping("/list")
public AjaxResult<List<?>> list(String prefix){
return AjaxResult.data(uploadService.list(prefix));
}
}

View File

@@ -0,0 +1,78 @@
package com.mikufufu.modules.storage.enums;
import com.mikufufu.modules.storage.strategy.mode.impl.LocalMode;
import com.mikufufu.modules.storage.strategy.mode.impl.MinioMode;
import com.mikufufu.modules.storage.strategy.mode.impl.OssMode;
import com.mikufufu.modules.storage.strategy.mode.StorageMode;
import com.mikufufu.core.utils.StringUtils;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 存储类型枚举类
*
*/
@Slf4j
@Getter
@AllArgsConstructor
public enum StorageType {
local(0,"local", LocalMode.class),
minio(1,"minio", MinioMode.class),
aliyun(2,"oss", OssMode.class),
;
private final Integer type;
private final String code;
/**
* 存储模式的类
*/
private final Class<? extends StorageMode> storageMode;
/**
*
* @param type 存储方式的类型
* @return {@link StorageType} 存储方式枚举类
*/
public static StorageType getStorageType(@NotNull Integer type){
for (StorageType storageType : StorageType.values()){
if (type.equals(storageType.getType())){
return storageType;
}
}
return null;
}
/**
* 获取存储模式
* @param code 存储方式的code
* @return {@link StorageMode} 存储模式
*/
public static StorageMode getStorageMode(String code){
if(StringUtils.isBlank(code)){
throw new RuntimeException("存储方式不能为空");
}
try {
// 遍历枚举类
for (StorageType storageType : StorageType.values()) {
if (storageType.getCode().equals(code)){
// return storageType.getStorageMode().newInstance();
// 17 版本以上废弃了 newInstance 方法可以改用Constructor 的 newInstance 方法来创建对象
Constructor<? extends StorageMode> constructor = storageType.getStorageMode().getDeclaredConstructor();
return constructor.newInstance();
}
}
} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
log.error("获取存储方式失败:{}",e.getMessage());
}
throw new RuntimeException("获取存储方式失败");
}
}

View File

@@ -0,0 +1,93 @@
package com.mikufufu.modules.storage.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serial;
import java.io.Serializable;
import com.mikufufu.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
* 存储桶表
* </p>
*
*
* @since 2024-12-16
*/
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("m_storage")
public class Storage extends BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 对象存储服务商目前支持阿里云、minio和本地
*/
@TableField("storage_code")
private String storageCode;
/**
* 对象存储服务的类型1.minio2.oss3.local
*/
@TableField("storage_type")
private Integer storageType;
/**
* 对象存储服务商名称
*/
@TableField("storage_name")
private String storageName;
/**
* 外链访问地址
*/
@TableField("host")
private String host;
/**
* API访问地址
*/
@TableField("endpoint")
private String endpoint;
/**
* 账号或者用户识别码
*/
@TableField("access_key")
private String accessKey;
/**
* 密钥
*/
@TableField("secret_key")
private String secretKey;
/**
* 存储桶名称
*/
@TableField("bucket_name")
private String bucketName;
/**
* icon的url链接
*/
@TableField("icon")
private String icon;
/**
* 是否强制使用路径模式1.使用2.不使用-默认)
*/
@TableField("forced_path_mode")
private Integer forcedPathMode;
}

View File

@@ -0,0 +1,36 @@
package com.mikufufu.modules.storage.service;
import com.mikufufu.modules.storage.model.entity.Storage;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 存储桶表 服务类
* </p>
*
*
* @since 2024-12-16
*/
public interface IStorageService extends IService<Storage> {
/**
* 根据存储服务编码获取对应的存储类型名称
* @param storageCode 存储服务编码
* @return 存储类型名称
*/
String getStorageType(String storageCode);
/**
* 获取默认存储服务
* @return 返回默认存储服务
*/
Storage getStorage();
/**
* 根据存储服务编码获取存储服务信息
* @param storageCode 存储服务编码
* @return 存储服务信息
*/
Storage getStorage(String storageCode);
}

View File

@@ -0,0 +1,65 @@
package com.mikufufu.modules.storage.service;
import org.springframework.web.multipart.MultipartFile;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
/**
* 文件服务接口
*
*/
public interface UploadService {
/**
* 文件上传
* @param multipartFile 文件流
* @return 文件资源链接
*/
Map<String,String> upload(MultipartFile multipartFile);
/**
* 文件上传
* @param multipartFile 文件流
* @param pathName 文件路径
* @return 文件资源链接
*/
Map<String,String> upload(MultipartFile multipartFile,String pathName);
/**
* 文件下载 外链
* @param fileName 文件名
* @return 文件资源链接
*/
String download(String fileName);
/**
* 文件删除 外链
* @param fileName 文件名
* @return flag 删除成功的标志
*/
Boolean delete(String fileName);
/**
* 图片下载二进制流
* @param fileName 文件名
* @param outputStream 输出流
*/
void downloadImage(String fileName, OutputStream outputStream);
/**
* 获取该前缀的对象列表信息 包括子目录下的对象
* @param objectNamePrefix 对象名前缀
* @param isSubDir 是否包含子目录
* @return 对象列表信息
*/
List<Map<String,String>> listObjects(String objectNamePrefix, Boolean isSubDir);
/**
* 获取该前缀的对象列表信息
* @param objectNamePrefix 对象名前缀
* @return 对象列表信息
*/
List<Map<String,String>> list(String objectNamePrefix);
}

View File

@@ -0,0 +1,53 @@
package com.mikufufu.modules.storage.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mikufufu.modules.storage.enums.StorageType;
import com.mikufufu.core.utils.StringUtils;
import com.mikufufu.modules.storage.model.entity.Storage;
import com.mikufufu.mapper.StorageMapper;
import com.mikufufu.modules.storage.service.IStorageService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mikufufu.modules.system.service.ISettingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 存储桶表 服务实现类
* </p>
*
*
* @since 2024-12-16
*/
@Service
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements IStorageService {
@Autowired
private ISettingService settingService;
@Override
public String getStorageType(String storageCode) {
Integer type = getStorage(storageCode).getStorageType();
StorageType storageType = StorageType.getStorageType(type);
return storageType!=null?storageType.getCode():null;
}
@Override
public Storage getStorage() {
return getStorage(settingService.getStorageCode());
}
@Override
public Storage getStorage(String storageCode) {
if(StringUtils.isBlank(storageCode)){
throw new RuntimeException("存储服务商编码为空");
}
Storage storage = getOne(new LambdaQueryWrapper<Storage>()
.eq(Storage::getStorageCode,storageCode)
);
if(StringUtils.isEmpty(storage)){
throw new RuntimeException("未找到存储方式");
}
return storage;
}
}

View File

@@ -0,0 +1,115 @@
package com.mikufufu.modules.storage.service.impl;
import com.mikufufu.modules.storage.strategy.mode.StorageMode;
import com.mikufufu.common.enums.UploadFileType;
import com.mikufufu.modules.storage.service.UploadService;
import com.mikufufu.modules.storage.strategy.StorageStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 文件处理接口实现类
*
*/
@Slf4j
@Service
public class UploadServiceImpl implements UploadService {
@Autowired
private StorageStrategy storageStrategy;
private StorageMode getStorageMode() {
return storageStrategy.getStorageMode();
}
@Override
public Map<String,String> upload(MultipartFile multipartFile) {
// 将上传文件的类型转换成全大写
String type = Objects.requireNonNull(multipartFile.getContentType()).split("/")[0].toUpperCase();
return upload(multipartFile, UploadFileType.valueOf(type).getPath());
}
@Override
public Map<String,String> upload(MultipartFile multipartFile, String pathName) {
String dateString = LocalDate.now().format(StorageMode.DATE_TIME_FORMATTER);
String fileName = dateString + "_" + multipartFile.getOriginalFilename();
// return StorageMode.upload(multipartFile,pathName,fileName);
InputStream inputStream = null;
try {
inputStream = multipartFile.getInputStream();
String url = getStorageMode().upload(multipartFile,pathName,fileName);
// TODO 实现缩略图功能
String thumbUrl = null;
// String thumbUrl = StorageMode.upload(ImageUtils.compressImageToInputStream(multipartFile,200,200),"thumb","thumb_" + fileName);
return new HashMap<>(){{
put("url",url);
put("thumbUrl",thumbUrl);
}};
}catch (IOException io){
log.error(io.getMessage());
}finally {
if (null != inputStream){
try {
inputStream.close();
}catch (Exception e){
log.error("文件流关闭失败:{}",e.getMessage());
}
}
}
return null;
}
@Override
public String download(String fileName) {
return getStorageMode().getObjectUrl(fileName);
}
@Override
public Boolean delete(String fileName) {
return getStorageMode().delete(fileName);
}
@Override
public void downloadImage(String fileName, OutputStream outputStream) {
InputStream stream = null;
try {
stream = getStorageMode().download(fileName);
} catch (Exception e) {
log.error("获取对象二进制流失败:{}",e.getMessage());
}
try {
if (stream == null) {
throw new IOException("文件下载失败");
}
BufferedImage bufferedImage = ImageIO.read(stream);
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
// ImageIO.write(bufferedImage, "jpg", outputStream);
ImageIO.write(bufferedImage, suffix, outputStream);
}catch (IOException e) {
log.error("二进制流转换成图片失败:{}",e.getMessage());
}
}
@Override
public List<Map<String,String>> listObjects(String objectNamePrefix, Boolean isSubDir) {
return getStorageMode().listObjects(objectNamePrefix,isSubDir);
}
@Override
public List<Map<String,String>> list(String objectNamePrefix) {
return getStorageMode().listObjects(objectNamePrefix);
}
}

View File

@@ -0,0 +1,89 @@
package com.mikufufu.modules.storage.strategy;
import com.mikufufu.core.utils.StringUtils;
import com.mikufufu.modules.storage.strategy.mode.StorageMode;
import com.mikufufu.modules.storage.enums.StorageType;
import com.mikufufu.modules.storage.service.IStorageService;
import com.mikufufu.modules.system.service.ISettingService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class StorageStrategy {
@Autowired
private ISettingService settingService;
@Autowired
private IStorageService storageService;
/**
* 存储模式
*/
private volatile StorageMode storageMode = null;
/**
* 获取存储类型。该方法首先从设置服务中获取存储编码,然后基于存储编码查询存储类型。
* 如果存储编码存在但对应的存储类型不存在,则记录错误日志并抛出运行时异常。
* 如果存储编码不存在,则默认返回"local"作为存储类型。
*
* @return 返回存储类型字符串。如果无法确定存储类型,则抛出运行时异常。
*/
private String getStorageType(){
// 从设置服务获取存储编码
String storageCode = settingService.getStorageCode();
if (StringUtils.isNotBlank(storageCode)) {
log.info("当前存储服务编码为:{}",storageCode);
// 根据存储编码获取存储类型
String storageType = storageService.getStorageType(storageCode);
log.info("当前存储服务类型为:{}",storageType);
if (StringUtils.isBlank(storageType)) {
log.error("当前存储服务商暂时不支持");
throw new RuntimeException("当前存储服务商暂时不支持");
}
// 返回获取到的存储类型
return storageType;
}
// 如果没有获取到存储编码,返回默认的存储类型"local"
return "local";
}
/**
* 初始化存储模式。如果存储模式尚未被初始化,则调用此方法。
* 由于使用了volatile关键字此方法确保了线程之间的可见性。
*/
private void initStorageMode() {
if (storageMode == null) {
synchronized (this) {
// 再次检查以避免双重检查锁定(DCL)的问题
if (storageMode == null) {
storageMode = StorageType.getStorageMode(getStorageType());
}
}
}
}
/**
* 更新存储模式。
* 该方法会检查当前的存储模式是否已经设置。如果已经设置,会根据获取的存储类型来更新存储模式;
* 如果未设置则会调用initStorageMode方法来初始化存储模式。
*
* @return 总是返回true表示存储模式已成功更新或初始化。
*/
public Boolean updateStorageMode() {
if (storageMode != null){
// 存储模式已设置,根据当前的存储类型更新存储模式
storageMode = StorageType.getStorageMode(getStorageType());
}else {
// 存储模式未设置,进行初始化
initStorageMode();
}
return true;
}
public StorageMode getStorageMode() {
initStorageMode();
return storageMode;
}
}

View File

@@ -0,0 +1,120 @@
package com.mikufufu.modules.storage.strategy.mode;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 存储模式接口类
*
*/
public interface StorageMode {
DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* 文件上传
* @param multipartFile 文件
* @return 文件资源链接
*/
String upload(MultipartFile multipartFile);
/**
* 文件上传
* @param multipartFile 文件
* @param objectName 文件路径
* @return 文件资源链接
*/
String upload(MultipartFile multipartFile,String objectName);
/**
* 文件上传
* @param pathName 文件路径
* @param multipartFile 文件
* @return 文件资源链接
*/
String upload(String pathName,MultipartFile multipartFile);
/**
* 文件上传
* @param multipartFile 文件
* @param pathName 文件路径
* @param objectName 文件名
* @return 文件资源链接
*/
String upload(MultipartFile multipartFile,String pathName,String objectName);
/**
* 文件上传
* @param inputStream 文件
* @param pathName 文件路径
* @param objectName 文件名
* @return 文件资源链接
*/
String upload(InputStream inputStream,String pathName,String objectName);
/**
* 文件下载 外链
* @param objectName 文件名
* @return {@link InputStream} 文件的二进制流
*/
InputStream download(String objectName);
/**
* 文件删除 外链
* @param objectName 文件名
* @return flag 删除成功的标志
*/
Boolean delete(String objectName);
/**
* 获取文件的外链
* @param objectName 文件名
* @return 文件的外链
*/
String getObjectUrl(String objectName);
/**
* 获取文件的外链
* @param objectName 文件名
* @param duration 有效时长
* @param unit 有效时长单位
* @return 文件的外链
*/
String getObjectUrl(String objectName, Integer duration, TimeUnit unit);
/**
* 获取文件的外链
* @param objectName 文件名
* @return 文件的外链
*/
String getObjectUrlLong(String objectName);
/**
* 获取该前缀的对象列表信息 包括子目录下的对象
* @param objectNamePrefix 对象名前缀
* @param isSubDir 是否包含子目录
* @return 对象列表信息
*/
List<Map<String,String>> listObjects(String objectNamePrefix, Boolean isSubDir);
/**
* minio对象存储 列出桶的对象列表信息
* @param objectNamePrefix 对象名前缀
* @param maxKeys 最大值
* @param isSubDir 是否包含子目录
* @return 对象列表信息
*/
List<Map<String,String>> listObjects(String objectNamePrefix, Integer maxKeys, Boolean isSubDir);
/**
* 获取该前缀的对象列表信息
* @param objectNamePrefix 对象名前缀
* @return 对象列表信息
*/
List<Map<String,String>> listObjects(String objectNamePrefix);
}

View File

@@ -0,0 +1,208 @@
package com.mikufufu.modules.storage.strategy.mode.impl;
import com.mikufufu.modules.storage.strategy.mode.StorageMode;
import com.mikufufu.core.utils.FileUtils;
import com.mikufufu.core.utils.SpringUtils;
import com.mikufufu.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 本地存储模式实现类
*
*/
@Slf4j
public class LocalMode implements StorageMode {
private final String host = SpringUtils.getHost();
private final String uploadPath = SpringUtils.getProperty("web.resource-path");
@Override
public String upload(MultipartFile multipartFile) {
return upload(multipartFile, multipartFile.getOriginalFilename());
}
@Override
public String upload(MultipartFile multipartFile, String objectName) {
return upload(multipartFile, "", objectName);
}
@Override
public String upload(String pathName, MultipartFile multipartFile) {
return upload(multipartFile, pathName, multipartFile.getOriginalFilename());
}
@Override
public String upload(MultipartFile multipartFile, String pathName, String objectName) {
if (null == multipartFile || 0 == multipartFile.getSize()) {
throw new RuntimeException("文件不能为空");
}
try {
String filePath = uploadPath + pathName;
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
String path = filePath + "/" + objectName;
multipartFile.transferTo(new File(path));
return host + "/" + pathName + "/" + objectName;
} catch (Exception e) {
log.error("文件上传失败", e);
throw new RuntimeException("文件上传失败");
}
}
@Override
public String upload(InputStream inputStream, String pathName, String objectName) {
try {
String filePath = uploadPath + pathName;
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
String fileName = filePath + "/" + objectName;
// 获取文件名的最后一个点的位置
int index = objectName.lastIndexOf(".");
if (index == -1) {
throw new RuntimeException("文件名没有后缀");
}
String suffix = objectName.substring(index + 1);
BufferedImage bufferedImage = ImageIO.read(inputStream);
ImageIO.write(bufferedImage,suffix,new File(fileName));
return host + "/" + pathName + "/" + objectName;
} catch (Exception e) {
log.error("文件上传失败", e);
throw new RuntimeException("文件上传失败");
}finally {
if (null != inputStream){
try {
inputStream.close();
}catch (Exception e){
log.error("文件流关闭失败:{}",e.getMessage());
}
}
}
}
@Override
public InputStream download(String objectName) {
if (StringUtils.isBlank(objectName)) {
throw new RuntimeException("文件名不能为空");
}
try {
// 本地文件路径
return new File(uploadPath + objectName).toURI().toURL().openStream();
} catch (Exception e) {
log.error("文件下载失败", e);
throw new RuntimeException("文件下载失败");
}
}
@Override
public Boolean delete(String objectName) {
if (StringUtils.isBlank(objectName)) {
return false;
}
try {
File file = new File(uploadPath + objectName);
if (file.exists()) {
return file.delete();
}
return false;
} catch (Exception e) {
log.error("文件删除失败", e);
throw new RuntimeException("文件删除失败");
}
}
@Override
public String getObjectUrl(String objectName) {
if (StringUtils.isBlank(objectName)) {
throw new RuntimeException("文件名不能为空");
}
return host + "/" + objectName;
}
@Override
public String getObjectUrl(String objectName, Integer duration, TimeUnit unit) {
return getObjectUrl(objectName);
}
@Override
public String getObjectUrlLong(String objectName) {
return getObjectUrl(objectName);
}
@Override
public List<Map<String, String>> listObjects(String objectNamePrefix, Boolean isSubDir) {
if (StringUtils.isBlank(objectNamePrefix)) {
throw new RuntimeException("文件名不能为空");
}
try {
// 本地文件路径
File file = new File(uploadPath + objectNamePrefix);
if (!file.exists()) {
throw new RuntimeException("文件不存在");
}
return Arrays.stream(Objects.requireNonNull(file.list())).map(fileName -> {
Map<String, String> map = new HashMap<>();
map.put("name", objectNamePrefix + "/" + fileName);
map.put("url", host + "/" + objectNamePrefix + "/" + fileName);
map.put("size", FileUtils.convertFileSize(new File(uploadPath + objectNamePrefix + "/" + fileName).length()));
// 将时间戳转换为Instant
Instant instant = Instant.ofEpochMilli(new File(uploadPath + objectNamePrefix + fileName).lastModified());
// 转换Instant到LocalDate使用默认时区
map.put("lastModified", DATE_TIME_FORMATTER.format(LocalDate.ofInstant(instant, ZoneId.systemDefault())));
return map;
}).collect(Collectors.toList());
} catch (Exception e) {
log.error("文件列表获取失败", e);
throw new RuntimeException("文件列表获取失败");
}
}
@Override
public List<Map<String, String>> listObjects(String objectNamePrefix, Integer maxKeys, Boolean isSubDir) {
if (StringUtils.isBlank(objectNamePrefix)) {
throw new RuntimeException("文件名不能为空");
}
try {
// 本地文件路径
File file = new File(uploadPath + objectNamePrefix);
if (!file.exists()) {
throw new RuntimeException("文件不存在");
}
return Arrays.stream(Objects.requireNonNull(file.list())).map(fileName -> {
Map<String, String> map = new HashMap<>();
map.put("name", objectNamePrefix + "/" + fileName);
map.put("url", host + "/" + objectNamePrefix + "/" + fileName);
map.put("size", FileUtils.convertFileSize(new File(uploadPath + objectNamePrefix + "/" + fileName).length()));
// 将时间戳转换为Instant
Instant instant = Instant.ofEpochMilli(new File(uploadPath + objectNamePrefix + fileName).lastModified());
// 转换Instant到LocalDate使用默认时区
map.put("lastModified", DATE_TIME_FORMATTER.format(LocalDate.ofInstant(instant, ZoneId.systemDefault())));
return map;
}).collect(Collectors.toList()).subList(0, maxKeys);
} catch (Exception e) {
log.error("文件列表获取失败", e);
throw new RuntimeException("文件列表获取失败");
}
}
@Override
public List<Map<String, String>> listObjects(String objectNamePrefix) {
return listObjects(objectNamePrefix, false);
}
}

View File

@@ -0,0 +1,293 @@
package com.mikufufu.modules.storage.strategy.mode.impl;
import com.mikufufu.modules.storage.strategy.mode.StorageMode;
import com.mikufufu.modules.storage.model.entity.Storage;
import com.mikufufu.modules.storage.service.IStorageService;
import com.mikufufu.core.utils.FileUtils;
import com.mikufufu.core.utils.SpringUtils;
import com.mikufufu.core.utils.StringUtils;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Minio存储模式实现类
*
*/
@Slf4j
public class MinioMode implements StorageMode {
/**
* 获取配置的Minio客户端
* @return Minio客户端
*/
public MinioClient getMinioClient(){
Storage storage = getOssProp();
if (storage != null) {
return MinioClient.builder()
.endpoint(storage.getEndpoint())
.credentials(storage.getAccessKey(), storage.getSecretKey())
.build();
}
throw new RuntimeException("未配置minio");
}
private Storage getOssProp(){
return SpringUtils.getBean(IStorageService.class).getStorage();
}
@Override
public String upload(MultipartFile multipartFile) {
// 判断上传文件是否为空
if (null == multipartFile || 0 == multipartFile.getSize()) {
throw new RuntimeException("文件不能为空");
}
log.info("打印文件资源名{}",multipartFile.getOriginalFilename());
return upload(multipartFile,multipartFile.getOriginalFilename());
}
@Override
public String upload(MultipartFile multipartFile, String objectName) {
// 判断上传文件是否为空
if (null == multipartFile || 0 == multipartFile.getSize()) {
throw new RuntimeException("文件不能为空");
}
return upload(multipartFile,"",objectName);
}
@Override
public String upload(String pathName, MultipartFile multipartFile) {
// 判断上传文件是否为空
if (null == multipartFile || 0 == multipartFile.getSize()) {
throw new RuntimeException("文件不能为空");
}
return upload(multipartFile,pathName,multipartFile.getOriginalFilename());
}
@Override
public String upload(MultipartFile multipartFile, String pathName, String objectName) {
// 判断上传文件是否为空
if (null == multipartFile || 0 == multipartFile.getSize()) {
throw new RuntimeException("文件不能为空");
}
InputStream inputStream = null;
try {
inputStream = multipartFile.getInputStream();
//minio的上传文件参数
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
//设置数据桶
.bucket(getOssProp().getBucketName())
//上传的文件名
.object(pathName + "/" + objectName)
//上传的文件类型
.contentType(multipartFile.getContentType())
//上传的文件二进制流
.stream(inputStream, multipartFile.getSize(), -1)
.build();
//调用MinioClient的putObject方法上传文件
getMinioClient().putObject(putObjectArgs);
String bucketName = getOssProp().getForcedPathMode() == 0 ? getOssProp().getBucketName() + "/":"";
if (StringUtils.isBlank(pathName)) {
return getOssProp().getHost() + "/" + bucketName + objectName;
}
return getOssProp().getHost() + "/" + bucketName + pathName + "/" + objectName;
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException("上传文件失败");
}finally {
if (null != inputStream){
try {
inputStream.close();
}catch (Exception e){
log.error("文件流关闭失败:{}",e.getMessage());
}
}
}
}
@Override
public String upload(InputStream inputStream, String pathName, String objectName) {
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(getOssProp().getBucketName())
.object(pathName + "/" + objectName)
.stream(inputStream,inputStream.available(),-1)
.build();
getMinioClient().putObject(putObjectArgs);
String bucketName = getOssProp().getForcedPathMode() == 0 ? getOssProp().getBucketName() + "/":"";
if (StringUtils.isBlank(pathName)) {
return getOssProp().getHost() + "/" + bucketName + objectName;
}
return getOssProp().getHost() + "/" + bucketName + pathName + "/" + objectName;
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException("文件上传失败");
}finally {
if (null != inputStream){
try {
inputStream.close();
}catch (Exception e){
log.error("文件流关闭失败:{}",e.getMessage());
}
}
}
}
@Override
public InputStream download(String objectName) {
try {
//minio的获取文件的参数
GetObjectArgs getObjectArgs = GetObjectArgs.builder()
.bucket(getOssProp().getBucketName())
.object(objectName)
.build();
//调用MinioClient的getObject方法获取文件
return getMinioClient().getObject(getObjectArgs);
}catch (Exception e){
log.error("获取文件失败:{}",e.getMessage());
throw new RuntimeException("文件下载失败");
}
}
@Override
public Boolean delete(String objectName) {
try {
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
.bucket(getOssProp().getBucketName())
.object(objectName)
.build();
getMinioClient().removeObject(removeObjectArgs);
return true;
}catch (Exception e){
log.error(e.getMessage());
return false;
}
}
@Override
public String getObjectUrl(String objectName) {
return getObjectUrl(objectName,7,TimeUnit.DAYS);
}
@Override
public String getObjectUrl(String objectName, Integer duration, TimeUnit unit) {
try {
if (null == duration || null == unit){
duration = 7;
unit = TimeUnit.DAYS;
}
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
.bucket(getOssProp().getBucketName())
.method(Method.GET)
.expiry(duration, unit)
.object(objectName)
.build();
return getMinioClient().getPresignedObjectUrl(args);
}catch (Exception e){
log.error(e.getMessage());
throw new RuntimeException("获取文件链接失败");
}
}
@Override
public String getObjectUrlLong(String objectName) {
return getObjectUrl(objectName,0,null);
}
@Override
public List<Map<String, String>> listObjects(String objectNamePrefix, Boolean isSubDir) {
return listObjects(objectNamePrefix, 1000, isSubDir);
}
@Override
public List<Map<String, String>> listObjects(String objectNamePrefix, Integer maxKeys, Boolean isSubDir) {
try {
ListObjectsArgs listObjectsArgs = ListObjectsArgs.builder()
.bucket(getOssProp().getBucketName())
//设置取出数量的最大值
.maxKeys(maxKeys)
//设置前缀
.prefix(objectNamePrefix)
.build();
Iterable<Result<Item>> listObjects = getMinioClient().listObjects(listObjectsArgs);
List<Map<String,String>> list = new ArrayList<>();
listObjects.forEach(itemResult -> {
try {
Item item = itemResult.get();
// 当这个对象是文件夹时
if (item.isDir()){
// 如果包含子文件夹的对象
if (isSubDir){
list.addAll(Objects.requireNonNull(listObjects(item.objectName(), maxKeys, isSubDir)));
}
}else {
Map<String,String> map = new HashMap<>();
map.put("name",item.objectName());
String bucketName = getOssProp().getForcedPathMode() == 0 ? getOssProp().getBucketName() + "/":"";
map.put("url",getOssProp().getHost() + "/" + bucketName + item.objectName());
map.put("size", FileUtils.convertFileSize(item.size()));
map.put("lastModified",item.lastModified().format(DATE_TIME_FORMATTER));
list.add(map);
}
// 写法2
// 当这个对象不是文件夹时
// if (!item.isDir()){
// String url = getOssProp().getHost() + "/" + getOssProp().getBucketName() + "/" + item.objectName();
// list.add(url);
// // 如果包含子文件夹的对象
// }else if (isSubDir){
// list.addAll(listObjects(item.objectName(), maxKeys, isSubDir));
// }
}catch (Exception e){
log.error(e.getMessage());
}
});
return list;
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException("获取存储对象列表失败");
}
}
@Override
public List<Map<String, String>> listObjects(String objectNamePrefix) {
try {
ListObjectsArgs listObjectsArgs = ListObjectsArgs.builder()
.bucket(getOssProp().getBucketName())
//设置前缀
.prefix(objectNamePrefix)
.build();
Iterable<Result<Item>> listObjects = getMinioClient().listObjects(listObjectsArgs);
List<Map<String,String>> list = new ArrayList<>();
listObjects.forEach(itemResult -> {
try {
Item item = itemResult.get();
// 当这个对象是文件夹时
if (item.isDir()){
list.addAll(Objects.requireNonNull(listObjects(item.objectName())));
}else {
Map<String,String> map = new HashMap<>();
map.put("name",item.objectName());
String bucketName = getOssProp().getForcedPathMode() == 0 ? getOssProp().getBucketName() + "/":"";
map.put("url",getOssProp().getHost() + "/" + bucketName + item.objectName());
map.put("size", FileUtils.convertFileSize(item.size()));
map.put("lastModified",item.lastModified().format(DATE_TIME_FORMATTER));
list.add(map);
}
}catch (Exception e){
log.error(e.getMessage());
}
});
return list;
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException("获取存储对象列表失败");
}
}
}

View File

@@ -0,0 +1,363 @@
package com.mikufufu.modules.storage.strategy.mode.impl;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.ListObjectsRequest;
import com.aliyun.oss.model.OSSObjectSummary;
import com.aliyun.oss.model.ObjectListing;
import com.mikufufu.modules.storage.strategy.mode.StorageMode;
import com.mikufufu.modules.storage.model.entity.Storage;
import com.mikufufu.modules.storage.service.IStorageService;
import com.mikufufu.core.utils.FileUtils;
import com.mikufufu.core.utils.SpringUtils;
import com.mikufufu.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 阿里OSS存储模式实现类
*
*/
@Slf4j
public class OssMode implements StorageMode {
/**
* 获取配置的阿里OSS客户端
* @return 阿里OSS客户端
*/
public OSS getOssClient(){
Storage storage = getOssProp();
if (storage != null) {
return new OSSClientBuilder().build(
storage.getEndpoint(),
storage.getAccessKey(),
storage.getSecretKey()
);
}
throw new RuntimeException("未配置minio");
}
private Storage getOssProp(){
return SpringUtils.getBean(IStorageService.class).getStorage();
}
@Override
public String upload(MultipartFile multipartFile) {
// 判断上传文件是否为空
if (null == multipartFile || 0 == multipartFile.getSize()) {
throw new RuntimeException("文件不能为空");
}
return upload(multipartFile, multipartFile.getOriginalFilename());
}
@Override
public String upload(MultipartFile multipartFile, String objectName) {
if(isExist(objectName)){
return getObjectUrlLong(objectName);
}else {
// 判断上传文件是否为空
if (null == multipartFile || 0 == multipartFile.getSize()) {
throw new RuntimeException("文件不能为空");
}
return upload(multipartFile, "", objectName);
}
}
/**
* 阿里OSS 判断文件是否已存在
* @param objectName 文件名称
* @return 文件是否存在 如果返回值为true则文件存在否则存储空间或者文件不存在。
*/
public boolean isExist(String objectName){
try {
// 判断oss上文件是否存在
return getOssClient().doesObjectExist(getOssProp().getBucketName(), objectName);
}catch (OSSException oe){
log.error("捕获到OSSException这意味着您的请求已发送到OSS "
+ "但是由于某种原因以错误响应被拒绝。");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
}catch (ClientException ce){
log.error("捕获ClientException这意味着客户端遇到"
+ "在尝试与OSS通信时出现了严重的内部问题"
+ "例如不能接入网络。");
log.error("Error Message:" + ce.getMessage());
}
return false;
}
@Override
public String upload(String pathName, MultipartFile multipartFile) {
// 判断上传文件是否为空
if (null == multipartFile || 0 == multipartFile.getSize()) {
throw new RuntimeException("文件不能为空");
}
return upload(multipartFile, pathName, multipartFile.getOriginalFilename());
}
@Override
public String upload(MultipartFile multipartFile, String pathName, String objectName) {
// 判断上传文件是否为空
if (null == multipartFile || 0 == multipartFile.getSize()) {
throw new RuntimeException("文件不能为空");
}
InputStream inputStream = null;
try {
inputStream = multipartFile.getInputStream();
// 上传文件
getOssClient().putObject(
// 存储空间
getOssProp().getBucketName(),
// 上传的文件名
pathName + "/" + objectName,
// 上传文件的输入流
inputStream
);
// 返回文件上传路径
if (StringUtils.isEmpty(pathName)) {
return getOssProp().getHost() + "/" + objectName;
}
return getOssProp().getHost() + "/" + pathName + "/" + objectName;
} catch (OSSException oe){
log.error("捕获到OSSException这意味着您的请求已发送到OSS "
+ "但是由于某种原因以错误响应被拒绝。");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
throw new RuntimeException("上传失败");
}catch (ClientException ce){
log.error("捕获ClientException这意味着客户端遇到"
+ "在尝试与OSS通信时出现了严重的内部问题"
+ "例如不能接入网络。");
log.error("Error Message:" + ce.getMessage());
throw new RuntimeException("上传失败");
}catch (IOException ioe){
log.error("捕获IOException这意味着客户端遇到"
+ "在尝试与OSS通信时出现了严重的内部问题"
+ "例如不能接入网络。");
log.error("Error Message:" + ioe.getMessage());
throw new RuntimeException("上传失败");
}finally {
if (null != inputStream){
try {
inputStream.close();
}catch (Exception e){
log.error("文件流关闭失败:{}",e.getMessage());
}
}
}
}
@Override
public String upload(InputStream inputStream, String pathName, String objectName) {
try {
getOssClient().putObject(
getOssProp().getBucketName(),
pathName + "/" + objectName,
inputStream
);
if (StringUtils.isBlank(pathName)) {
return getOssProp().getHost() + "/" + objectName;
}
return getOssProp().getHost() + "/" + pathName + "/" + objectName;
} catch (OSSException oe){
log.error("捕获到OSSException这意味着您的请求已发送到OSS "
+ "但是由于某种原因以错误响应被拒绝。");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
throw new RuntimeException("上传失败");
}catch (ClientException ce){
log.error("捕获ClientException这意味着客户端遇到"
+ "在尝试与OSS通信时出现了严重的内部问题"
+ "例如不能接入网络。");
log.error("Error Message:" + ce.getMessage());
throw new RuntimeException("上传失败");
} finally {
if (null != inputStream){
try {
inputStream.close();
}catch (Exception e){
log.error("文件流关闭失败:{}",e.getMessage());
}
}
}
}
@Override
public InputStream download(String objectName) {
try {
// 下载文件
return getOssClient().getObject(getOssProp().getBucketName(), objectName).getObjectContent();
} catch (OSSException oe) {
log.error("捕获到OSSException这意味着您的请求已发送到OSS "
+ "但是由于某种原因以错误响应被拒绝。");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
throw new RuntimeException("下载文件失败");
} catch (ClientException ce) {
log.error("捕获ClientException这意味着客户端遇到"
+ "在尝试与OSS通信时出现了严重的内部问题"
+ "例如不能接入网络。");
log.error("Error Message:" + ce.getMessage());
throw new RuntimeException("下载文件失败");
}
}
@Override
public Boolean delete(String objectName) {
try {
// 删除文件
getOssClient().deleteObject(getOssProp().getBucketName(), objectName);
return true;
} catch (OSSException oe) {
log.error("捕获到OSSException这意味着您的请求已发送到OSS "
+ "但是由于某种原因以错误响应被拒绝。");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
log.error("捕获ClientException这意味着客户端遇到"
+ "在尝试与OSS通信时出现了严重的内部问题"
+ "例如不能接入网络。");
log.error("Error Message:" + ce.getMessage());
}
return false;
}
@Override
public String getObjectUrl(String objectName) {
return getObjectUrl(objectName, 7, TimeUnit.DAYS);
}
@Override
public String getObjectUrl(String objectName, Integer duration, TimeUnit unit) {
try {
return getOssClient().generatePresignedUrl(getOssProp().getBucketName(),
objectName,
// 设置URL过期时间
new Date(System.currentTimeMillis() + unit.toMillis(duration))
).toString();
} catch (OSSException oe) {
log.error("捕获到OSSException这意味着您的请求已发送到OSS "
+ "但是由于某种原因以错误响应被拒绝。");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
throw new RuntimeException("获取外链失败");
} catch (ClientException ce) {
log.error("捕获ClientException这意味着客户端遇到"
+ "在尝试与OSS通信时出现了严重的内部问题"
+ "例如不能接入网络。");
log.error("Error Message:" + ce.getMessage());
throw new RuntimeException("获取外链失败");
}
}
@Override
public String getObjectUrlLong(String objectName) {
try {
// 判断文件是否存在。如果返回值为true则文件存在否则存储空间或者文件不存在。
boolean exist = getOssClient().doesObjectExist(getOssProp().getBucketName(), objectName);
if (exist) {
// 获取文件外链
return getOssProp().getHost() + "/" + objectName;
}
throw new RuntimeException("文件不存在");
} catch (OSSException oe) {
log.error("捕获到OSSException这意味着您的请求已发送到OSS "
+ "但是由于某种原因以错误响应被拒绝。");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
throw new RuntimeException("获取外链失败");
} catch (ClientException ce) {
log.error("捕获ClientException这意味着客户端遇到"
+ "在尝试与OSS通信时出现了严重的内部问题"
+ "例如不能接入网络。");
log.error("Error Message:" + ce.getMessage());
throw new RuntimeException("获取外链失败");
}
}
@Override
public List<Map<String, String>> listObjects(String objectNamePrefix, Boolean isSubDir) {
return listObjects(objectNamePrefix, 100, isSubDir);
}
@Override
public List<Map<String, String>> listObjects(String objectNamePrefix, Integer maxKeys, Boolean isSubDir) {
try {
ListObjectsRequest listObjectsRequest = new ListObjectsRequest(getOssProp().getBucketName())
// 列举文件。objectNamePrefix则列举存储空间下的所有文件。objectNamePrefix则列举包含指定前缀的文件。
.withPrefix(objectNamePrefix)
// 设置最大个数。
.withMaxKeys(maxKeys);
ObjectListing objectListing = getOssClient().listObjects(listObjectsRequest);
// 遍历所有文件。
List<OSSObjectSummary> sums = objectListing.getObjectSummaries();
// 获取该资源空间下所有objectName 例如:[test/1.txt, test/2.txt]
// stream().map()方法是将list中的每一个元素映射成一个新的元素然后将这些新的元素组成一个Stream流。
// collect(Collectors.toList())方法是将流中的元素收集到List中。
// return sums.stream().map(OSSObjectSummary::getKey).collect(Collectors.toList());
return sums.stream().map(ossObject -> {
Map<String,String> map = new HashMap<>();
map.put("name",ossObject.getKey());
map.put("url",getOssProp().getHost() + "/" + ossObject.getKey());
map.put("size", FileUtils.convertFileSize(ossObject.getSize()));
Instant instant = ossObject.getLastModified().toInstant();
LocalDate localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate();
map.put("lastModified",localDate.format(DATE_TIME_FORMATTER));
return map;
}).collect(Collectors.toList());
} catch (OSSException oe) {
log.error("捕获到OSSException这意味着您的请求已发送到OSS "
+ "但是由于某种原因以错误响应被拒绝。");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
throw new RuntimeException("获取对象存储列表失败");
} catch (ClientException ce) {
log.error("捕获ClientException这意味着客户端遇到"
+ "在尝试与OSS通信时出现了严重的内部问题"
+ "例如不能接入网络。");
log.error("Error Message:" + ce.getMessage());
throw new RuntimeException("获取对象存储列表失败");
}
// finally {
// // 关闭OSSClient。
// if (ossClient != null) {
// ossClient.shutdown();
// }
// }
}
@Override
public List<Map<String, String>> listObjects(String objectNamePrefix) {
return listObjects(objectNamePrefix, 100,false);
}
}

View File

@@ -0,0 +1,26 @@
package com.mikufufu.modules.system.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.modules.system.model.dto.LogDto;
import com.mikufufu.modules.system.model.entity.OperateLog;
import com.mikufufu.modules.system.service.IOperateLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/log")
public class OperateLogController {
@Autowired
private IOperateLogService operateLogService;
@PostMapping("/page")
public AjaxResult<IPage<OperateLog>> page(@RequestBody LogDto logDto) {
return AjaxResult.data(operateLogService.getPage(logDto));
}
}

View File

@@ -0,0 +1,80 @@
package com.mikufufu.modules.system.controller;
import com.mikufufu.common.annotation.AnonymousApi;
import com.mikufufu.common.annotation.OperationLog;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.common.enums.ModuleType;
import com.mikufufu.common.enums.OperationType;
import com.mikufufu.modules.system.model.entity.Permission;
import com.mikufufu.modules.system.model.vo.PermissionRoleVO;
import com.mikufufu.modules.system.service.IPermissionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 权限管理
*
*/
@RestController
@Tag(name = "权限",description = "权限管理")
@RequestMapping("/permission")
public class PermissionController {
@Autowired
private IPermissionService permissionService;
@Operation(summary = "获取权限列表")
@Parameter(name = "roleId",description = "角色id",required = true)
@GetMapping("/getSignList")
public AjaxResult<List<String>> getSignList(Integer roleId) {
return AjaxResult.data(permissionService.getPermissionList(roleId).stream()
.filter(permission -> permission.getSign() != null && permission.getStatus() == 0 )
.map(Permission::getSign)
.distinct()
.toList());
}
@Operation(summary = "获取权限角色列表")
@GetMapping("/getPermissionRoleList")
public AjaxResult<List<PermissionRoleVO>> getPermissionRoleList() {
return AjaxResult.data(permissionService.getPermissionRoleList());
}
@GetMapping("/addRolePermission")
@Operation(summary = "为指定角色新增权限")
@Parameters({
@Parameter(name = "roleId", description = "角色id", required = true),
@Parameter(name = "permissionId", description = "权限id", required = true)
})
public AjaxResult<Boolean> addRolePermission(Integer roleId, Integer permissionId) {
return AjaxResult.status(permissionService.addRolePermission(roleId, permissionId));
}
@Operation(summary = "保存所有权限")
@GetMapping("/saveAllPermission")
@OperationLog(module = ModuleType.SYSTEM,type = OperationType.INSERT,description = "保存所有权限")
public AjaxResult<Boolean> saveAllPermissionOfController() {
return AjaxResult.status(permissionService.saveAllPermissionOfController());
}
@AnonymousApi
@Operation(summary = "放行所有权限")
@GetMapping("/grantAnonymousPermission")
@OperationLog(module = ModuleType.SYSTEM,type = OperationType.DELETE,description = "释放所有权限")
public AjaxResult<Boolean> grantAnonymousPermission() {
return AjaxResult.status(permissionService.grantAnonymousPermission());
}
@Operation(summary = "清除所有权限")
@GetMapping("/cleanPermissionRole")
public AjaxResult<Boolean> clean() {
return AjaxResult.status(permissionService.cleanPermissionRole());
}
}

View File

@@ -0,0 +1,41 @@
package com.mikufufu.modules.system.controller;
import com.mikufufu.common.annotation.AnonymousApi;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.core.utils.BeanUtil;
import com.mikufufu.modules.system.model.entity.Role;
import com.mikufufu.modules.system.model.vo.RoleVO;
import com.mikufufu.modules.system.service.IRoleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 角色管理
*
*/
@RestController
@Tag(name = "角色",description = "角色管理")
@RequestMapping("/role")
public class RoleController {
@Autowired
private IRoleService roleService;
@AnonymousApi
@Operation(summary = "获取角色列表")
@GetMapping("/list")
public AjaxResult<List<RoleVO>> list(){
List<Role> list = roleService.list();
if(list != null && !list.isEmpty()){
return AjaxResult.data(list.stream().map(r -> BeanUtil.copy(r, RoleVO.class)).toList());
}
return AjaxResult.error();
}
}

View File

@@ -0,0 +1,60 @@
package com.mikufufu.modules.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.modules.system.model.entity.Setting;
import com.mikufufu.modules.system.model.vo.EmailConfig;
import com.mikufufu.modules.system.model.vo.WebSiteVO;
import com.mikufufu.modules.system.service.ISettingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/setting")
public class SettingController {
@Autowired
private ISettingService settingService;
@GetMapping("/getByCode")
public AjaxResult<Setting> get(String code){
// 根据配置编号查询并返回对应的系统设置信息
return AjaxResult.data(settingService.getOne(new LambdaQueryWrapper<Setting>().eq(Setting::getCode,code)));
}
@GetMapping("/getStorageCode")
public AjaxResult<String> getStorageCode() {
return AjaxResult.data(settingService.getStorageCode());
}
@PostMapping("/add")
public AjaxResult<Boolean> add(Setting setting) {
return AjaxResult.status(settingService.save(setting));
}
@PutMapping("/update")
public AjaxResult<Boolean> update(Setting setting) {
return AjaxResult.status(settingService.updateById(setting));
}
@DeleteMapping("/delete")
public AjaxResult<Boolean> delete(String code) {
return AjaxResult.status(settingService.remove(new LambdaQueryWrapper<Setting>().eq(Setting::getCode,code)));
}
@PostMapping("/saveWebInfo")
public AjaxResult<Boolean> add(@RequestBody WebSiteVO webSiteVO) {
return AjaxResult.status(settingService.saveWebInfo(webSiteVO));
}
@GetMapping("/getEmail")
public AjaxResult<EmailConfig> getEmail() {
EmailConfig emailConfig = settingService.getMail();
return AjaxResult.data(emailConfig);
}
@PostMapping("/setEmail")
public AjaxResult<Boolean> setEmail(@RequestBody EmailConfig emailConfig) {
return AjaxResult.status(settingService.setMail(emailConfig));
}
}

View File

@@ -0,0 +1,79 @@
package com.mikufufu.modules.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.mikufufu.common.entity.AjaxResult;
import com.mikufufu.common.entity.Query;
import com.mikufufu.modules.auth.utils.AuthUtils;
import com.mikufufu.modules.system.model.entity.User;
import com.mikufufu.modules.system.model.vo.UserVO;
import com.mikufufu.modules.system.service.IUserService;
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.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/user")
@Tag(name = "用户接口")
public class UserController {
@Autowired
private IUserService userService;
@GetMapping("/detail")
@Operation(summary = "查询用户详情")
@Parameter(name = "id", description = "用户id", required = true)
public AjaxResult<UserVO> detail(Integer id) {
User user = userService.getById(id);
return AjaxResult.data(UserVO.convert(user));
}
/**
* 通过token查询用户数据
* @return 用户数据
*/
@Operation(summary = "通过token查询用户数据", description = "通过token查询用户数据")
@GetMapping("/info")
public AjaxResult<UserVO> getUserInfo() {
Integer id = AuthUtils.getUserId();
User user = userService.getById(id);
return AjaxResult.data(UserVO.convert(user));
}
@GetMapping("/page")
@Operation(summary = "获取用户列表")
public AjaxResult<IPage<User>> page(Query query) {
return AjaxResult.data(userService.page(query.getPage(),new LambdaQueryWrapper<User>().eq(User::getStatus, 0)));
}
@PostMapping("/add")
@Operation(summary = "添加用户")
public AjaxResult<Boolean> add(@RequestBody User user) {
return AjaxResult.data(userService.save(user));
}
@PutMapping("/update")
@Operation(summary = "更新用户")
public AjaxResult<Boolean> update(@RequestBody User user) {
return AjaxResult.data(userService.updateById(user));
}
@DeleteMapping("/delete")
@Operation(summary = "删除用户")
@Parameter(name = "id", description = "用户id", required = true)
public AjaxResult<Boolean> delete(Integer id) {
return AjaxResult.data(userService.removeById(id));
}
@GetMapping("/export")
@Operation(summary = "导出用户数据")
public void export(HttpServletResponse response) {
userService.export(response);
}
}

View File

@@ -0,0 +1,57 @@
package com.mikufufu.modules.system.model.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.mikufufu.common.entity.Query;
import com.mikufufu.common.enums.OperationType;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
@EqualsAndHashCode(callSuper = true)
@Data
public class LogDto extends Query {
/**
* 模块
*/
private Integer module;
/**
* 操作类型 {@link OperationType} 默认 插入
*/
private Integer operateType;
/**
* 操作人
*/
private String operator;
/**
* 操作方法
*/
private String methodName;
/**
* 操作时间 开始
*/
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate startDate;
/**
* 操作时间 结束
*/
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate endDate;
/**
* 操作状态 1. 成功 0. 失败
*/
private String status;
/**
* ip地址
*/
private String ip;
}

View File

@@ -0,0 +1,77 @@
package com.mikufufu.modules.system.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.mikufufu.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
/**
* <p>
* 菜单信息表
* </p>
*
*
* @since 2024-12-16
*/
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("m_menu")
public class Menu extends BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 菜单id
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 菜单的名字
*/
@TableField("menu_name")
private String menuName;
/**
* 菜单的路由地址
*/
@TableField("path")
private String path;
/**
* 菜单对应组件地址
*/
@TableField("component")
private String component;
/**
* 父菜单id
*/
@TableField("parent_id")
private Integer parentId;
/**
* 菜单图标
*/
@TableField("icon")
private String icon;
/**
* 是否隐藏0.显示1.隐藏)
*/
@TableField("status")
private Integer status;
/**
* 排序
*/
@TableField("sort")
private String sort;
}

View File

@@ -0,0 +1,90 @@
package com.mikufufu.modules.system.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 操作日志表
*/
@Data
@TableName(value = "m_operate_log")
public class OperateLog implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 功能模块
*/
@TableField(value = "`module`")
private Integer module;
/**
* 接口地址
*/
@TableField(value = "url")
private String url;
/**
* 请求参数
*/
@TableField(value = "params")
private String params;
/**
* 操作类型
*/
@TableField(value = "operate_type")
private Integer operateType;
/**
* 方法名
*/
@TableField(value = "method_name")
private String methodName;
/**
* 操作人
*/
@TableField(value = "`operator`")
private String operator;
/**
* ip地址
*/
@TableField(value = "ip")
private String ip;
/**
* 操作时间
*/
@TableField(value = "operate_time")
private LocalDateTime operateTime;
/**
* 附加信息
*/
@TableField(value = "remark")
private String remark;
/**
* 状态1.成功0.失败)
*/
@TableField(value = "`status`")
private Integer status;
/**
* 错误信息
*/
@TableField(value = "error_message")
private String errorMessage;
}

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