first commit

This commit is contained in:
zc
2026-02-27 10:16:46 +08:00
commit 0ee56404c2
705 changed files with 47675 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
package top.ysoft.admin.common.config.doc;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.map.MapUtil;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.*;
import top.continew.starter.apidoc.autoconfigure.SpringDocExtensionProperties;
import top.continew.starter.auth.satoken.autoconfigure.SaTokenExtensionProperties;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
/**
* 全局鉴权参数定制器
*
* @author echo
* @since 2024/12/31 13:36
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GlobalAuthenticationCustomizer implements GlobalOpenApiCustomizer {
private final SpringDocExtensionProperties properties;
private final SaTokenExtensionProperties saTokenExtensionProperties;
private final ApplicationContext context;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 定制 OpenAPI 文档
*
* @param openApi 当前 OpenAPI 对象
*/
@Override
public void customise(OpenAPI openApi) {
if (MapUtil.isEmpty(openApi.getPaths())) {
return;
}
// 收集需要排除的路径(包括 Sa-Token 配置中的排除路径和 @SaIgnore 注解路径)
Set<String> excludedPaths = collectExcludedPaths();
// 遍历所有路径,为需要鉴权的路径添加安全认证配置
openApi.getPaths().forEach((path, pathItem) -> {
if (isPathExcluded(path, excludedPaths)) {
// 路径在排除列表中,跳过处理
return;
}
// 为路径添加安全认证参数
addAuthenticationParameters(pathItem);
});
}
/**
* 收集所有需要排除的路径
*
* @return 排除路径集合
*/
private Set<String> collectExcludedPaths() {
Set<String> excludedPaths = new HashSet<>();
excludedPaths.addAll(Arrays.asList(saTokenExtensionProperties.getSecurity().getExcludes()));
excludedPaths.addAll(resolveSaIgnorePaths());
return excludedPaths;
}
/**
* 为路径项添加认证参数
*
* @param pathItem 当前路径项
*/
private void addAuthenticationParameters(PathItem pathItem) {
Components components = properties.getComponents();
if (components == null || MapUtil.isEmpty(components.getSecuritySchemes())) {
return;
}
Map<String, SecurityScheme> securitySchemes = components.getSecuritySchemes();
List<String> schemeNames = securitySchemes.values().stream().map(SecurityScheme::getName).toList();
pathItem.readOperations().forEach(operation -> {
SecurityRequirement securityRequirement = new SecurityRequirement();
schemeNames.forEach(securityRequirement::addList);
operation.addSecurityItem(securityRequirement);
});
}
/**
* 解析所有带有 @SaIgnore 注解的路径
*
* @return 被忽略的路径集合
*/
private Set<String> resolveSaIgnorePaths() {
// 获取所有标注 @RestController 的 Bean
Map<String, Object> controllers = context.getBeansWithAnnotation(RestController.class);
Set<String> ignoredPaths = new HashSet<>();
// 遍历所有控制器,解析 @SaIgnore 注解路径
controllers.values().forEach(controllerBean -> {
Class<?> controllerClass = AopUtils.getTargetClass(controllerBean);
List<String> classPaths = getClassPaths(controllerClass);
// 类级别的 @SaIgnore 注解
if (controllerClass.isAnnotationPresent(SaIgnore.class)) {
classPaths.forEach(classPath -> ignoredPaths.add(classPath + "/**"));
}
// 方法级别的 @SaIgnore 注解
Arrays.stream(controllerClass.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(SaIgnore.class))
.forEach(method -> ignoredPaths.addAll(combinePaths(classPaths, getMethodPaths(method))));
});
return ignoredPaths;
}
/**
* 获取类上的所有路径
*
* @param controller 控制器类
* @return 类路径列表
*/
private List<String> getClassPaths(Class<?> controller) {
List<String> classPaths = new ArrayList<>();
// 处理 @RequestMapping 注解
if (controller.isAnnotationPresent(RequestMapping.class)) {
RequestMapping mapping = controller.getAnnotation(RequestMapping.class);
classPaths.addAll(Arrays.asList(mapping.value()));
}
// 处理 @CrudRequestMapping 注解
if (controller.isAnnotationPresent(CrudRequestMapping.class)) {
CrudRequestMapping mapping = controller.getAnnotation(CrudRequestMapping.class);
if (!mapping.value().isEmpty()) {
classPaths.add(mapping.value());
}
}
return classPaths;
}
/**
* 获取方法上的所有路径
*
* @param method 控制器方法
* @return 方法路径列表
*/
private List<String> getMethodPaths(Method method) {
List<String> methodPaths = new ArrayList<>();
// 检查方法上的各种映射注解
if (method.isAnnotationPresent(GetMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(GetMapping.class).value()));
} else if (method.isAnnotationPresent(PostMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(PostMapping.class).value()));
} else if (method.isAnnotationPresent(PutMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(PutMapping.class).value()));
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(DeleteMapping.class).value()));
} else if (method.isAnnotationPresent(RequestMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(RequestMapping.class).value()));
} else if (method.isAnnotationPresent(PatchMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(PatchMapping.class).value()));
}
return methodPaths;
}
/**
* 组合类路径和方法路径
*
* @param classPaths 类路径列表
* @param methodPaths 方法路径列表
* @return 完整路径集合
*/
private Set<String> combinePaths(List<String> classPaths, List<String> methodPaths) {
return classPaths.stream()
.flatMap(classPath -> methodPaths.stream().map(methodPath -> classPath + methodPath))
.collect(Collectors.toSet());
}
/**
* 检查路径是否在排除列表中
*
* @param path 当前路径
* @param excludedPaths 排除路径集合,支持通配符
* @return 是否匹配排除规则
*/
private boolean isPathExcluded(String path, Set<String> excludedPaths) {
return excludedPaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
}
}

View File

