负载均衡深度改造,增加分布式锁,表唯一约束等

This commit is contained in:
2026-02-24 11:17:33 +08:00
parent 8d711dc3a2
commit 148a08a3f1
27 changed files with 891 additions and 182 deletions

View File

@@ -6,7 +6,6 @@ import org.quartz.CronExpression;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.constant.enums.EnumJobStatus;
import tech.easyflow.common.domain.Result; import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController; import tech.easyflow.common.web.controller.BaseCurdController;
@@ -38,23 +37,14 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
@GetMapping("/start") @GetMapping("/start")
@SaCheckPermission("/api/v1/sysJob/save") @SaCheckPermission("/api/v1/sysJob/save")
public Result<Void> start(BigInteger id) { public Result<Void> start(BigInteger id) {
SysJob sysJob = service.getById(id); service.startJob(id);
sysJob.setStatus(EnumJobStatus.RUNNING.getCode());
service.addJob(sysJob);
service.updateById(sysJob);
return Result.ok(); return Result.ok();
} }
@GetMapping("/stop") @GetMapping("/stop")
@SaCheckPermission("/api/v1/sysJob/save") @SaCheckPermission("/api/v1/sysJob/save")
public Result<Void> stop(BigInteger id) { public Result<Void> stop(BigInteger id) {
SysJob sysJob = new SysJob(); service.stopJob(id);
sysJob.setId(id);
sysJob.setStatus(EnumJobStatus.STOP.getCode());
ArrayList<Serializable> ids = new ArrayList<>();
ids.add(id);
service.deleteJob(ids);
service.updateById(sysJob);
return Result.ok(); return Result.ok();
} }

View File

@@ -16,6 +16,7 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.crypto.digest.BCrypt; import cn.hutool.crypto.digest.BCrypt;
import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -156,4 +157,14 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
service.updateById(update); service.updateById(update);
return Result.ok(); return Result.ok();
} }
@Override
@PostMapping("save")
public Result<?> save(@JsonBody SysAccount entity) {
try {
return super.save(entity);
} catch (DuplicateKeyException e) {
return Result.fail(1, "用户名已存在");
}
}
} }

View File

@@ -10,12 +10,14 @@ import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.entity.SysOption; import tech.easyflow.system.entity.SysOption;
import tech.easyflow.system.service.SysOptionService; import tech.easyflow.system.service.SysOptionService;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -39,7 +41,10 @@ public class SysOptionController extends BaseController {
if (keys == null || keys.length == 0) { if (keys == null || keys.length == 0) {
return Result.ok(data); return Result.ok(data);
} }
List<SysOption> list = service.list(QueryWrapper.create().in(SysOption::getKey, (Object[]) keys)); BigInteger tenantId = SaTokenUtil.getLoginAccount().getTenantId();
List<SysOption> list = service.list(QueryWrapper.create()
.eq(SysOption::getTenantId, tenantId)
.in(SysOption::getKey, (Object[]) keys));
for (SysOption sysOption : list) { for (SysOption sysOption : list) {
data.put(sysOption.getKey(), sysOption.getValue()); data.put(sysOption.getKey(), sysOption.getValue());
} }
@@ -62,12 +67,21 @@ public class SysOptionController extends BaseController {
if (key == null || key.isEmpty()) { if (key == null || key.isEmpty()) {
throw new BusinessException("key is empty"); throw new BusinessException("key is empty");
} }
sysOption.setTenantId(SaTokenUtil.getLoginAccount().getTenantId()); BigInteger tenantId = SaTokenUtil.getLoginAccount().getTenantId();
SysOption record = service.getByOptionKey(key); sysOption.setTenantId(tenantId);
try {
SysOption record = service.getByOptionKey(key, tenantId);
if (record == null) { if (record == null) {
service.save(sysOption); service.save(sysOption);
} else { } else {
QueryWrapper w = QueryWrapper.create(); QueryWrapper w = QueryWrapper.create();
w.eq(SysOption::getTenantId, tenantId);
w.eq(SysOption::getKey, key);
service.update(sysOption, w);
}
} catch (DuplicateKeyException e) {
QueryWrapper w = QueryWrapper.create();
w.eq(SysOption::getTenantId, tenantId);
w.eq(SysOption::getKey, key); w.eq(SysOption::getKey, key);
service.update(sysOption, w); service.update(sysOption, w);
} }
@@ -79,6 +93,7 @@ public class SysOptionController extends BaseController {
if (key == null || key.isEmpty()) { if (key == null || key.isEmpty()) {
throw new BusinessException("key is empty"); throw new BusinessException("key is empty");
} }
return Result.ok(service.getByOptionKey(key)); BigInteger tenantId = SaTokenUtil.getLoginAccount().getTenantId();
return Result.ok(service.getByOptionKey(key, tenantId));
} }
} }

View File

@@ -6,8 +6,11 @@ import cn.dev33.satoken.annotation.SaIgnore;
import com.alicp.jetcache.Cache; import com.alicp.jetcache.Cache;
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator; import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -42,6 +45,8 @@ import java.util.Map;
@UsePermission(moduleName = "/api/v1/bot") @UsePermission(moduleName = "/api/v1/bot")
public class UcBotController extends BaseCurdController<BotService, Bot> { public class UcBotController extends BaseCurdController<BotService, Bot> {
private static final Logger log = LoggerFactory.getLogger(UcBotController.class);
private final ModelService modelService; private final ModelService modelService;
private final BotWorkflowService botWorkflowService; private final BotWorkflowService botWorkflowService;
private final BotDocumentCollectionService botDocumentCollectionService; private final BotDocumentCollectionService botDocumentCollectionService;
@@ -160,7 +165,12 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
conversation.setBotId(botId); conversation.setBotId(botId);
conversation.setAccountId(SaTokenUtil.getLoginAccount().getId()); conversation.setAccountId(SaTokenUtil.getLoginAccount().getId());
commonFiled(conversation, SaTokenUtil.getLoginAccount().getId(), SaTokenUtil.getLoginAccount().getTenantId(), SaTokenUtil.getLoginAccount().getDeptId()); commonFiled(conversation, SaTokenUtil.getLoginAccount().getId(), SaTokenUtil.getLoginAccount().getTenantId(), SaTokenUtil.getLoginAccount().getDeptId());
try {
conversationMessageService.save(conversation); conversationMessageService.save(conversation);
} catch (DuplicateKeyException e) {
// 并发重试场景下允许重复创建请求,唯一主键冲突按已创建处理。
log.debug("conversation already exists, conversationId={}", conversationId, e);
}
} }
return botService.startChat(botId, prompt, conversationId, messages, chatCheckResult, attachments); return botService.startChat(botId, prompt, conversationId, messages, chatCheckResult, attachments);

View File

@@ -3,8 +3,10 @@ package tech.easyflow.usercenter.controller.ai;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.dao.DuplicateKeyException;
import tech.easyflow.ai.entity.Bot; import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.entity.BotRecentlyUsed; import tech.easyflow.ai.entity.BotRecentlyUsed;
import tech.easyflow.ai.service.BotRecentlyUsedService; import tech.easyflow.ai.service.BotRecentlyUsedService;
@@ -14,13 +16,16 @@ import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController; import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -81,4 +86,53 @@ public class UcBotRecentlyUsedController extends BaseCurdController<BotRecentlyU
entity.setCreatedBy(SaTokenUtil.getLoginAccount().getId()); entity.setCreatedBy(SaTokenUtil.getLoginAccount().getId());
return super.onSaveOrUpdateBefore(entity, isSave); return super.onSaveOrUpdateBefore(entity, isSave);
} }
@PostMapping("save")
@Override
public Result<?> save(@JsonBody BotRecentlyUsed entity) {
if (entity == null || entity.getBotId() == null) {
return Result.fail("botId不能为空");
}
LoginAccount account = SaTokenUtil.getLoginAccount();
Date now = new Date();
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(BotRecentlyUsed::getCreatedBy, account.getId())
.eq(BotRecentlyUsed::getBotId, entity.getBotId());
BotRecentlyUsed exist = service.getOne(queryWrapper);
if (exist != null) {
BotRecentlyUsed update = new BotRecentlyUsed();
update.setId(exist.getId());
update.setCreated(now);
update.setSortNo(entity.getSortNo() == null ? exist.getSortNo() : entity.getSortNo());
service.updateById(update);
return buildSaveResult(exist.getId());
}
entity.setCreated(now);
entity.setCreatedBy(account.getId());
if (entity.getSortNo() == null) {
entity.setSortNo(0);
}
try {
service.save(entity);
return buildSaveResult(entity.getId());
} catch (DuplicateKeyException e) {
BotRecentlyUsed saved = service.getOne(queryWrapper);
if (saved != null) {
BotRecentlyUsed update = new BotRecentlyUsed();
update.setId(saved.getId());
update.setCreated(now);
service.updateById(update);
return buildSaveResult(saved.getId());
}
throw e;
}
}
private Result<?> buildSaveResult(BigInteger id) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("id", id);
return Result.ok(resultMap);
}
} }

