diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/job/SysJobController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/job/SysJobController.java index 17ea8a8..8c24463 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/job/SysJobController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/job/SysJobController.java @@ -6,7 +6,6 @@ import org.quartz.CronExpression; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import tech.easyflow.common.constant.enums.EnumJobStatus; import tech.easyflow.common.domain.Result; import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.web.controller.BaseCurdController; @@ -38,23 +37,14 @@ public class SysJobController extends BaseCurdController @GetMapping("/start") @SaCheckPermission("/api/v1/sysJob/save") public Result start(BigInteger id) { - SysJob sysJob = service.getById(id); - sysJob.setStatus(EnumJobStatus.RUNNING.getCode()); - service.addJob(sysJob); - service.updateById(sysJob); + service.startJob(id); return Result.ok(); } @GetMapping("/stop") @SaCheckPermission("/api/v1/sysJob/save") public Result stop(BigInteger id) { - SysJob sysJob = new SysJob(); - sysJob.setId(id); - sysJob.setStatus(EnumJobStatus.STOP.getCode()); - ArrayList ids = new ArrayList<>(); - ids.add(id); - service.deleteJob(ids); - service.updateById(sysJob); + service.stopJob(id); return Result.ok(); } @@ -88,4 +78,4 @@ public class SysJobController extends BaseCurdController service.deleteJob(ids); return super.onRemoveBefore(ids); } -} \ No newline at end of file +} diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java index e59f405..ca98c61 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java @@ -16,6 +16,7 @@ import cn.dev33.satoken.stp.StpUtil; import cn.hutool.crypto.digest.BCrypt; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.dao.DuplicateKeyException; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -156,4 +157,14 @@ public class SysAccountController extends BaseCurdController save(@JsonBody SysAccount entity) { + try { + return super.save(entity); + } catch (DuplicateKeyException e) { + return Result.fail(1, "用户名已存在"); + } + } +} diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysOptionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysOptionController.java index 2e93f10..6cdaa4a 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysOptionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysOptionController.java @@ -10,12 +10,14 @@ import tech.easyflow.common.web.jsonbody.JsonBody; import tech.easyflow.system.entity.SysOption; import tech.easyflow.system.service.SysOptionService; import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.dao.DuplicateKeyException; 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.RestController; import javax.annotation.Resource; +import java.math.BigInteger; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -39,7 +41,10 @@ public class SysOptionController extends BaseController { if (keys == null || keys.length == 0) { return Result.ok(data); } - List list = service.list(QueryWrapper.create().in(SysOption::getKey, (Object[]) keys)); + BigInteger tenantId = SaTokenUtil.getLoginAccount().getTenantId(); + List list = service.list(QueryWrapper.create() + .eq(SysOption::getTenantId, tenantId) + .in(SysOption::getKey, (Object[]) keys)); for (SysOption sysOption : list) { data.put(sysOption.getKey(), sysOption.getValue()); } @@ -62,12 +67,21 @@ public class SysOptionController extends BaseController { if (key == null || key.isEmpty()) { throw new BusinessException("key is empty"); } - sysOption.setTenantId(SaTokenUtil.getLoginAccount().getTenantId()); - SysOption record = service.getByOptionKey(key); - if (record == null) { - service.save(sysOption); - } else { + BigInteger tenantId = SaTokenUtil.getLoginAccount().getTenantId(); + sysOption.setTenantId(tenantId); + try { + SysOption record = service.getByOptionKey(key, tenantId); + if (record == null) { + service.save(sysOption); + } else { + 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); service.update(sysOption, w); } @@ -79,6 +93,7 @@ public class SysOptionController extends BaseController { if (key == null || key.isEmpty()) { throw new BusinessException("key is empty"); } - return Result.ok(service.getByOptionKey(key)); + BigInteger tenantId = SaTokenUtil.getLoginAccount().getTenantId(); + return Result.ok(service.getByOptionKey(key, tenantId)); } -} \ No newline at end of file +} diff --git a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java index 16988d6..a20ed87 100644 --- a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java +++ b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java @@ -6,8 +6,11 @@ import cn.dev33.satoken.annotation.SaIgnore; import com.alicp.jetcache.Cache; import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator; 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.Qualifier; +import org.springframework.dao.DuplicateKeyException; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -42,6 +45,8 @@ import java.util.Map; @UsePermission(moduleName = "/api/v1/bot") public class UcBotController extends BaseCurdController { + private static final Logger log = LoggerFactory.getLogger(UcBotController.class); + private final ModelService modelService; private final BotWorkflowService botWorkflowService; private final BotDocumentCollectionService botDocumentCollectionService; @@ -160,7 +165,12 @@ public class UcBotController extends BaseCurdController { conversation.setBotId(botId); conversation.setAccountId(SaTokenUtil.getLoginAccount().getId()); commonFiled(conversation, SaTokenUtil.getLoginAccount().getId(), SaTokenUtil.getLoginAccount().getTenantId(), SaTokenUtil.getLoginAccount().getDeptId()); - conversationMessageService.save(conversation); + try { + conversationMessageService.save(conversation); + } catch (DuplicateKeyException e) { + // 并发重试场景下允许重复创建请求,唯一主键冲突按已创建处理。 + log.debug("conversation already exists, conversationId={}", conversationId, e); + } } return botService.startChat(botId, prompt, conversationId, messages, chatCheckResult, attachments); diff --git a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotRecentlyUsedController.java b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotRecentlyUsedController.java index 441facd..f426e24 100644 --- a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotRecentlyUsedController.java +++ b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotRecentlyUsedController.java @@ -3,8 +3,10 @@ package tech.easyflow.usercenter.controller.ai; import cn.hutool.core.collection.CollectionUtil; import com.mybatisflex.core.query.QueryWrapper; 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.RestController; +import org.springframework.dao.DuplicateKeyException; import tech.easyflow.ai.entity.Bot; import tech.easyflow.ai.entity.BotRecentlyUsed; 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.satoken.util.SaTokenUtil; import tech.easyflow.common.web.controller.BaseCurdController; +import tech.easyflow.common.web.jsonbody.JsonBody; import javax.annotation.Resource; import java.math.BigInteger; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -81,4 +86,53 @@ public class UcBotRecentlyUsedController extends BaseCurdController 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 resultMap = new HashMap<>(); + resultMap.put("id", id); + return Result.ok(resultMap); + } +} diff --git a/easyflow-commons/easyflow-common-cache/pom.xml b/easyflow-commons/easyflow-common-cache/pom.xml index a72d713..20e4dad 100644 --- a/easyflow-commons/easyflow-common-cache/pom.xml +++ b/easyflow-commons/easyflow-common-cache/pom.xml @@ -11,6 +11,10 @@ easyflow-common-cache + + org.springframework.boot + spring-boot-starter-data-redis + redis.clients diff --git a/easyflow-commons/easyflow-common-cache/src/main/java/tech/easyflow/common/cache/RedisLockExecutor.java b/easyflow-commons/easyflow-common-cache/src/main/java/tech/easyflow/common/cache/RedisLockExecutor.java new file mode 100644 index 0000000..c0d6bfa --- /dev/null +++ b/easyflow-commons/easyflow-common-cache/src/main/java/tech/easyflow/common/cache/RedisLockExecutor.java @@ -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 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 executeWithLock(String lockKey, Duration waitTimeout, Duration leaseTimeout, Supplier 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); + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/listener/ChainEventListenerForSave.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/listener/ChainEventListenerForSave.java index d688170..45009f1 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/listener/ChainEventListenerForSave.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/listener/ChainEventListenerForSave.java @@ -9,6 +9,7 @@ import com.easyagents.flow.core.chain.listener.ChainEventListener; import com.easyagents.flow.core.chain.repository.NodeStateField; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Component; import tech.easyflow.ai.entity.Workflow; import tech.easyflow.ai.entity.WorkflowExecResult; @@ -63,6 +64,10 @@ public class ChainEventListenerForSave implements ChainEventListener { ChainState state = chain.getState(); Workflow workflow = workflowService.getById(definition.getId()); String instanceId = state.getInstanceId(); + WorkflowExecResult existed = workflowExecResultService.getByExecKey(instanceId); + if (existed != null) { + return; + } WorkflowExecResult record = new WorkflowExecResult(); record.setExecKey(instanceId); record.setWorkflowId(workflow.getId()); @@ -74,7 +79,12 @@ public class ChainEventListenerForSave implements ChainEventListener { record.setStatus(state.getStatus().getValue()); record.setCreatedKey(WorkFlowUtil.USER_KEY); record.setCreatedBy(WorkFlowUtil.getOperator(chain).getId().toString()); - workflowExecResultService.save(record); + try { + workflowExecResultService.save(record); + } catch (DuplicateKeyException e) { + // 多节点重试时可能并发写同一 exec_key,按幂等处理。 + log.debug("exec result already exists, execKey={}", instanceId, e); + } } private void handleChainEndEvent(ChainEndEvent event, Chain chain) { diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotDocumentCollectionServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotDocumentCollectionServiceImpl.java index fa68497..18cad7f 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotDocumentCollectionServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotDocumentCollectionServiceImpl.java @@ -5,10 +5,16 @@ import tech.easyflow.ai.mapper.BotDocumentCollectionMapper; import tech.easyflow.ai.service.BotDocumentCollectionService; import com.mybatisflex.spring.service.impl.ServiceImpl; 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.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; +import java.time.Duration; import com.mybatisflex.core.query.QueryWrapper; /** @@ -20,6 +26,13 @@ import com.mybatisflex.core.query.QueryWrapper; @Service public class BotDocumentCollectionServiceImpl extends ServiceImpl 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 public List listByBotId(BigInteger botId) { @@ -30,15 +43,30 @@ public class BotDocumentCollectionServiceImpl extends ServiceImpl list = new ArrayList<>(knowledgeIds.length); - for (BigInteger knowledgeId : knowledgeIds) { - BotDocumentCollection botDocumentCollection = new BotDocumentCollection(); - botDocumentCollection.setBotId(botId); - botDocumentCollection.setDocumentCollectionId(knowledgeId); - list.add(botDocumentCollection); - } - this.saveBatch(list); + redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> { + this.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId)); + Set uniqueKnowledgeIds = new LinkedHashSet<>(); + if (knowledgeIds != null) { + for (BigInteger knowledgeId : knowledgeIds) { + if (knowledgeId != null) { + uniqueKnowledgeIds.add(knowledgeId); + } + } + } + if (uniqueKnowledgeIds.isEmpty()) { + return; + } + + List list = new ArrayList<>(uniqueKnowledgeIds.size()); + for (BigInteger knowledgeId : uniqueKnowledgeIds) { + BotDocumentCollection botDocumentCollection = new BotDocumentCollection(); + botDocumentCollection.setBotId(botId); + botDocumentCollection.setDocumentCollectionId(knowledgeId); + list.add(botDocumentCollection); + } + this.saveBatch(list); + }); } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotMcpServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotMcpServiceImpl.java index 958a220..d8104f7 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotMcpServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotMcpServiceImpl.java @@ -7,11 +7,15 @@ import tech.easyflow.ai.entity.BotMcp; import tech.easyflow.ai.mapper.BotMcpMapper; import tech.easyflow.ai.service.BotMcpService; 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.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * 服务层实现。 @@ -22,29 +26,53 @@ import java.util.Map; @Service public class BotMcpServiceImpl extends ServiceImpl 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 @Transactional public void updateBotMcpToolIds(BigInteger botId, List>>> mcpSelectedData) { - // 删除原来绑定的mcp - this.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId)); - for (Map>> mcpItem : mcpSelectedData) { - for (Map.Entry>> entry : mcpItem.entrySet()) { - String mcpId = entry.getKey(); // 上一级id - List> toolList = entry.getValue(); // 包含name和description的二维数组 + redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> { + this.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId)); + if (mcpSelectedData == null || mcpSelectedData.isEmpty()) { + return; + } - // 遍历每个工具的[name, description] - for (List toolInfo : toolList) { - String toolName = toolInfo.get(0); // 工具名称 - String toolDesc = toolInfo.get(1); // 工具描述 - System.out.println("工具名称:" + toolName + ",描述:" + toolDesc); - BotMcp botMcp = new BotMcp(); - botMcp.setBotId(botId); - botMcp.setMcpId(new BigInteger(mcpId)); - botMcp.setMcpToolName(toolName); - botMcp.setMcpToolDescription(toolDesc); - this.save(botMcp); + Set uniqueTools = new LinkedHashSet<>(); + for (Map>> mcpItem : mcpSelectedData) { + for (Map.Entry>> entry : mcpItem.entrySet()) { + String mcpId = entry.getKey(); + List> toolList = entry.getValue(); + if (toolList == null || toolList.isEmpty()) { + continue; + } + + for (List toolInfo : toolList) { + if (toolInfo == null || toolInfo.size() < 2) { + continue; + } + 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.setBotId(botId); + botMcp.setMcpId(new BigInteger(mcpId)); + botMcp.setMcpToolName(toolName); + botMcp.setMcpToolDescription(toolDesc); + this.save(botMcp); + } } } - } + }); } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotPluginServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotPluginServiceImpl.java index 3ba7c26..a6a6a16 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotPluginServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotPluginServiceImpl.java @@ -3,16 +3,21 @@ package tech.easyflow.ai.service.impl; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.spring.service.impl.ServiceImpl; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import tech.easyflow.ai.entity.BotPlugin; import tech.easyflow.ai.entity.Plugin; import tech.easyflow.ai.mapper.BotPluginMapper; import tech.easyflow.ai.mapper.PluginMapper; import tech.easyflow.ai.service.BotPluginService; +import tech.easyflow.common.cache.RedisLockExecutor; import javax.annotation.Resource; +import java.time.Duration; import java.math.BigInteger; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; 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 public class BotPluginServiceImpl extends ServiceImpl 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 private BotPluginMapper botPluginMapper; @Resource private PluginMapper pluginMapper; + @Resource + private RedisLockExecutor redisLockExecutor; + @Override public List getList(String botId) { QueryWrapper w = QueryWrapper.create(); @@ -58,15 +70,31 @@ public class BotPluginServiceImpl extends ServiceImpl list = new ArrayList<>(pluginToolIds.length); - for (BigInteger pluginToolId : pluginToolIds) { - BotPlugin aiBotPluginTool = new BotPlugin(); - aiBotPluginTool.setBotId(botId); - aiBotPluginTool.setPluginItemId(pluginToolId); - list.add(aiBotPluginTool); - } - this.saveBatch(list); + redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> { + this.remove(QueryWrapper.create().eq(BotPlugin::getBotId, botId)); + + Set uniquePluginToolIds = new LinkedHashSet<>(); + if (pluginToolIds != null) { + for (BigInteger pluginToolId : pluginToolIds) { + if (pluginToolId != null) { + uniquePluginToolIds.add(pluginToolId); + } + } + } + if (uniquePluginToolIds.isEmpty()) { + return; + } + + List list = new ArrayList<>(uniquePluginToolIds.size()); + for (BigInteger pluginToolId : uniquePluginToolIds) { + BotPlugin aiBotPluginTool = new BotPlugin(); + aiBotPluginTool.setBotId(botId); + aiBotPluginTool.setPluginItemId(pluginToolId); + list.add(aiBotPluginTool); + } + this.saveBatch(list); + }); } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotWorkflowServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotWorkflowServiceImpl.java index d59c4dc..5f9ed43 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotWorkflowServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotWorkflowServiceImpl.java @@ -5,10 +5,16 @@ import tech.easyflow.ai.mapper.BotWorkflowMapper; import tech.easyflow.ai.service.BotWorkflowService; import com.mybatisflex.spring.service.impl.ServiceImpl; 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.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; +import java.time.Duration; import com.mybatisflex.core.query.QueryWrapper; /** @@ -20,6 +26,13 @@ import com.mybatisflex.core.query.QueryWrapper; @Service public class BotWorkflowServiceImpl extends ServiceImpl 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 public List listByBotId(BigInteger botId) { @@ -30,15 +43,31 @@ public class BotWorkflowServiceImpl extends ServiceImpl list = new ArrayList<>(workflowIds.length); - for (BigInteger workflowId : workflowIds) { - BotWorkflow botWorkflow = new BotWorkflow(); - botWorkflow.setBotId(botId); - botWorkflow.setWorkflowId(workflowId); - list.add(botWorkflow); - } - this.saveBatch(list); + redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> { + this.remove(QueryWrapper.create().eq(BotWorkflow::getBotId, botId)); + + Set uniqueWorkflowIds = new LinkedHashSet<>(); + if (workflowIds != null) { + for (BigInteger workflowId : workflowIds) { + if (workflowId != null) { + uniqueWorkflowIds.add(workflowId); + } + } + } + if (uniqueWorkflowIds.isEmpty()) { + return; + } + + List list = new ArrayList<>(uniqueWorkflowIds.size()); + for (BigInteger workflowId : uniqueWorkflowIds) { + BotWorkflow botWorkflow = new BotWorkflow(); + botWorkflow.setBotId(botId); + botWorkflow.setWorkflowId(workflowId); + list.add(botWorkflow); + } + this.saveBatch(list); + }); } } diff --git a/easyflow-modules/easyflow-module-datacenter/pom.xml b/easyflow-modules/easyflow-module-datacenter/pom.xml index 061695c..0dbaeb5 100644 --- a/easyflow-modules/easyflow-module-datacenter/pom.xml +++ b/easyflow-modules/easyflow-module-datacenter/pom.xml @@ -24,9 +24,13 @@ tech.easyflow easyflow-common-satoken + + tech.easyflow + easyflow-common-cache + tech.easyflow easyflow-common-web - \ No newline at end of file + diff --git a/easyflow-modules/easyflow-module-datacenter/src/main/java/tech/easyflow/datacenter/service/impl/DatacenterTableServiceImpl.java b/easyflow-modules/easyflow-module-datacenter/src/main/java/tech/easyflow/datacenter/service/impl/DatacenterTableServiceImpl.java index 1f89c91..4b55983 100644 --- a/easyflow-modules/easyflow-module-datacenter/src/main/java/tech/easyflow/datacenter/service/impl/DatacenterTableServiceImpl.java +++ b/easyflow-modules/easyflow-module-datacenter/src/main/java/tech/easyflow/datacenter/service/impl/DatacenterTableServiceImpl.java @@ -11,6 +11,7 @@ import com.mybatisflex.core.row.Row; import com.mybatisflex.spring.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import tech.easyflow.common.cache.RedisLockExecutor; import tech.easyflow.common.entity.DatacenterQuery; import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.web.exceptions.BusinessException; @@ -26,21 +27,35 @@ import tech.easyflow.datacenter.service.DatacenterTableService; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.Duration; import java.util.*; import java.util.stream.Collectors; @Service public class DatacenterTableServiceImpl extends ServiceImpl 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 private DbHandleManager dbHandleManager; @Resource private DatacenterTableFieldMapper fieldsMapper; + @Resource + private RedisLockExecutor redisLockExecutor; @Override @Transactional(rollbackFor = Exception.class) 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(); List fields = entity.getFields(); @@ -117,12 +132,19 @@ public class DatacenterTableServiceImpl extends ServiceImpl { + DatacenterTable record = getById(tableId); + dbHandleManager.getDbHandler().deleteTable(record); + removeById(tableId); + QueryWrapper wrapper = QueryWrapper.create(); + wrapper.eq(DatacenterTableField::getTableId, tableId); + fieldsMapper.deleteByQuery(wrapper); + } + ); } @Override @@ -247,6 +269,17 @@ public class DatacenterTableServiceImpl extends ServiceImpltech.easyflow easyflow-common-base + + tech.easyflow + easyflow-common-cache + tech.easyflow easyflow-common-satoken diff --git a/easyflow-modules/easyflow-module-job/src/main/java/tech/easyflow/job/service/SysJobService.java b/easyflow-modules/easyflow-module-job/src/main/java/tech/easyflow/job/service/SysJobService.java index f340c06..5be5dbb 100644 --- a/easyflow-modules/easyflow-module-job/src/main/java/tech/easyflow/job/service/SysJobService.java +++ b/easyflow-modules/easyflow-module-job/src/main/java/tech/easyflow/job/service/SysJobService.java @@ -4,6 +4,7 @@ import com.mybatisflex.core.service.IService; import tech.easyflow.job.entity.SysJob; import java.io.Serializable; +import java.math.BigInteger; import java.util.Collection; /** @@ -21,4 +22,8 @@ public interface SysJobService extends IService { void addJob(SysJob job); void deleteJob(Collection ids); + + void startJob(BigInteger id); + + void stopJob(BigInteger id); } diff --git a/easyflow-modules/easyflow-module-job/src/main/java/tech/easyflow/job/service/impl/SysJobServiceImpl.java b/easyflow-modules/easyflow-module-job/src/main/java/tech/easyflow/job/service/impl/SysJobServiceImpl.java index b5f30cc..a0c34aa 100644 --- a/easyflow-modules/easyflow-module-job/src/main/java/tech/easyflow/job/service/impl/SysJobServiceImpl.java +++ b/easyflow-modules/easyflow-module-job/src/main/java/tech/easyflow/job/service/impl/SysJobServiceImpl.java @@ -6,6 +6,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; 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.job.JobConstant; import tech.easyflow.job.job.QuartzJob; @@ -17,7 +19,9 @@ import tech.easyflow.job.util.JobUtil; import javax.annotation.Resource; import java.io.Serializable; import java.math.BigInteger; +import java.time.Duration; import java.util.Collection; +import java.util.Collections; /** * 系统任务表 服务层实现。 @@ -28,11 +32,18 @@ import java.util.Collection; @Service public class SysJobServiceImpl extends ServiceImpl 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); @Resource private Scheduler scheduler; + @Resource + private RedisLockExecutor redisLockExecutor; + @Override public void test() { System.out.println("java bean 动态执行"); @@ -92,4 +103,54 @@ public class SysJobServiceImpl extends ServiceImpl implem 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); + } + }); + } } diff --git a/easyflow-modules/easyflow-module-system/pom.xml b/easyflow-modules/easyflow-module-system/pom.xml index 2d6760c..700b8f6 100644 --- a/easyflow-modules/easyflow-module-system/pom.xml +++ b/easyflow-modules/easyflow-module-system/pom.xml @@ -24,6 +24,10 @@ tech.easyflow easyflow-common-satoken + + tech.easyflow + easyflow-common-cache + cn.hutool hutool-crypto diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/options/DefaultOptionStore.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/options/DefaultOptionStore.java index b82c74e..007e210 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/options/DefaultOptionStore.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/options/DefaultOptionStore.java @@ -5,11 +5,14 @@ import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.options.SysOptionStore; import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.util.StringUtil; +import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.system.entity.SysOption; import tech.easyflow.system.service.SysOptionService; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.math.BigInteger; @Component public class DefaultOptionStore implements SysOptionStore { @@ -20,28 +23,56 @@ public class DefaultOptionStore implements SysOptionStore { @Override public void save(String key, Object value) { + BigInteger tenantId = getTenantIdForWrite(); 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; } String newValue = value.toString().trim(); - SysOption option = optionService.getByOptionKey(key); - LoginAccount loginAccount = SaTokenUtil.getLoginAccount(); + SysOption option = optionService.getByOptionKey(key, tenantId); if (option == null) { option = new SysOption(key, newValue); - option.setTenantId(loginAccount.getTenantId()); - optionService.save(option); + option.setTenantId(tenantId); + try { + optionService.save(option); + } catch (DuplicateKeyException e) { + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(SysOption::getTenantId, tenantId) + .eq(SysOption::getKey, key); + optionService.update(option, queryWrapper); + } } else { 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); } } @Override 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; } + + 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; + } } diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysOptionService.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysOptionService.java index c357890..06308cc 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysOptionService.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysOptionService.java @@ -3,6 +3,8 @@ package tech.easyflow.system.service; import tech.easyflow.system.entity.SysOption; 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 getByOptionKey(String key); + SysOption getByOptionKey(String key, BigInteger tenantId); + + default SysOption getByOptionKey(String key) { + return getByOptionKey(key, null); + } } diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java index c5e27b7..88d7e1e 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java @@ -3,6 +3,8 @@ package tech.easyflow.system.service.impl; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.spring.service.impl.ServiceImpl; 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.SysAccountPosition; import tech.easyflow.system.entity.SysAccountRole; @@ -13,9 +15,12 @@ import tech.easyflow.system.mapper.SysRoleMapper; import tech.easyflow.system.service.SysAccountService; import javax.annotation.Resource; +import java.time.Duration; import java.math.BigInteger; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * 用户表 服务层实现。 @@ -26,53 +31,79 @@ import java.util.List; @Service public class SysAccountServiceImpl extends ServiceImpl 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 private SysAccountRoleMapper sysAccountRoleMapper; @Resource private SysAccountPositionMapper sysAccountPositionMapper; @Resource private SysRoleMapper sysRoleMapper; + @Resource + private RedisLockExecutor redisLockExecutor; @Override + @Transactional(rollbackFor = Exception.class) public void syncRelations(SysAccount entity) { if (entity == null || entity.getId() == null) { return; } - //sync roleIds - List roleIds = entity.getRoleIds(); - if (roleIds != null) { - QueryWrapper delW = QueryWrapper.create(); - delW.eq(SysAccountRole::getAccountId, entity.getId()); - sysAccountRoleMapper.deleteByQuery(delW); - if (!roleIds.isEmpty()) { - List rows = new ArrayList<>(roleIds.size()); - roleIds.forEach(roleId -> { - SysAccountRole row = new SysAccountRole(); - row.setAccountId(entity.getId()); - row.setRoleId(roleId); - rows.add(row); - }); - sysAccountRoleMapper.insertBatch(rows); - } - } + redisLockExecutor.executeWithLock( + ACCOUNT_RELATION_LOCK_KEY_PREFIX + entity.getId(), + LOCK_WAIT_TIMEOUT, + LOCK_LEASE_TIMEOUT, + () -> { + //sync roleIds + List roleIds = entity.getRoleIds(); + if (roleIds != null) { + QueryWrapper delW = QueryWrapper.create(); + delW.eq(SysAccountRole::getAccountId, entity.getId()); + sysAccountRoleMapper.deleteByQuery(delW); + Set uniqueRoleIds = new LinkedHashSet<>(roleIds); + if (!uniqueRoleIds.isEmpty()) { + List rows = new ArrayList<>(uniqueRoleIds.size()); + uniqueRoleIds.forEach(roleId -> { + if (roleId == null) { + return; + } + SysAccountRole row = new SysAccountRole(); + row.setAccountId(entity.getId()); + row.setRoleId(roleId); + rows.add(row); + }); + if (!rows.isEmpty()) { + sysAccountRoleMapper.insertBatch(rows); + } + } + } - //sync positionIds - List positionIds = entity.getPositionIds(); - if (positionIds != null) { - QueryWrapper delW = QueryWrapper.create(); - delW.eq(SysAccountPosition::getAccountId, entity.getId()); - sysAccountPositionMapper.deleteByQuery(delW); - if (!positionIds.isEmpty()) { - List rows = new ArrayList<>(positionIds.size()); - positionIds.forEach(positionId -> { - SysAccountPosition row = new SysAccountPosition(); - row.setAccountId(entity.getId()); - row.setPositionId(positionId); - rows.add(row); - }); - sysAccountPositionMapper.insertBatch(rows); - } - } + //sync positionIds + List positionIds = entity.getPositionIds(); + if (positionIds != null) { + QueryWrapper delW = QueryWrapper.create(); + delW.eq(SysAccountPosition::getAccountId, entity.getId()); + sysAccountPositionMapper.deleteByQuery(delW); + Set uniquePositionIds = new LinkedHashSet<>(positionIds); + if (!uniquePositionIds.isEmpty()) { + List rows = new ArrayList<>(uniquePositionIds.size()); + uniquePositionIds.forEach(positionId -> { + if (positionId == null) { + return; + } + SysAccountPosition row = new SysAccountPosition(); + row.setAccountId(entity.getId()); + row.setPositionId(positionId); + rows.add(row); + }); + if (!rows.isEmpty()) { + sysAccountPositionMapper.insertBatch(rows); + } + } + } + } + ); } @Override diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysApiKeyResourceMappingServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysApiKeyResourceMappingServiceImpl.java index 48bb5a0..134c4f1 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysApiKeyResourceMappingServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysApiKeyResourceMappingServiceImpl.java @@ -3,14 +3,20 @@ package tech.easyflow.system.service.impl; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.spring.service.impl.ServiceImpl; 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.SysApiKeyResourceMapping; import tech.easyflow.system.mapper.SysApiKeyResourceMappingMapper; import tech.easyflow.system.service.SysApiKeyResourceMappingService; +import javax.annotation.Resource; +import java.time.Duration; import java.math.BigInteger; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * apikey-请求接口表 服务层实现。 @@ -21,21 +27,43 @@ import java.util.List; @Service public class SysApiKeyResourceMappingServiceImpl extends ServiceImpl 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接口 * @param entity */ @Override + @Transactional(rollbackFor = Exception.class) public void authInterface(SysApiKey entity) { - this.remove(QueryWrapper.create().eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())); - List rows = new ArrayList<>(entity.getPermissionIds().size()); BigInteger apiKeyId = entity.getId(); - for (BigInteger resourceId : entity.getPermissionIds()) { - SysApiKeyResourceMapping sysApiKeyResourcePermissionRelationship = new SysApiKeyResourceMapping(); - sysApiKeyResourcePermissionRelationship.setApiKeyId(apiKeyId); - sysApiKeyResourcePermissionRelationship.setApiKeyResourceId(resourceId); - rows.add(sysApiKeyResourcePermissionRelationship); + if (apiKeyId == null) { + return; } - this.saveBatch(rows); + 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 uniqueResourceIds = new LinkedHashSet<>(entity.getPermissionIds()); + List rows = new ArrayList<>(uniqueResourceIds.size()); + for (BigInteger resourceId : uniqueResourceIds) { + if (resourceId == null) { + continue; + } + SysApiKeyResourceMapping sysApiKeyResourcePermissionRelationship = new SysApiKeyResourceMapping(); + sysApiKeyResourcePermissionRelationship.setApiKeyId(apiKeyId); + sysApiKeyResourcePermissionRelationship.setApiKeyResourceId(resourceId); + rows.add(sysApiKeyResourcePermissionRelationship); + } + if (!rows.isEmpty()) { + this.saveBatch(rows); + } + }); } } diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysOptionServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysOptionServiceImpl.java index 4be4819..8d6b480 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysOptionServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysOptionServiceImpl.java @@ -7,6 +7,8 @@ import tech.easyflow.system.service.SysOptionService; import com.mybatisflex.spring.service.impl.ServiceImpl; import org.springframework.stereotype.Service; +import java.math.BigInteger; + /** * 系统配置信息表。 服务层实现。 * @@ -17,9 +19,12 @@ import org.springframework.stereotype.Service; public class SysOptionServiceImpl extends ServiceImpl implements SysOptionService { @Override - public SysOption getByOptionKey(String key) { + public SysOption getByOptionKey(String key, BigInteger tenantId) { QueryWrapper w = QueryWrapper.create(); w.eq(SysOption::getKey, key); + if (tenantId != null) { + w.eq(SysOption::getTenantId, tenantId); + } return getOne(w); } } diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysRoleServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysRoleServiceImpl.java index 197022e..691f5a2 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysRoleServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysRoleServiceImpl.java @@ -5,6 +5,7 @@ import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.spring.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import tech.easyflow.common.cache.RedisLockExecutor; import tech.easyflow.common.constant.enums.EnumDataScope; import tech.easyflow.system.entity.SysAccountRole; import tech.easyflow.system.entity.SysRole; @@ -17,9 +18,12 @@ import tech.easyflow.system.service.SysAccountRoleService; import tech.easyflow.system.service.SysRoleService; import javax.annotation.Resource; +import java.time.Duration; import java.math.BigInteger; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; /** @@ -31,29 +35,49 @@ import java.util.stream.Collectors; @Service public class SysRoleServiceImpl extends ServiceImpl 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 private SysAccountRoleService sysAccountRoleService; @Resource private SysRoleMenuMapper sysRoleMenuMapper; @Resource private SysRoleDeptMapper sysRoleDeptMapper; + @Resource + private RedisLockExecutor redisLockExecutor; @Override @Transactional(rollbackFor = Exception.class) public void saveRoleMenu(BigInteger roleId, List keys) { + redisLockExecutor.executeWithLock(ROLE_LOCK_KEY_PREFIX + roleId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> { + QueryWrapper delW = QueryWrapper.create(); + delW.eq(SysRoleMenu::getRoleId, roleId); + sysRoleMenuMapper.deleteByQuery(delW); - QueryWrapper delW = QueryWrapper.create(); - delW.eq(SysRoleMenu::getRoleId, roleId); - sysRoleMenuMapper.deleteByQuery(delW); - - List rows = new ArrayList<>(keys.size()); - keys.forEach(string -> { - SysRoleMenu row = new SysRoleMenu(); - row.setRoleId(roleId); - row.setMenuId(new BigInteger(string)); - rows.add(row); + if (CollectionUtil.isEmpty(keys)) { + return; + } + Set uniqueMenuIds = new LinkedHashSet<>(); + for (String key : keys) { + if (key != null && !key.isEmpty()) { + uniqueMenuIds.add(new BigInteger(key)); + } + } + if (uniqueMenuIds.isEmpty()) { + return; + } + List rows = new ArrayList<>(uniqueMenuIds.size()); + for (BigInteger menuId : uniqueMenuIds) { + SysRoleMenu row = new SysRoleMenu(); + row.setRoleId(roleId); + row.setMenuId(menuId); + rows.add(row); + } + sysRoleMenuMapper.insertBatch(rows); }); - sysRoleMenuMapper.insertBatch(rows); } @Override @@ -71,40 +95,63 @@ public class SysRoleServiceImpl extends ServiceImpl impl @Override @Transactional(rollbackFor = Exception.class) public void saveRole(SysRole sysRole) { + String lockKey = buildRoleLockKey(sysRole); + redisLockExecutor.executeWithLock(lockKey, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> { + saveOrUpdate(sysRole); - saveOrUpdate(sysRole); - - // 非自定义数据权限,则部门id集合为空 - if (!EnumDataScope.CUSTOM.getCode().equals(sysRole.getDataScope())) { - sysRole.setDeptIds(new ArrayList<>()); - } - - List menuIds = sysRole.getMenuIds(); - List deptIds = sysRole.getDeptIds(); - - QueryWrapper wrm = QueryWrapper.create(); - wrm.eq(SysRoleMenu::getRoleId, sysRole.getId()); - sysRoleMenuMapper.deleteByQuery(wrm); - QueryWrapper wrd = QueryWrapper.create(); - wrd.eq(SysRoleDept::getRoleId, sysRole.getId()); - sysRoleDeptMapper.deleteByQuery(wrd); - - if (CollectionUtil.isNotEmpty(menuIds)) { - for (BigInteger menuId : menuIds) { - SysRoleMenu roleMenu = new SysRoleMenu(); - roleMenu.setRoleId(sysRole.getId()); - roleMenu.setMenuId(menuId); - sysRoleMenuMapper.insert(roleMenu); + // 非自定义数据权限,则部门id集合为空 + if (!EnumDataScope.CUSTOM.getCode().equals(sysRole.getDataScope())) { + sysRole.setDeptIds(new ArrayList<>()); } - } - if (CollectionUtil.isNotEmpty(deptIds)) { - for (BigInteger deptId : deptIds) { - SysRoleDept roleDept = new SysRoleDept(); - roleDept.setRoleId(sysRole.getId()); - roleDept.setDeptId(deptId); - sysRoleDeptMapper.insert(roleDept); + Set uniqueMenuIds = new LinkedHashSet<>(); + if (CollectionUtil.isNotEmpty(sysRole.getMenuIds())) { + uniqueMenuIds.addAll(sysRole.getMenuIds()); } + Set uniqueDeptIds = new LinkedHashSet<>(); + if (CollectionUtil.isNotEmpty(sysRole.getDeptIds())) { + uniqueDeptIds.addAll(sysRole.getDeptIds()); + } + + QueryWrapper wrm = QueryWrapper.create(); + wrm.eq(SysRoleMenu::getRoleId, sysRole.getId()); + sysRoleMenuMapper.deleteByQuery(wrm); + QueryWrapper wrd = QueryWrapper.create(); + wrd.eq(SysRoleDept::getRoleId, sysRole.getId()); + sysRoleDeptMapper.deleteByQuery(wrd); + + if (CollectionUtil.isNotEmpty(uniqueMenuIds)) { + for (BigInteger menuId : uniqueMenuIds) { + if (menuId == null) { + continue; + } + SysRoleMenu roleMenu = new SysRoleMenu(); + roleMenu.setRoleId(sysRole.getId()); + roleMenu.setMenuId(menuId); + sysRoleMenuMapper.insert(roleMenu); + } + } + + if (CollectionUtil.isNotEmpty(uniqueDeptIds)) { + for (BigInteger deptId : uniqueDeptIds) { + if (deptId == null) { + continue; + } + SysRoleDept roleDept = new SysRoleDept(); + roleDept.setRoleId(sysRole.getId()); + roleDept.setDeptId(deptId); + 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; } } diff --git a/sql/01-easyflow-v2.ddl.sql b/sql/01-easyflow-v2.ddl.sql index 20730f7..d8579ed 100644 --- a/sql/01-easyflow-v2.ddl.sql +++ b/sql/01-easyflow-v2.ddl.sql @@ -72,7 +72,8 @@ CREATE TABLE `tb_bot_document_collection` `bot_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, - 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; -- ---------------------------- @@ -120,7 +121,8 @@ CREATE TABLE `tb_bot_plugin` `bot_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, - 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; -- ---------------------------- @@ -134,7 +136,8 @@ CREATE TABLE `tb_bot_recently_used` `created` datetime(0) NOT NULL COMMENT '创建时间', `created_by` bigint(0) UNSIGNED NOT NULL 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; -- ---------------------------- @@ -147,7 +150,8 @@ CREATE TABLE `tb_bot_workflow` `bot_id` bigint UNSIGNED NULL DEFAULT NULL, `workflow_id` bigint UNSIGNED NULL DEFAULT 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; -- ---------------------------- @@ -704,7 +708,8 @@ CREATE TABLE `tb_sys_account_position` `id` bigint UNSIGNED NOT NULL COMMENT '主键', `account_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; -- ---------------------------- @@ -716,7 +721,8 @@ CREATE TABLE `tb_sys_account_role` `id` bigint UNSIGNED NOT NULL COMMENT '主键', `account_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; -- ---------------------------- @@ -733,7 +739,8 @@ CREATE TABLE `tb_sys_api_key` `tenant_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '租户id', `expired_at` datetime 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; -- ---------------------------- @@ -758,7 +765,8 @@ CREATE TABLE `tb_sys_api_key_resource_mapping` `id` bigint UNSIGNED NOT NULL COMMENT 'id', `api_key_id` bigint UNSIGNED NOT NULL COMMENT 'api_key_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; -- ---------------------------- @@ -924,7 +932,7 @@ CREATE TABLE `tb_sys_option` `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', `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 '配置内容', - 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; -- ---------------------------- @@ -980,7 +988,8 @@ CREATE TABLE `tb_sys_role_dept` `id` bigint UNSIGNED NOT NULL COMMENT '主键', `role_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; -- ---------------------------- @@ -992,7 +1001,8 @@ CREATE TABLE `tb_sys_role_menu` `id` bigint UNSIGNED NOT NULL COMMENT '主键', `role_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; -- ---------------------------- @@ -1140,7 +1150,8 @@ CREATE TABLE `tb_bot_mcp` `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_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; SET diff --git a/sql/03-easyflow-v2.p1-ha.sql b/sql/03-easyflow-v2.p1-ha.sql new file mode 100644 index 0000000..01b86bf --- /dev/null +++ b/sql/03-easyflow-v2.p1-ha.sql @@ -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; diff --git a/sql/04-easyflow-v2.p1-system-ha.sql b/sql/04-easyflow-v2.p1-system-ha.sql new file mode 100644 index 0000000..1dc5fd0 --- /dev/null +++ b/sql/04-easyflow-v2.p1-system-ha.sql @@ -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;