@@ -0,0 +1,49 @@
package top.ysoft.admin.common.config.doc;
import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.models.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import java.util.ArrayList;
import java.util.List;
/**
* 全局描述定制器 - 处理 sa-token 的注解权限码
*
* @author echo
* @since 2025/1/24 14:59
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GlobalDescriptionCustomizer implements GlobalOperationCustomizer {
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
// 将 sa-token 注解数据添加到 operation 的描述中
// 权限
List<String> noteList = new ArrayList<>(new OperationDescriptionCustomizer().getPermission(handlerMethod));
// 如果注解数据列表为空,直接返回原 operation
if (noteList.isEmpty()) {
return operation;
}
// 拼接注解数据为字符串
String noteStr = StrUtil.join("<br/>", noteList);
// 获取原描述
String originalDescription = operation.getDescription();
// 根据原描述是否为空,更新描述
String newDescription = StringUtils.isNotEmpty(originalDescription)
? originalDescription + "<br/>" + noteStr
: noteStr;
// 设置新描述
operation.setDescription(newDescription);
return operation;
}
}

View File

@@ -0,0 +1,164 @@
package top.ysoft.admin.common.config.doc;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.text.CharSequenceUtil;
import org.springframework.web.method.HandlerMethod;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.annotation.CrudApi;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
/**
* Operation 描述定制器 处理 sa-token 鉴权标识符
*
* @author echo
* @since 2024/6/14 11:18
*/
public class OperationDescriptionCustomizer {
/**
* 获取 sa-token 注解信息
*
* @param handlerMethod 处理程序方法
* @return 包含权限和角色校验信息的列表
*/
public List<String> getPermission(HandlerMethod handlerMethod) {
List<String> values = new ArrayList<>();
// 获取权限校验信息
String permissionInfo = getAnnotationInfo(handlerMethod, SaCheckPermission.class, "权限校验:");
if (!permissionInfo.isEmpty()) {
values.add(permissionInfo);
}
// 获取角色校验信息
String roleInfo = getAnnotationInfo(handlerMethod, SaCheckRole.class, "角色校验:");
if (!roleInfo.isEmpty()) {
values.add(roleInfo);
}
// 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息
String crudPermissionInfo = getCrudPermissionInfo(handlerMethod);
if (!crudPermissionInfo.isEmpty()) {
values.add(crudPermissionInfo);
}
return values;
}
/**
* 获取类和方法上指定注解的信息
*
* @param handlerMethod 处理程序方法
* @param annotationClass 注解类
* @param title 信息标题
* @param <A> 注解类型
* @return 拼接好的注解信息字符串
*/
@SuppressWarnings("unchecked")
private <A extends Annotation> String getAnnotationInfo(HandlerMethod handlerMethod,
Class<A> annotationClass,
String title) {
StringBuilder infoBuilder = new StringBuilder();
// 获取类上的注解
A classAnnotation = handlerMethod.getBeanType().getAnnotation(annotationClass);
if (classAnnotation != null) {
appendAnnotationInfo(infoBuilder, "类:", classAnnotation);
}
// 获取方法上的注解
A methodAnnotation = handlerMethod.getMethodAnnotation(annotationClass);
if (methodAnnotation != null) {
appendAnnotationInfo(infoBuilder, "方法:", methodAnnotation);
}
// 如果有注解信息,添加标题
if (!infoBuilder.isEmpty()) {
infoBuilder.insert(0, "<font style=\"color:red\" class=\"light-red\">" + title + "</font></br>");
}
return infoBuilder.toString();
}
/**
* 拼接注解信息到 StringBuilder 中
*
* @param builder 用于拼接信息的 StringBuilder
* @param prefix 前缀信息,如 "类:" 或 "方法:"
* @param annotation 注解对象
*/
private void appendAnnotationInfo(StringBuilder builder, String prefix, Annotation annotation) {
String[] values = null;
SaMode mode = null;
String type = "";
String[] orRole = new String[0];
if (annotation instanceof SaCheckPermission checkPermission) {
values = checkPermission.value();
mode = checkPermission.mode();
type = checkPermission.type();
orRole = checkPermission.orRole();
} else if (annotation instanceof SaCheckRole checkRole) {
values = checkRole.value();
mode = checkRole.mode();
type = checkRole.type();
}
if (values != null && mode != null) {
builder.append("<font style=\"color:red\" class=\"light-red\">");
builder.append(prefix);
if (!type.isEmpty()) {
builder.append("(类型:").append(type).append("");
}
builder.append(getAnnotationNote(values, mode));
if (orRole.length > 0) {
builder.append(" 或 角色校验(").append(getAnnotationNote(orRole, mode)).append("");
}
builder.append("</font></br>");
}
}
/**
* 根据注解的模式拼接注解值
*
* @param values 注解的值数组
* @param mode 注解的模式AND 或 OR
* @return 拼接好的注解值字符串
*/
private String getAnnotationNote(String[] values, SaMode mode) {
if (mode.equals(SaMode.AND)) {
return String.join("", values);
} else {
return String.join("", values);
}
}
/**
* 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息
*
* @param handlerMethod 处理程序方法
* @return 拼接好的权限信息字符串
*/
private String getCrudPermissionInfo(HandlerMethod handlerMethod) {
CrudRequestMapping crudRequestMapping = handlerMethod.getBeanType().getAnnotation(CrudRequestMapping.class);
CrudApi crudApi = handlerMethod.getMethodAnnotation(CrudApi.class);
if (crudRequestMapping == null || crudApi == null) {
return "";
}
String path = crudRequestMapping.value();
String prefix = String.join(StringConstants.COLON, CharSequenceUtil.splitTrim(path, StringConstants.SLASH));
Api api = crudApi.value();
String apiName = Api.PAGE.equals(api) || Api.TREE.equals(api) ? Api.LIST.name() : api.name();
String permission = "%s:%s".formatted(prefix, apiName.toLowerCase());
return "<font style=\"color:red\" class=\"light-red\">Crud 权限校验:</font></br><font style=\"color:red\" class=\"light-red\">方法:</font><font style=\"color:red\" class=\"light-red\">" + permission + "</font>";
}
}

View File

@@ -0,0 +1,105 @@
package top.ysoft.admin.common.config.exception;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.CharSequenceUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.web.model.R;
/**
* 全局异常处理器
*
* @author Charles7c
* @author echo
* @since 2024/8/7 20:21
*/
@Slf4j
@Order(99)
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 拦截业务异常
*/
@ExceptionHandler(BusinessException.class)
public R handleBusinessException(BusinessException e, HttpServletRequest request) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
return R.fail(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage());
}
/**
* 拦截自定义验证异常-错误请求
*/
@ExceptionHandler(BadRequestException.class)
public R handleBadRequestException(BadRequestException e, HttpServletRequest request) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), e.getMessage());
}
/**
* 拦截校验异常-方法参数类型不匹配异常
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e,
HttpServletRequest request) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), "参数 '%s' 类型不匹配".formatted(e.getName()));
}
/**
* 拦截文件上传异常-超过上传大小限制
*/
@ExceptionHandler(MultipartException.class)
public R handleMultipartException(MultipartException e, HttpServletRequest request) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
String msg = e.getMessage();
R defaultFail = R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), msg);
if (CharSequenceUtil.isBlank(msg)) {
return defaultFail;
}
String sizeLimit;
Throwable cause = e.getCause();
if (null != cause) {
msg = msg.concat(cause.getMessage().toLowerCase());
}
if (msg.contains("larger than")) {
sizeLimit = CharSequenceUtil.subAfter(msg, "larger than ", true);
} else if (msg.contains("size") && msg.contains("exceed")) {
sizeLimit = CharSequenceUtil.subBetween(msg, "the maximum size ", " for");
} else {
return defaultFail;
}
return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), "请上传小于 %s 的文件".formatted(FileUtil
.readableFileSize(Long.parseLong(sizeLimit))));
}
/**
* 拦截请求 URL 不存在异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
public R handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
return R.fail(String.valueOf(HttpStatus.NOT_FOUND.value()), "请求 URL '%s' 不存在".formatted(request
.getRequestURI()));
}
/**
* 拦截不支持的 HTTP 请求方法异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public R handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
return R.fail(String.valueOf(HttpStatus.METHOD_NOT_ALLOWED.value()), "请求方式 '%s' 不支持".formatted(e.getMethod()));
}
}

View File

@@ -0,0 +1,56 @@
package top.ysoft.admin.common.config.exception;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import top.continew.starter.web.model.R;
/**
* 全局 SaToken 异常处理器
*
* @author Charles7c
* @since 2024/8/7 20:21
*/
@Slf4j
@Order(99)
@RestControllerAdvice
public class GlobalSaTokenExceptionHandler {
/**
* 认证异常-登录认证
*/
@ExceptionHandler(NotLoginException.class)
public R handleNotLoginException(NotLoginException e, HttpServletRequest request) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
String errorMsg = switch (e.getType()) {
case NotLoginException.KICK_OUT -> "您已被踢下线";
case NotLoginException.BE_REPLACED_MESSAGE -> "您已被顶下线";
default -> "您的登录状态已过期,请重新登录";
};
return R.fail(String.valueOf(HttpStatus.UNAUTHORIZED.value()), errorMsg);
}
/**
* 认证异常-权限认证
*/
@ExceptionHandler(NotPermissionException.class)
public R handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
return R.fail(String.valueOf(HttpStatus.FORBIDDEN.value()), "没有访问权限,请联系管理员授权");
}
/**
* 认证异常-角色认证
*/
@ExceptionHandler(NotRoleException.class)
public R handleNotRoleException(NotRoleException e, HttpServletRequest request) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
return R.fail(String.valueOf(HttpStatus.FORBIDDEN.value()), "没有访问权限,请联系管理员授权");
}
}

View File

