chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>platform-system</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>system-domain</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-sdk</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-infra</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Cache.
|
||||
*/
|
||||
package com.xspaceagi.system.domain.facade.cache;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Distributed Lock.
|
||||
*/
|
||||
package com.xspaceagi.system.domain.facade.lock;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Message Queue.
|
||||
*/
|
||||
package com.xspaceagi.system.domain.facade.mq;
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* DDD Domain层和Infrastructure的粘合剂:通过倒置依赖.
|
||||
*
|
||||
* <p>domain层声明需要基础设施层实现的接口:RPC, DB, Cache, MQ等.</p>
|
||||
* <p>为了方便产品人员查看领域层代码,梳理业务,统一放在facade package,减少对产品同学的干扰.</p>
|
||||
*/
|
||||
package com.xspaceagi.system.domain.facade;
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.xspaceagi.system.domain.facade.rpc;
|
||||
|
||||
import com.xspaceagi.system.infra.dao.model.OperatorLogModel;
|
||||
import com.xspaceagi.system.infra.repository.ISysOperatorLogRepository;
|
||||
import com.xspaceagi.system.sdk.retry.annotation.Retry;
|
||||
import com.xspaceagi.system.sdk.server.ITrackerReportService;
|
||||
|
||||
import com.xspaceagi.system.spec.common.OperatorLogContext;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class TrackerReportServiceImpl implements ITrackerReportService {
|
||||
|
||||
@Resource
|
||||
private ISysOperatorLogRepository sysOperatorLogRepository;
|
||||
|
||||
@Retry(async = false)
|
||||
@Override
|
||||
public void reportLog(OperatorLogContext context) {
|
||||
|
||||
// RequestContext.get() 为空,租户id就为空,无法插入日志,忽略插入日志
|
||||
if (Objects.isNull(RequestContext.get()) || Objects.isNull(RequestContext.get().getTenantId())) {
|
||||
log.warn("RequestContext.get() 为空,租户id就为空,无法插入日志,忽略插入日志");
|
||||
return;
|
||||
}
|
||||
|
||||
var model = OperatorLogModel.convertToModel(context);
|
||||
|
||||
this.sysOperatorLogRepository.addInfo(model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* RPC.
|
||||
*/
|
||||
package com.xspaceagi.system.domain.facade.rpc;
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import(LogPrintConfiguration.class)
|
||||
public @interface EnableLogPrint {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
/**
|
||||
* 告警接口,需要用户手动实现,如果开启 @LogRecordPrint 的sendAlarm 为 true的话
|
||||
*/
|
||||
public interface ILogRecordAlarm {
|
||||
|
||||
/**
|
||||
* 发送告警信息
|
||||
*
|
||||
* @param alarmCode 告警自定义编码[可选值]
|
||||
* @param content 步骤,自定义
|
||||
* @param message 消息,自定义
|
||||
*/
|
||||
void sendAlarm(String alarmCode, String content, String message);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
public final class LogConstants {
|
||||
|
||||
public static final String PREFIX = "prefix";
|
||||
|
||||
public static final String TENANT_ID = "tenantId";
|
||||
|
||||
public static final String LOCALE_ID = "localeId";
|
||||
|
||||
public static final String BILL_NO = "billNo";
|
||||
|
||||
public static final String STEP = "step";
|
||||
|
||||
public static final String TOPIC = "topic";
|
||||
|
||||
public static final String BUSINESS_TYPE = "businessType";
|
||||
|
||||
public static final String MESSAGE_ID = "messageId";
|
||||
|
||||
public static final String CONTENT = "content";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface LogPrint {
|
||||
|
||||
/**
|
||||
* 门店ID
|
||||
*/
|
||||
String localeId() default "";
|
||||
|
||||
/**
|
||||
* 业务逻辑名字,方便区分日志
|
||||
*/
|
||||
String step() default "";
|
||||
|
||||
/**
|
||||
* 单号位置
|
||||
*/
|
||||
String billNo() default "";
|
||||
|
||||
|
||||
/**
|
||||
* topic
|
||||
*/
|
||||
String topic() default "";
|
||||
|
||||
|
||||
/**
|
||||
* messageId
|
||||
*/
|
||||
String messageId() default "";
|
||||
|
||||
|
||||
/**
|
||||
* 业务类型
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String businessType() default "";
|
||||
|
||||
|
||||
/**
|
||||
* 自定义的消息
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String content() default "";
|
||||
|
||||
|
||||
/**
|
||||
* 发生异常后是否发送自定义告警,默认不发送
|
||||
*/
|
||||
boolean alarmWhenException() default false;
|
||||
|
||||
|
||||
/**
|
||||
* 日志记录类型,默认
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
LogType logType() default LogType.NORMAL;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
import com.xspaceagi.system.domain.log.record.LogPrintAspect;
|
||||
import com.xspaceagi.system.domain.log.record.LogPrintAspectSupport;
|
||||
import com.xspaceagi.system.domain.log.record.LogRecordPrintAspect;
|
||||
import com.xspaceagi.system.domain.log.record.LogRecordPrintAspectSupport;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class LogPrintConfiguration {
|
||||
|
||||
|
||||
/**
|
||||
* 打印请求入参和结果
|
||||
*/
|
||||
@Bean
|
||||
public LogRecordPrintAspectSupport logRecordPrintAspectSupport() {
|
||||
return new LogRecordPrintAspectSupport();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogRecordPrintAspect logRecordPrintAspect() {
|
||||
return new LogRecordPrintAspect();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public LogPrintAspectSupport logPrintAspectSupport() {
|
||||
return new LogPrintAspectSupport();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogPrintAspect logPrintAspect() {
|
||||
return new LogPrintAspect();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class LogPrintEvaluationContext extends MethodBasedEvaluationContext {
|
||||
|
||||
public LogPrintEvaluationContext(final Object rootObject, final Method method, final Object[] arguments,
|
||||
final ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
super(rootObject, method, arguments, parameterNameDiscoverer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.context.expression.AnnotatedElementKey;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.context.expression.CachedExpressionEvaluator;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class LogPrintExpressionEvaluator extends CachedExpressionEvaluator {
|
||||
|
||||
|
||||
private final Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64);
|
||||
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
|
||||
|
||||
|
||||
@Nullable
|
||||
public Object parseExpression(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
|
||||
return getExpression(this.expressionCache, methodKey, expression).getValue(evalContext);
|
||||
}
|
||||
|
||||
|
||||
public EvaluationContext createEvaluationContext(Method method, Object[] args, Class<?> targetClass,
|
||||
BeanFactory beanFactory) {
|
||||
Method targetMethod = getTargetMethod(targetClass, method);
|
||||
LogPrintEvaluationContext evaluationContext = new LogPrintEvaluationContext(
|
||||
null, targetMethod, args, getParameterNameDiscoverer());
|
||||
if (beanFactory != null) {
|
||||
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
|
||||
}
|
||||
return evaluationContext;
|
||||
}
|
||||
|
||||
|
||||
private Method getTargetMethod(Class<?> targetClass, Method method) {
|
||||
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
|
||||
Method targetMethod = this.targetMethodCache.get(methodKey);
|
||||
if (targetMethod == null) {
|
||||
targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
|
||||
this.targetMethodCache.put(methodKey, targetMethod);
|
||||
}
|
||||
return targetMethod;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
|
||||
public class LogPrintValueParser implements BeanFactoryAware {
|
||||
|
||||
protected BeanFactory beanFactory;
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(final BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 打印请求入参,和结果
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface LogRecordPrint {
|
||||
|
||||
|
||||
/**
|
||||
* 自定义的消息
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String content() default "";
|
||||
|
||||
|
||||
/**
|
||||
* 是否发送告警,默认不发送告警
|
||||
*/
|
||||
boolean sendAlarm() default false;
|
||||
|
||||
|
||||
/**
|
||||
* 告警自定义编码code,用于区分告警
|
||||
*/
|
||||
String alarmCode() default "";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.xspaceagi.system.domain.log;
|
||||
|
||||
public enum LogType {
|
||||
NORMAL,
|
||||
MQ;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.xspaceagi.system.domain.log.record;
|
||||
|
||||
import com.xspaceagi.system.domain.log.LogPrint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
|
||||
/**
|
||||
* @author soddy
|
||||
*/
|
||||
@Aspect
|
||||
public class LogPrintAspect {
|
||||
|
||||
|
||||
@Autowired
|
||||
private LogPrintAspectSupport logPrintAspectSupport;
|
||||
|
||||
|
||||
/**
|
||||
* 方法环绕日志
|
||||
*
|
||||
* @param joinPoint
|
||||
* @param logPrint
|
||||
*/
|
||||
@Around("@annotation(logPrint)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, LogPrint logPrint) throws Throwable {
|
||||
return logPrintAspectSupport.printLog(joinPoint, logPrint);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.xspaceagi.system.domain.log.record;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.xspaceagi.system.domain.log.LogConstants;
|
||||
import com.xspaceagi.system.domain.log.LogPrint;
|
||||
import com.xspaceagi.system.domain.log.LogPrintExpressionEvaluator;
|
||||
import com.xspaceagi.system.domain.log.LogPrintValueParser;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.context.expression.AnnotatedElementKey;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
public class LogPrintAspectSupport extends LogPrintValueParser {
|
||||
|
||||
public static final String underline = "_";
|
||||
private final static Logger LOGGER = LoggerFactory.getLogger(LogPrintAspectSupport.class);
|
||||
private final static ObjectMapper OM = new ObjectMapper();
|
||||
private final LogPrintExpressionEvaluator expressionEvaluator = new LogPrintExpressionEvaluator();
|
||||
|
||||
|
||||
public Object printLog(ProceedingJoinPoint joinPoint, LogPrint logPrint) throws Throwable {
|
||||
Class<?> targetClass = getTargetClass(joinPoint.getTarget());
|
||||
Method method = getMethod(joinPoint);
|
||||
Object[] args = getArgs(joinPoint);
|
||||
|
||||
MethodResult methodResult = new MethodResult(true, null, null);
|
||||
|
||||
final Map<String, String> expressionValueMap = new HashMap<>();
|
||||
try {
|
||||
Map<String, String> expressionMap = LogExpression.toMap(logPrint);
|
||||
|
||||
EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(method, args, targetClass,
|
||||
this.beanFactory);
|
||||
|
||||
expressionValueMap.put(LogConstants.STEP, logPrint.step());
|
||||
|
||||
AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(method, targetClass);
|
||||
expressionMap.forEach((name, expression) -> {
|
||||
Object value = null;
|
||||
try {
|
||||
value = expressionEvaluator.parseExpression(expression, annotatedElementKey, evaluationContext);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("parse expression occur exception", e);
|
||||
}
|
||||
if (null == value) {
|
||||
return;
|
||||
}
|
||||
if (value instanceof String) {
|
||||
expressionValueMap.put(name, (String) value);
|
||||
} else {
|
||||
try {
|
||||
expressionValueMap.put(name, OM.writeValueAsString(value));
|
||||
} catch (JsonProcessingException e) {
|
||||
LOGGER.error("parse value occur exception", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
String logPrefix = getLogPrefix(expressionValueMap);
|
||||
|
||||
|
||||
// 增加日志统一前缀
|
||||
MDC.put(LogConstants.PREFIX, logPrefix);
|
||||
|
||||
String content = expressionValueMap.get(LogConstants.CONTENT);
|
||||
if (StringUtils.isNotEmpty(content)) {
|
||||
LOGGER.info("content :{}", content);
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
// 此处异常不影响正常流程
|
||||
LOGGER.error("log record parse exception", e);
|
||||
}
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
} catch (Exception e) {
|
||||
methodResult = new MethodResult(false, e, e.getMessage());
|
||||
} finally {
|
||||
// 删除mdc中的线程变量
|
||||
MDC.remove(LogConstants.PREFIX);
|
||||
}
|
||||
|
||||
if (methodResult.throwable != null) {
|
||||
throw methodResult.throwable;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志前缀
|
||||
*
|
||||
* @param expressionValueMap
|
||||
* @return
|
||||
*/
|
||||
private String getLogPrefix(final Map<String, String> expressionValueMap) {
|
||||
//TODO 其它前缀拼接参数(待定)
|
||||
String localeId = expressionValueMap.get(LogConstants.LOCALE_ID);
|
||||
String billNo = expressionValueMap.get(LogConstants.BILL_NO);
|
||||
String step = expressionValueMap.get(LogConstants.STEP);
|
||||
String topic = expressionValueMap.get(LogConstants.TOPIC);
|
||||
String businessType = expressionValueMap.get(LogConstants.BUSINESS_TYPE);
|
||||
String messageId = expressionValueMap.get(LogConstants.MESSAGE_ID);
|
||||
|
||||
StringBuilder prefixBuilder = new StringBuilder();
|
||||
|
||||
boolean appended = false;
|
||||
if (StringUtils.isNotEmpty(topic)) {
|
||||
prefixBuilder.append("T[").append(topic).append("]");
|
||||
appended = true;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(messageId)) {
|
||||
if (appended) {
|
||||
prefixBuilder.append(underline);
|
||||
}
|
||||
prefixBuilder.append("M[").append(messageId).append("]");
|
||||
appended = true;
|
||||
}
|
||||
|
||||
Long tenantId = RequestContext.get().getTenantId();
|
||||
if (Objects.nonNull(tenantId)) {
|
||||
if (appended) {
|
||||
prefixBuilder.append(underline);
|
||||
}
|
||||
|
||||
prefixBuilder.append("TE[").append(tenantId).append("]");
|
||||
appended = true;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(localeId)) {
|
||||
if (appended) {
|
||||
prefixBuilder.append(underline);
|
||||
}
|
||||
|
||||
prefixBuilder.append("L[").append(localeId).append("]");
|
||||
appended = true;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(billNo)) {
|
||||
if (appended) {
|
||||
prefixBuilder.append(underline);
|
||||
}
|
||||
|
||||
prefixBuilder.append("N[").append(tenantId).append("]");
|
||||
appended = true;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(businessType)) {
|
||||
if (appended) {
|
||||
prefixBuilder.append(underline);
|
||||
}
|
||||
|
||||
prefixBuilder.append("B[").append(businessType).append("]");
|
||||
appended = true;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(step)) {
|
||||
if (appended) {
|
||||
prefixBuilder.append(underline);
|
||||
}
|
||||
prefixBuilder.append("S[").append(step).append("]");
|
||||
}
|
||||
|
||||
return prefixBuilder.toString();
|
||||
}
|
||||
|
||||
|
||||
private Object[] getArgs(final ProceedingJoinPoint joinPoint) {
|
||||
return joinPoint.getArgs();
|
||||
}
|
||||
|
||||
|
||||
private Class<?> getTargetClass(Object target) {
|
||||
return AopProxyUtils.ultimateTargetClass(target);
|
||||
}
|
||||
|
||||
|
||||
private Method getMethod(ProceedingJoinPoint joinPoint) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
if (method.getDeclaringClass().isInterface()) {
|
||||
try {
|
||||
method = joinPoint
|
||||
.getTarget()
|
||||
.getClass()
|
||||
.getDeclaredMethod(joinPoint.getSignature().getName(),
|
||||
method.getParameterTypes());
|
||||
} catch (SecurityException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
|
||||
static class MethodResult {
|
||||
|
||||
private boolean success;
|
||||
private Throwable throwable;
|
||||
private String errorMsg;
|
||||
|
||||
public MethodResult(boolean success, Throwable throwable, String errorMsg) {
|
||||
this.success = success;
|
||||
this.throwable = throwable;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class LogExpression {
|
||||
|
||||
|
||||
private static Map<String, String> toMap(LogPrint logPrint) {
|
||||
Map<String, String> expressionMap = new HashMap<>();
|
||||
|
||||
putIfValid(expressionMap, LogConstants.LOCALE_ID, logPrint.localeId());
|
||||
putIfValid(expressionMap, LogConstants.BILL_NO, logPrint.billNo());
|
||||
putIfValid(expressionMap, LogConstants.TOPIC, logPrint.topic());
|
||||
putIfValid(expressionMap, LogConstants.MESSAGE_ID, logPrint.messageId());
|
||||
putIfValid(expressionMap, LogConstants.BUSINESS_TYPE, logPrint.businessType());
|
||||
putIfValid(expressionMap, LogConstants.CONTENT, logPrint.content());
|
||||
|
||||
return expressionMap;
|
||||
}
|
||||
|
||||
|
||||
private static void putIfValid(Map<String, String> expressionMap, String key, String value) {
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
expressionMap.put(key, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.xspaceagi.system.domain.log.record;
|
||||
|
||||
import com.xspaceagi.system.domain.log.LogRecordPrint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
|
||||
/**
|
||||
* @author soddy
|
||||
*/
|
||||
@Aspect
|
||||
public class LogRecordPrintAspect {
|
||||
|
||||
|
||||
@Autowired
|
||||
private LogRecordPrintAspectSupport logRecordPrintAspectSupport;
|
||||
|
||||
|
||||
@Around("@annotation(logRecordPrint)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, LogRecordPrint logRecordPrint) throws Throwable {
|
||||
return logRecordPrintAspectSupport.printLog(joinPoint, logRecordPrint);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.xspaceagi.system.domain.log.record;
|
||||
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.xspaceagi.system.domain.log.ILogRecordAlarm;
|
||||
import com.xspaceagi.system.domain.log.LogPrintValueParser;
|
||||
import com.xspaceagi.system.domain.log.LogRecordPrint;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LogRecordPrintAspectSupport extends LogPrintValueParser {
|
||||
|
||||
|
||||
@Autowired(required = false)
|
||||
private ILogRecordAlarm logRecordAlarm;
|
||||
|
||||
|
||||
public Object printLog(ProceedingJoinPoint joinPoint, LogRecordPrint logRecordPrint) throws Throwable {
|
||||
Object[] args = getArgs(joinPoint);
|
||||
|
||||
String content = logRecordPrint.content();
|
||||
|
||||
MethodResult methodResult = new MethodResult(true, null, null);
|
||||
// 记录开始时间
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
methodResult = new MethodResult(false, e, e.getMessage());
|
||||
throw e;
|
||||
} finally {
|
||||
// 记录结束时间
|
||||
long endTime = System.currentTimeMillis();
|
||||
long executionTime = endTime - startTime;
|
||||
|
||||
if (methodResult.success) {
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("LogRecordPrint-content={},耗时={} ms,请求={},结果={}",
|
||||
content, executionTime, JSON.toJSONString(args), JSON.toJSONString(result));
|
||||
}
|
||||
} else {
|
||||
if (log.isWarnEnabled()) {
|
||||
log.warn("LogRecordPrint-content={},耗时={} ms,请求={},异常={}",
|
||||
content, executionTime, JSON.toJSONString(args), methodResult.errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
if (methodResult.throwable != null && logRecordPrint.sendAlarm()) {
|
||||
if (Objects.nonNull(logRecordAlarm)) {
|
||||
//判断是否发送告警信息
|
||||
String msg = String.format("请求=%s,错误信息=%s", JSON.toJSONString(args), methodResult.errorMsg);
|
||||
logRecordAlarm.sendAlarm(logRecordPrint.alarmCode(), content, msg);
|
||||
} else {
|
||||
log.info("[logRecordAlarm]bean为空,无法发送告警");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private Object[] getArgs(final ProceedingJoinPoint joinPoint) {
|
||||
return joinPoint.getArgs();
|
||||
}
|
||||
|
||||
|
||||
private Class<?> getTargetClass(Object target) {
|
||||
return AopProxyUtils.ultimateTargetClass(target);
|
||||
}
|
||||
|
||||
|
||||
private Method getMethod(ProceedingJoinPoint joinPoint) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
if (method.getDeclaringClass().isInterface()) {
|
||||
try {
|
||||
method = joinPoint
|
||||
.getTarget()
|
||||
.getClass()
|
||||
.getDeclaredMethod(joinPoint.getSignature().getName(),
|
||||
method.getParameterTypes());
|
||||
} catch (SecurityException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
|
||||
static class MethodResult {
|
||||
|
||||
private boolean success;
|
||||
private Throwable throwable;
|
||||
private String errorMsg;
|
||||
|
||||
public MethodResult(boolean success, Throwable throwable, String errorMsg) {
|
||||
this.success = success;
|
||||
this.throwable = throwable;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.system.domain.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户组菜单绑定领域模型
|
||||
*/
|
||||
@Data
|
||||
public class GroupBindMenuModel implements Serializable {
|
||||
|
||||
/**
|
||||
* 用户组ID
|
||||
*/
|
||||
private Long groupId;
|
||||
|
||||
/**
|
||||
* 菜单绑定资源列表
|
||||
*/
|
||||
private List<MenuNode> menuBindResourceList;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.xspaceagi.system.domain.model;
|
||||
|
||||
import com.xspaceagi.system.spec.enums.BindTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.OpenTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单节点模型
|
||||
*/
|
||||
@Data
|
||||
public class MenuNode implements Serializable {
|
||||
|
||||
/**
|
||||
* 菜单ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 子菜单绑定类型 0:未绑定 1:全部绑定 2:部分绑定
|
||||
* @see BindTypeEnum
|
||||
*/
|
||||
private Integer menuBindType;
|
||||
|
||||
/**
|
||||
* 资源树(包含每个节点的绑定类型)
|
||||
*/
|
||||
private List<ResourceNode> resourceTree;
|
||||
|
||||
/**
|
||||
* 资源列表(包含每个节点的绑定类型)
|
||||
*/
|
||||
private List<ResourceNode> resourceNodes;
|
||||
|
||||
/**
|
||||
* 子菜单列表
|
||||
*/
|
||||
private List<MenuNode> children;
|
||||
|
||||
/**
|
||||
* 菜单编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 菜单名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 来源
|
||||
*/
|
||||
private Integer source;
|
||||
|
||||
/**
|
||||
* 父级ID
|
||||
*/
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 访问路径
|
||||
*/
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* 打开方式
|
||||
* @see OpenTypeEnum
|
||||
*/
|
||||
private Integer openType;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sortIndex;
|
||||
|
||||
/**
|
||||
* 状态 1:启用 0:禁用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
public List<ResourceNode> getFlattenResourceList() {
|
||||
return flattenResourceTree(resourceTree);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 资源树扁平化处理
|
||||
*/
|
||||
public List<ResourceNode> flattenResourceTree(List<ResourceNode> nodes) {
|
||||
if (CollectionUtils.isEmpty(nodes)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<ResourceNode> result = new ArrayList<>();
|
||||
for (ResourceNode node : nodes) {
|
||||
if (node.getId() != null) {
|
||||
// 创建新节点,不包含children
|
||||
ResourceNode flatNode = new ResourceNode();
|
||||
flatNode.setId(node.getId());
|
||||
flatNode.setResourceBindType(node.getResourceBindType());
|
||||
result.add(flatNode);
|
||||
}
|
||||
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
|
||||
result.addAll(flattenResourceTree(node.getChildren()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.xspaceagi.system.domain.model;
|
||||
|
||||
import com.xspaceagi.system.spec.enums.BindTypeEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 资源树节点
|
||||
*/
|
||||
@Data
|
||||
public class ResourceNode implements Serializable {
|
||||
|
||||
/**
|
||||
* 资源ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 子资源绑定类型 0:未绑定 1:全部绑定 2:部分绑定
|
||||
* @see BindTypeEnum
|
||||
*/
|
||||
private Integer resourceBindType;
|
||||
|
||||
/**
|
||||
* 子资源列表
|
||||
*/
|
||||
private List<ResourceNode> children;
|
||||
|
||||
/**
|
||||
* 资源编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 资源名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 来源
|
||||
*/
|
||||
private Integer source;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 父级ID
|
||||
*/
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 访问路径
|
||||
*/
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sortIndex;
|
||||
|
||||
/**
|
||||
* 状态 1:启用 0:禁用
|
||||
*/
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.system.domain.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色菜单绑定领域模型
|
||||
*/
|
||||
@Data
|
||||
public class RoleBindMenuModel implements Serializable {
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 菜单绑定资源列表
|
||||
*/
|
||||
private List<MenuNode> menuBindResourceList;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.xspaceagi.system.domain.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 顺序调整项
|
||||
*/
|
||||
@Data
|
||||
public class SortIndex implements Serializable {
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 父级ID,0表示根节点,null表示不修改(无层级则忽略)
|
||||
*/
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 排序索引,null表示不修改
|
||||
*/
|
||||
private Integer sortIndex;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Domain layer.
|
||||
*/
|
||||
package com.xspaceagi.system.domain;
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.xspaceagi.system.infra.dao.entity.I18nConfig;
|
||||
import com.xspaceagi.system.infra.dao.entity.I18nLang;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public interface I18nDomainService {
|
||||
|
||||
IPage<I18nConfig> queryI18nConfigPage(Long tenantId, String type, String side, String module, String dataId, String lang, String fieldKey, String fieldValue, Long pageNo, Long pageSize);
|
||||
|
||||
List<I18nConfig> queryI18nConfigList(Long tenantId, String type, String side, String module, String dataId, String lang, String fieldKey, String fieldValue);
|
||||
|
||||
long countI18nConfigList(Long tenantId, String type, String side, String module, String dataId, String lang, String fieldKey, String fieldValue);
|
||||
|
||||
I18nConfig queryByKey(String lang, String fieldKey);
|
||||
|
||||
/**
|
||||
* 按租户 + 语言 + 一批 fieldKey 查询配置(用于批量判断目标语言是否已有值)
|
||||
*/
|
||||
List<I18nConfig> listByTenantLangAndFieldKeys(Long tenantId, String lang, Collection<String> fieldKeys);
|
||||
|
||||
void addOrUpdate(I18nConfig i18nConfig);
|
||||
|
||||
void addOrUpdateBatch(List<I18nConfig> i18nConfigs);
|
||||
|
||||
void batchDelete(List<I18nConfig> i18nConfigs);
|
||||
|
||||
void deleteByLang(Long tenantId, String lang);
|
||||
|
||||
void initConfigsForNewLang(Long tenantId, String lang);
|
||||
|
||||
/**
|
||||
* 在所有语言下新增或更新配置
|
||||
*/
|
||||
void addOrUpdateInAllLangs(List<I18nLang> allLangs, I18nConfig i18nConfig);
|
||||
|
||||
/**
|
||||
* 查询所有语言
|
||||
*/
|
||||
List<I18nLang> queryAllLangs();
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import com.xspaceagi.system.infra.dao.entity.I18nLang;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 语言表领域服务
|
||||
*/
|
||||
public interface I18nLangDomainService {
|
||||
|
||||
/**
|
||||
* 新增语言
|
||||
*
|
||||
* @param i18nLang 语言信息
|
||||
*/
|
||||
void add(I18nLang i18nLang);
|
||||
|
||||
/**
|
||||
* 删除语言
|
||||
*
|
||||
* @param id 语言 ID
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
/**
|
||||
* 更新语言(可修改名字、状态、默认状态、排序,不能修改 lang 字段)
|
||||
*
|
||||
* @param i18nLang 语言信息
|
||||
*/
|
||||
void update(I18nLang i18nLang);
|
||||
|
||||
/**
|
||||
* 设置为默认语言(同时将其他语言设为非默认)
|
||||
*
|
||||
* @param id 语言 ID
|
||||
*/
|
||||
void setDefault(Long id);
|
||||
|
||||
/**
|
||||
* 查询全部语言
|
||||
*
|
||||
* @return 语言列表
|
||||
*/
|
||||
List<I18nLang> queryAll();
|
||||
|
||||
/**
|
||||
* 批量更新排序
|
||||
*
|
||||
* @param sortMap key: 语言 ID, value: 排序值
|
||||
*/
|
||||
void updateSort(Map<Long, Integer> sortMap);
|
||||
|
||||
/**
|
||||
* 根据 ID 查询语言
|
||||
*
|
||||
* @param id 语言 ID
|
||||
* @return 语言信息
|
||||
*/
|
||||
I18nLang queryById(Long id);
|
||||
|
||||
/**
|
||||
* 获取默认语言
|
||||
*
|
||||
* @return 默认语言信息
|
||||
*/
|
||||
I18nLang getDefault(Long tenantId);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
public interface ISysOperatorLogDomainService {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
|
||||
import com.xspaceagi.system.infra.dao.entity.NotifyMessage;
|
||||
import com.xspaceagi.system.infra.dao.entity.NotifyMessageUser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface NotifyMessageDomainService {
|
||||
|
||||
void addNotifyMessage(NotifyMessage notifyMessage, List<Long> userIds);
|
||||
|
||||
void updateReadStatus(Long userId, List<Long> notifyIds);
|
||||
|
||||
List<NotifyMessageUser> queryNotifyMessageUserList(Long userId, Long lastId, Integer size, NotifyMessageUser.ReadStatus readStatus);
|
||||
|
||||
//根据notifyIds查询消息列表
|
||||
List<NotifyMessage> queryNotifyMessageList(List<Long> notifyIds);
|
||||
|
||||
//统计用户未读消息数
|
||||
Long countUnreadNotifyMessage(Long userId);
|
||||
|
||||
//更新用户所有未读状态为已读
|
||||
void updateAllUnreadNotifyMessage(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
public interface RetryDomainService {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
|
||||
import com.xspaceagi.system.infra.dao.entity.ScheduleTask;
|
||||
|
||||
public interface ScheduleTaskDomainService {
|
||||
|
||||
Long start(ScheduleTask scheduleTask);
|
||||
|
||||
void update(ScheduleTask scheduleTask);
|
||||
|
||||
void complete(String taskId);
|
||||
|
||||
void cancel(String taskId);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
|
||||
import com.xspaceagi.system.infra.dao.entity.Space;
|
||||
import com.xspaceagi.system.infra.dao.entity.SpaceUser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SpaceDomainService {
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param space
|
||||
*/
|
||||
void add(Space space);
|
||||
|
||||
/**
|
||||
* 向空间添加用户
|
||||
*/
|
||||
void addSpaceUser(SpaceUser spaceUser);
|
||||
|
||||
/**
|
||||
* 删除空间用户
|
||||
*
|
||||
* @param spaceId
|
||||
* @param userId
|
||||
*/
|
||||
void deleteSpaceUser(Long spaceId, Long userId);
|
||||
|
||||
/**
|
||||
* 删除空间
|
||||
*
|
||||
* @param spaceId
|
||||
*/
|
||||
void delete(Long spaceId);
|
||||
|
||||
/**
|
||||
* 更新空间
|
||||
*
|
||||
* @param space
|
||||
*/
|
||||
void update(Space space);
|
||||
|
||||
/**
|
||||
* 更新空间用户
|
||||
*/
|
||||
void updateSpaceUser(SpaceUser spaceUser);
|
||||
|
||||
/**
|
||||
* 根据ID查询空间
|
||||
*
|
||||
* @param spaceId
|
||||
* @return
|
||||
*/
|
||||
Space queryById(Long spaceId);
|
||||
|
||||
/**
|
||||
* 根据ID集合查询空间
|
||||
*
|
||||
* @param spaceIds
|
||||
* @return
|
||||
*/
|
||||
List<Space> queryListByIds(List<Long> spaceIds);
|
||||
|
||||
/**
|
||||
* 查询空间用户列表
|
||||
*/
|
||||
List<SpaceUser> querySpaceUserList(Long spaceId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询空间用户列表
|
||||
*
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
List<SpaceUser> querySpaceUserListByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 查询空间用户
|
||||
*
|
||||
* @param spaceId
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
SpaceUser querySpaceUser(Long spaceId, Long userId);
|
||||
|
||||
/**
|
||||
* 转移空间
|
||||
*
|
||||
* @param spaceId
|
||||
* @param targetUserId
|
||||
*/
|
||||
void transfer(Long spaceId, Long targetUserId);
|
||||
|
||||
/**
|
||||
* 统计工作空间总数
|
||||
*/
|
||||
Long countTotalSpaces();
|
||||
|
||||
Long countUserCreatedTeamSpaces(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import com.xspaceagi.system.infra.dao.entity.SysDataPermission;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.enums.PermissionTargetTypeEnum;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SysDataPermissionDomainService {
|
||||
|
||||
void add(SysDataPermission dataPermission, UserContext userContext);
|
||||
|
||||
void update(Long id, SysDataPermission dataPermission, UserContext userContext);
|
||||
|
||||
SysDataPermission getByTarget(PermissionTargetTypeEnum targetType, Long targetId);
|
||||
|
||||
List<SysDataPermission> getByTargetList(PermissionTargetTypeEnum targetType, List<Long> targetIds);
|
||||
|
||||
void deleteByTaret(PermissionTargetTypeEnum permissionTargetTypeEnum, Long roleId, UserContext userContext);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.domain.model.ResourceNode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SysFlattenService {
|
||||
|
||||
List<MenuNode> flattenMenuTree(List<MenuNode> tree);
|
||||
|
||||
List<ResourceNode> flattenResourceTree(List<ResourceNode> tree);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.xspaceagi.system.domain.model.GroupBindMenuModel;
|
||||
import com.xspaceagi.system.domain.model.SortIndex;
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysDataPermission;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysGroup;
|
||||
import com.xspaceagi.system.infra.dao.entity.User;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
|
||||
/**
|
||||
* 用户组领域服务
|
||||
*/
|
||||
public interface SysGroupDomainService {
|
||||
|
||||
|
||||
/**
|
||||
* 添加用户组
|
||||
*/
|
||||
void addGroup(SysGroup group, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 更新用户组
|
||||
* @return 是否更新了status
|
||||
*/
|
||||
boolean updateGroup(SysGroup group, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 用户组绑定数据权限(全量覆盖)
|
||||
*/
|
||||
void bindDataPermission(Long groupId, SysDataPermission dataPermission, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
void deleteGroup(Long groupId, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 根据ID查询用户组
|
||||
*/
|
||||
SysGroup queryGroupById(Long groupId);
|
||||
|
||||
/**
|
||||
* 根据ID批量查询用户组
|
||||
*/
|
||||
List<SysGroup> queryGroupListByIds(List<Long> groupIds);
|
||||
|
||||
/**
|
||||
* 根据编码查询角色
|
||||
*/
|
||||
SysGroup queryGroupByCode(String groupCode);
|
||||
|
||||
/**
|
||||
* 查询角色列表
|
||||
*/
|
||||
List<SysGroup> queryGroupList(SysGroup group);
|
||||
|
||||
/**
|
||||
* 根据用户组ID查询用户列表
|
||||
*/
|
||||
List<User> getUserListByGroupId(Long groupId);
|
||||
|
||||
/**
|
||||
* 根据用户组ID分页查询用户列表,支持按userName模糊筛选
|
||||
*/
|
||||
IPage<User> getUserPageByGroupId(Long groupId, String userName, long pageNo, long pageSize);
|
||||
|
||||
/**
|
||||
* 根据用户组ID查询用户数量(轻量查询,仅 COUNT)
|
||||
*/
|
||||
long countUsersByGroupId(Long groupId);
|
||||
|
||||
/**
|
||||
* 根据用户组ID查询用户ID列表(轻量查询,仅查关联表,不加载 User 实体)
|
||||
*/
|
||||
List<Long> getUserIdsByGroupId(Long groupId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询角色列表
|
||||
*/
|
||||
List<SysGroup> queryGroupListByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 用户组绑定用户(全量覆盖)
|
||||
*/
|
||||
void groupBindUser(Long groupId, List<Long> userIds, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 用户组添加用户(增量,单条插入)
|
||||
*/
|
||||
void groupAddUser(Long groupId, Long userId, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 用户组移除用户(增量,单条删除)
|
||||
*/
|
||||
void groupRemoveUser(Long groupId, Long userId, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 用户绑定用户组(全量覆盖)
|
||||
*/
|
||||
void userBindGroup(Long userId, List<Long> groupIds, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 绑定菜单权限(全量覆盖)
|
||||
*/
|
||||
void bindMenu(GroupBindMenuModel model, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 查询用户组已绑定的菜单及资源权限(树形结构)
|
||||
*/
|
||||
List<MenuNode> getMenuTreeByGroupId(Long groupId);
|
||||
|
||||
/**
|
||||
* 批量调整用户组顺序
|
||||
*/
|
||||
void batchUpdateGroupSort(List<SortIndex> sortIndexList, UserContext userContext);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.domain.model.SortIndex;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysMenu;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysMenuResource;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单领域服务
|
||||
*/
|
||||
public interface SysMenuDomainService {
|
||||
|
||||
/**
|
||||
* 新增菜单
|
||||
*/
|
||||
void addMenu(SysMenu menu, MenuNode menuNode, Integer source, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 更新菜单
|
||||
*/
|
||||
void updateMenu(SysMenu menu, MenuNode menuNode, Integer source, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
*/
|
||||
void deleteMenu(Long menuId, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 根据ID查询菜单
|
||||
*/
|
||||
SysMenu queryMenuById(Long menuId);
|
||||
|
||||
/**
|
||||
* 根据编码查询菜单
|
||||
*/
|
||||
SysMenu queryMenuByCode(String code);
|
||||
|
||||
/**
|
||||
* 查询菜单列表
|
||||
*/
|
||||
List<SysMenu> queryMenuList(SysMenu menu);
|
||||
|
||||
/**
|
||||
* 按ID批量查询菜单
|
||||
*/
|
||||
List<SysMenu> queryMenuByIds(Collection<Long> ids);
|
||||
|
||||
/**
|
||||
* 菜单绑定资源
|
||||
*/
|
||||
void bindResource(MenuNode model, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 查询菜单资源关联
|
||||
*/
|
||||
List<SysMenuResource> queryResourceListByMenuId(Long menuId);
|
||||
|
||||
/**
|
||||
* 按多个菜单ID批量查询菜单资源关联(用于避免 N+1 查询)
|
||||
*
|
||||
* @param menuIds 菜单ID集合,可为空
|
||||
* @return 所有关联记录,按 menuId 分组由调用方处理
|
||||
*/
|
||||
List<SysMenuResource> queryResourceListByMenuIds(Collection<Long> menuIds);
|
||||
|
||||
/**
|
||||
* 批量调整菜单顺序,支持修改 parentId 和 sortIndex
|
||||
*/
|
||||
boolean batchUpdateMenuSort(List<SortIndex> sortIndexList, UserContext userContext);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import com.xspaceagi.system.domain.model.SortIndex;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysResource;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统资源领域服务
|
||||
*/
|
||||
public interface SysResourceDomainService {
|
||||
|
||||
/**
|
||||
* 添加角色
|
||||
*/
|
||||
void addResource(SysResource resource, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
void updateResource(SysResource resource, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
void deleteResource(Long resourceId, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 根据ID查询角色
|
||||
*/
|
||||
SysResource queryResourceById(Long resourceId);
|
||||
|
||||
/**
|
||||
* 根据编码查询角色
|
||||
*/
|
||||
SysResource queryResourceByCode(String resourceCode);
|
||||
|
||||
/**
|
||||
* 查询角色列表
|
||||
*/
|
||||
List<SysResource> queryResourceList(SysResource resource);
|
||||
|
||||
/**
|
||||
* 按ID批量查询资源
|
||||
*/
|
||||
List<SysResource> queryResourceByIds(Collection<Long> ids);
|
||||
|
||||
/**
|
||||
* 批量调整资源顺序,支持修改 parentId 和 sortIndex
|
||||
*/
|
||||
boolean batchUpdateResourceSort(List<SortIndex> sortIndexList, UserContext userContext);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.domain.model.SortIndex;
|
||||
import com.xspaceagi.system.domain.model.RoleBindMenuModel;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysDataPermission;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysRole;
|
||||
import com.xspaceagi.system.infra.dao.entity.User;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
|
||||
/**
|
||||
* 系统角色领域服务
|
||||
*/
|
||||
public interface SysRoleDomainService {
|
||||
|
||||
/**
|
||||
* 添加角色
|
||||
*/
|
||||
void addRole(SysRole role, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
* @return 是否更新了status
|
||||
*/
|
||||
boolean updateRole(SysRole role, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 角色绑定数据权限(全量覆盖)
|
||||
*/
|
||||
void bindDataPermission(Long roleId, SysDataPermission dataPermission, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
void deleteRole(Long roleId, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 根据ID查询角色
|
||||
*/
|
||||
SysRole queryRoleById(Long roleId);
|
||||
|
||||
/**
|
||||
* 根据ID批量查询角色
|
||||
*/
|
||||
List<SysRole> queryRoleListByIds(List<Long> roleIds);
|
||||
|
||||
/**
|
||||
* 根据编码查询角色
|
||||
*/
|
||||
SysRole queryRoleByCode(String roleCode);
|
||||
|
||||
/**
|
||||
* 查询角色列表
|
||||
*/
|
||||
List<SysRole> queryRoleList(SysRole role);
|
||||
|
||||
/**
|
||||
* 根据角色ID查询用户列表
|
||||
*/
|
||||
List<User> getUserListByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 根据角色ID分页查询用户列表,支持按userName模糊筛选
|
||||
*/
|
||||
IPage<User> getUserPageByRoleId(Long roleId, String userName, long pageNo, long pageSize);
|
||||
|
||||
/**
|
||||
* 根据角色ID查询用户数量(轻量查询,仅 COUNT)
|
||||
*/
|
||||
long countUsersByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 根据角色ID查询用户ID列表(轻量查询,仅查关联表,不加载 User 实体)
|
||||
*/
|
||||
List<Long> getUserIdsByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询角色列表
|
||||
*/
|
||||
List<SysRole> queryRoleListByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 角色绑定用户(全量覆盖)
|
||||
*/
|
||||
void roleBindUser(Long roleId, List<Long> userIds, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 角色添加用户(增量,单条插入)
|
||||
*/
|
||||
void roleAddUser(Long roleId, Long userId, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 角色移除用户(增量,单条删除)
|
||||
*/
|
||||
void roleRemoveUser(Long roleId, Long userId, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 用户绑定角色(全量覆盖)
|
||||
*/
|
||||
void userBindRole(Long userId, List<Long> roleIds, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 绑定菜单权限(全量覆盖)
|
||||
*/
|
||||
void bindMenu(RoleBindMenuModel model, UserContext userContext);
|
||||
|
||||
/**
|
||||
* 查询角色已绑定的菜单及资源权限(树形结构)
|
||||
*/
|
||||
List<MenuNode> getMenuTreeByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 批量调整角色顺序
|
||||
*/
|
||||
void batchUpdateRoleSort(List<SortIndex> sortIndexList, UserContext userContext);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import com.xspaceagi.system.infra.dao.entity.SysSubjectPermission;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.enums.PermissionSubjectTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.PermissionTargetTypeEnum;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface SysSubjectPermissionDomainService {
|
||||
|
||||
/**
|
||||
* 查询指定主体的权限配置(若返回空,表示该主体不限制访问)
|
||||
*/
|
||||
List<SysSubjectPermission> getBySubject(PermissionSubjectTypeEnum subjectType, Long subjectId);
|
||||
|
||||
/**
|
||||
* 查询指定 target(角色/用户组)绑定的主体ID列表
|
||||
*/
|
||||
List<Long> listSubjectIdsByTarget(PermissionTargetTypeEnum targetType, Long targetId, PermissionSubjectTypeEnum subjectType);
|
||||
|
||||
/**
|
||||
* 获取指定 target(角色/用户组)绑定的主体KEY列表
|
||||
*/
|
||||
List<String> listSubjectKeysByTarget(PermissionTargetTypeEnum targetType, Long targetId, PermissionSubjectTypeEnum subjectType);
|
||||
|
||||
/**
|
||||
* 获取指定 target(角色/用户组)绑定的主体KEY和配置
|
||||
*/
|
||||
Map<String, String> listSubjectKeyConfigByTarget(PermissionTargetTypeEnum targetType, Long targetId, PermissionSubjectTypeEnum subjectType);
|
||||
|
||||
/**
|
||||
* 全量覆盖:设置 target(角色/用户组)在某 subjectType 下绑定的 subjectIds
|
||||
* <p>
|
||||
* 传空/空集合表示清空绑定(即该 target 不再拥有任何受限主体的访问权限)
|
||||
* </p>
|
||||
*/
|
||||
void replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum targetType, Long targetId,
|
||||
PermissionSubjectTypeEnum subjectType, List<Long> subjectIds,
|
||||
UserContext userContext);
|
||||
|
||||
/**
|
||||
* 全量覆盖:设置 target(角色/用户组)在某 subjectType 下绑定的 subjectKey 配置
|
||||
*/
|
||||
void replaceSubjectsByTargetAndSubjectKeyConfig(PermissionTargetTypeEnum targetType, Long targetId,
|
||||
PermissionSubjectTypeEnum subjectType, Map<String, String> subjectKeyConfigMap,
|
||||
UserContext userContext);
|
||||
|
||||
/**
|
||||
* 为主体增加一个目标(角色/用户组)访问权限
|
||||
*/
|
||||
void addTarget(PermissionSubjectTypeEnum subjectType, Long subjectId, String subjectKey,
|
||||
PermissionTargetTypeEnum targetType, Long targetId,
|
||||
UserContext userContext);
|
||||
|
||||
/**
|
||||
* 为主体移除一个目标(角色/用户组)访问权限
|
||||
* 若移除后该主体在该 targetType 下无任何目标,则删除配置记录(恢复为不限制访问)
|
||||
*/
|
||||
void removeTarget(PermissionSubjectTypeEnum subjectType, Long subjectId,
|
||||
PermissionTargetTypeEnum targetType, Long targetId,
|
||||
UserContext userContext);
|
||||
|
||||
/**
|
||||
* 全量覆盖:设置主体(subject)的可访问目标(角色/用户组)
|
||||
* <p>
|
||||
* 传空/空集合表示清空绑定(即该主体不限制访问,任何人可访问)
|
||||
* </p>
|
||||
*/
|
||||
void replaceTargetsBySubject(PermissionSubjectTypeEnum subjectType, Long subjectId,
|
||||
List<Long> roleIds, List<Long> groupIds,
|
||||
UserContext userContext);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import com.xspaceagi.system.spec.enums.TenantStatus;
|
||||
import com.xspaceagi.system.infra.dao.entity.Tenant;
|
||||
import com.xspaceagi.system.infra.dao.entity.User;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface UserDomainService {
|
||||
|
||||
void add(User user);
|
||||
|
||||
void update(User user);
|
||||
|
||||
void delete(Long userId);
|
||||
|
||||
User queryById(Long userId);
|
||||
|
||||
User queryByEmail(String email);
|
||||
|
||||
User queryByPhone(String phone);
|
||||
|
||||
User queryByPhoneAndPassword(String phone, String password);
|
||||
|
||||
User queryByUserName(String userName);
|
||||
|
||||
User queryUserByUid(String uid);
|
||||
|
||||
List<User> queryUserListByIds(List<Long> userIds);
|
||||
|
||||
/**
|
||||
* 根据用户UID列表查询用户列表,uid是字符串
|
||||
*
|
||||
* @param uids 用户UID列表,字符串
|
||||
* @return 用户列表
|
||||
*/
|
||||
List<User> queryUserListByUids(List<String> uids);
|
||||
|
||||
List<Long> queryUserIdList(Long lastId, Integer size);
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定状态的租户列表
|
||||
*
|
||||
* @param status 租户状态
|
||||
* @return 指定状态的租户列表
|
||||
*/
|
||||
List<Tenant> queryTenantsByStatus(TenantStatus status);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.xspaceagi.system.domain.service;
|
||||
|
||||
import com.xspaceagi.system.infra.dao.entity.UserMetric;
|
||||
import com.xspaceagi.system.spec.enums.PeriodTypeEnum;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public interface UserMetricDomainService {
|
||||
|
||||
/**
|
||||
* 新增用户计量数据
|
||||
*/
|
||||
void add(UserMetric userMetric);
|
||||
|
||||
/**
|
||||
* 更新用户计量数据
|
||||
*/
|
||||
void update(UserMetric userMetric);
|
||||
|
||||
/**
|
||||
* 根据ID查询用户计量数据
|
||||
*/
|
||||
UserMetric queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询指定用户的所有计量数据
|
||||
*/
|
||||
List<UserMetric> queryByUserId(Long userId, String period);
|
||||
|
||||
/**
|
||||
* 根据唯一键查询用户计量数据(用户ID + 业务类型 + 时段类型 + 时段值)
|
||||
*/
|
||||
UserMetric queryByUniqueKey(Long tenantId, Long userId, String bizType, PeriodTypeEnum periodType, String period);
|
||||
|
||||
|
||||
/**
|
||||
* 增加计量值(原子操作)
|
||||
*
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean incrementValue(Long userId, String bizType, PeriodTypeEnum periodType, String period, BigDecimal delta);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.xspaceagi.system.domain.service.I18nDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.I18nConfig;
|
||||
import com.xspaceagi.system.infra.dao.entity.I18nLang;
|
||||
import com.xspaceagi.system.infra.dao.service.I18nLangService;
|
||||
import com.xspaceagi.system.infra.dao.service.I18nService;
|
||||
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class I18nDomainServiceImpl implements I18nDomainService {
|
||||
|
||||
@Resource
|
||||
private I18nService i18nService;
|
||||
|
||||
@Resource
|
||||
private I18nLangService i18nLangService;
|
||||
|
||||
@Override
|
||||
public IPage<I18nConfig> queryI18nConfigPage(Long tenantId, String type, String side, String module, String dataId, String lang, String fieldKey, String fieldValue, Long pageNo, Long pageSize) {
|
||||
LambdaQueryWrapper<I18nConfig> queryWrapper = buildTenantSystemI18nWrapper(tenantId, type, side, module, dataId, lang, fieldKey, fieldValue);
|
||||
queryWrapper.orderByAsc(I18nConfig::getSide, I18nConfig::getFieldKey);
|
||||
return i18nService.page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<I18nConfig> queryI18nConfigList(Long tenantId, String type, String side, String module, String dataId, String lang, String fieldKey, String fieldValue) {
|
||||
LambdaQueryWrapper<I18nConfig> queryWrapper = buildTenantSystemI18nWrapper(tenantId, type, side, module, dataId, lang, fieldKey, fieldValue);
|
||||
queryWrapper.orderByAsc(I18nConfig::getSide, I18nConfig::getFieldKey);
|
||||
return TenantFunctions.callWithIgnoreCheck(() -> i18nService.list(queryWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countI18nConfigList(Long tenantId, String type, String side, String module, String dataId, String lang, String fieldKey, String fieldValue) {
|
||||
return i18nService.count(buildTenantSystemI18nWrapper(tenantId, type, side, module, dataId, lang, fieldKey, fieldValue));
|
||||
}
|
||||
|
||||
private static LambdaQueryWrapper<I18nConfig> buildTenantSystemI18nWrapper(Long tenantId, String type, String side, String module, String dataId, String lang, String fieldKey, String fieldValue) {
|
||||
LambdaQueryWrapper<I18nConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(I18nConfig::getTenantId, tenantId);
|
||||
queryWrapper.eq(I18nConfig::getType, type);
|
||||
queryWrapper.eq(StringUtils.isNotBlank(side), I18nConfig::getSide, side);
|
||||
queryWrapper.eq(StringUtils.isNotBlank(module), I18nConfig::getModule, module);
|
||||
queryWrapper.eq(dataId != null, I18nConfig::getDataId, dataId);
|
||||
queryWrapper.eq(StringUtils.isNotBlank(lang), I18nConfig::getLang, lang);
|
||||
queryWrapper.like(StringUtils.isNotBlank(fieldKey), I18nConfig::getFieldKey, fieldKey);
|
||||
queryWrapper.like(StringUtils.isNotBlank(fieldValue), I18nConfig::getFieldValue, fieldValue);
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public I18nConfig queryByKey(String lang, String fieldKey) {
|
||||
LambdaQueryWrapper<I18nConfig> wrapper = Wrappers.<I18nConfig>lambdaQuery()
|
||||
.eq(I18nConfig::getLang, lang)
|
||||
.eq(I18nConfig::getFieldKey, fieldKey);
|
||||
return i18nService.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<I18nConfig> listByTenantLangAndFieldKeys(Long tenantId, String lang, Collection<String> fieldKeys) {
|
||||
if (CollectionUtils.isEmpty(fieldKeys)) {
|
||||
return List.of();
|
||||
}
|
||||
LambdaQueryWrapper<I18nConfig> wrapper = Wrappers.<I18nConfig>lambdaQuery()
|
||||
.eq(I18nConfig::getTenantId, tenantId)
|
||||
.eq(I18nConfig::getLang, lang)
|
||||
.in(I18nConfig::getFieldKey, fieldKeys);
|
||||
return i18nService.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addOrUpdate(I18nConfig i18n) {
|
||||
LambdaQueryWrapper<I18nConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(I18nConfig::getType, i18n.getType());
|
||||
queryWrapper.eq(I18nConfig::getSide, i18n.getSide());
|
||||
queryWrapper.eq(I18nConfig::getModule, i18n.getModule());
|
||||
queryWrapper.eq(I18nConfig::getDataId, i18n.getDataId() == null ? "-1" : i18n.getDataId());
|
||||
queryWrapper.eq(I18nConfig::getLang, i18n.getLang());
|
||||
queryWrapper.eq(I18nConfig::getFieldKey, i18n.getFieldKey());
|
||||
I18nConfig i18n0 = i18nService.getOne(queryWrapper);
|
||||
if (i18n0 != null) {
|
||||
i18n.setId(i18n0.getId());
|
||||
i18nService.updateById(i18n);
|
||||
} else {
|
||||
i18nService.save(i18n);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addOrUpdateBatch(List<I18nConfig> i18nConfigs) {
|
||||
if (CollectionUtils.isEmpty(i18nConfigs)) {
|
||||
return;
|
||||
}
|
||||
// 业务约定:同一租户 + 同一 lang + 同一 fieldKey 至多一条,按 (tenantId, lang) 分桶后一次 IN 查询即可
|
||||
Map<String, List<I18nConfig>> byLang = new HashMap<>();
|
||||
for (I18nConfig cfg : i18nConfigs) {
|
||||
byLang.computeIfAbsent(cfg.getLang(), k -> new ArrayList<>()).add(cfg);
|
||||
}
|
||||
|
||||
// 按 lang 多次查询
|
||||
for (List<I18nConfig> group : byLang.values()) {
|
||||
I18nConfig sample = group.get(0);
|
||||
Long tenantId = sample.getTenantId();
|
||||
Set<String> fieldKeys = new LinkedHashSet<>();
|
||||
for (I18nConfig cfg : group) {
|
||||
fieldKeys.add(cfg.getFieldKey());
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<I18nConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(I18nConfig::getLang, sample.getLang());
|
||||
queryWrapper.in(I18nConfig::getFieldKey, fieldKeys);
|
||||
List<I18nConfig> exists = i18nService.list(queryWrapper);
|
||||
Map<String, I18nConfig> existsMap = new HashMap<>();
|
||||
for (I18nConfig exist : exists) {
|
||||
existsMap.putIfAbsent(exist.getFieldKey(), exist);
|
||||
}
|
||||
|
||||
for (I18nConfig cfg : group) {
|
||||
I18nConfig exist = existsMap.get(cfg.getFieldKey());
|
||||
if (exist != null) {
|
||||
cfg.setId(exist.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
i18nService.saveOrUpdateBatch(i18nConfigs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void batchDelete(List<I18nConfig> i18nConfigs) {
|
||||
if (CollectionUtils.isEmpty(i18nConfigs)) {
|
||||
return;
|
||||
}
|
||||
// 按 type + side 分组后使用 field_key IN 删除,不区分语言
|
||||
Map<String, List<I18nConfig>> grouped = new HashMap<>();
|
||||
for (I18nConfig cfg : i18nConfigs) {
|
||||
String groupKey = String.join("||",
|
||||
cfg.getType(),
|
||||
cfg.getSide());
|
||||
grouped.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(cfg);
|
||||
}
|
||||
|
||||
grouped.values().forEach(group -> {
|
||||
I18nConfig sample = group.get(0);
|
||||
Set<String> fieldKeys = new LinkedHashSet<>();
|
||||
for (I18nConfig cfg : group) {
|
||||
fieldKeys.add(cfg.getFieldKey());
|
||||
}
|
||||
if (fieldKeys.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
LambdaQueryWrapper<I18nConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(I18nConfig::getType, sample.getType());
|
||||
queryWrapper.eq(I18nConfig::getSide, sample.getSide());
|
||||
queryWrapper.in(I18nConfig::getFieldKey, fieldKeys);
|
||||
i18nService.remove(queryWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByLang(Long tenantId, String lang) {
|
||||
LambdaQueryWrapper<I18nConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(I18nConfig::getTenantId, tenantId);
|
||||
queryWrapper.eq(I18nConfig::getLang, lang);
|
||||
i18nService.remove(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initConfigsForNewLang(Long tenantId, String lang) {
|
||||
String templateLang = resolveTemplateLang(tenantId, lang);
|
||||
if (StringUtils.isBlank(templateLang)) {
|
||||
return;
|
||||
}
|
||||
LambdaQueryWrapper<I18nConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(I18nConfig::getTenantId, tenantId);
|
||||
queryWrapper.eq(I18nConfig::getLang, templateLang);
|
||||
List<I18nConfig> existing = i18nService.list(queryWrapper);
|
||||
if (CollectionUtils.isEmpty(existing)) {
|
||||
return;
|
||||
}
|
||||
Map<String, I18nConfig> uniqueByKey = new LinkedHashMap<>();
|
||||
for (I18nConfig item : existing) {
|
||||
String uniqueKey = String.join("||",
|
||||
item.getType(),
|
||||
item.getSide(),
|
||||
item.getModule(),
|
||||
item.getDataId() == null ? "-1" : item.getDataId(),
|
||||
item.getFieldKey());
|
||||
uniqueByKey.putIfAbsent(uniqueKey, item);
|
||||
}
|
||||
List<I18nConfig> newLangConfigs = uniqueByKey.values().stream().map(item -> {
|
||||
I18nConfig config = new I18nConfig();
|
||||
config.setTenantId(tenantId);
|
||||
config.setType(item.getType());
|
||||
config.setModule(item.getModule());
|
||||
config.setSide(item.getSide());
|
||||
config.setDataId(item.getDataId() == null ? "-1" : item.getDataId());
|
||||
config.setLang(lang);
|
||||
config.setFieldKey(item.getFieldKey());
|
||||
config.setRemark(item.getRemark());
|
||||
return config;
|
||||
}).toList();
|
||||
addOrUpdateBatch(newLangConfigs);
|
||||
}
|
||||
|
||||
private String resolveTemplateLang(Long tenantId, String newLang) {
|
||||
LambdaQueryWrapper<I18nLang> defaultQuery = new LambdaQueryWrapper<>();
|
||||
defaultQuery.eq(I18nLang::getTenantId, tenantId);
|
||||
defaultQuery.eq(I18nLang::getIsDefault, 1);
|
||||
defaultQuery.eq(I18nLang::getStatus, 1);
|
||||
defaultQuery.last("limit 1");
|
||||
I18nLang defaultLang = i18nLangService.getOne(defaultQuery);
|
||||
if (defaultLang != null && StringUtils.isNotBlank(defaultLang.getLang())
|
||||
&& !defaultLang.getLang().equalsIgnoreCase(newLang)) {
|
||||
return defaultLang.getLang();
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<I18nConfig> fallbackQuery = new LambdaQueryWrapper<>();
|
||||
fallbackQuery.eq(I18nConfig::getTenantId, tenantId);
|
||||
fallbackQuery.ne(I18nConfig::getLang, newLang);
|
||||
fallbackQuery.select(I18nConfig::getLang);
|
||||
fallbackQuery.last("limit 1");
|
||||
I18nConfig fallbackConfig = i18nService.getOne(fallbackQuery);
|
||||
return fallbackConfig == null ? null : fallbackConfig.getLang();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addOrUpdateInAllLangs(List<I18nLang> allLangs, I18nConfig i18nConfig) {
|
||||
// 在所有语言下创建或更新配置
|
||||
for (I18nLang lang : allLangs) {
|
||||
I18nConfig config = new I18nConfig();
|
||||
config.setType(i18nConfig.getType());
|
||||
config.setModule(i18nConfig.getModule());
|
||||
config.setDataId(i18nConfig.getDataId());
|
||||
config.setLang(lang.getLang());
|
||||
config.setFieldKey(i18nConfig.getFieldKey());
|
||||
config.setSide(i18nConfig.getSide());
|
||||
if (lang.getLang().equals(i18nConfig.getLang())) {
|
||||
config.setFieldValue(i18nConfig.getFieldValue());
|
||||
config.setRemark(i18nConfig.getRemark());
|
||||
}
|
||||
addOrUpdate(config);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<I18nLang> queryAllLangs() {
|
||||
return i18nLangService.list();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.xspaceagi.system.domain.service.I18nLangDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.I18nLang;
|
||||
import com.xspaceagi.system.infra.dao.service.I18nLangService;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 语言表领域服务实现
|
||||
*/
|
||||
@Service
|
||||
public class I18nLangDomainServiceImpl implements I18nLangDomainService {
|
||||
|
||||
@Resource
|
||||
private I18nLangService i18nLangService;
|
||||
|
||||
@Override
|
||||
public void add(I18nLang i18nLang) {
|
||||
Assert.notNull(i18nLang, "The parameter cannot be left blank.");
|
||||
Assert.notNull(i18nLang.getName(), "Parameter 'name' cannot be left blank.");
|
||||
Assert.notNull(i18nLang.getLang(), "Parameter 'lang' cannot be left blank.");
|
||||
|
||||
// 检查语言标识是否已存在
|
||||
LambdaQueryWrapper<I18nLang> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(I18nLang::getLang, i18nLang.getLang());
|
||||
I18nLang existing = i18nLangService.getOne(queryWrapper);
|
||||
Assert.isNull(existing, "语言标识已存在:" + i18nLang.getLang());
|
||||
|
||||
// 如果是默认语言,先将其他语言设为非默认
|
||||
if (i18nLang.getIsDefault() != null && i18nLang.getIsDefault() == 1) {
|
||||
setAllNonDefault();
|
||||
}
|
||||
|
||||
i18nLangService.save(i18nLang);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long id) {
|
||||
Assert.notNull(id, "Parameter 'id' cannot be left blank.");
|
||||
i18nLangService.removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(I18nLang i18nLang) {
|
||||
Assert.notNull(i18nLang, "The parameter cannot be left blank.");
|
||||
Assert.notNull(i18nLang.getId(), "Parameter 'id' cannot be left blank.");
|
||||
|
||||
// 查询原有记录
|
||||
I18nLang existing = i18nLangService.getById(i18nLang.getId());
|
||||
if (existing == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemI18nOriginalRecordNotFound);
|
||||
}
|
||||
|
||||
// 不允许修改 lang 字段,保留原有值
|
||||
i18nLang.setLang(existing.getLang());
|
||||
|
||||
// 如果设置为默认语言,先将其他语言设为非默认
|
||||
if (i18nLang.getIsDefault() != null && i18nLang.getIsDefault() == 1) {
|
||||
setAllNonDefault();
|
||||
}
|
||||
|
||||
i18nLangService.updateById(i18nLang);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void setDefault(Long id) {
|
||||
Assert.notNull(id, "Parameter 'id' cannot be left blank.");
|
||||
|
||||
// 先将所有语言设为非默认
|
||||
setAllNonDefault();
|
||||
|
||||
// 再将指定语言设为默认
|
||||
I18nLang i18nLang = new I18nLang();
|
||||
i18nLang.setId(id);
|
||||
i18nLang.setIsDefault(1);
|
||||
i18nLang.setStatus(1);
|
||||
i18nLangService.updateById(i18nLang);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<I18nLang> queryAll() {
|
||||
LambdaQueryWrapper<I18nLang> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.orderByAsc(I18nLang::getSort, I18nLang::getId);
|
||||
return i18nLangService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateSort(Map<Long, Integer> sortMap) {
|
||||
Assert.notNull(sortMap, "Parameter 'sortMap' cannot be left blank.");
|
||||
|
||||
List<I18nLang> updateList = new ArrayList<>();
|
||||
for (Map.Entry<Long, Integer> entry : sortMap.entrySet()) {
|
||||
I18nLang lang = new I18nLang();
|
||||
lang.setId(entry.getKey());
|
||||
lang.setSort(entry.getValue());
|
||||
updateList.add(lang);
|
||||
}
|
||||
|
||||
i18nLangService.updateBatchById(updateList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public I18nLang queryById(Long id) {
|
||||
Assert.notNull(id, "Parameter 'id' cannot be left blank.");
|
||||
return i18nLangService.getById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public I18nLang getDefault(Long tenantId) {
|
||||
LambdaQueryWrapper<I18nLang> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(I18nLang::getTenantId, tenantId);
|
||||
queryWrapper.eq(I18nLang::getIsDefault, 1);
|
||||
queryWrapper.eq(I18nLang::getStatus, 1);
|
||||
queryWrapper.orderByAsc(I18nLang::getSort);
|
||||
queryWrapper.last("LIMIT 1");
|
||||
return TenantFunctions.callWithIgnoreCheck(() -> i18nLangService.getOne(queryWrapper));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所有语言设为非默认
|
||||
*/
|
||||
private void setAllNonDefault() {
|
||||
List<I18nLang> allLangs = i18nLangService.list();
|
||||
if (allLangs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<I18nLang> updateList = new ArrayList<>();
|
||||
for (I18nLang lang : allLangs) {
|
||||
if (lang.getIsDefault() != null && lang.getIsDefault() == 1) {
|
||||
I18nLang updateLang = new I18nLang();
|
||||
updateLang.setId(lang.getId());
|
||||
updateLang.setIsDefault(0);
|
||||
updateList.add(updateLang);
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateList.isEmpty()) {
|
||||
i18nLangService.updateBatchById(updateList);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.domain.model.ResourceNode;
|
||||
import com.xspaceagi.system.domain.service.SysMenuDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysMenu;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysMenuResource;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysResource;
|
||||
import com.xspaceagi.system.spec.enums.BindTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.ResourceTypeEnum;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
|
||||
|
||||
/**
|
||||
* 菜单 + 资源权限计算通用逻辑(角色 / 用户组共用)
|
||||
*/
|
||||
public class MenuAuthHelper {
|
||||
|
||||
private static Logger log = LoggerFactory.getLogger(MenuAuthHelper.class);
|
||||
|
||||
/**
|
||||
* 构建带资源权限的菜单列表
|
||||
*
|
||||
* @param bindings 角色或用户组的菜单绑定列表(如 SysRoleMenu / SysGroupMenu)
|
||||
* @param menuIdGetter 从绑定对象获取 menuId 的函数
|
||||
* @param menuBindTypeGetter 从绑定对象获取 menuBindType 的函数
|
||||
* @param resourceTreeJsonGetter 从绑定对象获取 resourceTreeJson 的函数
|
||||
* @param allMenus 全量有效菜单
|
||||
* @param allResources 全量有效资源
|
||||
* @param sysMenuDomainService 菜单领域服务(用于查询 sys_menu_resource)
|
||||
*/
|
||||
public static <B> List<MenuNode> buildMenuListWithAuth(List<B> bindings,
|
||||
Function<B, Long> menuIdGetter,
|
||||
Function<B, Integer> menuBindTypeGetter,
|
||||
Function<B, String> resourceTreeJsonGetter,
|
||||
List<SysMenu> allMenus,
|
||||
List<SysResource> allResources,
|
||||
SysMenuDomainService sysMenuDomainService) {
|
||||
if (CollectionUtils.isEmpty(allMenus)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 绑定关系:menuId -> 绑定记录
|
||||
Map<Long, B> bindingMap = CollectionUtils.isEmpty(bindings)
|
||||
? Collections.emptyMap()
|
||||
: bindings.stream().collect(Collectors.toMap(
|
||||
menuIdGetter,
|
||||
b -> b,
|
||||
(a, b) -> a
|
||||
));
|
||||
|
||||
// 菜单ID -> 菜单实体
|
||||
Map<Long, SysMenu> allMenuMap = allMenus.stream().collect(Collectors.toMap(SysMenu::getId, m -> m));
|
||||
|
||||
// 1. 先根据绑定记录,得到每个菜单的“初始” menuBindType(无记录视为 NONE)
|
||||
Map<Long, Integer> menuBindTypeMap = new HashMap<>();
|
||||
for (SysMenu menu : allMenus) {
|
||||
B binding = bindingMap.get(menu.getId());
|
||||
Integer menuBindType = (binding != null && menuBindTypeGetter.apply(binding) != null)
|
||||
? menuBindTypeGetter.apply(binding)
|
||||
: BindTypeEnum.NONE.getCode();
|
||||
menuBindTypeMap.put(menu.getId(), menuBindType);
|
||||
}
|
||||
|
||||
// 2. 构建菜单树 childrenMap,按父子关系自上而下传播父菜单的 ALL
|
||||
// parentId=0 表示父节点为 root,需加入 childrenMap.get(0),否则 root 的子菜单无法被传播
|
||||
Map<Long, List<SysMenu>> childrenMap = new HashMap<>();
|
||||
for (SysMenu menu : allMenus) {
|
||||
Long parentId = menu.getParentId();
|
||||
if (parentId == null) {
|
||||
continue;
|
||||
}
|
||||
childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(menu);
|
||||
}
|
||||
|
||||
// 从所有根菜单开始 DFS,将父菜单为 ALL 的权限向下传递给所有子菜单
|
||||
for (SysMenu menu : allMenus) {
|
||||
Long parentId = menu.getParentId();
|
||||
if (parentId == null || parentId == 0L || !allMenuMap.containsKey(parentId)) {
|
||||
propagateMenuBindType(menu, menuBindTypeMap, childrenMap);
|
||||
}
|
||||
}
|
||||
|
||||
// 一次性批量查询所有菜单的菜单-资源关联,避免 N+1
|
||||
List<Long> allMenuIds = allMenus.stream()
|
||||
.map(SysMenu::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
List<SysMenuResource> allMenuResources = sysMenuDomainService.queryResourceListByMenuIds(allMenuIds);
|
||||
Map<Long, List<SysMenuResource>> menuResourcesByMenuId = CollectionUtils.isEmpty(allMenuResources)
|
||||
? Collections.emptyMap()
|
||||
: allMenuResources.stream().collect(Collectors.groupingBy(SysMenuResource::getMenuId));
|
||||
|
||||
// 3. 构建最终返回的 MenuNode 列表
|
||||
List<MenuNode> result = new ArrayList<>();
|
||||
for (SysMenu menu : allMenus) {
|
||||
MenuNode model = new MenuNode();
|
||||
model.setId(menu.getId());
|
||||
|
||||
B binding = bindingMap.get(menu.getId());
|
||||
// 使用自上而下传播后的“有效菜单绑定类型”
|
||||
Integer menuBindType = menuBindTypeMap.getOrDefault(menu.getId(), BindTypeEnum.NONE.getCode());
|
||||
model.setMenuBindType(menuBindType);
|
||||
|
||||
// 构建资源树(从预加载的 menuResourcesByMenuId 取,不按菜单逐条查库)
|
||||
// allResources 为空时仍设置 resourceTree 为 [],避免语义不清的 null
|
||||
List<SysMenuResource> menuResources = menuResourcesByMenuId.getOrDefault(menu.getId(), Collections.emptyList());
|
||||
List<ResourceNode> resourceTree;
|
||||
if (!CollectionUtils.isEmpty(allResources)) {
|
||||
if (binding == null) {
|
||||
// 这种情况是角色/用户组没有直接绑定菜单,通过传播,此菜单有可能被绑定,也可能未绑定
|
||||
Integer defaultResourceBindType = (menuBindType != null && !BindTypeEnum.NONE.getCode().equals(menuBindType))
|
||||
? BindTypeEnum.ALL.getCode()
|
||||
: BindTypeEnum.NONE.getCode();
|
||||
resourceTree = buildResourceTreeForUnboundMenu(
|
||||
menuResources,
|
||||
allResources,
|
||||
defaultResourceBindType
|
||||
);
|
||||
} else {
|
||||
// 这种情况是角色/用户组直接绑定了菜单
|
||||
resourceTree = buildResourceTreeForBinding(
|
||||
allResources,
|
||||
binding,
|
||||
menuIdGetter,
|
||||
resourceTreeJsonGetter,
|
||||
menuResources
|
||||
);
|
||||
}
|
||||
} else {
|
||||
resourceTree = new ArrayList<>();
|
||||
}
|
||||
model.setResourceTree(resourceTree);
|
||||
|
||||
result.add(model);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单绑定类型优先级:
|
||||
* NONE < ALL < PART
|
||||
* - NONE 可以被 ALL / PART 覆盖
|
||||
* - ALL 可以被 PART 覆盖
|
||||
* - PART 不会被 ALL / NONE 覆盖
|
||||
*/
|
||||
private static int menuBindPriority(Integer type) {
|
||||
if (type == null) {
|
||||
return 0;
|
||||
}
|
||||
if (BindTypeEnum.NONE.getCode().equals(type)) {
|
||||
return 1;
|
||||
}
|
||||
if (BindTypeEnum.ALL.getCode().equals(type)) {
|
||||
return 2;
|
||||
}
|
||||
if (BindTypeEnum.PART.getCode().equals(type)) {
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Integer mergeMenuBindType(Integer oldType, Integer newType) {
|
||||
if (newType == null) {
|
||||
return oldType;
|
||||
}
|
||||
if (oldType == null) {
|
||||
return newType;
|
||||
}
|
||||
// 只有当新类型优先级更高时才覆盖旧类型
|
||||
return menuBindPriority(newType) > menuBindPriority(oldType) ? newType : oldType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为未绑定角色/用户组的菜单构建资源树
|
||||
* 只返回菜单绑定的资源范围(由调用方传入已查询的 menuResources),所有资源统一标记为 defaultBindType
|
||||
*/
|
||||
private static List<ResourceNode> buildResourceTreeForUnboundMenu(List<SysMenuResource> menuResources,
|
||||
List<SysResource> allResources,
|
||||
Integer defaultBindType) {
|
||||
if (CollectionUtils.isEmpty(allResources)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
if (CollectionUtils.isEmpty(menuResources)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 构建资源子节点索引
|
||||
Map<Long, List<SysResource>> resourceChildrenMap = new HashMap<>();
|
||||
for (SysResource resource : allResources) {
|
||||
Long parentId = resource.getParentId();
|
||||
if (parentId != null && parentId != 0L) {
|
||||
resourceChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(resource);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单有权限的资源ID集合(包括ALL类型的子资源)
|
||||
Set<Long> menuAuthorizedResourceIds = new HashSet<>();
|
||||
for (SysMenuResource mr : menuResources) {
|
||||
Long resId = mr.getResourceId();
|
||||
if (resId == null) {
|
||||
continue;
|
||||
}
|
||||
menuAuthorizedResourceIds.add(resId);
|
||||
|
||||
// 如果菜单绑定的资源是ALL类型,展开其所有子资源
|
||||
if (BindTypeEnum.ALL.getCode().equals(mr.getResourceBindType())) {
|
||||
collectChildrenResourceIds(resId, resourceChildrenMap, menuAuthorizedResourceIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选出菜单有权限的资源
|
||||
List<SysResource> menuAuthorizedResources = allResources.stream()
|
||||
.filter(resource -> menuAuthorizedResourceIds.contains(resource.getId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (CollectionUtils.isEmpty(menuAuthorizedResources)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 所有资源统一标记为 defaultBindType(例如:NONE 或 ALL)
|
||||
Map<Long, Integer> resourceBindTypeMap = new HashMap<>();
|
||||
for (SysResource resource : menuAuthorizedResources) {
|
||||
resourceBindTypeMap.put(resource.getId(), defaultBindType);
|
||||
}
|
||||
|
||||
// 构建资源树 - 不裁剪为末级模块/叶子组件结构,返回的是完整的菜单的资源树
|
||||
// return buildResourceTree(menuAuthorizedResources, resourceBindTypeMap, defaultBindType);
|
||||
|
||||
// 构建资源树并裁剪为末级模块/叶子组件结构
|
||||
List<ResourceNode> tree = buildResourceTree(menuAuthorizedResources, resourceBindTypeMap, defaultBindType);
|
||||
return pruneResourceTreeToLeafModules(tree, allResources);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据绑定记录(角色 / 用户组)构建对应菜单的资源树
|
||||
* 返回菜单原本绑定的所有资源(由调用方传入已查询的 menuResources),对 resourceBindType 打标。
|
||||
*/
|
||||
private static <B> List<ResourceNode> buildResourceTreeForBinding(List<SysResource> allResources,
|
||||
B binding,
|
||||
Function<B, Long> menuIdGetter,
|
||||
Function<B, String> resourceTreeJsonGetter,
|
||||
List<SysMenuResource> menuResources) {
|
||||
if (binding == null || CollectionUtils.isEmpty(allResources)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
if (CollectionUtils.isEmpty(menuResources)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 构建资源子节点索引(resourceId -> children),用于展开 ALL 的子资源
|
||||
Map<Long, List<SysResource>> resourceChildrenMap = new HashMap<>();
|
||||
for (SysResource resource : allResources) {
|
||||
Long parentId = resource.getParentId();
|
||||
if (parentId == null || parentId == 0L) {
|
||||
continue;
|
||||
}
|
||||
resourceChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(resource);
|
||||
}
|
||||
|
||||
// 构建菜单“最终有权限”的资源ID集合:
|
||||
// - 所有在 sys_menu_resource 中出现的 resourceId
|
||||
// - 对于其中 resourceBindType=ALL 的资源,展开其所有子资源,一并加入
|
||||
Set<Long> menuAuthorizedResourceIds = new HashSet<>();
|
||||
for (SysMenuResource mr : menuResources) {
|
||||
Long resId = mr.getResourceId();
|
||||
if (resId == null) {
|
||||
continue;
|
||||
}
|
||||
menuAuthorizedResourceIds.add(resId);
|
||||
|
||||
// 如果菜单侧对该资源的 resourceBindType=ALL,则其所有子资源都算作菜单有权限
|
||||
if (BindTypeEnum.ALL.getCode().equals(mr.getResourceBindType())) {
|
||||
collectChildrenResourceIds(resId, resourceChildrenMap, menuAuthorizedResourceIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 从所有资源中筛选出菜单有权限的资源(只在这些资源范围内构建资源树)
|
||||
List<SysResource> menuAuthorizedResources = allResources.stream()
|
||||
.filter(resource -> menuAuthorizedResourceIds.contains(resource.getId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (CollectionUtils.isEmpty(menuAuthorizedResources)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 从绑定实体存储的 JSON 中反序列化出原始的资源节点树
|
||||
List<ResourceNode> storedTree = null;
|
||||
String json = resourceTreeJsonGetter.apply(binding);
|
||||
if (StringUtils.isNotBlank(json)) {
|
||||
try {
|
||||
storedTree = JsonSerializeUtil.parseObject(json, new TypeReference<List<ResourceNode>>() {});
|
||||
} catch (Exception e) {
|
||||
log.error("反序列化 resourceTreeJson 失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 将反序列化出来的资源节点树扁平化为 resourceId -> 绑定实体的资源绑定类型 的映射
|
||||
Map<Long, Integer> ownerResourceBindTypeMap = storedTree == null
|
||||
? new HashMap<>()
|
||||
: buildBindTypeMapFromResourceTree(storedTree);
|
||||
|
||||
// 根据 resourceTreeJson 中的记录来确定每个资源的最终绑定类型
|
||||
// 如果 resourceTreeJson 为空,则所有资源都是 NONE
|
||||
Map<Long, Integer> finalResourceBindTypeMap = new HashMap<>();
|
||||
|
||||
for (SysResource resource : menuAuthorizedResources) {
|
||||
Long resourceId = resource.getId();
|
||||
Integer bindType = ownerResourceBindTypeMap.get(resourceId);
|
||||
if (bindType != null) {
|
||||
// 在 resourceTreeJson 中有记录,使用记录中的绑定类型
|
||||
finalResourceBindTypeMap.put(resourceId, bindType);
|
||||
} else {
|
||||
// 菜单有权限但绑定实体没有授权:返回 NONE
|
||||
finalResourceBindTypeMap.put(resourceId, BindTypeEnum.NONE.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
// 传播资源绑定类型:ALL的子节点也是ALL,NONE的子节点也是NONE
|
||||
// 必须按「父节点优先」顺序迭代,确保父节点先于子节点被处理,这样父节点的传播才能正确覆盖子节点
|
||||
Set<Long> authorizedIds = new HashSet<>(menuAuthorizedResourceIds);
|
||||
List<SysResource> sortedByDepth = sortResourcesByDepth(menuAuthorizedResources, authorizedIds);
|
||||
Set<Long> processedResourceIds = new HashSet<>();
|
||||
for (SysResource resource : sortedByDepth) {
|
||||
Long resourceId = resource.getId();
|
||||
Integer bindType = finalResourceBindTypeMap.get(resourceId);
|
||||
if (bindType != null && !processedResourceIds.contains(resourceId)) {
|
||||
if (BindTypeEnum.ALL.getCode().equals(bindType) || BindTypeEnum.NONE.getCode().equals(bindType)) {
|
||||
propagateResourceBindType(resourceId, resourceChildrenMap, finalResourceBindTypeMap,
|
||||
processedResourceIds, bindType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回菜单绑定的所有资源,不过滤 NONE;无权限的打标为 NONE,有权限的按实际 resourceBindType
|
||||
|
||||
// 构建资源树 - 不裁剪为末级模块/叶子组件结构,返回的是完整的菜单的资源树
|
||||
// return buildResourceTree(menuAuthorizedResources, finalResourceBindTypeMap, BindTypeEnum.NONE.getCode());
|
||||
|
||||
List<ResourceNode> tree = buildResourceTree(menuAuthorizedResources, finalResourceBindTypeMap,
|
||||
BindTypeEnum.NONE.getCode());
|
||||
return pruneResourceTreeToLeafModules(tree, allResources);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将资源树扁平化,构建 resourceId -> resourceBindType 的映射
|
||||
*/
|
||||
private static Map<Long, Integer> buildBindTypeMapFromResourceTree(List<ResourceNode> nodes) {
|
||||
Map<Long, Integer> map = new HashMap<>();
|
||||
if (CollectionUtils.isEmpty(nodes)) {
|
||||
return map;
|
||||
}
|
||||
for (ResourceNode node : nodes) {
|
||||
if (node.getId() != null && node.getResourceBindType() != null) {
|
||||
map.put(node.getId(), node.getResourceBindType());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(node.getChildren())) {
|
||||
map.putAll(buildBindTypeMapFromResourceTree(node.getChildren()));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按规则裁剪资源树,规则见 pruneByLevel 注释
|
||||
*/
|
||||
private static List<ResourceNode> pruneResourceTreeToLeafModules(List<ResourceNode> resourceTree,
|
||||
List<SysResource> allResources) {
|
||||
if (CollectionUtils.isEmpty(resourceTree) || CollectionUtils.isEmpty(allResources)) {
|
||||
return resourceTree;
|
||||
}
|
||||
return pruneByLevel(resourceTree);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按层级递归裁剪,规则:
|
||||
* 1、多个同级模块/组件:返回所有节点本身及子节点,树形
|
||||
* 2、单个模块:无子节点→返回模块本身;有子节点且无同级→只返回子节点,递归处理子节点
|
||||
* (1)子节点有组件:返回该级所有节点,模块按树形
|
||||
* (2)子节点只有模块:递归(多同级 or 单同级)
|
||||
* 3、只有组件:返回组件
|
||||
*/
|
||||
private static List<ResourceNode> pruneByLevel(List<ResourceNode> nodes) {
|
||||
if (CollectionUtils.isEmpty(nodes)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
boolean allOperation = nodes.stream()
|
||||
.filter(n -> n != null && n.getId() != null)
|
||||
.allMatch(n -> ResourceTypeEnum.OPERATION.getCode().equals(n.getType()));
|
||||
|
||||
if (allOperation) {
|
||||
return nodes.stream()
|
||||
.filter(n -> n != null && n.getId() != null)
|
||||
.map(MenuAuthHelper::copyNodeWithoutChildren)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (nodes.size() > 1) {
|
||||
return nodes.stream()
|
||||
.filter(n -> n != null && n.getId() != null)
|
||||
.map(MenuAuthHelper::copyNodeWithSubtree)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
ResourceNode node = nodes.get(0);
|
||||
if (node == null || node.getId() == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
if (ResourceTypeEnum.OPERATION.getCode().equals(node.getType())) {
|
||||
return Collections.singletonList(copyNodeWithoutChildren(node));
|
||||
}
|
||||
|
||||
List<ResourceNode> children = node.getChildren();
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return Collections.singletonList(copyNodeWithoutChildren(node));
|
||||
}
|
||||
|
||||
return pruneByLevel(children);
|
||||
}
|
||||
|
||||
private static ResourceNode copyNodeWithoutChildren(ResourceNode node) {
|
||||
ResourceNode copy = new ResourceNode();
|
||||
copy.setId(node.getId());
|
||||
copy.setParentId(node.getParentId());
|
||||
copy.setResourceBindType(node.getResourceBindType());
|
||||
copy.setType(node.getType());
|
||||
copy.setCode(node.getCode());
|
||||
copy.setName(node.getName());
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝节点及其子树,子模块按树形保留
|
||||
*/
|
||||
private static ResourceNode copyNodeWithSubtree(ResourceNode node) {
|
||||
ResourceNode copy = copyNodeWithoutChildren(node);
|
||||
List<ResourceNode> children = node.getChildren();
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return copy;
|
||||
}
|
||||
List<ResourceNode> copiedChildren = new ArrayList<>();
|
||||
for (ResourceNode child : children) {
|
||||
if (child == null || child.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (ResourceTypeEnum.OPERATION.getCode().equals(child.getType())) {
|
||||
copiedChildren.add(copyNodeWithoutChildren(child));
|
||||
} else if (ResourceTypeEnum.MODULE.getCode().equals(child.getType())) {
|
||||
copiedChildren.add(copyNodeWithSubtree(child));
|
||||
}
|
||||
}
|
||||
if (!copiedChildren.isEmpty()) {
|
||||
copy.setChildren(copiedChildren);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据完整资源列表和绑定关系,构建资源树(仅使用资源ID和绑定类型)
|
||||
*
|
||||
* @param allResources 所有资源
|
||||
* @param resourceBindTypeMap 资源ID到绑定类型的映射,可为空
|
||||
* @param defaultBindType 未出现在映射中的资源默认绑定类型
|
||||
*/
|
||||
private static List<ResourceNode> buildResourceTree(List<SysResource> allResources,
|
||||
Map<Long, Integer> resourceBindTypeMap,
|
||||
Integer defaultBindType) {
|
||||
if (CollectionUtils.isEmpty(allResources)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 先为每个资源构建对应的节点
|
||||
Map<Long, ResourceNode> nodeMap = new HashMap<>();
|
||||
for (SysResource resource : allResources) {
|
||||
ResourceNode node = new ResourceNode();
|
||||
node.setId(resource.getId());
|
||||
node.setType(resource.getType());
|
||||
node.setParentId(resource.getParentId());
|
||||
Integer bt = resourceBindTypeMap != null ? resourceBindTypeMap.get(resource.getId()) : null;
|
||||
node.setResourceBindType(bt != null ? bt : defaultBindType);
|
||||
nodeMap.put(resource.getId(), node);
|
||||
}
|
||||
|
||||
// 再根据 parentId 组装树结构
|
||||
List<ResourceNode> roots = new ArrayList<>();
|
||||
for (SysResource resource : allResources) {
|
||||
ResourceNode current = nodeMap.get(resource.getId());
|
||||
Long parentId = resource.getParentId();
|
||||
if (parentId == null || parentId == 0L || !nodeMap.containsKey(parentId)) {
|
||||
roots.add(current);
|
||||
} else {
|
||||
ResourceNode parent = nodeMap.get(parentId);
|
||||
if (parent.getChildren() == null) {
|
||||
parent.setChildren(new ArrayList<>());
|
||||
}
|
||||
parent.getChildren().add(current);
|
||||
}
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集某个资源节点下的所有子资源ID
|
||||
*/
|
||||
private static void collectChildrenResourceIds(Long resourceId,
|
||||
Map<Long, List<SysResource>> resourceChildrenMap,
|
||||
Set<Long> idSet) {
|
||||
List<SysResource> children = resourceChildrenMap.get(resourceId);
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
}
|
||||
for (SysResource child : children) {
|
||||
if (child.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (idSet.add(child.getId())) {
|
||||
collectChildrenResourceIds(child.getId(), resourceChildrenMap, idSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按资源树深度排序,确保父节点在子节点之前(用于传播时父节点先处理)
|
||||
*/
|
||||
private static List<SysResource> sortResourcesByDepth(List<SysResource> resources, Set<Long> authorizedIds) {
|
||||
if (CollectionUtils.isEmpty(resources)) {
|
||||
return resources;
|
||||
}
|
||||
// 使用手动构建 Map,因为 Collectors.toMap 不允许 null 值,而 parentId 可能为 null(根节点)
|
||||
Map<Long, Long> parentIdMap = new HashMap<>();
|
||||
for (SysResource r : resources) {
|
||||
if (r.getId() != null) {
|
||||
parentIdMap.put(r.getId(), r.getParentId());
|
||||
}
|
||||
}
|
||||
Map<Long, Integer> depthMap = new HashMap<>();
|
||||
for (SysResource r : resources) {
|
||||
if (r.getId() != null) {
|
||||
computeDepth(r.getId(), parentIdMap, authorizedIds, depthMap);
|
||||
}
|
||||
}
|
||||
return resources.stream()
|
||||
.sorted((a, b) -> Integer.compare(
|
||||
depthMap.getOrDefault(a.getId(), 0),
|
||||
depthMap.getOrDefault(b.getId(), 0)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static int computeDepth(Long resourceId, Map<Long, Long> parentIdMap, Set<Long> authorizedIds,
|
||||
Map<Long, Integer> depthMap) {
|
||||
if (resourceId == null) {
|
||||
return 0;
|
||||
}
|
||||
if (depthMap.containsKey(resourceId)) {
|
||||
return depthMap.get(resourceId);
|
||||
}
|
||||
Long parentId = parentIdMap.get(resourceId);
|
||||
int depth;
|
||||
if (parentId == null || parentId == 0L || !authorizedIds.contains(parentId)) {
|
||||
depth = 0;
|
||||
} else {
|
||||
depth = 1 + computeDepth(parentId, parentIdMap, authorizedIds, depthMap);
|
||||
}
|
||||
depthMap.put(resourceId, depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归传播资源绑定类型:
|
||||
* - 如果当前资源是 ALL,则所有子资源都强制为 ALL
|
||||
* - 如果当前资源是 NONE,则所有子资源都强制为 NONE
|
||||
* - PART 类型不传播,子资源保持自己的绑定类型
|
||||
*/
|
||||
private static void propagateResourceBindType(Long resourceId,
|
||||
Map<Long, List<SysResource>> resourceChildrenMap,
|
||||
Map<Long, Integer> resourceBindTypeMap,
|
||||
Set<Long> processedResourceIds,
|
||||
Integer bindType) {
|
||||
if (processedResourceIds.contains(resourceId)) {
|
||||
return;
|
||||
}
|
||||
processedResourceIds.add(resourceId);
|
||||
|
||||
List<SysResource> children = resourceChildrenMap.get(resourceId);
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (SysResource child : children) {
|
||||
if (child.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
// 强制设置子资源的绑定类型
|
||||
resourceBindTypeMap.put(child.getId(), bindType);
|
||||
// 递归处理子资源的子资源
|
||||
propagateResourceBindType(child.getId(), resourceChildrenMap, resourceBindTypeMap,
|
||||
processedResourceIds, bindType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自上而下传播菜单绑定类型:
|
||||
* - 如果当前菜单是 ALL,则所有子菜单都强制为 ALL
|
||||
* - 如果当前菜单是 NONE,则所有子菜单都强制为 NONE
|
||||
* - 如果当前菜单是 PART,子菜单保持自己的 menuBindType(按数据记录的实际值)
|
||||
*/
|
||||
private static void propagateMenuBindType(SysMenu menu,
|
||||
Map<Long, Integer> menuBindTypeMap,
|
||||
Map<Long, List<SysMenu>> childrenMap) {
|
||||
if (menu == null) {
|
||||
return;
|
||||
}
|
||||
Integer currentType = menuBindTypeMap.get(menu.getId());
|
||||
if (currentType == null) {
|
||||
currentType = BindTypeEnum.NONE.getCode();
|
||||
menuBindTypeMap.put(menu.getId(), currentType);
|
||||
}
|
||||
|
||||
List<SysMenu> children = childrenMap.get(menu.getId());
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (SysMenu child : children) {
|
||||
Integer childCurrent = menuBindTypeMap.get(child.getId());
|
||||
|
||||
if (BindTypeEnum.ALL.getCode().equals(currentType)) {
|
||||
// 父菜单 ALL:尝试将子菜单升级为 ALL(遵循优先级合并规则)
|
||||
Integer merged = mergeMenuBindType(childCurrent, BindTypeEnum.ALL.getCode());
|
||||
menuBindTypeMap.put(child.getId(), merged);
|
||||
} else if (BindTypeEnum.NONE.getCode().equals(currentType)) {
|
||||
// 父菜单 NONE:只在子菜单当前为 NONE(或未设置)时才设置为 NONE,
|
||||
// 避免覆盖子菜单在绑定记录中已有的 ALL / PART
|
||||
if (childCurrent == null
|
||||
|| BindTypeEnum.NONE.getCode().equals(childCurrent)) {
|
||||
Integer merged = mergeMenuBindType(childCurrent, BindTypeEnum.NONE.getCode());
|
||||
menuBindTypeMap.put(child.getId(), merged);
|
||||
}
|
||||
}
|
||||
// PART 类型不传播,子菜单保持自己的 menuBindType
|
||||
propagateMenuBindType(child, menuBindTypeMap, childrenMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.domain.model.ResourceNode;
|
||||
import com.xspaceagi.system.domain.service.SysFlattenService;
|
||||
import com.xspaceagi.system.domain.service.SysMenuDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysMenu;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysMenuResource;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysResource;
|
||||
import com.xspaceagi.system.spec.enums.BindTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.OpenTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.SourceEnum;
|
||||
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
|
||||
import com.xspaceagi.system.spec.enums.YnEnum;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 角色/用户组绑定菜单与资源时的共用逻辑。
|
||||
* 提供:构建菜单/资源映射、菜单绑定类型传播、资源树过滤与重建等。
|
||||
*/
|
||||
@Component
|
||||
public class MenuBindResourceHelper {
|
||||
|
||||
@Resource
|
||||
private SysMenuDomainService sysMenuDomainService;
|
||||
@Resource
|
||||
private SysFlattenService sysFlattenService;
|
||||
|
||||
/**
|
||||
* 菜单与资源的映射容器
|
||||
*/
|
||||
public static class MenuResourceMaps {
|
||||
public final Map<Long, SysMenu> menuMap;
|
||||
public final Map<Long, List<SysMenu>> menuChildrenMap;
|
||||
public final Map<Long, SysResource> resourceMap;
|
||||
public final Map<Long, List<SysResource>> resourceChildrenMap;
|
||||
|
||||
public MenuResourceMaps(Map<Long, SysMenu> menuMap, Map<Long, List<SysMenu>> menuChildrenMap,
|
||||
Map<Long, SysResource> resourceMap, Map<Long, List<SysResource>> resourceChildrenMap) {
|
||||
this.menuMap = menuMap;
|
||||
this.menuChildrenMap = menuChildrenMap;
|
||||
this.resourceMap = resourceMap;
|
||||
this.resourceChildrenMap = resourceChildrenMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据全量菜单和资源构建映射(menuMap、menuChildrenMap、resourceMap、resourceChildrenMap)
|
||||
*/
|
||||
public MenuResourceMaps buildMenuAndResourceMaps(List<SysMenu> allMenus, List<SysResource> allResources) {
|
||||
Map<Long, SysMenu> menuMap = allMenus.stream().collect(Collectors.toMap(SysMenu::getId, m -> m));
|
||||
// parentId=0 表示 root 的子节点,需纳入 menuChildrenMap 以便 root 为 ALL 时能正确排除其所有子节点
|
||||
Map<Long, List<SysMenu>> menuChildrenMap = new HashMap<>();
|
||||
for (SysMenu menu : allMenus) {
|
||||
Long parentId = menu.getParentId();
|
||||
if (parentId != null) {
|
||||
menuChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(menu);
|
||||
}
|
||||
}
|
||||
Map<Long, SysResource> resourceMap = allResources.stream()
|
||||
.collect(Collectors.toMap(SysResource::getId, r -> r));
|
||||
Map<Long, List<SysResource>> resourceChildrenMap = new HashMap<>();
|
||||
for (SysResource resource : allResources) {
|
||||
Long parentId = resource.getParentId();
|
||||
if (parentId != null && parentId != 0L) {
|
||||
resourceChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(resource);
|
||||
}
|
||||
}
|
||||
return new MenuResourceMaps(menuMap, menuChildrenMap, resourceMap, resourceChildrenMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据前端传入的菜单列表构建 menuBindType 映射,并对 ALL/NONE 进行递归传播(单遍遍历)
|
||||
*/
|
||||
public Map<Long, Integer> buildAndPropagateMenuBindTypeMap(List<MenuNode> menuBindResourceList,
|
||||
Map<Long, List<SysMenu>> menuChildrenMap) {
|
||||
Map<Long, Integer> menuBindTypeMap = new HashMap<>();
|
||||
Set<Long> processedMenuIds = new HashSet<>();
|
||||
for (MenuNode menuNode : menuBindResourceList) {
|
||||
if (menuNode.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
Long menuId = menuNode.getId();
|
||||
Integer bindType = menuNode.getMenuBindType();
|
||||
menuBindTypeMap.put(menuId, bindType);
|
||||
if (BindTypeEnum.ALL.getCode().equals(bindType)) {
|
||||
propagateMenuBindType(menuId, menuChildrenMap, menuBindTypeMap,
|
||||
processedMenuIds, BindTypeEnum.ALL.getCode());
|
||||
} else if (BindTypeEnum.NONE.getCode().equals(bindType)) {
|
||||
propagateMenuBindType(menuId, menuChildrenMap, menuBindTypeMap,
|
||||
processedMenuIds, BindTypeEnum.NONE.getCode());
|
||||
}
|
||||
}
|
||||
return menuBindTypeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集某菜单的全部子节点 ID(不包含自身)。
|
||||
* 用于绑定入库时:menuBindType=1(ALL) 的节点只需存父节点,子节点不重复入库。
|
||||
*/
|
||||
public static Set<Long> collectAllDescendantMenuIds(Long menuId, Map<Long, List<SysMenu>> menuChildrenMap) {
|
||||
Set<Long> descendantIds = new HashSet<>();
|
||||
if (menuId == null || menuChildrenMap == null) {
|
||||
return descendantIds;
|
||||
}
|
||||
List<SysMenu> children = menuChildrenMap.get(menuId);
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return descendantIds;
|
||||
}
|
||||
for (SysMenu child : children) {
|
||||
if (child.getId() != null) {
|
||||
descendantIds.add(child.getId());
|
||||
descendantIds.addAll(collectAllDescendantMenuIds(child.getId(), menuChildrenMap));
|
||||
}
|
||||
}
|
||||
return descendantIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归传播菜单绑定类型
|
||||
*/
|
||||
public static void propagateMenuBindType(Long menuId, Map<Long, List<SysMenu>> menuChildrenMap,
|
||||
Map<Long, Integer> menuBindTypeMap, Set<Long> processedMenuIds,
|
||||
Integer bindType) {
|
||||
if (processedMenuIds.contains(menuId)) {
|
||||
return;
|
||||
}
|
||||
processedMenuIds.add(menuId);
|
||||
List<SysMenu> children = menuChildrenMap.get(menuId);
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
}
|
||||
for (SysMenu child : children) {
|
||||
if (child.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
menuBindTypeMap.put(child.getId(), bindType);
|
||||
propagateMenuBindType(child.getId(), menuChildrenMap, menuBindTypeMap, processedMenuIds, bindType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理菜单的资源树绑定逻辑:校验范围、过滤 ALL 子资源、过滤 resourceBindType=0/null,并重建树。
|
||||
* 支持前端仅传叶子节点(扁平列表):会自动向上构建父节点并推断父节点的 resourceBindType。
|
||||
* 父节点绑定类型推断规则:子节点全为1→父为1,全为0→父为0,否则父为2(PART)。
|
||||
*/
|
||||
public List<ResourceNode> processResourceTreeForMenu(Long menuId, List<ResourceNode> resourceTree,
|
||||
Map<Long, SysResource> resourceMap,
|
||||
Map<Long, List<SysResource>> resourceChildrenMap) {
|
||||
if (CollectionUtils.isEmpty(resourceTree)) {
|
||||
return null;
|
||||
}
|
||||
List<SysMenuResource> menuResources = sysMenuDomainService.queryResourceListByMenuId(menuId);
|
||||
Set<Long> menuAuthorizedResourceIds = new HashSet<>();
|
||||
if (CollectionUtils.isNotEmpty(menuResources)) {
|
||||
for (SysMenuResource menuResource : menuResources) {
|
||||
if (menuResource.getResourceId() != null) {
|
||||
menuAuthorizedResourceIds.add(menuResource.getResourceId());
|
||||
if (BindTypeEnum.ALL.getCode().equals(menuResource.getResourceBindType())) {
|
||||
collectChildrenResourceIds(menuResource.getResourceId(), resourceChildrenMap, menuAuthorizedResourceIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
List<ResourceNode> flattenResourceList = sysFlattenService.flattenResourceTree(resourceTree);
|
||||
// 向上构建父节点并推断父节点的 resourceBindType(前端仅传叶子节点时)
|
||||
flattenResourceList = expandWithAncestorNodes(flattenResourceList, resourceMap, resourceChildrenMap);
|
||||
for (ResourceNode resourceNode : flattenResourceList) {
|
||||
Long resourceId = resourceNode.getId();
|
||||
if (resourceId == null) {
|
||||
continue;
|
||||
}
|
||||
if (resourceId != 0L && !resourceMap.containsKey(resourceId)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemMenuBindResourceNotFound, resourceId);
|
||||
}
|
||||
if (!menuAuthorizedResourceIds.contains(resourceId)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemMenuBindResourceOutOfScope, resourceId, menuId);
|
||||
}
|
||||
Integer resourceBindType = resourceNode.getResourceBindType();
|
||||
if (resourceBindType != null && BindTypeEnum.isInValid(resourceBindType)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemMenuBindResourceBindTypeInvalid, resourceId, resourceBindType);
|
||||
}
|
||||
}
|
||||
Set<Long> allBindResourceIds = flattenResourceList.stream()
|
||||
.filter(r -> r.getId() != null && BindTypeEnum.ALL.getCode().equals(r.getResourceBindType()))
|
||||
.map(ResourceNode::getId)
|
||||
.collect(Collectors.toSet());
|
||||
Set<Long> childrenResourceIdsToExclude = new HashSet<>();
|
||||
for (Long allBindResourceId : allBindResourceIds) {
|
||||
collectChildrenResourceIds(allBindResourceId, resourceChildrenMap, childrenResourceIdsToExclude);
|
||||
}
|
||||
List<ResourceNode> filteredResourceList = flattenResourceList.stream()
|
||||
.filter(resourceNode -> {
|
||||
Long resourceId = resourceNode.getId();
|
||||
Integer resourceBindType = resourceNode.getResourceBindType();
|
||||
if (resourceId == null || childrenResourceIdsToExclude.contains(resourceId)) {
|
||||
return false;
|
||||
}
|
||||
if (resourceBindType == null || BindTypeEnum.NONE.getCode().equals(resourceBindType)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return rebuildResourceTree(filteredResourceList, resourceMap, resourceChildrenMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上构建父节点并推断父节点的 resourceBindType。
|
||||
* 当前端仅传叶子节点时,根据 parentId 链补齐祖先节点;
|
||||
* 父节点 resourceBindType 推断规则:直接子节点全为1→父为1,全为0→父为0,否则父为2(PART)。
|
||||
* 需考虑祖先节点的全部子节点:未传入的子节点视为 NONE(0)。
|
||||
*/
|
||||
private List<ResourceNode> expandWithAncestorNodes(List<ResourceNode> flattenResourceList,
|
||||
Map<Long, SysResource> resourceMap,
|
||||
Map<Long, List<SysResource>> resourceChildrenMap) {
|
||||
if (CollectionUtils.isEmpty(flattenResourceList) || resourceMap == null) {
|
||||
return flattenResourceList;
|
||||
}
|
||||
Map<Long, ResourceNode> nodeMap = flattenResourceList.stream()
|
||||
.filter(n -> n.getId() != null)
|
||||
.collect(Collectors.toMap(ResourceNode::getId, n -> n, (a, b) -> a));
|
||||
Set<Long> ancestorIds = new HashSet<>();
|
||||
for (ResourceNode node : flattenResourceList) {
|
||||
if (node.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
SysResource resource = resourceMap.get(node.getId());
|
||||
if (resource == null) {
|
||||
continue;
|
||||
}
|
||||
Long parentId = resource.getParentId();
|
||||
while (parentId != null && parentId != 0L && resourceMap.containsKey(parentId)) {
|
||||
if (!nodeMap.containsKey(parentId)) {
|
||||
ancestorIds.add(parentId);
|
||||
}
|
||||
SysResource parentResource = resourceMap.get(parentId);
|
||||
parentId = parentResource != null ? parentResource.getParentId() : null;
|
||||
}
|
||||
}
|
||||
if (ancestorIds.isEmpty()) {
|
||||
return flattenResourceList;
|
||||
}
|
||||
List<Long> sortedAncestorIds = sortAncestorIdsByDepth(ancestorIds, resourceMap);
|
||||
for (Long ancestorId : sortedAncestorIds) {
|
||||
SysResource resource = resourceMap.get(ancestorId);
|
||||
if (resource == null) {
|
||||
continue;
|
||||
}
|
||||
// 祖先节点的全部直接子节点:传入的用其 resourceBindType,未传入的视为 NONE(0)
|
||||
List<Integer> allChildrenBindTypes = collectAllChildrenBindTypes(ancestorId, nodeMap, resourceMap,
|
||||
resourceChildrenMap);
|
||||
Integer inferredBindType = inferResourceBindTypeFromChildrenBindTypes(allChildrenBindTypes);
|
||||
ResourceNode ancestorNode = new ResourceNode();
|
||||
ancestorNode.setId(ancestorId);
|
||||
ancestorNode.setParentId(resource.getParentId());
|
||||
ancestorNode.setResourceBindType(inferredBindType);
|
||||
ancestorNode.setCode(resource.getCode());
|
||||
nodeMap.put(ancestorId, ancestorNode);
|
||||
}
|
||||
return new ArrayList<>(nodeMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按深度升序排序祖先ID(叶子级祖先先处理,用于自底向上推断 resourceBindType)
|
||||
*/
|
||||
private static List<Long> sortAncestorIdsByDepth(Set<Long> ancestorIds, Map<Long, SysResource> resourceMap) {
|
||||
Map<Long, Integer> depthMap = new HashMap<>();
|
||||
for (Long id : ancestorIds) {
|
||||
int depth = 0;
|
||||
Long currentId = id;
|
||||
while (currentId != null && currentId != 0L) {
|
||||
SysResource r = resourceMap.get(currentId);
|
||||
currentId = r != null ? r.getParentId() : null;
|
||||
depth++;
|
||||
}
|
||||
depthMap.put(id, depth);
|
||||
}
|
||||
// 按深度降序,先处理叶子级祖先(如513),再处理根级祖先(如512)
|
||||
return ancestorIds.stream()
|
||||
.sorted(Comparator.comparing(depthMap::get).reversed())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集祖先节点全部直接子节点的 resourceBindType:
|
||||
* 在 nodeMap(传入的节点)中的用其真实值,未传入的视为 NONE(0)。
|
||||
*/
|
||||
private static List<Integer> collectAllChildrenBindTypes(Long ancestorId,
|
||||
Map<Long, ResourceNode> nodeMap,
|
||||
Map<Long, SysResource> resourceMap,
|
||||
Map<Long, List<SysResource>> resourceChildrenMap) {
|
||||
List<SysResource> allChildren = resourceChildrenMap != null ? resourceChildrenMap.get(ancestorId) : null;
|
||||
if (CollectionUtils.isEmpty(allChildren)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Integer> bindTypes = new ArrayList<>(allChildren.size());
|
||||
for (SysResource child : allChildren) {
|
||||
if (child.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
ResourceNode nodeInSelection = nodeMap.get(child.getId());
|
||||
if (nodeInSelection != null) {
|
||||
Integer bt = nodeInSelection.getResourceBindType();
|
||||
bindTypes.add(bt != null ? bt : BindTypeEnum.NONE.getCode());
|
||||
} else {
|
||||
bindTypes.add(BindTypeEnum.NONE.getCode());
|
||||
}
|
||||
}
|
||||
return bindTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据直接子节点的 resourceBindType 推断父节点的 resourceBindType。
|
||||
* 全为1→1(ALL),全为0→0(NONE),否则→2(PART)。
|
||||
*/
|
||||
private static Integer inferResourceBindTypeFromChildrenBindTypes(List<Integer> childrenBindTypes) {
|
||||
if (CollectionUtils.isEmpty(childrenBindTypes)) {
|
||||
return BindTypeEnum.NONE.getCode();
|
||||
}
|
||||
boolean allAll = childrenBindTypes.stream().allMatch(b -> BindTypeEnum.ALL.getCode().equals(b));
|
||||
if (allAll) {
|
||||
return BindTypeEnum.ALL.getCode();
|
||||
}
|
||||
boolean allNone = childrenBindTypes.stream().allMatch(b -> BindTypeEnum.NONE.getCode().equals(b));
|
||||
if (allNone) {
|
||||
return BindTypeEnum.NONE.getCode();
|
||||
}
|
||||
return BindTypeEnum.PART.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据扁平列表重新构建资源树(按 resourceId 去重,避免同一节点重复挂到父节点或根下)
|
||||
*/
|
||||
public static List<ResourceNode> rebuildResourceTree(List<ResourceNode> flatList,
|
||||
Map<Long, SysResource> resourceMap,
|
||||
Map<Long, List<SysResource>> resourceChildrenMap) {
|
||||
if (CollectionUtils.isEmpty(flatList)) {
|
||||
return null;
|
||||
}
|
||||
// 按 resourceId 去重,同一 resourceId 只保留一个节点,避免存入重复数据
|
||||
Map<Long, ResourceNode> nodeMap = new HashMap<>();
|
||||
for (ResourceNode node : flatList) {
|
||||
if (node.getId() != null) {
|
||||
nodeMap.put(node.getId(), node);
|
||||
}
|
||||
}
|
||||
// 清空所有节点的 children,避免沿用入参树上的旧子节点导致重复(重建时只通过下方挂载逻辑设置子节点)
|
||||
for (ResourceNode node : nodeMap.values()) {
|
||||
node.setChildren(null);
|
||||
}
|
||||
List<ResourceNode> rootNodes = new ArrayList<>();
|
||||
// 遍历去重后的 nodeMap.values(),确保每个节点只挂载一次
|
||||
for (ResourceNode node : nodeMap.values()) {
|
||||
if (node.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (node.getId() == 0L) {
|
||||
rootNodes.add(node);
|
||||
continue;
|
||||
}
|
||||
SysResource resource = resourceMap.get(node.getId());
|
||||
if (resource == null) {
|
||||
continue;
|
||||
}
|
||||
Long parentId = resource.getParentId();
|
||||
if (parentId == null || parentId == 0L || !nodeMap.containsKey(parentId)) {
|
||||
rootNodes.add(node);
|
||||
} else {
|
||||
ResourceNode parent = nodeMap.get(parentId);
|
||||
if (parent.getChildren() == null) {
|
||||
parent.setChildren(new ArrayList<>());
|
||||
}
|
||||
parent.getChildren().add(node);
|
||||
}
|
||||
}
|
||||
return rootNodes.isEmpty() ? null : rootNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集某个资源节点下的所有子资源ID
|
||||
*/
|
||||
public static void collectChildrenResourceIds(Long resourceId, Map<Long, List<SysResource>> resourceChildrenMap,
|
||||
Set<Long> idSet) {
|
||||
List<SysResource> children = resourceChildrenMap.get(resourceId);
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
}
|
||||
for (SysResource child : children) {
|
||||
if (child.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (idSet.add(child.getId())) {
|
||||
collectChildrenResourceIds(child.getId(), resourceChildrenMap, idSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 角色/用户组 getMenuTree 共用:根菜单、菜单树构建、自下而上/自上而下打标、资源树填充 ----------
|
||||
|
||||
/**
|
||||
* 返回虚拟根菜单(id=0)
|
||||
*/
|
||||
public static SysMenu getRootMenu() {
|
||||
SysMenu rootMenu = new SysMenu();
|
||||
rootMenu.setId(0L);
|
||||
rootMenu.setCode("root");
|
||||
rootMenu.setName("根节点");
|
||||
rootMenu.setDescription("根节点");
|
||||
rootMenu.setSource(SourceEnum.SYSTEM.getCode());
|
||||
rootMenu.setStatus(YesOrNoEnum.Y.getKey());
|
||||
rootMenu.setParentId(null);
|
||||
rootMenu.setOpenType(OpenTypeEnum.CURRENT_TAB.getCode());
|
||||
rootMenu.setCreatorId(0L);
|
||||
rootMenu.setCreator("System");
|
||||
rootMenu.setYn(YnEnum.Y.getKey());
|
||||
return rootMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单树形结构(根节点集合),会修改 menuList
|
||||
*/
|
||||
public static void buildMenuTree(List<MenuNode> menuList) {
|
||||
if (CollectionUtils.isEmpty(menuList)) {
|
||||
return;
|
||||
}
|
||||
Map<Long, MenuNode> menuMap = menuList.stream()
|
||||
.filter(node -> node.getId() != null)
|
||||
.collect(Collectors.toMap(MenuNode::getId, node -> node, (a, b) -> a));
|
||||
List<MenuNode> rootMenus = new ArrayList<>();
|
||||
for (MenuNode menuNode : menuList) {
|
||||
Long parentId = menuNode.getParentId();
|
||||
Long menuId = menuNode.getId();
|
||||
if (menuId != null && menuId == 0L) {
|
||||
rootMenus.add(menuNode);
|
||||
continue;
|
||||
}
|
||||
if (parentId == null || parentId == 0L) {
|
||||
MenuNode root = menuMap.get(0L);
|
||||
if (root != null) {
|
||||
if (root.getChildren() == null) {
|
||||
root.setChildren(new ArrayList<>());
|
||||
}
|
||||
root.getChildren().add(menuNode);
|
||||
} else {
|
||||
rootMenus.add(menuNode);
|
||||
}
|
||||
} else {
|
||||
MenuNode parent = menuMap.get(parentId);
|
||||
if (parent != null) {
|
||||
if (parent.getChildren() == null) {
|
||||
parent.setChildren(new ArrayList<>());
|
||||
}
|
||||
parent.getChildren().add(menuNode);
|
||||
} else {
|
||||
rootMenus.add(menuNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
sortMenuTree(rootMenus);
|
||||
menuList.clear();
|
||||
menuList.addAll(rootMenus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归排序菜单树
|
||||
*/
|
||||
public static void sortMenuTree(List<MenuNode> menuList) {
|
||||
if (CollectionUtils.isEmpty(menuList)) {
|
||||
return;
|
||||
}
|
||||
menuList.sort((a, b) -> {
|
||||
Integer sortA = a.getSortIndex() != null ? a.getSortIndex() : 0;
|
||||
Integer sortB = b.getSortIndex() != null ? b.getSortIndex() : 0;
|
||||
return sortA.compareTo(sortB);
|
||||
});
|
||||
for (MenuNode menuNode : menuList) {
|
||||
if (CollectionUtils.isNotEmpty(menuNode.getChildren())) {
|
||||
sortMenuTree(menuNode.getChildren());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自下而上打标菜单绑定类型:显式绑定的保留,未显式绑定的根据子菜单合成 NONE/ALL/PART
|
||||
*/
|
||||
public static void adjustMenuBindTypeBottomUp(MenuNode node, Set<Long> explicitlyBoundMenuIds) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
List<MenuNode> children = node.getChildren();
|
||||
if (CollectionUtils.isNotEmpty(children)) {
|
||||
for (MenuNode child : children) {
|
||||
adjustMenuBindTypeBottomUp(child, explicitlyBoundMenuIds);
|
||||
}
|
||||
Long menuId = node.getId();
|
||||
if (menuId != null && explicitlyBoundMenuIds.contains(menuId)) {
|
||||
return;
|
||||
}
|
||||
boolean allNone = true;
|
||||
boolean allAll = true;
|
||||
boolean hasNonNone = false;
|
||||
for (MenuNode child : children) {
|
||||
Integer bindType = child.getMenuBindType();
|
||||
if (bindType == null || BindTypeEnum.NONE.getCode().equals(bindType)) {
|
||||
allAll = false;
|
||||
} else if (BindTypeEnum.ALL.getCode().equals(bindType)) {
|
||||
hasNonNone = true;
|
||||
allNone = false;
|
||||
} else {
|
||||
hasNonNone = true;
|
||||
allNone = false;
|
||||
allAll = false;
|
||||
}
|
||||
}
|
||||
if (allNone) {
|
||||
node.setMenuBindType(BindTypeEnum.NONE.getCode());
|
||||
} else if (allAll && hasNonNone) {
|
||||
node.setMenuBindType(BindTypeEnum.ALL.getCode());
|
||||
} else {
|
||||
node.setMenuBindType(BindTypeEnum.PART.getCode());
|
||||
}
|
||||
} else {
|
||||
Long menuId = node.getId();
|
||||
if (menuId == null || explicitlyBoundMenuIds.contains(menuId)) {
|
||||
return;
|
||||
}
|
||||
if (node.getMenuBindType() == null) {
|
||||
node.setMenuBindType(BindTypeEnum.NONE.getCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对 root 节点自上而下强制传播 ALL/NONE 到所有子菜单
|
||||
*/
|
||||
public static void propagateRootMenuBindType(MenuNode root) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
Integer bindType = root.getMenuBindType();
|
||||
if (bindType == null || BindTypeEnum.PART.getCode().equals(bindType)) {
|
||||
return;
|
||||
}
|
||||
if (!BindTypeEnum.ALL.getCode().equals(bindType) && !BindTypeEnum.NONE.getCode().equals(bindType)) {
|
||||
return;
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(root.getChildren())) {
|
||||
propagateMenuBindTypeFromRootChildren(root.getChildren(), bindType);
|
||||
}
|
||||
}
|
||||
|
||||
private static void propagateMenuBindTypeFromRootChildren(List<MenuNode> children, Integer bindType) {
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
}
|
||||
for (MenuNode child : children) {
|
||||
child.setMenuBindType(bindType);
|
||||
if (CollectionUtils.isNotEmpty(child.getChildren())) {
|
||||
propagateMenuBindTypeFromRootChildren(child.getChildren(), bindType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充资源树中的资源详细信息
|
||||
*/
|
||||
public static void fillResourceTreeDetails(List<ResourceNode> resourceNodes, Map<Long, SysResource> resourceMap) {
|
||||
if (CollectionUtils.isEmpty(resourceNodes) || resourceMap == null || resourceMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (ResourceNode node : resourceNodes) {
|
||||
if (node.getId() != null) {
|
||||
SysResource resource = resourceMap.get(node.getId());
|
||||
if (resource != null) {
|
||||
node.setParentId(resource.getParentId());
|
||||
node.setCode(resource.getCode());
|
||||
node.setName(resource.getName());
|
||||
node.setDescription(resource.getDescription());
|
||||
node.setSource(resource.getSource());
|
||||
node.setType(resource.getType());
|
||||
node.setPath(resource.getPath());
|
||||
node.setIcon(resource.getIcon());
|
||||
node.setSortIndex(resource.getSortIndex());
|
||||
node.setStatus(resource.getStatus());
|
||||
}
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(node.getChildren())) {
|
||||
fillResourceTreeDetails(node.getChildren(), resourceMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.xspaceagi.system.domain.service.NotifyMessageDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.NotifyMessage;
|
||||
import com.xspaceagi.system.infra.dao.entity.NotifyMessageUser;
|
||||
import com.xspaceagi.system.infra.dao.service.NotifyMessageService;
|
||||
import com.xspaceagi.system.infra.dao.service.NotifyMessageUserService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class NotifyMessageDomainServiceImpl implements NotifyMessageDomainService {
|
||||
|
||||
@Resource
|
||||
private NotifyMessageService notifyMessageService;
|
||||
|
||||
@Resource
|
||||
private NotifyMessageUserService notifyMessageUserService;
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public void addNotifyMessage(NotifyMessage notifyMessage, List<Long> userIds) {
|
||||
notifyMessageUserService.saveBatch(userIds.stream().map(userId -> {
|
||||
NotifyMessageUser notifyMessageUser = new NotifyMessageUser();
|
||||
notifyMessageUser.setUserId(userId);
|
||||
notifyMessageUser.setNotifyId(notifyMessage.getId());
|
||||
notifyMessageUser.setReadStatus(NotifyMessageUser.ReadStatus.Unread);
|
||||
return notifyMessageUser;
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateReadStatus(Long userId, List<Long> notifyIds) {
|
||||
LambdaQueryWrapper<NotifyMessageUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.in(NotifyMessageUser::getNotifyId, notifyIds);
|
||||
queryWrapper.eq(NotifyMessageUser::getUserId, userId);
|
||||
NotifyMessageUser notifyMessageUser = new NotifyMessageUser();
|
||||
notifyMessageUser.setReadStatus(NotifyMessageUser.ReadStatus.Read);
|
||||
notifyMessageUserService.update(notifyMessageUser, queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NotifyMessageUser> queryNotifyMessageUserList(Long userId, Long lastId, Integer size, NotifyMessageUser.ReadStatus readStatus) {
|
||||
if (lastId == null) {
|
||||
lastId = Long.MAX_VALUE;
|
||||
}
|
||||
if (size == null || size <= 0) {
|
||||
size = 10;
|
||||
}
|
||||
LambdaQueryWrapper<NotifyMessageUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(NotifyMessageUser::getUserId, userId);
|
||||
queryWrapper.lt(NotifyMessageUser::getNotifyId, lastId);
|
||||
if (readStatus != null) {
|
||||
queryWrapper.eq(NotifyMessageUser::getReadStatus, readStatus);
|
||||
}
|
||||
queryWrapper.last("limit " + size);
|
||||
queryWrapper.orderByDesc(NotifyMessageUser::getNotifyId);
|
||||
return notifyMessageUserService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NotifyMessage> queryNotifyMessageList(List<Long> notifyIds) {
|
||||
if (notifyIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
LambdaQueryWrapper<NotifyMessage> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.in(NotifyMessage::getId, notifyIds);
|
||||
return notifyMessageService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countUnreadNotifyMessage(Long userId) {
|
||||
LambdaQueryWrapper<NotifyMessageUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(NotifyMessageUser::getUserId, userId);
|
||||
queryWrapper.eq(NotifyMessageUser::getReadStatus, NotifyMessageUser.ReadStatus.Unread);
|
||||
return notifyMessageUserService.count(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAllUnreadNotifyMessage(Long userId) {
|
||||
LambdaQueryWrapper<NotifyMessageUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(NotifyMessageUser::getUserId, userId);
|
||||
queryWrapper.eq(NotifyMessageUser::getReadStatus, NotifyMessageUser.ReadStatus.Unread);
|
||||
|
||||
NotifyMessageUser notifyMessageUser = new NotifyMessageUser();
|
||||
notifyMessageUser.setReadStatus(NotifyMessageUser.ReadStatus.Read);
|
||||
notifyMessageUserService.update(notifyMessageUser, queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.xspaceagi.system.domain.service.RetryDomainService;
|
||||
import com.xspaceagi.system.sdk.retry.dto.RetryExecDto;
|
||||
import com.xspaceagi.system.sdk.retry.utils.ExceptionUtils;
|
||||
import com.xspaceagi.system.infra.dao.entity.RetryData;
|
||||
import com.xspaceagi.system.infra.dao.service.RetryDataService;
|
||||
import com.xspaceagi.system.infra.rpc.RetryCallBackRpcService;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import com.xspaceagi.system.spec.enums.RetryStatusEnum;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RetryDomainServiceImpl implements RetryDomainService {
|
||||
|
||||
private static final int MAX_EXEC_CT = 1000;
|
||||
@Resource
|
||||
private RetryDataService retryDataService;
|
||||
|
||||
@Resource
|
||||
private RetryCallBackRpcService retryCallBackRpcService;
|
||||
|
||||
private ScheduledExecutorService retryExecutor = Executors.newScheduledThreadPool(1);
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
retryExecutor.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
int ct = MAX_EXEC_CT;
|
||||
RetryData retryData = queryWaitingRetryDataAndLock();
|
||||
while (null != retryData && ct-- > 0) {
|
||||
exec(retryData, true);
|
||||
retryData = queryWaitingRetryDataAndLock();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("重试数据处理错误", e);
|
||||
}
|
||||
}, 50, 5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private RetryData queryWaitingRetryDataAndLock() {
|
||||
QueryWrapper<RetryData> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.in("status", Lists.newArrayList(RetryStatusEnum.WAIT.getValue(), RetryStatusEnum.FAIL.getValue()));
|
||||
queryWrapper.lt("lock_time", new Date());
|
||||
queryWrapper.last("LIMIT 1");
|
||||
RetryData retryDataEntity = retryDataService.getOne(queryWrapper);
|
||||
if (retryDataEntity == null) {
|
||||
return null;
|
||||
}
|
||||
int retryCt = retryDataEntity.getRetryCnt() + 1;
|
||||
int step = retryCt > retryDataEntity.getMaxRetryCnt() ? retryDataEntity.getMaxRetryCnt() : retryCt;
|
||||
retryDataEntity.setLockTime(new Date(System.currentTimeMillis() + 30000 * step));//30秒内不能再重试
|
||||
retryDataService.updateById(retryDataEntity);
|
||||
return retryDataEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行重试任务
|
||||
*/
|
||||
private void exec(RetryData retryData, boolean checkRetryTimes) {
|
||||
if (retryData == null) {
|
||||
return;
|
||||
}
|
||||
Integer reqId = ThreadLocalRandom.current().nextInt(100, 999999);
|
||||
MDC.put("tid", reqId + "" + Instant.now().toEpochMilli());
|
||||
try {
|
||||
RetryExecDto dto = new RetryExecDto();
|
||||
dto.setBeanName(retryData.getBeanName());
|
||||
dto.setArgStr(retryData.getArgStr());
|
||||
dto.setMethodName(retryData.getMethodName());
|
||||
dto.setArgClassNames(JSONObject.parseObject(retryData.getArgClassNames(), String[].class));
|
||||
ReqResult<?> result = retryCallBackRpcService.methodInvoke(dto);
|
||||
retryData.setStatus(RetryStatusEnum.SUCCESS.getValue());
|
||||
if (!result.isSuccess()) {
|
||||
retryData.setStatus(RetryStatusEnum.FAIL.getValue());
|
||||
}
|
||||
String resultStr = JSONObject.toJSONString(result);
|
||||
retryData.setResult(resultStr);
|
||||
retryData.setRetryCnt(retryData.getRetryCnt() + 1);
|
||||
if (checkRetryTimes && retryData.getRetryCnt() >= retryData.getMaxRetryCnt()) {
|
||||
retryData.setStatus(RetryStatusEnum.BAN.getValue());
|
||||
}
|
||||
log.info("重试结果, class: {}, method:{}, result: {}", retryData.getBeanName(), retryData.getMethodName(), resultStr);
|
||||
} catch (Throwable e) {
|
||||
log.error("重试异常, class: {}, method:{}", retryData.getBeanName(), retryData.getMethodName(), e);
|
||||
retryData.setResult(ExceptionUtils.getStackTrace(e));
|
||||
retryData.setStatus(RetryStatusEnum.FAIL.getValue());
|
||||
} finally {
|
||||
retryDataService.updateById(retryData);
|
||||
MDC.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.xspaceagi.system.domain.service.ScheduleTaskDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.ScheduleTask;
|
||||
import com.xspaceagi.system.infra.dao.service.ScheduleTaskService;
|
||||
import com.xspaceagi.system.sdk.retry.annotation.Retry;
|
||||
import com.xspaceagi.system.sdk.service.TaskExecuteService;
|
||||
import com.xspaceagi.system.sdk.service.dto.ScheduleTaskDto;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.quartz.CronScheduleBuilder;
|
||||
import org.quartz.CronTrigger;
|
||||
import org.quartz.TriggerBuilder;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ScheduleTaskDomainServiceImpl implements ScheduleTaskDomainService {
|
||||
|
||||
private static final int MAX_EXEC_CT = 1000;
|
||||
|
||||
@Resource
|
||||
private ScheduleTaskService scheduleTaskService;
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Autowired
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1, new CustomThreadFactory("schedule-task-main-loop"));
|
||||
private final ExecutorService callbackExecutor = new ThreadPoolExecutor(0, 100,
|
||||
60L, TimeUnit.SECONDS,
|
||||
new SynchronousQueue<Runnable>(),
|
||||
new CustomThreadFactory("schedule-task-executor"));
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 查询状态为正在执行的数据,如果存在,修改状态,避免重启服务导致部分任务无法执行
|
||||
LambdaQueryWrapper<ScheduleTask> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
lambdaQueryWrapper.eq(ScheduleTask::getStatus, ScheduleTaskDto.Status.EXECUTING);
|
||||
ScheduleTask scheduleTaskUpdate = new ScheduleTask();
|
||||
scheduleTaskUpdate.setStatus(ScheduleTaskDto.Status.CONTINUE);
|
||||
scheduleTaskService.update(scheduleTaskUpdate, lambdaQueryWrapper);
|
||||
// 轮询任务数据
|
||||
executor.scheduleAtFixedRate(() -> callbackExecutor.execute(() -> checkAndExecuteTask(new AtomicInteger(MAX_EXEC_CT))), 5, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void checkAndExecuteTask(AtomicInteger ct) {
|
||||
if (ct.decrementAndGet() < 0) {
|
||||
return;
|
||||
}
|
||||
ScheduleTask scheduleTaskForExec = queryOneAndLock();
|
||||
if (null != scheduleTaskForExec) {
|
||||
String info = JSONObject.toJSONString(scheduleTaskForExec);
|
||||
log.debug("查询任务查询数据,第{}次处理:{}", scheduleTaskForExec.getExecTimes(), info);
|
||||
callback(scheduleTaskForExec).subscribe(res -> {
|
||||
log.debug("任务数据处理结束:{}", info);
|
||||
ScheduleTask scheduleTask = queryOne(scheduleTaskForExec.getTaskId());
|
||||
if (scheduleTask.getStatus() == ScheduleTaskDto.Status.COMPLETE || scheduleTask.getStatus() == ScheduleTaskDto.Status.CANCEL) {
|
||||
callbackExecutor.execute(() -> checkAndExecuteTask(ct));
|
||||
return;
|
||||
}
|
||||
scheduleTask.setStatus(res ? ScheduleTaskDto.Status.COMPLETE : ScheduleTaskDto.Status.CONTINUE);
|
||||
if (!res && scheduleTask.getExecTimes() >= scheduleTask.getMaxExecTimes()) {
|
||||
scheduleTask.setStatus(ScheduleTaskDto.Status.COMPLETE);
|
||||
}
|
||||
scheduleTask.setError("");
|
||||
scheduleTaskService.updateById(scheduleTask);
|
||||
callbackExecutor.execute(() -> checkAndExecuteTask(ct));
|
||||
}, e -> {
|
||||
log.error("任务回调业务模块失败 {}", JSONObject.toJSONString(scheduleTaskForExec), e);
|
||||
ScheduleTask taskUpdate = new ScheduleTask();
|
||||
taskUpdate.setStatus(ScheduleTaskDto.Status.FAIL);
|
||||
taskUpdate.setError(e.getMessage());
|
||||
taskUpdate.setId(scheduleTaskForExec.getId());
|
||||
taskUpdate.setLockTime(getNextExecDate(scheduleTaskForExec.getCron()));
|
||||
scheduleTaskService.updateById(taskUpdate);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class CustomThreadFactory implements ThreadFactory {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final AtomicInteger count = new AtomicInteger(0);
|
||||
|
||||
private CustomThreadFactory(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
return new Thread(r, "CustomThread-" + name + "-" + count.incrementAndGet());
|
||||
}
|
||||
}
|
||||
|
||||
private ScheduleTask queryOneAndLock() {
|
||||
String taskServerInfo = System.getenv("TASK_SERVER_INFO");
|
||||
LambdaQueryWrapper<ScheduleTask> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
lambdaQueryWrapper.lt(ScheduleTask::getLockTime, new Date());
|
||||
lambdaQueryWrapper.in(ScheduleTask::getStatus, List.of(ScheduleTaskDto.Status.CREATE, ScheduleTaskDto.Status.FAIL, ScheduleTaskDto.Status.CONTINUE));
|
||||
if (StringUtils.isNotBlank(taskServerInfo)) {
|
||||
lambdaQueryWrapper.and(wrapper -> wrapper.eq(ScheduleTask::getServerInfo, taskServerInfo).or().isNull(ScheduleTask::getServerInfo));
|
||||
} else {
|
||||
lambdaQueryWrapper.isNull(ScheduleTask::getServerInfo);
|
||||
}
|
||||
lambdaQueryWrapper.last("LIMIT 1");
|
||||
lambdaQueryWrapper.orderByAsc(ScheduleTask::getLockTime);
|
||||
ScheduleTask scheduleTask = scheduleTaskService.getOne(lambdaQueryWrapper);
|
||||
if (scheduleTask == null) {
|
||||
return null;
|
||||
}
|
||||
LambdaUpdateWrapper<ScheduleTask> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(ScheduleTask::getId, scheduleTask.getId());
|
||||
updateWrapper.eq(ScheduleTask::getExecTimes, scheduleTask.getExecTimes());
|
||||
updateWrapper.set(ScheduleTask::getStatus, ScheduleTaskDto.Status.EXECUTING);
|
||||
if (StringUtils.isNotBlank(scheduleTask.getCron())) {
|
||||
updateWrapper.set(ScheduleTask::getLockTime, getNextExecDate(scheduleTask.getCron()));
|
||||
}
|
||||
updateWrapper.set(ScheduleTask::getExecTimes, scheduleTask.getExecTimes() + 1);
|
||||
updateWrapper.set(ScheduleTask::getLatestExecTime, new Date());
|
||||
if (!scheduleTaskService.update(updateWrapper)) {
|
||||
return null;
|
||||
}
|
||||
return scheduleTask;
|
||||
}
|
||||
|
||||
private Mono<Boolean> callback(ScheduleTask scheduleTask) {
|
||||
TaskExecuteService taskExecuteService;
|
||||
try {
|
||||
taskExecuteService = (TaskExecuteService) applicationContext.getBean(scheduleTask.getBeanId());
|
||||
} catch (Exception e) {
|
||||
if (e instanceof NoSuchBeanDefinitionException) {
|
||||
log.error("任务回调业务模块不存在 {}", scheduleTask.getBeanId());
|
||||
return Mono.just(true);// 任务回调业务模块不存在,后续不再执行
|
||||
}
|
||||
return Mono.just(false);
|
||||
}
|
||||
ScheduleTaskDto scheduleTaskDto = new ScheduleTaskDto();
|
||||
BeanUtils.copyProperties(scheduleTask, scheduleTaskDto);
|
||||
try {
|
||||
return taskExecuteService.asyncExecute(scheduleTaskDto);
|
||||
} catch (Exception e) {
|
||||
return Mono.just(false);
|
||||
}
|
||||
}
|
||||
|
||||
private ScheduleTask queryOne(String taskId) {
|
||||
LambdaQueryWrapper<ScheduleTask> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
lambdaQueryWrapper.eq(ScheduleTask::getTaskId, taskId);
|
||||
return scheduleTaskService.getOne(lambdaQueryWrapper, false);
|
||||
}
|
||||
|
||||
@Retry
|
||||
@Override
|
||||
public Long start(ScheduleTask scheduleTask) {
|
||||
ScheduleTask scheduleTask1 = queryOne(scheduleTask.getTaskId());
|
||||
if (null != scheduleTask1) {
|
||||
// 曾按 taskId 注销后,再次 register 时若仅 return,
|
||||
// 记录仍为 CANCEL,重新start需把同 taskId 任务拉回到 CREATE。
|
||||
if (scheduleTask1.getStatus() != ScheduleTaskDto.Status.CREATE
|
||||
&& scheduleTask1.getStatus() != ScheduleTaskDto.Status.EXECUTING
|
||||
&& scheduleTask1.getStatus() != ScheduleTaskDto.Status.CONTINUE) {
|
||||
if (StringUtils.isNotBlank(scheduleTask.getCron())) {
|
||||
scheduleTask1.setCron(scheduleTask.getCron());
|
||||
}
|
||||
if (StringUtils.isNotBlank(scheduleTask.getBeanId())) {
|
||||
scheduleTask1.setBeanId(scheduleTask.getBeanId());
|
||||
}
|
||||
if (scheduleTask.getParams() != null) {
|
||||
scheduleTask1.setParams(scheduleTask.getParams());
|
||||
}
|
||||
if (scheduleTask.getMaxExecTimes() != null) {
|
||||
scheduleTask1.setMaxExecTimes(scheduleTask.getMaxExecTimes());
|
||||
}
|
||||
if (StringUtils.isNotBlank(scheduleTask.getTaskName())) {
|
||||
scheduleTask1.setTaskName(scheduleTask.getTaskName());
|
||||
}
|
||||
if (StringUtils.isNotBlank(scheduleTask.getTargetType())) {
|
||||
scheduleTask1.setTargetType(scheduleTask.getTargetType());
|
||||
}
|
||||
if (StringUtils.isNotBlank(scheduleTask.getTargetId())) {
|
||||
scheduleTask1.setTargetId(scheduleTask.getTargetId());
|
||||
}
|
||||
scheduleTask1.setStatus(ScheduleTaskDto.Status.CREATE);
|
||||
scheduleTask1.setExecTimes(0L);
|
||||
scheduleTask1.setError("");
|
||||
scheduleTask1.setLockTime(getNextExecDate(scheduleTask1.getCron()));
|
||||
scheduleTaskService.updateById(scheduleTask1);
|
||||
return scheduleTask1.getId();
|
||||
}
|
||||
return scheduleTask1.getId();
|
||||
}
|
||||
scheduleTask.setStatus(ScheduleTaskDto.Status.CREATE);
|
||||
if (scheduleTask.getLockTime() == null) {
|
||||
scheduleTask.setLockTime(getNextExecDate(scheduleTask.getCron()));
|
||||
}
|
||||
scheduleTaskService.save(scheduleTask);
|
||||
return scheduleTask.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(ScheduleTask scheduleTask) {
|
||||
//根据taskId更新
|
||||
LambdaUpdateWrapper<ScheduleTask> lambdaQueryWrapper = new LambdaUpdateWrapper<>();
|
||||
lambdaQueryWrapper.eq(ScheduleTask::getTaskId, scheduleTask.getTaskId());
|
||||
if (StringUtils.isNotBlank(scheduleTask.getCron())) {
|
||||
scheduleTask.setLockTime(getNextExecDate(scheduleTask.getCron()));
|
||||
}
|
||||
scheduleTaskService.update(scheduleTask, lambdaQueryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete(String taskId) {
|
||||
LambdaQueryWrapper<ScheduleTask> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
lambdaQueryWrapper.eq(ScheduleTask::getTaskId, taskId);
|
||||
ScheduleTask scheduleTask = scheduleTaskService.getOne(lambdaQueryWrapper);
|
||||
if (null != scheduleTask) {
|
||||
scheduleTask.setStatus(ScheduleTaskDto.Status.COMPLETE);
|
||||
scheduleTaskService.updateById(scheduleTask);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel(String taskId) {
|
||||
LambdaQueryWrapper<ScheduleTask> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
lambdaQueryWrapper.eq(ScheduleTask::getTaskId, taskId);
|
||||
lambdaQueryWrapper.notIn(ScheduleTask::getStatus, ScheduleTaskDto.Status.CANCEL, ScheduleTaskDto.Status.COMPLETE, ScheduleTaskDto.Status.OVERFLOW_MAX_EXEC_TIMES, ScheduleTaskDto.Status.FAIL);
|
||||
ScheduleTask scheduleTask = scheduleTaskService.getOne(lambdaQueryWrapper, false);
|
||||
if (null != scheduleTask) {
|
||||
scheduleTask.setStatus(ScheduleTaskDto.Status.CANCEL);
|
||||
scheduleTaskService.updateById(scheduleTask);
|
||||
}
|
||||
}
|
||||
|
||||
public static Date getNextExecDate(String cron) {
|
||||
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("Caclulate Date").withSchedule(CronScheduleBuilder.cronSchedule(cron)).build();
|
||||
Date time = trigger.getStartTime();
|
||||
return trigger.getFireTimeAfter(time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.xspaceagi.system.domain.service.SpaceDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.Space;
|
||||
import com.xspaceagi.system.infra.dao.entity.SpaceUser;
|
||||
import com.xspaceagi.system.infra.dao.service.SpaceService;
|
||||
import com.xspaceagi.system.infra.dao.service.SpaceUserService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class SpaceDomainServiceImpl implements SpaceDomainService {
|
||||
|
||||
@Resource
|
||||
private SpaceService spaceService;
|
||||
|
||||
@Resource
|
||||
private SpaceUserService spaceUserService;
|
||||
|
||||
@Override
|
||||
public void add(Space space) {
|
||||
Assert.notNull(space, "space must be non-null");
|
||||
Assert.notNull(space.getName(), "name must be non-null");
|
||||
Assert.notNull(space.getCreatorId(), "creatorId must be non-null");
|
||||
spaceService.save(space);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSpaceUser(SpaceUser spaceUser) {
|
||||
Assert.notNull(spaceUser, "spaceUser must be non-null");
|
||||
Assert.notNull(spaceUser.getSpaceId(), "spaceId must be non-null");
|
||||
Assert.notNull(spaceUser.getUserId(), "userId must be non-null");
|
||||
Assert.notNull(spaceUser.getRole(), "role must be non-null");
|
||||
if (spaceUserService.exists(new QueryWrapper<>(SpaceUser.builder().spaceId(spaceUser.getSpaceId()).userId(spaceUser.getUserId()).build()))) {
|
||||
return;
|
||||
}
|
||||
spaceUserService.save(spaceUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSpaceUser(Long spaceId, Long userId) {
|
||||
Assert.notNull(spaceId, "spaceId must be non-null");
|
||||
Assert.notNull(userId, "userId must be non-null");
|
||||
QueryWrapper<SpaceUser> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("space_id", spaceId);
|
||||
queryWrapper.eq("user_id", userId);
|
||||
spaceUserService.remove(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long spaceId) {
|
||||
Assert.notNull(spaceId, "spaceId must be non-null");
|
||||
spaceService.removeById(spaceId);
|
||||
spaceUserService.remove(new QueryWrapper<>(SpaceUser.builder().spaceId(spaceId).build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Space space) {
|
||||
Assert.notNull(space, "space must be non-null");
|
||||
Assert.notNull(space.getId(), "id must be non-null");
|
||||
spaceService.updateById(space);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSpaceUser(SpaceUser spaceUser) {
|
||||
Assert.notNull(spaceUser, "spaceUser must be non-null");
|
||||
Assert.notNull(spaceUser.getId(), "id must be non-null");
|
||||
spaceUserService.updateById(spaceUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Space queryById(Long spaceId) {
|
||||
return spaceService.getById(spaceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Space> queryListByIds(List<Long> spaceIds) {
|
||||
Assert.notNull(spaceIds, "spaceIds must be non-null");
|
||||
if (spaceIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
LambdaQueryWrapper<Space> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.in(Space::getId, spaceIds);
|
||||
return spaceService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpaceUser> querySpaceUserList(Long spaceId) {
|
||||
Assert.notNull(spaceId, "spaceId must be non-null");
|
||||
LambdaQueryWrapper<SpaceUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SpaceUser::getSpaceId, spaceId);
|
||||
return spaceUserService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpaceUser> querySpaceUserListByUserId(Long userId) {
|
||||
Assert.notNull(userId, "userId must be non-null");
|
||||
LambdaQueryWrapper<SpaceUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SpaceUser::getUserId, userId);
|
||||
return spaceUserService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpaceUser querySpaceUser(Long spaceId, Long userId) {
|
||||
Assert.notNull(spaceId, "spaceId must be non-null");
|
||||
Assert.notNull(userId, "userId must be non-null");
|
||||
return spaceUserService.getOne(new QueryWrapper<>(SpaceUser.builder().spaceId(spaceId).userId(userId).build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public void transfer(Long spaceId, Long targetUserId) {
|
||||
Assert.notNull(spaceId, "spaceId must be non-null");
|
||||
Assert.notNull(targetUserId, "targetUserId must be non-null");
|
||||
Space space = spaceService.getById(spaceId);
|
||||
Long originUserId = space.getCreatorId();
|
||||
space.setCreatorId(targetUserId);
|
||||
spaceService.updateById(space);
|
||||
deleteSpaceUser(spaceId, targetUserId);
|
||||
deleteSpaceUser(spaceId, originUserId);
|
||||
addSpaceUser(SpaceUser.builder().spaceId(spaceId).userId(targetUserId).role(SpaceUser.Role.Owner).build());
|
||||
addSpaceUser(SpaceUser.builder().spaceId(spaceId).userId(originUserId).role(SpaceUser.Role.Admin).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countTotalSpaces() {
|
||||
return spaceService.count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long countUserCreatedTeamSpaces(Long userId) {
|
||||
LambdaQueryWrapper<Space> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(Space::getCreatorId, userId);
|
||||
queryWrapper.eq(Space::getType, Space.Type.Team);
|
||||
return spaceService.count(queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.xspaceagi.system.domain.service.SysDataPermissionDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysDataPermission;
|
||||
import com.xspaceagi.system.infra.dao.service.SysDataPermissionService;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.enums.PermissionTargetTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.YnEnum;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class SysDataPermissionDomainServiceImpl implements SysDataPermissionDomainService {
|
||||
|
||||
@Resource
|
||||
private SysDataPermissionService sysDataPermissionService;
|
||||
|
||||
@Override
|
||||
public void add(SysDataPermission dataPermission, UserContext userContext) {
|
||||
if (PermissionTargetTypeEnum.getByCode(dataPermission.getTargetType()) == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemDataPermissionTargetTypeInvalid);
|
||||
}
|
||||
if (dataPermission.getTargetId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemDataPermissionTargetIdInvalid);
|
||||
}
|
||||
dataPermission.setTenantId(userContext.getTenantId());
|
||||
dataPermission.setCreatorId(userContext.getUserId());
|
||||
dataPermission.setCreator(userContext.getUserName());
|
||||
dataPermission.setYn(YnEnum.Y.getKey());
|
||||
sysDataPermissionService.save(dataPermission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Long id, SysDataPermission dataPermission, UserContext userContext) {
|
||||
SysDataPermission updateObject = new SysDataPermission();
|
||||
updateObject.setId(id);
|
||||
updateObject.setModelIds(dataPermission.getModelIds());
|
||||
updateObject.setTokenLimit(dataPermission.getTokenLimit());
|
||||
updateObject.setMaxSpaceCount(dataPermission.getMaxSpaceCount());
|
||||
updateObject.setMaxAgentCount(dataPermission.getMaxAgentCount());
|
||||
updateObject.setMaxPageAppCount(dataPermission.getMaxPageAppCount());
|
||||
updateObject.setMaxKnowledgeCount(dataPermission.getMaxKnowledgeCount());
|
||||
updateObject.setKnowledgeStorageLimitGb(dataPermission.getKnowledgeStorageLimitGb());
|
||||
updateObject.setMaxDataTableCount(dataPermission.getMaxDataTableCount());
|
||||
updateObject.setMaxScheduledTaskCount(dataPermission.getMaxScheduledTaskCount());
|
||||
//updateObject.setAllowApiExternalCall(dataPermission.getAllowApiExternalCall());
|
||||
updateObject.setAgentComputerMemoryGb(dataPermission.getAgentComputerMemoryGb());
|
||||
updateObject.setAgentComputerSwapGb(dataPermission.getAgentComputerSwapGb());
|
||||
updateObject.setAgentComputerCpuCores(dataPermission.getAgentComputerCpuCores());
|
||||
updateObject.setAgentFileStorageDays(dataPermission.getAgentFileStorageDays());
|
||||
updateObject.setAgentDailyPromptLimit(dataPermission.getAgentDailyPromptLimit());
|
||||
updateObject.setPageDailyPromptLimit(dataPermission.getPageDailyPromptLimit());
|
||||
updateObject.setModifierId(userContext.getUserId());
|
||||
updateObject.setModifier(userContext.getUserName());
|
||||
sysDataPermissionService.updateById(updateObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysDataPermission getByTarget(PermissionTargetTypeEnum targetType, Long targetId) {
|
||||
if (targetType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemDataPermissionTargetTypeInvalid);
|
||||
}
|
||||
if (targetId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemDataPermissionTargetIdInvalid);
|
||||
}
|
||||
return sysDataPermissionService.getOne(Wrappers.<SysDataPermission>lambdaQuery()
|
||||
.eq(SysDataPermission::getTargetType, targetType.getCode())
|
||||
.eq(SysDataPermission::getTargetId, targetId)
|
||||
.eq(SysDataPermission::getYn, YnEnum.Y.getKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysDataPermission> getByTargetList(PermissionTargetTypeEnum targetType, List<Long> targetIds) {
|
||||
if (targetType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemDataPermissionTargetTypeInvalid);
|
||||
}
|
||||
if (targetIds == null || targetIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return sysDataPermissionService.list(Wrappers.<SysDataPermission>lambdaQuery()
|
||||
.eq(SysDataPermission::getTargetType, targetType.getCode())
|
||||
.in(SysDataPermission::getTargetId, targetIds)
|
||||
.eq(SysDataPermission::getYn, YnEnum.Y.getKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByTaret(PermissionTargetTypeEnum targetType, Long targetId, UserContext userContext) {
|
||||
LambdaUpdateWrapper<SysDataPermission> wrapper = Wrappers.<SysDataPermission>lambdaUpdate()
|
||||
.eq(SysDataPermission::getTargetType, targetType.getCode())
|
||||
.eq(SysDataPermission::getTargetId, targetId);
|
||||
sysDataPermissionService.remove(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.domain.model.ResourceNode;
|
||||
import com.xspaceagi.system.domain.service.SysFlattenService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SysFlattenServiceImpl implements SysFlattenService {
|
||||
|
||||
@Override
|
||||
public List<MenuNode> flattenMenuTree(List<MenuNode> tree) {
|
||||
if (CollectionUtils.isEmpty(tree)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<MenuNode> result = new ArrayList<>();
|
||||
for (MenuNode node : tree) {
|
||||
if (node.getId() != null) {
|
||||
result.add(node);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(node.getChildren())) {
|
||||
result.addAll(flattenMenuTree(node.getChildren()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceNode> flattenResourceTree(List<ResourceNode> tree) {
|
||||
if (CollectionUtils.isEmpty(tree)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<ResourceNode> result = new ArrayList<>();
|
||||
for (ResourceNode node : tree) {
|
||||
if (node.getId() != null) {
|
||||
result.add(node);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(node.getChildren())) {
|
||||
result.addAll(flattenResourceTree(node.getChildren()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.xspaceagi.system.domain.model.GroupBindMenuModel;
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.domain.model.ResourceNode;
|
||||
import com.xspaceagi.system.domain.model.SortIndex;
|
||||
import com.xspaceagi.system.domain.service.*;
|
||||
import com.xspaceagi.system.infra.dao.entity.*;
|
||||
import com.xspaceagi.system.infra.dao.service.SysGroupMenuService;
|
||||
import com.xspaceagi.system.infra.dao.service.SysGroupService;
|
||||
import com.xspaceagi.system.infra.dao.service.SysUserGroupService;
|
||||
import com.xspaceagi.system.infra.dao.service.UserService;
|
||||
import com.xspaceagi.system.sdk.service.dto.TokenLimit;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.enums.*;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
|
||||
import com.xspaceagi.system.spec.utils.CodeGeneratorUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户组领域服务实现
|
||||
*/
|
||||
@Service
|
||||
public class SysGroupDomainServiceImpl implements SysGroupDomainService {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
@Resource
|
||||
private SysGroupService sysGroupService;
|
||||
@Resource
|
||||
private SysUserGroupService sysUserGroupService;
|
||||
@Resource
|
||||
private SysGroupMenuService sysGroupMenuService;
|
||||
@Resource
|
||||
private SysMenuDomainService sysMenuDomainService;
|
||||
@Resource
|
||||
private SysResourceDomainService sysResourceDomainService;
|
||||
@Resource
|
||||
private SysDataPermissionDomainService sysDataPermissionDomainService;
|
||||
@Resource
|
||||
private SysSubjectPermissionDomainService sysSubjectPermissionDomainService;
|
||||
@Resource
|
||||
private MenuBindResourceHelper menuBindResourceHelper;
|
||||
|
||||
private void normalizeGroup(SysGroup group) {
|
||||
group.setCode(StringUtils.trim(group.getCode()));
|
||||
group.setName(StringUtils.trim(group.getName()));
|
||||
group.setDescription(StringUtils.trim(group.getDescription()));
|
||||
|
||||
if (StringUtils.isNotBlank(group.getCode()) && !group.getCode().matches("^[a-zA-Z][a-zA-Z0-9_]*$")) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacCodeFormatInvalid);
|
||||
}
|
||||
if (StringUtils.length(group.getCode()) > 100) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacCodeLengthExceeded);
|
||||
}
|
||||
if (StringUtils.length(group.getName()) > 50) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacNameLengthExceeded);
|
||||
}
|
||||
if (StringUtils.length(group.getDescription()) > 500) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacDescLengthExceeded);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addGroup(SysGroup group, UserContext userContext) {
|
||||
if (StringUtils.isBlank(group.getName())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "名称");
|
||||
}
|
||||
if (group.getMaxUserCount() == null) {
|
||||
group.setMaxUserCount(-1);
|
||||
}
|
||||
|
||||
// 如果编码为空,根据名称自动生成编码
|
||||
if (StringUtils.isBlank(group.getCode())) {
|
||||
String generatedCode = CodeGeneratorUtil.generateUniqueCodeFromName(
|
||||
group.getName(),
|
||||
"group_",
|
||||
code -> queryGroupByCode(code) != null
|
||||
);
|
||||
group.setCode(generatedCode);
|
||||
}
|
||||
|
||||
normalizeGroup(group);
|
||||
|
||||
SysGroup exist = queryGroupByCode(group.getCode());
|
||||
if (exist != null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupCodeDuplicate);
|
||||
}
|
||||
|
||||
if (group.getSource() == null) {
|
||||
group.setSource(SourceEnum.CUSTOM.getCode());
|
||||
}
|
||||
if (group.getStatus() == null) {
|
||||
group.setStatus(StatusEnum.ENABLED.getCode());
|
||||
}
|
||||
if (group.getSortIndex() == null) {
|
||||
group.setSortIndex(0);
|
||||
}
|
||||
group.setTenantId(userContext.getTenantId());
|
||||
group.setCreatorId(userContext.getUserId());
|
||||
group.setCreator(userContext.getUserName());
|
||||
group.setYn(YnEnum.Y.getKey());
|
||||
sysGroupService.save(group);
|
||||
|
||||
if (group.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacSaveFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateGroup(SysGroup group, UserContext userContext) {
|
||||
if (group.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "ID");
|
||||
}
|
||||
if (group.getMaxUserCount() == null) {
|
||||
group.setMaxUserCount(-1);
|
||||
}
|
||||
normalizeGroup(group);
|
||||
|
||||
SysGroup exist = queryGroupById(group.getId());
|
||||
if (exist == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupNotFound);
|
||||
}
|
||||
if (SourceEnum.SYSTEM.getCode().equals(exist.getSource())) {
|
||||
if (group.getCode() != null && !group.getCode().equals(exist.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemBuiltinGroupCodeImmutable);
|
||||
}
|
||||
if (group.getName() != null && !group.getName().equals(exist.getName())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemBuiltinGroupNameImmutable);
|
||||
}
|
||||
if (group.getStatus() != null && !group.getStatus().equals(StatusEnum.ENABLED.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemBuiltinGroupCannotDisable);
|
||||
}
|
||||
}
|
||||
|
||||
if (group.getMaxUserCount() != null && group.getMaxUserCount() > -1) {
|
||||
// 校验最大用户数不能小于当前已绑定用户数
|
||||
long boundUserCount = sysUserGroupService.count(
|
||||
Wrappers.<SysUserGroup>lambdaQuery()
|
||||
.eq(SysUserGroup::getGroupId, group.getId())
|
||||
.eq(SysUserGroup::getYn, YnEnum.Y.getKey()));
|
||||
if (group.getMaxUserCount() < boundUserCount) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupMaxUsersBelowBound,
|
||||
String.valueOf(boundUserCount));
|
||||
}
|
||||
}
|
||||
|
||||
boolean statusChanged = group.getStatus() != null
|
||||
&& !Objects.equals(exist.getStatus(), group.getStatus());
|
||||
|
||||
group.setModifierId(userContext.getUserId());
|
||||
group.setModifier(userContext.getUserName());
|
||||
sysGroupService.updateById(group);
|
||||
return statusChanged;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindDataPermission(Long groupId, SysDataPermission dataPermission, UserContext userContext) {
|
||||
if (groupId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户组ID");
|
||||
}
|
||||
SysGroup exist = queryGroupById(groupId);
|
||||
if (exist == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupNotFound);
|
||||
}
|
||||
if (dataPermission == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacDataPermissionRequired);
|
||||
}
|
||||
|
||||
if (CollectionUtils.isNotEmpty(dataPermission.getModelIds())) {
|
||||
List<Long> modelIds = dataPermission.getModelIds().stream()
|
||||
.filter(modelId -> Objects.nonNull(modelId) && modelId >= 1)
|
||||
.collect(Collectors.toList());
|
||||
dataPermission.setModelIds(modelIds);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(dataPermission.getAgentIds())) {
|
||||
List<Long> agentIds = dataPermission.getAgentIds().stream()
|
||||
.filter(agentId -> Objects.nonNull(agentId) && agentId >= 1)
|
||||
.toList();
|
||||
dataPermission.setAgentIds(agentIds);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(dataPermission.getPageAgentIds())) {
|
||||
List<Long> pageAgentIds = dataPermission.getPageAgentIds().stream()
|
||||
.filter(pageAgentId -> Objects.nonNull(pageAgentId) && pageAgentId >= 1)
|
||||
.toList();
|
||||
dataPermission.setPageAgentIds(pageAgentIds);
|
||||
}
|
||||
if (dataPermission.getOpenApiConfigMap() != null && !dataPermission.getOpenApiConfigMap().isEmpty()) {
|
||||
Map<String, String> normalizedConfig = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> entry : dataPermission.getOpenApiConfigMap().entrySet()) {
|
||||
if (StringUtils.isBlank(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
normalizedConfig.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
dataPermission.setOpenApiConfigMap(normalizedConfig);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(dataPermission.getKnowledgeIds())) {
|
||||
List<Long> knowledgeIds = dataPermission.getKnowledgeIds().stream()
|
||||
.filter(knowledgeId -> Objects.nonNull(knowledgeId) && knowledgeId >= 1)
|
||||
.toList();
|
||||
dataPermission.setKnowledgeIds(knowledgeIds);
|
||||
}
|
||||
|
||||
if (dataPermission.getTokenLimit() != null) {
|
||||
if (dataPermission.getTokenLimit().getLimitPerDay() < -1) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacTokenLimitMinInvalid);
|
||||
}
|
||||
} else {
|
||||
dataPermission.setTokenLimit(new TokenLimit(-1L));
|
||||
}
|
||||
|
||||
dataPermission.setTargetType(PermissionTargetTypeEnum.GROUP.getCode());
|
||||
dataPermission.setTargetId(groupId);
|
||||
SysDataPermission oldDataPermission = sysDataPermissionDomainService.getByTarget(PermissionTargetTypeEnum.GROUP, groupId);
|
||||
if (oldDataPermission != null) {
|
||||
sysDataPermissionDomainService.update(oldDataPermission.getId(), dataPermission, userContext);
|
||||
} else {
|
||||
sysDataPermissionDomainService.add(dataPermission, userContext);
|
||||
}
|
||||
|
||||
// 全量覆盖写入
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum.GROUP, groupId,
|
||||
PermissionSubjectTypeEnum.MODEL, dataPermission.getModelIds(), userContext);
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum.GROUP, groupId,
|
||||
PermissionSubjectTypeEnum.AGENT, dataPermission.getAgentIds(), userContext);
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum.GROUP, groupId,
|
||||
PermissionSubjectTypeEnum.PAGE, dataPermission.getPageAgentIds(), userContext);
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectKeyConfig(PermissionTargetTypeEnum.GROUP, groupId,
|
||||
PermissionSubjectTypeEnum.OPEN_API, dataPermission.getOpenApiConfigMap(), userContext);
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum.GROUP, groupId,
|
||||
PermissionSubjectTypeEnum.KNOWLEDGE, dataPermission.getKnowledgeIds(), userContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void batchUpdateGroupSort(List<SortIndex> sortIndexList, UserContext userContext) {
|
||||
if (CollectionUtils.isEmpty(sortIndexList)) {
|
||||
return;
|
||||
}
|
||||
for (SortIndex item : sortIndexList) {
|
||||
if (item == null || item.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户组ID");
|
||||
}
|
||||
SysGroup exist = queryGroupById(item.getId());
|
||||
if (exist == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupNotFoundWithRowId, item.getId());
|
||||
}
|
||||
if (item.getSortIndex() == null) {
|
||||
continue;
|
||||
}
|
||||
SysGroup updateGroup = new SysGroup();
|
||||
updateGroup.setId(item.getId());
|
||||
updateGroup.setSortIndex(item.getSortIndex());
|
||||
updateGroup.setModifierId(userContext.getUserId());
|
||||
updateGroup.setModifier(userContext.getUserName());
|
||||
sysGroupService.updateById(updateGroup);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteGroup(Long groupId, UserContext userContext) {
|
||||
if (groupId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "ID");
|
||||
}
|
||||
SysGroup exist = queryGroupById(groupId);
|
||||
if (exist == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupNotFound);
|
||||
}
|
||||
if (SourceEnum.SYSTEM.getCode().equals(exist.getSource())) {
|
||||
//throw new BizException("系统内置用户组不能删除");
|
||||
}
|
||||
|
||||
// 删除用户组
|
||||
sysGroupService.removeById(groupId);
|
||||
// 删除数据权限
|
||||
sysDataPermissionDomainService.deleteByTaret(PermissionTargetTypeEnum.GROUP, groupId, userContext);
|
||||
// 删除组绑定的用户
|
||||
sysUserGroupService.remove(Wrappers.<SysUserGroup>lambdaUpdate().eq(SysUserGroup::getGroupId, groupId));
|
||||
// 删除组绑定的菜单
|
||||
sysGroupMenuService.remove(Wrappers.<SysGroupMenu>lambdaUpdate().eq(SysGroupMenu::getGroupId, groupId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysGroup queryGroupById(Long groupId) {
|
||||
LambdaQueryWrapper<SysGroup> wrapper = Wrappers.<SysGroup>lambdaQuery().eq(SysGroup::getId, groupId).eq(SysGroup::getYn, YnEnum.Y.getKey());
|
||||
return sysGroupService.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysGroup> queryGroupListByIds(List<Long> groupIds) {
|
||||
if (CollectionUtils.isEmpty(groupIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Long> ids = groupIds.stream().filter(Objects::nonNull).distinct().toList();
|
||||
return sysGroupService.list(Wrappers.<SysGroup>lambdaQuery()
|
||||
.in(SysGroup::getId, ids)
|
||||
.eq(SysGroup::getYn, YnEnum.Y.getKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysGroup queryGroupByCode(String groupCode) {
|
||||
LambdaQueryWrapper<SysGroup> wrapper = Wrappers.<SysGroup>lambdaQuery().eq(SysGroup::getCode, groupCode).eq(SysGroup::getYn, YnEnum.Y.getKey());
|
||||
return sysGroupService.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysGroup> queryGroupList(SysGroup group) {
|
||||
LambdaQueryWrapper<SysGroup> wrapper = Wrappers.<SysGroup>lambdaQuery()
|
||||
.eq(StringUtils.isNotBlank(group.getCode()), SysGroup::getCode, group.getCode())
|
||||
.like(StringUtils.isNotBlank(group.getName()), SysGroup::getName, group.getName())
|
||||
.eq(Objects.nonNull(group.getSource()), SysGroup::getSource, group.getSource())
|
||||
.eq(Objects.nonNull(group.getStatus()), SysGroup::getStatus, group.getStatus())
|
||||
.eq(SysGroup::getYn, YnEnum.Y.getKey()).orderByAsc(SysGroup::getSortIndex);
|
||||
return sysGroupService.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> getUserListByGroupId(Long groupId) {
|
||||
LambdaQueryWrapper<SysUserGroup> wrapper = Wrappers.<SysUserGroup>lambdaQuery().eq(SysUserGroup::getGroupId, groupId).eq(SysUserGroup::getYn, YnEnum.Y.getKey());
|
||||
List<SysUserGroup> list = sysUserGroupService.list(wrapper);
|
||||
if (CollectionUtils.isEmpty(list)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Long> userIdList = list.stream().map(SysUserGroup::getUserId).toList();
|
||||
List<User> users = userService.listByIds(userIdList);
|
||||
return users;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<User> getUserPageByGroupId(Long groupId, String userName, long pageNo, long pageSize) {
|
||||
List<Long> matchingUserIds = null;
|
||||
if (StringUtils.isNotBlank(userName)) {
|
||||
List<User> matchingUsers = userService.list(Wrappers.<User>lambdaQuery()
|
||||
.and(w -> w.like(User::getNickName, userName).or().like(User::getUserName, userName))
|
||||
.select(User::getId));
|
||||
matchingUserIds = matchingUsers.stream().map(User::getId).filter(Objects::nonNull).toList();
|
||||
if (CollectionUtils.isEmpty(matchingUserIds)) {
|
||||
return new Page<>(pageNo, pageSize, 0);
|
||||
}
|
||||
}
|
||||
LambdaQueryWrapper<SysUserGroup> wrapper = Wrappers.<SysUserGroup>lambdaQuery()
|
||||
.eq(SysUserGroup::getGroupId, groupId)
|
||||
.eq(SysUserGroup::getYn, YnEnum.Y.getKey())
|
||||
.in(CollectionUtils.isNotEmpty(matchingUserIds), SysUserGroup::getUserId, matchingUserIds)
|
||||
.orderByAsc(SysUserGroup::getUserId);
|
||||
IPage<SysUserGroup> userGroupPage = sysUserGroupService.page(new Page<>(pageNo, pageSize), wrapper);
|
||||
if (CollectionUtils.isEmpty(userGroupPage.getRecords())) {
|
||||
return new Page<>(pageNo, pageSize, userGroupPage.getTotal());
|
||||
}
|
||||
List<Long> pageUserIds = userGroupPage.getRecords().stream().map(SysUserGroup::getUserId).filter(Objects::nonNull).toList();
|
||||
List<User> users = userService.listByIds(pageUserIds);
|
||||
Map<Long, User> userMap = users.stream().collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a));
|
||||
List<User> orderedUsers = pageUserIds.stream().map(userMap::get).filter(Objects::nonNull).toList();
|
||||
Page<User> page = new Page<>(pageNo, pageSize, userGroupPage.getTotal());
|
||||
page.setRecords(orderedUsers);
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUsersByGroupId(Long groupId) {
|
||||
LambdaQueryWrapper<SysUserGroup> wrapper = Wrappers.<SysUserGroup>lambdaQuery()
|
||||
.eq(SysUserGroup::getGroupId, groupId)
|
||||
.eq(SysUserGroup::getYn, YnEnum.Y.getKey());
|
||||
return sysUserGroupService.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getUserIdsByGroupId(Long groupId) {
|
||||
LambdaQueryWrapper<SysUserGroup> wrapper = Wrappers.<SysUserGroup>lambdaQuery()
|
||||
.eq(SysUserGroup::getGroupId, groupId)
|
||||
.eq(SysUserGroup::getYn, YnEnum.Y.getKey())
|
||||
.select(SysUserGroup::getUserId);
|
||||
List<SysUserGroup> list = sysUserGroupService.list(wrapper);
|
||||
if (CollectionUtils.isEmpty(list)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return list.stream().map(SysUserGroup::getUserId).filter(Objects::nonNull).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysGroup> queryGroupListByUserId(Long userId) {
|
||||
// 先查询用户和组关联
|
||||
LambdaQueryWrapper<SysUserGroup> wrapper = Wrappers.<SysUserGroup>lambdaQuery()
|
||||
.eq(SysUserGroup::getUserId, userId)
|
||||
.eq(SysUserGroup::getYn, YnEnum.Y.getKey());
|
||||
List<SysUserGroup> userGroups = sysUserGroupService.list(wrapper);
|
||||
|
||||
if (CollectionUtils.isEmpty(userGroups)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 获取用户组ID列表
|
||||
List<Long> groupIds = userGroups.stream().map(SysUserGroup::getGroupId).collect(Collectors.toList());
|
||||
|
||||
// 查询用户组信息
|
||||
LambdaQueryWrapper<SysGroup> groupWrapper = new LambdaQueryWrapper<>();
|
||||
groupWrapper.in(SysGroup::getId, groupIds)
|
||||
.eq(SysGroup::getYn, YnEnum.Y.getKey())
|
||||
.orderByAsc(SysGroup::getSortIndex);
|
||||
return sysGroupService.list(groupWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void groupBindUser(Long groupId, List<Long> userIds, UserContext userContext) {
|
||||
if (groupId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "组ID");
|
||||
}
|
||||
SysGroup sysGroup = queryGroupById(groupId);
|
||||
if (sysGroup == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupNotFoundWithGroupId, groupId);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(userIds)
|
||||
&& sysGroup.getMaxUserCount() != null
|
||||
&& sysGroup.getMaxUserCount() > -1
|
||||
&& userIds.size() > sysGroup.getMaxUserCount()) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupUserLimitExceeded);
|
||||
}
|
||||
// 校验用户是否存在
|
||||
if (CollectionUtils.isNotEmpty(userIds)) {
|
||||
Set<Long> distinctUserIds = new HashSet<>(userIds);
|
||||
List<User> users = userService.listByIds(distinctUserIds);
|
||||
List<Long> existUserIds = users.stream().map(User::getId).toList();
|
||||
|
||||
userIds.forEach(userId -> {
|
||||
if (!existUserIds.contains(userId)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacUserNotFoundWithId, userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 物理删除原绑定关系
|
||||
LambdaUpdateWrapper<SysUserGroup> wrapper = Wrappers.<SysUserGroup>lambdaUpdate().eq(SysUserGroup::getGroupId, groupId);
|
||||
sysUserGroupService.remove(wrapper);
|
||||
|
||||
if (CollectionUtils.isEmpty(userIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SysUserGroup> userGroupList = userIds.stream().map(userId -> {
|
||||
SysUserGroup userGroup = new SysUserGroup();
|
||||
userGroup.setUserId(userId);
|
||||
userGroup.setGroupId(groupId);
|
||||
userGroup.setTenantId(userContext.getTenantId());
|
||||
userGroup.setCreatorId(userContext.getUserId());
|
||||
userGroup.setCreator(userContext.getUserName());
|
||||
userGroup.setYn(YnEnum.Y.getKey());
|
||||
return userGroup;
|
||||
}).toList();
|
||||
|
||||
if (!CollectionUtils.isEmpty(userGroupList)) {
|
||||
sysUserGroupService.saveBatch(userGroupList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void groupAddUser(Long groupId, Long userId, UserContext userContext) {
|
||||
if (groupId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "组ID");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户ID");
|
||||
}
|
||||
SysGroup sysGroup = queryGroupById(groupId);
|
||||
if (sysGroup == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupNotFoundWithGroupId, groupId);
|
||||
}
|
||||
User user = userService.getById(userId);
|
||||
if (user == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacUserNotFoundWithId, userId);
|
||||
}
|
||||
// 检查是否已绑定,避免重复插入
|
||||
LambdaQueryWrapper<SysUserGroup> queryWrapper = Wrappers.<SysUserGroup>lambdaQuery()
|
||||
.eq(SysUserGroup::getGroupId, groupId)
|
||||
.eq(SysUserGroup::getUserId, userId)
|
||||
.eq(SysUserGroup::getYn, YnEnum.Y.getKey());
|
||||
if (sysUserGroupService.count(queryWrapper) > 0) {
|
||||
return;
|
||||
}
|
||||
// 校验用户数量限制
|
||||
if (sysGroup.getMaxUserCount() != null && sysGroup.getMaxUserCount() > -1) {
|
||||
long currentCount = countUsersByGroupId(groupId);
|
||||
if (currentCount >= sysGroup.getMaxUserCount()) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupUserLimitExceeded);
|
||||
}
|
||||
}
|
||||
SysUserGroup userGroup = new SysUserGroup();
|
||||
userGroup.setUserId(userId);
|
||||
userGroup.setGroupId(groupId);
|
||||
userGroup.setTenantId(userContext.getTenantId());
|
||||
userGroup.setCreatorId(userContext.getUserId());
|
||||
userGroup.setCreator(userContext.getUserName());
|
||||
userGroup.setYn(YnEnum.Y.getKey());
|
||||
sysUserGroupService.save(userGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void groupRemoveUser(Long groupId, Long userId, UserContext userContext) {
|
||||
if (groupId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "组ID");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户ID");
|
||||
}
|
||||
LambdaUpdateWrapper<SysUserGroup> wrapper = Wrappers.<SysUserGroup>lambdaUpdate()
|
||||
.eq(SysUserGroup::getGroupId, groupId)
|
||||
.eq(SysUserGroup::getUserId, userId);
|
||||
sysUserGroupService.remove(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userBindGroup(Long userId, List<Long> groupIds, UserContext userContext) {
|
||||
if (userId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户ID");
|
||||
}
|
||||
User user = userService.getById(userId);
|
||||
if (user == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacUserNotFoundWithId, userId);
|
||||
}
|
||||
|
||||
// 校验组是否存在
|
||||
if (CollectionUtils.isNotEmpty(groupIds)) {
|
||||
Set<Long> distinctGroupIds = new HashSet<>(groupIds);
|
||||
List<SysGroup> groups = sysGroupService.listByIds(distinctGroupIds);
|
||||
if (CollectionUtils.isEmpty(groups)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupNotFound);
|
||||
}
|
||||
Set<Long> existGroupIds = groups.stream().map(SysGroup::getId).collect(Collectors.toSet());
|
||||
groupIds.forEach(groupId -> {
|
||||
if (!existGroupIds.contains(groupId)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupNotFoundWithGroupIdAlt, groupId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 物理删除原绑定关系
|
||||
LambdaUpdateWrapper<SysUserGroup> wrapper = Wrappers.<SysUserGroup>lambdaUpdate().eq(SysUserGroup::getUserId, userId);
|
||||
sysUserGroupService.remove(wrapper);
|
||||
|
||||
if (CollectionUtils.isEmpty(groupIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SysUserGroup> userGroupList = groupIds.stream().map(groupId -> {
|
||||
SysUserGroup userGroup = new SysUserGroup();
|
||||
userGroup.setUserId(userId);
|
||||
userGroup.setGroupId(groupId);
|
||||
userGroup.setTenantId(userContext.getTenantId());
|
||||
userGroup.setCreatorId(userContext.getUserId());
|
||||
userGroup.setCreator(userContext.getUserName());
|
||||
userGroup.setYn(YnEnum.Y.getKey());
|
||||
return userGroup;
|
||||
}).toList();
|
||||
|
||||
if (!CollectionUtils.isEmpty(userGroupList)) {
|
||||
sysUserGroupService.saveBatch(userGroupList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindMenu(GroupBindMenuModel model, UserContext userContext) {
|
||||
if (model == null || model.getGroupId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户组ID");
|
||||
}
|
||||
|
||||
Long groupId = model.getGroupId();
|
||||
LambdaUpdateWrapper<SysGroupMenu> deleteWrapper = Wrappers.<SysGroupMenu>lambdaUpdate().eq(SysGroupMenu::getGroupId, groupId);
|
||||
sysGroupMenuService.remove(deleteWrapper);
|
||||
|
||||
List<MenuNode> menuBindResourceList = model.getMenuBindResourceList();
|
||||
if (CollectionUtils.isEmpty(menuBindResourceList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SysMenu> allMenus = sysMenuDomainService.queryMenuList(null);
|
||||
List<SysResource> allResources = sysResourceDomainService.queryResourceList(null);
|
||||
MenuBindResourceHelper.MenuResourceMaps maps = menuBindResourceHelper.buildMenuAndResourceMaps(allMenus, allResources);
|
||||
Map<Long, Integer> menuBindTypeMap = menuBindResourceHelper.buildAndPropagateMenuBindTypeMap(menuBindResourceList, maps.menuChildrenMap);
|
||||
|
||||
// menuBindType=1(ALL) 表示该节点下所有子菜单均被绑定,但每个子菜单的资源绑定可能不同,需存每个子菜单并各自存 resource_tree_json
|
||||
List<SysGroupMenu> groupMenus = new ArrayList<>();
|
||||
for (MenuNode menuNode : menuBindResourceList) {
|
||||
Long menuId = menuNode.getId();
|
||||
if (menuId == null) {
|
||||
continue;
|
||||
}
|
||||
if (menuId != 0L && !maps.menuMap.containsKey(menuId)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacMenuIdNotFound, menuId);
|
||||
}
|
||||
Integer menuBindType = menuBindTypeMap.getOrDefault(menuId, BindTypeEnum.NONE.getCode());
|
||||
if (menuBindType == null || BindTypeEnum.NONE.getCode().equals(menuBindType)) {
|
||||
continue;
|
||||
}
|
||||
SysGroupMenu groupMenu = new SysGroupMenu();
|
||||
groupMenu.setGroupId(groupId);
|
||||
groupMenu.setMenuId(menuId);
|
||||
groupMenu.setMenuBindType(menuBindType);
|
||||
List<ResourceNode> processedResourceTree = menuBindResourceHelper.processResourceTreeForMenu(
|
||||
menuId, menuNode.getResourceTree(), maps.resourceMap, maps.resourceChildrenMap);
|
||||
groupMenu.setResourceTreeJson(CollectionUtils.isEmpty(processedResourceTree)
|
||||
? null : JsonSerializeUtil.toJSONString(processedResourceTree));
|
||||
groupMenu.setTenantId(userContext.getTenantId());
|
||||
groupMenu.setCreatorId(userContext.getUserId());
|
||||
groupMenu.setCreator(userContext.getUserName());
|
||||
groupMenu.setYn(YnEnum.Y.getKey());
|
||||
groupMenus.add(groupMenu);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(groupMenus)) {
|
||||
sysGroupMenuService.saveBatch(groupMenus);
|
||||
}
|
||||
}
|
||||
|
||||
public List<MenuNode> getMenuTreeByGroupId(Long groupId) {
|
||||
if (groupId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户组ID");
|
||||
}
|
||||
|
||||
// 查询用户组绑定的菜单关系
|
||||
List<SysGroupMenu> groupMenus = sysGroupMenuService.list(Wrappers.<SysGroupMenu>lambdaQuery()
|
||||
.eq(SysGroupMenu::getGroupId, groupId)
|
||||
.eq(SysGroupMenu::getYn, YnEnum.Y.getKey()));
|
||||
// menuId -> 绑定记录(包含 menu_id = 0 的 root 绑定)
|
||||
Map<Long, SysGroupMenu> groupMenuMap = CollectionUtils.isEmpty(groupMenus)
|
||||
? new HashMap<>()
|
||||
: groupMenus.stream()
|
||||
.filter(rm -> rm.getMenuId() != null)
|
||||
.collect(Collectors.toMap(SysGroupMenu::getMenuId, rm -> rm, (a, b) -> a));
|
||||
|
||||
// 查询所有有效菜单
|
||||
List<SysMenu> allMenus = sysMenuDomainService.queryMenuList(null);
|
||||
if (CollectionUtils.isEmpty(allMenus)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 先把 root 菜单(menuId = 0)加入到全量菜单中,让其参与自上而下、自下而上的打标逻辑
|
||||
boolean hasRootMenu = allMenus.stream()
|
||||
.anyMatch(menu -> menu.getId() != null && menu.getId() == 0L);
|
||||
if (!hasRootMenu) {
|
||||
SysMenu rootMenu = MenuBindResourceHelper.getRootMenu();
|
||||
allMenus.add(rootMenu);
|
||||
}
|
||||
|
||||
// 查询所有有效资源(用于构建完整资源树)
|
||||
List<SysResource> allResources = sysResourceDomainService.queryResourceList(null);
|
||||
|
||||
// 基于绑定记录 + 全量菜单/资源构建基础权限信息(含自上而下传播)
|
||||
List<MenuNode> menuNodes = MenuAuthHelper.buildMenuListWithAuth(
|
||||
groupMenus,
|
||||
SysGroupMenu::getMenuId,
|
||||
SysGroupMenu::getMenuBindType,
|
||||
SysGroupMenu::getResourceTreeJson,
|
||||
allMenus,
|
||||
allResources,
|
||||
sysMenuDomainService
|
||||
);
|
||||
|
||||
// 填充菜单信息和资源详细信息
|
||||
Map<Long, SysMenu> allMenuMap = allMenus.stream().collect(Collectors.toMap(SysMenu::getId, m -> m));
|
||||
Map<Long, SysResource> allResourceMap = CollectionUtils.isEmpty(allResources)
|
||||
? new HashMap<>()
|
||||
: allResources.stream().collect(Collectors.toMap(SysResource::getId, r -> r));
|
||||
|
||||
List<MenuNode> filledMenuList = new ArrayList<>();
|
||||
if (!CollectionUtils.isEmpty(menuNodes)) {
|
||||
for (MenuNode menuNode : menuNodes) {
|
||||
Long menuId = menuNode.getId();
|
||||
if (menuId != null) {
|
||||
SysMenu menu = allMenuMap.get(menuId);
|
||||
if (menu == null) {
|
||||
// 菜单已被删除,忽略
|
||||
continue;
|
||||
}
|
||||
|
||||
// 填充菜单基本信息
|
||||
menuNode.setCode(menu.getCode());
|
||||
menuNode.setName(menu.getName());
|
||||
menuNode.setDescription(menu.getDescription());
|
||||
menuNode.setSource(menu.getSource());
|
||||
menuNode.setParentId(menu.getParentId());
|
||||
menuNode.setPath(menu.getPath());
|
||||
menuNode.setOpenType(menu.getOpenType());
|
||||
menuNode.setIcon(menu.getIcon());
|
||||
menuNode.setSortIndex(menu.getSortIndex());
|
||||
menuNode.setStatus(menu.getStatus());
|
||||
}
|
||||
|
||||
// 填充资源树中的资源详细信息
|
||||
if (CollectionUtils.isNotEmpty(menuNode.getResourceTree())) {
|
||||
MenuBindResourceHelper.fillResourceTreeDetails(menuNode.getResourceTree(), allResourceMap);
|
||||
}
|
||||
|
||||
filledMenuList.add(menuNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (CollectionUtils.isEmpty(filledMenuList)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 构建菜单树
|
||||
MenuBindResourceHelper.buildMenuTree(filledMenuList);
|
||||
|
||||
// 获取/构建 root 节点(menuId = 0)
|
||||
MenuNode rootNode = filledMenuList.stream()
|
||||
.filter(node -> node.getId() != null && node.getId() == 0L)
|
||||
.findFirst()
|
||||
.orElseGet(() -> {
|
||||
SysMenu rootMenu = MenuBindResourceHelper.getRootMenu();
|
||||
MenuNode r = new MenuNode();
|
||||
BeanUtils.copyProperties(rootMenu, r);
|
||||
r.setChildren(filledMenuList);
|
||||
return r;
|
||||
});
|
||||
|
||||
// 如果绑定关系中存在 menu_id = 0 的记录,则直接使用该记录的 menuBindType
|
||||
SysGroupMenu rootBinding = groupMenuMap.get(0L);
|
||||
if (rootBinding != null && rootBinding.getMenuBindType() != null) {
|
||||
rootNode.setMenuBindType(rootBinding.getMenuBindType());
|
||||
}
|
||||
|
||||
// 自下而上打标 menuBindType(仅对“未显式绑定”的菜单和 root 进行合成打标)
|
||||
Set<Long> explicitlyBoundMenuIds = CollectionUtils.isEmpty(groupMenus)
|
||||
? new HashSet<>()
|
||||
: groupMenus.stream()
|
||||
.filter(rm -> rm.getMenuId() != null)
|
||||
.map(SysGroupMenu::getMenuId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
MenuBindResourceHelper.adjustMenuBindTypeBottomUp(rootNode, explicitlyBoundMenuIds);
|
||||
|
||||
// 如果 root 的 menuBindType 为 ALL 或 NONE,则按规则将该类型自上而下强制传播到所有子菜单
|
||||
MenuBindResourceHelper.propagateRootMenuBindType(rootNode);
|
||||
|
||||
List<MenuNode> result = new ArrayList<>();
|
||||
result.add(rootNode);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.xspaceagi.system.spec.utils.CodeGeneratorUtil;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.domain.model.ResourceNode;
|
||||
import com.xspaceagi.system.domain.model.SortIndex;
|
||||
import com.xspaceagi.system.domain.service.SysMenuDomainService;
|
||||
import com.xspaceagi.system.domain.service.SysResourceDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysGroupMenu;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysMenu;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysMenuResource;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysResource;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysRoleMenu;
|
||||
import com.xspaceagi.system.infra.dao.service.SysGroupMenuService;
|
||||
import com.xspaceagi.system.infra.dao.service.SysMenuResourceService;
|
||||
import com.xspaceagi.system.infra.dao.service.SysMenuService;
|
||||
import com.xspaceagi.system.infra.dao.service.SysRoleMenuService;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.enums.BindTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.OpenTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.SourceEnum;
|
||||
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
|
||||
import com.xspaceagi.system.spec.enums.YnEnum;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 菜单领域服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SysMenuDomainServiceImpl implements SysMenuDomainService {
|
||||
|
||||
@Resource
|
||||
private SysMenuService sysMenuService;
|
||||
@Resource
|
||||
private SysMenuResourceService sysMenuResourceService;
|
||||
@Resource
|
||||
private SysRoleMenuService sysRoleMenuService;
|
||||
@Resource
|
||||
private SysGroupMenuService sysGroupMenuService;
|
||||
@Resource
|
||||
private SysResourceDomainService sysResourceDomainService;
|
||||
|
||||
private void normalizeMenu(SysMenu menu) {
|
||||
menu.setCode(StringUtils.trim(menu.getCode()));
|
||||
menu.setName(StringUtils.trim(menu.getName()));
|
||||
menu.setDescription(StringUtils.trim(menu.getDescription()));
|
||||
menu.setPath(StringUtils.trim(menu.getPath()));
|
||||
|
||||
if (StringUtils.isNotBlank(menu.getCode()) && !menu.getCode().matches("^[a-zA-Z][a-zA-Z0-9_]*$")) {
|
||||
throw new IllegalArgumentException("Code may only contain letters, digits and underscores, and must start with a letter");
|
||||
}
|
||||
if (StringUtils.length(menu.getCode()) > 100) {
|
||||
throw new IllegalArgumentException("Code length cannot exceed 100");
|
||||
}
|
||||
if (StringUtils.length(menu.getName()) > 50) {
|
||||
throw new IllegalArgumentException("Name length cannot exceed 50");
|
||||
}
|
||||
if (StringUtils.length(menu.getDescription()) > 500) {
|
||||
throw new IllegalArgumentException("Description length cannot exceed 500");
|
||||
}
|
||||
if (StringUtils.length(menu.getPath()) > 500) {
|
||||
throw new IllegalArgumentException("Path length cannot exceed 500");
|
||||
}
|
||||
if (menu.getOpenType() != null && OpenTypeEnum.isInValid(menu.getOpenType())) {
|
||||
throw new IllegalArgumentException("Invalid open mode parameter");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMenu(SysMenu menu, MenuNode menuNode, Integer source, UserContext userContext) {
|
||||
if (StringUtils.isBlank(menu.getName())) {
|
||||
throw new IllegalArgumentException("Name cannot be empty");
|
||||
}
|
||||
|
||||
// 如果编码为空,根据名称自动生成编码
|
||||
if (StringUtils.isBlank(menu.getCode())) {
|
||||
String generatedCode = CodeGeneratorUtil.generateUniqueCodeFromName(
|
||||
menu.getName(),
|
||||
"menu_",
|
||||
code -> queryMenuByCode(code) != null
|
||||
);
|
||||
menu.setCode(generatedCode);
|
||||
}
|
||||
|
||||
normalizeMenu(menu);
|
||||
|
||||
// 禁止使用 root 作为编码(不区分大小写),直接返回不入库
|
||||
if ("root".equalsIgnoreCase(menu.getCode())) {
|
||||
return;
|
||||
}
|
||||
|
||||
SysMenu exists = queryMenuByCode(menu.getCode());
|
||||
if (exists != null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemMenuCodeAlreadyExists);
|
||||
}
|
||||
if (menu.getParentId() != null && menu.getParentId() != 0) {
|
||||
if (menu.getParentId() < 0) {
|
||||
throw new IllegalArgumentException("Invalid parent menu ID");
|
||||
} else {
|
||||
SysMenu parent = queryMenuById(menu.getParentId());
|
||||
if (parent == null) {
|
||||
throw new IllegalArgumentException("Parent menu does not exist");
|
||||
}
|
||||
if (menu.getParentId().equals(menu.getId())) {
|
||||
throw new IllegalArgumentException("Parent node cannot be itself [id:" + menu.getId() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.getSource() == null) {
|
||||
menu.setSource(SourceEnum.CUSTOM.getCode());
|
||||
}
|
||||
if (menu.getParentId() == null) {
|
||||
menu.setParentId(0L);
|
||||
}
|
||||
if (menu.getSortIndex() == null) {
|
||||
menu.setSortIndex(0);
|
||||
}
|
||||
if (menu.getStatus() == null) {
|
||||
menu.setStatus(YesOrNoEnum.Y.getKey());
|
||||
}
|
||||
menu.setTenantId(userContext.getTenantId());
|
||||
menu.setCreatorId(userContext.getUserId());
|
||||
menu.setCreator(userContext.getUserName());
|
||||
menu.setYn(YnEnum.Y.getKey());
|
||||
sysMenuService.save(menu);
|
||||
|
||||
Long menuId = menu.getId();
|
||||
if (menuId != null && menuNode != null && CollectionUtils.isNotEmpty(menuNode.getResourceTree())) {
|
||||
menuNode.setId(menuId);
|
||||
bindResource(menuNode, userContext);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMenu(SysMenu menu, MenuNode menuNode, Integer source, UserContext userContext) {
|
||||
if (menu.getId() == null) {
|
||||
throw new IllegalArgumentException("ID cannot be empty");
|
||||
}
|
||||
normalizeMenu(menu);
|
||||
|
||||
SysMenu exist = queryMenuById(menu.getId());
|
||||
if (exist == null) {
|
||||
throw new IllegalArgumentException("Menu does not exist");
|
||||
}
|
||||
if (SourceEnum.SYSTEM.getCode().equals(exist.getSource())) {
|
||||
if (menu.getCode() != null && !menu.getCode().equals(exist.getCode())) {
|
||||
throw new IllegalArgumentException("Built-in menu code cannot be changed");
|
||||
}
|
||||
}
|
||||
if (menu.getParentId() != null && menu.getParentId() != 0) {
|
||||
if (menu.getParentId() < 0) {
|
||||
throw new IllegalArgumentException("Invalid parent ID");
|
||||
} else {
|
||||
SysMenu parent = queryMenuById(menu.getParentId());
|
||||
if (parent == null) {
|
||||
throw new IllegalArgumentException("Parent menu does not exist");
|
||||
}
|
||||
if (menu.getParentId().equals(menu.getId())) {
|
||||
throw new IllegalArgumentException("Parent node cannot be itself [id:" + menu.getId() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu.setModifierId(userContext.getUserId());
|
||||
menu.setModifier(userContext.getUserName());
|
||||
sysMenuService.updateById(menu);
|
||||
|
||||
Long menuId = menu.getId();
|
||||
if (menuNode != null) {
|
||||
menuNode.setId(menuId);
|
||||
bindResource(menuNode, userContext);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMenu(Long menuId, UserContext userContext) {
|
||||
SysMenu root = queryMenuById(menuId);
|
||||
if (root == null) {
|
||||
throw new IllegalArgumentException("Menu does not exist");
|
||||
}
|
||||
SysMenu exist = queryMenuById(menuId);
|
||||
if (exist == null) {
|
||||
throw new IllegalArgumentException("Menu does not exist");
|
||||
}
|
||||
if (SourceEnum.SYSTEM.getCode().equals(exist.getSource())) {
|
||||
//throw new IllegalArgumentException("Built-in menu cannot be deleted");
|
||||
}
|
||||
|
||||
// 收集以该菜单为根的整棵子树的所有菜单ID(含自身)
|
||||
Set<Long> menuIds = collectSubTreeMenuIds(menuId, userContext.getTenantId());
|
||||
|
||||
if (CollectionUtils.isEmpty(menuIds)) {
|
||||
return;
|
||||
}
|
||||
// 1. 删除菜单-资源关联
|
||||
sysMenuResourceService.remove(Wrappers.<SysMenuResource>lambdaQuery().in(SysMenuResource::getMenuId, menuIds));
|
||||
// 2. 删除菜单-角色关联
|
||||
sysRoleMenuService.remove(Wrappers.<SysRoleMenu>lambdaQuery().in(SysRoleMenu::getMenuId, menuIds));
|
||||
// 3. 删除菜单-用户组关联
|
||||
sysGroupMenuService.remove(Wrappers.<SysGroupMenu>lambdaQuery().in(SysGroupMenu::getMenuId, menuIds));
|
||||
// 4. 删除菜单树中所有菜单
|
||||
sysMenuService.remove(Wrappers.<SysMenu>lambdaQuery().in(SysMenu::getId, menuIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集以 rootId 为根的菜单子树中所有菜单ID(含根)
|
||||
*/
|
||||
private Set<Long> collectSubTreeMenuIds(Long rootId, Long tenantId) {
|
||||
List<SysMenu> allMenus = sysMenuService.list(Wrappers.<SysMenu>lambdaQuery()
|
||||
.eq(SysMenu::getTenantId, tenantId)
|
||||
.eq(SysMenu::getYn, YnEnum.Y.getKey()));
|
||||
Map<Long, List<SysMenu>> childrenMap = new HashMap<>();
|
||||
if (CollectionUtils.isNotEmpty(allMenus)) {
|
||||
for (SysMenu m : allMenus) {
|
||||
Long parentId = m.getParentId();
|
||||
if (parentId != null && parentId != 0L) {
|
||||
childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Long> out = new HashSet<>();
|
||||
collectSubTreeIds(rootId, childrenMap, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private void collectSubTreeIds(Long id, Map<Long, List<SysMenu>> childrenMap, Set<Long> out) {
|
||||
out.add(id);
|
||||
for (SysMenu c : childrenMap.getOrDefault(id, List.of())) {
|
||||
collectSubTreeIds(c.getId(), childrenMap, out);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysMenu queryMenuById(Long menuId) {
|
||||
LambdaQueryWrapper<SysMenu> wrapper = Wrappers.<SysMenu>lambdaQuery()
|
||||
.eq(SysMenu::getId, menuId)
|
||||
.eq(SysMenu::getYn, YnEnum.Y.getKey());
|
||||
return sysMenuService.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysMenu queryMenuByCode(String code) {
|
||||
LambdaQueryWrapper<SysMenu> wrapper = Wrappers.<SysMenu>lambdaQuery()
|
||||
.eq(SysMenu::getCode, code)
|
||||
.eq(SysMenu::getYn, YnEnum.Y.getKey());
|
||||
return sysMenuService.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysMenu> queryMenuList(SysMenu menu) {
|
||||
LambdaQueryWrapper<SysMenu> wrapper = Wrappers.<SysMenu>lambdaQuery()
|
||||
.eq(SysMenu::getYn, YnEnum.Y.getKey())
|
||||
.orderByAsc(SysMenu::getSortIndex);
|
||||
if (menu != null) {
|
||||
wrapper.eq(StringUtils.isNotBlank(menu.getCode()), SysMenu::getCode, menu.getCode())
|
||||
.like(StringUtils.isNotBlank(menu.getName()), SysMenu::getName, menu.getName())
|
||||
.eq(menu.getSource() != null, SysMenu::getSource, menu.getSource())
|
||||
.eq(menu.getOpenType() != null, SysMenu::getOpenType, menu.getOpenType())
|
||||
.eq(menu.getParentId() != null, SysMenu::getParentId, menu.getParentId())
|
||||
.eq(menu.getStatus() != null, SysMenu::getStatus, menu.getStatus());
|
||||
}
|
||||
return sysMenuService.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysMenu> queryMenuByIds(Collection<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return sysMenuService.listByIds(ids);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<SysMenuResource> queryResourceListByMenuId(Long menuId) {
|
||||
LambdaQueryWrapper<SysMenuResource> wrapper = Wrappers.<SysMenuResource>lambdaQuery()
|
||||
.eq(SysMenuResource::getMenuId, menuId)
|
||||
.eq(SysMenuResource::getYn, YnEnum.Y.getKey());
|
||||
return sysMenuResourceService.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysMenuResource> queryResourceListByMenuIds(java.util.Collection<Long> menuIds) {
|
||||
if (menuIds == null || menuIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
LambdaQueryWrapper<SysMenuResource> wrapper = Wrappers.<SysMenuResource>lambdaQuery()
|
||||
.in(SysMenuResource::getMenuId, menuIds)
|
||||
.eq(SysMenuResource::getYn, YnEnum.Y.getKey());
|
||||
return sysMenuResourceService.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindResource(MenuNode menuNode, UserContext userContext) {
|
||||
if (menuNode == null || menuNode.getId() == null) {
|
||||
throw new IllegalArgumentException("Menu ID cannot be empty");
|
||||
}
|
||||
|
||||
Long menuId = menuNode.getId();
|
||||
// 根节点 menuId=0 是合法的,不需要校验是否存在
|
||||
|
||||
log.info("开始绑定菜单资源: menuId={}, resourceTree={}", menuId,
|
||||
menuNode.getResourceTree() != null ? menuNode.getResourceTree().size() : 0);
|
||||
|
||||
// 物理删除原绑定关系
|
||||
LambdaUpdateWrapper<SysMenuResource> deleteWrapper = Wrappers.<SysMenuResource>lambdaUpdate().eq(SysMenuResource::getMenuId, menuId);
|
||||
sysMenuResourceService.remove(deleteWrapper);
|
||||
|
||||
// 扁平化资源列表
|
||||
List<ResourceNode> resourceList = menuNode.getFlattenResourceList();
|
||||
log.info("扁平化后的资源列表大小: {}", resourceList != null ? resourceList.size() : 0);
|
||||
if (CollectionUtils.isEmpty(resourceList)) {
|
||||
log.warn("菜单 {} 的资源列表为空,跳过绑定", menuId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果存在绑定类型为 ALL 的资源,需要过滤掉其子资源
|
||||
// 1. 查询所有资源,构建父子关系映射
|
||||
List<SysResource> allResources = sysResourceDomainService.queryResourceList(null);
|
||||
Map<Long, SysResource> resourceMap = new HashMap<>();
|
||||
Map<Long, List<SysResource>> resourceChildrenMap = new HashMap<>();
|
||||
if (CollectionUtils.isNotEmpty(allResources)) {
|
||||
for (SysResource resource : allResources) {
|
||||
resourceMap.put(resource.getId(), resource);
|
||||
Long parentId = resource.getParentId();
|
||||
if (parentId != null && parentId != 0L) {
|
||||
resourceChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 验证前端传入的数据
|
||||
for (ResourceNode resourceNode : resourceList) {
|
||||
Long resourceId = resourceNode.getId();
|
||||
if (resourceId == null) {
|
||||
throw new IllegalArgumentException("Resource ID cannot be empty");
|
||||
}
|
||||
|
||||
// 根节点 resourceId=0 是合法的,不需要校验是否存在
|
||||
// 验证资源是否存在(跳过根节点 id=0)
|
||||
if (resourceId != 0L && !resourceMap.containsKey(resourceId)) {
|
||||
throw new IllegalArgumentException("Resource ID [" + resourceId + "] does not exist");
|
||||
}
|
||||
|
||||
// 验证绑定类型是否有效
|
||||
Integer resourceBindType = resourceNode.getResourceBindType();
|
||||
if (resourceBindType != null && BindTypeEnum.isInValid(resourceBindType)) {
|
||||
throw new IllegalArgumentException("For resource ID [" + resourceId + "], bind type [" + resourceBindType + "] is invalid; must be 0 (NONE), 1 (ALL), or 2 (PART)");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 找出所有绑定类型为 ALL 的资源ID,并收集其所有子资源ID
|
||||
Set<Long> allBindResourceIds = resourceList.stream()
|
||||
.filter(r -> r.getId() != null && BindTypeEnum.ALL.getCode().equals(r.getResourceBindType()))
|
||||
.map(ResourceNode::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<Long> childrenResourceIdsToExclude = new HashSet<>();
|
||||
for (Long allBindResourceId : allBindResourceIds) {
|
||||
collectChildrenResourceIds(allBindResourceId, resourceChildrenMap, childrenResourceIdsToExclude);
|
||||
}
|
||||
|
||||
// 4. 过滤掉子资源,只保留父资源(绑定类型为 ALL 的资源本身);resourceBindType=0 或 null 的不入库
|
||||
List<SysMenuResource> menuResourceList = resourceList.stream()
|
||||
.filter(resourceNode -> {
|
||||
Long resourceId = resourceNode.getId();
|
||||
Integer resourceBindType = resourceNode.getResourceBindType();
|
||||
// 如果是绑定类型为 ALL 的资源的子资源,则过滤掉
|
||||
if (resourceId == null || childrenResourceIdsToExclude.contains(resourceId)) {
|
||||
return false;
|
||||
}
|
||||
// resourceBindType=0(NONE) 或 null 的不存数据库
|
||||
if (resourceBindType == null || BindTypeEnum.NONE.getCode().equals(resourceBindType)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(resourceNode -> {
|
||||
SysMenuResource menuResource = new SysMenuResource();
|
||||
menuResource.setMenuId(menuId);
|
||||
menuResource.setResourceId(resourceNode.getId());
|
||||
menuResource.setResourceBindType(resourceNode.getResourceBindType());
|
||||
|
||||
menuResource.setTenantId(userContext.getTenantId());
|
||||
menuResource.setCreatorId(userContext.getUserId());
|
||||
menuResource.setCreator(userContext.getUserName());
|
||||
menuResource.setYn(YnEnum.Y.getKey());
|
||||
return menuResource;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.info("菜单 {} 过滤后的资源列表大小: {}, 需要排除的子资源数量: {}",
|
||||
menuId, menuResourceList.size(), childrenResourceIdsToExclude.size());
|
||||
|
||||
if (!CollectionUtils.isEmpty(menuResourceList)) {
|
||||
log.info("菜单 {} 准备保存 {} 条资源绑定记录", menuId, menuResourceList.size());
|
||||
sysMenuResourceService.saveBatch(menuResourceList);
|
||||
log.info("菜单 {} 成功保存 {} 条资源绑定记录", menuId, menuResourceList.size());
|
||||
} else {
|
||||
log.warn("菜单 {} 过滤后的资源列表为空,未保存任何数据", menuId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean batchUpdateMenuSort(List<SortIndex> sortIndexList, UserContext userContext) {
|
||||
if (CollectionUtils.isEmpty(sortIndexList)) {
|
||||
return false;
|
||||
}
|
||||
boolean hasUpdateParent = false;
|
||||
|
||||
List<Long> parentIdsToValidate = sortIndexList.stream().map(SortIndex::getParentId).filter(pid -> pid != null && pid != 0).toList();
|
||||
|
||||
Set<Long> existingParentIds = new HashSet<>();
|
||||
if (!parentIdsToValidate.isEmpty()) {
|
||||
List<SysMenu> parents = sysMenuService.listByIds(parentIdsToValidate);
|
||||
if (CollectionUtils.isNotEmpty(parents)) {
|
||||
existingParentIds = parents.stream().map(SysMenu::getId).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
for (SortIndex item : sortIndexList) {
|
||||
if (item == null || item.getId() == null) {
|
||||
throw new IllegalArgumentException("Menu ID cannot be empty");
|
||||
}
|
||||
SysMenu updateMenu = new SysMenu();
|
||||
updateMenu.setId(item.getId());
|
||||
boolean hasUpdate = false;
|
||||
if (item.getParentId() != null) {
|
||||
if (item.getParentId() < 0) {
|
||||
throw new IllegalArgumentException("Invalid parent ID: menuId=" + item.getId());
|
||||
}
|
||||
if (item.getParentId() != 0 && !existingParentIds.contains(item.getParentId())) {
|
||||
throw new IllegalArgumentException("Parent node does not exist [parentId:" + item.getParentId() + "]");
|
||||
}
|
||||
if (item.getParentId().equals(item.getId())) {
|
||||
throw new IllegalArgumentException("Parent node cannot be itself [id:" + item.getId() + "]");
|
||||
}
|
||||
updateMenu.setParentId(item.getParentId());
|
||||
hasUpdate = true;
|
||||
hasUpdateParent = true;
|
||||
}
|
||||
if (item.getSortIndex() != null) {
|
||||
updateMenu.setSortIndex(item.getSortIndex());
|
||||
hasUpdate = true;
|
||||
}
|
||||
if (hasUpdate) {
|
||||
updateMenu.setModifierId(userContext.getUserId());
|
||||
updateMenu.setModifier(userContext.getUserName());
|
||||
sysMenuService.updateById(updateMenu);
|
||||
}
|
||||
}
|
||||
return hasUpdateParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集某个资源节点下的所有子资源ID
|
||||
*/
|
||||
private void collectChildrenResourceIds(Long resourceId,
|
||||
Map<Long, List<SysResource>> resourceChildrenMap,
|
||||
Set<Long> idSet) {
|
||||
List<SysResource> children = resourceChildrenMap.get(resourceId);
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
}
|
||||
for (SysResource child : children) {
|
||||
if (child.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (idSet.add(child.getId())) {
|
||||
collectChildrenResourceIds(child.getId(), resourceChildrenMap, idSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.xspaceagi.system.domain.service.ISysOperatorLogDomainService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SysOperatorLogDomainServiceImpl implements ISysOperatorLogDomainService {
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.xspaceagi.system.spec.enums.*;
|
||||
import com.xspaceagi.system.spec.utils.CodeGeneratorUtil;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.xspaceagi.system.domain.model.ResourceNode;
|
||||
import com.xspaceagi.system.domain.model.SortIndex;
|
||||
import com.xspaceagi.system.domain.service.SysResourceDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysGroupMenu;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysMenuResource;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysResource;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysRoleMenu;
|
||||
import com.xspaceagi.system.infra.dao.service.SysGroupMenuService;
|
||||
import com.xspaceagi.system.infra.dao.service.SysMenuResourceService;
|
||||
import com.xspaceagi.system.infra.dao.service.SysResourceService;
|
||||
import com.xspaceagi.system.infra.dao.service.SysRoleMenuService;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 系统资源领域服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SysResourceDomainServiceImpl implements SysResourceDomainService {
|
||||
|
||||
@Resource
|
||||
private SysResourceService sysResourceService;
|
||||
@Resource
|
||||
private SysMenuResourceService sysMenuResourceService;
|
||||
@Resource
|
||||
private SysRoleMenuService sysRoleMenuService;
|
||||
@Resource
|
||||
private SysGroupMenuService sysGroupMenuService;
|
||||
|
||||
private void normalizeResource(SysResource resource) {
|
||||
resource.setCode(StringUtils.trim(resource.getCode()));
|
||||
resource.setName(StringUtils.trim(resource.getName()));
|
||||
resource.setDescription(StringUtils.trim(resource.getDescription()));
|
||||
resource.setPath(StringUtils.trim(resource.getPath()));
|
||||
|
||||
if (StringUtils.isNotBlank(resource.getCode()) && !resource.getCode().matches("^[a-zA-Z][a-zA-Z0-9_]*$")) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacResourceCodeFormatInvalid);
|
||||
}
|
||||
if (StringUtils.length(resource.getCode()) > 100) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacResourceCodeLengthExceeded);
|
||||
}
|
||||
if (StringUtils.length(resource.getName()) > 50) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacNameLengthExceeded);
|
||||
}
|
||||
if (StringUtils.length(resource.getDescription()) > 500) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacDescLengthExceeded);
|
||||
}
|
||||
if (StringUtils.length(resource.getPath()) > 500) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacPathLengthExceeded);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResource(SysResource resource, UserContext userContext) {
|
||||
if (StringUtils.isBlank(resource.getName())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "名称");
|
||||
}
|
||||
if (resource.getType() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "类型");
|
||||
}
|
||||
|
||||
// 如果编码为空,根据名称自动生成编码
|
||||
if (StringUtils.isBlank(resource.getCode())) {
|
||||
String generatedCode = CodeGeneratorUtil.generateUniqueCodeFromName(
|
||||
resource.getName(),
|
||||
"resource_",
|
||||
code -> queryResourceByCode(code) != null
|
||||
);
|
||||
resource.setCode(generatedCode);
|
||||
}
|
||||
|
||||
normalizeResource(resource);
|
||||
|
||||
// 禁止使用 root 作为编码(不区分大小写),直接返回不入库
|
||||
if ("root".equalsIgnoreCase(resource.getCode())) {
|
||||
return;
|
||||
}
|
||||
|
||||
SysResource exists = queryResourceByCode(resource.getCode());
|
||||
if (exists != null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemResourceCodeDuplicate);
|
||||
}
|
||||
if (resource.getParentId() != null && resource.getParentId() != 0) {
|
||||
if (resource.getParentId() < 0) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentIdInvalid);
|
||||
} else {
|
||||
SysResource parent = queryResourceById(resource.getParentId());
|
||||
if (parent == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentResourceNotFoundWithParentId,
|
||||
resource.getParentId());
|
||||
}
|
||||
if (!parent.getType().equals(ResourceTypeEnum.MODULE.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentNameNotModule,
|
||||
parent.getName());
|
||||
}
|
||||
if (resource.getParentId().equals(resource.getId())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentSelfReferenceForbidden,
|
||||
resource.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.getSource() == null) {
|
||||
resource.setSource(SourceEnum.CUSTOM.getCode());
|
||||
}
|
||||
if (resource.getParentId() == null) {
|
||||
resource.setParentId(0L);
|
||||
}
|
||||
if (resource.getSortIndex() == null) {
|
||||
resource.setSortIndex(0);
|
||||
}
|
||||
if (resource.getStatus() == null) {
|
||||
resource.setStatus(YesOrNoEnum.Y.getKey());
|
||||
}
|
||||
resource.setTenantId(userContext.getTenantId());
|
||||
resource.setCreatorId(userContext.getUserId());
|
||||
resource.setCreator(userContext.getUserName());
|
||||
resource.setYn(YnEnum.Y.getKey());
|
||||
sysResourceService.save(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateResource(SysResource resource, UserContext userContext) {
|
||||
if (resource.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "ID");
|
||||
}
|
||||
normalizeResource(resource);
|
||||
SysResource exist = queryResourceById(resource.getId());
|
||||
if (exist == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemResourceNotFound);
|
||||
}
|
||||
if (SourceEnum.SYSTEM.getCode().equals(exist.getSource())) {
|
||||
if (resource.getCode() != null && !resource.getCode().equals(exist.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemBuiltinResourceCodeImmutable);
|
||||
}
|
||||
if (resource.getName() != null && !resource.getName().equals(exist.getName())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemBuiltinResourceNameImmutable);
|
||||
}
|
||||
if (resource.getStatus() != null && !resource.getStatus().equals(StatusEnum.ENABLED.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemBuiltinResourceCannotDisable);
|
||||
}
|
||||
}
|
||||
if (resource.getParentId() != null && resource.getParentId() != 0) {
|
||||
if (resource.getParentId() < 0) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentIdInvalid);
|
||||
} else {
|
||||
SysResource parent = queryResourceById(resource.getParentId());
|
||||
if (parent == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentResourceNotFound);
|
||||
}
|
||||
if (!parent.getType().equals(ResourceTypeEnum.MODULE.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentNameNotModule,
|
||||
parent.getName());
|
||||
}
|
||||
if (resource.getParentId().equals(resource.getId())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentSelfReferenceForbidden,
|
||||
resource.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource.setModifierId(userContext.getUserId());
|
||||
resource.setModifier(userContext.getUserName());
|
||||
sysResourceService.updateById(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteResource(Long resourceId, UserContext userContext) {
|
||||
if (resourceId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "ID");
|
||||
}
|
||||
SysResource root = queryResourceById(resourceId);
|
||||
if (root == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemResourceNotFound);
|
||||
}
|
||||
if (SourceEnum.SYSTEM.getCode().equals(root.getSource())) {
|
||||
//throw new BizException("系统内置资源不能删除");
|
||||
}
|
||||
|
||||
Long tenantId = userContext.getTenantId();
|
||||
// 收集以该资源为根的整棵子树的所有资源ID(含自身)
|
||||
Set<Long> resourceIds = collectSubTreeResourceIds(resourceId, tenantId);
|
||||
|
||||
if (CollectionUtils.isEmpty(resourceIds)) {
|
||||
return;
|
||||
}
|
||||
// 1. 删除菜单-资源关联
|
||||
sysMenuResourceService.remove(Wrappers.<SysMenuResource>lambdaQuery().in(SysMenuResource::getResourceId, resourceIds));
|
||||
// 2. 更新角色-菜单的 resource_tree_json,移除已删除的资源节点
|
||||
updateRoleMenuResourceTree(resourceIds, tenantId);
|
||||
// 3. 更新用户组-菜单的 resource_tree_json,移除已删除的资源节点
|
||||
updateGroupMenuResourceTree(resourceIds, tenantId);
|
||||
// 4. 删除资源树中所有资源
|
||||
sysResourceService.remove(Wrappers.<SysResource>lambdaQuery().in(SysResource::getId, resourceIds));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean batchUpdateResourceSort(List<SortIndex> sortIndexList, UserContext userContext) {
|
||||
if (CollectionUtils.isEmpty(sortIndexList)) {
|
||||
return false;
|
||||
}
|
||||
boolean hasUpdateParent = false;
|
||||
List<Long> parentIdsToValidate = sortIndexList.stream().map(SortIndex::getParentId).filter(pid -> pid != null && pid != 0).toList();
|
||||
|
||||
Set<Long> existingParentIds = new HashSet<>();
|
||||
if (!parentIdsToValidate.isEmpty()) {
|
||||
List<SysResource> parents = sysResourceService.listByIds(parentIdsToValidate);
|
||||
if (CollectionUtils.isNotEmpty(parents)) {
|
||||
existingParentIds = parents.stream().map(SysResource::getId).collect(Collectors.toSet());
|
||||
}
|
||||
parents.forEach(p -> {
|
||||
if (!p.getType().equals(ResourceTypeEnum.MODULE.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentNameNotModule,
|
||||
p.getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
for (SortIndex item : sortIndexList) {
|
||||
if (item == null || item.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "资源ID");
|
||||
}
|
||||
SysResource updateResource = new SysResource();
|
||||
updateResource.setId(item.getId());
|
||||
boolean hasUpdate = false;
|
||||
if (item.getParentId() != null) {
|
||||
if (item.getParentId() < 0) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentIdInvalidForResource,
|
||||
item.getId());
|
||||
}
|
||||
if (item.getParentId() != 0 && !existingParentIds.contains(item.getParentId())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentNodeNotFound,
|
||||
item.getParentId());
|
||||
}
|
||||
if (item.getParentId().equals(item.getId())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacParentSelfReferenceForbidden,
|
||||
item.getId());
|
||||
}
|
||||
updateResource.setParentId(item.getParentId());
|
||||
hasUpdate = true;
|
||||
hasUpdateParent = true;
|
||||
}
|
||||
if (item.getSortIndex() != null) {
|
||||
updateResource.setSortIndex(item.getSortIndex());
|
||||
hasUpdate = true;
|
||||
}
|
||||
if (hasUpdate) {
|
||||
updateResource.setModifierId(userContext.getUserId());
|
||||
updateResource.setModifier(userContext.getUserName());
|
||||
sysResourceService.updateById(updateResource);
|
||||
}
|
||||
}
|
||||
return hasUpdateParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集以 rootId 为根的资源子树中所有资源ID(含根)
|
||||
*/
|
||||
private Set<Long> collectSubTreeResourceIds(Long rootId, Long tenantId) {
|
||||
LambdaQueryWrapper<SysResource> wrapper = Wrappers.<SysResource>lambdaQuery()
|
||||
.eq(SysResource::getYn, YnEnum.Y.getKey());
|
||||
if (tenantId != null) {
|
||||
wrapper.eq(SysResource::getTenantId, tenantId);
|
||||
}
|
||||
List<SysResource> allResources = sysResourceService.list(wrapper);
|
||||
Map<Long, List<SysResource>> childrenMap = new HashMap<>();
|
||||
if (CollectionUtils.isNotEmpty(allResources)) {
|
||||
for (SysResource r : allResources) {
|
||||
Long parentId = r.getParentId();
|
||||
if (parentId != null && parentId != 0L) {
|
||||
childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Long> out = new HashSet<>();
|
||||
collectSubTreeResourceIdsRec(rootId, childrenMap, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private void collectSubTreeResourceIdsRec(Long id, Map<Long, List<SysResource>> childrenMap, Set<Long> out) {
|
||||
out.add(id);
|
||||
for (SysResource c : childrenMap.getOrDefault(id, List.of())) {
|
||||
if (c.getId() != null) {
|
||||
collectSubTreeResourceIdsRec(c.getId(), childrenMap, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 sys_role_menu 的 resource_tree_json,移除已删除的资源ID对应节点
|
||||
*/
|
||||
private void updateRoleMenuResourceTree(Set<Long> toDeleteIds, Long tenantId) {
|
||||
LambdaQueryWrapper<SysRoleMenu> wrapper = Wrappers.<SysRoleMenu>lambdaQuery()
|
||||
.isNotNull(SysRoleMenu::getResourceTreeJson)
|
||||
.ne(SysRoleMenu::getResourceTreeJson, "");
|
||||
if (tenantId != null) {
|
||||
wrapper.eq(SysRoleMenu::getTenantId, tenantId);
|
||||
}
|
||||
List<SysRoleMenu> list = sysRoleMenuService.list(wrapper);
|
||||
for (SysRoleMenu rm : list) {
|
||||
List<ResourceNode> tree = null;
|
||||
try {
|
||||
tree = JsonSerializeUtil.parseObject(rm.getResourceTreeJson(), new TypeReference<List<ResourceNode>>() {});
|
||||
} catch (Exception e) {
|
||||
log.error("resourceTreeJson deserialize failed: " + e.getMessage(), e);
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemResourceTreeJsonInvalid);
|
||||
}
|
||||
if (tree == null || !containsAnyResourceId(tree, toDeleteIds)) {
|
||||
continue;
|
||||
}
|
||||
List<ResourceNode> pruned = pruneResourceTree(tree, toDeleteIds);
|
||||
rm.setResourceTreeJson(CollectionUtils.isEmpty(pruned) ? null : JsonSerializeUtil.toJSONString(pruned));
|
||||
sysRoleMenuService.updateById(rm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 sys_group_menu 的 resource_tree_json,移除已删除的资源ID对应节点
|
||||
*/
|
||||
private void updateGroupMenuResourceTree(Set<Long> toDeleteIds, Long tenantId) {
|
||||
LambdaQueryWrapper<SysGroupMenu> wrapper = Wrappers.<SysGroupMenu>lambdaQuery()
|
||||
.isNotNull(SysGroupMenu::getResourceTreeJson)
|
||||
.ne(SysGroupMenu::getResourceTreeJson, "");
|
||||
if (tenantId != null) {
|
||||
wrapper.eq(SysGroupMenu::getTenantId, tenantId);
|
||||
}
|
||||
List<SysGroupMenu> list = sysGroupMenuService.list(wrapper);
|
||||
for (SysGroupMenu gm : list) {
|
||||
List<ResourceNode> tree = null;
|
||||
try {
|
||||
tree = JsonSerializeUtil.parseObject(gm.getResourceTreeJson(), new TypeReference<List<ResourceNode>>() {});
|
||||
} catch (Exception e) {
|
||||
log.error("resourceTreeJson deserialize failed: " + e.getMessage(), e);
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemResourceTreeJsonInvalid);
|
||||
}
|
||||
if (tree == null || !containsAnyResourceId(tree, toDeleteIds)) {
|
||||
continue;
|
||||
}
|
||||
List<ResourceNode> pruned = pruneResourceTree(tree, toDeleteIds);
|
||||
gm.setResourceTreeJson(CollectionUtils.isEmpty(pruned) ? null : JsonSerializeUtil.toJSONString(pruned));
|
||||
sysGroupMenuService.updateById(gm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 parseObjectGeneric 得到的 Object(List/Map 结构)转为 List<ResourceNode>
|
||||
*/
|
||||
private List<ResourceNode> parseResourceTreeFromObject(Object obj) {
|
||||
if (obj == null || !(obj instanceof List)) {
|
||||
return null;
|
||||
}
|
||||
List<ResourceNode> out = new ArrayList<>();
|
||||
for (Object e : (List<?>) obj) {
|
||||
ResourceNode n = parseResourceNodeFromObject(e);
|
||||
if (n != null) {
|
||||
out.add(n);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private ResourceNode parseResourceNodeFromObject(Object obj) {
|
||||
if (obj == null || !(obj instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
Map<?, ?> m = (Map<?, ?>) obj;
|
||||
ResourceNode n = new ResourceNode();
|
||||
Object ro = m.get("resourceId");
|
||||
if (ro instanceof Number) {
|
||||
n.setId(((Number) ro).longValue());
|
||||
}
|
||||
Object rbo = m.get("resourceBindType");
|
||||
if (rbo instanceof Number) {
|
||||
n.setResourceBindType(((Number) rbo).intValue());
|
||||
}
|
||||
Object ch = m.get("children");
|
||||
n.setChildren(parseResourceTreeFromObject(ch));
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归检查资源树中是否包含 toCheck 中的任意 resourceId
|
||||
*/
|
||||
private boolean containsAnyResourceId(List<ResourceNode> tree, Set<Long> toCheck) {
|
||||
if (CollectionUtils.isEmpty(tree)) {
|
||||
return false;
|
||||
}
|
||||
for (ResourceNode n : tree) {
|
||||
if (n.getId() != null && toCheck.contains(n.getId())) {
|
||||
return true;
|
||||
}
|
||||
if (containsAnyResourceId(n.getChildren(), toCheck)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归修剪资源树:移除 resourceId 在 toDelete 中的节点(及其整棵子树)
|
||||
*/
|
||||
private List<ResourceNode> pruneResourceTree(List<ResourceNode> tree, Set<Long> toDelete) {
|
||||
if (CollectionUtils.isEmpty(tree)) {
|
||||
return null;
|
||||
}
|
||||
List<ResourceNode> out = new ArrayList<>();
|
||||
for (ResourceNode n : tree) {
|
||||
if (n.getId() != null && toDelete.contains(n.getId())) {
|
||||
continue;
|
||||
}
|
||||
ResourceNode c = new ResourceNode();
|
||||
c.setId(n.getId());
|
||||
c.setResourceBindType(n.getResourceBindType());
|
||||
List<ResourceNode> prunedCh = pruneResourceTree(n.getChildren(), toDelete);
|
||||
c.setChildren(prunedCh);
|
||||
out.add(c);
|
||||
}
|
||||
return out.isEmpty() ? null : out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysResource queryResourceById(Long resourceId) {
|
||||
LambdaQueryWrapper<SysResource> wrapper = Wrappers.<SysResource>lambdaQuery()
|
||||
.eq(SysResource::getId, resourceId)
|
||||
.eq(SysResource::getYn, YnEnum.Y.getKey());
|
||||
return sysResourceService.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysResource queryResourceByCode(String resourceCode) {
|
||||
LambdaQueryWrapper<SysResource> wrapper = Wrappers.<SysResource>lambdaQuery()
|
||||
.eq(SysResource::getCode, resourceCode)
|
||||
.eq(SysResource::getYn, YnEnum.Y.getKey());
|
||||
return sysResourceService.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysResource> queryResourceList(SysResource resource) {
|
||||
LambdaQueryWrapper<SysResource> wrapper = Wrappers.<SysResource>lambdaQuery()
|
||||
.eq(SysResource::getYn, YnEnum.Y.getKey())
|
||||
.orderByAsc(SysResource::getSortIndex);
|
||||
if (resource != null) {
|
||||
wrapper.eq(StringUtils.isNotBlank(resource.getCode()), SysResource::getCode, resource.getCode())
|
||||
.like(StringUtils.isNotBlank(resource.getName()), SysResource::getName, resource.getName())
|
||||
.eq(Objects.nonNull(resource.getSource()), SysResource::getSource, resource.getSource())
|
||||
.eq(Objects.nonNull(resource.getType()), SysResource::getType, resource.getType())
|
||||
.eq(Objects.nonNull(resource.getParentId()), SysResource::getParentId, resource.getParentId())
|
||||
.eq(Objects.nonNull(resource.getStatus()), SysResource::getStatus, resource.getStatus());
|
||||
}
|
||||
return sysResourceService.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysResource> queryResourceByIds(Collection<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return sysResourceService.listByIds(ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,843 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.xspaceagi.system.domain.model.MenuNode;
|
||||
import com.xspaceagi.system.domain.model.ResourceNode;
|
||||
import com.xspaceagi.system.domain.model.RoleBindMenuModel;
|
||||
import com.xspaceagi.system.domain.model.SortIndex;
|
||||
import com.xspaceagi.system.domain.service.SysMenuDomainService;
|
||||
import com.xspaceagi.system.domain.service.SysResourceDomainService;
|
||||
import com.xspaceagi.system.domain.service.SysRoleDomainService;
|
||||
import com.xspaceagi.system.domain.service.SysSubjectPermissionDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.*;
|
||||
import com.xspaceagi.system.infra.dao.service.*;
|
||||
import com.xspaceagi.system.sdk.service.dto.TokenLimit;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.enums.*;
|
||||
import com.xspaceagi.system.spec.constants.PermissionConstants;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
|
||||
import com.xspaceagi.system.spec.utils.CodeGeneratorUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 系统角色领域服务实现
|
||||
*/
|
||||
@Service
|
||||
public class SysRoleDomainServiceImpl implements SysRoleDomainService {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
@Resource
|
||||
private SysRoleService sysRoleService;
|
||||
@Resource
|
||||
private SysUserRoleService sysUserRoleService;
|
||||
@Resource
|
||||
private SysRoleMenuService sysRoleMenuService;
|
||||
@Resource
|
||||
private SysMenuService sysMenuService;
|
||||
@Resource
|
||||
private SysResourceService sysResourceService;
|
||||
@Resource
|
||||
private SysMenuDomainService sysMenuDomainService;
|
||||
@Resource
|
||||
private SysResourceDomainService sysResourceDomainService;
|
||||
@Resource
|
||||
private SysDataPermissionDomainServiceImpl sysDataPermissionDomainService;
|
||||
@Resource
|
||||
private SysSubjectPermissionDomainService sysSubjectPermissionDomainService;
|
||||
@Resource
|
||||
private MenuBindResourceHelper menuBindResourceHelper;
|
||||
|
||||
private void normalizeRole(SysRole role) {
|
||||
role.setCode(StringUtils.trim(role.getCode()));
|
||||
role.setName(StringUtils.trim(role.getName()));
|
||||
role.setDescription(StringUtils.trim(role.getDescription()));
|
||||
|
||||
if (StringUtils.isNotBlank(role.getCode()) && !role.getCode().matches("^[a-zA-Z][a-zA-Z0-9_]*$")) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacCodeFormatInvalid);
|
||||
}
|
||||
if (StringUtils.length(role.getCode()) > 100) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacCodeLengthExceeded);
|
||||
}
|
||||
if (StringUtils.length(role.getName()) > 50) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacNameLengthExceeded);
|
||||
}
|
||||
if (StringUtils.length(role.getDescription()) > 500) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacDescLengthExceeded);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRole(SysRole role, UserContext userContext) {
|
||||
if (StringUtils.isBlank(role.getName())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "名称");
|
||||
}
|
||||
|
||||
// 如果编码为空,根据名称自动生成编码
|
||||
if (StringUtils.isBlank(role.getCode())) {
|
||||
String generatedCode = CodeGeneratorUtil.generateUniqueCodeFromName(
|
||||
role.getName(),
|
||||
"role_",
|
||||
code -> queryRoleByCode(code) != null
|
||||
);
|
||||
role.setCode(generatedCode);
|
||||
}
|
||||
|
||||
normalizeRole(role);
|
||||
|
||||
SysRole exist = queryRoleByCode(role.getCode());
|
||||
if (exist != null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleCodeDuplicate);
|
||||
}
|
||||
|
||||
if (role.getSource() == null) {
|
||||
role.setSource(SourceEnum.CUSTOM.getCode());
|
||||
}
|
||||
if (role.getStatus() == null) {
|
||||
role.setStatus(StatusEnum.ENABLED.getCode());
|
||||
}
|
||||
if (role.getSortIndex() == null) {
|
||||
role.setSortIndex(0);
|
||||
}
|
||||
role.setTenantId(userContext.getTenantId());
|
||||
role.setCreatorId(userContext.getUserId());
|
||||
role.setCreator(userContext.getUserName());
|
||||
role.setYn(YnEnum.Y.getKey());
|
||||
sysRoleService.save(role);
|
||||
|
||||
if (role.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacSaveFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateRole(SysRole role, UserContext userContext) {
|
||||
if (role.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "ID");
|
||||
}
|
||||
normalizeRole(role);
|
||||
|
||||
SysRole exist = queryRoleById(role.getId());
|
||||
if (exist == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleNotFound);
|
||||
}
|
||||
if (SourceEnum.SYSTEM.getCode().equals(exist.getSource())) {
|
||||
if (role.getCode() != null && !role.getCode().equals(exist.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemBuiltinRoleCodeImmutable);
|
||||
}
|
||||
if (role.getName() != null && !role.getName().equals(exist.getName())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemBuiltinRoleNameImmutable);
|
||||
}
|
||||
if (role.getStatus() != null && !role.getStatus().equals(StatusEnum.ENABLED.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemBuiltinRoleCannotDisable);
|
||||
}
|
||||
}
|
||||
|
||||
boolean statusChanged = role.getStatus() != null
|
||||
&& !Objects.equals(exist.getStatus(), role.getStatus());
|
||||
|
||||
role.setModifierId(userContext.getUserId());
|
||||
role.setModifier(userContext.getUserName());
|
||||
sysRoleService.updateById(role);
|
||||
return statusChanged;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindDataPermission(Long roleId, SysDataPermission dataPermission, UserContext userContext) {
|
||||
if (roleId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "角色ID");
|
||||
}
|
||||
SysRole exist = queryRoleById(roleId);
|
||||
if (exist == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleNotFound);
|
||||
}
|
||||
if (dataPermission == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacDataPermissionRequired);
|
||||
}
|
||||
|
||||
if (CollectionUtils.isNotEmpty(dataPermission.getModelIds())) {
|
||||
List<Long> modelIds = dataPermission.getModelIds().stream()
|
||||
.filter(modelId -> Objects.nonNull(modelId) && modelId >= 1)
|
||||
.collect(Collectors.toList());
|
||||
dataPermission.setModelIds(modelIds);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(dataPermission.getAgentIds())) {
|
||||
List<Long> agentIds = dataPermission.getAgentIds().stream()
|
||||
.filter(agentId -> Objects.nonNull(agentId) && agentId >= 1)
|
||||
.toList();
|
||||
dataPermission.setAgentIds(agentIds);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(dataPermission.getPageAgentIds())) {
|
||||
List<Long> pageAgentIds = dataPermission.getPageAgentIds().stream()
|
||||
.filter(pageAgentId -> Objects.nonNull(pageAgentId) && pageAgentId >= 1)
|
||||
.toList();
|
||||
dataPermission.setPageAgentIds(pageAgentIds);
|
||||
}
|
||||
if (dataPermission.getOpenApiConfigMap() != null && !dataPermission.getOpenApiConfigMap().isEmpty()) {
|
||||
Map<String, String> normalizedConfig = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> entry : dataPermission.getOpenApiConfigMap().entrySet()) {
|
||||
if (StringUtils.isBlank(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
normalizedConfig.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
dataPermission.setOpenApiConfigMap(normalizedConfig);
|
||||
}
|
||||
|
||||
if (CollectionUtils.isNotEmpty(dataPermission.getKnowledgeIds())) {
|
||||
List<Long> knowledgeIds = dataPermission.getKnowledgeIds().stream()
|
||||
.filter(knowledgeId -> Objects.nonNull(knowledgeId) && knowledgeId >= 1)
|
||||
.toList();
|
||||
dataPermission.setKnowledgeIds(knowledgeIds);
|
||||
}
|
||||
|
||||
if (dataPermission.getTokenLimit() != null) {
|
||||
if (dataPermission.getTokenLimit().getLimitPerDay() < -1) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacTokenLimitMinInvalid);
|
||||
}
|
||||
} else {
|
||||
dataPermission.setTokenLimit(new TokenLimit(-1L));
|
||||
}
|
||||
|
||||
dataPermission.setTargetType(PermissionTargetTypeEnum.ROLE.getCode());
|
||||
dataPermission.setTargetId(roleId);
|
||||
SysDataPermission oldDataPermission = sysDataPermissionDomainService.getByTarget(PermissionTargetTypeEnum.ROLE, roleId);
|
||||
if (oldDataPermission != null) {
|
||||
sysDataPermissionDomainService.update(oldDataPermission.getId(), dataPermission, userContext);
|
||||
} else {
|
||||
sysDataPermissionDomainService.add(dataPermission, userContext);
|
||||
}
|
||||
|
||||
// 全量覆盖写入
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum.ROLE, roleId,
|
||||
PermissionSubjectTypeEnum.MODEL, dataPermission.getModelIds(), userContext);
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum.ROLE, roleId,
|
||||
PermissionSubjectTypeEnum.AGENT, dataPermission.getAgentIds(), userContext);
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum.ROLE, roleId,
|
||||
PermissionSubjectTypeEnum.PAGE, dataPermission.getPageAgentIds(), userContext);
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectKeyConfig(PermissionTargetTypeEnum.ROLE, roleId,
|
||||
PermissionSubjectTypeEnum.OPEN_API, dataPermission.getOpenApiConfigMap(), userContext);
|
||||
sysSubjectPermissionDomainService.replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum.ROLE, roleId,
|
||||
PermissionSubjectTypeEnum.KNOWLEDGE, dataPermission.getKnowledgeIds(), userContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void batchUpdateRoleSort(List<SortIndex> sortIndexList, UserContext userContext) {
|
||||
if (CollectionUtils.isEmpty(sortIndexList)) {
|
||||
return;
|
||||
}
|
||||
for (SortIndex item : sortIndexList) {
|
||||
if (item == null || item.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "角色ID");
|
||||
}
|
||||
SysRole exist = queryRoleById(item.getId());
|
||||
if (exist == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleNotFoundWithRowId, item.getId());
|
||||
}
|
||||
if (item.getSortIndex() == null) {
|
||||
continue;
|
||||
}
|
||||
SysRole updateRole = new SysRole();
|
||||
updateRole.setId(item.getId());
|
||||
updateRole.setSortIndex(item.getSortIndex());
|
||||
updateRole.setModifierId(userContext.getUserId());
|
||||
updateRole.setModifier(userContext.getUserName());
|
||||
sysRoleService.updateById(updateRole);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRole(Long roleId, UserContext userContext) {
|
||||
if (roleId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "ID");
|
||||
}
|
||||
SysRole exist = queryRoleById(roleId);
|
||||
if (exist == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleNotFound);
|
||||
}
|
||||
|
||||
if (SourceEnum.SYSTEM.getCode().equals(exist.getSource())) {
|
||||
//throw new BizException("系统内置角色不能删除");
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
sysRoleService.removeById(roleId);
|
||||
// 删除数据权限
|
||||
sysDataPermissionDomainService.deleteByTaret(PermissionTargetTypeEnum.ROLE, roleId, userContext);
|
||||
// 删除角色绑定的用户
|
||||
sysUserRoleService.remove(Wrappers.<SysUserRole>lambdaUpdate().eq(SysUserRole::getRoleId, roleId));
|
||||
// 删除角色绑定的菜单
|
||||
sysRoleMenuService.remove(Wrappers.<SysRoleMenu>lambdaUpdate().eq(SysRoleMenu::getRoleId, roleId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysRole queryRoleById(Long roleId) {
|
||||
LambdaQueryWrapper<SysRole> wrapper = Wrappers.<SysRole>lambdaQuery().eq(SysRole::getId, roleId).eq(SysRole::getYn, YnEnum.Y.getKey());
|
||||
return sysRoleService.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysRole> queryRoleListByIds(List<Long> roleIds) {
|
||||
if (CollectionUtils.isEmpty(roleIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Long> ids = roleIds.stream().filter(Objects::nonNull).distinct().toList();
|
||||
return sysRoleService.list(Wrappers.<SysRole>lambdaQuery()
|
||||
.in(SysRole::getId, ids)
|
||||
.eq(SysRole::getYn, YnEnum.Y.getKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysRole queryRoleByCode(String roleCode) {
|
||||
LambdaQueryWrapper<SysRole> wrapper = Wrappers.<SysRole>lambdaQuery().eq(SysRole::getCode, roleCode).eq(SysRole::getYn, YnEnum.Y.getKey());
|
||||
return sysRoleService.getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysRole> queryRoleList(SysRole role) {
|
||||
LambdaQueryWrapper<SysRole> wrapper = Wrappers.<SysRole>lambdaQuery()
|
||||
.eq(StringUtils.isNotBlank(role.getCode()), SysRole::getCode, role.getCode())
|
||||
.like(StringUtils.isNotBlank(role.getName()), SysRole::getName, role.getName())
|
||||
.eq(Objects.nonNull(role.getSource()), SysRole::getSource, role.getSource())
|
||||
.eq(Objects.nonNull(role.getStatus()), SysRole::getStatus, role.getStatus())
|
||||
.eq(SysRole::getYn, YnEnum.Y.getKey()).orderByAsc(SysRole::getSortIndex);
|
||||
return sysRoleService.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> getUserListByRoleId(Long roleId) {
|
||||
LambdaQueryWrapper<SysUserRole> wrapper = Wrappers.<SysUserRole>lambdaQuery().eq(SysUserRole::getRoleId, roleId).eq(SysUserRole::getYn, YnEnum.Y.getKey());
|
||||
List<SysUserRole> list = sysUserRoleService.list(wrapper);
|
||||
if (CollectionUtils.isEmpty(list)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Long> userIdList = list.stream().map(SysUserRole::getUserId).toList();
|
||||
List<User> users = userService.listByIds(userIdList);
|
||||
return users;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<User> getUserPageByRoleId(Long roleId, String userName, long pageNo, long pageSize) {
|
||||
List<Long> matchingUserIds = null;
|
||||
if (StringUtils.isNotBlank(userName)) {
|
||||
List<User> matchingUsers = userService.list(Wrappers.<User>lambdaQuery()
|
||||
.and(w -> w.like(User::getNickName, userName).or().like(User::getUserName, userName))
|
||||
.select(User::getId));
|
||||
matchingUserIds = matchingUsers.stream().map(User::getId).filter(Objects::nonNull).toList();
|
||||
if (CollectionUtils.isEmpty(matchingUserIds)) {
|
||||
return new Page<>(pageNo, pageSize, 0);
|
||||
}
|
||||
}
|
||||
LambdaQueryWrapper<SysUserRole> wrapper = Wrappers.<SysUserRole>lambdaQuery()
|
||||
.eq(SysUserRole::getRoleId, roleId)
|
||||
.eq(SysUserRole::getYn, YnEnum.Y.getKey())
|
||||
.in(CollectionUtils.isNotEmpty(matchingUserIds), SysUserRole::getUserId, matchingUserIds)
|
||||
.orderByAsc(SysUserRole::getUserId);
|
||||
IPage<SysUserRole> userRolePage = sysUserRoleService.page(new Page<>(pageNo, pageSize), wrapper);
|
||||
if (CollectionUtils.isEmpty(userRolePage.getRecords())) {
|
||||
return new Page<>(pageNo, pageSize, userRolePage.getTotal());
|
||||
}
|
||||
List<Long> pageUserIds = userRolePage.getRecords().stream().map(SysUserRole::getUserId).filter(Objects::nonNull).toList();
|
||||
List<User> users = userService.listByIds(pageUserIds);
|
||||
Map<Long, User> userMap = users.stream().collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a));
|
||||
List<User> orderedUsers = pageUserIds.stream().map(userMap::get).filter(Objects::nonNull).toList();
|
||||
Page<User> page = new Page<>(pageNo, pageSize, userRolePage.getTotal());
|
||||
page.setRecords(orderedUsers);
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUsersByRoleId(Long roleId) {
|
||||
LambdaQueryWrapper<SysUserRole> wrapper = Wrappers.<SysUserRole>lambdaQuery()
|
||||
.eq(SysUserRole::getRoleId, roleId)
|
||||
.eq(SysUserRole::getYn, YnEnum.Y.getKey());
|
||||
return sysUserRoleService.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getUserIdsByRoleId(Long roleId) {
|
||||
LambdaQueryWrapper<SysUserRole> wrapper = Wrappers.<SysUserRole>lambdaQuery()
|
||||
.eq(SysUserRole::getRoleId, roleId)
|
||||
.eq(SysUserRole::getYn, YnEnum.Y.getKey())
|
||||
.select(SysUserRole::getUserId);
|
||||
List<SysUserRole> list = sysUserRoleService.list(wrapper);
|
||||
if (CollectionUtils.isEmpty(list)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return list.stream().map(SysUserRole::getUserId).filter(Objects::nonNull).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysRole> queryRoleListByUserId(Long userId) {
|
||||
// 先查询用户角色关联
|
||||
LambdaQueryWrapper<SysUserRole> wrapper = Wrappers.<SysUserRole>lambdaQuery()
|
||||
.eq(SysUserRole::getUserId, userId)
|
||||
.eq(SysUserRole::getYn, YnEnum.Y.getKey());
|
||||
List<SysUserRole> userRoles = sysUserRoleService.list(wrapper);
|
||||
|
||||
if (CollectionUtils.isEmpty(userRoles)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 获取角色ID列表
|
||||
List<Long> roleIds = userRoles.stream().map(SysUserRole::getRoleId).collect(Collectors.toList());
|
||||
|
||||
// 查询角色信息
|
||||
LambdaQueryWrapper<SysRole> roleWrapper = new LambdaQueryWrapper<>();
|
||||
roleWrapper.in(SysRole::getId, roleIds)
|
||||
.eq(SysRole::getYn, YnEnum.Y.getKey())
|
||||
.orderByAsc(SysRole::getSortIndex);
|
||||
return sysRoleService.list(roleWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roleBindUser(Long roleId, List<Long> userIds, UserContext userContext) {
|
||||
if (roleId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "角色ID");
|
||||
}
|
||||
SysRole sysRole = queryRoleById(roleId);
|
||||
if (sysRole == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleNotFoundWithRoleId, roleId);
|
||||
}
|
||||
|
||||
// 校验用户是否存在且为管理员类型
|
||||
if (CollectionUtils.isNotEmpty(userIds)) {
|
||||
Set<Long> distinctUserIds = new HashSet<>(userIds);
|
||||
List<User> users = userService.listByIds(distinctUserIds);
|
||||
List<Long> existUserIds = users.stream().map(User::getId).toList();
|
||||
|
||||
userIds.forEach(userId -> {
|
||||
if (!existUserIds.contains(userId)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacUserNotFoundWithId, userId);
|
||||
}
|
||||
});
|
||||
users.forEach(user -> {
|
||||
if (user.getRole() == null || !User.Role.Admin.equals(user.getRole())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleBindRequiresAdminUser,
|
||||
user.getUserName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 物理删除原绑定关系
|
||||
LambdaUpdateWrapper<SysUserRole> wrapper = Wrappers.<SysUserRole>lambdaUpdate().eq(SysUserRole::getRoleId, roleId);
|
||||
sysUserRoleService.remove(wrapper);
|
||||
|
||||
if (CollectionUtils.isEmpty(userIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SysUserRole> userRoleList = userIds.stream().map(userId -> {
|
||||
SysUserRole userRole = new SysUserRole();
|
||||
userRole.setUserId(userId);
|
||||
userRole.setRoleId(roleId);
|
||||
userRole.setTenantId(userContext.getTenantId());
|
||||
userRole.setCreatorId(userContext.getUserId());
|
||||
userRole.setCreator(userContext.getUserName());
|
||||
userRole.setYn(YnEnum.Y.getKey());
|
||||
return userRole;
|
||||
}).toList();
|
||||
|
||||
if (!CollectionUtils.isEmpty(userRoleList)) {
|
||||
sysUserRoleService.saveBatch(userRoleList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roleAddUser(Long roleId, Long userId, UserContext userContext) {
|
||||
if (roleId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "角色ID");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户ID");
|
||||
}
|
||||
SysRole sysRole = queryRoleById(roleId);
|
||||
if (sysRole == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleNotFoundWithRoleId, roleId);
|
||||
}
|
||||
User user = userService.getById(userId);
|
||||
if (user == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacUserNotFoundWithId, userId);
|
||||
}
|
||||
if (user.getRole() == null || !User.Role.Admin.equals(user.getRole())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleBindRequiresAdminUser,
|
||||
user.getUserName());
|
||||
}
|
||||
// 检查是否已绑定,避免重复插入
|
||||
LambdaQueryWrapper<SysUserRole> queryWrapper = Wrappers.<SysUserRole>lambdaQuery()
|
||||
.eq(SysUserRole::getRoleId, roleId)
|
||||
.eq(SysUserRole::getUserId, userId)
|
||||
.eq(SysUserRole::getYn, YnEnum.Y.getKey());
|
||||
if (sysUserRoleService.count(queryWrapper) > 0) {
|
||||
return;
|
||||
}
|
||||
SysUserRole userRole = new SysUserRole();
|
||||
userRole.setUserId(userId);
|
||||
userRole.setRoleId(roleId);
|
||||
userRole.setTenantId(userContext.getTenantId());
|
||||
userRole.setCreatorId(userContext.getUserId());
|
||||
userRole.setCreator(userContext.getUserName());
|
||||
userRole.setYn(YnEnum.Y.getKey());
|
||||
sysUserRoleService.save(userRole);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roleRemoveUser(Long roleId, Long userId, UserContext userContext) {
|
||||
if (roleId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "角色ID");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户ID");
|
||||
}
|
||||
LambdaUpdateWrapper<SysUserRole> wrapper = Wrappers.<SysUserRole>lambdaUpdate()
|
||||
.eq(SysUserRole::getRoleId, roleId)
|
||||
.eq(SysUserRole::getUserId, userId);
|
||||
sysUserRoleService.remove(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userBindRole(Long userId, List<Long> roleIds, UserContext userContext) {
|
||||
if (userId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "用户ID");
|
||||
}
|
||||
User user = userService.getById(userId);
|
||||
if (user == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacUserNotFoundWithId, userId);
|
||||
}
|
||||
if ((user.getRole() == null || !User.Role.Admin.equals(user.getRole())) && CollectionUtils.isNotEmpty(roleIds)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleBindRequiresAdminUser,
|
||||
user.getUserName());
|
||||
}
|
||||
|
||||
// 校验角色是否存在
|
||||
if (CollectionUtils.isNotEmpty(roleIds)) {
|
||||
Set<Long> distinctRoleIds = new HashSet<>(roleIds);
|
||||
List<SysRole> roles = sysRoleService.listByIds(distinctRoleIds);
|
||||
if (CollectionUtils.isEmpty(roles)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleNotFound);
|
||||
}
|
||||
Set<Long> existRoleIds = roles.stream().map(SysRole::getId).collect(Collectors.toSet());
|
||||
roleIds.forEach(roleId -> {
|
||||
if (!existRoleIds.contains(roleId)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleNotFoundWithRoleId, roleId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 物理删除原绑定关系
|
||||
LambdaUpdateWrapper<SysUserRole> wrapper = Wrappers.<SysUserRole>lambdaUpdate().eq(SysUserRole::getUserId, userId);
|
||||
sysUserRoleService.remove(wrapper);
|
||||
|
||||
if (CollectionUtils.isEmpty(roleIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SysUserRole> userRoleList = roleIds.stream().map(roleId -> {
|
||||
SysUserRole userRole = new SysUserRole();
|
||||
userRole.setUserId(userId);
|
||||
userRole.setRoleId(roleId);
|
||||
userRole.setTenantId(userContext.getTenantId());
|
||||
userRole.setCreatorId(userContext.getUserId());
|
||||
userRole.setCreator(userContext.getUserName());
|
||||
userRole.setYn(YnEnum.Y.getKey());
|
||||
return userRole;
|
||||
}).toList();
|
||||
|
||||
if (!CollectionUtils.isEmpty(userRoleList)) {
|
||||
sysUserRoleService.saveBatch(userRoleList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindMenu(RoleBindMenuModel model, UserContext userContext) {
|
||||
if (model == null || model.getRoleId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "角色ID");
|
||||
}
|
||||
|
||||
Long roleId = model.getRoleId();
|
||||
SysRole role = sysRoleService.getById(roleId);
|
||||
if (role == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRoleNotFound);
|
||||
}
|
||||
|
||||
List<MenuNode> menuBindResourceList = model.getMenuBindResourceList();
|
||||
if (CollectionUtils.isEmpty(menuBindResourceList)) {
|
||||
if (RoleEnum.SUPER_ADMIN.getCode().equals(role.getCode())) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemSuperAdminRequiresRoleMenuResources);
|
||||
}
|
||||
LambdaUpdateWrapper<SysRoleMenu> deleteWrapper = Wrappers.<SysRoleMenu>lambdaUpdate().eq(SysRoleMenu::getRoleId, roleId);
|
||||
sysRoleMenuService.remove(deleteWrapper);
|
||||
return;
|
||||
}
|
||||
|
||||
if (RoleEnum.SUPER_ADMIN.getCode().equals(role.getCode())) {
|
||||
validateSuperAdminNecessaryPermissions(menuBindResourceList);
|
||||
}
|
||||
|
||||
LambdaUpdateWrapper<SysRoleMenu> deleteWrapper = Wrappers.<SysRoleMenu>lambdaUpdate().eq(SysRoleMenu::getRoleId, roleId);
|
||||
sysRoleMenuService.remove(deleteWrapper);
|
||||
|
||||
List<SysMenu> allMenus = sysMenuDomainService.queryMenuList(null);
|
||||
List<SysResource> allResources = sysResourceDomainService.queryResourceList(null);
|
||||
MenuBindResourceHelper.MenuResourceMaps maps = menuBindResourceHelper.buildMenuAndResourceMaps(allMenus, allResources);
|
||||
Map<Long, Integer> menuBindTypeMap = menuBindResourceHelper.buildAndPropagateMenuBindTypeMap(menuBindResourceList, maps.menuChildrenMap);
|
||||
|
||||
// menuBindType=1(ALL) 表示该节点下所有子菜单均被绑定,但每个子菜单的资源绑定可能不同,需存每个子菜单并各自存 resource_tree_json
|
||||
List<SysRoleMenu> roleMenus = new ArrayList<>();
|
||||
for (MenuNode menuNode : menuBindResourceList) {
|
||||
Long menuId = menuNode.getId();
|
||||
if (menuId == null) {
|
||||
continue;
|
||||
}
|
||||
if (menuId != 0L && !maps.menuMap.containsKey(menuId)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemRbacMenuIdNotFound, menuId);
|
||||
}
|
||||
Integer menuBindType = menuBindTypeMap.getOrDefault(menuId, BindTypeEnum.NONE.getCode());
|
||||
if (menuBindType == null || BindTypeEnum.NONE.getCode().equals(menuBindType)) {
|
||||
continue;
|
||||
}
|
||||
SysRoleMenu roleMenu = new SysRoleMenu();
|
||||
roleMenu.setRoleId(roleId);
|
||||
roleMenu.setMenuId(menuId);
|
||||
roleMenu.setMenuBindType(menuBindType);
|
||||
List<ResourceNode> processedResourceTree = menuBindResourceHelper.processResourceTreeForMenu(
|
||||
menuId, menuNode.getResourceTree(), maps.resourceMap, maps.resourceChildrenMap);
|
||||
roleMenu.setResourceTreeJson(CollectionUtils.isEmpty(processedResourceTree)
|
||||
? null : JsonSerializeUtil.toJSONString(processedResourceTree));
|
||||
roleMenu.setTenantId(userContext.getTenantId());
|
||||
roleMenu.setCreatorId(userContext.getUserId());
|
||||
roleMenu.setCreator(userContext.getUserName());
|
||||
roleMenu.setYn(YnEnum.Y.getKey());
|
||||
roleMenus.add(roleMenu);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(roleMenus)) {
|
||||
sysRoleMenuService.saveBatch(roleMenus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 超级管理员角色绑定菜单时,必须包含必要权限,否则抛异常。
|
||||
*/
|
||||
private void validateSuperAdminNecessaryPermissions(List<MenuNode> menuBindResourceList) {
|
||||
List<SysMenu> allMenus = sysMenuDomainService.queryMenuList(null);
|
||||
List<SysResource> allResources = sysResourceDomainService.queryResourceList(null);
|
||||
if (CollectionUtils.isEmpty(allMenus) || CollectionUtils.isEmpty(allResources)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemSuperAdminRequiresRoleMenuResourcesAlt);
|
||||
}
|
||||
Map<String, SysMenu> menuCodeMap = allMenus.stream()
|
||||
.filter(m -> StringUtils.isNotBlank(m.getCode()))
|
||||
.collect(Collectors.toMap(SysMenu::getCode, m -> m, (a, b) -> a));
|
||||
Map<String, SysResource> allResourceCodeMap = allResources.stream()
|
||||
.filter(r -> StringUtils.isNotBlank(r.getCode()))
|
||||
.collect(Collectors.toMap(SysResource::getCode, r -> r, (a, b) -> a));
|
||||
MenuBindResourceHelper.MenuResourceMaps maps = menuBindResourceHelper.buildMenuAndResourceMaps(allMenus, allResources);
|
||||
Map<Long, Integer> menuBindTypeMap = menuBindResourceHelper.buildAndPropagateMenuBindTypeMap(menuBindResourceList, maps.menuChildrenMap);
|
||||
Map<Long, MenuNode> bindMap = menuBindResourceList.stream()
|
||||
.filter(n -> n.getId() != null)
|
||||
.collect(Collectors.toMap(MenuNode::getId, n -> n, (a, b) -> a));
|
||||
|
||||
List<String> necessaryMenuCodes = new ArrayList<>(PermissionConstants.superAdminNecessaryMenus);
|
||||
for (String menuCode : necessaryMenuCodes) {
|
||||
SysMenu menu = menuCodeMap.get(menuCode);
|
||||
if (menu == null || menu.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
Integer bindType = menuBindTypeMap.get(menu.getId());
|
||||
if (bindType == null || BindTypeEnum.NONE.getCode().equals(bindType)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemSuperAdminRequiresMenuByName,
|
||||
menu.getName());
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> necessaryResourceCodes = new HashSet<>(PermissionConstants.superAdminNecessaryResources);
|
||||
Set<Long> necessaryMenuIds = necessaryMenuCodes.stream()
|
||||
.map(menuCodeMap::get)
|
||||
.filter(Objects::nonNull)
|
||||
.map(SysMenu::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
List<SysMenuResource> allMenuResources = sysMenuDomainService.queryResourceListByMenuIds(necessaryMenuIds);
|
||||
Map<Long, Long> resourceIdToMenuId = new HashMap<>();
|
||||
for (SysMenuResource mr : allMenuResources) {
|
||||
if (mr.getResourceId() == null || mr.getMenuId() == null) {
|
||||
continue;
|
||||
}
|
||||
resourceIdToMenuId.put(mr.getResourceId(), mr.getMenuId());
|
||||
// resourceBindType=1(ALL) 时,子资源也视为该菜单下已授权,需纳入映射
|
||||
if (BindTypeEnum.ALL.getCode().equals(mr.getResourceBindType())) {
|
||||
Set<Long> descendantIds = new HashSet<>();
|
||||
MenuBindResourceHelper.collectChildrenResourceIds(mr.getResourceId(), maps.resourceChildrenMap, descendantIds);
|
||||
for (Long childId : descendantIds) {
|
||||
resourceIdToMenuId.putIfAbsent(childId, mr.getMenuId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (String resourceCode : necessaryResourceCodes) {
|
||||
SysResource resource = allResourceCodeMap.get(resourceCode);
|
||||
if (resource == null || resource.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemSuperAdminResourceCodeMissing,
|
||||
resourceCode);
|
||||
}
|
||||
Long menuId = resourceIdToMenuId.get(resource.getId());
|
||||
if (menuId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemSuperAdminResourceMenuOrphan,
|
||||
resource.getName());
|
||||
}
|
||||
MenuNode menuNode = bindMap.get(menuId);
|
||||
if (menuNode == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemSuperAdminBindIncomplete);
|
||||
}
|
||||
|
||||
List<ResourceNode> flatResources = menuNode.getFlattenResourceList();
|
||||
boolean hasValidBind = flatResources.stream()
|
||||
.anyMatch(r -> resource.getId().equals(r.getId())
|
||||
&& r.getResourceBindType() != null
|
||||
&& !BindTypeEnum.NONE.getCode().equals(r.getResourceBindType()));
|
||||
if (!hasValidBind) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemSuperAdminBindIncomplete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<MenuNode> getMenuTreeByRoleId(Long roleId) {
|
||||
if (roleId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "角色ID");
|
||||
}
|
||||
|
||||
// 查询角色绑定的菜单关系
|
||||
List<SysRoleMenu> roleMenus = sysRoleMenuService.list(Wrappers.<SysRoleMenu>lambdaQuery()
|
||||
.eq(SysRoleMenu::getRoleId, roleId)
|
||||
.eq(SysRoleMenu::getYn, YnEnum.Y.getKey()));
|
||||
// menuId -> 绑定记录(包含 menu_id = 0 的 root 绑定)
|
||||
Map<Long, SysRoleMenu> roleMenuMap = CollectionUtils.isEmpty(roleMenus)
|
||||
? new HashMap<>()
|
||||
: roleMenus.stream()
|
||||
.filter(rm -> rm.getMenuId() != null)
|
||||
.collect(Collectors.toMap(SysRoleMenu::getMenuId, rm -> rm, (a, b) -> a));
|
||||
|
||||
// 查询所有有效菜单
|
||||
List<SysMenu> allMenus = sysMenuDomainService.queryMenuList(null);
|
||||
if (CollectionUtils.isEmpty(allMenus)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 先把 root 菜单(menuId = 0)加入到全量菜单中,让其参与自上而下、自下而上的打标逻辑
|
||||
boolean hasRootMenu = allMenus.stream()
|
||||
.anyMatch(menu -> menu.getId() != null && menu.getId() == 0L);
|
||||
if (!hasRootMenu) {
|
||||
SysMenu rootMenu = MenuBindResourceHelper.getRootMenu();
|
||||
allMenus.add(rootMenu);
|
||||
}
|
||||
|
||||
// 查询所有有效资源(用于构建完整资源树)
|
||||
List<SysResource> allResources = sysResourceDomainService.queryResourceList(null);
|
||||
|
||||
// 基于绑定记录 + 全量菜单/资源构建基础权限信息(含自上而下传播)
|
||||
List<MenuNode> menuNodes = MenuAuthHelper.buildMenuListWithAuth(
|
||||
roleMenus,
|
||||
SysRoleMenu::getMenuId,
|
||||
SysRoleMenu::getMenuBindType,
|
||||
SysRoleMenu::getResourceTreeJson,
|
||||
allMenus,
|
||||
allResources,
|
||||
sysMenuDomainService
|
||||
);
|
||||
|
||||
// 填充菜单信息和资源详细信息
|
||||
Map<Long, SysMenu> allMenuMap = allMenus.stream().collect(Collectors.toMap(SysMenu::getId, m -> m));
|
||||
Map<Long, SysResource> allResourceMap = CollectionUtils.isEmpty(allResources)
|
||||
? new HashMap<>()
|
||||
: allResources.stream().collect(Collectors.toMap(SysResource::getId, r -> r));
|
||||
|
||||
List<MenuNode> filledMenuList = new ArrayList<>();
|
||||
if (!CollectionUtils.isEmpty(menuNodes)) {
|
||||
for (MenuNode menuNode : menuNodes) {
|
||||
Long menuId = menuNode.getId();
|
||||
if (menuId != null) {
|
||||
SysMenu menu = allMenuMap.get(menuId);
|
||||
if (menu == null) {
|
||||
// 菜单已被删除,忽略
|
||||
continue;
|
||||
}
|
||||
|
||||
// 填充菜单基本信息
|
||||
menuNode.setParentId(menu.getParentId());
|
||||
menuNode.setCode(menu.getCode());
|
||||
menuNode.setName(menu.getName());
|
||||
menuNode.setDescription(menu.getDescription());
|
||||
menuNode.setSource(menu.getSource());
|
||||
menuNode.setPath(menu.getPath());
|
||||
menuNode.setOpenType(menu.getOpenType());
|
||||
menuNode.setIcon(menu.getIcon());
|
||||
menuNode.setSortIndex(menu.getSortIndex());
|
||||
menuNode.setStatus(menu.getStatus());
|
||||
}
|
||||
|
||||
// 填充资源树中的资源详细信息
|
||||
if (CollectionUtils.isNotEmpty(menuNode.getResourceTree())) {
|
||||
MenuBindResourceHelper.fillResourceTreeDetails(menuNode.getResourceTree(), allResourceMap);
|
||||
}
|
||||
|
||||
filledMenuList.add(menuNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (CollectionUtils.isEmpty(filledMenuList)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 构建菜单树
|
||||
MenuBindResourceHelper.buildMenuTree(filledMenuList);
|
||||
|
||||
// 获取/构建 root 节点(menuId = 0)
|
||||
MenuNode rootNode = filledMenuList.stream()
|
||||
.filter(node -> node.getId() != null && node.getId() == 0L)
|
||||
.findFirst()
|
||||
.orElseGet(() -> {
|
||||
SysMenu rootMenu = MenuBindResourceHelper.getRootMenu();
|
||||
MenuNode r = new MenuNode();
|
||||
BeanUtils.copyProperties(rootMenu, r);
|
||||
r.setChildren(filledMenuList);
|
||||
return r;
|
||||
});
|
||||
|
||||
// 如果绑定关系中存在 menu_id = 0 的记录,则直接使用该记录的 menuBindType
|
||||
SysRoleMenu rootBinding = roleMenuMap.get(0L);
|
||||
if (rootBinding != null && rootBinding.getMenuBindType() != null) {
|
||||
rootNode.setMenuBindType(rootBinding.getMenuBindType());
|
||||
}
|
||||
|
||||
// 自下而上打标 menuBindType(仅对“未显式绑定”的菜单和 root 进行合成打标)
|
||||
Set<Long> explicitlyBoundMenuIds = CollectionUtils.isEmpty(roleMenus)
|
||||
? new HashSet<>()
|
||||
: roleMenus.stream()
|
||||
.filter(rm -> rm.getMenuId() != null)
|
||||
.map(SysRoleMenu::getMenuId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
MenuBindResourceHelper.adjustMenuBindTypeBottomUp(rootNode, explicitlyBoundMenuIds);
|
||||
|
||||
// 如果 root 的 menuBindType 为 ALL 或 NONE,则按规则将该类型自上而下强制传播到所有子菜单
|
||||
MenuBindResourceHelper.propagateRootMenuBindType(rootNode);
|
||||
|
||||
List<MenuNode> result = new ArrayList<>();
|
||||
result.add(rootNode);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.xspaceagi.system.domain.service.SysSubjectPermissionDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.SysSubjectPermission;
|
||||
import com.xspaceagi.system.infra.dao.service.SysSubjectPermissionService;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.enums.PermissionSubjectTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.PermissionTargetTypeEnum;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.enums.YnEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class SysSubjectPermissionDomainServiceImpl implements SysSubjectPermissionDomainService {
|
||||
|
||||
@Resource
|
||||
private SysSubjectPermissionService sysSubjectPermissionService;
|
||||
|
||||
@Override
|
||||
public List<SysSubjectPermission> getBySubject(PermissionSubjectTypeEnum subjectType, Long subjectId) {
|
||||
if (subjectType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "主体类型");
|
||||
}
|
||||
if (subjectId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "主体ID");
|
||||
}
|
||||
return sysSubjectPermissionService.list(Wrappers.<SysSubjectPermission>lambdaQuery()
|
||||
.eq(SysSubjectPermission::getSubjectType, subjectType.getCode())
|
||||
.eq(SysSubjectPermission::getSubjectId, subjectId)
|
||||
.eq(SysSubjectPermission::getYn, YnEnum.Y.getKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> listSubjectIdsByTarget(PermissionTargetTypeEnum targetType, Long targetId, PermissionSubjectTypeEnum subjectType) {
|
||||
if (targetType == null || subjectType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标类型/主体类型");
|
||||
}
|
||||
if (targetId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标ID");
|
||||
}
|
||||
return sysSubjectPermissionService.list(Wrappers.<SysSubjectPermission>lambdaQuery()
|
||||
.eq(SysSubjectPermission::getTargetType, targetType.getCode())
|
||||
.eq(SysSubjectPermission::getTargetId, targetId)
|
||||
.eq(SysSubjectPermission::getSubjectType, subjectType.getCode())
|
||||
.eq(SysSubjectPermission::getYn, YnEnum.Y.getKey()))
|
||||
.stream()
|
||||
.map(SysSubjectPermission::getSubjectId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listSubjectKeysByTarget(PermissionTargetTypeEnum targetType, Long targetId, PermissionSubjectTypeEnum subjectType) {
|
||||
if (targetType == null || subjectType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标类型/主体类型");
|
||||
}
|
||||
if (targetId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标ID");
|
||||
}
|
||||
return sysSubjectPermissionService.list(Wrappers.<SysSubjectPermission>lambdaQuery()
|
||||
.eq(SysSubjectPermission::getTargetType, targetType.getCode())
|
||||
.eq(SysSubjectPermission::getTargetId, targetId)
|
||||
.eq(SysSubjectPermission::getSubjectType, subjectType.getCode())
|
||||
.eq(SysSubjectPermission::getYn, YnEnum.Y.getKey()))
|
||||
.stream()
|
||||
.map(SysSubjectPermission::getSubjectKey)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> listSubjectKeyConfigByTarget(PermissionTargetTypeEnum targetType, Long targetId, PermissionSubjectTypeEnum subjectType) {
|
||||
if (targetType == null || subjectType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标类型/主体类型");
|
||||
}
|
||||
if (targetId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标ID");
|
||||
}
|
||||
return sysSubjectPermissionService.list(Wrappers.<SysSubjectPermission>lambdaQuery()
|
||||
.eq(SysSubjectPermission::getTargetType, targetType.getCode())
|
||||
.eq(SysSubjectPermission::getTargetId, targetId)
|
||||
.eq(SysSubjectPermission::getSubjectType, subjectType.getCode())
|
||||
.eq(SysSubjectPermission::getYn, YnEnum.Y.getKey()))
|
||||
.stream()
|
||||
.filter(item -> item != null && StringUtils.isNotBlank(item.getSubjectKey()))
|
||||
.collect(Collectors.toMap(
|
||||
SysSubjectPermission::getSubjectKey,
|
||||
SysSubjectPermission::getConfig,
|
||||
(a, b) -> b,
|
||||
LinkedHashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replaceSubjectsByTargetAndSubjectIds(PermissionTargetTypeEnum targetType, Long targetId,
|
||||
PermissionSubjectTypeEnum subjectType, List<Long> subjectIds,
|
||||
UserContext userContext) {
|
||||
if (targetType == null || subjectType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标类型/主体类型");
|
||||
}
|
||||
if (targetId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标ID");
|
||||
}
|
||||
|
||||
// 全量覆盖:先删除旧绑定
|
||||
sysSubjectPermissionService.remove(Wrappers.<SysSubjectPermission>lambdaQuery()
|
||||
.eq(SysSubjectPermission::getTargetType, targetType.getCode())
|
||||
.eq(SysSubjectPermission::getTargetId, targetId)
|
||||
.eq(SysSubjectPermission::getSubjectType, subjectType.getCode())
|
||||
.eq(SysSubjectPermission::getYn, YnEnum.Y.getKey()));
|
||||
|
||||
if (CollectionUtils.isEmpty(subjectIds)) {
|
||||
return;
|
||||
}
|
||||
// 去重后插入
|
||||
Set<Long> uniq = new HashSet<>();
|
||||
for (Long sid : subjectIds) {
|
||||
if (sid != null) {
|
||||
uniq.add(sid);
|
||||
}
|
||||
}
|
||||
for (Long sid : uniq) {
|
||||
addTarget(subjectType, sid, null, targetType, targetId, userContext);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replaceSubjectsByTargetAndSubjectKeyConfig(PermissionTargetTypeEnum targetType, Long targetId, PermissionSubjectTypeEnum subjectType, Map<String, String> subjectKeyConfigMap, UserContext userContext) {
|
||||
if (targetType == null || subjectType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标类型/主体类型");
|
||||
}
|
||||
if (targetId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标ID");
|
||||
}
|
||||
sysSubjectPermissionService.remove(Wrappers.<SysSubjectPermission>lambdaQuery()
|
||||
.eq(SysSubjectPermission::getTargetType, targetType.getCode())
|
||||
.eq(SysSubjectPermission::getTargetId, targetId)
|
||||
.eq(SysSubjectPermission::getSubjectType, subjectType.getCode())
|
||||
.eq(SysSubjectPermission::getYn, YnEnum.Y.getKey()));
|
||||
if (subjectKeyConfigMap == null || subjectKeyConfigMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<String, String> entry : subjectKeyConfigMap.entrySet()) {
|
||||
String skey = entry.getKey();
|
||||
if (StringUtils.isBlank(skey)) {
|
||||
continue;
|
||||
}
|
||||
SysSubjectPermission toSave = new SysSubjectPermission();
|
||||
toSave.setSubjectType(subjectType.getCode());
|
||||
toSave.setSubjectId(null);
|
||||
toSave.setSubjectKey(skey);
|
||||
toSave.setTargetType(targetType.getCode());
|
||||
toSave.setTargetId(targetId);
|
||||
toSave.setConfig(normalizeSubjectConfig(entry.getValue()));
|
||||
toSave.setCreatorId(userContext.getUserId());
|
||||
toSave.setCreator(userContext.getUserName());
|
||||
toSave.setYn(YnEnum.Y.getKey());
|
||||
sysSubjectPermissionService.save(toSave);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeSubjectConfig(String rawConfig) {
|
||||
if (StringUtils.isBlank(rawConfig)) {
|
||||
return rawConfig;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> raw = JsonSerializeUtil.parseObject(rawConfig, new TypeReference<Map<String, Object>>() {});
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return rawConfig;
|
||||
}
|
||||
Map<String, Object> normalized = new LinkedHashMap<>();
|
||||
normalized.put("rpm", raw.get("rpm"));
|
||||
normalized.put("rpd", raw.get("rpd"));
|
||||
return JsonSerializeUtil.toJSONString(normalized);
|
||||
} catch (Exception e) {
|
||||
return rawConfig;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTarget(PermissionSubjectTypeEnum subjectType, Long subjectId, String subjectKey,
|
||||
PermissionTargetTypeEnum targetType, Long targetId,
|
||||
UserContext userContext) {
|
||||
if (subjectType == null || targetType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "主体类型/目标类型");
|
||||
}
|
||||
if (targetId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "目标ID");
|
||||
}
|
||||
if (subjectId == null && StringUtils.isBlank(subjectKey)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "主体ID/key");
|
||||
}
|
||||
|
||||
SysSubjectPermission exist = sysSubjectPermissionService.getOne(Wrappers.<SysSubjectPermission>lambdaQuery()
|
||||
.eq(SysSubjectPermission::getSubjectType, subjectType.getCode())
|
||||
.eq(subjectId != null, SysSubjectPermission::getSubjectId, subjectId)
|
||||
.eq(StringUtils.isNotBlank(subjectKey), SysSubjectPermission::getSubjectKey, subjectKey)
|
||||
.eq(SysSubjectPermission::getTargetType, targetType.getCode())
|
||||
.eq(SysSubjectPermission::getTargetId, targetId)
|
||||
.eq(SysSubjectPermission::getYn, YnEnum.Y.getKey()));
|
||||
if (exist != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SysSubjectPermission toSave = new SysSubjectPermission();
|
||||
toSave.setSubjectType(subjectType.getCode());
|
||||
toSave.setSubjectId(subjectId);
|
||||
toSave.setSubjectKey(subjectKey);
|
||||
toSave.setTargetType(targetType.getCode());
|
||||
toSave.setTargetId(targetId);
|
||||
toSave.setCreatorId(userContext.getUserId());
|
||||
toSave.setCreator(userContext.getUserName());
|
||||
toSave.setYn(YnEnum.Y.getKey());
|
||||
sysSubjectPermissionService.save(toSave);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeTarget(PermissionSubjectTypeEnum subjectType, Long subjectId,
|
||||
PermissionTargetTypeEnum targetType, Long targetId,
|
||||
UserContext userContext) {
|
||||
if (subjectType == null || targetType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "主体类型/目标类型");
|
||||
}
|
||||
if (subjectId == null || targetId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "主体ID/目标ID");
|
||||
}
|
||||
|
||||
sysSubjectPermissionService.remove(Wrappers.<SysSubjectPermission>lambdaQuery()
|
||||
.eq(SysSubjectPermission::getSubjectType, subjectType.getCode())
|
||||
.eq(SysSubjectPermission::getSubjectId, subjectId)
|
||||
.eq(SysSubjectPermission::getTargetType, targetType.getCode())
|
||||
.eq(SysSubjectPermission::getTargetId, targetId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replaceTargetsBySubject(PermissionSubjectTypeEnum subjectType, Long subjectId,
|
||||
List<Long> roleIds, List<Long> groupIds,
|
||||
UserContext userContext) {
|
||||
if (subjectType == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "主体类型");
|
||||
}
|
||||
if (subjectId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "主体ID");
|
||||
}
|
||||
|
||||
// 全量覆盖:先删除该主体下所有目标绑定
|
||||
sysSubjectPermissionService.remove(Wrappers.<SysSubjectPermission>lambdaQuery()
|
||||
.eq(SysSubjectPermission::getSubjectType, subjectType.getCode())
|
||||
.eq(SysSubjectPermission::getSubjectId, subjectId)
|
||||
.eq(SysSubjectPermission::getYn, YnEnum.Y.getKey()));
|
||||
|
||||
if (CollectionUtils.isEmpty(roleIds) && CollectionUtils.isEmpty(groupIds)) {
|
||||
return;
|
||||
}
|
||||
Set<Long> uniqRoleIds = new HashSet<>();
|
||||
if (roleIds != null) {
|
||||
roleIds.stream().filter(Objects::nonNull).forEach(uniqRoleIds::add);
|
||||
}
|
||||
Set<Long> uniqGroupIds = new HashSet<>();
|
||||
if (groupIds != null) {
|
||||
groupIds.stream().filter(Objects::nonNull).forEach(uniqGroupIds::add);
|
||||
}
|
||||
for (Long roleId : uniqRoleIds) {
|
||||
addTarget(subjectType, subjectId, null, PermissionTargetTypeEnum.ROLE, roleId, userContext);
|
||||
}
|
||||
for (Long groupId : uniqGroupIds) {
|
||||
addTarget(subjectType, subjectId, null, PermissionTargetTypeEnum.GROUP, groupId, userContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.xspaceagi.system.domain.service.UserDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.Tenant;
|
||||
import com.xspaceagi.system.infra.dao.entity.User;
|
||||
import com.xspaceagi.system.infra.dao.service.TenantService;
|
||||
import com.xspaceagi.system.infra.dao.service.UserService;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.enums.TenantStatus;
|
||||
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
|
||||
import com.xspaceagi.system.spec.utils.MD5;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class UserDomainServiceImpl implements UserDomainService {
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Resource
|
||||
private TenantService tenantService;
|
||||
|
||||
@Override
|
||||
public void add(User user) {
|
||||
user.setPassword(MD5.StrongMD5Encode(user.getPassword()));
|
||||
userService.save(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(User user) {
|
||||
if (StringUtils.isNotBlank(user.getPassword())) {
|
||||
user.setPassword(MD5.StrongMD5Encode(user.getPassword()));
|
||||
}
|
||||
userService.updateById(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long userId) {
|
||||
userService.removeById(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User queryById(Long userId) {
|
||||
RequestContext.addTenantIgnoreEntity(User.class);
|
||||
try {
|
||||
return userService.getById(userId);
|
||||
} finally {
|
||||
RequestContext.removeTenantIgnoreEntity(User.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public User queryByEmail(String email) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getEmail, email);
|
||||
return userService.getOne(queryWrapper, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User queryByPhone(String phone) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getPhone, phone);
|
||||
return userService.getOne(queryWrapper, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User queryByPhoneAndPassword(String phone, String password) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getPhone, phone);
|
||||
queryWrapper.eq(User::getPassword, MD5.StrongMD5Encode(password));
|
||||
return userService.getOne(queryWrapper, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User queryByUserName(String userName) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getUserName, userName);
|
||||
return userService.getOne(queryWrapper, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User queryUserByUid(String uid) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getUid, uid);
|
||||
return userService.getOne(queryWrapper, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> queryUserListByIds(List<Long> userIds) {
|
||||
if (userIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.in(User::getId, userIds);
|
||||
RequestContext.addTenantIgnoreEntity(User.class);
|
||||
try {
|
||||
return userService.list(queryWrapper);
|
||||
} finally {
|
||||
RequestContext.removeTenantIgnoreEntity(User.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> queryUserIdList(Long lastId, Integer size) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.lt(User::getId, lastId);
|
||||
queryWrapper.orderByDesc(User::getId);
|
||||
queryWrapper.select(User::getId, User::getUserName);
|
||||
queryWrapper.last("limit " + size);
|
||||
return userService.list(queryWrapper).stream().map(User::getId).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> queryUserListByUids(List<String> uids) {
|
||||
if (uids.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 忽略租户
|
||||
return TenantFunctions.callWithIgnoreCheck(() -> {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.in(User::getUid, uids);
|
||||
return userService.list(queryWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Tenant> queryTenantsByStatus(TenantStatus status) {
|
||||
// 查询指定状态的租户信息需要忽略租户隔离;当前租户状态忽略
|
||||
return TenantFunctions.callWithIgnoreCheck(tenantService::list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.xspaceagi.system.domain.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.xspaceagi.system.domain.service.UserMetricDomainService;
|
||||
import com.xspaceagi.system.infra.dao.entity.UserMetric;
|
||||
import com.xspaceagi.system.infra.dao.service.UserMetricService;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.enums.PeriodTypeEnum;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UserMetricDomainServiceImpl implements UserMetricDomainService {
|
||||
|
||||
@Resource
|
||||
private UserMetricService userMetricService;
|
||||
|
||||
@Override
|
||||
public void add(UserMetric userMetric) {
|
||||
userMetric.setCreated(new Date());
|
||||
userMetric.setModified(new Date());
|
||||
userMetricService.save(userMetric);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(UserMetric userMetric) {
|
||||
userMetric.setModified(new Date());
|
||||
userMetricService.updateById(userMetric);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserMetric queryById(Long id) {
|
||||
return userMetricService.getById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserMetric> queryByUserId(Long userId, String period) {
|
||||
LambdaQueryWrapper<UserMetric> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(UserMetric::getUserId, userId);
|
||||
queryWrapper.eq(UserMetric::getPeriod, period);
|
||||
queryWrapper.orderByDesc(UserMetric::getPeriodType)
|
||||
.orderByDesc(UserMetric::getPeriod);
|
||||
return userMetricService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserMetric queryByUniqueKey(Long tenantId, Long userId, String bizType, PeriodTypeEnum periodType, String period) {
|
||||
LambdaQueryWrapper<UserMetric> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(UserMetric::getTenantId, tenantId);
|
||||
queryWrapper.eq(UserMetric::getUserId, userId);
|
||||
queryWrapper.eq(UserMetric::getBizType, bizType);
|
||||
queryWrapper.eq(UserMetric::getPeriodType, periodType.getCode());
|
||||
queryWrapper.eq(UserMetric::getPeriod, period);
|
||||
return userMetricService.getOne(queryWrapper, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incrementValue(Long userId, String bizType, PeriodTypeEnum periodType, String period, BigDecimal delta) {
|
||||
UserMetric userMetric = queryByUniqueKey(RequestContext.get().getTenantId(), userId, bizType, periodType, period);
|
||||
if (userMetric == null) {
|
||||
userMetric = new UserMetric();
|
||||
userMetric.setUserId(userId);
|
||||
userMetric.setBizType(bizType);
|
||||
userMetric.setPeriodType(periodType.getCode());
|
||||
userMetric.setPeriod(period);
|
||||
userMetric.setValue(delta);
|
||||
userMetric.setCreated(new Date());
|
||||
userMetric.setModified(new Date());
|
||||
try {
|
||||
return userMetricService.save(userMetric);
|
||||
} catch (Exception e) {
|
||||
log.debug("save user metric error: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
userMetric.setValue(userMetric.getValue().add(delta));
|
||||
userMetric.setModified(new Date());
|
||||
return userMetricService.updateById(userMetric);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.xspaceagi.system.domain.track;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum ActionTypeEnum {
|
||||
|
||||
EXPORT("导出"),
|
||||
PRINT("打印"),
|
||||
ADD("新增"),
|
||||
EFFECT("审核"),
|
||||
DELETE("删除"),
|
||||
MODIFY("修改"),
|
||||
COMMIT("提交"),
|
||||
HANDLE("受理"),
|
||||
CLOSE("关单"),
|
||||
;
|
||||
|
||||
private final String desc;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.xspaceagi.system.domain.track.reporter;
|
||||
|
||||
|
||||
/**
|
||||
* 日志上报常量
|
||||
*/
|
||||
public class LogConstant {
|
||||
|
||||
|
||||
public static final String PROJECT = "p1";
|
||||
|
||||
public static final String MODULE = "系统管理";
|
||||
|
||||
|
||||
public static final String STATUS_success = "成功";
|
||||
|
||||
public static final String STATUS_FAIL = "失败";
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package com.xspaceagi.system.domain.track.reporter;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.xspaceagi.system.sdk.operate.OperateTypeEnum;
|
||||
import com.xspaceagi.system.sdk.operate.OperationLogReporter;
|
||||
import com.xspaceagi.system.sdk.server.ITrackerReportService;
|
||||
import com.xspaceagi.system.spec.common.LoginWrapper;
|
||||
import com.xspaceagi.system.spec.common.OperatorLogContext;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.common.UserContext;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.AfterThrowing;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 日志上报切面
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class OperateLogAspect {
|
||||
@Autowired
|
||||
private LoginWrapper loginWrapper;
|
||||
|
||||
@Resource
|
||||
private ITrackerReportService trackerReportService;
|
||||
|
||||
/**
|
||||
* SpEL 表达式解析器
|
||||
*/
|
||||
private final ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
/**
|
||||
* 参数名发现器
|
||||
*/
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
@AfterReturning(returning = "res", pointcut = "@annotation(operationLogReporter)")
|
||||
public void afterReturn(JoinPoint joinPoint, Object res,
|
||||
OperationLogReporter operationLogReporter) {
|
||||
try {
|
||||
//获取用户Id
|
||||
|
||||
var userContext = Optional.ofNullable(RequestContext.get())
|
||||
.map(RequestContext::getUserContext)
|
||||
.orElseGet(() ->
|
||||
UserContext.builder()
|
||||
.userId(0L)
|
||||
.userName("无法获取")
|
||||
.build()
|
||||
);
|
||||
Long userId = userContext.getUserId();
|
||||
String userName = userContext.getUserName();
|
||||
|
||||
//获取IP
|
||||
String remoteAddr = "";
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
remoteAddr = request.getRemoteAddr();
|
||||
}
|
||||
// 获取请求参数
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
Enumeration<String> parameterNames = request.getParameterNames();
|
||||
while (parameterNames.hasMoreElements()) {
|
||||
String paramName = parameterNames.nextElement();
|
||||
String paramValue = request.getParameter(paramName);
|
||||
params.put(paramName, paramValue);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建额外数据对象(同时包含 HTTP 参数和 SpEL 提取的参数)
|
||||
String extraContent = "";
|
||||
try {
|
||||
OperateLogExtraData.OperateLogExtraDataBuilder extraDataBuilder = OperateLogExtraData.builder()
|
||||
.httpParams(params);
|
||||
|
||||
// 如果配置了 SpEL 表达式,提取 SpEL 参数
|
||||
if (StringUtils.hasText(operationLogReporter.spelExpression())) {
|
||||
Object spelData = extractSpelData(joinPoint, operationLogReporter.spelExpression());
|
||||
extraDataBuilder.spelData(spelData)
|
||||
.spelExpression(operationLogReporter.spelExpression());
|
||||
}
|
||||
|
||||
extraContent = JSON.toJSONString(extraDataBuilder.build());
|
||||
} catch (Exception e) {
|
||||
log.error("参数转换异常", e);
|
||||
}
|
||||
|
||||
var resp = (ReqResult<?>) res;
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
log.debug("uuid:{}", uuid);
|
||||
//1:操作类型;2:访问日志; 查询操作,默认都是访问日志
|
||||
var actionType = operationLogReporter.actionType();
|
||||
log.debug("actionType:{}", actionType.getName());
|
||||
var operateType = switch (actionType) {
|
||||
case QUERY -> OperateTypeEnum.ACCESS.getType();
|
||||
default -> OperateTypeEnum.OPERATE.getType();
|
||||
};
|
||||
|
||||
|
||||
OperatorLogContext vo = OperatorLogContext.builder()
|
||||
.clientTime(System.currentTimeMillis())
|
||||
.operateType(operateType)
|
||||
.userId(userId)
|
||||
.creator(userName)
|
||||
.orgId(userContext.getOrgId())
|
||||
.orgName(userContext.getOrgName())
|
||||
.systemCode(operationLogReporter.systemCode().toString())
|
||||
.systemName(operationLogReporter.systemCode().getDesc())
|
||||
.object(operationLogReporter.objectName())
|
||||
.ip(remoteAddr)
|
||||
.action(operationLogReporter.actionType().getName())
|
||||
.operateContent(operationLogReporter.action())
|
||||
.extraContent(extraContent)
|
||||
.tenantId(userContext.getTenantId())
|
||||
.build();
|
||||
|
||||
|
||||
//文件下载流 ,resp 是void,为空
|
||||
if (Objects.isNull(resp) || "0000".equals(resp.getCode())) {
|
||||
vo.setStatus(LogConstant.STATUS_success);
|
||||
} else {
|
||||
vo.setStatus(LogConstant.STATUS_FAIL);
|
||||
}
|
||||
|
||||
Optional<OperationLogContext> operationLogContextOptional = OperationLogContextHandler.get();
|
||||
if (operationLogContextOptional.isPresent()) {
|
||||
OperationLogContext operationLogContext = operationLogContextOptional.get();
|
||||
if (Objects.nonNull(operationLogContext.getOrgId())) {
|
||||
vo.setOrgId(operationLogContext.getOrgId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.trackerReportService.reportLog(vo);
|
||||
|
||||
OperationLogContextHandler.remove();
|
||||
} catch (Exception e) {
|
||||
log.error("Log report error: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@AfterThrowing(throwing = "ex", pointcut = "@annotation(operationLogReporter)")
|
||||
public void afterThrow(JoinPoint joinPoint, Throwable ex, OperationLogReporter operationLogReporter) {
|
||||
try {
|
||||
//获取用户Id
|
||||
var userContext = Optional.ofNullable(RequestContext.get())
|
||||
.map(RequestContext::getUserContext)
|
||||
.orElseGet(() ->
|
||||
UserContext.builder()
|
||||
.userId(0L)
|
||||
.userName("无法获取")
|
||||
.build()
|
||||
);
|
||||
Long userId = userContext.getUserId();
|
||||
String userName = userContext.getUserName();
|
||||
|
||||
//获取IP
|
||||
String remoteAddr = "";
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
remoteAddr = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
log.info("uuid:{}", uuid);
|
||||
|
||||
//1:操作类型;2:访问日志; 查询操作,默认都是访问日志
|
||||
var actionType = operationLogReporter.actionType();
|
||||
log.debug("actionType:{}", actionType.getName());
|
||||
var operateType = switch (actionType) {
|
||||
case QUERY -> OperateTypeEnum.ACCESS.getType();
|
||||
default -> OperateTypeEnum.OPERATE.getType();
|
||||
};
|
||||
OperatorLogContext vo = OperatorLogContext.builder()
|
||||
.clientTime(System.currentTimeMillis())
|
||||
.operateType(operateType)
|
||||
.userId(userId)
|
||||
.creator(userName)
|
||||
.orgId(userContext.getOrgId())
|
||||
.orgName(userContext.getOrgName())
|
||||
.systemCode(operationLogReporter.systemCode().toString())
|
||||
.systemName(operationLogReporter.systemCode().getDesc())
|
||||
.object(operationLogReporter.objectName())
|
||||
.ip(remoteAddr)
|
||||
.action(operationLogReporter.actionType().getName())
|
||||
.operateContent(operationLogReporter.action())
|
||||
.extraContent(operationLogReporter.objectName())
|
||||
.tenantId(userContext.getTenantId())
|
||||
.build();
|
||||
|
||||
|
||||
this.trackerReportService.reportLog(vo);
|
||||
|
||||
OperationLogContextHandler.remove();
|
||||
} catch (Exception e) {
|
||||
log.error("Log report error: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 SpEL 表达式提取方法参数中的特定数据
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param spelExpression SpEL 表达式
|
||||
* @return 提取的数据对象(非 JSON 字符串)
|
||||
*/
|
||||
private Object extractSpelData(JoinPoint joinPoint, String spelExpression) {
|
||||
try {
|
||||
// 获取方法参数名
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
String[] parameterNames = parameterNameDiscoverer.getParameterNames(signature.getMethod());
|
||||
if (parameterNames == null) {
|
||||
log.warn("无法获取方法参数名,SpEL 表达式无法执行");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建 SpEL 上下文
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
for (int i = 0; i < parameterNames.length; i++) {
|
||||
context.setVariable(parameterNames[i], args[i]);
|
||||
}
|
||||
|
||||
// 执行 SpEL 表达式并返回原始对象
|
||||
return parser.parseExpression(spelExpression).getValue(context);
|
||||
} catch (Exception e) {
|
||||
log.error("SpEL 表达式解析失败: {}", spelExpression, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.xspaceagi.system.domain.track.reporter;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 操作日志额外数据
|
||||
* 用于同时记录 HTTP 请求参数和 SpEL 表达式提取的参数
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OperateLogExtraData {
|
||||
|
||||
/**
|
||||
* HTTP 请求参数(从 request.getParameterMap() 获取)
|
||||
*/
|
||||
private Map<String, String> httpParams;
|
||||
|
||||
/**
|
||||
* SpEL 表达式提取的业务参数
|
||||
*/
|
||||
private Object spelData;
|
||||
|
||||
/**
|
||||
* SpEL 表达式(用于记录使用了哪个表达式)
|
||||
*/
|
||||
private String spelExpression;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.xspaceagi.system.domain.track.reporter;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
public class OperationLogContext {
|
||||
|
||||
private String objectId;
|
||||
|
||||
private String objectName;
|
||||
|
||||
private String preValue;
|
||||
|
||||
private String laterValue;
|
||||
|
||||
private String orgCode;
|
||||
|
||||
/**
|
||||
* 机构ID
|
||||
*/
|
||||
private Long orgId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.xspaceagi.system.domain.track.reporter;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class OperationLogContextHandler {
|
||||
|
||||
private static final ThreadLocal<OperationLogContext> threadLocal = new ThreadLocal<>();
|
||||
|
||||
|
||||
public static void set(OperationLogContext context) {
|
||||
threadLocal.set(context);
|
||||
}
|
||||
|
||||
public static Optional<OperationLogContext> get() {
|
||||
return Optional.ofNullable(threadLocal.get());
|
||||
}
|
||||
|
||||
public static void remove() {
|
||||
threadLocal.remove();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user