View File

@@ -11,6 +11,10 @@
<artifactId>easyflow-common-cache</artifactId> <artifactId>easyflow-common-cache</artifactId>
<dependencies> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency> <dependency>
<groupId>redis.clients</groupId> <groupId>redis.clients</groupId>

View File

@@ -0,0 +1,80 @@
package tech.easyflow.common.cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Collections;
import java.util.UUID;
import java.util.function.Supplier;
@Component
public class RedisLockExecutor {
private static final Logger log = LoggerFactory.getLogger(RedisLockExecutor.class);
private static final long RETRY_INTERVAL_MILLIS = 50L;
private static final DefaultRedisScript<Long> RELEASE_LOCK_SCRIPT;
static {
RELEASE_LOCK_SCRIPT = new DefaultRedisScript<>();
RELEASE_LOCK_SCRIPT.setScriptText(
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end"
);
RELEASE_LOCK_SCRIPT.setResultType(Long.class);
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void executeWithLock(String lockKey, Duration waitTimeout, Duration leaseTimeout, Runnable task) {
executeWithLock(lockKey, waitTimeout, leaseTimeout, () -> {
task.run();
return null;
});
}
public <T> T executeWithLock(String lockKey, Duration waitTimeout, Duration leaseTimeout, Supplier<T> task) {
String lockValue = UUID.randomUUID().toString();
boolean acquired = false;
long deadline = System.nanoTime() + waitTimeout.toNanos();
try {
while (System.nanoTime() <= deadline) {
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, leaseTimeout);
if (Boolean.TRUE.equals(success)) {
acquired = true;
break;
}
Thread.sleep(RETRY_INTERVAL_MILLIS);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("等待分布式锁被中断lockKey=" + lockKey, e);
}
if (!acquired) {
throw new IllegalStateException("获取分布式锁失败请稍后重试lockKey=" + lockKey);
}
try {
return task.get();
} finally {
releaseLock(lockKey, lockValue);
}
}
private void releaseLock(String lockKey, String lockValue) {
try {
stringRedisTemplate.execute(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey), lockValue);
} catch (Exception e) {
log.warn("释放分布式锁失败lockKey={}", lockKey, e);
}
}
}

View File