@@ -0,0 +1,29 @@
package top.ysoft.admin.common.config.mybatis;
import org.springframework.security.crypto.password.PasswordEncoder;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
/**
* BCrypt 加/解密处理器(不可逆)
*
* @author Charles7c
* @since 2024/2/8 22:29
*/
public class BCryptEncryptor implements IEncryptor {
private final PasswordEncoder passwordEncoder;
public BCryptEncryptor(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public String encrypt(String plaintext, String password, String publicKey) throws Exception {
return passwordEncoder.encode(plaintext);
}
@Override
public String decrypt(String ciphertext, String password, String privateKey) throws Exception {
return ciphertext;
}
}

View File

@@ -0,0 +1,41 @@
package top.ysoft.admin.common.config.mybatis;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import top.continew.starter.data.mp.base.BaseMapper;
import top.continew.starter.extension.datapermission.annotation.DataPermission;
import java.util.List;
/**
* 数据权限 Mapper 基类
*
* @param <T> 实体类
* @author Charles7c
* @since 2023/9/3 21:50
*/
public interface DataPermissionMapper<T> extends BaseMapper<T> {
/**
* 根据 entity 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null
* @return 全部记录
*/
@Override
@DataPermission
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类(可以为 null
* @return 全部记录(并翻页)
*/
@Override
@DataPermission
List<T> selectList(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}

View File

@@ -0,0 +1,37 @@
package top.ysoft.admin.common.config.mybatis;
import cn.hutool.core.convert.Convert;
import top.ysoft.admin.common.context.UserContextHolder;
import top.continew.starter.extension.datapermission.enums.DataScope;
import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider;
import top.continew.starter.extension.datapermission.model.RoleContext;
import top.continew.starter.extension.datapermission.model.UserContext;
import java.util.stream.Collectors;
/**
* 数据权限用户上下文提供者
*
* @author Charles7c
* @since 2023/12/21 21:19
*/
public class DefaultDataPermissionUserContextProvider implements DataPermissionUserContextProvider {
@Override
public boolean isFilter() {
return !UserContextHolder.isAdmin();
}
@Override
public UserContext getUserContext() {
top.ysoft.admin.common.context.UserContext context = UserContextHolder.getContext();
UserContext userContext = new UserContext();
userContext.setUserId(Convert.toStr(context.getId()));
userContext.setDeptId(Convert.toStr(context.getDeptId()));
userContext.setRoles(context.getRoles()
.stream()
.map(r -> new RoleContext(Convert.toStr(r.getId()), DataScope.valueOf(r.getDataScope().name())))
.collect(Collectors.toSet()));
return userContext;
}
}

View File

@@ -0,0 +1,96 @@
package top.ysoft.admin.common.config.mybatis;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import top.ysoft.admin.common.context.UserContextHolder;
import top.ysoft.admin.common.model.entity.BaseDO;
import java.time.LocalDateTime;
/**
* MyBatis Plus 元对象处理器配置(插入或修改时自动填充)
*
* @author Charles7c
* @since 2022/12/22 19:52
*/
public class MyBatisPlusMetaObjectHandler implements MetaObjectHandler {
/**
* 创建人
*/
private static final String CREATE_USER = "createUser";
/**
* 创建时间
*/
private static final String CREATE_TIME = "createTime";
/**
* 修改人
*/
private static final String UPDATE_USER = "updateUser";
/**
* 修改时间
*/
private static final String UPDATE_TIME = "updateTime";
/**
* 插入数据时填充
*
* @param metaObject 元对象
*/
@Override
public void insertFill(MetaObject metaObject) {
if (null == metaObject) {
return;
}
Long createUser = UserContextHolder.getUserId();
LocalDateTime createTime = LocalDateTime.now();
if (metaObject.getOriginalObject() instanceof BaseDO baseDO) {
// 继承了 BaseDO 的类,填充创建信息字段
baseDO.setCreateUser(ObjectUtil.defaultIfNull(baseDO.getCreateUser(), createUser));
baseDO.setCreateTime(ObjectUtil.defaultIfNull(baseDO.getCreateTime(), createTime));
} else {
// 未继承 BaseDO 的类,如存在创建信息字段则进行填充
this.fillFieldValue(metaObject, CREATE_USER, createUser, false);
this.fillFieldValue(metaObject, CREATE_TIME, createTime, false);
}
}
/**
* 修改数据时填充
*
* @param metaObject 元对象
*/
@Override
public void updateFill(MetaObject metaObject) {
if (null == metaObject) {
return;
}
Long updateUser = UserContextHolder.getUserId();
LocalDateTime updateTime = LocalDateTime.now();
if (metaObject.getOriginalObject() instanceof BaseDO baseDO) {
// 继承了 BaseDO 的类,填充修改信息
baseDO.setUpdateUser(updateUser);
baseDO.setUpdateTime(updateTime);
} else {
// 未继承 BaseDO 的类,根据类中拥有的修改信息字段进行填充,不存在修改信息字段不进行填充
this.fillFieldValue(metaObject, UPDATE_USER, updateUser, true);
this.fillFieldValue(metaObject, UPDATE_TIME, updateTime, true);
}
}
/**
* 填充字段值
*
* @param metaObject 元数据对象
* @param fieldName 要填充的字段名
* @param fillFieldValue 要填充的字段值
* @param isOverride 如果字段值不为空是否覆盖true覆盖false不覆盖
*/
private void fillFieldValue(MetaObject metaObject, String fieldName, Object fillFieldValue, boolean isOverride) {
if (metaObject.hasSetter(fieldName)) {
Object fieldValue = metaObject.getValue(fieldName);
setFieldValByName(fieldName, null != fieldValue && !isOverride ? fieldValue : fillFieldValue, metaObject);
}
}
}

View File

@@ -0,0 +1,41 @@
package top.ysoft.admin.common.config.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider;
/**
* MyBatis Plus 配置
*
* @author Charles7c
* @since 2022/12/22 19:51
*/
@Configuration
public class MybatisPlusConfiguration {
/**
* 元对象处理器配置(插入或修改时自动填充)
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MyBatisPlusMetaObjectHandler();
}
/**
* 数据权限用户上下文提供者
*/
@Bean
public DataPermissionUserContextProvider dataPermissionUserContextProvider() {
return new DefaultDataPermissionUserContextProvider();
}
/**
* BCrypt 加/解密处理器
*/
@Bean
public BCryptEncryptor bCryptEncryptor(PasswordEncoder passwordEncoder) {
return new BCryptEncryptor(passwordEncoder);
}
}

View File

@@ -0,0 +1,76 @@
package top.ysoft.admin.common.config.properties;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 验证码配置属性
*
* @author Charles7c
* @since 2022/12/11 13:35
*/
@Data
@Component
@ConfigurationProperties(prefix = "captcha")
public class CaptchaProperties {
/**
* 图形验证码过期时间
*/
@Value("${continew-starter.captcha.graphic.expirationInMinutes}")
private long expirationInMinutes;
/**
* 邮箱验证码配置
*/
private CaptchaMail mail;
/**
* 短信验证码配置
*/
private CaptchaSms sms;
/**
* 邮箱验证码配置
*/
@Data
public static class CaptchaMail {
/**
* 内容长度
*/
private int length;
/**
* 过期时间
*/
private long expirationInMinutes;
/**
* 模板路径
*/
private String templatePath;
}
/**
* 短信验证码配置
*/
@Data
public static class CaptchaSms {
/**
* 内容长度
*/
private int length;
/**
* 过期时间
*/
private long expirationInMinutes;
/**
* 模板 ID
*/
private String templateId;
}
}

View File

@@ -0,0 +1,27 @@
package top.ysoft.admin.common.config.properties;
import cn.hutool.extra.spring.SpringUtil;
/**
* RSA 配置属性
*
* @author Zheng JieELADMIN
* @author Charles7c
* @since 2022/12/21 20:21
*/
public class RsaProperties {
/**
* 私钥
*/
public static final String PRIVATE_KEY;
public static final String PUBLIC_KEY;
static {
PRIVATE_KEY = SpringUtil.getProperty("continew-starter.security.crypto.private-key");
PUBLIC_KEY = SpringUtil.getProperty("continew-starter.security.crypto.public-key");
}
private RsaProperties() {
}
}

View File

@@ -0,0 +1,28 @@
package top.ysoft.admin.common.config.websocket;
import cn.dev33.satoken.stp.StpUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.messaging.websocket.core.WebSocketClientService;
/**
* 当前登录用户 Provider
*
* @author Charles7c
* @since 2024/6/4 22:13
*/
@Component
public class WebSocketClientServiceImpl implements WebSocketClientService {
@Override
public String getClientId(ServletServerHttpRequest request) {
HttpServletRequest servletRequest = request.getServletRequest();
String token = servletRequest.getParameter("token");
if (null == StpUtil.getLoginIdByToken(token)) {
throw new BusinessException("登录已过期,请重新登录");
}
return token;
}
}

View File

@@ -0,0 +1,65 @@
package top.ysoft.admin.common.constant;
import top.continew.starter.core.constant.StringConstants;
/**
* 缓存相关常量
*
* @author Charles7c
* @since 2022/12/22 19:30
*/
public class CacheConstants {
/**
* 分隔符
*/
public static final String DELIMITER = StringConstants.COLON;
/**
* 验证码键前缀
*/
public static final String CAPTCHA_KEY_PREFIX = "CAPTCHA" + DELIMITER;
/**
* 用户缓存键前缀
*/
public static final String USER_KEY_PREFIX = "USER" + DELIMITER;
/**
* 角色菜单缓存键前缀
*/
public static final String ROLE_MENU_KEY_PREFIX = "ROLE_MENU" + DELIMITER;
/**
* 字典缓存键前缀
*/
public static final String DICT_KEY_PREFIX = "DICT" + DELIMITER;
/**
* 参数缓存键前缀
*/
public static final String OPTION_KEY_PREFIX = "OPTION" + DELIMITER;
/**
* 仪表盘缓存键前缀
*/
public static final String DASHBOARD_KEY_PREFIX = "DASHBOARD" + DELIMITER;
/**
* 用户密码错误次数缓存键前缀
*/
public static final String USER_PASSWORD_ERROR_KEY_PREFIX = USER_KEY_PREFIX + "PASSWORD_ERROR" + DELIMITER;
/**
* 数据导入临时会话key
*/
public static final String DATA_IMPORT_KEY = "SYSTEM" + DELIMITER + "DATA_IMPORT" + DELIMITER;
/**
* 参数管理 cache key
*/
public static final String SYS_CONFIG_KEY = "sys_config:";
private CacheConstants() {
}
}

View File

@@ -0,0 +1,202 @@
package top.ysoft.admin.common.constant;
/**
* 通用常量信息
*
* @author dcsoft
*/
public class Constants {
/**
* UTF-8 字符集
*/
public static final String UTF8 = "UTF-8";
/**
* GBK 字符集
*/
public static final String GBK = "GBK";
/**
* www主域
*/
public static final String WWW = "www.";
/**
* RMI 远程方法调用
*/
public static final String LOOKUP_RMI = "rmi:";
/**
* LDAP 远程方法调用
*/
public static final String LOOKUP_LDAP = "ldap:";
/**
* LDAPS 远程方法调用
*/
public static final String LOOKUP_LDAPS = "ldaps:";
/**
* http请求
*/
public static final String HTTP = "http://";
/**
* https请求
*/
public static final String HTTPS = "https://";
/**
* 成功标记
*/
public static final Integer SUCCESS = 200;
/**
* 失败标记
*/
public static final Integer FAIL = 500;
/**
* 登录成功状态
*/
public static final String LOGIN_SUCCESS_STATUS = "0";
/**
* 登录失败状态
*/
public static final String LOGIN_FAIL_STATUS = "1";
/**
* 登录成功
*/
public static final String LOGIN_SUCCESS = "Success";
/**
* 注销
*/
public static final String LOGOUT = "Logout";
/**
* 注册
*/
public static final String REGISTER = "Register";
/**
* 登录失败
*/
public static final String LOGIN_FAIL = "Error";
/**
* 当前记录起始索引
*/
public static final String PAGE_NUM = "pageNum";
/**
* 每页显示记录数
*/
public static final String PAGE_SIZE = "pageSize";
/**
* 排序列
*/
public static final String ORDER_BY_COLUMN = "orderByColumn";
/**
* 排序的方向 "desc" 或者 "asc".
*/
public static final String IS_ASC = "isAsc";
/**
* 验证码有效期(分钟)
*/
public static final long CAPTCHA_EXPIRATION = 2;
/**
* 资源映射路径 前缀
*/
public static final String RESOURCE_PREFIX = "/profile";
/**
* 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
*/
public static final String[] JOB_WHITELIST_STR = {"com.dcsoft"};
/**
* 定时任务违规的字符
*/
public static final String[] JOB_ERROR_STR = {"java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
"org.springframework", "org.apache", "com.dcsoft.common.core.utils.file"};
/**
* 0
*/
public static final String ZERO = "0";
/**
* 1
*/
public static final String ONE = "1";
/**
* 2
*/
public static final String TWO = "2";
/**
* 5
*/
public static final String FIVE = "5";
/**
* 200
*/
public static final String CODE = "200";
/**
* OK
*/
public static final String OK = "ok";
/**
* 宇泛门禁设备开关门状态1开门
*/
public static final Integer ACCESS_CONTROL_ONE = 1;
/**
* 宇泛门禁设备开关门状态2关门
*/
public static final Integer ACCESS_CONTROL_TWO = 2;
/**
*
*/
public static final String TTS_MOD_CONTENT = "未授权,请联系管理员";
/**
*
*/
public static final String DISPLAY_MOD_CONTENT = "未授权,请联系管理员";
/**
* 梯控结束时间
*/
public static final String END_TIME = "2099-12-31 23:59:59";
/**
* 梯控起始可用时间
*/
public static final String START_DAY_TIME = "00:00:00";
/**
* 梯控结束可用时间
*/
public static final String END_DAY_TIME = "23:59:59";
/**
* 梯控星期
*/
public static final String WEEK = "1 1 1 1 1 1 1";
public static final String CONTENT = "{\n" + " \"touser\": \"%\", \n" + " \"template_id\": \"wODO_BDj8n1grelUSGTNvpZbSnFdhzC3odWHek0brSM\",\n" + " \"url\": \"#\",\n" + " \"data\": {\n" + " \"first\": {\n" + " \"value\": \"广州腾讯科技有限公司\"\n" + " },\n" + " \"keyword1\": {\n" + " \"value\": \"广州腾讯科技有限公司\"\n" + " },\n" + " \"keyword2\": {\n" + " \"value\": \"2019年8月8日\"\n" + " }\n" + " }\n" + "}";
}

View File

@@ -0,0 +1,73 @@
package top.ysoft.admin.common.constant;
/**
* 数据源容器相关常量Crane4j 数据填充组件使用)
*
* @author Charles7c
* @since 2024/1/20 12:33
*/
public class ContainerConstants {
/**
* 用户昵称
*/
public static final String USER_NICKNAME = "UserNickname";
/**
* 文件信息
*/
public static final String FILE_INFO = "FileInfo";
/**
* 用户角色 ID 列表
*/
public static final String USER_ROLE_ID_LIST = "UserRoleIdList";
/**
* 用户角色名称列表
*/
public static final String USER_ROLE_NAME_LIST = "UserRoleNameList";
/**
* 用户列表
*/
public static final String USER_NAME_LIST = "UserNameList";
/**
* 用户名称
*/
public static final String USER_NAME = "UserName";
/**
* 部门名称
*/
public static final String BRANCH_NAME = "BranchName";
/**
* 空间名称
*/
public static final String SPACE_NAME = "SpaceName";
/**
* 产品名称图片
*/
public static final String PRODUCT_NAME_AVATAR = "ProductNameAvatar";
/**
* 设备名称
*/
public static final String EQUIPMENT_NAME = "EquipmentName";
/**
* 人员名称
*/
public static final String PEOPLE_NAME = "PeopleName";
/**
* 规则名称
*/
public static final String RULE_NAME = "RuleName";
private ContainerConstants() {
}
}

View File

@@ -0,0 +1,53 @@
package top.ysoft.admin.common.constant;
/**
* 正则相关常量
*
* @author Charles7c
* @since 2023/1/10 20:06
*/
public class RegexConstants {
/**
* 用户名正则(用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头)
*/
public static final String USERNAME = "^[a-zA-Z][a-zA-Z0-9_]{3,64}$";
/**
* 密码正则模板(密码长度为 min-max 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字)
*/
public static final String PASSWORD_TEMPLATE = "^(?=.*\\d)(?=.*[a-z]).{%s,%s}$";
/**
* 密码正则(密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字)
*/
public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{8,32}$";
/**
* 特殊字符正则
*/
public static final String SPECIAL_CHARACTER = "[-_`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~@#¥%……&*()——+|{}【】‘;:”“’。,、?]|\\n|\\r|\\t";
/**
* 通用编码正则(长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头)
*/
public static final String GENERAL_CODE = "^[a-zA-Z][a-zA-Z0-9_]{1,29}$";
/**
* 通用名称正则(长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线)
*/
public static final String GENERAL_NAME = "^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{2,30}$";
/**
* 包名正则(可以包含大小写字母、数字、下划线,每一级包名不能以数字开头)
*/
public static final String PACKAGE_NAME = "^(?:[a-zA-Z_][a-zA-Z0-9_]*\\.)*[a-zA-Z_][a-zA-Z0-9_]*$";
/**
* 金额正则表达式:纯数字,保留两位小数
*/
public static final String MONEY = "^\\d+(\\.\\d{2})?$";
private RegexConstants() {
}
}

View File

@@ -0,0 +1,68 @@
package top.ysoft.admin.common.constant;
/**
* 系统相关常量
*
* @author Charles7c
* @since 2023/2/9 22:11
*/
public class SysConstants {
/**
* 否
*/
public static final Integer NO = 0;
/**
* 是
*/
public static final Integer YES = 1;
/**
* 超管用户 ID
*/
public static final Long SUPER_USER_ID = 1L;
/**
* 顶级部门 ID
*/
public static final Long SUPER_DEPT_ID = 1L;
/**
* 顶级父 ID
*/
public static final Long SUPER_PARENT_ID = 0L;
/**
* 超管角色编码
*/
public static final String SUPER_ROLE_CODE = "admin";
/**
* 超管角色 ID
*/
public static final Long SUPER_ROLE_ID = 1L;
/**
* 全部权限标识
*/
public static final String ALL_PERMISSION = "*:*:*";
/**
* 登录 URI
*/
public static final String LOGIN_URI = "/auth/login";
/**
* 登出 URI
*/
public static final String LOGOUT_URI = "/auth/logout";
/**
* 描述类字段后缀
*/
public static final String DESCRIPTION_FIELD_SUFFIX = "String";
private SysConstants() {
}
}

View File

@@ -0,0 +1,38 @@
package top.ysoft.admin.common.constant;
/**
* UI 相关常量
*
* @author Charles7c
* @since 2023/9/17 14:12
*/
public class UiConstants {
/**
* 主色(极致蓝)
*/
public static final String COLOR_PRIMARY = "arcoblue";
/**
* 成功色(仙野绿)
*/
public static final String COLOR_SUCCESS = "green";
/**
* 警告色(活力橙)
*/
public static final String COLOR_WARNING = "orangered";
/**
* 错误色(浪漫红)
*/
public static final String COLOR_ERROR = "red";
/**
* 默认色(中性灰)
*/
public static final String COLOR_DEFAULT = "gray";
private UiConstants() {
}
}

View File

@@ -0,0 +1,43 @@
package top.ysoft.admin.common.context;
import lombok.Data;
import lombok.NoArgsConstructor;
import top.ysoft.admin.common.enums.DataScopeEnum;
import java.io.Serial;
import java.io.Serializable;
/**
* 角色上下文
*
* @author Charles7c
* @since 2023/3/7 22:08
*/
@Data
@NoArgsConstructor
public class RoleContext implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 角色编码
*/
private String code;
/**
* 数据权限
*/
private DataScopeEnum dataScope;
public RoleContext(Long id, String code, DataScopeEnum dataScope) {
this.id = id;
this.code = code;
this.dataScope = dataScope;
}
}

View File

@@ -0,0 +1,116 @@
package top.ysoft.admin.common.context;
import cn.hutool.core.collection.CollUtil;
import lombok.Data;
import lombok.NoArgsConstructor;
import top.ysoft.admin.common.constant.SysConstants;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 用户上下文
*
* @author Charles7c
* @since 2024/10/9 20:29
*/
@Data
@NoArgsConstructor
public class UserContext implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 部门 ID
*/
private Long deptId;
/**
* 最后一次修改密码时间
*/
private LocalDateTime pwdResetTime;
/**
* 登录时系统设置的密码过期天数
*/
private Integer passwordExpirationDays;
/**
* 权限码集合
*/
private Set<String> permissions;
/**
* 角色编码集合
*/
private Set<String> roleCodes;
/**
* 角色集合
*/
private Set<RoleContext> roles;
/**
* 终端类型
*/
private String clientType;
/**
* 终端 ID
*/
private String clientId;
public UserContext(Set<String> permissions, Set<RoleContext> roles, Integer passwordExpirationDays) {
this.permissions = permissions;
this.setRoles(roles);
this.passwordExpirationDays = passwordExpirationDays;
}
public void setRoles(Set<RoleContext> roles) {
this.roles = roles;
this.roleCodes = roles.stream().map(RoleContext::getCode).collect(Collectors.toSet());
}
/**
* 是否为管理员
*
* @return truefalse
*/
public boolean isAdmin() {
if (CollUtil.isEmpty(roleCodes)) {
return false;
}
return roleCodes.contains(SysConstants.SUPER_ROLE_CODE);
}
/**
* 密码是否已过期
*
* @return 是否过期
*/
public boolean isPasswordExpired() {
// 永久有效
if (this.passwordExpirationDays == null || this.passwordExpirationDays <= SysConstants.NO) {
return false;
}
// 初始密码(第三方登录用户)暂不提示修改
if (this.pwdResetTime == null) {
return false;
}
return this.pwdResetTime.plusDays(this.passwordExpirationDays).isBefore(LocalDateTime.now());
}
}

View File

@@ -0,0 +1,167 @@
package top.ysoft.admin.common.context;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.extra.spring.SpringUtil;
import top.ysoft.admin.common.service.CommonUserService;
import top.continew.starter.core.util.ExceptionUtils;
/**
* 用户上下文 Holder
*
* @author Charles7c
* @since 2022/12/24 12:58
*/
public class UserContextHolder {
private static final ThreadLocal<UserContext> CONTEXT_HOLDER = new ThreadLocal<>();
private static final ThreadLocal<UserExtraContext> EXTRA_CONTEXT_HOLDER = new ThreadLocal<>();
private UserContextHolder() {
}
/**
* 设置上下文
*
* @param context 上下文
*/
public static void setContext(UserContext context) {
setContext(context, true);
}
/**
* 设置上下文
*
* @param context 上下文
* @param isUpdate 是否更新
*/
public static void setContext(UserContext context, boolean isUpdate) {
CONTEXT_HOLDER.set(context);
if (isUpdate) {
StpUtil.getSessionByLoginId(context.getId()).set(SaSession.USER, context);
}
}
/**
* 获取上下文
*
* @return 上下文
*/
public static UserContext getContext() {
UserContext context = CONTEXT_HOLDER.get();
if (null == context) {
context = StpUtil.getSession().getModel(SaSession.USER, UserContext.class);
CONTEXT_HOLDER.set(context);
}
return context;
}
/**
* 获取指定用户的上下文
*
* @param userId 用户 ID
* @return 上下文
*/
public static UserContext getContext(Long userId) {
SaSession session = StpUtil.getSessionByLoginId(userId, false);
if (null == session) {
return null;
}
return session.getModel(SaSession.USER, UserContext.class);
}
/**
* 设置额外上下文
*
* @param context 额外上下文
*/
public static void setExtraContext(UserExtraContext context) {
EXTRA_CONTEXT_HOLDER.set(context);
}
/**
* 获取额外上下文
*
* @return 额外上下文
*/
public static UserExtraContext getExtraContext() {
UserExtraContext context = EXTRA_CONTEXT_HOLDER.get();
if (null == context) {
context = getExtraContext(StpUtil.getTokenValue());
EXTRA_CONTEXT_HOLDER.set(context);
}
return context;
}
/**
* 获取额外上下文
*
* @param token 令牌
* @return 额外上下文
*/
public static UserExtraContext getExtraContext(String token) {
UserExtraContext context = new UserExtraContext();
context.setIp(Convert.toStr(StpUtil.getExtra(token, "ip")));
context.setAddress(Convert.toStr(StpUtil.getExtra(token, "address")));
context.setBrowser(Convert.toStr(StpUtil.getExtra(token, "browser")));
context.setOs(Convert.toStr(StpUtil.getExtra(token, "os")));
context.setLoginTime(Convert.toLocalDateTime(StpUtil.getExtra(token, "loginTime")));
return context;
}
/**
* 清除上下文
*/
public static void clearContext() {
CONTEXT_HOLDER.remove();
EXTRA_CONTEXT_HOLDER.remove();
}
/**
* 获取用户 ID
*
* @return 用户 ID
*/
public static Long getUserId() {
return ExceptionUtils.exToNull(() -> getContext().getId());
}
/**
* 获取用户名
*
* @return 用户名
*/
public static String getUsername() {
return ExceptionUtils.exToNull(() -> getContext().getUsername());
}
/**
* 获取用户昵称
*
* @return 用户昵称
*/
public static String getNickname() {
return getNickname(getUserId());
}
/**
* 获取用户昵称
*
* @param userId 登录用户 ID
* @return 用户昵称
*/
public static String getNickname(Long userId) {
return ExceptionUtils.exToNull(() -> SpringUtil.getBean(CommonUserService.class).getNicknameById(userId));
}
/**
* 是否为管理员
*
* @return 是否为管理员
*/
public static boolean isAdmin() {
StpUtil.checkLogin();
return getContext().isAdmin();
}
}

View File

@@ -0,0 +1,61 @@
package top.ysoft.admin.common.context;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.NoArgsConstructor;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.util.IpUtils;
import top.continew.starter.web.util.ServletUtils;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户额外上下文
*
* @author Charles7c
* @since 2024/10/9 20:29
*/
@Data
@NoArgsConstructor
public class UserExtraContext implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* IP
*/
private String ip;
/**
* IP 归属地
*/
private String address;
/**
* 浏览器
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 登录时间
*/
private LocalDateTime loginTime;
public UserExtraContext(HttpServletRequest request) {
this.ip = JakartaServletUtil.getClientIP(request);
this.address = ExceptionUtils.exToNull(() -> IpUtils.getIpv4Address(this.ip));
this.setBrowser(ServletUtils.getBrowser(request));
this.setLoginTime(LocalDateTime.now());
this.setOs(StrUtil.subBefore(ServletUtils.getOs(request), " or", false));
}
}

