--- File: .gitignore ---
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
target
.idea
*.iml
nb-configuration.xml
nbactions.xml
.project
.settings
*/.settings
*/.project
.classpath
*/.idea
.idea/
.vscode/
.settings/
target/
ideaproj/
*/bin
.DS_Store
*.ipr
*.iws
bin/
nbproject/
.springBeans
/log/
/logs/
/storage/
/var/log/*
*.factorypath
/storage/
--- File: README.md ---
# PeiPei 后端项目
基于 Spring Boot 的多模块项目,为 PeiPei 应用提供后端服务。
## 项目结构
- **play-admin**: 主要的 Spring Boot 应用模块
- **play-common**: 公共工具类和共享代码
- **play-generator**: 代码生成工具
## 构建要求
- **Java 11** (必须)
- **Maven 3.6+**
## 快速开始
### 1. 安装 Java 11
在 macOS 上使用 Homebrew:
```bash
brew install --cask zulu@11
```
设置 Java 11 为默认版本:
#### 临时设置 (当前会话):
```bash
export JAVA_HOME=$(/usr/libexec/java_home -v 11)
```
#### 永久设置方法:
**方法1: Shell 配置 (推荐)**
添加到 `~/.zshrc` 或 `~/.bash_profile`:
```bash
export JAVA_HOME=$(/usr/libexec/java_home -v 11)
export PATH="$JAVA_HOME/bin:$PATH"
```
**方法2: Maven 专用**
创建 `~/.mavenrc`:
```bash
export JAVA_HOME=$(/usr/libexec/java_home -v 11)
```
**方法3: VS Code 项目配置**
项目已配置 `.vscode/settings.json` 使用 Java 11:
```json
{
"java.configuration.runtimes": [
{
"name": "JavaSE-11",
"path": "/usr/libexec/java_home -v 11"
}
],
"java.jdt.ls.java.home": "/usr/libexec/java_home -v 11"
}
```
### 2. 构建项目
```bash
# 清理并构建所有模块
mvn clean install
# 或者仅编译
mvn clean compile
```
### 3. 运行应用
```bash
# 运行主应用
java -jar play-admin/target/play-admin-1.0.jar
# 或使用 Maven
cd play-admin
mvn spring-boot:run
```
## 配置说明
项目在所有模块中统一使用 Java 11:
- 所有模块都配置为 Java 11 源码和目标版本
- Lombok 注解自动处理
- 无需显式配置注解处理器
## 开发说明
- 项目已更新为所有模块统一使用 Java 11
- Lombok 依赖使用 `scope=provided` 启用自动注解处理
- Maven 编译插件继承 Spring Boot 父 POM 配置
### 代码格式化和质量检查
项目集成了 Spotless 和 Checkstyle 插件:
#### Spotless (代码格式化)
- 基于空格的缩进 (4个空格)
- 自动清理尾随空白字符
- 文件末尾添加换行符
- 基本的导入组织
常用命令:
```bash
# 检查代码格式
mvn spotless:check
# 自动格式化代码
mvn spotless:apply
```
#### Checkstyle (代码规范检查)
- 使用 Sun Java 编码规范 (比 Google 规范更宽松)
- 在编译时自动检查代码规范
- 与 Java 11 和 Lombok 兼容
常用命令:
```bash
# 检查代码规范
mvn checkstyle:check
# 生成规范检查报告
mvn checkstyle:checkstyle
```
#### 集成命令
```bash
# 格式化代码并编译
mvn spotless:apply compile
# 完整检查 (格式化 + 规范检查 + 编译)
mvn spotless:apply checkstyle:check compile
```
## 模块介绍
### play-admin
主要的 Spring Boot 应用,包含:
- REST API 接口
- 安全配置
- 数据库集成
- 微信集成
### play-common
共享工具库,包含:
- 公共域对象
- 工具类
- Redis 配置
- 安全工具
### play-generator
代码生成工具,包含:
- MyBatis Plus 代码生成
- 基于模板的代码生成
## 故障排除
### 常见问题
**编译失败: "cannot find symbol" 错误**
- 确保使用 Java 11: `java -version` 应显示 Java 11
- 设置正确的 JAVA_HOME: `export JAVA_HOME=$(/usr/libexec/java_home -v 11)`
- 清理并重新编译: `mvn clean compile`
**Maven 使用错误的 Java 版本**
- 检查 Maven 版本: `mvn -version`
- 创建 `~/.mavenrc` 文件设置 JAVA_HOME
- 或在命令前加环境变量: `JAVA_HOME=$(/usr/libexec/java_home -v 11) mvn clean compile`
**VS Code Java 支持问题**
- 确保安装了 Extension Pack for Java
- 检查 `.vscode/settings.json` 中的 Java 配置
- 重新加载窗口: Cmd+Shift+P → "Developer: Reload Window"
**Spotless 格式化问题**
- 修复格式问题: `mvn spotless:apply`
- 跳过格式检查: `mvn compile -Dspotless.check.skip=true`
### 验证配置
```bash
# 验证 Java 版本
java -version
# 验证 Maven Java 版本
mvn -version
# 验证编译
mvn clean compile
# 验证完整构建
mvn clean install
```
## 构建状态
✅ 所有模块使用 Java 11 编译成功
✅ Lombok 注解自动处理
✅ 模块间配置一致
✅ Spotless 代码格式化已配置
✅ Checkstyle 代码规范检查已配置
✅ VS Code Java 配置已设置
--- File: deploy.sh ---
#!/bin/sh
# 发包脚本
set -e
echo "发布开始,当前时间是:$current_time"
#mvn clean install
scp ./play-admin/target/play-admin-1.0.jar root@122.51.20.105:/www/wwwroot/july.hucs.top
ssh root@122.51.20.105 "source /etc/profile;cd /www/wwwroot/july.hucs.top;sh start.sh restart"
# 获取当前时间并格式化为指定格式
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "发布完成,当前时间是:$current_time"
--- File: docker/Dockerfile ---
FROM openjdk:11-jre-slim
# 设置工作目录
WORKDIR /app
# 只复制应用程序JAR包,不复制lib目录
COPY ./*.jar app.jar
# 设置环境变量
ENV APP_NAME=app.jar
# 暴露应用端口
EXPOSE 8080
# 设置启动命令
CMD ["sh", "-c", "java -Dloader.path=./lib/ -Xms2g -Xmx2g -jar $APP_NAME --spring.profiles.active=test"]
--- File: docker/docker-compose.yml ---
version: '3'
services:
spring-boot-app:
build: .
container_name: spring-boot-app
ports:
- "7003:7002"
volumes:
- ./lib:/app/lib # 挂载主机的lib目录到容器内的lib目录
- ./log:/app/log # 挂载日志目录到主机
restart: always
--- File: fetch-log.sh ---
#!/bin/sh
# 拉取日志脚本
set -e
# 获取当前时间并格式化为指定格式
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "开始拉取日志,当前时间是:$current_time"
# 创建本地log目录(如果不存在)
mkdir -p ./log
# 从远程服务器拉取整个log文件夹
echo "正在从远程服务器拉取日志文件..."
scp -r root@122.51.20.105:/www/wwwroot/july.hucs.top/log/* ./log/
# 获取当前时间并格式化为指定格式
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "日志拉取完成,当前时间是:$current_time"
--- File: play-admin/pom.xml ---
4.0.0
com.starry
play-with
1.0
play-admin
11
11
11
UTF-8
org.springframework.boot
spring-boot-starter-web
org.flywaydb
flyway-core
com.starry
play-common
1.0
com.starry
play-generator
io.jsonwebtoken
jjwt
mysql
mysql-connector-java
8.0.26
com.github.yulichang
mybatis-plus-join-boot-starter
com.alibaba
easyexcel
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-data-redis
com.aliyun.oss
aliyun-sdk-oss
com.github.binarywang
wx-java-mp-spring-boot-starter
com.github.wxpay
wxpay-sdk
ws.schild
jave-core
ws.schild
jave-nativebin-linux64
com.github.binarywang
weixin-java-pay
4.5.0
com.squareup.okio
okio
com.tencentcloudapi
tencentcloud-sdk-java-dnspod
3.1.322
okio
com.squareup.okio
org.projectlombok
lombok
provided
org.projectlombok
lombok-mapstruct-binding
0.2.0
ruoyi-admin-mrwho
org.apache.maven.plugins
maven-compiler-plugin
org.projectlombok
lombok
1.18.30
org.flywaydb
flyway-maven-plugin
7.15.0
org.springframework.boot
spring-boot-maven-plugin
2.7.9
ZIP
nothing
nothing
repackage
--- File: play-admin/src/main/java/com/starry/admin/Application.java ---
package com.starry.admin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* @author admin
*/
@EnableTransactionManagement
@SpringBootApplication
@EnableScheduling
@ComponentScan("com.starry")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/aspect/ClerkUserLogin.java ---
package com.starry.admin.common.aspect;
import java.lang.annotation.*;
/**
* 陪聊登录注解
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ClerkUserLogin {
boolean manage() default false;
}
--- File: play-admin/src/main/java/com/starry/admin/common/aspect/ClerkUserLoginAspect.java ---
package com.starry.admin.common.aspect;
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserInfoServiceImpl;
import com.starry.admin.modules.weichat.service.WxTokenService;
import com.starry.common.constant.Constants;
import com.starry.common.constant.HttpStatus;
import com.starry.common.utils.StringUtils;
import java.util.Objects;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 限流处理
*
* @author ruoyi
*/
@Slf4j
@Aspect
@Component
public class ClerkUserLoginAspect {
@Resource
private PlayClerkUserInfoServiceImpl clerkUserInfoService;
@Resource
private WxTokenService tokenService;
@Resource
private HttpServletRequest request;
@Before("@annotation(clerkUserLogin)")
public void doBefore(JoinPoint point, ClerkUserLogin clerkUserLogin) {
String userToken = request.getHeader(Constants.CLERK_USER_LOGIN_TOKEN);
if (StringUtils.isEmpty(userToken)) {
throw new ServiceException("token为空", HttpStatus.UNAUTHORIZED);
}
if (userToken.startsWith(Constants.TOKEN_PREFIX)) {
userToken = userToken.replace(Constants.TOKEN_PREFIX, "");
}
// 解析token
String userId;
try {
userId = tokenService.getWxUserIdByToken(userToken);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
}
PlayClerkUserInfoEntity entity = clerkUserInfoService.selectById(userId);
if (Objects.isNull(entity)) {
throw new ServiceException("未查询到有效用户", HttpStatus.UNAUTHORIZED);
}
if (!userToken.equals(entity.getToken())) {
throw new ServiceException("token异常", HttpStatus.UNAUTHORIZED);
}
ThreadLocalRequestDetail.setRequestDetail(entity);
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/aspect/CustomUserLogin.java ---
package com.starry.admin.common.aspect;
import java.lang.annotation.*;
/**
* 客户登录注解
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomUserLogin {
boolean manage() default false;
}
--- File: play-admin/src/main/java/com/starry/admin/common/aspect/CustomUserLoginAspect.java ---
package com.starry.admin.common.aspect;
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.impl.PlayCustomUserInfoServiceImpl;
import com.starry.admin.modules.weichat.service.WxTokenService;
import com.starry.common.constant.Constants;
import com.starry.common.constant.HttpStatus;
import com.starry.common.utils.StringUtils;
import java.util.Objects;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 限流处理
*
* @author ruoyi
*/
@Slf4j
@Aspect
@Component
public class CustomUserLoginAspect {
@Resource
private PlayCustomUserInfoServiceImpl customUserInfoService;
@Resource
private WxTokenService tokenService;
@Resource
private HttpServletRequest request;
@Before("@annotation(customUserLogin)")
public void doBefore(JoinPoint point, CustomUserLogin customUserLogin) {
String userToken = request.getHeader(Constants.CUSTOM_USER_LOGIN_TOKEN);
if (StringUtils.isEmpty(userToken)) {
throw new ServiceException("token为空", HttpStatus.UNAUTHORIZED);
}
if (userToken.startsWith(Constants.TOKEN_PREFIX)) {
userToken = userToken.replace(Constants.TOKEN_PREFIX, "");
}
// 解析token
String userId;
try {
userId = tokenService.getWxUserIdByToken(userToken);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
}
PlayCustomUserInfoEntity entity = customUserInfoService.selectById(userId);
if (Objects.isNull(entity)) {
throw new ServiceException("未查询到有效用户", HttpStatus.UNAUTHORIZED);
}
if (!userToken.equals(entity.getToken())) {
throw new ServiceException("token异常", HttpStatus.UNAUTHORIZED);
}
ThreadLocalRequestDetail.setRequestDetail(entity);
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/aspect/DataScopeAspect.java ---
package com.starry.admin.common.aspect;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.common.domain.LoginUser;
import com.starry.admin.modules.system.module.entity.SysRoleEntity;
import com.starry.admin.modules.system.module.entity.SysUserEntity;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.annotation.DataScope;
import com.starry.common.context.CustomSecurityContextHolder;
import com.starry.common.domain.BaseEntity;
import com.starry.common.utils.StringUtils;
import java.util.ArrayList;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 数据过滤处理
*
* @author vctgo
*/
@Aspect
@Component
public class DataScopeAspect {
/**
* 全部数据权限
*/
public static final String DATA_SCOPE_ALL = "1";
/**
* 自定数据权限
*/
public static final String DATA_SCOPE_CUSTOM = "2";
/**
* 部门数据权限
*/
public static final String DATA_SCOPE_DEPT = "3";
/**
* 部门及以下数据权限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/**
* 仅本人数据权限
*/
public static final String DATA_SCOPE_SELF = "5";
/**
* 数据权限过滤关键字
*/
public static final String DATA_SCOPE = "dataScope";
/**
* 数据范围过滤
*
* @param joinPoint
* 切点
* @param user
* 用户
* @param deptAlias
* 部门别名
* @param userAlias
* 用户别名
* @param permission
* 权限字符
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUserEntity user, String deptAlias, String userAlias,
String permission) {
StringBuilder sqlString = new StringBuilder();
List conditions = new ArrayList<>();
for (SysRoleEntity role : user.getRoles()) {
String dataScope = role.getDataScope();
if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)) {
continue;
}
if (StrUtil.isNotBlank(permission) && StringUtils.isNotEmpty(role.getPermissions())
&& !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) {
continue;
}
if (DATA_SCOPE_ALL.equals(dataScope)) {
sqlString = new StringBuilder();
break;
} else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
} else if (DATA_SCOPE_DEPT.equals(dataScope)) {
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
} else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
} else if (DATA_SCOPE_SELF.equals(dataScope)) {
if (StringUtils.isNotBlank(userAlias)) {
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
} else {
// 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
}
}
conditions.add(dataScope);
}
if (StringUtils.isNotBlank(sqlString.toString())) {
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}
}
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) {
clearDataScope(point);
handleDataScope(point, controllerDataScope);
}
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) {
if (controllerDataScope == null) {
return;
}
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNotNull(loginUser)) {
SysUserEntity currentUser = loginUser.getUser();
// 如果是超级管理员,则不过滤数据
if (StringUtils.isNotNull(currentUser) && SysUserEntity.isAdmin(currentUser)) {
String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(),
CustomSecurityContextHolder.getPermission());
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias(), permission);
}
}
}
/**
* 拼接权限sql前先清空params.dataScope参数防止注入
*/
private void clearDataScope(final JoinPoint joinPoint) {
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, "");
}
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/aspect/LogAspect.java ---
package com.starry.admin.common.aspect;
import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson2.JSON;
import com.starry.admin.common.domain.LoginUser;
import com.starry.admin.modules.system.module.entity.SysOperationLogEntity;
import com.starry.admin.modules.system.service.ISysOperationLogService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.annotation.Log;
import com.starry.common.utils.ServletUtils;
import com.starry.common.utils.StringUtils;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;
/**
* @author admin
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
@Resource
private ISysOperationLogService operLogService;
/**
* 处理完请求后执行
*
* @param joinPoint
* 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturn(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, jsonResult);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
try {
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
// 日志记录
SysOperationLogEntity operLog = new SysOperationLogEntity();
operLog.setStatus(0);
// 请求的IP地址
String iP = ServletUtil.getClientIP(ServletUtils.getRequest());
if ("0:0:0:0:0:0:0:1".equals(iP)) {
iP = "127.0.0.1";
}
operLog.setOperIp(iP);
operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
if (loginUser != null) {
operLog.setOperName(loginUser.getUsername());
}
if (null != null) {
operLog.setStatus(1);
operLog.setErrorMsg(StringUtils.substring(((Exception) null).getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
operLog.setOperTime(new Date());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 保存数据库
operLogService.save(operLog);
} catch (Exception exp) {
log.error("异常信息:{}", exp.getMessage());
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log
* 日志
* @param operLog
* 操作日志
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperationLogEntity operLog,
Object jsonResult) {
// 设置操作业务类型
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 是否需要保存request,参数和值
if (log.isSaveRequestData()) {
// 设置参数的信息
setRequestValue(joinPoint, operLog);
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) {
operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
}
}
/**
* 获取请求的参数,放到log中
*
* @param operLog
* 操作日志
*/
private void setRequestValue(JoinPoint joinPoint, SysOperationLogEntity operLog) {
String requsetMethod = operLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requsetMethod) || HttpMethod.POST.name().equals(requsetMethod)) {
String params = argsArrayToString(joinPoint.getArgs());
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
} else {
Map, ?> paramsMap = (Map, ?>) ServletUtils.getRequest()
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
}
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
StringBuilder params = new StringBuilder();
if (paramsArray != null) {
for (Object object : paramsArray) {
// 不为空 并且是不需要过滤的 对象
if (StringUtils.isNotNull(object) && !isFilterObject(object)) {
Object jsonObj = JSON.toJSON(object);
params.append(jsonObj.toString()).append(" ");
}
}
}
return params.toString().trim();
}
/**
* 判断是否需要过滤的对象。
*
* @param object
* 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object object) {
Class> clazz = object.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) object;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) object;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return object instanceof MultipartFile || object instanceof HttpServletRequest
|| object instanceof HttpServletResponse || object instanceof BindingResult;
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/aspect/MybatisAspectj.java ---
package com.starry.admin.common.aspect;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* @Author: huchuansai
* @Date: 2023/8/2 4:38 PM
* @Description:
*/
@Aspect
@Component
public class MybatisAspectj {
// 配置织入点
@Pointcut("execution(public * com.baomidou.mybatisplus.core.mapper.BaseMapper.selectOne(..))")
public void selectOneAspect() {
}
@Before("selectOneAspect()")
public void beforeSelect(JoinPoint point) {
Object arg = point.getArgs()[0];
if (arg instanceof AbstractWrapper) {
arg = (AbstractWrapper) arg;
((AbstractWrapper) arg).last("limit 1");
}
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/component/JwtToken.java ---
package com.starry.admin.common.component;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.starry.admin.common.domain.LoginUser;
import com.starry.admin.common.security.entity.JwtUser;
import com.starry.common.constant.CacheConstants;
import com.starry.common.constant.Constants;
import com.starry.common.constant.SecurityConstants;
import com.starry.common.context.CustomSecurityContextHolder;
import com.starry.common.redis.RedisCache;
import com.starry.common.utils.IdUtils;
import com.starry.common.utils.ServletUtils;
import com.starry.common.utils.ip.AddressUtils;
import com.starry.common.utils.ip.IpUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
/**
* @author admin token 组件
* @since 2021/9/6
*/
@Slf4j
@Component
public class JwtToken {
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expire;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Resource
private RedisCache redisCache;
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 校验token
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 创建令牌
*
* @param jwtUser
* 用户信息
* @return 令牌
*/
public String createToken(JwtUser jwtUser) {
String token = IdUtils.getUuid();
jwtUser.setToken(token);
setUserAgent(jwtUser);
refersToken(jwtUser);
Map claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
/**
* 从数据声明生成令牌
*
* @param claims
* 数据声明
* @return 令牌
*/
private String createToken(Map claims) {
String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getClaimsFromToken(token).getExpiration();
return expiredDate.before(new Date());
}
private String generateToken(Map claims) {
return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate())
// 签名算法
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expire * 1000);
}
/**
* 从令牌中获取数据声明
*
* @param token
* 令牌
* @return 数据声明
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
log.info("JWT格式验证失败:{}", token);
}
return claims;
}
/**
* 设置用户代理信息
*
* @param jwtUser
* 登录信息
*/
public void setUserAgent(JwtUser jwtUser) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtil.getClientIP(ServletUtils.getRequest());
jwtUser.setIpaddr(ip);
jwtUser.setLoginLocation(AddressUtils.getRealAddressByIp(ip));
jwtUser.setBrowser(userAgent.getBrowser().getName());
jwtUser.setOs(userAgent.getOs().getName());
}
public void refersToken(JwtUser jwtUser) {
jwtUser.setLoginTime(System.currentTimeMillis());
jwtUser.setExpireTime(jwtUser.getLoginTime() + expire * 1000);
String userKey = getTokenKey(jwtUser.getToken());
redisCache.setCacheObject(userKey, jwtUser, expire, TimeUnit.SECONDS);
String key = "login:resource:" + jwtUser.getUserId();
redisCache.setCacheObject(key, userKey, expire, TimeUnit.SECONDS);
}
private String getTokenKey(String uuid) {
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
}
/**
* 获取登录用户身份信息
*
* @return 用户信息
*/
public JwtUser getLoginUser(HttpServletRequest request) {
String token = getToken(request);
if (StrUtil.isNotBlank(token)) {
try {
Claims claims = getClaimsFromToken(token);
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
JwtUser user = redisCache.getCacheObject(userKey);
return user;
} catch (Exception e) {
}
}
return null;
}
/**
* 获取请求token
*
* @param request
* @return token
*/
private String getToken(HttpServletRequest request) {
// 获取请求头
String token = request.getHeader(tokenHeader);
if (StrUtil.isNotBlank(token) && token.startsWith(tokenHead)) {
token = token.replace(tokenHead, "");
}
return token;
}
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param jwtUser
* @return 令牌
*/
/**
* 删除用户身份信息
*/
public void removeJwtUser(String token) {
if (StrUtil.isNotBlank(token)) {
String userKey = getTokenKey(token);
redisCache.deleteObject(userKey);
}
}
/**
* 创建令牌
*/
public Map createToken(LoginUser loginUser) {
String token = IdUtils.getUuid();
String userId = loginUser.getUser().getUserId();
String userName = loginUser.getUser().getUserCode();
String tenantId = loginUser.getUser().getTenantId();
Long deptId = loginUser.getUser().getDeptId();
loginUser.setToken(token);
loginUser.setUserId(userId);
loginUser.setUserName(userName);
loginUser.setIpaddr(IpUtils.getIpAddr(ServletUtils.getRequest()));
// 添加地址信息
setUserAgent(loginUser);
String userRedisKey = refreshToken(loginUser);
String key = "login:resource:" + loginUser.getUserId();
redisCache.setCacheObject(key, userRedisKey, expire, TimeUnit.SECONDS);
// Jwt存储信息
Map claimsMap = new HashMap<>(8);
claimsMap.put(SecurityConstants.USER_KEY, token);
claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);
// 租户id
claimsMap.put(SecurityConstants.DETAILS_TENANT_ID, tenantId);
// 部门id
claimsMap.put(SecurityConstants.DETAILS_DEPT_ID, deptId);
// 接口返回信息
Map rspMap = new HashMap<>();
rspMap.put("token", this.createToken(claimsMap));
rspMap.put("expires_in", expire);
rspMap.put("tenant_id", tenantId);
rspMap.put("tokenHead", tokenHead);
return rspMap;
}
/**
* 设置用户代理信息
*
* @param loginUser
* 登录信息
*/
public void setUserAgent(LoginUser loginUser) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtil.getClientIP(ServletUtils.getRequest());
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIp(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOs().getName());
}
/**
* 刷新令牌有效期
*
* @param loginUser
* 登录信息
* @return
*/
public String refreshToken(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expire * 1000);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expire, TimeUnit.MINUTES);
return userKey;
}
/**
* 获取登录用户身份信息
*
* @return 用户信息
*/
public LoginUser getNewLoginUser(HttpServletRequest request) {
String token = getToken(request);
if (StrUtil.isNotBlank(token)) {
try {
Claims claims = getClaimsFromToken(token);
String uuid = (String) claims.get(SecurityConstants.USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser loginUser = redisCache.getCacheObject(userKey);
CustomSecurityContextHolder.set(SecurityConstants.DETAILS_TENANT_ID, loginUser.getUser().getTenantId());
CustomSecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
return loginUser;
} catch (Exception e) {
log.error("getNewLoginUser error", e);
}
}
return null;
}
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser
* @return 令牌
*/
public void verifyToken(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(loginUser);
}
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/component/PermissionService.java ---
package com.starry.admin.common.component;
import com.starry.admin.common.domain.LoginUser;
import com.starry.admin.modules.system.module.entity.SysRoleEntity;
import com.starry.admin.utils.SecurityUtils;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
/**
* 自定义权限实现,ss取自SpringSecurity首字母
*
* @author admin
*/
@Service("customSs")
public class PermissionService {
/**
* 所有权限标识
*/
private static final String ALL_PERMISSION = "*:*:*";
/**
* 管理员角色权限标识
*/
private static final String SUPER_ADMIN = "admin";
private static final String ROLE_DELIMITER = ",";
private static final String PERMISSION_DELIMETER = ",";
/**
* 验证用户是否具备某权限
*
* @param permission
* 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermission(String permission) {
if (StringUtils.isEmpty(permission)) {
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
return hasPermissions(loginUser.getPermissions(), permission);
}
/**
* 验证用户是否不具备某权限,与 hasPermission逻辑相反
*
* @param permission
* 权限字符串
* @return 用户是否不具备某权限
*/
public boolean lacksPermission(String permission) {
return hasPermission(permission) != true;
}
/**
* 验证用户是否具有以下任意一个权限
*
* @param permissions
* 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
* @return 用户是否具有以下任意一个权限
*/
public boolean hasAnyPermission(String permissions) {
if (StringUtils.isEmpty(permissions)) {
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
Set authorities = loginUser.getPermissions();
for (String permission : permissions.split(PERMISSION_DELIMETER)) {
if (permission != null && hasPermissions(authorities, permission)) {
return true;
}
}
return false;
}
/**
* 判断用户是否拥有某个角色
*
* @param role
* 角色字符串
* @return 用户是否具备某角色
*/
public boolean hasRole(String role) {
if (StringUtils.isEmpty(role)) {
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {
return false;
}
for (SysRoleEntity sysRoleEntity : loginUser.getUser().getRoles()) {
String roleKey = sysRoleEntity.getRoleKey();
if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) {
return true;
}
}
return false;
}
/**
* 验证用户是否不具备某角色,与 isRole逻辑相反。
*
* @param role
* 角色名称
* @return 用户是否不具备某角色
*/
public boolean lacksRole(String role) {
return !hasRole(role);
}
/**
* 验证用户是否具有以下任意一个角色
*
* @param roles
* 以 ROLE_NAMES_DELIMITER 为分隔符的角色列表
* @return 用户是否具有以下任意一个角色
*/
public boolean hasAnyRoles(String roles) {
if (StringUtils.isEmpty(roles)) {
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {
return false;
}
for (String role : roles.split(ROLE_DELIMITER)) {
if (hasRole(role)) {
return true;
}
}
return false;
}
/**
* 判断是否包含权限
*
* @param permissions
* 权限列表
* @param permission
* 权限字符串
* @return 用户是否具备某权限
*/
private boolean hasPermissions(Set permissions, String permission) {
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/conf/AbstractListTypeHandler.java ---
package com.starry.admin.common.conf;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
/**
* 数据库数据-String 和 List 自动转换
*
* @author admin
*/
@Slf4j
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({List.class})
public abstract class AbstractListTypeHandler extends BaseTypeHandler> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType)
throws SQLException {
String content = StrUtil.isEmptyIfStr(parameter) ? null : JSON.toJSONString(parameter);
ps.setString(i, content);
}
@Override
public List getNullableResult(ResultSet rs, String columnName) throws SQLException {
return this.getListByJsonArrayString(rs.getString(columnName));
}
@Override
public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return this.getListByJsonArrayString(rs.getString(columnIndex));
}
@Override
public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return this.getListByJsonArrayString(cs.getString(columnIndex));
}
private List getListByJsonArrayString(String content) {
return StrUtil.isEmptyIfStr(content) ? new ArrayList<>() : JSON.parseObject(content, this.specificType());
}
/**
* 具体类型,由子类提供
*
* @return 具体类型
*/
protected abstract TypeReference> specificType();
}
--- File: play-admin/src/main/java/com/starry/admin/common/conf/DataSourceConfig.java ---
package com.starry.admin.common.conf;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class DataSourceConfig {
// For flyway only
@Bean(name = "primaryDataSource")
@Primary
public DataSource dataSource() {
return DruidDataSourceBuilder.create().build();
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/conf/StringTypeHandler.java ---
package com.starry.admin.common.conf;
import com.alibaba.fastjson2.TypeReference;
import java.util.List;
/**
* @author admin
*/
public class StringTypeHandler extends AbstractListTypeHandler {
@Override
protected TypeReference> specificType() {
return new TypeReference>() {
};
}
}
--- File: play-admin/src/main/java/com/starry/admin/common/conf/ThreadLocalRequestDetail.java ---
package com.starry.admin.common.conf;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
/**
* @author : huchuansai
* @since : 2024/4/2 12:10 AM
*/
public class ThreadLocalRequestDetail {
private static final TransmittableThreadLocal