diff --git a/backend.txt b/backend.txt
new file mode 100644
index 0000000..555cff4
--- /dev/null
+++ b/backend.txt
@@ -0,0 +1,67364 @@
+--- 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