diff --git a/servers/boe-server-all/src/main/java/com/xboe/config/ThreadPoolConfig.java b/servers/boe-server-all/src/main/java/com/xboe/config/ThreadPoolConfig.java new file mode 100644 index 00000000..a7a26eca --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/config/ThreadPoolConfig.java @@ -0,0 +1,47 @@ +package com.xboe.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置类 + */ +@Configuration +@Slf4j +public class ThreadPoolConfig { + + /** + * 执行AI文档接口的的线程池 + * 策略:单线程等待队列 + */ + @Bean(name = "aiDocExecutor") + public ThreadPoolTaskExecutor aiDocExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + // 设置核心线程数 + int corePoolSize = Runtime.getRuntime().availableProcessors(); + executor.setCorePoolSize(Math.max(4, corePoolSize)); + // 设置最大线程数 + executor.setMaxPoolSize(Math.max(16, corePoolSize * 2)); + // 设置队列容量(确保任务排队) + executor.setQueueCapacity(100); + // keepalive + executor.setKeepAliveSeconds(30); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(30); + // 设置线程名称前缀 + executor.setThreadNamePrefix("ai_doc_task-"); + // 设置拒绝策略(当队列满时,由调用线程处理该任务) + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 初始化线程池 + executor.initialize(); + log.info("AI文档线程池初始化完成 - 核心线程: {}, 最大线程: {}, 队列容量: {}", + executor.getCorePoolSize(), + executor.getMaxPoolSize(), + executor.getQueueCapacity()); + return executor; + } +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java index d0ca8c73..2eeeb38e 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java @@ -4,6 +4,7 @@ import com.xboe.core.api.ApiBaseController; import com.xboe.core.JsonResponse; import com.xboe.module.boecase.dto.CaseAiChatDto; import com.xboe.module.boecase.service.ICaseAiChatService; +import com.xboe.module.boecase.service.ICaseAiPermissionService; import com.xboe.module.boecase.vo.CaseAiMessageVo; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -31,6 +32,9 @@ public class CaseAiChatApi extends ApiBaseController { */ @Autowired private ICaseAiChatService caseAiChatService; + + @Autowired + private ICaseAiPermissionService caseAiPermissionService; /** * 聊天 @@ -63,4 +67,20 @@ public class CaseAiChatApi extends ApiBaseController { return error("查询失败", e.getMessage()); } } + + /** + * 判断当前登录用户是否显示"案例专家"功能入口 + * @return 是否显示功能入口 + */ + @GetMapping("/show-entrance") + public JsonResponse showCaseAiEntrance() { + try { + String currentUserCode = getCurrent().getCode(); + boolean shouldShow = caseAiPermissionService.shouldShowCaseAiEntrance(currentUserCode); + return success(shouldShow); + } catch (Exception e) { + log.error("判断案例专家功能入口显示权限异常", e); + return error("判断失败", e.getMessage()); + } + } } diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseDocumentLogApi.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseDocumentLogApi.java index 342457f0..e674166f 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseDocumentLogApi.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseDocumentLogApi.java @@ -85,13 +85,15 @@ public class CaseDocumentLogApi extends ApiBaseController { @PostMapping("/retry") @AutoLog(module = "AI调用日志", action = "重试调用", info = "AI调用日志重试操作") public JsonResponse retry(@RequestBody RetryRequest request) { - try { - boolean result = caseDocumentLogService.retryByLogId(request.getLogId()); - return success(result); - } catch (Exception e) { - log.error("AI调用重试失败", e); - return error("重试失败", e.getMessage()); - } +// try { +// boolean result = caseDocumentLogService.retryByLogId(request.getLogId()); +// return success(result); +// } catch (Exception e) { +// log.error("AI调用重试失败", e); +// return error("重试失败", e.getMessage()); +// } + // 先走挡板 + return success(true); } /** diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/async/CaseAiDocumentAsyncHandler.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/async/CaseAiDocumentAsyncHandler.java new file mode 100644 index 00000000..3357e8a7 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/async/CaseAiDocumentAsyncHandler.java @@ -0,0 +1,66 @@ +package com.xboe.module.boecase.async; + +import com.xboe.enums.CaseDocumentLogOptTypeEnum; +import com.xboe.module.boecase.entity.Cases; +import com.xboe.module.boecase.service.ICaseKnowledgeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicInteger; + +@Component +@Slf4j +public class CaseAiDocumentAsyncHandler { + + private final AtomicInteger currentTaskCount = new AtomicInteger(0); + + @Autowired + @Qualifier("aiDocExecutor") + private ThreadPoolTaskExecutor aiDocExecutor; + + @Autowired + private ICaseKnowledgeService caseKnowledgeService; + + public void process(CaseDocumentLogOptTypeEnum optTypeEnum, Cases... caseList) { + for (Cases cases : caseList) { + // 控制并发数量 + while (currentTaskCount.get() >= 15) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + + currentTaskCount.incrementAndGet(); + + aiDocExecutor.submit(() -> { + processCases(cases, optTypeEnum); + currentTaskCount.decrementAndGet(); + }); + } + } + + private void processCases(Cases cases, CaseDocumentLogOptTypeEnum optTypeEnum) { + try { + switch (optTypeEnum) { + case UPDATE: + caseKnowledgeService.updateCaseDocument(cases.getId()); + break; + case DELETE: + caseKnowledgeService.deleteCaseDocument(cases.getId()); + break; + case CREATE: + default: + caseKnowledgeService.uploadCaseDocument(cases.getId()); + break; + } + } catch (Exception e) { + log.error("处理案例失败,caseId: {}, optType: {}", cases.getId(), optTypeEnum.getCode(), e); + } + } +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/properties/CaseAiProperties.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/properties/CaseAiProperties.java index 58265825..cba2be47 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/properties/CaseAiProperties.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/properties/CaseAiProperties.java @@ -3,6 +3,8 @@ package com.xboe.module.boecase.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; +import java.util.List; + /** * 案例专家AI相关配置项 */ @@ -39,4 +41,14 @@ public class CaseAiProperties { * 文档上传回调接口地址 */ private String fileUploadCallbackUrl; + + /** + * 是否启用白名单 + */ + private boolean useWhiteList; + + /** + * 白名单用户列表 + */ + private List whiteUserCodeList; } diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseAiPermissionService.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseAiPermissionService.java new file mode 100644 index 00000000..b9740300 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseAiPermissionService.java @@ -0,0 +1,14 @@ +package com.xboe.module.boecase.service; + +/** + * 案例AI权限服务接口 + */ +public interface ICaseAiPermissionService { + + /** + * 判断指定用户是否显示"案例专家"功能入口 + * @param userCode 用户编码 + * @return 是否显示功能入口 + */ + boolean shouldShowCaseAiEntrance(String userCode); +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiPermissionServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiPermissionServiceImpl.java new file mode 100644 index 00000000..c3f14452 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiPermissionServiceImpl.java @@ -0,0 +1,43 @@ +package com.xboe.module.boecase.service.impl; + +import com.xboe.module.boecase.properties.CaseAiProperties; +import com.xboe.module.boecase.service.ICaseAiPermissionService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 案例AI权限服务实现类 + */ +@Slf4j +@Service +@Transactional +public class CaseAiPermissionServiceImpl implements ICaseAiPermissionService { + + @Autowired + private CaseAiProperties caseAiProperties; + + /** + * 判断指定用户是否显示"案例专家"功能入口 + * @param userCode 用户编码 + * @return 是否显示功能入口 + */ + @Override + public boolean shouldShowCaseAiEntrance(String userCode) { + log.debug("判断用户[{}]是否显示案例专家功能入口", userCode); + + // 如果不启用白名单,直接返回true + if (!caseAiProperties.isUseWhiteList()) { + log.debug("未启用白名单,所有用户都显示功能入口"); + return true; + } + + // 启用白名单时,判断当前用户是否在白名单中 + boolean isInWhiteList = caseAiProperties.getWhiteUserCodeList() != null + && caseAiProperties.getWhiteUserCodeList().contains(userCode); + + log.debug("用户[{}]{}在白名单中", userCode, isInWhiteList ? "" : "不"); + return isInWhiteList; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseDocumentLogServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseDocumentLogServiceImpl.java index f5679657..3724cdda 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseDocumentLogServiceImpl.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseDocumentLogServiceImpl.java @@ -16,6 +16,7 @@ import com.xboe.module.boecase.vo.CaseDocumentLogVo; import com.xboe.enums.CaseDocumentLogOptTypeEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -39,6 +40,9 @@ public class CaseDocumentLogServiceImpl implements ICaseDocumentLogService { @Resource private ICaseKnowledgeService caseKnowledgeService; + @Resource(name = "aiDocExecutor") + private ThreadPoolTaskExecutor aiDocExecutor; + @Override public PageList pageQuery(int pageIndex, int pageSize, CaseDocumentLogQueryDto queryDto) { // 构建查询条件 @@ -170,56 +174,64 @@ public class CaseDocumentLogServiceImpl implements ICaseDocumentLogService { return false; } - log.info("开始重试AI调用,原始日志ID: {}, 案例标题: {}, 操作类型: {}", + log.info("开始异步重试AI调用,原始日志ID: {}, 案例标题: {}, 操作类型: {}", logId, originalLog.getCaseTitle(), originalLog.getOptType()); - // 2. 执行AI调用重试逻辑 + // 2. 使用线程池异步执行AI调用重试逻辑 + String optType = originalLog.getOptType(); + String caseId = originalLog.getCaseId(); + + aiDocExecutor.execute(() -> executeRetryLogic(optType, caseId)); + + // 立即返回true表示重试请求已接受,具体结果通过日志异步处理 + return true; + } + + /** + * 执行AI调用重试逻辑 + * @param optType 操作类型 + * @param caseId 案例ID + */ + private void executeRetryLogic(String optType, String caseId) { boolean retrySuccess = false; try { - // 根据操作类型调用对应的接口方法 - String optType = originalLog.getOptType(); - String caseId = originalLog.getCaseId(); - if (StringUtil.isBlank(caseId)) { throw new IllegalArgumentException("案例ID不能为空"); } - log.info("正在执行AI调用重试,操作类型: {}, caseId: {}", optType, caseId); + log.info("[异步任务] 正在执行AI调用重试,操作类型: {}, caseId: {}", optType, caseId); // 根据操作类型执行对应的方法(这些方法内部会自动创建日志记录) if (CaseDocumentLogOptTypeEnum.CREATE.getCode().equals(optType)) { // 上传案例文档 retrySuccess = caseKnowledgeService.uploadCaseDocument(caseId); - log.info("执行上传案例文档重试,caseId: {}, 结果: {}", caseId, retrySuccess); + log.info("[异步任务] 执行上传案例文档重试,caseId: {}, 结果: {}", caseId, retrySuccess); } else if (CaseDocumentLogOptTypeEnum.DELETE.getCode().equals(optType)) { // 删除案例文档 retrySuccess = caseKnowledgeService.deleteCaseDocument(caseId); - log.info("执行删除案例文档重试,caseId: {}, 结果: {}", caseId, retrySuccess); + log.info("[异步任务] 执行删除案例文档重试,caseId: {}, 结果: {}", caseId, retrySuccess); } else if (CaseDocumentLogOptTypeEnum.UPDATE.getCode().equals(optType)) { // 更新案例文档 retrySuccess = caseKnowledgeService.updateCaseDocument(caseId); - log.info("执行更新案例文档重试,caseId: {}, 结果: {}", caseId, retrySuccess); + log.info("[异步任务] 执行更新案例文档重试,caseId: {}, 结果: {}", caseId, retrySuccess); } else { throw new IllegalArgumentException("不支持的操作类型: " + optType); } if (retrySuccess) { - log.info("AI调用重试成功,操作类型: {}, caseId: {}", optType, caseId); + log.info("[异步任务] AI调用重试成功,操作类型: {}, caseId: {}", optType, caseId); } else { - log.warn("AI调用重试失败,操作类型: {}, caseId: {}", optType, caseId); + log.warn("[异步任务] AI调用重试失败,操作类型: {}, caseId: {}", optType, caseId); } } catch (Exception e) { - log.error("AI调用重试异常,操作类型: {}, caseId: {}", - originalLog.getOptType(), originalLog.getCaseId(), e); - retrySuccess = false; + log.error("[异步任务] AI调用重试异常,操作类型: {}, caseId: {}", + optType, caseId, e); } - - return retrySuccess; } @Override