View File

@@ -0,0 +1,51 @@
package top.ysoft.admin.common.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.sign.SaSignTemplate;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.text.CharSequenceUtil;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.annotation.CrudApi;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.controller.AbstractBaseController;
import top.continew.starter.extension.crud.enums.Api;
import top.continew.starter.extension.crud.service.BaseService;
import java.lang.reflect.Method;
import java.util.List;
/**
* 控制器基类
*
* @param <S> 业务接口
* @param <L> 列表类型
* @param <D> 详情类型
* @param <Q> 查询条件
* @param <C> 创建或修改参数类型
* @author Charles7c
* @since 2024/12/6 20:30
*/
public class BaseController<S extends BaseService<L, D, Q, C>, L, D, Q, C> extends AbstractBaseController<S, L, D, Q, C> {
@Override
public void preHandle(CrudApi crudApi, Object[] args, Method targetMethod, Class<?> targetClass) throws Exception {
SaRequest saRequest = SaHolder.getRequest();
List<String> paramNames = saRequest.getParamNames();
if (paramNames.stream().anyMatch(SaSignTemplate.sign::equals)) {
return;
}
if (AnnotationUtil.hasAnnotation(targetMethod, SaIgnore.class) || AnnotationUtil
.hasAnnotation(targetClass, SaIgnore.class)) {
return;
}
CrudRequestMapping crudRequestMapping = targetClass.getDeclaredAnnotation(CrudRequestMapping.class);
String path = crudRequestMapping.value();
String prefix = String.join(StringConstants.COLON, CharSequenceUtil.splitTrim(path, StringConstants.SLASH));
Api api = crudApi.value();
String apiName = Api.PAGE.equals(api) || Api.TREE.equals(api) ? Api.LIST.name() : api.name();
StpUtil.checkPermission("%s:%s".formatted(prefix, apiName.toLowerCase()));
}
}