@@ -9,6 +9,7 @@ import com.easyagents.flow.core.chain.listener.ChainEventListener;
import com.easyagents.flow.core.chain.repository.NodeStateField; import com.easyagents.flow.core.chain.repository.NodeStateField;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.Workflow; import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.entity.WorkflowExecResult; import tech.easyflow.ai.entity.WorkflowExecResult;
@@ -63,6 +64,10 @@ public class ChainEventListenerForSave implements ChainEventListener {
ChainState state = chain.getState(); ChainState state = chain.getState();
Workflow workflow = workflowService.getById(definition.getId()); Workflow workflow = workflowService.getById(definition.getId());
String instanceId = state.getInstanceId(); String instanceId = state.getInstanceId();
WorkflowExecResult existed = workflowExecResultService.getByExecKey(instanceId);
if (existed != null) {
return;
}
WorkflowExecResult record = new WorkflowExecResult(); WorkflowExecResult record = new WorkflowExecResult();
record.setExecKey(instanceId); record.setExecKey(instanceId);
record.setWorkflowId(workflow.getId()); record.setWorkflowId(workflow.getId());
@@ -74,7 +79,12 @@ public class ChainEventListenerForSave implements ChainEventListener {
record.setStatus(state.getStatus().getValue()); record.setStatus(state.getStatus().getValue());
record.setCreatedKey(WorkFlowUtil.USER_KEY); record.setCreatedKey(WorkFlowUtil.USER_KEY);
record.setCreatedBy(WorkFlowUtil.getOperator(chain).getId().toString()); record.setCreatedBy(WorkFlowUtil.getOperator(chain).getId().toString());
try {
workflowExecResultService.save(record); workflowExecResultService.save(record);
} catch (DuplicateKeyException e) {
// 多节点重试时可能并发写同一 exec_key按幂等处理。
log.debug("exec result already exists, execKey={}", instanceId, e);
}
} }
private void handleChainEndEvent(ChainEndEvent event, Chain chain) { private void handleChainEndEvent(ChainEndEvent event, Chain chain) {

View File

@@ -5,10 +5,16 @@ import tech.easyflow.ai.mapper.BotDocumentCollectionMapper;
import tech.easyflow.ai.service.BotDocumentCollectionService; import tech.easyflow.ai.service.BotDocumentCollectionService;
import com.mybatisflex.spring.service.impl.ServiceImpl; import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.common.cache.RedisLockExecutor;
import javax.annotation.Resource;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.time.Duration;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
/** /**
@@ -20,6 +26,13 @@ import com.mybatisflex.core.query.QueryWrapper;
@Service @Service
public class BotDocumentCollectionServiceImpl extends ServiceImpl<BotDocumentCollectionMapper, BotDocumentCollection> implements BotDocumentCollectionService { public class BotDocumentCollectionServiceImpl extends ServiceImpl<BotDocumentCollectionMapper, BotDocumentCollection> implements BotDocumentCollectionService {
private static final String BOT_BINDING_LOCK_KEY_PREFIX = "easyflow:lock:bot:binding:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
@Resource
private RedisLockExecutor redisLockExecutor;
@Override @Override
public List<BotDocumentCollection> listByBotId(BigInteger botId) { public List<BotDocumentCollection> listByBotId(BigInteger botId) {
@@ -30,15 +43,30 @@ public class BotDocumentCollectionServiceImpl extends ServiceImpl<BotDocumentCol
} }
@Override @Override
@Transactional
public void saveBotAndKnowledge(BigInteger botId, BigInteger[] knowledgeIds) { public void saveBotAndKnowledge(BigInteger botId, BigInteger[] knowledgeIds) {
redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
this.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId)); this.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId));
List<BotDocumentCollection> list = new ArrayList<>(knowledgeIds.length); Set<BigInteger> uniqueKnowledgeIds = new LinkedHashSet<>();
if (knowledgeIds != null) {
for (BigInteger knowledgeId : knowledgeIds) { for (BigInteger knowledgeId : knowledgeIds) {
if (knowledgeId != null) {
uniqueKnowledgeIds.add(knowledgeId);
}
}
}
if (uniqueKnowledgeIds.isEmpty()) {
return;
}
List<BotDocumentCollection> list = new ArrayList<>(uniqueKnowledgeIds.size());
for (BigInteger knowledgeId : uniqueKnowledgeIds) {
BotDocumentCollection botDocumentCollection = new BotDocumentCollection(); BotDocumentCollection botDocumentCollection = new BotDocumentCollection();
botDocumentCollection.setBotId(botId); botDocumentCollection.setBotId(botId);
botDocumentCollection.setDocumentCollectionId(knowledgeId); botDocumentCollection.setDocumentCollectionId(knowledgeId);
list.add(botDocumentCollection); list.add(botDocumentCollection);
} }
this.saveBatch(list); this.saveBatch(list);
});
} }
} }

View File

@@ -7,11 +7,15 @@ import tech.easyflow.ai.entity.BotMcp;
import tech.easyflow.ai.mapper.BotMcpMapper; import tech.easyflow.ai.mapper.BotMcpMapper;
import tech.easyflow.ai.service.BotMcpService; import tech.easyflow.ai.service.BotMcpService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import tech.easyflow.common.cache.RedisLockExecutor;
import javax.annotation.Resource;
import java.time.Duration;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Collections; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* 服务层实现。 * 服务层实现。
@@ -22,21 +26,44 @@ import java.util.Map;
@Service @Service
public class BotMcpServiceImpl extends ServiceImpl<BotMcpMapper, BotMcp> implements BotMcpService{ public class BotMcpServiceImpl extends ServiceImpl<BotMcpMapper, BotMcp> implements BotMcpService{
private static final String BOT_BINDING_LOCK_KEY_PREFIX = "easyflow:lock:bot:binding:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
@Resource
private RedisLockExecutor redisLockExecutor;
@Override @Override
@Transactional @Transactional
public void updateBotMcpToolIds(BigInteger botId, List<Map<String, List<List<String>>>> mcpSelectedData) { public void updateBotMcpToolIds(BigInteger botId, List<Map<String, List<List<String>>>> mcpSelectedData) {
// 删除原来绑定的mcp redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
this.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId)); this.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId));
if (mcpSelectedData == null || mcpSelectedData.isEmpty()) {
return;
}
Set<String> uniqueTools = new LinkedHashSet<>();
for (Map<String, List<List<String>>> mcpItem : mcpSelectedData) { for (Map<String, List<List<String>>> mcpItem : mcpSelectedData) {
for (Map.Entry<String, List<List<String>>> entry : mcpItem.entrySet()) { for (Map.Entry<String, List<List<String>>> entry : mcpItem.entrySet()) {
String mcpId = entry.getKey(); // 上一级id String mcpId = entry.getKey();
List<List<String>> toolList = entry.getValue(); // 包含name和description的二维数组 List<List<String>> toolList = entry.getValue();
if (toolList == null || toolList.isEmpty()) {
continue;
}
// 遍历每个工具的[name, description]
for (List<String> toolInfo : toolList) { for (List<String> toolInfo : toolList) {
String toolName = toolInfo.get(0); // 工具名称 if (toolInfo == null || toolInfo.size() < 2) {
String toolDesc = toolInfo.get(1); // 工具描述 continue;
System.out.println("工具名称:" + toolName + ",描述:" + toolDesc); }
String toolName = toolInfo.get(0);
String toolDesc = toolInfo.get(1);
if (toolName == null) {
continue;
}
String uniqueKey = mcpId + "|" + toolName;
if (!uniqueTools.add(uniqueKey)) {
continue;
}
BotMcp botMcp = new BotMcp(); BotMcp botMcp = new BotMcp();
botMcp.setBotId(botId); botMcp.setBotId(botId);
botMcp.setMcpId(new BigInteger(mcpId)); botMcp.setMcpId(new BigInteger(mcpId));
@@ -46,5 +73,6 @@ public class BotMcpServiceImpl extends ServiceImpl<BotMcpMapper, BotMcp> implem
} }
} }
} }
});
} }
} }

View File

@@ -3,16 +3,21 @@ package tech.easyflow.ai.service.impl;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl; import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.entity.BotPlugin; import tech.easyflow.ai.entity.BotPlugin;
import tech.easyflow.ai.entity.Plugin; import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.mapper.BotPluginMapper; import tech.easyflow.ai.mapper.BotPluginMapper;
import tech.easyflow.ai.mapper.PluginMapper; import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.service.BotPluginService; import tech.easyflow.ai.service.BotPluginService;
import tech.easyflow.common.cache.RedisLockExecutor;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.time.Duration;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import static tech.easyflow.ai.entity.table.BotPluginTableDef.BOT_PLUGIN; import static tech.easyflow.ai.entity.table.BotPluginTableDef.BOT_PLUGIN;
@@ -25,12 +30,19 @@ import static tech.easyflow.ai.entity.table.BotPluginTableDef.BOT_PLUGIN;
@Service @Service
public class BotPluginServiceImpl extends ServiceImpl<BotPluginMapper, BotPlugin> implements BotPluginService { public class BotPluginServiceImpl extends ServiceImpl<BotPluginMapper, BotPlugin> implements BotPluginService {
private static final String BOT_BINDING_LOCK_KEY_PREFIX = "easyflow:lock:bot:binding:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
@Resource @Resource
private BotPluginMapper botPluginMapper; private BotPluginMapper botPluginMapper;
@Resource @Resource
private PluginMapper pluginMapper; private PluginMapper pluginMapper;
@Resource
private RedisLockExecutor redisLockExecutor;
@Override @Override
public List<Plugin> getList(String botId) { public List<Plugin> getList(String botId) {
QueryWrapper w = QueryWrapper.create(); QueryWrapper w = QueryWrapper.create();
@@ -58,15 +70,31 @@ public class BotPluginServiceImpl extends ServiceImpl<BotPluginMapper, BotPlugin
} }
@Override @Override
@Transactional
public void saveBotAndPluginTool(BigInteger botId, BigInteger[] pluginToolIds) { public void saveBotAndPluginTool(BigInteger botId, BigInteger[] pluginToolIds) {
redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
this.remove(QueryWrapper.create().eq(BotPlugin::getBotId, botId)); this.remove(QueryWrapper.create().eq(BotPlugin::getBotId, botId));
List<BotPlugin> list = new ArrayList<>(pluginToolIds.length);
Set<BigInteger> uniquePluginToolIds = new LinkedHashSet<>();
if (pluginToolIds != null) {
for (BigInteger pluginToolId : pluginToolIds) { for (BigInteger pluginToolId : pluginToolIds) {
if (pluginToolId != null) {
uniquePluginToolIds.add(pluginToolId);
}
}
}
if (uniquePluginToolIds.isEmpty()) {
return;
}
List<BotPlugin> list = new ArrayList<>(uniquePluginToolIds.size());
for (BigInteger pluginToolId : uniquePluginToolIds) {
BotPlugin aiBotPluginTool = new BotPlugin(); BotPlugin aiBotPluginTool = new BotPlugin();
aiBotPluginTool.setBotId(botId); aiBotPluginTool.setBotId(botId);
aiBotPluginTool.setPluginItemId(pluginToolId); aiBotPluginTool.setPluginItemId(pluginToolId);
list.add(aiBotPluginTool); list.add(aiBotPluginTool);
} }
this.saveBatch(list); this.saveBatch(list);
});
} }
} }

View File

@@ -5,10 +5,16 @@ import tech.easyflow.ai.mapper.BotWorkflowMapper;
import tech.easyflow.ai.service.BotWorkflowService; import tech.easyflow.ai.service.BotWorkflowService;
import com.mybatisflex.spring.service.impl.ServiceImpl; import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.common.cache.RedisLockExecutor;
import javax.annotation.Resource;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.time.Duration;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
/** /**
@@ -20,6 +26,13 @@ import com.mybatisflex.core.query.QueryWrapper;
@Service @Service
public class BotWorkflowServiceImpl extends ServiceImpl<BotWorkflowMapper, BotWorkflow> implements BotWorkflowService { public class BotWorkflowServiceImpl extends ServiceImpl<BotWorkflowMapper, BotWorkflow> implements BotWorkflowService {
private static final String BOT_BINDING_LOCK_KEY_PREFIX = "easyflow:lock:bot:binding:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
@Resource
private RedisLockExecutor redisLockExecutor;
@Override @Override
public List<BotWorkflow> listByBotId(BigInteger botId) { public List<BotWorkflow> listByBotId(BigInteger botId) {
@@ -30,15 +43,31 @@ public class BotWorkflowServiceImpl extends ServiceImpl<BotWorkflowMapper, BotWo
} }
@Override @Override
@Transactional
public void saveBotAndWorkflowTool(BigInteger botId, BigInteger[] workflowIds) { public void saveBotAndWorkflowTool(BigInteger botId, BigInteger[] workflowIds) {
redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
this.remove(QueryWrapper.create().eq(BotWorkflow::getBotId, botId)); this.remove(QueryWrapper.create().eq(BotWorkflow::getBotId, botId));
List<BotWorkflow> list = new ArrayList<>(workflowIds.length);
Set<BigInteger> uniqueWorkflowIds = new LinkedHashSet<>();
if (workflowIds != null) {
for (BigInteger workflowId : workflowIds) { for (BigInteger workflowId : workflowIds) {
if (workflowId != null) {
uniqueWorkflowIds.add(workflowId);
}
}
}
if (uniqueWorkflowIds.isEmpty()) {
return;
}
List<BotWorkflow> list = new ArrayList<>(uniqueWorkflowIds.size());
for (BigInteger workflowId : uniqueWorkflowIds) {
BotWorkflow botWorkflow = new BotWorkflow(); BotWorkflow botWorkflow = new BotWorkflow();
botWorkflow.setBotId(botId); botWorkflow.setBotId(botId);
botWorkflow.setWorkflowId(workflowId); botWorkflow.setWorkflowId(workflowId);
list.add(botWorkflow); list.add(botWorkflow);
} }
this.saveBatch(list); this.saveBatch(list);
});
} }
} }

View File

@@ -24,6 +24,10 @@
<groupId>tech.easyflow</groupId> <groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-satoken</artifactId> <artifactId>easyflow-common-satoken</artifactId>
</dependency> </dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-cache</artifactId>
</dependency>
<dependency> <dependency>
<groupId>tech.easyflow</groupId> <groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-web</artifactId> <artifactId>easyflow-common-web</artifactId>

View File

@@ -11,6 +11,7 @@ import com.mybatisflex.core.row.Row;
import com.mybatisflex.spring.service.impl.ServiceImpl; import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.common.entity.DatacenterQuery; import tech.easyflow.common.entity.DatacenterQuery;
import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.exceptions.BusinessException;
@@ -26,21 +27,35 @@ import tech.easyflow.datacenter.service.DatacenterTableService;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
public class DatacenterTableServiceImpl extends ServiceImpl<DatacenterTableMapper, DatacenterTable> implements DatacenterTableService { public class DatacenterTableServiceImpl extends ServiceImpl<DatacenterTableMapper, DatacenterTable> implements DatacenterTableService {
private static final String DATACENTER_TABLE_LOCK_KEY_PREFIX = "easyflow:lock:datacenter:table:";
private static final String DATACENTER_TABLE_CREATE_LOCK_KEY_PREFIX = "easyflow:lock:datacenter:table:create:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(15);
@Resource @Resource
private DbHandleManager dbHandleManager; private DbHandleManager dbHandleManager;
@Resource @Resource
private DatacenterTableFieldMapper fieldsMapper; private DatacenterTableFieldMapper fieldsMapper;
@Resource
private RedisLockExecutor redisLockExecutor;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void saveTable(DatacenterTable entity, LoginAccount loginUser) { public void saveTable(DatacenterTable entity, LoginAccount loginUser) {
String lockKey = buildTableLockKey(entity, loginUser);
redisLockExecutor.executeWithLock(lockKey, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
doSaveTable(entity, loginUser);
});
}
private void doSaveTable(DatacenterTable entity, LoginAccount loginUser) {
DbHandleService dbHandler = dbHandleManager.getDbHandler(); DbHandleService dbHandler = dbHandleManager.getDbHandler();
List<DatacenterTableField> fields = entity.getFields(); List<DatacenterTableField> fields = entity.getFields();
@@ -117,6 +132,11 @@ public class DatacenterTableServiceImpl extends ServiceImpl<DatacenterTableMappe
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void removeTable(BigInteger tableId) { public void removeTable(BigInteger tableId) {
redisLockExecutor.executeWithLock(
DATACENTER_TABLE_LOCK_KEY_PREFIX + tableId,
LOCK_WAIT_TIMEOUT,
LOCK_LEASE_TIMEOUT,
() -> {
DatacenterTable record = getById(tableId); DatacenterTable record = getById(tableId);
dbHandleManager.getDbHandler().deleteTable(record); dbHandleManager.getDbHandler().deleteTable(record);
removeById(tableId); removeById(tableId);
@@ -124,6 +144,8 @@ public class DatacenterTableServiceImpl extends ServiceImpl<DatacenterTableMappe
wrapper.eq(DatacenterTableField::getTableId, tableId); wrapper.eq(DatacenterTableField::getTableId, tableId);
fieldsMapper.deleteByQuery(wrapper); fieldsMapper.deleteByQuery(wrapper);
} }
);
}
@Override @Override
public Long getCount(DatacenterQuery where) { public Long getCount(DatacenterQuery where) {
@@ -247,6 +269,17 @@ public class DatacenterTableServiceImpl extends ServiceImpl<DatacenterTableMappe
return "tb_dynamic_" + tableName + "_" + id; return "tb_dynamic_" + tableName + "_" + id;
} }
private String buildTableLockKey(DatacenterTable table, LoginAccount loginUser) {
if (table.getId() != null) {
return DATACENTER_TABLE_LOCK_KEY_PREFIX + table.getId();
}
String tenant = table.getTenantId() != null
? table.getTenantId().toString()
: (loginUser != null && loginUser.getTenantId() != null ? loginUser.getTenantId().toString() : "0");
String tableName = table.getTableName() == null ? "unknown" : table.getTableName();
return DATACENTER_TABLE_CREATE_LOCK_KEY_PREFIX + tenant + ":" + tableName;
}
/** /**
* 构建查询条件 * 构建查询条件
*/ */

View File

@@ -22,6 +22,10 @@
<groupId>tech.easyflow</groupId> <groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-base</artifactId> <artifactId>easyflow-common-base</artifactId>
</dependency> </dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-cache</artifactId>
</dependency>
<dependency> <dependency>
<groupId>tech.easyflow</groupId> <groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-satoken</artifactId> <artifactId>easyflow-common-satoken</artifactId>

View File

@@ -4,6 +4,7 @@ import com.mybatisflex.core.service.IService;
import tech.easyflow.job.entity.SysJob; import tech.easyflow.job.entity.SysJob;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection; import java.util.Collection;
/** /**
@@ -21,4 +22,8 @@ public interface SysJobService extends IService<SysJob> {
void addJob(SysJob job); void addJob(SysJob job);
void deleteJob(Collection<Serializable> ids); void deleteJob(Collection<Serializable> ids);
void startJob(BigInteger id);
void stopJob(BigInteger id);
} }

View File

@@ -6,6 +6,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import tech.easyflow.common.constant.enums.EnumMisfirePolicy; import tech.easyflow.common.constant.enums.EnumMisfirePolicy;
import tech.easyflow.common.constant.enums.EnumJobStatus;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.job.entity.SysJob; import tech.easyflow.job.entity.SysJob;
import tech.easyflow.job.job.JobConstant; import tech.easyflow.job.job.JobConstant;
import tech.easyflow.job.job.QuartzJob; import tech.easyflow.job.job.QuartzJob;
@@ -17,7 +19,9 @@ import tech.easyflow.job.util.JobUtil;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigInteger; import java.math.BigInteger;
import java.time.Duration;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
/** /**
* 系统任务表 服务层实现。 * 系统任务表 服务层实现。
@@ -28,11 +32,18 @@ import java.util.Collection;
@Service @Service
public class SysJobServiceImpl extends ServiceImpl<SysJobMapper, SysJob> implements SysJobService { public class SysJobServiceImpl extends ServiceImpl<SysJobMapper, SysJob> implements SysJobService {
private static final String JOB_LOCK_KEY_PREFIX = "easyflow:lock:job:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
protected Logger log = LoggerFactory.getLogger(SysJobServiceImpl.class); protected Logger log = LoggerFactory.getLogger(SysJobServiceImpl.class);
@Resource @Resource
private Scheduler scheduler; private Scheduler scheduler;
@Resource
private RedisLockExecutor redisLockExecutor;
@Override @Override
public void test() { public void test() {
System.out.println("java bean 动态执行"); System.out.println("java bean 动态执行");
@@ -92,4 +103,54 @@ public class SysJobServiceImpl extends ServiceImpl<SysJobMapper, SysJob> implem
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@Override
public void startJob(BigInteger id) {
redisLockExecutor.executeWithLock(JOB_LOCK_KEY_PREFIX + id, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
SysJob sysJob = this.getById(id);
if (sysJob == null) {
throw new IllegalStateException("任务不存在id=" + id);
}
try {
JobKey jobKey = JobUtil.getJobKey(sysJob);
if (!scheduler.checkExists(jobKey)) {
addJob(sysJob);
}
if (!Integer.valueOf(EnumJobStatus.RUNNING.getCode()).equals(sysJob.getStatus())) {
SysJob update = new SysJob();
update.setId(id);
update.setStatus(EnumJobStatus.RUNNING.getCode());
this.updateById(update);
}
} catch (SchedulerException e) {
log.error("启动任务失败id={}", id, e);
throw new RuntimeException(e);
}
});
}
@Override
public void stopJob(BigInteger id) {
redisLockExecutor.executeWithLock(JOB_LOCK_KEY_PREFIX + id, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
SysJob sysJob = this.getById(id);
if (sysJob == null) {
throw new IllegalStateException("任务不存在id=" + id);
}
try {
JobKey jobKey = JobUtil.getJobKey(sysJob);
if (scheduler.checkExists(jobKey)) {
deleteJob(Collections.singletonList(id));
}
if (!Integer.valueOf(EnumJobStatus.STOP.getCode()).equals(sysJob.getStatus())) {
SysJob update = new SysJob();
update.setId(id);
update.setStatus(EnumJobStatus.STOP.getCode());
this.updateById(update);
}
} catch (SchedulerException e) {
log.error("停止任务失败id={}", id, e);
throw new RuntimeException(e);
}
});
}
} }

View File

@@ -24,6 +24,10 @@
<groupId>tech.easyflow</groupId> <groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-satoken</artifactId> <artifactId>easyflow-common-satoken</artifactId>
</dependency> </dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-cache</artifactId>
</dependency>
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId> <artifactId>hutool-crypto</artifactId>

View File

@@ -5,11 +5,14 @@ import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.options.SysOptionStore; import tech.easyflow.common.options.SysOptionStore;
import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.util.StringUtil; import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysOption; import tech.easyflow.system.entity.SysOption;
import tech.easyflow.system.service.SysOptionService; import tech.easyflow.system.service.SysOptionService;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigInteger;
@Component @Component
public class DefaultOptionStore implements SysOptionStore { public class DefaultOptionStore implements SysOptionStore {
@@ -20,28 +23,56 @@ public class DefaultOptionStore implements SysOptionStore {
@Override @Override
public void save(String key, Object value) { public void save(String key, Object value) {
BigInteger tenantId = getTenantIdForWrite();
if (value == null || !StringUtil.hasText(value.toString())) { if (value == null || !StringUtil.hasText(value.toString())) {
optionService.remove(QueryWrapper.create().eq(SysOption::getKey, key)); optionService.remove(QueryWrapper.create()
.eq(SysOption::getTenantId, tenantId)
.eq(SysOption::getKey, key));
return; return;
} }
String newValue = value.toString().trim(); String newValue = value.toString().trim();
SysOption option = optionService.getByOptionKey(key); SysOption option = optionService.getByOptionKey(key, tenantId);
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
if (option == null) { if (option == null) {
option = new SysOption(key, newValue); option = new SysOption(key, newValue);
option.setTenantId(loginAccount.getTenantId()); option.setTenantId(tenantId);
try {
optionService.save(option); optionService.save(option);
} catch (DuplicateKeyException e) {
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(SysOption::getTenantId, tenantId)
.eq(SysOption::getKey, key);
optionService.update(option, queryWrapper);
}
} else { } else {
option.setValue(newValue); option.setValue(newValue);
QueryWrapper queryWrapper = QueryWrapper.create().eq(SysOption::getKey, key); QueryWrapper queryWrapper = QueryWrapper.create()
.eq(SysOption::getTenantId, tenantId)
.eq(SysOption::getKey, key);
optionService.update(option, queryWrapper); optionService.update(option, queryWrapper);
} }
} }
@Override @Override
public String get(String key) { public String get(String key) {
SysOption option = optionService.getById(key); BigInteger tenantId = getTenantIdForRead();
if (tenantId == null) {
return null;
}
SysOption option = optionService.getByOptionKey(key, tenantId);
return option != null ? option.getValue() : null; return option != null ? option.getValue() : null;
} }
private BigInteger getTenantIdForWrite() {
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
if (loginAccount == null || loginAccount.getTenantId() == null) {
throw new BusinessException("未获取到租户信息,无法保存系统配置");
}
return loginAccount.getTenantId();
}
private BigInteger getTenantIdForRead() {
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
return loginAccount != null ? loginAccount.getTenantId() : null;
}
} }

View File

@@ -3,6 +3,8 @@ package tech.easyflow.system.service;
import tech.easyflow.system.entity.SysOption; import tech.easyflow.system.entity.SysOption;
import com.mybatisflex.core.service.IService; import com.mybatisflex.core.service.IService;
import java.math.BigInteger;
/** /**
* 系统配置信息表。 服务层。 * 系统配置信息表。 服务层。
* *
@@ -11,5 +13,9 @@ import com.mybatisflex.core.service.IService;
*/ */
public interface SysOptionService extends IService<SysOption> { public interface SysOptionService extends IService<SysOption> {
SysOption getByOptionKey(String key); SysOption getByOptionKey(String key, BigInteger tenantId);
default SysOption getByOptionKey(String key) {
return getByOptionKey(key, null);
}
} }

View File

@@ -3,6 +3,8 @@ package tech.easyflow.system.service.impl;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl; import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.system.entity.SysAccount; import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysAccountPosition; import tech.easyflow.system.entity.SysAccountPosition;
import tech.easyflow.system.entity.SysAccountRole; import tech.easyflow.system.entity.SysAccountRole;
@@ -13,9 +15,12 @@ import tech.easyflow.system.mapper.SysRoleMapper;
import tech.easyflow.system.service.SysAccountService; import tech.easyflow.system.service.SysAccountService;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.time.Duration;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* 用户表 服务层实现。 * 用户表 服务层实现。
@@ -26,35 +31,53 @@ import java.util.List;
@Service @Service
public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAccount> implements SysAccountService { public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAccount> implements SysAccountService {
private static final String ACCOUNT_RELATION_LOCK_KEY_PREFIX = "easyflow:lock:sys:account:relation:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
@Resource @Resource
private SysAccountRoleMapper sysAccountRoleMapper; private SysAccountRoleMapper sysAccountRoleMapper;
@Resource @Resource
private SysAccountPositionMapper sysAccountPositionMapper; private SysAccountPositionMapper sysAccountPositionMapper;
@Resource @Resource
private SysRoleMapper sysRoleMapper; private SysRoleMapper sysRoleMapper;
@Resource
private RedisLockExecutor redisLockExecutor;
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void syncRelations(SysAccount entity) { public void syncRelations(SysAccount entity) {
if (entity == null || entity.getId() == null) { if (entity == null || entity.getId() == null) {
return; return;
} }
redisLockExecutor.executeWithLock(
ACCOUNT_RELATION_LOCK_KEY_PREFIX + entity.getId(),
LOCK_WAIT_TIMEOUT,
LOCK_LEASE_TIMEOUT,
() -> {
//sync roleIds //sync roleIds
List<BigInteger> roleIds = entity.getRoleIds(); List<BigInteger> roleIds = entity.getRoleIds();
if (roleIds != null) { if (roleIds != null) {
QueryWrapper delW = QueryWrapper.create(); QueryWrapper delW = QueryWrapper.create();
delW.eq(SysAccountRole::getAccountId, entity.getId()); delW.eq(SysAccountRole::getAccountId, entity.getId());
sysAccountRoleMapper.deleteByQuery(delW); sysAccountRoleMapper.deleteByQuery(delW);
if (!roleIds.isEmpty()) { Set<BigInteger> uniqueRoleIds = new LinkedHashSet<>(roleIds);
List<SysAccountRole> rows = new ArrayList<>(roleIds.size()); if (!uniqueRoleIds.isEmpty()) {
roleIds.forEach(roleId -> { List<SysAccountRole> rows = new ArrayList<>(uniqueRoleIds.size());
uniqueRoleIds.forEach(roleId -> {
if (roleId == null) {
return;
}
SysAccountRole row = new SysAccountRole(); SysAccountRole row = new SysAccountRole();
row.setAccountId(entity.getId()); row.setAccountId(entity.getId());
row.setRoleId(roleId); row.setRoleId(roleId);
rows.add(row); rows.add(row);
}); });
if (!rows.isEmpty()) {
sysAccountRoleMapper.insertBatch(rows); sysAccountRoleMapper.insertBatch(rows);
} }
} }
}
//sync positionIds //sync positionIds
List<BigInteger> positionIds = entity.getPositionIds(); List<BigInteger> positionIds = entity.getPositionIds();
@@ -62,18 +85,26 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
QueryWrapper delW = QueryWrapper.create(); QueryWrapper delW = QueryWrapper.create();
delW.eq(SysAccountPosition::getAccountId, entity.getId()); delW.eq(SysAccountPosition::getAccountId, entity.getId());
sysAccountPositionMapper.deleteByQuery(delW); sysAccountPositionMapper.deleteByQuery(delW);
if (!positionIds.isEmpty()) { Set<BigInteger> uniquePositionIds = new LinkedHashSet<>(positionIds);
List<SysAccountPosition> rows = new ArrayList<>(positionIds.size()); if (!uniquePositionIds.isEmpty()) {
positionIds.forEach(positionId -> { List<SysAccountPosition> rows = new ArrayList<>(uniquePositionIds.size());
uniquePositionIds.forEach(positionId -> {
if (positionId == null) {
return;
}
SysAccountPosition row = new SysAccountPosition(); SysAccountPosition row = new SysAccountPosition();
row.setAccountId(entity.getId()); row.setAccountId(entity.getId());
row.setPositionId(positionId); row.setPositionId(positionId);
rows.add(row); rows.add(row);
}); });
if (!rows.isEmpty()) {
sysAccountPositionMapper.insertBatch(rows); sysAccountPositionMapper.insertBatch(rows);
} }
} }
} }
}
);
}
@Override @Override
public SysAccount getByUsername(String userKey) { public SysAccount getByUsername(String userKey) {

View File

@@ -3,14 +3,20 @@ package tech.easyflow.system.service.impl;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl; import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.system.entity.SysApiKey; import tech.easyflow.system.entity.SysApiKey;
import tech.easyflow.system.entity.SysApiKeyResourceMapping; import tech.easyflow.system.entity.SysApiKeyResourceMapping;
import tech.easyflow.system.mapper.SysApiKeyResourceMappingMapper; import tech.easyflow.system.mapper.SysApiKeyResourceMappingMapper;
import tech.easyflow.system.service.SysApiKeyResourceMappingService; import tech.easyflow.system.service.SysApiKeyResourceMappingService;
import javax.annotation.Resource;
import java.time.Duration;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* apikey-请求接口表 服务层实现。 * apikey-请求接口表 服务层实现。
@@ -21,21 +27,43 @@ import java.util.List;
@Service @Service
public class SysApiKeyResourceMappingServiceImpl extends ServiceImpl<SysApiKeyResourceMappingMapper, SysApiKeyResourceMapping> implements SysApiKeyResourceMappingService { public class SysApiKeyResourceMappingServiceImpl extends ServiceImpl<SysApiKeyResourceMappingMapper, SysApiKeyResourceMapping> implements SysApiKeyResourceMappingService {
private static final String API_KEY_MAPPING_LOCK_PREFIX = "easyflow:lock:sys:apikey:mapping:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
@Resource
private RedisLockExecutor redisLockExecutor;
/** /**
* 批量授权apiKey接口 * 批量授权apiKey接口
* @param entity * @param entity
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void authInterface(SysApiKey entity) { public void authInterface(SysApiKey entity) {
this.remove(QueryWrapper.create().eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId()));
List<SysApiKeyResourceMapping> rows = new ArrayList<>(entity.getPermissionIds().size());
BigInteger apiKeyId = entity.getId(); BigInteger apiKeyId = entity.getId();
for (BigInteger resourceId : entity.getPermissionIds()) { if (apiKeyId == null) {
return;
}
redisLockExecutor.executeWithLock(API_KEY_MAPPING_LOCK_PREFIX + apiKeyId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
this.remove(QueryWrapper.create().eq(SysApiKeyResourceMapping::getApiKeyId, apiKeyId));
if (entity.getPermissionIds() == null || entity.getPermissionIds().isEmpty()) {
return;
}
Set<BigInteger> uniqueResourceIds = new LinkedHashSet<>(entity.getPermissionIds());
List<SysApiKeyResourceMapping> rows = new ArrayList<>(uniqueResourceIds.size());
for (BigInteger resourceId : uniqueResourceIds) {
if (resourceId == null) {
continue;
}
SysApiKeyResourceMapping sysApiKeyResourcePermissionRelationship = new SysApiKeyResourceMapping(); SysApiKeyResourceMapping sysApiKeyResourcePermissionRelationship = new SysApiKeyResourceMapping();
sysApiKeyResourcePermissionRelationship.setApiKeyId(apiKeyId); sysApiKeyResourcePermissionRelationship.setApiKeyId(apiKeyId);
sysApiKeyResourcePermissionRelationship.setApiKeyResourceId(resourceId); sysApiKeyResourcePermissionRelationship.setApiKeyResourceId(resourceId);
rows.add(sysApiKeyResourcePermissionRelationship); rows.add(sysApiKeyResourcePermissionRelationship);
} }
if (!rows.isEmpty()) {
this.saveBatch(rows); this.saveBatch(rows);
} }
});
}
} }

View File

@@ -7,6 +7,8 @@ import tech.easyflow.system.service.SysOptionService;
import com.mybatisflex.spring.service.impl.ServiceImpl; import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigInteger;
/** /**
* 系统配置信息表。 服务层实现。 * 系统配置信息表。 服务层实现。
* *
@@ -17,9 +19,12 @@ import org.springframework.stereotype.Service;
public class SysOptionServiceImpl extends ServiceImpl<SysOptionMapper, SysOption> implements SysOptionService { public class SysOptionServiceImpl extends ServiceImpl<SysOptionMapper, SysOption> implements SysOptionService {
@Override @Override
public SysOption getByOptionKey(String key) { public SysOption getByOptionKey(String key, BigInteger tenantId) {
QueryWrapper w = QueryWrapper.create(); QueryWrapper w = QueryWrapper.create();
w.eq(SysOption::getKey, key); w.eq(SysOption::getKey, key);
if (tenantId != null) {
w.eq(SysOption::getTenantId, tenantId);
}
return getOne(w); return getOne(w);
} }
} }

View File

@@ -5,6 +5,7 @@ import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl; import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.common.constant.enums.EnumDataScope; import tech.easyflow.common.constant.enums.EnumDataScope;
import tech.easyflow.system.entity.SysAccountRole; import tech.easyflow.system.entity.SysAccountRole;
import tech.easyflow.system.entity.SysRole; import tech.easyflow.system.entity.SysRole;
@@ -17,9 +18,12 @@ import tech.easyflow.system.service.SysAccountRoleService;
import tech.easyflow.system.service.SysRoleService; import tech.easyflow.system.service.SysRoleService;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.time.Duration;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -31,29 +35,49 @@ import java.util.stream.Collectors;
@Service @Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService { public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {
private static final String ROLE_LOCK_KEY_PREFIX = "easyflow:lock:sys:role:";
private static final String ROLE_CREATE_LOCK_KEY_PREFIX = "easyflow:lock:sys:role:create:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
@Resource @Resource
private SysAccountRoleService sysAccountRoleService; private SysAccountRoleService sysAccountRoleService;
@Resource @Resource
private SysRoleMenuMapper sysRoleMenuMapper; private SysRoleMenuMapper sysRoleMenuMapper;
@Resource @Resource
private SysRoleDeptMapper sysRoleDeptMapper; private SysRoleDeptMapper sysRoleDeptMapper;
@Resource
private RedisLockExecutor redisLockExecutor;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void saveRoleMenu(BigInteger roleId, List<String> keys) { public void saveRoleMenu(BigInteger roleId, List<String> keys) {
redisLockExecutor.executeWithLock(ROLE_LOCK_KEY_PREFIX + roleId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
QueryWrapper delW = QueryWrapper.create(); QueryWrapper delW = QueryWrapper.create();
delW.eq(SysRoleMenu::getRoleId, roleId); delW.eq(SysRoleMenu::getRoleId, roleId);
sysRoleMenuMapper.deleteByQuery(delW); sysRoleMenuMapper.deleteByQuery(delW);
List<SysRoleMenu> rows = new ArrayList<>(keys.size()); if (CollectionUtil.isEmpty(keys)) {
keys.forEach(string -> { return;
}
Set<BigInteger> uniqueMenuIds = new LinkedHashSet<>();
for (String key : keys) {
if (key != null && !key.isEmpty()) {
uniqueMenuIds.add(new BigInteger(key));
}
}
if (uniqueMenuIds.isEmpty()) {
return;
}
List<SysRoleMenu> rows = new ArrayList<>(uniqueMenuIds.size());
for (BigInteger menuId : uniqueMenuIds) {
SysRoleMenu row = new SysRoleMenu(); SysRoleMenu row = new SysRoleMenu();
row.setRoleId(roleId); row.setRoleId(roleId);
row.setMenuId(new BigInteger(string)); row.setMenuId(menuId);
rows.add(row); rows.add(row);
}); }
sysRoleMenuMapper.insertBatch(rows); sysRoleMenuMapper.insertBatch(rows);
});
} }
@Override @Override
@@ -71,7 +95,8 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void saveRole(SysRole sysRole) { public void saveRole(SysRole sysRole) {
String lockKey = buildRoleLockKey(sysRole);
redisLockExecutor.executeWithLock(lockKey, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
saveOrUpdate(sysRole); saveOrUpdate(sysRole);
// 非自定义数据权限则部门id集合为空 // 非自定义数据权限则部门id集合为空
@@ -79,8 +104,14 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
sysRole.setDeptIds(new ArrayList<>()); sysRole.setDeptIds(new ArrayList<>());
} }
List<BigInteger> menuIds = sysRole.getMenuIds(); Set<BigInteger> uniqueMenuIds = new LinkedHashSet<>();
List<BigInteger> deptIds = sysRole.getDeptIds(); if (CollectionUtil.isNotEmpty(sysRole.getMenuIds())) {
uniqueMenuIds.addAll(sysRole.getMenuIds());
}
Set<BigInteger> uniqueDeptIds = new LinkedHashSet<>();
if (CollectionUtil.isNotEmpty(sysRole.getDeptIds())) {
uniqueDeptIds.addAll(sysRole.getDeptIds());
}
QueryWrapper wrm = QueryWrapper.create(); QueryWrapper wrm = QueryWrapper.create();
wrm.eq(SysRoleMenu::getRoleId, sysRole.getId()); wrm.eq(SysRoleMenu::getRoleId, sysRole.getId());
@@ -89,8 +120,11 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
wrd.eq(SysRoleDept::getRoleId, sysRole.getId()); wrd.eq(SysRoleDept::getRoleId, sysRole.getId());
sysRoleDeptMapper.deleteByQuery(wrd); sysRoleDeptMapper.deleteByQuery(wrd);
if (CollectionUtil.isNotEmpty(menuIds)) { if (CollectionUtil.isNotEmpty(uniqueMenuIds)) {
for (BigInteger menuId : menuIds) { for (BigInteger menuId : uniqueMenuIds) {
if (menuId == null) {
continue;
}
SysRoleMenu roleMenu = new SysRoleMenu(); SysRoleMenu roleMenu = new SysRoleMenu();
roleMenu.setRoleId(sysRole.getId()); roleMenu.setRoleId(sysRole.getId());
roleMenu.setMenuId(menuId); roleMenu.setMenuId(menuId);
@@ -98,13 +132,26 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
} }
} }
if (CollectionUtil.isNotEmpty(deptIds)) { if (CollectionUtil.isNotEmpty(uniqueDeptIds)) {
for (BigInteger deptId : deptIds) { for (BigInteger deptId : uniqueDeptIds) {
if (deptId == null) {
continue;
}
SysRoleDept roleDept = new SysRoleDept(); SysRoleDept roleDept = new SysRoleDept();
roleDept.setRoleId(sysRole.getId()); roleDept.setRoleId(sysRole.getId());
roleDept.setDeptId(deptId); roleDept.setDeptId(deptId);
sysRoleDeptMapper.insert(roleDept); sysRoleDeptMapper.insert(roleDept);
} }
} }
});
}
private String buildRoleLockKey(SysRole sysRole) {
if (sysRole.getId() != null) {
return ROLE_LOCK_KEY_PREFIX + sysRole.getId();
}
String tenantPart = sysRole.getTenantId() == null ? "0" : sysRole.getTenantId().toString();
String roleKeyPart = sysRole.getRoleKey() == null ? "unknown" : sysRole.getRoleKey();
return ROLE_CREATE_LOCK_KEY_PREFIX + tenantPart + ":" + roleKeyPart;
} }
} }

View File

@@ -72,7 +72,8 @@ CREATE TABLE `tb_bot_document_collection`
`bot_id` bigint UNSIGNED NULL DEFAULT NULL, `bot_id` bigint UNSIGNED NULL DEFAULT NULL,
`document_collection_id` bigint UNSIGNED NULL DEFAULT NULL, `document_collection_id` bigint UNSIGNED NULL DEFAULT NULL,
`options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_bot_document_collection`(`bot_id`, `document_collection_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 36 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的知识库' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 36 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的知识库' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
@@ -120,7 +121,8 @@ CREATE TABLE `tb_bot_plugin`
`bot_id` bigint UNSIGNED NULL DEFAULT NULL, `bot_id` bigint UNSIGNED NULL DEFAULT NULL,
`plugin_item_id` bigint UNSIGNED NULL DEFAULT NULL, `plugin_item_id` bigint UNSIGNED NULL DEFAULT NULL,
`options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_bot_plugin`(`bot_id`, `plugin_item_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的插件' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的插件' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
@@ -134,7 +136,8 @@ CREATE TABLE `tb_bot_recently_used`
`created` datetime(0) NOT NULL COMMENT '创建时间', `created` datetime(0) NOT NULL COMMENT '创建时间',
`created_by` bigint(0) UNSIGNED NOT NULL COMMENT '创建者', `created_by` bigint(0) UNSIGNED NOT NULL COMMENT '创建者',
`sort_no` int(0) NULL DEFAULT 0 COMMENT '排序', `sort_no` int(0) NULL DEFAULT 0 COMMENT '排序',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_bot_recently_used`(`created_by`, `bot_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '最近使用' ROW_FORMAT = Dynamic; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '最近使用' ROW_FORMAT = Dynamic;
-- ---------------------------- -- ----------------------------
@@ -147,7 +150,8 @@ CREATE TABLE `tb_bot_workflow`
`bot_id` bigint UNSIGNED NULL DEFAULT NULL, `bot_id` bigint UNSIGNED NULL DEFAULT NULL,
`workflow_id` bigint UNSIGNED NULL DEFAULT NULL, `workflow_id` bigint UNSIGNED NULL DEFAULT NULL,
`options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_bot_workflow`(`bot_id`, `workflow_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的工作流' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的工作流' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
@@ -704,7 +708,8 @@ CREATE TABLE `tb_sys_account_position`
`id` bigint UNSIGNED NOT NULL COMMENT '主键', `id` bigint UNSIGNED NOT NULL COMMENT '主键',
`account_id` bigint UNSIGNED NOT NULL COMMENT '用户ID', `account_id` bigint UNSIGNED NOT NULL COMMENT '用户ID',
`position_id` bigint UNSIGNED NOT NULL COMMENT '职位ID', `position_id` bigint UNSIGNED NOT NULL COMMENT '职位ID',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_account_position`(`account_id`, `position_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户-职位表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户-职位表' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
@@ -716,7 +721,8 @@ CREATE TABLE `tb_sys_account_role`
`id` bigint UNSIGNED NOT NULL COMMENT '主键', `id` bigint UNSIGNED NOT NULL COMMENT '主键',
`account_id` bigint UNSIGNED NOT NULL COMMENT '用户ID', `account_id` bigint UNSIGNED NOT NULL COMMENT '用户ID',
`role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID', `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_account_role`(`account_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户-角色表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户-角色表' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
@@ -733,7 +739,8 @@ CREATE TABLE `tb_sys_api_key`
`tenant_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '租户id', `tenant_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '租户id',
`expired_at` datetime NULL DEFAULT NULL COMMENT '失效时间', `expired_at` datetime NULL DEFAULT NULL COMMENT '失效时间',
`created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建人', `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建人',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_api_key`(`api_key`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'apikey表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'apikey表' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
@@ -758,7 +765,8 @@ CREATE TABLE `tb_sys_api_key_resource_mapping`
`id` bigint UNSIGNED NOT NULL COMMENT 'id', `id` bigint UNSIGNED NOT NULL COMMENT 'id',
`api_key_id` bigint UNSIGNED NOT NULL COMMENT 'api_key_id', `api_key_id` bigint UNSIGNED NOT NULL COMMENT 'api_key_id',
`api_key_resource_id` bigint UNSIGNED NOT NULL COMMENT '请求接口资源访问id', `api_key_resource_id` bigint UNSIGNED NOT NULL COMMENT '请求接口资源访问id',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_api_key_resource`(`api_key_id`, `api_key_resource_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'apikey-请求接口表' ROW_FORMAT = Dynamic; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'apikey-请求接口表' ROW_FORMAT = Dynamic;
-- ---------------------------- -- ----------------------------
@@ -924,7 +932,7 @@ CREATE TABLE `tb_sys_option`
`tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID',
`key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置KEY', `key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置KEY',
`value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '配置内容', `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '配置内容',
INDEX `uni_key`(`tenant_id`, `key`) USING BTREE UNIQUE INDEX `uni_key`(`tenant_id`, `key`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统配置信息表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统配置信息表' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
@@ -980,7 +988,8 @@ CREATE TABLE `tb_sys_role_dept`
`id` bigint UNSIGNED NOT NULL COMMENT '主键', `id` bigint UNSIGNED NOT NULL COMMENT '主键',
`role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID', `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID',
`dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_role_dept`(`role_id`, `dept_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色-部门表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色-部门表' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
@@ -992,7 +1001,8 @@ CREATE TABLE `tb_sys_role_menu`
`id` bigint UNSIGNED NOT NULL COMMENT '主键', `id` bigint UNSIGNED NOT NULL COMMENT '主键',
`role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID', `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID',
`menu_id` bigint UNSIGNED NOT NULL COMMENT '菜单ID', `menu_id` bigint UNSIGNED NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_role_menu`(`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色-菜单表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色-菜单表' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
@@ -1140,7 +1150,8 @@ CREATE TABLE `tb_bot_mcp`
`mcp_id` bigint(0) UNSIGNED NULL DEFAULT NULL COMMENT 'mcpId', `mcp_id` bigint(0) UNSIGNED NULL DEFAULT NULL COMMENT 'mcpId',
`mcp_tool_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'mcp工具名称', `mcp_tool_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'mcp工具名称',
`mcp_tool_description` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'mcp工具描述', `mcp_tool_description` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'mcp工具描述',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uni_bot_mcp`(`bot_id`, `mcp_id`, `mcp_tool_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET SET

View File

@@ -0,0 +1,64 @@
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- P1: 多机部署并发安全增强(唯一约束 + 去重)
-- 注意:本脚本为增量脚本,建议仅执行一次。
-- 1) 清理 bot-knowledge 重复绑定
DELETE t1
FROM tb_bot_document_collection t1
INNER JOIN tb_bot_document_collection t2
ON t1.bot_id <=> t2.bot_id
AND t1.document_collection_id <=> t2.document_collection_id
AND t1.id > t2.id;
-- 2) 清理 bot-workflow 重复绑定
DELETE t1
FROM tb_bot_workflow t1
INNER JOIN tb_bot_workflow t2
ON t1.bot_id <=> t2.bot_id
AND t1.workflow_id <=> t2.workflow_id
AND t1.id > t2.id;
-- 3) 清理 bot-plugin 重复绑定
DELETE t1
FROM tb_bot_plugin t1
INNER JOIN tb_bot_plugin t2
ON t1.bot_id <=> t2.bot_id
AND t1.plugin_item_id <=> t2.plugin_item_id
AND t1.id > t2.id;
-- 4) 清理 bot-mcp 重复绑定
DELETE t1
FROM tb_bot_mcp t1
INNER JOIN tb_bot_mcp t2
ON t1.bot_id <=> t2.bot_id
AND t1.mcp_id <=> t2.mcp_id
AND t1.mcp_tool_name <=> t2.mcp_tool_name
AND t1.id > t2.id;
-- 5) 清理最近使用重复记录
DELETE t1
FROM tb_bot_recently_used t1
INNER JOIN tb_bot_recently_used t2
ON t1.created_by = t2.created_by
AND t1.bot_id = t2.bot_id
AND t1.id > t2.id;
-- 增加唯一索引(并发写最终一致性兜底)
ALTER TABLE tb_bot_document_collection
ADD UNIQUE INDEX uni_bot_document_collection (bot_id, document_collection_id);
ALTER TABLE tb_bot_workflow
ADD UNIQUE INDEX uni_bot_workflow (bot_id, workflow_id);
ALTER TABLE tb_bot_plugin
ADD UNIQUE INDEX uni_bot_plugin (bot_id, plugin_item_id);
ALTER TABLE tb_bot_mcp
ADD UNIQUE INDEX uni_bot_mcp (bot_id, mcp_id, mcp_tool_name);
ALTER TABLE tb_bot_recently_used
ADD UNIQUE INDEX uni_bot_recently_used (created_by, bot_id);
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,88 @@
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- P1: 非 AI 核心模块并发安全增强(系统模块/数据关系模块)
-- 说明:本脚本为一次性增量执行脚本。
-- 1) 关系表去重(按业务复合键保留最小 id
DELETE t1
FROM tb_sys_account_position t1
INNER JOIN tb_sys_account_position t2
ON t1.account_id = t2.account_id
AND t1.position_id = t2.position_id
AND t1.id > t2.id;
DELETE t1
FROM tb_sys_account_role t1
INNER JOIN tb_sys_account_role t2
ON t1.account_id = t2.account_id
AND t1.role_id = t2.role_id
AND t1.id > t2.id;
DELETE t1
FROM tb_sys_role_menu t1
INNER JOIN tb_sys_role_menu t2
ON t1.role_id = t2.role_id
AND t1.menu_id = t2.menu_id
AND t1.id > t2.id;
DELETE t1
FROM tb_sys_role_dept t1
INNER JOIN tb_sys_role_dept t2
ON t1.role_id = t2.role_id
AND t1.dept_id = t2.dept_id
AND t1.id > t2.id;
DELETE t1
FROM tb_sys_api_key_resource_mapping t1
INNER JOIN tb_sys_api_key_resource_mapping t2
ON t1.api_key_id = t2.api_key_id
AND t1.api_key_resource_id = t2.api_key_resource_id
AND t1.id > t2.id;
DELETE t1
FROM tb_sys_api_key t1
INNER JOIN tb_sys_api_key t2
ON t1.api_key = t2.api_key
AND t1.id > t2.id
WHERE t1.api_key IS NOT NULL;
-- 2) sys_option 去重(无主键表,采用临时表重建)
DROP TABLE IF EXISTS tb_sys_option_tmp;
CREATE TABLE tb_sys_option_tmp LIKE tb_sys_option;
ALTER TABLE tb_sys_option_tmp DROP INDEX uni_key;
INSERT INTO tb_sys_option_tmp (tenant_id, `key`, `value`)
SELECT tenant_id, `key`, ANY_VALUE(`value`)
FROM tb_sys_option
GROUP BY tenant_id, `key`;
DELETE
FROM tb_sys_option;
INSERT INTO tb_sys_option (tenant_id, `key`, `value`)
SELECT tenant_id, `key`, `value`
FROM tb_sys_option_tmp;
DROP TABLE IF EXISTS tb_sys_option_tmp;
-- 3) 增加唯一约束(并发写兜底)
ALTER TABLE tb_sys_account_position
ADD UNIQUE INDEX uni_account_position (account_id, position_id);
ALTER TABLE tb_sys_account_role
ADD UNIQUE INDEX uni_account_role (account_id, role_id);
ALTER TABLE tb_sys_role_menu
ADD UNIQUE INDEX uni_role_menu (role_id, menu_id);
ALTER TABLE tb_sys_role_dept
ADD UNIQUE INDEX uni_role_dept (role_id, dept_id);
ALTER TABLE tb_sys_api_key_resource_mapping
ADD UNIQUE INDEX uni_api_key_resource (api_key_id, api_key_resource_id);
ALTER TABLE tb_sys_api_key
ADD UNIQUE INDEX uni_api_key (api_key);
ALTER TABLE tb_sys_option
DROP INDEX uni_key,
ADD UNIQUE INDEX uni_key (tenant_id, `key`);
SET FOREIGN_KEY_CHECKS = 1;