chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
/**
* Cache.
*/
package com.xspaceagi.system.domain.facade.cache;

View File

@@ -0,0 +1,4 @@
/**
* Distributed Lock.
*/
package com.xspaceagi.system.domain.facade.lock;

View File

@@ -0,0 +1,4 @@
/**
* Message Queue.
*/
package com.xspaceagi.system.domain.facade.mq;

View File

@@ -0,0 +1,7 @@
/**
* DDD Domain层和Infrastructure的粘合剂通过倒置依赖.
*
* <p>domain层声明需要基础设施层实现的接口RPC, DB, Cache, MQ等.</p>
* <p>为了方便产品人员查看领域层代码梳理业务统一放在facade package减少对产品同学的干扰.</p>
*/
package com.xspaceagi.system.domain.facade;

View File

@@ -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);
}
}

View File

@@ -0,0 +1,4 @@
/**
* RPC.
*/
package com.xspaceagi.system.domain.facade.rpc;

View File

@@ -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 {
}

View File

@@ -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);
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 "";
}

View File

@@ -0,0 +1,6 @@
package com.xspaceagi.system.domain.log;
public enum LogType {
NORMAL,
MQ;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
/**
* 父级ID0表示根节点null表示不修改无层级则忽略
*/
private Long parentId;
/**
* 排序索引null表示不修改
*/
private Integer sortIndex;
}

View File

@@ -0,0 +1,4 @@
/**
* Domain layer.
*/
package com.xspaceagi.system.domain;

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -0,0 +1,6 @@
package com.xspaceagi.system.domain.service;
public interface ISysOperatorLogDomainService {
}

View File

@@ -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);
}

View File

@@ -0,0 +1,4 @@
package com.xspaceagi.system.domain.service;
public interface RetryDomainService {
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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的子节点也是ALLNONE的子节点也是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);
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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 {
}

View File

@@ -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 得到的 ObjectList/Map 结构)转为 List&lt;ResourceNode&gt;
*/
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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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 = "失败";
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}