View File

@@ -0,0 +1,22 @@
package top.ysoft.admin.common.enums;
public enum CommonExceptionEnum {
RULE_ERROR(1000, "您所所在得部门所属规则未绑定设备"), REQUEST_ERROR(1001, "请求异常"), PEOPLE_ERROR(1002, "您的信息未录入"),
QR_CODE_ERROR(1003, "二维码过期"), DEVICE_INITIALIZE_ERROR(1004, "设备序列号不能为空"),;
private Integer errCode;
private String errMsg;
CommonExceptionEnum(Integer errCode, String errMsg) {
this.errCode = errCode;
this.errMsg = errMsg;
}
public Integer getCode() {
return errCode;
}
public String getMessage() {
return errMsg;
}
}

View File

@@ -0,0 +1,24 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 交易状态枚举
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum ConsumeOrderTypeEnum implements BaseEnum<Integer> {
/**
* 在线消费
*/
CONSUME(0, "消费");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,49 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 支付方式枚举
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum ConsumePayModeEnum implements BaseEnum<Integer> {
/**
* 人脸
*/
FACE(0, "人脸"),
/**
* 云卡
*/
CLOUD_CARD(1, "云卡"),
/**
* 刷卡
*/
SWIPE_CARD(2, "刷卡"),
/**
* 支付宝
*/
ALIPAY(3, "支付宝"),
/**
* 微信
*/
WECHAT(4, "微信"),
/**
* 取餐码
*/
MEAL_CODE(5, "取餐码");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,39 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 消费-充值方式
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum ConsumeRechargeModeEnum implements BaseEnum<Integer> {
/**
* 现金
*/
CASH(0, "现金"),
/**
* 支付宝
*/
ALIPAY(1, "支付宝"),
/**
* 微信
*/
WECHAT(2, "微信"),
/**
* 网银
*/
ONLINE_BANKING(3, "网银");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,49 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 消费-操作类型
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum ConsumeRechargeTypeEnum implements BaseEnum<Integer> {
/**
* 补贴
*/
SUBSIDY(0, "补贴"),
/**
* 充值
*/
RECHARGE(1, "充值"),
/**
* 退款
*/
REFUND(2, "退款"),
/**
* 清零(全部)
*/
CLEAR(3, "清零(全部)"),
/**
* 清零(充值)
*/
CLEAR_CZ(4, "清零(充值)"),
/**
* 清零(补贴)
*/
CLEAR_BT(5, "清零(补贴)");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,29 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 消费-充值来源
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum ConsumeRechargeWayEnum implements BaseEnum<Integer> {
/**
* 在线消费
*/
WEB(0, "平台"),
/**
* 离线消费
*/
PHONE(1, "手机");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,44 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 交易状态枚举
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum ConsumeResultEnum implements BaseEnum<Integer> {
/**
* 在线消费
*/
ONLINE_CONSUME(0, "在线消费"),
/**
* 离线消费
*/
OFFLINE_CONSUME(1, "离线消费"),
/**
* 超时
*/
TIMEOUT(2, "超时"),
/**
* 消费异常
*/
CONSUMPTION_EXCEPTION(3, "消费异常"),
/**
* 异常消费
*/
EXCEPTION_CONSUMPTION(4, "异常消费");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,38 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 消费-充值来源
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum ConsumeTmrtypeEnum implements BaseEnum<Integer> {
/**
* 早餐
*/
BREAKFAST(0, "早餐"),
/**
* 中餐
*/
LUNCH(1, "中餐"),
/**
* 午餐
*/
DINNER(2, "午餐"),
/**
* 夜餐
*/
NIGHT(3, "夜餐");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,49 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 消费类型枚举
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum ConsumeTypeConverter implements BaseEnum<Integer> {
/**
* 单价
*/
UNIT_PRICE(0, "单价"),
/**
* 定额
*/
FIXED_PRICE(1, "定额"),
/**
* 时段模式
*/
TIME_PERIOD(2, "时段模式"),
/**
* 计次
*/
COUNTING(3, "计次"),
/**
* 点餐机模式
*/
ORDERING_MACHINE(5, "点餐机模式"),
/**
* 身份模式
*/
IDENTITY(9, "身份模式");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,34 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 钱包消费模式枚举
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum ConsumeWalletModeEnum implements BaseEnum<Integer> {
/**
* 先消费补贴再个人
*/
SUBSIDY_THEN_PERSONAL(0, "先消费补贴再个人"),
/**
* 仅现金
*/
ONLY_CASH(1, "仅现金"),
/**
* 仅补贴
*/
ONLY_SUBSIDY(2, "仅补贴");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,44 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 数据权限枚举
*
* @author Charles7c
* @since 2023/2/8 22:58
*/
@Getter
@RequiredArgsConstructor
public enum DataScopeEnum implements BaseEnum<Integer> {
/**
* 全部数据权限
*/
ALL(1, "全部数据权限"),
/**
* 本部门及以下数据权限
*/
DEPT_AND_CHILD(2, "本部门及以下数据权限"),
/**
* 本部门数据权限
*/
DEPT(3, "本部门数据权限"),
/**
* 仅本人数据权限
*/
SELF(4, "仅本人数据权限"),
/**
* 自定义数据权限
*/
CUSTOM(5, "自定义数据权限"),;
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,31 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.ysoft.admin.common.constant.UiConstants;
import top.continew.starter.core.enums.BaseEnum;
/**
* 启用/禁用状态枚举
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum DisEnableStatusEnum implements BaseEnum<Integer> {
/**
* 启用
*/
ENABLE(1, "启用", UiConstants.COLOR_SUCCESS),
/**
* 禁用
*/
DISABLE(2, "禁用", UiConstants.COLOR_ERROR),;
private final Integer value;
private final String description;
private final String color;
}

View File

@@ -0,0 +1,30 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 启用禁用
*
* @author Charles7c
* @since 2022/12/29 22:38
*/
@Getter
@RequiredArgsConstructor
public enum EnableEnum implements BaseEnum<Integer> {
/**
* 禁用
*/
DISABLE(0, "禁用"),
/**
* 在线消费
*/
ENABLE(1, "启用");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,34 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
/**
* 性别枚举
*
* @author Charles7c
* @since 2022/12/29 21:59
*/
@Getter
@RequiredArgsConstructor
public enum GenderEnum implements BaseEnum<Integer> {
/**
* 未知
*/
UNKNOWN(0, "未知"),
/**
* 男
*/
MALE(1, ""),
/**
* 女
*/
FEMALE(2, ""),;
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,33 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
@Getter
@RequiredArgsConstructor
public enum OperTypeEnum implements BaseEnum<Integer> {
/**
* 新增
*/
ADD(0, "新增"),
/**
* 修改
*/
UPDATE(1, "修改"),
/**
* 下发
*/
DOWN(2, "下发"),
/**
* 删除
*/
DEL(3, "删除");
private final Integer value;
private final String description;
}

View File

@@ -0,0 +1,31 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.ysoft.admin.common.constant.UiConstants;
import top.continew.starter.core.enums.BaseEnum;
/**
* 成功/失败状态枚举
*
* @author Charles7c
* @since 2023/2/26 21:35
*/
@Getter
@RequiredArgsConstructor
public enum SuccessFailureStatusEnum implements BaseEnum<Integer> {
/**
* 成功
*/
SUCCESS(0, "成功", UiConstants.COLOR_SUCCESS),
/**
* 失败
*/
FAILURE(1, "失败", UiConstants.COLOR_ERROR),;
private final Integer value;
private final String description;
private final String color;
}

View File

@@ -0,0 +1,45 @@
package top.ysoft.admin.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.core.enums.BaseEnum;
import top.ysoft.admin.common.constant.UiConstants;
/**
* 成功/失败状态枚举
*
* @author Charles7c
* @since 2023/2/26 21:35
*/
@Getter
@RequiredArgsConstructor
public enum YesNoEnum implements BaseEnum<Integer> {
/**
* 否
*/
NO(0, "", UiConstants.COLOR_ERROR),
/**
* 是
*/
YES(1, "", UiConstants.COLOR_SUCCESS),;
private final Integer value;
private final String description;
private final String color;
/**
* 根据描述获取枚举常量
* @param description 描述
* @return 枚举常量
*/
public static YesNoEnum getByDescription(String description) {
for (YesNoEnum enumValue : values()) {
if (enumValue.getDescription().equals(description)) {
return enumValue;
}
}
throw new IllegalArgumentException("No enum constant with description: " + description);
}
}

View File

@@ -0,0 +1,38 @@
package top.ysoft.admin.common.model.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 实体类基类
*
* <p>
* 通用字段:创建人、创建时间
* </p>
*
* @author Charles7c
* @since 2025/1/12 23:00
*/
@Data
public class BaseCreateDO extends BaseIdDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private Long createUser;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,46 @@
package top.ysoft.admin.common.model.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 实体类基类
*
* @author Charles7c
* @since 2025/1/12 23:00
*/
@Data
public class BaseDO extends BaseIdDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private Long createUser;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 修改人
*/
@TableField(fill = FieldFill.UPDATE)
private Long updateUser;
/**
* 修改时间
*/
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,51 @@
package top.ysoft.admin.common.model.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 实体类基类
*
* @author Charles7c
* @since 2025/1/12 23:00
*/
@Data
public class BaseStrIdDO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private String id;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private Long createUser;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 修改人
*/
@TableField(fill = FieldFill.UPDATE)
private Long updateUser;
/**
* 修改时间
*/
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,39 @@
package top.ysoft.admin.common.model.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serial;
import java.time.LocalDateTime;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
/**
* 实体类基类
*
* <p>
* 通用字段:创建人、创建时间
* </p>
*
* @author Charles7c
* @since 2025/1/12 23:00
*/
@Data
public class BaseUpdateDO extends BaseIdDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 修改人
*/
@TableField(fill = FieldFill.UPDATE)
private Long updateUser;
/**
* 修改时间
*/
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,26 @@
package top.ysoft.admin.common.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import top.ysoft.admin.common.enums.DisEnableStatusEnum;
import java.io.Serializable;
/**
* 修改状态请求参数
*
* @author Charles7c
* @since 2025/3/4 20:09
*/
@Data
@Schema(description = "修改状态请求参数")
public class CommonStatusUpdateReq implements Serializable {
/**
* 状态
*/
@Schema(description = "状态", example = "1")
@NotNull(message = "状态非法")
private DisEnableStatusEnum status;
}

View File

@@ -0,0 +1,51 @@
package top.ysoft.admin.common.model.resp;
import cn.crane4j.annotation.Assemble;
import cn.crane4j.annotation.Mapping;
import cn.crane4j.annotation.condition.ConditionOnPropertyNotNull;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.ysoft.admin.common.constant.ContainerConstants;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 详情响应基类
*
* @author Charles7c
* @since 2024/12/27 20:32
*/
@Data
public class BaseDetailResp extends BaseResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 修改人
*/
@JsonIgnore
@ConditionOnPropertyNotNull
@Assemble(container = ContainerConstants.USER_NICKNAME, props = @Mapping(ref = "updateUserString"))
@ExcelIgnore
private Long updateUser;
/**
* 修改人
*/
@Schema(description = "修改人", example = "李四")
// @ExcelProperty(value = "修改人", order = Integer.MAX_VALUE - 2)
@ExcelIgnore
private String updateUserString;
/**
* 修改时间
*/
@Schema(description = "修改时间", example = "2023-08-08 08:08:08", type = "string")
// @ExcelProperty(value = "修改时间", order = Integer.MAX_VALUE - 1)
@ExcelIgnore
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,67 @@
package top.ysoft.admin.common.model.resp;
import cn.crane4j.annotation.Assemble;
import cn.crane4j.annotation.Mapping;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.ysoft.admin.common.constant.ContainerConstants;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 响应参数基类
*
* @author Charles7c
* @since 2024/12/27 20:32
*/
@Data
public class BaseResp implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@Schema(description = "ID", example = "1")
// @ExcelProperty(value = "ID", order = 1)
@ExcelIgnore
private Long id;
/**
* 创建人
*/
@JsonIgnore
@Assemble(container = ContainerConstants.USER_NICKNAME, props = @Mapping(ref = "createUserString"))
@ExcelIgnore
private Long createUser;
/**
* 创建人
*/
@Schema(description = "创建人", example = "超级管理员")
// @ExcelProperty(value = "创建人", order = Integer.MAX_VALUE - 4)
@ExcelIgnore
private String createUserString;
/**
* 创建时间
*/
@Schema(description = "创建时间", example = "2023-08-08 08:08:08", type = "string")
// @ExcelProperty(value = "创建时间", order = Integer.MAX_VALUE - 3)
@ExcelIgnore
private LocalDateTime createTime;
/**
* 是否禁用修改
*/
@Schema(description = "是否禁用修改", example = "true")
@JsonInclude(JsonInclude.Include.NON_NULL)
@ExcelIgnore
private Boolean disabled;
}

View File

@@ -0,0 +1,62 @@
package top.ysoft.admin.common.model.resp;
import cn.crane4j.annotation.Assemble;
import cn.crane4j.annotation.Mapping;
import com.alibaba.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.ysoft.admin.common.constant.ContainerConstants;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 响应参数基类
*
* @author Charles7c
* @since 2024/12/27 20:32
*/
@Data
public class BaseStrIdResp implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@Schema(description = "ID", example = "1")
@ExcelProperty(value = "ID", order = 1)
private String id;
/**
* 创建人
*/
@JsonIgnore
@Assemble(container = ContainerConstants.USER_NICKNAME, props = @Mapping(ref = "createUserString"))
private Long createUser;
/**
* 创建人
*/
@Schema(description = "创建人", example = "超级管理员")
@ExcelProperty(value = "创建人", order = Integer.MAX_VALUE - 4)
private String createUserString;
/**
* 创建时间
*/
@Schema(description = "创建时间", example = "2023-08-08 08:08:08", type = "string")
@ExcelProperty(value = "创建时间", order = Integer.MAX_VALUE - 3)
private LocalDateTime createTime;
/**
* 是否禁用修改
*/
@Schema(description = "是否禁用修改", example = "true")
@JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean disabled;
}

View File

@@ -0,0 +1,27 @@
package top.ysoft.admin.common.service;
import cn.crane4j.annotation.ContainerMethod;
import cn.crane4j.annotation.MappingType;
import top.ysoft.admin.common.constant.ContainerConstants;
/**
* 公共用户业务接口
*
* @author Charles7c
* @since 2025/1/9 20:17
*/
public interface CommonUserService {
/**
* 根据 ID 查询昵称
*
* <p>
* 数据填充容器 {@link ContainerConstants#USER_NICKNAME}
* </p>
*
* @param id ID
* @return 昵称
*/
@ContainerMethod(namespace = ContainerConstants.USER_NICKNAME, type = MappingType.ORDER_OF_KEYS)
String getNicknameById(Long id);
}

View File

@@ -0,0 +1,148 @@
package top.ysoft.admin.common.util;
import org.apache.commons.codec.binary.Base64;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
public class ImageToBase64Utils {
/**
* 本地图片转base64
*/
public static String getImgFileToBase642(String imgFile) {
//将图片文件转化为字节数组字符串并对其进行Base64编码处理
byte[] buffer = null;
//读取图片字节数组
try (InputStream inputStream = new FileInputStream(imgFile);) {
int count = 0;
while (count == 0) {
count = inputStream.available();
}
buffer = new byte[count];
inputStream.read(buffer);
} catch (IOException e) {
e.printStackTrace();
}
// 对字节数组Base64编码
return new String(Base64.encodeBase64(buffer));
}
/**
* 网络图片转base64
*/
public static String getImgUrlToBase64(String imgUrl) {
byte[] buffer = null;
InputStream inputStream = null;
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();) {
// 创建URL
URL url = new URL(imgUrl);
// 创建链接
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
inputStream = conn.getInputStream();
// 将内容读取内存中
buffer = new byte[1024];
int len = -1;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
buffer = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
// 关闭inputStream流
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 对字节数组Base64编码
return new String(Base64.encodeBase64(buffer));
}
/**
* 本地或网络图片转base64
*/
public static String getImgStrToBase64(String imgStr) {
InputStream inputStream = null;
byte[] buffer = null;
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();) {
//判断网络链接图片文件/本地目录图片文件
if (imgStr.startsWith("http://") || imgStr.startsWith("https://")) {
// 创建URL
URL url = new URL(imgStr);
// 创建链接
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
inputStream = conn.getInputStream();
// 将内容读取内存中
buffer = new byte[1024];
int len = -1;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
buffer = outputStream.toByteArray();
} else {
inputStream = new FileInputStream(imgStr);
int count = 0;
while (count == 0) {
count = inputStream.available();
}
buffer = new byte[count];
inputStream.read(buffer);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
// 关闭inputStream流
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 对字节数组Base64编码
return new String(Base64.encodeBase64(buffer));
}
/**
* 对字节数组字符串进行Base64解码并生成图片
*
* @param imgStr 图片数据
* @param imgFilePath 保存图片全路径地址
* @return
*/
public static boolean generateImage(String imgStr, String imgFilePath) {
//
if (imgStr == null) //图像数据为空
return false;
try {
//Base64解码
byte[] b = Base64.decodeBase64(imgStr);
for (int i = 0; i < b.length; ++i) {
if (b[i] < 0) {//调整异常数据
b[i] += 256;
}
}
//生成jpeg图片
OutputStream out = new FileOutputStream(imgFilePath);
out.write(b);
out.flush();
out.close();
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@@ -0,0 +1,155 @@
package top.ysoft.admin.common.util;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import top.continew.starter.core.exception.BusinessException;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
// java将图片的url转换成FileFile转换成二进制流byte
public class PictureUtils {
//将Url转换为File
public static File UrltoFile(String url) throws Exception {
HttpURLConnection httpUrl = (HttpURLConnection)new URL(url).openConnection();
httpUrl.connect();
InputStream ins = httpUrl.getInputStream();
File file = new File(System.getProperty("java.io.tmpdir") + File.separator + "xie.jpg");
if (file.exists()) {
file.delete();//如果缓存中存在该文件就删除
}
OutputStream os = new FileOutputStream(file);
int bytesRead;
int len = 8192;
byte[] buffer = new byte[len];
while ((bytesRead = ins.read(buffer, 0, len)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
ins.close();
return file;
}
//将File对象转换为byte[]的形式
public static byte[] FileTobyte(File file) {
FileInputStream fileInputStream = null;
byte[] imgData = null;
try {
imgData = new byte[(int)file.length()];
//read file into bytes[]
fileInputStream = new FileInputStream(file);
fileInputStream.read(imgData);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return imgData;
}
/**
* url转变为 MultipartFile对象
*
* @param imageUrl 网络地址链接
* @param fileName 文件名
* @return
* @throws Exception
*/
public static MultipartFile createMultipartFile(String imageUrl, String fileName) {
try {
// 将在线图片地址转换为URL对象
URL url = new URL(imageUrl);
// 打开URL连接
URLConnection connection = url.openConnection();
// 转换为HttpURLConnection对象
HttpURLConnection httpURLConnection = (HttpURLConnection)connection;
// 获取输入流
InputStream inputStream = httpURLConnection.getInputStream();
// 读取输入流中的数据,并保存到字节数组中
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
// 将字节数组转换为字节数组
byte[] bytes = byteArrayOutputStream.toByteArray();
// 创建ByteArrayInputStream对象将字节数组传递给它
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// 创建MultipartFile对象将ByteArrayInputStream对象作为构造函数的参数
return new MockMultipartFile(fileName, fileName + ".jpg", "image/jpg", byteArrayInputStream);
} catch (IOException ex) {
throw new BusinessException("附件无效");
}
}
/**
* 根据文件名获取contentType
*/
private static String getContentType(String fileName) {
// 使用Java标准库的方法替代Hutool的StrUtil
String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
Map<String, String> contentTypeMap = new HashMap<>();
contentTypeMap.put("jpg", "image/jpeg");
contentTypeMap.put("jpeg", "image/jpeg");
contentTypeMap.put("png", "image/png");
contentTypeMap.put("gif", "image/gif");
contentTypeMap.put("bmp", "image/bmp");
// 可以根据需要添加更多的文件类型
return contentTypeMap.getOrDefault(extension, "application/octet-stream");
}
/**
* 直接从输入流压缩图片并转换为MultipartFile
*
* @param inputStream 图片输入流
* @param originalFilename 原始文件名
* @param quality 压缩质量 (0.0-1.0)
* @return MultipartFile对象
*/
public static MultipartFile compressImage(InputStream inputStream,
String originalFilename,
float quality) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
// 直接将压缩后的图片写入内存流
Thumbnails.of(inputStream).scale(1f).outputQuality(quality).toOutputStream(outputStream);
// 获取文件扩展名和contentType
String extension = originalFilename.substring(originalFilename.lastIndexOf('.') + 1).toLowerCase();
String contentType = getContentType(originalFilename);
// 创建MultipartFile对象
byte[] compressedBytes = outputStream.toByteArray();
ByteArrayInputStream input = new ByteArrayInputStream(compressedBytes);
return new MockMultipartFile("file", originalFilename, contentType, input);
} finally {
// 确保流关闭
outputStream.close();
inputStream.close();
}
}
}

View File

@@ -0,0 +1,91 @@
package top.ysoft.admin.common.util;
import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.extra.spring.SpringUtil;
import top.ysoft.admin.common.config.properties.RsaProperties;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
import top.continew.starter.security.crypto.encryptor.AesEncryptor;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import java.util.List;
import java.util.stream.Collectors;
/**
* 加密/解密工具类
*
* @author Charles7c
* @since 2022/12/21 21:41
*/
public class SecureUtils {
private SecureUtils() {
}
/**
* 公钥加密
*
* @param data 要加密的内容
* @return 加密后的内容
*/
public static String encryptByRsaPublicKey(String data) {
String publicKey = RsaProperties.PUBLIC_KEY;
ValidationUtils.throwIfBlank(publicKey, "请配置 RSA 公钥");
return encryptByRsaPublicKey(data, publicKey);
}
/**
* 私钥解密
*
* @param data 要解密的内容Base64 加密过)
* @return 解密后的内容
*/
public static String decryptByRsaPrivateKey(String data) {
String privateKey = RsaProperties.PRIVATE_KEY;
ValidationUtils.throwIfBlank(privateKey, "请配置 RSA 私钥");
return decryptByRsaPrivateKey(data, privateKey);
}
/**
* 公钥加密
*
* @param data 要加密的内容
* @param publicKey 公钥
* @return 加密后的内容
*/
public static String encryptByRsaPublicKey(String data, String publicKey) {
return new String(SecureUtil.rsa(null, publicKey).encrypt(data, KeyType.PublicKey));
}
/**
* 私钥解密
*
* @param data 要解密的内容Base64 加密过)
* @param privateKey 私钥
* @return 解密后的内容
*/
public static String decryptByRsaPrivateKey(String data, String privateKey) {
return new String(SecureUtil.rsa(privateKey, null).decrypt(Base64.decode(data), KeyType.PrivateKey));
}
/**
* 对普通加密字段列表进行AES加密优化starter加密模块后优化这个方法
*
* @param values 待加密内容
* @return 加密后内容
*/
public static List<String> encryptFieldByAes(List<String> values) {
IEncryptor encryptor = new AesEncryptor();
CryptoProperties properties = SpringUtil.getBean(CryptoProperties.class);
return values.stream().map(value -> {
try {
return encryptor.encrypt(value, properties.getPassword(), properties.getPublicKey());
} catch (Exception e) {
throw new BusinessException("字段加密异常");
}
}).collect(Collectors.toList());
}
}