From 5a2e08f87dc73e58ff526e09b0167001218063ca Mon Sep 17 00:00:00 2001 From: Administrator <1037463791@qq.com> Date: Fri, 8 Aug 2025 13:39:37 +0800 Subject: [PATCH 01/36] =?UTF-8?q?=E9=97=AE=E9=A2=98=E6=A6=82=E8=BF=B0=201.?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=B6=88=E6=81=AF=E5=92=8C=E8=AE=A1=E8=B4=B9?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=AD=98=E5=9C=A8=E8=80=A6=E5=90=88=202.?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=AE=A1=E8=B4=B9=E9=80=BB=E8=BE=91=EF=BC=9A?= =?UTF-8?q?=20=E6=8C=89=E6=AC=A1=E8=AE=A1=E8=B4=B9=E8=A2=AB=E9=98=88?= =?UTF-8?q?=E5=80=BC=E9=99=90=E5=88=B6=EF=BC=9A=E6=97=A7=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E6=8A=8A=20TIMES=20=E5=88=86=E6=94=AF=E6=94=BE=E5=9C=A8=20tota?= =?UTF-8?q?lTokens=20=E2=89=A5=20100=20=E7=9A=84=E5=A4=A7=E5=88=86?= =?UTF-8?q?=E6=94=AF=E9=87=8C=EF=BC=8C=E5=AF=BC=E8=87=B4=E6=B2=A1=E5=88=B0?= =?UTF-8?q?100=20token=E6=97=B6=E4=B8=8D=E6=89=A3=E8=B4=B9=EF=BC=8C?= =?UTF-8?q?=E8=BF=9D=E8=83=8C=E2=80=9C=E6=AF=8F=E6=AC=A1=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E5=B0=B1=E6=89=A3=E8=B4=B9=E2=80=9D=E7=9A=84=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E3=80=82=20token=E7=B4=AF=E8=AE=A1=E4=B8=8D=E5=BD=93=EF=BC=9AT?= =?UTF-8?q?IMES=20=E5=88=86=E6=94=AF=E5=8F=AA=E6=89=A3=E8=B4=B9=E4=B8=8D?= =?UTF-8?q?=E5=A4=84=E7=90=86=E7=B4=AF=E8=AE=A1=EF=BC=8C=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E5=9C=A8=20totalTokens=20<=20100=20=E6=97=B6=E4=B8=8D=E4=BC=9A?= =?UTF-8?q?=E8=BF=9B=E5=85=A5=E4=BB=BB=E4=BD=95TIMES=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E7=B4=AF=E8=AE=A1=E4=BC=9A=E6=97=A0=E6=84=8F=E4=B9=89?= =?UTF-8?q?=E5=A2=9E=E9=95=BF=E3=80=82=20=E7=B2=92=E5=BA=A6=E4=B8=8D?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A=EF=BC=9ATOKEN=20=E8=AE=A1=E8=B4=B9=E4=B8=80?= =?UTF-8?q?=E6=97=A6=E8=BE=BE=E9=98=88=E5=80=BC=E5=B0=B1=E6=8A=8A=20total?= =?UTF-8?q?=20=E5=85=A8=E6=89=A3=E5=AE=8C=E5=B9=B6=E6=B8=85=E9=9B=B6?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=88=A9=E4=BA=8E=E5=AF=B9=E8=B4=A6=E4=B8=8E?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82=20=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E6=96=B9=E5=BC=8F=EF=BC=9A=E4=BD=BF=E7=94=A8=20System?= =?UTF-8?q?.out.println=EF=BC=8C=E4=B8=8D=E5=88=A9=E4=BA=8E=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E8=BF=BD=E8=B8=AA=E3=80=82=203.=E5=BB=BA=E8=AE=AE?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E4=B8=8D=E8=A6=81=E5=AD=98=E6=89=A3?= =?UTF-8?q?=E9=99=A4=E9=87=91=E9=A2=9D=E5=92=8C=E7=B4=AF=E8=AE=A1=E6=B6=88?= =?UTF-8?q?=E8=80=97token=EF=BC=8C=E6=B6=88=E6=81=AF=E8=A1=A8=E9=87=8C?= =?UTF-8?q?=E4=B8=8D=E9=9C=80=E8=A6=81=E5=AD=98=E2=80=9C=E7=B4=AF=E8=AE=A1?= =?UTF-8?q?=E5=88=B0=E7=9B=AE=E5=89=8D=E4=B8=BA=E6=AD=A2=E5=A4=9A=E5=B0=91?= =?UTF-8?q?=E2=80=9D=EF=BC=8C=E5=90=A6=E5=88=99=E6=AF=8F=E6=9D=A1=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E9=83=BD=E5=8F=98=E6=88=90=E5=BF=AB=E7=85=A7=EF=BC=8C?= =?UTF-8?q?=E6=97=A2=E5=86=97=E4=BD=99=E5=8F=88=E6=98=93=E4=B8=8D=E4=B8=80?= =?UTF-8?q?=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改动要点 1.新增独立方法 saveMessage(ChatRequest): 只落库。 publishBillingEvent(ChatRequest): 只发布异步计费事件。 保留组合方法 saveMessageAndPublishEvent(ChatRequest) 以便需要一行调用时使用。 调用处已改为“先保存,再发布事件” SseServiceImpl: 先 saveMessage,再 publishBillingEvent。 SSEEventSourceListener: 同上。 DifyServiceImpl: 同上。 2.计费模式分流: TIMES:每次调用直接扣费,不累计。 TOKEN:按阈值(100)批量扣费,保留余数,账单颗粒稳定。 保留余数:total = prev + delta;billable = floor(total/threshold)threshold;remainder = total % threshold。 日志替换:统一使用 log.debug。 结构更清晰、可维护。 所有金额计算统一用 BigDecimal,保留两位小数,RoundingMode.HALF_UP 按次计费:每次直接扣费(BigDecimal),边界转 Double 按 token 计费:按阈值批量结算,保留余数;费用=单价(BigDecimal)×可结算token数 --- .../chat/event/ChatMessageCreatedEvent.java | 31 ++++ .../chat/listener/BillingEventListener.java | 37 +++++ .../chat/listener/SSEEventSourceListener.java | 4 +- .../chat/service/chat/IChatCostService.java | 16 ++ .../chat/impl/ChatCostServiceImpl.java | 157 +++++++++--------- .../service/chat/impl/DifyServiceImpl.java | 4 +- .../service/chat/impl/SseServiceImpl.java | 5 +- 7 files changed, 171 insertions(+), 83 deletions(-) create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/event/ChatMessageCreatedEvent.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/event/ChatMessageCreatedEvent.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/event/ChatMessageCreatedEvent.java new file mode 100644 index 00000000..7ecaea26 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/event/ChatMessageCreatedEvent.java @@ -0,0 +1,31 @@ +package org.ruoyi.chat.event; + +import org.springframework.context.ApplicationEvent; + +/** + * 聊天消息创建事件(用于异步计费/累计等) + */ +public class ChatMessageCreatedEvent extends ApplicationEvent { + + private final Long userId; + private final Long sessionId; + private final String modelName; + private final String role; + private final String content; + + public ChatMessageCreatedEvent(Long userId, Long sessionId, String modelName, String role, String content) { + super(userId); + this.userId = userId; + this.sessionId = sessionId; + this.modelName = modelName; + this.role = role; + this.content = content; + } + + public Long getUserId() { return userId; } + public Long getSessionId() { return sessionId; } + public String getModelName() { return modelName; } + public String getRole() { return role; } + public String getContent() { return content; } +} + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java new file mode 100644 index 00000000..427730c0 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java @@ -0,0 +1,37 @@ +package org.ruoyi.chat.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.chat.event.ChatMessageCreatedEvent; +import org.ruoyi.chat.service.chat.IChatCostService; +import org.ruoyi.common.chat.request.ChatRequest; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@Component +@RequiredArgsConstructor +public class BillingEventListener { + + private final IChatCostService chatCostService; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onChatMessageCreated(ChatMessageCreatedEvent event) { + try { + ChatRequest chatRequest = new ChatRequest(); + chatRequest.setUserId(event.getUserId()); + chatRequest.setSessionId(event.getSessionId()); + chatRequest.setModel(event.getModelName()); + chatRequest.setRole(event.getRole()); + chatRequest.setPrompt(event.getContent()); + // 异步执行计费累计与扣费 + chatCostService.deductToken(chatRequest); + } catch (Exception ex) { + log.error("BillingEventListener onChatMessageCreated error", ex); + } + } +} + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java index c91e28de..b8c8167e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java @@ -87,7 +87,9 @@ public class SSEEventSourceListener extends EventSourceListener { chatRequest.setPrompt(stringBuffer.toString()); // 记录会话token BaseContext.setCurrentToken(token); - chatCostService.deductToken(chatRequest); + // 先保存助手消息,再发布异步计费事件 + chatCostService.saveMessage(chatRequest); + chatCostService.publishBillingEvent(chatRequest); return; } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java index 73c0c443..36104f9f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java @@ -19,6 +19,22 @@ public interface IChatCostService { void deductToken(ChatRequest chatRequest); + /** + * 保存聊天消息记录(不进行计费) + * + * @param chatRequest 对话信息 + */ + void saveMessage(ChatRequest chatRequest); + + + + /** + * 仅发布异步计费事件(不做入库) + * + * @param chatRequest 对话信息 + */ + void publishBillingEvent(ChatRequest chatRequest); + /** * 直接扣除用户的余额 * diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java index c460bba4..0511cbb1 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java @@ -3,7 +3,10 @@ package org.ruoyi.chat.service.chat.impl; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import java.math.BigDecimal; +import java.math.RoundingMode; import org.ruoyi.chat.enums.BillingType; +import org.ruoyi.chat.event.ChatMessageCreatedEvent; import org.ruoyi.chat.enums.UserGradeType; import org.ruoyi.chat.service.chat.IChatCostService; import org.ruoyi.common.chat.request.ChatRequest; @@ -20,6 +23,7 @@ import org.ruoyi.service.IChatModelService; import org.ruoyi.service.IChatTokenService; import org.ruoyi.system.domain.SysUser; import org.ruoyi.system.mapper.SysUserMapper; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -42,106 +46,97 @@ public class ChatCostServiceImpl implements IChatCostService { private final IChatModelService chatModelService; + private final ApplicationEventPublisher eventPublisher; + /** - * 扣除用户余额 + * 扣除用户余额(仅计费与累计,不保存消息) */ @Override public void deductToken(ChatRequest chatRequest) { - - - if(chatRequest.getUserId()==null || chatRequest.getSessionId()==null){ + if (chatRequest.getUserId() == null || chatRequest.getSessionId() == null) { return; } - int tokens = TikTokensUtil.tokens(chatRequest.getModel(), chatRequest.getPrompt()); - - System.out.println("deductToken->本次提交token数 : "+tokens); + log.debug("deductToken->本次提交token数: {}", tokens); String modelName = chatRequest.getModel(); + ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName); + BigDecimal unitPrice = BigDecimal.valueOf(chatModelVo.getModelPrice()); - ChatMessageBo chatMessageBo = new ChatMessageBo(); + // 按次计费:每次调用都直接扣费,不累计token + if (BillingType.TIMES.getCode().equals(chatModelVo.getModelType())) { + BigDecimal numberCost = unitPrice.setScale(2, RoundingMode.HALF_UP); + deductUserBalance(chatRequest.getUserId(), numberCost.doubleValue()); + log.debug("deductToken->按次数扣费,费用: {},模型: {}", numberCost, modelName); + return; + } - // 设置用户id - chatMessageBo.setUserId(chatRequest.getUserId()); - // 设置会话id - chatMessageBo.setSessionId(chatRequest.getSessionId()); - - // 设置对话角色 - chatMessageBo.setRole(chatRequest.getRole()); - - // 设置对话内容 - chatMessageBo.setContent(chatRequest.getPrompt()); - - // 设置模型名字 - chatMessageBo.setModelName(chatRequest.getModel()); + // 按token计费:累加并按阈值批量扣费,保留余数 + final int threshold = 100; // 获得记录的累计token数 - ChatUsageToken chatToken = chatTokenService.queryByUserId(chatMessageBo.getUserId(), modelName); - - + ChatUsageToken chatToken = chatTokenService.queryByUserId(chatRequest.getUserId(), modelName); if (chatToken == null) { chatToken = new ChatUsageToken(); chatToken.setToken(0); } - // 计算总token数 - int totalTokens = chatToken.getToken() + tokens; - - //当前未付费token - int token = chatToken.getToken(); - - System.out.println("deductToken->未付费的token数 : "+token); - System.out.println("deductToken->本次提交+未付费token数 : "+totalTokens); - - - //扣费核心逻辑(总token大于100就要对未结清的token进行扣费) - if (totalTokens >= 100) {// 如果总token数大于等于100,进行费用扣除 - - ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName); - double cost = chatModelVo.getModelPrice(); - if (BillingType.TIMES.getCode().equals(chatModelVo.getModelType())) { - // 按次数扣费 - deductUserBalance(chatMessageBo.getUserId(), cost); - chatMessageBo.setDeductCost(cost); - }else { - // 按token扣费 - Double numberCost = totalTokens * cost; - System.out.println("deductToken->按token扣费 计算token数量: "+totalTokens); - System.out.println("deductToken->按token扣费 每token的价格: "+cost); - - deductUserBalance(chatMessageBo.getUserId(), numberCost); - chatMessageBo.setDeductCost(numberCost); - - // 保存剩余tokens - chatToken.setModelName(modelName); - chatToken.setUserId(chatMessageBo.getUserId()); - chatToken.setToken(0);//因为判断大于100token直接全部计算扣除了所以这里直接=0就可以了 - chatTokenService.editToken(chatToken); - } - + int previousUnpaid = chatToken.getToken(); + int totalTokens = previousUnpaid + tokens; + log.debug("deductToken->未付费token数: {},本次累计后总数: {}", previousUnpaid, totalTokens); + int billable = (totalTokens / threshold) * threshold; // 可计费整批token数 + int remainder = totalTokens - billable; // 结算后保留的余数 + if (billable > 0) { + BigDecimal numberCost = unitPrice + .multiply(BigDecimal.valueOf(billable)) + .setScale(2, RoundingMode.HALF_UP); + log.debug("deductToken->按token扣费,结算token数量: {},单价: {},费用: {}", billable, unitPrice, numberCost); + deductUserBalance(chatRequest.getUserId(), numberCost.doubleValue()); } else { - //不满100Token,不需要进行扣费啊啊啊 - //deductUserBalance(chatMessageBo.getUserId(), 0.0); - chatMessageBo.setDeductCost(0d); - chatMessageBo.setRemark("不满100Token,计入下一次!"); - System.out.println("deductToken->不满100Token,计入下一次!"); - chatToken.setToken(totalTokens); - chatToken.setModelName(chatMessageBo.getModelName()); - chatToken.setUserId(chatMessageBo.getUserId()); - chatTokenService.editToken(chatToken); + log.debug("deductToken->未达到计费阈值({}),累积到下次", threshold); } + // 保存剩余tokens(保留余数) + chatToken.setModelName(modelName); + chatToken.setUserId(chatRequest.getUserId()); + chatToken.setToken(remainder); + chatTokenService.editToken(chatToken); + } + /** + * 保存聊天消息记录(不进行计费) + */ + @Override + public void saveMessage(ChatRequest chatRequest) { + if (chatRequest.getUserId() == null || chatRequest.getSessionId() == null) { + return; + } + ChatMessageBo chatMessageBo = new ChatMessageBo(); + chatMessageBo.setUserId(chatRequest.getUserId()); + chatMessageBo.setSessionId(chatRequest.getSessionId()); + chatMessageBo.setRole(chatRequest.getRole()); + chatMessageBo.setContent(chatRequest.getPrompt()); + chatMessageBo.setModelName(chatRequest.getModel()); + - // 保存消息记录 chatMessageService.insertByBo(chatMessageBo); + } - System.out.println("deductToken->chatMessageService.insertByBo(: "+chatMessageBo); - System.out.println("----------------------------------------"); + + + @Override + public void publishBillingEvent(ChatRequest chatRequest) { + eventPublisher.publishEvent(new ChatMessageCreatedEvent( + chatRequest.getUserId(), + chatRequest.getSessionId(), + chatRequest.getModel(), + chatRequest.getRole(), + chatRequest.getPrompt() + )); } /** @@ -158,22 +153,26 @@ public class ChatCostServiceImpl implements IChatCostService { return; } - Double userBalance = sysUser.getUserBalance(); + BigDecimal userBalance = BigDecimal.valueOf(sysUser.getUserBalance() == null ? 0D : sysUser.getUserBalance()) + .setScale(2, RoundingMode.HALF_UP); + BigDecimal cost = BigDecimal.valueOf(numberCost == null ? 0D : numberCost) + .setScale(2, RoundingMode.HALF_UP); + log.debug("deductUserBalance->准备扣除: {},当前余额: {}", cost, userBalance); - System.out.println("deductUserBalance->准备扣除:numberCost: "+numberCost); - System.out.println("deductUserBalance->剩余金额:userBalance: "+userBalance); - - - if (userBalance < numberCost || userBalance == 0) { + if (userBalance.compareTo(cost) < 0 || userBalance.compareTo(BigDecimal.ZERO) == 0) { throw new ServiceException("余额不足, 请充值"); } - + BigDecimal newBalance = userBalance.subtract(cost); + if (newBalance.compareTo(BigDecimal.ZERO) < 0) { + newBalance = BigDecimal.ZERO; + } + newBalance = newBalance.setScale(2, RoundingMode.HALF_UP); sysUserMapper.update(null, new LambdaUpdateWrapper() - .set(SysUser::getUserBalance, Math.max(userBalance - numberCost, 0)) + .set(SysUser::getUserBalance, newBalance.doubleValue()) .eq(SysUser::getUserId, userId)); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java index ac3ebab7..3d0eeefa 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java @@ -111,7 +111,9 @@ public class DifyServiceImpl implements IChatService { chatRequestResponse.setUserId(chatRequest.getUserId()); chatRequestResponse.setSessionId(chatRequest.getSessionId()); chatRequestResponse.setPrompt(respMessage.toString()); - chatCostService.deductToken(chatRequestResponse); + // 先保存助手消息,再发布异步计费事件 + chatCostService.saveMessage(chatRequestResponse); + chatCostService.publishBillingEvent(chatRequestResponse); } @Override diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index 269fcac0..59c70d41 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -101,8 +101,9 @@ public class SseServiceImpl implements ISseService { } - // 保存消息记录 并扣除费用 - chatCostService.deductToken(chatRequest); + // 先保存消息,再发布异步计费事件 + chatCostService.saveMessage(chatRequest); + chatCostService.publishBillingEvent(chatRequest); chatRequest.setUserId(chatCostService.getUserId()); if(chatRequest.getSessionId()==null){ ChatSessionBo chatSessionBo = new ChatSessionBo(); From affdc5e3a692cfba238459443e8dc4f465f6f39a Mon Sep 17 00:00:00 2001 From: Administrator <1037463791@qq.com> Date: Thu, 14 Aug 2025 14:00:48 +0800 Subject: [PATCH 02/36] =?UTF-8?q?=E9=97=AE=E9=A2=98=E6=A6=82=E8=BF=B0=201.?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=B6=88=E6=81=AF=E5=92=8C=E8=AE=A1=E8=B4=B9?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=AD=98=E5=9C=A8=E8=80=A6=E5=90=88=202.?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=AE=A1=E8=B4=B9=E9=80=BB=E8=BE=91=EF=BC=9A?= =?UTF-8?q?=20=E6=8C=89=E6=AC=A1=E8=AE=A1=E8=B4=B9=E8=A2=AB=E9=98=88?= =?UTF-8?q?=E5=80=BC=E9=99=90=E5=88=B6=EF=BC=9A=E6=97=A7=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E6=8A=8A=20TIMES=20=E5=88=86=E6=94=AF=E6=94=BE=E5=9C=A8=20tota?= =?UTF-8?q?lTokens=20=E2=89=A5=20100=20=E7=9A=84=E5=A4=A7=E5=88=86?= =?UTF-8?q?=E6=94=AF=E9=87=8C=EF=BC=8C=E5=AF=BC=E8=87=B4=E6=B2=A1=E5=88=B0?= =?UTF-8?q?100=20token=E6=97=B6=E4=B8=8D=E6=89=A3=E8=B4=B9=EF=BC=8C?= =?UTF-8?q?=E8=BF=9D=E8=83=8C=E2=80=9C=E6=AF=8F=E6=AC=A1=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E5=B0=B1=E6=89=A3=E8=B4=B9=E2=80=9D=E7=9A=84=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E3=80=82=20token=E7=B4=AF=E8=AE=A1=E4=B8=8D=E5=BD=93=EF=BC=9AT?= =?UTF-8?q?IMES=20=E5=88=86=E6=94=AF=E5=8F=AA=E6=89=A3=E8=B4=B9=E4=B8=8D?= =?UTF-8?q?=E5=A4=84=E7=90=86=E7=B4=AF=E8=AE=A1=EF=BC=8C=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E5=9C=A8=20totalTokens=20<=20100=20=E6=97=B6=E4=B8=8D=E4=BC=9A?= =?UTF-8?q?=E8=BF=9B=E5=85=A5=E4=BB=BB=E4=BD=95TIMES=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E7=B4=AF=E8=AE=A1=E4=BC=9A=E6=97=A0=E6=84=8F=E4=B9=89?= =?UTF-8?q?=E5=A2=9E=E9=95=BF=E3=80=82=20=E7=B2=92=E5=BA=A6=E4=B8=8D?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A=EF=BC=9ATOKEN=20=E8=AE=A1=E8=B4=B9=E4=B8=80?= =?UTF-8?q?=E6=97=A6=E8=BE=BE=E9=98=88=E5=80=BC=E5=B0=B1=E6=8A=8A=20total?= =?UTF-8?q?=20=E5=85=A8=E6=89=A3=E5=AE=8C=E5=B9=B6=E6=B8=85=E9=9B=B6?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=88=A9=E4=BA=8E=E5=AF=B9=E8=B4=A6=E4=B8=8E?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82=20=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E6=96=B9=E5=BC=8F=EF=BC=9A=E4=BD=BF=E7=94=A8=20System?= =?UTF-8?q?.out.println=EF=BC=8C=E4=B8=8D=E5=88=A9=E4=BA=8E=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E8=BF=BD=E8=B8=AA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改动要点 1.新增独立方法 saveMessage(ChatRequest): 只落库。 publishBillingEvent(ChatRequest): 只发布异步计费事件。 保留组合方法 saveMessageAndPublishEvent(ChatRequest) 以便需要一行调用时使用。 调用处已改为“先保存,再发布事件” SseServiceImpl: 先 saveMessage,再 publishBillingEvent。 SSEEventSourceListener: 同上。 DifyServiceImpl: 同上。 2.计费模式分流: TIMES:每次调用直接扣费,不累计。 TOKEN:按阈值(100)批量扣费,保留余数,账单颗粒稳定。 保留余数:total = prev + delta;billable = floor(total/threshold)threshold;remainder = total % threshold。 日志替换:统一使用 log.debug。 结构更清晰、可维护。 所有金额计算统一用 BigDecimal,保留两位小数,RoundingMode.HALF_UP 按次计费:每次直接扣费(BigDecimal),边界转 Double 按 token 计费:按阈值批量结算,保留余数;费用=单价(BigDecimal)×可结算token数 1. 消息分类存储 用户消息:role="user", deductCost=null, totalTokens=本次token数, remark="用户消息" 系统账单:role="system", deductCost=实际扣费, totalTokens=计费token数, remark="TIMES_BILLING/TOKEN_BILLING" 2. 数据流程 用户发送消息 → 预检查余额 → 保存用户消息 → 发布计费事件 → 异步扣费 → 保存账单记录 --- .../chat/listener/BillingEventListener.java | 15 +- .../chat/impl/ChatCostServiceImpl.java | 175 ++++++++++++++++-- 2 files changed, 176 insertions(+), 14 deletions(-) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java index 427730c0..74eda273 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java @@ -7,6 +7,7 @@ import org.ruoyi.chat.service.chat.IChatCostService; import org.ruoyi.common.chat.request.ChatRequest; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +import org.springframework.context.event.EventListener; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; @@ -18,8 +19,10 @@ public class BillingEventListener { private final IChatCostService chatCostService; @Async - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @EventListener public void onChatMessageCreated(ChatMessageCreatedEvent event) { + log.debug("BillingEventListener->接收到计费事件,用户ID: {},会话ID: {},模型: {}", + event.getUserId(), event.getSessionId(), event.getModelName()); try { ChatRequest chatRequest = new ChatRequest(); chatRequest.setUserId(event.getUserId()); @@ -28,9 +31,17 @@ public class BillingEventListener { chatRequest.setRole(event.getRole()); chatRequest.setPrompt(event.getContent()); // 异步执行计费累计与扣费 + log.debug("BillingEventListener->开始执行计费逻辑"); chatCostService.deductToken(chatRequest); + log.debug("BillingEventListener->计费逻辑执行完成"); } catch (Exception ex) { - log.error("BillingEventListener onChatMessageCreated error", ex); + // 由于已有预检查,这里的异常主要是系统异常(数据库连接等) + // 记录错误但不中断异步线程 + log.error("BillingEventListener->异步计费异常,用户ID: {},模型: {},错误: {}", + event.getUserId(), event.getModelName(), ex.getMessage(), ex); + + // TODO: 可以考虑加入重试机制或者错误通知机制 + // 例如:发送到死信队列,或者通知运维人员 } } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java index 0511cbb1..6947893d 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java @@ -53,7 +53,8 @@ public class ChatCostServiceImpl implements IChatCostService { */ @Override public void deductToken(ChatRequest chatRequest) { - if (chatRequest.getUserId() == null || chatRequest.getSessionId() == null) { + if (chatRequest.getUserId() == null) { + log.warn("deductToken->用户ID为空,跳过计费"); return; } @@ -69,6 +70,17 @@ public class ChatCostServiceImpl implements IChatCostService { BigDecimal numberCost = unitPrice.setScale(2, RoundingMode.HALF_UP); deductUserBalance(chatRequest.getUserId(), numberCost.doubleValue()); log.debug("deductToken->按次数扣费,费用: {},模型: {}", numberCost, modelName); + + // 清理可能存在的历史累计token(模型计费方式可能发生过变更) + ChatUsageToken existingToken = chatTokenService.queryByUserId(chatRequest.getUserId(), modelName); + if (existingToken != null && existingToken.getToken() > 0) { + existingToken.setToken(0); + chatTokenService.editToken(existingToken); + log.debug("deductToken->按次计费,清理历史累计token: {}", existingToken.getToken()); + } + + // 记录账单消息 + saveBillingRecord(chatRequest, tokens, numberCost.doubleValue(), "TIMES_BILLING"); return; } @@ -76,10 +88,13 @@ public class ChatCostServiceImpl implements IChatCostService { final int threshold = 100; // 获得记录的累计token数 + // TODO: 这里存在并发竞态条件,需要在chatTokenService层面添加乐观锁或分布式锁 ChatUsageToken chatToken = chatTokenService.queryByUserId(chatRequest.getUserId(), modelName); if (chatToken == null) { chatToken = new ChatUsageToken(); chatToken.setToken(0); + chatToken.setModelName(modelName); + chatToken.setUserId(chatRequest.getUserId()); } int previousUnpaid = chatToken.getToken(); @@ -94,16 +109,32 @@ public class ChatCostServiceImpl implements IChatCostService { .multiply(BigDecimal.valueOf(billable)) .setScale(2, RoundingMode.HALF_UP); log.debug("deductToken->按token扣费,结算token数量: {},单价: {},费用: {}", billable, unitPrice, numberCost); - deductUserBalance(chatRequest.getUserId(), numberCost.doubleValue()); + + try { + // 先尝试扣费 + deductUserBalance(chatRequest.getUserId(), numberCost.doubleValue()); + // 扣费成功后,保存余数 + chatToken.setModelName(modelName); + chatToken.setUserId(chatRequest.getUserId()); + chatToken.setToken(remainder); + chatTokenService.editToken(chatToken); + log.debug("deductToken->扣费成功,更新余数: {}", remainder); + + // 记录账单消息 + saveBillingRecord(chatRequest, billable, numberCost.doubleValue(), "TOKEN_BILLING"); + } catch (ServiceException e) { + // 余额不足时,不更新token累计,保持原有累计数 + log.warn("deductToken->余额不足,本次token累计保持不变: {}", totalTokens); + throw e; // 重新抛出异常 + } } else { + // 未达阈值,累积token log.debug("deductToken->未达到计费阈值({}),累积到下次", threshold); + chatToken.setModelName(modelName); + chatToken.setUserId(chatRequest.getUserId()); + chatToken.setToken(totalTokens); + chatTokenService.editToken(chatToken); } - - // 保存剩余tokens(保留余数) - chatToken.setModelName(modelName); - chatToken.setUserId(chatRequest.getUserId()); - chatToken.setToken(remainder); - chatTokenService.editToken(chatToken); } /** @@ -112,24 +143,57 @@ public class ChatCostServiceImpl implements IChatCostService { @Override public void saveMessage(ChatRequest chatRequest) { if (chatRequest.getUserId() == null || chatRequest.getSessionId() == null) { + log.warn("saveMessage->用户ID或会话ID为空,跳过保存消息"); return; } + + // 验证消息内容 + if (chatRequest.getPrompt() == null || chatRequest.getPrompt().trim().isEmpty()) { + log.warn("saveMessage->消息内容为空,跳过保存"); + return; + } + ChatMessageBo chatMessageBo = new ChatMessageBo(); chatMessageBo.setUserId(chatRequest.getUserId()); chatMessageBo.setSessionId(chatRequest.getSessionId()); chatMessageBo.setRole(chatRequest.getRole()); - chatMessageBo.setContent(chatRequest.getPrompt()); + chatMessageBo.setContent(chatRequest.getPrompt().trim()); chatMessageBo.setModelName(chatRequest.getModel()); + + // 计算并保存本次消息的token数 + int tokens = TikTokensUtil.tokens(chatRequest.getModel(), chatRequest.getPrompt()); + chatMessageBo.setTotalTokens(tokens); + + // 普通消息不涉及扣费,deductCost保持null + chatMessageBo.setDeductCost(null); + chatMessageBo.setRemark("用户消息"); - - - chatMessageService.insertByBo(chatMessageBo); + try { + chatMessageService.insertByBo(chatMessageBo); + log.debug("saveMessage->成功保存消息,用户ID: {}, 会话ID: {}, tokens: {}", + chatRequest.getUserId(), chatRequest.getSessionId(), tokens); + } catch (Exception e) { + log.error("saveMessage->保存消息失败", e); + throw new ServiceException("保存消息失败"); + } } @Override public void publishBillingEvent(ChatRequest chatRequest) { + log.debug("publishBillingEvent->发布计费事件,用户ID: {},会话ID: {},模型: {}", + chatRequest.getUserId(), chatRequest.getSessionId(), chatRequest.getModel()); + + // 预检查:评估可能的扣费金额,如果余额不足则直接抛异常 + try { + preCheckBalance(chatRequest); + } catch (ServiceException e) { + log.warn("publishBillingEvent->预检查余额不足,用户ID: {},模型: {}", + chatRequest.getUserId(), chatRequest.getModel()); + throw e; // 直接抛出,阻止消息保存和对话继续 + } + eventPublisher.publishEvent(new ChatMessageCreatedEvent( chatRequest.getUserId(), chatRequest.getSessionId(), @@ -137,6 +201,93 @@ public class ChatCostServiceImpl implements IChatCostService { chatRequest.getRole(), chatRequest.getPrompt() )); + log.debug("publishBillingEvent->计费事件发布完成"); + } + + /** + * 预检查用户余额是否足够支付可能的费用 + */ + private void preCheckBalance(ChatRequest chatRequest) { + if (chatRequest.getUserId() == null) { + return; + } + + int tokens = TikTokensUtil.tokens(chatRequest.getModel(), chatRequest.getPrompt()); + String modelName = chatRequest.getModel(); + ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName); + BigDecimal unitPrice = BigDecimal.valueOf(chatModelVo.getModelPrice()); + + // 按次计费:直接检查单次费用 + if (BillingType.TIMES.getCode().equals(chatModelVo.getModelType())) { + BigDecimal numberCost = unitPrice.setScale(2, RoundingMode.HALF_UP); + checkUserBalanceWithoutDeduct(chatRequest.getUserId(), numberCost.doubleValue()); + return; + } + + // 按token计费:检查累计后可能的费用 + final int threshold = 100; + ChatUsageToken chatToken = chatTokenService.queryByUserId(chatRequest.getUserId(), modelName); + int previousUnpaid = (chatToken == null) ? 0 : chatToken.getToken(); + int totalTokens = previousUnpaid + tokens; + + int billable = (totalTokens / threshold) * threshold; + if (billable > 0) { + BigDecimal numberCost = unitPrice + .multiply(BigDecimal.valueOf(billable)) + .setScale(2, RoundingMode.HALF_UP); + checkUserBalanceWithoutDeduct(chatRequest.getUserId(), numberCost.doubleValue()); + } + } + + /** + * 检查用户余额是否足够,但不扣除 + */ + private void checkUserBalanceWithoutDeduct(Long userId, Double numberCost) { + SysUser sysUser = sysUserMapper.selectById(userId); + if (sysUser == null) { + throw new ServiceException("用户不存在"); + } + + BigDecimal userBalance = BigDecimal.valueOf(sysUser.getUserBalance() == null ? 0D : sysUser.getUserBalance()) + .setScale(2, RoundingMode.HALF_UP); + BigDecimal cost = BigDecimal.valueOf(numberCost == null ? 0D : numberCost) + .setScale(2, RoundingMode.HALF_UP); + + if (userBalance.compareTo(cost) < 0 || userBalance.compareTo(BigDecimal.ZERO) == 0) { + throw new ServiceException("余额不足, 请充值。当前余额: " + userBalance + ",需要: " + cost); + } + } + + /** + * 保存账单记录 + */ + private void saveBillingRecord(ChatRequest chatRequest, int billedTokens, double cost, String billingType) { + try { + ChatMessageBo billingMessage = new ChatMessageBo(); + billingMessage.setUserId(chatRequest.getUserId()); + billingMessage.setSessionId(chatRequest.getSessionId()); + billingMessage.setRole("system"); // 系统账单消息 + billingMessage.setModelName(chatRequest.getModel()); + billingMessage.setTotalTokens(billedTokens); + billingMessage.setDeductCost(cost); + billingMessage.setRemark(billingType); + + // 构建账单消息内容 + String content; + if ("TIMES_BILLING".equals(billingType)) { + content = String.format("按次计费:消耗 %d tokens,扣费 %.2f 元", billedTokens, cost); + } else { + content = String.format("按量计费:结算 %d tokens,扣费 %.2f 元", billedTokens, cost); + } + billingMessage.setContent(content); + + chatMessageService.insertByBo(billingMessage); + log.debug("saveBillingRecord->保存账单记录成功,用户ID: {}, 计费类型: {}, 费用: {}", + chatRequest.getUserId(), billingType, cost); + } catch (Exception e) { + log.error("saveBillingRecord->保存账单记录失败", e); + // 账单记录失败不影响主流程,只记录错误日志 + } } /** From 42aabeed96b1015173d8b058017f30d9985dcb31 Mon Sep 17 00:00:00 2001 From: l90215 Date: Thu, 7 Aug 2025 21:13:02 +0800 Subject: [PATCH 03/36] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=A9=BA?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java index 9ef76085..e4a85949 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java @@ -69,6 +69,7 @@ public class GenTableServiceImpl implements IGenTableService { log.warn("Schema不存在,表名: {}", tableName); return; } + // 查询Schema字段信息 List fields = schemaFieldService.queryListByTableName(tableName); if (CollUtil.isEmpty(fields)) { From 4834b615a6f0a90f6afb31b8c6e9c8dd227bf9a7 Mon Sep 17 00:00:00 2001 From: l90215 Date: Sun, 10 Aug 2025 00:25:49 +0800 Subject: [PATCH 04/36] =?UTF-8?q?feat:=20fix=E4=BB=A3=E7=A0=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=B1=BB=E5=9E=8B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generator/impl/GenTableServiceImpl.java | 106 ++++++++---------- .../impl/SchemaFieldServiceImpl.java | 12 +- .../src/main/resources/vm/java/bo.java.vm | 4 +- .../src/main/resources/vm/java/domain.java.vm | 26 +---- 4 files changed, 60 insertions(+), 88 deletions(-) diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java index e4a85949..bc885cb9 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java @@ -129,17 +129,17 @@ public class GenTableServiceImpl implements IGenTableService { */ private VelocityContext prepareSchemaContext(SchemaVo schema, List fields) { VelocityContext context = new VelocityContext(); - + // 从配置文件读取基本配置 String packageName = GenConfig.getPackageName(); String author = GenConfig.getAuthor(); String tablePrefix = GenConfig.getTablePrefix(); boolean autoRemovePre = GenConfig.getAutoRemovePre(); - + // 处理表名和类名 String tableName = schema.getTableName(); String baseClassName = schema.getTableName(); - + // 自动去除表前缀 if (autoRemovePre && StrUtil.isNotBlank(tablePrefix)) { String[] prefixes = tablePrefix.split(","); @@ -150,12 +150,12 @@ public class GenTableServiceImpl implements IGenTableService { } } } - + String className = toCamelCase(baseClassName, true); // 首字母大写的类名,如:SysRole String classname = toCamelCase(baseClassName, false); // 首字母小写的类名,如:sysRole String businessName = toCamelCase(baseClassName, false); String moduleName = getModuleName(packageName); - + // 基本信息 context.put("tableName", tableName); context.put("tableComment", schema.getComment()); @@ -169,18 +169,18 @@ public class GenTableServiceImpl implements IGenTableService { context.put("packageName", packageName); context.put("moduleName", moduleName); context.put("businessName", businessName); - + // 权限相关 context.put("permissionPrefix", moduleName + ":" + businessName); context.put("parentMenuId", "2000"); // 默认父菜单ID,可配置 - + // 生成菜单ID List menuIds = new ArrayList<>(); for (int i = 0; i < 6; i++) { menuIds.add(IdUtil.getSnowflakeNextId()); } context.put("menuIds", menuIds); - + // 创建table对象,包含menuIds等信息和方法 Map table = new HashMap<>(); table.put("menuIds", menuIds); @@ -189,29 +189,19 @@ public class GenTableServiceImpl implements IGenTableService { table.put("className", className); table.put("classname", classname); table.put("functionName", schema.getName()); - + // 添加表类型属性(默认为crud类型) table.put("crud", true); table.put("sub", false); table.put("tree", false); - - // 添加isSuperColumn方法 - table.put("isSuperColumn", new Object() { - public boolean isSuperColumn(String javaField) { - // 定义超类字段(BaseEntity中的字段) - return "createBy".equals(javaField) || "createTime".equals(javaField) - || "updateBy".equals(javaField) || "updateTime".equals(javaField) - || "remark".equals(javaField) || "tenantId".equals(javaField); - } - }); - + context.put("table", table); - + // 处理字段信息 List> columns = new ArrayList<>(); Map pkColumn = null; Set importList = new HashSet<>(); - + // 添加基础导入 importList.add("java.io.Serializable"); @@ -219,7 +209,7 @@ public class GenTableServiceImpl implements IGenTableService { Map column = new HashMap<>(); String javaType = getJavaType(field.getType()); String javaField = StrUtil.toCamelCase(field.getCode()); - + column.put("columnName", field.getCode()); column.put("columnComment", field.getName()); column.put("comment", field.getName()); // 添加comment别名 @@ -227,7 +217,7 @@ public class GenTableServiceImpl implements IGenTableService { column.put("javaType", javaType); column.put("javaField", javaField); column.put("capJavaField", toCamelCase(field.getCode(), true)); - + // 布尔值属性(兼容两种格式) boolean isPk = "1".equals(field.getIsPk()); boolean isRequired = "1".equals(field.getIsRequired()); @@ -235,7 +225,7 @@ public class GenTableServiceImpl implements IGenTableService { boolean isEdit = "1".equals(field.getIsEdit()); boolean isList = "1".equals(field.getIsList()); boolean isQuery = "1".equals(field.getIsQuery()); - + column.put("isPk", isPk ? 1 : 0); column.put("pk", isPk); // 添加pk别名 column.put("isRequired", isRequired); @@ -248,27 +238,27 @@ public class GenTableServiceImpl implements IGenTableService { column.put("list", isList); // 添加list别名 column.put("isQuery", isQuery); column.put("query", isQuery); // 添加query别名 - + column.put("queryType", field.getQueryType()); column.put("htmlType", field.getHtmlType()); column.put("dictType", field.getDictType()); column.put("sort", field.getSort()); - + // 添加readConverterExp方法 column.put("readConverterExp", new Object() { }); - + // 根据Java类型添加相应的导入 addImportForJavaType(javaType, importList); - + columns.add(column); - + // 设置主键列 if (isPk) { pkColumn = column; } } - + // 如果没有主键,使用第一个字段作为主键 if (pkColumn == null && !columns.isEmpty()) { pkColumn = columns.get(0); @@ -276,27 +266,28 @@ public class GenTableServiceImpl implements IGenTableService { pkColumn.put("isPk", 1); pkColumn.put("pk", true); } - + context.put("columns", columns); context.put("pkColumn", pkColumn); context.put("importList", new ArrayList<>(importList)); - + return context; } - + /** - * 根据Java类型添加相应的导入 - */ - private void addImportForJavaType(String javaType, Set importList) { - switch (javaType) { - case "BigDecimal" -> importList.add("java.math.BigDecimal"); - case "Date" -> importList.add("java.util.Date"); - case "LocalDateTime" -> importList.add("java.time.LocalDateTime"); - case "LocalDate" -> importList.add("java.time.LocalDate"); - case "LocalTime" -> importList.add("java.time.LocalTime"); - default -> {} - } - } + * 根据Java类型添加相应的导入 + */ + private void addImportForJavaType(String javaType, Set importList) { + switch (javaType) { + case "BigDecimal" -> importList.add("java.math.BigDecimal"); + case "Date" -> importList.add("java.util.Date"); + case "LocalDateTime" -> importList.add("java.time.LocalDateTime"); + case "LocalDate" -> importList.add("java.time.LocalDate"); + case "LocalTime" -> importList.add("java.time.LocalTime"); + default -> { + } + } + } /** * 从包名中提取模块名 @@ -320,10 +311,10 @@ public class GenTableServiceImpl implements IGenTableService { String packageName = GenConfig.getPackageName(); String tablePrefix = GenConfig.getTablePrefix(); boolean autoRemovePre = GenConfig.getAutoRemovePre(); - + // 处理类名 String baseClassName = schema.getTableName(); - + // 自动去除表前缀 if (autoRemovePre && StrUtil.isNotBlank(tablePrefix)) { String[] prefixes = tablePrefix.split(","); @@ -334,13 +325,13 @@ public class GenTableServiceImpl implements IGenTableService { } } } - + String className = toCamelCase(baseClassName, true); // 首字母大写,如:SysRole // 首字母小写,如:sysRole String moduleName = getModuleName(packageName); String javaPath = "src/main/java/"; String mybatisPath = "src/main/resources/mapper/"; - + if (template.contains("domain.java.vm")) { return javaPath + packageName.replace(".", "/") + "/domain/" + className + ".java"; } else if (template.contains("mapper.java.vm")) { @@ -413,16 +404,17 @@ public class GenTableServiceImpl implements IGenTableService { return "String"; } String type = dbType.toLowerCase(); - if (type.contains("int") || type.contains("tinyint") || type.contains("smallint")) { + if (StrUtil.equalsAny(type, "int", "tinyint")) { return "Integer"; - } else if (type.contains("bigint")) { + } else if (StrUtil.equalsAny(type, "bigint")) { return "Long"; - } else if (type.contains("decimal") || type.contains("numeric") || type.contains("float") || type.contains( - "double")) { + } else if (StrUtil.equalsAny(type, "decimal", "numeric", "float", "double")) { return "BigDecimal"; - } else if (type.contains("date") || type.contains("time")) { - return "Date"; - } else if (type.contains("bit") || type.contains("boolean")) { + } else if (StrUtil.equalsAny(type, "date")) { + return "LocalDate"; + } else if (StrUtil.equalsAny(type, "datetime", "timestamp")) { + return "LocalDateTime"; + } else if (StrUtil.equalsAny(type, "bit", "boolean")) { return "Boolean"; } else { return "String"; diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaFieldServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaFieldServiceImpl.java index e8295f88..a0e40f95 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaFieldServiceImpl.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaFieldServiceImpl.java @@ -211,7 +211,6 @@ public class SchemaFieldServiceImpl implements SchemaFieldService { result.put("tableName", schema.getTableName()); result.put("tableComment", schema.getComment()); result.put("className", toCamelCase(schema.getTableName(), true)); - // result.put("className", StrUtil.toCamelCase(schema.getTableName())); result.put("tableCamelName", StrUtil.toCamelCase(schema.getTableName())); result.put("functionName", schema.getName()); result.put("schemaName", schema.getName()); @@ -363,16 +362,15 @@ public class SchemaFieldServiceImpl implements SchemaFieldService { } String type = dbType.toLowerCase(); - if (type.contains("int") || type.contains("tinyint") || type.contains("smallint")) { + if (StrUtil.equalsAny(type, "int", "tinyint", "smallint")) { return "Integer"; - } else if (type.contains("bigint")) { + } else if (StrUtil.equalsAny(type, "bigint")) { return "Long"; - } else if (type.contains("decimal") || type.contains("numeric") || type.contains("float") || type.contains( - "double")) { + } else if (StrUtil.equalsAny(type, "decimal", "numeric", "float", "double")) { return "BigDecimal"; - } else if (type.contains("date") || type.contains("time")) { + } else if (StrUtil.equalsAny(type, "date", "datetime","timestamp")) { return "Date"; - } else if (type.contains("bit") || type.contains("boolean")) { + } else if (StrUtil.equalsAny(type, "bit", "boolean")) { return "Boolean"; } else { return "String"; diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/bo.java.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/bo.java.vm index 65835a77..491f8bc0 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/bo.java.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/bo.java.vm @@ -25,11 +25,11 @@ import org.ruoyi.common.core.validate.EditGroup; public class ${ClassName}Bo implements Serializable { #foreach ($column in $columns) - #if(!$table.isSuperColumn($column.javaField) && ($column.isPk || $column.query || $column.insert || $column.edit)) + #if($column.isPk || $column.query || $column.insert || $column.edit|| $column.required) /** * $column.columnComment */ - #if($column.insert && $column.edit) + #if(($column.insert && $column.edit) || $column.required) #set($Group="AddGroup.class, EditGroup.class") #elseif($column.insert) #set($Group="AddGroup.class") diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/domain.java.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/domain.java.vm index d9aa46a7..d32dff79 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/domain.java.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/domain.java.vm @@ -1,22 +1,10 @@ package ${packageName}.domain; - #foreach ($column in $columns) - #if($column.javaField=='tenantId') - #set($IsTenant=1) - #end - #end - #if($IsTenant==1) - import core.tenant.common.org.ruoyi.TenantEntity; - #else - #end import com.baomidou.mybatisplus.annotation.*; import lombok.Data; -import lombok.EqualsAndHashCode; - #foreach ($import in $importList) - import ${import}; - #end - -import org.ruoyi.core.domain.BaseEntity; +#foreach ($import in $importList) +import ${import}; +#end /** * ${functionName}对象 ${tableName} @@ -24,15 +12,9 @@ import org.ruoyi.core.domain.BaseEntity; * @author ${author} * @date ${datetime} */ -#if($IsTenant==1) - #set($Entity="TenantEntity") -#else - #set($Entity="BaseEntity") -#end @Data -@EqualsAndHashCode(callSuper = true) @TableName("${tableName}") -public class ${ClassName} extends ${Entity} { +public class ${ClassName} implements Serializable { #foreach ($column in $columns) From a6eb98daab76eeac8b9bfb9b4cad183b959eca91 Mon Sep 17 00:00:00 2001 From: l90215 Date: Mon, 11 Aug 2025 21:59:02 +0800 Subject: [PATCH 05/36] =?UTF-8?q?feat:=20fix=E4=BB=A3=E7=A0=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=B1=BB=E5=9E=8B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knowledge/KnowledgeController.java | 20 +- .../controller/SchemaGroupController.java | 1 + .../org/ruoyi/generator/domain/Schema.java | 76 +++---- .../ruoyi/generator/domain/SchemaField.java | 51 +++-- .../ruoyi/generator/domain/SchemaGroup.java | 51 +++-- .../ruoyi/generator/domain/bo/SchemaBo.java | 49 +---- .../generator/domain/bo/SchemaFieldBo.java | 19 +- .../generator/domain/bo/SchemaGroupBo.java | 17 +- .../generator/domain/vo/SchemaFieldVo.java | 22 -- .../generator/domain/vo/SchemaGroupVo.java | 9 - .../ruoyi/generator/domain/vo/SchemaVo.java | 3 - .../generator/impl/GenTableServiceImpl.java | 7 +- .../impl/SchemaFieldServiceImpl.java | 17 +- .../impl/SchemaGroupServiceImpl.java | 8 +- .../generator/impl/SchemaServiceImpl.java | 34 ++-- .../src/main/resources/vm/java/bo.java.vm | 38 ++-- script/sql/ruoyi-ai.sql | 188 ++++++------------ 17 files changed, 235 insertions(+), 375 deletions(-) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java index 54ef52e9..76443156 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java @@ -27,10 +27,18 @@ import org.ruoyi.service.IKnowledgeAttachService; import org.ruoyi.service.IKnowledgeFragmentService; import org.ruoyi.service.IKnowledgeInfoService; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.util.List; +import java.util.Objects; /** * 知识库管理 @@ -60,7 +68,9 @@ public class KnowledgeController extends BaseController { if (!StpUtil.isLogin()) { throw new SecurityException("请先去登录!"); } - bo.setUid(LoginHelper.getUserId()); + if (!Objects.equals(LoginHelper.getUserId(), 1L)) { + bo.setUid(LoginHelper.getUserId()); + } return knowledgeInfoService.queryPageList(bo, pageQuery); } @@ -72,13 +82,15 @@ public class KnowledgeController extends BaseController { if (!StpUtil.isLogin()) { throw new SecurityException("请先去登录!"); } - LoginUser loginUser = LoginHelper.getLoginUser(); // 管理员跳过权限 - if (loginUser.getUserId().equals(1L) || !knowledgeRoleConfig.getEnable()) { + if (Objects.equals(LoginHelper.getUserId(), 1L)) { + return knowledgeInfoService.queryPageList(bo, pageQuery); + } else if (!knowledgeRoleConfig.getEnable()) { bo.setUid(LoginHelper.getUserId()); return knowledgeInfoService.queryPageList(bo, pageQuery); } else { + // TODO 自己创建的知识库+角色分配的知识库 return knowledgeInfoService.queryPageListByRole(pageQuery); } } diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/controller/SchemaGroupController.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/controller/SchemaGroupController.java index d19fe54c..d930c71a 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/controller/SchemaGroupController.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/controller/SchemaGroupController.java @@ -53,6 +53,7 @@ public class SchemaGroupController extends BaseController { /** * 获取数据模型分组选择列表 */ + @SaCheckPermission("dev:schemaGroup:select") @GetMapping("/select") public R> select() { SchemaGroupBo bo = new SchemaGroupBo(); diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/Schema.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/Schema.java index f12fb823..c1c26870 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/Schema.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/Schema.java @@ -1,13 +1,14 @@ package org.ruoyi.generator.domain; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import lombok.EqualsAndHashCode; -import org.ruoyi.core.domain.BaseEntity; -import java.io.Serial; +import java.io.Serializable; +import java.util.Date; /** * 数据模型对象 dev_schema @@ -16,12 +17,9 @@ import java.io.Serial; * @date 2024-01-01 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("dev_schema") -public class Schema extends BaseEntity { +public class Schema implements Serializable { - @Serial - private static final long serialVersionUID = 1L; /** * 主键 @@ -49,41 +47,6 @@ public class Schema extends BaseEntity { */ private String tableName; - /** - * 表注释 - */ - private String comment; - - /** - * 存储引擎 - */ - private String engine; - - /** - * 列表字段 - */ - private String listKeys; - - /** - * 搜索表单字段 - */ - private String searchFormKeys; - - /** - * 表单设计 - */ - private String designer; - - /** - * 状态 - */ - private String status; - - /** - * 排序 - */ - private Integer sort; - /** * 备注 */ @@ -96,8 +59,33 @@ public class Schema extends BaseEntity { private String delFlag; /** - * 租户编号 + * 创建部门 */ - private String tenantId; + @TableField(fill = FieldFill.INSERT) + private Long createDept; + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private Long createBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long updateBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; } \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/SchemaField.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/SchemaField.java index c683a92a..f02f548d 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/SchemaField.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/SchemaField.java @@ -1,13 +1,14 @@ package org.ruoyi.generator.domain; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import lombok.EqualsAndHashCode; -import org.ruoyi.core.domain.BaseEntity; -import java.io.Serial; +import java.io.Serializable; +import java.util.Date; /** * 数据模型字段对象 dev_schema_field @@ -16,12 +17,8 @@ import java.io.Serial; * @date 2024-01-01 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("dev_schema_field") -public class SchemaField extends BaseEntity { - - @Serial - private static final long serialVersionUID = 1L; +public class SchemaField implements Serializable { /** * 主键 @@ -129,15 +126,6 @@ public class SchemaField extends BaseEntity { */ private String dictType; - /** - * 状态 - */ - private String status; - - /** - * 扩展JSON - */ - private String extendJson; /** * 备注 @@ -151,8 +139,33 @@ public class SchemaField extends BaseEntity { private String delFlag; /** - * 租户编号 + * 创建部门 */ - private String tenantId; + @TableField(fill = FieldFill.INSERT) + private Long createDept; + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private Long createBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long updateBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; } \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/SchemaGroup.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/SchemaGroup.java index c921e285..bf3ee605 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/SchemaGroup.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/SchemaGroup.java @@ -1,13 +1,14 @@ package org.ruoyi.generator.domain; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import lombok.EqualsAndHashCode; -import org.ruoyi.core.domain.BaseEntity; -import java.io.Serial; +import java.io.Serializable; +import java.util.Date; /** * 数据模型分组对象 dev_schema_group @@ -16,12 +17,8 @@ import java.io.Serial; * @date 2024-01-01 */ @Data -@EqualsAndHashCode(callSuper = true) @TableName("dev_schema_group") -public class SchemaGroup extends BaseEntity { - - @Serial - private static final long serialVersionUID = 1L; +public class SchemaGroup implements Serializable { /** * 主键 @@ -44,16 +41,6 @@ public class SchemaGroup extends BaseEntity { */ private String icon; - /** - * 排序 - */ - private Integer sort; - - /** - * 状态 - */ - private String status; - /** * 备注 */ @@ -66,8 +53,32 @@ public class SchemaGroup extends BaseEntity { private String delFlag; /** - * 租户编号 + * 创建部门 */ - private String tenantId; + @TableField(fill = FieldFill.INSERT) + private Long createDept; + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private Long createBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Long updateBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; } \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaBo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaBo.java index cb5ee424..4c91a52a 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaBo.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaBo.java @@ -4,12 +4,12 @@ import io.github.linpeilie.annotations.AutoMapper; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; -import lombok.EqualsAndHashCode; import org.ruoyi.common.core.validate.AddGroup; import org.ruoyi.common.core.validate.EditGroup; -import org.ruoyi.core.domain.BaseEntity; import org.ruoyi.generator.domain.Schema; +import java.io.Serializable; + /** * 数据模型业务对象 SchemaBo * @@ -17,9 +17,8 @@ import org.ruoyi.generator.domain.Schema; * @date 2024-01-01 */ @Data -@EqualsAndHashCode(callSuper = true) @AutoMapper(target = Schema.class, reverseConvertGenerate = false) -public class SchemaBo extends BaseEntity { +public class SchemaBo implements Serializable { /** * 主键 @@ -38,52 +37,12 @@ public class SchemaBo extends BaseEntity { @NotBlank(message = "模型名称不能为空", groups = {AddGroup.class, EditGroup.class}) private String name; - /** - * 模型编码 - */ - @NotBlank(message = "模型编码不能为空", groups = {AddGroup.class, EditGroup.class}) - private String code; - /** * 表名 */ + @NotBlank(message = "表名不能为空", groups = {AddGroup.class, EditGroup.class}) private String tableName; - /** - * 表注释 - */ - private String comment; - - /** - * 存储引擎 - */ - private String engine; - - /** - * 列表字段 - */ - private String listKeys; - - /** - * 搜索表单字段 - */ - private String searchFormKeys; - - /** - * 表单设计 - */ - private String designer; - - /** - * 状态 - */ - private String status; - - /** - * 排序 - */ - private Integer sort; - /** * 备注 */ diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaFieldBo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaFieldBo.java index 491d8d2d..161c60c0 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaFieldBo.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaFieldBo.java @@ -4,12 +4,12 @@ import io.github.linpeilie.annotations.AutoMapper; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; -import lombok.EqualsAndHashCode; import org.ruoyi.common.core.validate.AddGroup; import org.ruoyi.common.core.validate.EditGroup; -import org.ruoyi.core.domain.BaseEntity; import org.ruoyi.generator.domain.SchemaField; +import java.io.Serializable; + /** * 数据模型字段业务对象 SchemaFieldBo * @@ -17,9 +17,8 @@ import org.ruoyi.generator.domain.SchemaField; * @date 2024-01-01 */ @Data -@EqualsAndHashCode(callSuper = true) @AutoMapper(target = SchemaField.class, reverseConvertGenerate = false) -public class SchemaFieldBo extends BaseEntity { +public class SchemaFieldBo implements Serializable { /** * 主键 @@ -36,7 +35,7 @@ public class SchemaFieldBo extends BaseEntity { /** * 模型名称 */ - @NotNull(message = "模型名称不能为空", groups = {AddGroup.class, EditGroup.class}) + // @NotNull(message = "模型名称不能为空", groups = {AddGroup.class, EditGroup.class}) private String schemaName; /** @@ -131,16 +130,6 @@ public class SchemaFieldBo extends BaseEntity { */ private String dictType; - /** - * 状态 - */ - private String status; - - /** - * 扩展JSON - */ - private String extendJson; - /** * 备注 */ diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaGroupBo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaGroupBo.java index 21c49d26..9a1f7057 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaGroupBo.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/bo/SchemaGroupBo.java @@ -4,12 +4,12 @@ import io.github.linpeilie.annotations.AutoMapper; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; -import lombok.EqualsAndHashCode; import org.ruoyi.common.core.validate.AddGroup; import org.ruoyi.common.core.validate.EditGroup; -import org.ruoyi.core.domain.BaseEntity; import org.ruoyi.generator.domain.SchemaGroup; +import java.io.Serializable; + /** * 数据模型分组业务对象 SchemaGroupBo * @@ -17,9 +17,8 @@ import org.ruoyi.generator.domain.SchemaGroup; * @date 2024-01-01 */ @Data -@EqualsAndHashCode(callSuper = true) @AutoMapper(target = SchemaGroup.class, reverseConvertGenerate = false) -public class SchemaGroupBo extends BaseEntity { +public class SchemaGroupBo implements Serializable { /** * 主键 @@ -44,16 +43,6 @@ public class SchemaGroupBo extends BaseEntity { */ private String icon; - /** - * 排序 - */ - private Integer sort; - - /** - * 状态 - */ - private String status; - /** * 备注 */ diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaFieldVo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaFieldVo.java index 7897d87b..dd672320 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaFieldVo.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaFieldVo.java @@ -6,9 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.ruoyi.generator.domain.SchemaField; -import java.io.Serial; import java.io.Serializable; -import java.util.Date; /** * 数据模型字段视图对象 SchemaFieldVo @@ -20,9 +18,6 @@ import java.util.Date; @AutoMapper(target = SchemaField.class) public class SchemaFieldVo implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - /** * 主键 */ @@ -136,25 +131,8 @@ public class SchemaFieldVo implements Serializable { @Schema(description = "字典类型") private String dictType; - /** - * 状态 - */ - @Schema(description = "状态") - private String status; - - /** - * 扩展JSON - */ - private String extendJson; - /** * 备注 */ private String remark; - - /** - * 创建时间 - */ - private Date createTime; - } \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaGroupVo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaGroupVo.java index 76741e45..66780b5e 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaGroupVo.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaGroupVo.java @@ -4,7 +4,6 @@ import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; import org.ruoyi.generator.domain.SchemaGroup; -import java.io.Serial; import java.io.Serializable; import java.util.Date; @@ -18,9 +17,6 @@ import java.util.Date; @AutoMapper(target = SchemaGroup.class) public class SchemaGroupVo implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - /** * 主键 */ @@ -46,11 +42,6 @@ public class SchemaGroupVo implements Serializable { */ private Integer sort; - /** - * 状态 - */ - private String status; - /** * 备注 */ diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaVo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaVo.java index c2dfd90a..378017bd 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaVo.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/domain/vo/SchemaVo.java @@ -19,9 +19,6 @@ import java.util.Date; @AutoMapper(target = Schema.class) public class SchemaVo implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - /** * 主键 */ diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java index bc885cb9..6ecbfd15 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/GenTableServiceImpl.java @@ -11,9 +11,11 @@ import org.apache.velocity.app.Velocity; import org.ruoyi.common.core.constant.Constants; import org.ruoyi.generator.config.GenConfig; import org.ruoyi.generator.domain.vo.SchemaFieldVo; +import org.ruoyi.generator.domain.vo.SchemaGroupVo; import org.ruoyi.generator.domain.vo.SchemaVo; import org.ruoyi.generator.service.IGenTableService; import org.ruoyi.generator.service.SchemaFieldService; +import org.ruoyi.generator.service.SchemaGroupService; import org.ruoyi.generator.service.SchemaService; import org.ruoyi.generator.util.VelocityInitializer; import org.ruoyi.generator.util.VelocityUtils; @@ -44,6 +46,7 @@ public class GenTableServiceImpl implements IGenTableService { private final SchemaService schemaService; private final SchemaFieldService schemaFieldService; + private final SchemaGroupService schemaGroupService; /** * 基于表名称批量生成代码到classpath路径 @@ -137,6 +140,8 @@ public class GenTableServiceImpl implements IGenTableService { boolean autoRemovePre = GenConfig.getAutoRemovePre(); // 处理表名和类名 + Long schemaGroupId = schema.getSchemaGroupId(); + SchemaGroupVo schemaGroupVo = schemaGroupService.queryById(schemaGroupId); String tableName = schema.getTableName(); String baseClassName = schema.getTableName(); @@ -154,7 +159,7 @@ public class GenTableServiceImpl implements IGenTableService { String className = toCamelCase(baseClassName, true); // 首字母大写的类名,如:SysRole String classname = toCamelCase(baseClassName, false); // 首字母小写的类名,如:sysRole String businessName = toCamelCase(baseClassName, false); - String moduleName = getModuleName(packageName); + String moduleName = schemaGroupVo.getCode(); // 基本信息 context.put("tableName", tableName); diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaFieldServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaFieldServiceImpl.java index a0e40f95..3240c366 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaFieldServiceImpl.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaFieldServiceImpl.java @@ -79,7 +79,6 @@ public class SchemaFieldServiceImpl implements SchemaFieldService { lqw.eq(StringUtils.isNotBlank(bo.getQueryType()), SchemaField::getQueryType, bo.getQueryType()); lqw.eq(StringUtils.isNotBlank(bo.getHtmlType()), SchemaField::getHtmlType, bo.getHtmlType()); lqw.like(StringUtils.isNotBlank(bo.getDictType()), SchemaField::getDictType, bo.getDictType()); - lqw.eq(StringUtils.isNotBlank(bo.getStatus()), SchemaField::getStatus, bo.getStatus()); lqw.orderByAsc(SchemaField::getSort); return lqw; } @@ -150,7 +149,6 @@ public class SchemaFieldServiceImpl implements SchemaFieldService { public List queryListBySchemaId(Long schemaId) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(SchemaField::getSchemaId, schemaId); - lqw.eq(SchemaField::getStatus, "0"); // 只查询正常状态的字段 lqw.orderByAsc(SchemaField::getSort); return baseMapper.selectVoList(lqw); } @@ -264,8 +262,7 @@ public class SchemaFieldServiceImpl implements SchemaFieldService { return false; } LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.eq(SchemaField::getSchemaName, tableName); - lqw.eq(SchemaField::getStatus, "0"); + lqw.eq(SchemaField::getSchemaId, schemaId); // 检查是否已存在字段数据 List existingFields = baseMapper.selectVoList(lqw); if (CollUtil.isNotEmpty(existingFields)) { @@ -279,20 +276,26 @@ public class SchemaFieldServiceImpl implements SchemaFieldService { SchemaField field = new SchemaField(); field.setSchemaId(schemaId); field.setSchemaName(tableName); + field.setDefaultValue((String) columnInfo.get("columnDefault")); + field.setComment((String) columnInfo.get("columnComment")); field.setName((String) columnInfo.get("columnComment")); field.setCode(StrUtil.toCamelCase((String) columnInfo.get("columnName"))); field.setType((String) columnInfo.get("dataType")); field.setLength(Integer.valueOf(String.valueOf(columnInfo.get("columnSize")))); field.setIsPk((Boolean) columnInfo.get("isPrimaryKey") ? "1" : "0"); field.setIsRequired(!(Boolean) columnInfo.get("isNullable") ? "1" : "0"); - field.setIsInsert("1"); - field.setIsEdit("1"); + if ("1".equals(field.getIsPk())) { + field.setIsInsert("0"); + field.setIsEdit("0"); + }else { + field.setIsInsert("1"); + field.setIsEdit("1"); + } field.setIsList("1"); field.setIsQuery("1"); field.setQueryType("EQ"); field.setHtmlType(getDefaultHtmlType((String) columnInfo.get("dataType"))); field.setSort(sort++); - field.setStatus("0"); // 如果字段名为空,使用字段代码作为名称 if (StringUtils.isBlank(field.getName())) { field.setName(field.getCode()); diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaGroupServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaGroupServiceImpl.java index cf2be19b..809c364d 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaGroupServiceImpl.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaGroupServiceImpl.java @@ -60,9 +60,6 @@ public class SchemaGroupServiceImpl implements SchemaGroupService { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.like(StringUtils.isNotBlank(bo.getName()), SchemaGroup::getName, bo.getName()); lqw.eq(StringUtils.isNotBlank(bo.getCode()), SchemaGroup::getCode, bo.getCode()); - lqw.eq(bo.getSort() != null, SchemaGroup::getSort, bo.getSort()); - lqw.eq(StringUtils.isNotBlank(bo.getStatus()), SchemaGroup::getStatus, bo.getStatus()); - lqw.orderByAsc(SchemaGroup::getSort); return lqw; } @@ -102,9 +99,6 @@ public class SchemaGroupServiceImpl implements SchemaGroupService { */ @Override public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { - if (isValid) { - //TODO 做一些业务上的校验,判断是否需要校验 - } - return baseMapper.deleteBatchIds(ids) > 0; + return baseMapper.deleteByIds(ids) > 0; } } \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaServiceImpl.java index 9e9906fa..4b552dd8 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaServiceImpl.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/impl/SchemaServiceImpl.java @@ -8,7 +8,10 @@ import org.ruoyi.common.core.utils.MapstructUtils; import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.core.page.PageQuery; import org.ruoyi.core.page.TableDataInfo; +import org.ruoyi.generator.domain.SchemaGroup; +import org.ruoyi.generator.domain.vo.SchemaGroupVo; import org.ruoyi.generator.event.SchemaDeletedEvent; +import org.ruoyi.generator.service.SchemaGroupService; import org.ruoyi.generator.service.SchemaService; import org.ruoyi.generator.domain.Schema; import org.ruoyi.generator.domain.bo.SchemaBo; @@ -20,6 +23,8 @@ import org.springframework.stereotype.Service; import java.util.Collection; import java.util.List; +import java.util.Objects; +import java.util.Optional; /** * 数据模型Service业务层处理 @@ -31,6 +36,7 @@ import java.util.List; public class SchemaServiceImpl implements SchemaService { private final SchemaMapper baseMapper; + private final SchemaGroupService schemaGroupService; private final ApplicationEventPublisher eventPublisher; /** @@ -64,10 +70,7 @@ public class SchemaServiceImpl implements SchemaService { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(bo.getSchemaGroupId() != null, Schema::getSchemaGroupId, bo.getSchemaGroupId()); lqw.like(StringUtils.isNotBlank(bo.getName()), Schema::getName, bo.getName()); - lqw.eq(StringUtils.isNotBlank(bo.getCode()), Schema::getCode, bo.getCode()); lqw.eq(StringUtils.isNotBlank(bo.getTableName()), Schema::getTableName, bo.getTableName()); - lqw.eq(StringUtils.isNotBlank(bo.getStatus()), Schema::getStatus, bo.getStatus()); - lqw.orderByAsc(Schema::getSort); return lqw; } @@ -76,15 +79,18 @@ public class SchemaServiceImpl implements SchemaService { */ @Override public Boolean insertByBo(SchemaBo bo) { - Schema add = MapstructUtils.convert(bo, Schema.class); - validEntityBeforeSave(add); - boolean flag = baseMapper.insert(add) > 0; + Schema schema = MapstructUtils.convert(bo, Schema.class); + Long schemaGroupId = bo.getSchemaGroupId(); + SchemaGroupVo schemaGroupVo = schemaGroupService.queryById(schemaGroupId); + if (Objects.nonNull(schemaGroupVo)) { + schema.setCode(schemaGroupVo.getCode()); + } + boolean flag = baseMapper.insert(schema) > 0; if (flag) { - bo.setId(add.getId()); - + bo.setId(schema.getId()); // 发布数据模型添加事件,由事件监听器处理字段插入 if (StringUtils.isNotBlank(bo.getTableName())) { - eventPublisher.publishEvent(new SchemaAddedEvent(this, add.getId(), bo.getTableName())); + eventPublisher.publishEvent(new SchemaAddedEvent(this, schema.getId(), bo.getTableName())); } } return flag; @@ -96,17 +102,9 @@ public class SchemaServiceImpl implements SchemaService { @Override public Boolean updateByBo(SchemaBo bo) { Schema update = MapstructUtils.convert(bo, Schema.class); - validEntityBeforeSave(update); return baseMapper.updateById(update) > 0; } - /** - * 保存前的数据校验 - */ - private void validEntityBeforeSave(Schema entity) { - //TODO 做一些数据校验,如唯一约束 - } - /** * 批量删除数据模型 */ @@ -127,8 +125,6 @@ public class SchemaServiceImpl implements SchemaService { public SchemaVo queryByTableName(String tableName) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(Schema::getTableName, tableName); - // 只查询正常状态的模型 - lqw.eq(Schema::getStatus, "0"); return baseMapper.selectVoOne(lqw); } } \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/bo.java.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/bo.java.vm index 491f8bc0..939c1b2e 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/bo.java.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/bo.java.vm @@ -25,26 +25,28 @@ import org.ruoyi.common.core.validate.EditGroup; public class ${ClassName}Bo implements Serializable { #foreach ($column in $columns) - #if($column.isPk || $column.query || $column.insert || $column.edit|| $column.required) - /** - * $column.columnComment - */ - #if(($column.insert && $column.edit) || $column.required) - #set($Group="AddGroup.class, EditGroup.class") - #elseif($column.insert) - #set($Group="AddGroup.class") - #elseif($column.edit) - #set($Group="EditGroup.class") - #end - #if($column.required) - #if($column.javaType == 'String') - @NotBlank(message = "$column.columnComment不能为空", groups = { $Group }) - #else - @NotNull(message = "$column.columnComment不能为空", groups = { $Group }) - #end - #end + #if($column.isPk) private $column.javaType $column.javaField; + #elseif($column.insert || $column.edit) + /** + * $column.columnComment + */ + #if($column.insert && $column.edit) + #set($Group="AddGroup.class, EditGroup.class") + #elseif($column.insert) + #set($Group="AddGroup.class") + #elseif($column.edit) + #set($Group="EditGroup.class") + #end + #if($column.required) + #if($column.javaType == 'String') + @NotBlank(message = "$column.columnComment不能为空", groups = { $Group }) + #else + @NotNull(message = "$column.columnComment不能为空", groups = { $Group }) + #end + #end + private $column.javaType $column.javaField; #end #end diff --git a/script/sql/ruoyi-ai.sql b/script/sql/ruoyi-ai.sql index 47892e9d..2c824c8f 100644 --- a/script/sql/ruoyi-ai.sql +++ b/script/sql/ruoyi-ai.sql @@ -2496,11 +2496,56 @@ CREATE TABLE prompt_template ROW_FORMAT = Dynamic; +DROP TABLE IF EXISTS `dev_schema_group`; +create table dev_schema_group +( + id bigint auto_increment comment '主键' primary key, + name varchar(100) null comment '分组名称', + code varchar(100) null comment '分组编码', + icon varchar(100) null comment '图标', + remark varchar(500) null comment '备注', + del_flag char default '0' null comment '删除标志(0代表存在 2代表删除)', + create_dept bigint null comment '创建部门', + create_by bigint null comment '创建者', + create_time datetime null comment '创建时间', + update_by bigint null comment '更新者', + update_time datetime null comment '更新时间' +) ENGINE = InnoDB + CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT = '数据模型分组表'; + +INSERT INTO dev_schema_group (id, name, code, icon, remark, del_flag, create_dept, create_by, create_time, update_by, update_time) VALUES +(1944240213530648567, '系统管理', 'system', 'eos-icons:system-group', '系统默认分组', '0', null, null, '2025-07-13 11:37:28', 1, '2025-07-13 18:42:48'); +INSERT INTO dev_schema_group (id, name, code, icon, remark, del_flag, create_dept, create_by, create_time, update_by, update_time) VALUES +(1944240213530648577, '运营管理', 'operator', 'icon-park-outline:appointment', '运营管理', '0', null, null, '2025-07-13 11:39:24', 1, '2025-07-13 18:42:31'); +INSERT INTO dev_schema_group (id, name, code, icon, remark, del_flag, create_dept, create_by, create_time, update_by, update_time) VALUES +(1944346023254429697, '在线开发', 'dev', 'carbon:development', '在线开发', '0', null, null, '2025-07-13 18:39:51', 1, '2025-07-13 18:42:07'); + + + +DROP TABLE IF EXISTS `dev_schema`; +create table dev_schema +( + id bigint auto_increment comment '主键' primary key, + schema_group_id bigint null comment '分组ID', + name varchar(100) null comment '模型名称', + code varchar(100) null comment '模型编码', + table_name varchar(100) null comment '表名', + remark varchar(500) null comment '备注', + del_flag char default '0' null comment '删除标志(0代表存在 2代表删除)', + create_dept bigint null comment '创建部门', + create_by bigint null comment '创建者', + create_time datetime null comment '创建时间', + update_by bigint null comment '更新者', + update_time datetime null comment '更新时间' +) ENGINE = InnoDB + CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT = '数据模型表'; + DROP TABLE IF EXISTS `dev_schema_field`; create table dev_schema_field ( - id bigint auto_increment comment '主键' - primary key, + id bigint auto_increment comment '主键' primary key, schema_id bigint null comment '模型ID', schema_name varchar(64) null comment '模型名称', name varchar(100) null comment '字段名称', @@ -2513,17 +2558,6 @@ create table dev_schema_field default_value varchar(200) null comment '默认值', length int null comment '字段长度', scale int null comment '小数位数', - sort int null comment '排序', - status char default '0' null comment '状态(0正常 1停用)', - extend_json text null comment '扩展配置', - remark varchar(500) null comment '备注', - del_flag char default '0' null comment '删除标志(0代表存在 2代表删除)', - tenant_id varchar(20) default '000000' null comment '租户编号', - create_dept bigint null comment '创建部门', - create_by bigint null comment '创建者', - create_time datetime null comment '创建时间', - update_by bigint null comment '更新者', - update_time datetime null comment '更新时间', is_list char default '1' null comment '是否列表显示(0否 1是)', is_query char default '1' null comment '是否查询字段(0否 1是)', is_insert char default '1' null comment '是否插入字段(0否 1是)', @@ -2531,125 +2565,23 @@ create table dev_schema_field query_type varchar(200) default null comment '查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围)', html_type varchar(200) default 'input' null comment '显示类型(input输入框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传、upload文件上传、editor富文本编辑器)', dict_type varchar(200) default '' null comment '字典类型', - constraint fk_schema_field_schema - foreign key (schema_id) references dev_schema (id) - on delete cascade -) comment '数据模型字段表'; - -create index idx_html_type - on dev_schema_field (html_type); - -create index idx_is_list - on dev_schema_field (is_list); - -create index idx_is_query - on dev_schema_field (is_query); - -create index idx_query_type - on dev_schema_field (query_type); - -create index idx_schema_field_code - on dev_schema_field (code); - -create index idx_schema_field_schema_id - on dev_schema_field (schema_id); - -create index idx_schema_field_status - on dev_schema_field (status); - -create index idx_schema_field_tenant - on dev_schema_field (tenant_id); - - -DROP TABLE IF EXISTS `dev_schema`; -create table dev_schema -( - id bigint auto_increment comment '主键' - primary key, - schema_group_id bigint null comment '分组ID', - name varchar(100) null comment '模型名称', - code varchar(100) null comment '模型编码', - table_name varchar(100) null comment '表名', - comment varchar(500) null comment '表注释', - engine varchar(50) default 'InnoDB' null comment '存储引擎', - list_keys text null comment '列表字段', - search_form_keys text null comment '搜索表单字段', - designer longtext null comment '表单设计', - status char default '0' null comment '状态(0正常 1停用)', - sort int null comment '排序', - remark varchar(500) null comment '备注', - del_flag char default '0' null comment '删除标志(0代表存在 2代表删除)', - tenant_id varchar(20) default '000000' null comment '租户编号', - create_dept bigint null comment '创建部门', - create_by bigint null comment '创建者', - create_time datetime null comment '创建时间', - update_by bigint null comment '更新者', - update_time datetime null comment '更新时间', - constraint fk_schema_group - foreign key (schema_group_id) references dev_schema_group (id) - on delete set null -) - comment '数据模型表'; - -create index idx_schema_code - on dev_schema (code); - -create index idx_schema_group_id - on dev_schema (schema_group_id); - -create index idx_schema_status - on dev_schema (status); - -create index idx_schema_table_name - on dev_schema (table_name); - -create index idx_schema_tenant - on dev_schema (tenant_id); - - -DROP TABLE IF EXISTS `dev_schema_group`; -create table dev_schema_group -( - id bigint auto_increment comment '主键' - primary key, - name varchar(100) null comment '分组名称', - code varchar(100) null comment '分组编码', - icon varchar(100) null comment '图标', - sort int null comment '排序', - status char default '0' null comment '状态(0正常 1停用)', - remark varchar(500) null comment '备注', - del_flag char default '0' null comment '删除标志(0代表存在 2代表删除)', - tenant_id varchar(20) default '000000' null comment '租户编号', - create_dept bigint null comment '创建部门', - create_by bigint null comment '创建者', - create_time datetime null comment '创建时间', - update_by bigint null comment '更新者', - update_time datetime null comment '更新时间' -) - comment '数据模型分组表'; - -create index idx_schema_group_code - on dev_schema_group (code); - -create index idx_schema_group_status - on dev_schema_group (status); - -create index idx_schema_group_tenant - on dev_schema_group (tenant_id); - - -INSERT INTO dev_schema_group (id, name, code, icon, sort, status, remark, del_flag, tenant_id, create_dept, create_by, create_time, update_by, update_time) VALUES (1944240213530648567, '系统管理', 'system', 'eos-icons:system-group', 2, '0', '系统默认分组', '0', '000000', null, null, '2025-07-13 11:37:28', 1, '2025-07-13 18:42:48'); -INSERT INTO dev_schema_group (id, name, code, icon, sort, status, remark, del_flag, tenant_id, create_dept, create_by, create_time, update_by, update_time) VALUES (1944240213530648577, '运营管理', 'operator', 'icon-park-outline:appointment', 1, '0', null, '0', '000000', null, null, '2025-07-13 11:39:24', 1, '2025-07-13 18:42:31'); -INSERT INTO dev_schema_group (id, name, code, icon, sort, status, remark, del_flag, tenant_id, create_dept, create_by, create_time, update_by, update_time) VALUES (1944346023254429697, '在线开发', 'dev', 'carbon:development', 3, '0', null, '0', '000000', null, null, '2025-07-13 18:39:51', 1, '2025-07-13 18:42:07'); - - - + sort int null comment '排序', + del_flag char default '0' null comment '删除标志(0代表存在 2代表删除)', + create_dept bigint null comment '创建部门', + create_by bigint null comment '创建者', + create_time datetime null comment '创建时间', + update_by bigint null comment '更新者', + update_time datetime null comment '更新时间', + remark varchar(500) null comment '备注' +) ENGINE = InnoDB + CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT = '数据模型字段表'; +DROP TABLE IF EXISTS `knowledge_role`; -- ---------------------------- -- Table structure for knowledge_role -- ---------------------------- -DROP TABLE IF EXISTS `knowledge_role`; CREATE TABLE `knowledge_role` ( `id` bigint NOT NULL COMMENT '知识库角色id', `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库角色name', @@ -2664,10 +2596,10 @@ CREATE TABLE `knowledge_role` ( PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '知识库角色表' ROW_FORMAT = DYNAMIC; +DROP TABLE IF EXISTS `knowledge_role_group`; -- ---------------------------- -- Table structure for knowledge_role_group -- ---------------------------- -DROP TABLE IF EXISTS `knowledge_role_group`; CREATE TABLE `knowledge_role_group` ( `id` bigint NOT NULL COMMENT '知识库角色组id', `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库角色组name', From caf7f147816720ee4707feb44c17d935aef8fa5b Mon Sep 17 00:00:00 2001 From: violateer <1828257089@qq.com> Date: Sun, 10 Aug 2025 17:19:59 +0800 Subject: [PATCH 06/36] =?UTF-8?q?feature:=20=E6=96=B0=E5=A2=9E=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=89=8D=E7=AB=AF=E6=96=87=E4=BB=B6=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generator/controller/GenController.java | 18 +++++++++++++++++- .../generator/service/IGenTableService.java | 8 ++++++++ script/sql/update/knowledge-role-bak.sql | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/controller/GenController.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/controller/GenController.java index 52f78ecd..2e394a05 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/controller/GenController.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/controller/GenController.java @@ -1,5 +1,6 @@ package org.ruoyi.generator.controller; +import cn.hutool.core.net.URLDecoder; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.ruoyi.common.core.domain.R; @@ -8,10 +9,11 @@ import org.ruoyi.generator.service.IGenTableService; import org.ruoyi.generator.service.SchemaFieldService; import org.springframework.context.annotation.Profile; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.nio.charset.StandardCharsets; + /** * 代码生成 操作处理 * @@ -46,4 +48,18 @@ public class GenController extends BaseController { genTableService.generateCodeToClasspathByTableNames(tableNameStr); return R.ok("代码生成成功"); } + + /** + * 生成前端代码 + * + * @param workPath 执行命令路径 + * @param previewCode 执行生成前端文件命令 + */ + @GetMapping("/batchGenFrontendCode") + public R batchGenFrontendCode(@NotNull(message = "路径不能为空") String workPath, @NotNull(message = "指令不能为空") String previewCode) { + String decodedWorkPath = URLDecoder.decode(workPath, StandardCharsets.UTF_8); + String decodedPreviewCode = URLDecoder.decode(previewCode, StandardCharsets.UTF_8); + genTableService.generateFrontendTemplateFiles(decodedWorkPath, decodedPreviewCode); + return R.ok("代码生成成功"); + } } diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/service/IGenTableService.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/service/IGenTableService.java index bd8e291a..4403d3b7 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/service/IGenTableService.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/service/IGenTableService.java @@ -13,4 +13,12 @@ public interface IGenTableService { * @param tableName 表名称数组 */ void generateCodeToClasspathByTableNames(String tableName); + + /** + * 生成前端文件 + * + * @param workPath 执行命令路径 + * @param previewCode 执行生成前端文件命令 + */ + void generateFrontendTemplateFiles(String workPath, String previewCode); } diff --git a/script/sql/update/knowledge-role-bak.sql b/script/sql/update/knowledge-role-bak.sql index 5dfd35e7..62a7ce67 100644 --- a/script/sql/update/knowledge-role-bak.sql +++ b/script/sql/update/knowledge-role-bak.sql @@ -74,7 +74,7 @@ SET FOREIGN_KEY_CHECKS = 1; -- 菜单 -INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1946483381643743233, '知识库角色管理', 1775500307898949634, '12', 'knowledgeRole', 'system/knowledgeRole/index', NULL, 1, 0, 'C', '0', '0', NULL, 'ri:user-3-fill', 103, 1, '2025-07-19 16:41:17', NULL, NULL, '知识库角色管理'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1946483381643743233, '知识库角色管理', 1775500307898949634, '12', 'knowledgeRole', 'operator/knowledgeRole/index', NULL, 1, 0, 'C', '0', '0', NULL, 'ri:user-3-fill', 103, 1, '2025-07-19 16:41:17', NULL, NULL, '知识库角色管理'); -- 用户表添加字段 ALTER TABLE sys_user From bfeb38917197b0aa29e8879e023238521569c906 Mon Sep 17 00:00:00 2001 From: likunlong Date: Mon, 11 Aug 2025 09:33:35 +0800 Subject: [PATCH 07/36] =?UTF-8?q?feat:=20=E8=8E=B7=E5=8F=96=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E8=83=BD=E5=8A=9B=EF=BC=9B=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E8=A1=A8=E5=A2=9E=E5=8A=A0=E6=A8=A1=E5=9E=8B=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/ruoyi/domain/ChatModel.java | 6 + .../java/org/ruoyi/domain/vo/ChatModelVo.java | 140 +++++++++++++++++- script/sql/update/20250808.sql | 3 + 3 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 script/sql/update/20250808.sql diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java index c4e25a1b..b708b0a3 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java @@ -8,6 +8,7 @@ import lombok.EqualsAndHashCode; import org.ruoyi.core.domain.BaseEntity; import java.io.Serial; +import java.util.List; /** * 聊天模型对象 chat_model @@ -80,5 +81,10 @@ public class ChatModel extends BaseEntity { */ private String remark; + /** + * 模型能力 + */ + private String modelCapability; + } diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java index 77afe2f4..8472929d 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java @@ -5,14 +5,14 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; +import lombok.Getter; import org.ruoyi.common.sensitive.annotation.Sensitive; import org.ruoyi.common.sensitive.core.SensitiveStrategy; import org.ruoyi.domain.ChatModel; import java.io.Serial; import java.io.Serializable; - - +import java.util.List; /** @@ -96,4 +96,138 @@ public class ChatModelVo implements Serializable { @ExcelProperty(value = "备注") private String remark; -} + /** + * 模型能力 + */ + @ExcelProperty(value = "模型能力") + private String modelCapability; + + /** + * 模型能力列表 + */ + private List modelAbilities = getModelAbilities(); + + /** + * 模型能力类,类似枚举的静态内部类 + */ + @Getter + public static final class Ability { + // 获取能力名称 + private final String name; + private final String description; + + // 静态字段存储默认能力(类似枚举常量) + public static final Ability IMAGE = new Ability("IMAGE", "图片理解"); + public static final Ability VIDEO = new Ability("VIDEO", "视频理解"); + public static final Ability SPEECH = new Ability("SPEECH", "语音理解"); + + // 动态扩展能力存储 + private static final java.util.Map EXTENDED_ABILITIES = new java.util.HashMap<>(); + + // 私有构造确保受限 + private Ability(String name, String description) { + this.name = name; + this.description = description; + } + + // 静态工厂方法(类似枚举valueOf) + public static Ability valueOf(String name) { + // 先检查默认能力 + switch (name) { + case "IMAGE": return IMAGE; + case "VIDEO": return VIDEO; + case "SPEECH": return SPEECH; + default: + // 检查扩展能力 + Ability ability = EXTENDED_ABILITIES.get(name); + if (ability != null) return ability; + throw new IllegalArgumentException("Unknown ability: " + name); + } + } + + // 动态注册新能力(后期从数据库调用) + public static synchronized Ability registerAbility(String name, String description) { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Ability name cannot be empty"); + } + + // 避免重复注册 + if (EXTENDED_ABILITIES.containsKey(name)) { + return EXTENDED_ABILITIES.get(name); + } + + // 检查是否与默认能力冲突 + try { + valueOf(name); + throw new IllegalArgumentException("Ability already exists as default: " + name); + } catch (IllegalArgumentException e) { + // 正常情况,继续注册 + } + + Ability newAbility = new Ability(name, description); + EXTENDED_ABILITIES.put(name, newAbility); + return newAbility; + } + + // 获取所有能力(默认+扩展) + public static java.util.Set getAllAbilities() { + java.util.Map all = new java.util.HashMap<>(); + all.put(IMAGE.name, IMAGE); + all.put(VIDEO.name, VIDEO); + all.put(SPEECH.name, SPEECH); + all.putAll(EXTENDED_ABILITIES); + return java.util.Collections.unmodifiableSet((java.util.Set) all.values()); + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Ability ability = (Ability) o; + return name.equals(ability.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + } + + /** + * 将 modelCapability 字符串转换为 Ability 列表 + * @return Ability 列表 + */ + public java.util.List getModelAbilities() { + if (modelCapability == null || modelCapability.trim().isEmpty()) { + return java.util.Collections.emptyList(); + } + + // 解析 JSON 格式的字符串数组 + String trimmed = modelCapability.trim(); + if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) { + throw new IllegalArgumentException("Invalid modelCapability format: " + modelCapability); + } + + String content = trimmed.substring(1, trimmed.length() - 1).trim(); + if (content.isEmpty()) { + return java.util.Collections.emptyList(); + } + + java.util.List abilities = new java.util.ArrayList<>(); + String[] items = content.split(","); + for (String item : items) { + String cleanedItem = item.trim(); + if (cleanedItem.startsWith("\"") && cleanedItem.endsWith("\"") && cleanedItem.length() >= 2) { + String abilityName = cleanedItem.substring(1, cleanedItem.length() - 1); + abilities.add(Ability.valueOf(abilityName)); + } + } + + return abilities; + } +} \ No newline at end of file diff --git a/script/sql/update/20250808.sql b/script/sql/update/20250808.sql new file mode 100644 index 00000000..3c54c207 --- /dev/null +++ b/script/sql/update/20250808.sql @@ -0,0 +1,3 @@ +-- 聊天模型表添加模型能力字段 +alter table chat_model + add model_capability varchar(255) default '[]' not null comment '模型能力'; From 416f011c73dd920beda8a944feb61dc53180dbde Mon Sep 17 00:00:00 2001 From: l90215 Date: Wed, 13 Aug 2025 12:37:10 +0800 Subject: [PATCH 08/36] =?UTF-8?q?feat:=20fix=E4=BB=A3=E7=A0=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=B1=BB=E5=9E=8B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/vm/java/vo.java.vm | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/vo.java.vm b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/vo.java.vm index 15e437c9..aa9b8543 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/vo.java.vm +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/vm/java/vo.java.vm @@ -23,29 +23,30 @@ import java.util.Date; @AutoMapper(target = ${ClassName}.class) public class ${ClassName}Vo implements Serializable { - #foreach ($column in $columns) - #if($column.list) - /** - * $column.columnComment - */ - #set($parentheseIndex=$column.columnComment.indexOf("(")) - #if($parentheseIndex != -1) - #set($comment=$column.columnComment.substring(0, $parentheseIndex)) - #else - #set($comment=$column.columnComment) - #end - #if(${column.dictType} && ${column.dictType} != '') - @ExcelProperty(value = "${comment}", converter = ExcelDictConvert.class) - @ExcelDictFormat(dictType = "${column.dictType}") - #elseif($parentheseIndex != -1) - @ExcelProperty(value = "${comment}", converter = ExcelDictConvert.class) - @ExcelDictFormat(readConverterExp = "$column.readConverterExp()") - #else - @ExcelProperty(value = "${comment}") - #end - private $column.javaType $column.javaField; - - #end +#foreach ($column in $columns) + #if($column.isPk) + private $column.javaType $column.javaField; + #elseif($column.list) + /** + * $column.columnComment + */ + #set($parentheseIndex=$column.columnComment.indexOf("(")) + #if($parentheseIndex != -1) + #set($comment=$column.columnComment.substring(0, $parentheseIndex)) + #else + #set($comment=$column.columnComment) #end + #if(${column.dictType} && ${column.dictType} != '') + @ExcelProperty(value = "${comment}", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "${column.dictType}") + #elseif($parentheseIndex != -1) + @ExcelProperty(value = "${comment}", converter = ExcelDictConvert.class) + @ExcelDictFormat(readConverterExp = "$column.readConverterExp()") + #else + @ExcelProperty(value = "${comment}") + #end + private $column.javaType $column.javaField; + #end +#end } From ebc13c06afbd812aebb7f5f05d7c23ddf3dd74fe Mon Sep 17 00:00:00 2001 From: l90215 Date: Fri, 15 Aug 2025 09:56:47 +0800 Subject: [PATCH 09/36] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=8F=AA?= =?UTF-8?q?=E6=98=AF=E5=BA=93=E8=A7=92=E8=89=B2=E9=BB=98=E8=AE=A4=E4=B8=8D?= =?UTF-8?q?=E5=BC=80=E5=90=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-admin/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index f6abd846..1992a5ab 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -162,7 +162,7 @@ tenant: - sys_user_role knowledge-role: - enable: true + enable: false # MyBatisPlus配置 # https://baomidou.com/config/ From 0cdba56a07b254cd2e5501862c2a74a7e0976a5c Mon Sep 17 00:00:00 2001 From: violateer <1828257089@qq.com> Date: Fri, 15 Aug 2025 20:41:59 +0800 Subject: [PATCH 10/36] =?UTF-8?q?feature:=20=E6=B7=BB=E5=8A=A0=E5=BC=80?= =?UTF-8?q?=E5=90=AF=E7=9F=A5=E8=AF=86=E5=BA=93=E8=A7=92=E8=89=B2=EF=BC=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=8F=AF=E8=A7=81=E4=B8=AA=E4=BA=BA=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E5=8F=8A=E8=A7=92=E8=89=B2=E5=88=86=E9=85=8D?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-admin/src/main/resources/application.yml | 2 +- .../org/ruoyi/service/IKnowledgeInfoService.java | 2 +- .../controller/knowledge/KnowledgeController.java | 14 +++----------- .../knowledge/KnowledgeInfoServiceImpl.java | 13 ++++++++++--- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 1992a5ab..f6abd846 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -162,7 +162,7 @@ tenant: - sys_user_role knowledge-role: - enable: false + enable: true # MyBatisPlus配置 # https://baomidou.com/config/ diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/IKnowledgeInfoService.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/IKnowledgeInfoService.java index 3972677d..754ba7c7 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/IKnowledgeInfoService.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/IKnowledgeInfoService.java @@ -31,7 +31,7 @@ public interface IKnowledgeInfoService { /** * 查询知识库列表 */ - TableDataInfo queryPageListByRole(PageQuery pageQuery); + TableDataInfo queryPageListByRole(KnowledgeInfoBo bo, PageQuery pageQuery); /** * 查询知识库列表 diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java index 76443156..a1a72e1a 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/knowledge/KnowledgeController.java @@ -7,7 +7,6 @@ import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.ruoyi.chat.config.KnowledgeRoleConfig; import org.ruoyi.common.core.domain.R; -import org.ruoyi.common.core.domain.model.LoginUser; import org.ruoyi.common.core.validate.AddGroup; import org.ruoyi.common.excel.utils.ExcelUtil; import org.ruoyi.common.log.annotation.Log; @@ -27,14 +26,7 @@ import org.ruoyi.service.IKnowledgeAttachService; import org.ruoyi.service.IKnowledgeFragmentService; import org.ruoyi.service.IKnowledgeInfoService; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -90,8 +82,8 @@ public class KnowledgeController extends BaseController { bo.setUid(LoginHelper.getUserId()); return knowledgeInfoService.queryPageList(bo, pageQuery); } else { - // TODO 自己创建的知识库+角色分配的知识库 - return knowledgeInfoService.queryPageListByRole(pageQuery); + bo.setUid(LoginHelper.getUserId()); + return knowledgeInfoService.queryPageListByRole(bo, pageQuery); } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java index d2eb1276..12ccab1f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java @@ -89,7 +89,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService { * 根据知识库角色查询知识库列表 */ @Override - public TableDataInfo queryPageListByRole(PageQuery pageQuery) { + public TableDataInfo queryPageListByRole(KnowledgeInfoBo bo, PageQuery pageQuery) { // 查询用户关联角色 LoginUser loginUser = LoginHelper.getLoginUser(); if (StringUtils.isEmpty(loginUser.getKroleGroupIds()) || StringUtils.isEmpty(loginUser.getKroleGroupType())) { @@ -122,8 +122,15 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService { return new TableDataInfo<>(); } - LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.in(KnowledgeInfo::getId, knowledgeRoleRelations.stream().map(KnowledgeRoleRelation::getKnowledgeId).filter(Objects::nonNull).collect(Collectors.toList())); + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + // 在查询用户创建的知识库条件下,拼接角色分配知识库 + lqw.or(q -> q.in( + KnowledgeInfo::getId, + knowledgeRoleRelations.stream() + .map(KnowledgeRoleRelation::getKnowledgeId) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + )); Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); return TableDataInfo.build(result); } From 0780e3b8c9435bba47e6277e18636774178cb818 Mon Sep 17 00:00:00 2001 From: violateer <1828257089@qq.com> Date: Fri, 15 Aug 2025 20:43:14 +0800 Subject: [PATCH 11/36] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9krole=5Fgroup=5F?= =?UTF-8?q?ids=E5=AD=97=E6=AE=B5=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/sql/update/knowledge-role-bak.sql | 97 ++++++++++++++---------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/script/sql/update/knowledge-role-bak.sql b/script/sql/update/knowledge-role-bak.sql index 62a7ce67..e2e4842e 100644 --- a/script/sql/update/knowledge-role-bak.sql +++ b/script/sql/update/knowledge-role-bak.sql @@ -21,63 +21,80 @@ SET FOREIGN_KEY_CHECKS = 0; -- Table structure for knowledge_role -- ---------------------------- DROP TABLE IF EXISTS `knowledge_role`; -CREATE TABLE `knowledge_role` ( - `id` bigint NOT NULL COMMENT '知识库角色id', - `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库角色name', - `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', - `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', - `create_by` bigint NULL DEFAULT NULL COMMENT '创建者', - `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', - `update_by` bigint NULL DEFAULT NULL COMMENT '更新者', - `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', - `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', - `group_id` bigint NULL DEFAULT NULL COMMENT '知识库角色组id', - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '知识库角色表' ROW_FORMAT = DYNAMIC; +CREATE TABLE `knowledge_role` +( + `id` bigint NOT NULL COMMENT '知识库角色id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库角色name', + `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', + `create_by` bigint NULL DEFAULT NULL COMMENT '创建者', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` bigint NULL DEFAULT NULL COMMENT '更新者', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `group_id` bigint NULL DEFAULT NULL COMMENT '知识库角色组id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB + CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT = '知识库角色表' + ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Table structure for knowledge_role_group -- ---------------------------- DROP TABLE IF EXISTS `knowledge_role_group`; -CREATE TABLE `knowledge_role_group` ( - `id` bigint NOT NULL COMMENT '知识库角色组id', - `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库角色组name', - `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', - `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', - `create_by` bigint NULL DEFAULT NULL COMMENT '创建者', - `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', - `update_by` bigint NULL DEFAULT NULL COMMENT '更新者', - `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', - `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '知识库角色组表' ROW_FORMAT = DYNAMIC; +CREATE TABLE `knowledge_role_group` +( + `id` bigint NOT NULL COMMENT '知识库角色组id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库角色组name', + `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', + `create_by` bigint NULL DEFAULT NULL COMMENT '创建者', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` bigint NULL DEFAULT NULL COMMENT '更新者', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB + CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT = '知识库角色组表' + ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Table structure for knowledge_role_relation -- ---------------------------- DROP TABLE IF EXISTS `knowledge_role_relation`; -CREATE TABLE `knowledge_role_relation` ( - `id` bigint NOT NULL COMMENT 'id', - `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', - `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', - `create_by` bigint NULL DEFAULT NULL COMMENT '创建者', - `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', - `update_by` bigint NULL DEFAULT NULL COMMENT '更新者', - `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', - `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', - `knowledge_role_id` bigint NULL DEFAULT NULL COMMENT '知识库角色id', - `knowledge_id` bigint NULL DEFAULT NULL COMMENT '知识库id', - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '知识库角色与知识库关联表' ROW_FORMAT = DYNAMIC; +CREATE TABLE `knowledge_role_relation` +( + `id` bigint NOT NULL COMMENT 'id', + `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', + `create_by` bigint NULL DEFAULT NULL COMMENT '创建者', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` bigint NULL DEFAULT NULL COMMENT '更新者', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `knowledge_role_id` bigint NULL DEFAULT NULL COMMENT '知识库角色id', + `knowledge_id` bigint NULL DEFAULT NULL COMMENT '知识库id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB + CHARACTER SET = utf8mb4 + COLLATE = utf8mb4_general_ci COMMENT = '知识库角色与知识库关联表' + ROW_FORMAT = DYNAMIC; SET FOREIGN_KEY_CHECKS = 1; -- 菜单 -INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1946483381643743233, '知识库角色管理', 1775500307898949634, '12', 'knowledgeRole', 'operator/knowledgeRole/index', NULL, 1, 0, 'C', '0', '0', NULL, 'ri:user-3-fill', 103, 1, '2025-07-19 16:41:17', NULL, NULL, '知识库角色管理'); +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, + `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, + `create_by`, `create_time`, `update_by`, `update_time`, `remark`) +VALUES (1946483381643743233, '知识库角色管理', 1775500307898949634, '12', 'knowledgeRole', + 'operator/knowledgeRole/index', NULL, 1, 0, 'C', '0', '0', NULL, 'ri:user-3-fill', 103, 1, + '2025-07-19 16:41:17', NULL, NULL, '知识库角色管理'); -- 用户表添加字段 ALTER TABLE sys_user ADD COLUMN `krole_group_type` VARCHAR(50) COMMENT '关联知识库角色/角色组', -ADD COLUMN `krole_group_id` TEXT COMMENT '关联知识库角色/角色组id'; + ADD COLUMN `krole_group_ids` TEXT COMMENT '关联知识库角色/角色组id'; From 22e59fe5a10e4d528db3aabb2825469cc9d6e6f4 Mon Sep 17 00:00:00 2001 From: lixiang <17851857880@163.com> Date: Mon, 18 Aug 2025 11:12:06 +0800 Subject: [PATCH 12/36] =?UTF-8?q?=E5=90=91=E9=87=8F=E5=BA=93sql=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=8E=BB=E9=99=A4=E5=8C=B9=E9=85=8D=E5=88=86=E5=80=BC?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/VectorStoreServiceImpl.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java index 85534b2c..f3e83b11 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java @@ -42,7 +42,7 @@ public class VectorStoreServiceImpl implements VectorStoreService { private final ConfigService configService; - private EmbeddingStore embeddingStore; +// private EmbeddingStore embeddingStore; private WeaviateClient client; @@ -82,14 +82,14 @@ public class VectorStoreServiceImpl implements VectorStoreService { log.info("Schema 创建成功: {}", className); } } - embeddingStore = WeaviateEmbeddingStore.builder() - .scheme(protocol) - .host(host) - .objectClass(className) - .scheme(protocol) - .avoidDups(true) - .consistencyLevel("ALL") - .build(); +// embeddingStore = WeaviateEmbeddingStore.builder() +// .scheme(protocol) +// .host(host) +// .objectClass(className) +// .scheme(protocol) +// .avoidDups(true) +// .consistencyLevel("ALL") +// .build(); } @Override @@ -148,7 +148,7 @@ public class VectorStoreServiceImpl implements VectorStoreService { String graphQLQuery = String.format( "{\n" + " Get {\n" + - " %s(nearVector: {vector: [%s], certainty: %f} limit: %d) {\n" + + " %s(nearVector: {vector: [%s]} limit: %d) {\n" + " text\n" + " fid\n" + " kid\n" + From f448a18e44936ea30fc61b96ab51559a3896ca25 Mon Sep 17 00:00:00 2001 From: fy53888 Date: Sat, 9 Aug 2025 21:55:23 +0800 Subject: [PATCH 13/36] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=A1=9E=E5=9E=8B=20Integer=E5=87=BA?= =?UTF-8?q?=E9=8C=AF=E7=9A=84=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yml | 2 +- ruoyi-modules/ruoyi-generator/pom.xml | 4 + .../system/controller/SysDemoController.java | 105 ++++++++++++++++ .../java/org/ruoyi/system/domain/SysDemo.java | 75 +++++++++++ .../org/ruoyi/system/domain/bo/SysDemoBo.java | 84 +++++++++++++ .../org/ruoyi/system/domain/vo/SysDemoVo.java | 86 +++++++++++++ .../ruoyi/system/mapper/SysDemoMapper.java | 17 +++ .../ruoyi/system/service/SysDemoService.java | 48 ++++++++ .../service/impl/SysDemoServiceImpl.java | 116 ++++++++++++++++++ .../org/ruoyi/system/sql/sys_demo_menu.sql | 19 +++ .../resources/mapper/system/SysDemoMapper.xml | 7 ++ 11 files changed, 562 insertions(+), 1 deletion(-) create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/SysDemoController.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/SysDemo.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/SysDemoBo.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/SysDemoVo.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/SysDemoMapper.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/SysDemoService.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/SysDemoServiceImpl.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/sys_demo_menu.sql create mode 100644 ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/SysDemoMapper.xml diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 8abf1a68..0b1661e2 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -18,7 +18,7 @@ spring: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true username: root - password: root + password: 123456 hikari: # 最大连接池数量 diff --git a/ruoyi-modules/ruoyi-generator/pom.xml b/ruoyi-modules/ruoyi-generator/pom.xml index c6ab5e3c..e545d91b 100644 --- a/ruoyi-modules/ruoyi-generator/pom.xml +++ b/ruoyi-modules/ruoyi-generator/pom.xml @@ -48,6 +48,10 @@ org.apache.velocity velocity-engine-core + + org.ruoyi + ruoyi-common-excel + diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/SysDemoController.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/SysDemoController.java new file mode 100644 index 00000000..0c795ed9 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/SysDemoController.java @@ -0,0 +1,105 @@ +package org.ruoyi.system.controller; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.ruoyi.common.idempotent.annotation.RepeatSubmit; +import org.ruoyi.common.log.annotation.Log; +import org.ruoyi.common.web.core.BaseController; +import org.ruoyi.core.page.PageQuery; +import org.ruoyi.common.core.domain.R; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; +import org.ruoyi.common.log.enums.BusinessType; +import org.ruoyi.common.excel.utils.ExcelUtil; +import org.ruoyi.system.domain.vo.SysDemoVo; +import org.ruoyi.system.domain.bo.SysDemoBo; +import org.ruoyi.system.service.SysDemoService; +import org.ruoyi.core.page.TableDataInfo; + +/** + * dome管理 + * + * @author ageerle + * @date Sat Aug 09 21:38:09 CST 2025 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("dev/sysDemo") +public class SysDemoController extends BaseController { + + private final SysDemoService sysDemoService; + +/** + * 查询dome管理列表 + */ +@SaCheckPermission("system:sysDemo:list") +@GetMapping("/list") + public TableDataInfo list(SysDemoBo bo, PageQuery pageQuery) { + return sysDemoService.queryPageList(bo, pageQuery); + } + + /** + * 导出dome管理列表 + */ + @SaCheckPermission("system:sysDemo:export") + @Log(title = "dome管理", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(SysDemoBo bo, HttpServletResponse response) { + List list = sysDemoService.queryList(bo); + ExcelUtil.exportExcel(list, "dome管理", SysDemoVo.class, response); + } + + /** + * 获取dome管理详细信息 + * + * @param id 主键 + */ + @SaCheckPermission("system:sysDemo:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Integer id) { + return R.ok(sysDemoService.queryById(id)); + } + + /** + * 新增dome管理 + */ + @SaCheckPermission("system:sysDemo:add") + @Log(title = "dome管理", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody SysDemoBo bo) { + return toAjax(sysDemoService.insertByBo(bo)); + } + + /** + * 修改dome管理 + */ + @SaCheckPermission("system:sysDemo:edit") + @Log(title = "dome管理", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody SysDemoBo bo) { + return toAjax(sysDemoService.updateByBo(bo)); + } + + /** + * 删除dome管理 + * + * @param ids 主键串 + */ + @SaCheckPermission("system:sysDemo:remove") + @Log(title = "dome管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Integer[] ids) { + return toAjax(sysDemoService.deleteWithValidByIds(List.of(ids), true)); + } +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/SysDemo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/SysDemo.java new file mode 100644 index 00000000..81187bf2 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/SysDemo.java @@ -0,0 +1,75 @@ +package org.ruoyi.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + import java.util.Date; + import java.io.Serializable; + +import org.ruoyi.core.domain.BaseEntity; + +/** + * dome管理对象 sys_demo + * + * @author ageerle + * @date Sat Aug 09 21:38:09 CST 2025 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_demo") +public class SysDemo extends BaseEntity { + + + /** + * ID + */ + @TableId(value = "id") + private Integer id; + + /** + * 系统代码 + */ + private String sysCode; + + /** + * 系统名称 + */ + private String sysName; + + /** + * 系统状态 + */ + private Integer sysStatus; + + /** + * 创建部门 + */ + private Integer createDept; + + /** + * 创建者 + */ + private Integer createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新者 + */ + private Integer updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/SysDemoBo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/SysDemoBo.java new file mode 100644 index 00000000..caca34c1 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/SysDemoBo.java @@ -0,0 +1,84 @@ +package org.ruoyi.system.domain.bo; + +import org.ruoyi.system.domain.SysDemo; +import org.ruoyi.core.domain.BaseEntity; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; +import java.util.Date; +import java.io.Serializable; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; +import java.io.Serializable; +import java.io.Serializable; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; + +/** + * dome管理业务对象 sys_demo + * + * @author ageerle + * @date Sat Aug 09 21:38:09 CST 2025 + */ +@Data + +@AutoMapper(target = SysDemo.class, reverseConvertGenerate = false) +public class SysDemoBo implements Serializable { + + /** + * ID + */ + @NotNull(message = "ID不能为空", groups = { EditGroup.class }) + private Integer id; + + /** + * 系统代码 + */ + @NotBlank(message = "系统代码不能为空", groups = { AddGroup.class, EditGroup.class }) + private String sysCode; + + /** + * 系统名称 + */ + @NotBlank(message = "系统名称不能为空", groups = { AddGroup.class, EditGroup.class }) + private String sysName; + + /** + * 系统状态 + */ + @NotNull(message = "系统状态不能为空", groups = { AddGroup.class, EditGroup.class }) + private Integer sysStatus; + + /** + * 创建部门 + */ + private Integer createDept; + + /** + * 创建者 + */ + private Integer createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新者 + */ + private Integer updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/SysDemoVo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/SysDemoVo.java new file mode 100644 index 00000000..0fd9f1a5 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/SysDemoVo.java @@ -0,0 +1,86 @@ +package org.ruoyi.system.domain.vo; + + import java.util.Date; + import java.io.Serializable; +import org.ruoyi.system.domain.SysDemo; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; + +import java.util.Date; + + +/** + * dome管理视图对象 sys_demo + * + * @author ageerle + * @date Sat Aug 09 21:38:09 CST 2025 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = SysDemo.class) +public class SysDemoVo implements Serializable { + + /** + * ID + */ + @ExcelProperty(value = "ID") + private Integer id; + + /** + * 系统代码 + */ + @ExcelProperty(value = "系统代码") + private String sysCode; + + /** + * 系统名称 + */ + @ExcelProperty(value = "系统名称") + private String sysName; + + /** + * 系统状态 + */ + @ExcelProperty(value = "系统状态") + private Integer sysStatus; + + /** + * 创建部门 + */ + @ExcelProperty(value = "创建部门") + private Integer createDept; + + /** + * 创建者 + */ + @ExcelProperty(value = "创建者") + private Integer createBy; + + /** + * 创建时间 + */ + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 更新者 + */ + @ExcelProperty(value = "更新者") + private Integer updateBy; + + /** + * 更新时间 + */ + @ExcelProperty(value = "更新时间") + private Date updateTime; + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remark; + + +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/SysDemoMapper.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/SysDemoMapper.java new file mode 100644 index 00000000..94351479 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/SysDemoMapper.java @@ -0,0 +1,17 @@ +package org.ruoyi.system.mapper; + +import org.ruoyi.system.domain.SysDemo; +import org.ruoyi.system.domain.vo.SysDemoVo; +import org.ruoyi.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Mapper; + +/** + * dome管理Mapper接口 + * + * @author ageerle + * @date Sat Aug 09 21:38:09 CST 2025 + */ +@Mapper +public interface SysDemoMapper extends BaseMapperPlus { + +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/SysDemoService.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/SysDemoService.java new file mode 100644 index 00000000..ae7f4901 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/SysDemoService.java @@ -0,0 +1,48 @@ +package org.ruoyi.system.service; + +import org.ruoyi.system.domain.vo.SysDemoVo; +import org.ruoyi.system.domain.bo.SysDemoBo; + import org.ruoyi.core.page.TableDataInfo; + import org.ruoyi.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * dome管理Service接口 + * + * @author ageerle + * @date Sat Aug 09 21:38:09 CST 2025 + */ +public interface SysDemoService { + + /** + * 查询dome管理 + */ + SysDemoVo queryById(Integer id); + + /** + * 查询dome管理列表 + */ + TableDataInfo queryPageList(SysDemoBo bo, PageQuery pageQuery); + + /** + * 查询dome管理列表 + */ + List queryList(SysDemoBo bo); + + /** + * 新增dome管理 + */ + Boolean insertByBo(SysDemoBo bo); + + /** + * 修改dome管理 + */ + Boolean updateByBo(SysDemoBo bo); + + /** + * 校验并批量删除dome管理信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/SysDemoServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/SysDemoServiceImpl.java new file mode 100644 index 00000000..a0bd9054 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/SysDemoServiceImpl.java @@ -0,0 +1,116 @@ +package org.ruoyi.system.service.impl; + +import org.ruoyi.common.core.utils.MapstructUtils; + import org.ruoyi.core.page.TableDataInfo; + import org.ruoyi.core.page.PageQuery; + import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.ruoyi.system.domain.bo.SysDemoBo; +import org.ruoyi.system.domain.vo.SysDemoVo; +import org.ruoyi.system.domain.SysDemo; +import org.ruoyi.system.mapper.SysDemoMapper; +import org.ruoyi.system.service.SysDemoService; +import org.ruoyi.common.core.utils.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.Collection; + +/** + * dome管理Service业务层处理 + * + * @author ageerle + * @date Sat Aug 09 21:38:09 CST 2025 + */ +@RequiredArgsConstructor +@Service +public class SysDemoServiceImpl implements SysDemoService { + + private final SysDemoMapper baseMapper; + + /** + * 查询dome管理 + */ + @Override + public SysDemoVo queryById(Integer id) { + return baseMapper.selectVoById(id); + } + + /** + * 查询dome管理列表 + */ + @Override + public TableDataInfo queryPageList(SysDemoBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询dome管理列表 + */ + @Override + public List queryList(SysDemoBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(SysDemoBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(StringUtils.isNotBlank(bo.getSysCode()), SysDemo::getSysCode, bo.getSysCode()); + lqw.eq(StringUtils.isNotBlank(bo.getSysName()), SysDemo::getSysName, bo.getSysName()); + lqw.eq(bo.getSysStatus() != null, SysDemo::getSysStatus, bo.getSysStatus()); + lqw.eq(bo.getCreateDept() != null, SysDemo::getCreateDept, bo.getCreateDept()); + lqw.eq(bo.getCreateBy() != null, SysDemo::getCreateBy, bo.getCreateBy()); + lqw.eq(bo.getCreateTime() != null, SysDemo::getCreateTime, bo.getCreateTime()); + lqw.eq(bo.getUpdateBy() != null, SysDemo::getUpdateBy, bo.getUpdateBy()); + lqw.eq(bo.getUpdateTime() != null, SysDemo::getUpdateTime, bo.getUpdateTime()); + lqw.eq(StringUtils.isNotBlank(bo.getRemark()), SysDemo::getRemark, bo.getRemark()); + return lqw; + } + + /** + * 新增dome管理 + */ + @Override + public Boolean insertByBo(SysDemoBo bo) { + SysDemo add = MapstructUtils.convert(bo, SysDemo. class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改dome管理 + */ + @Override + public Boolean updateByBo(SysDemoBo bo) { + SysDemo update = MapstructUtils.convert(bo, SysDemo. class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(SysDemo entity) { + //TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除dome管理 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/sys_demo_menu.sql b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/sys_demo_menu.sql new file mode 100644 index 00000000..5b19392a --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/sys_demo_menu.sql @@ -0,0 +1,19 @@ +-- 菜单 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954175368920645632, 'dome管理', '2000', '1', 'sysDemo', 'system/sysDemo/index', 1, 0, 'C', '0', '0', 'system:sysDemo:list', '#', 103, 1, sysdate(), null, null, 'dome管理菜单'); + +-- 按钮 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954175368920645633, 'dome管理查询', 1954175368920645632, '1', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:query', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954175368920645634, 'dome管理新增', 1954175368920645632, '2', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:add', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954175368920645635, 'dome管理修改', 1954175368920645632, '3', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:edit', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954175368920645636, 'dome管理删除', 1954175368920645632, '4', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:remove', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954175368920645637, 'dome管理导出', 1954175368920645632, '5', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:export', '#', 103, 1, sysdate(), null, null, ''); diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/SysDemoMapper.xml b/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/SysDemoMapper.xml new file mode 100644 index 00000000..c35ba06c --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/SysDemoMapper.xml @@ -0,0 +1,7 @@ + + + + + From 268be2d9ec4acc8a73f276390eca98fbbd564398 Mon Sep 17 00:00:00 2001 From: fy53888 Date: Sat, 9 Aug 2025 21:56:16 +0800 Subject: [PATCH 14/36] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=A1=9E=E5=9E=8B=20Integer=E5=87=BA?= =?UTF-8?q?=E9=8C=AF=E7=9A=84=E5=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/controller/SysDemoController.java | 105 ---------------- .../java/org/ruoyi/system/domain/SysDemo.java | 75 ----------- .../org/ruoyi/system/domain/bo/SysDemoBo.java | 84 ------------- .../org/ruoyi/system/domain/vo/SysDemoVo.java | 86 ------------- .../ruoyi/system/mapper/SysDemoMapper.java | 17 --- .../ruoyi/system/service/SysDemoService.java | 48 -------- .../service/impl/SysDemoServiceImpl.java | 116 ------------------ .../org/ruoyi/system/sql/sys_demo_menu.sql | 19 --- 8 files changed, 550 deletions(-) delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/SysDemoController.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/SysDemo.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/SysDemoBo.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/SysDemoVo.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/SysDemoMapper.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/SysDemoService.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/SysDemoServiceImpl.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/sys_demo_menu.sql diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/SysDemoController.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/SysDemoController.java deleted file mode 100644 index 0c795ed9..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/SysDemoController.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.ruoyi.system.controller; - -import java.util.List; - -import lombok.RequiredArgsConstructor; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.constraints.*; -import cn.dev33.satoken.annotation.SaCheckPermission; -import org.springframework.web.bind.annotation.*; -import org.springframework.validation.annotation.Validated; -import org.ruoyi.common.idempotent.annotation.RepeatSubmit; -import org.ruoyi.common.log.annotation.Log; -import org.ruoyi.common.web.core.BaseController; -import org.ruoyi.core.page.PageQuery; -import org.ruoyi.common.core.domain.R; -import org.ruoyi.common.core.validate.AddGroup; -import org.ruoyi.common.core.validate.EditGroup; -import org.ruoyi.common.log.enums.BusinessType; -import org.ruoyi.common.excel.utils.ExcelUtil; -import org.ruoyi.system.domain.vo.SysDemoVo; -import org.ruoyi.system.domain.bo.SysDemoBo; -import org.ruoyi.system.service.SysDemoService; -import org.ruoyi.core.page.TableDataInfo; - -/** - * dome管理 - * - * @author ageerle - * @date Sat Aug 09 21:38:09 CST 2025 - */ -@Validated -@RequiredArgsConstructor -@RestController -@RequestMapping("dev/sysDemo") -public class SysDemoController extends BaseController { - - private final SysDemoService sysDemoService; - -/** - * 查询dome管理列表 - */ -@SaCheckPermission("system:sysDemo:list") -@GetMapping("/list") - public TableDataInfo list(SysDemoBo bo, PageQuery pageQuery) { - return sysDemoService.queryPageList(bo, pageQuery); - } - - /** - * 导出dome管理列表 - */ - @SaCheckPermission("system:sysDemo:export") - @Log(title = "dome管理", businessType = BusinessType.EXPORT) - @PostMapping("/export") - public void export(SysDemoBo bo, HttpServletResponse response) { - List list = sysDemoService.queryList(bo); - ExcelUtil.exportExcel(list, "dome管理", SysDemoVo.class, response); - } - - /** - * 获取dome管理详细信息 - * - * @param id 主键 - */ - @SaCheckPermission("system:sysDemo:query") - @GetMapping("/{id}") - public R getInfo(@NotNull(message = "主键不能为空") - @PathVariable Integer id) { - return R.ok(sysDemoService.queryById(id)); - } - - /** - * 新增dome管理 - */ - @SaCheckPermission("system:sysDemo:add") - @Log(title = "dome管理", businessType = BusinessType.INSERT) - @RepeatSubmit() - @PostMapping() - public R add(@Validated(AddGroup.class) @RequestBody SysDemoBo bo) { - return toAjax(sysDemoService.insertByBo(bo)); - } - - /** - * 修改dome管理 - */ - @SaCheckPermission("system:sysDemo:edit") - @Log(title = "dome管理", businessType = BusinessType.UPDATE) - @RepeatSubmit() - @PutMapping() - public R edit(@Validated(EditGroup.class) @RequestBody SysDemoBo bo) { - return toAjax(sysDemoService.updateByBo(bo)); - } - - /** - * 删除dome管理 - * - * @param ids 主键串 - */ - @SaCheckPermission("system:sysDemo:remove") - @Log(title = "dome管理", businessType = BusinessType.DELETE) - @DeleteMapping("/{ids}") - public R remove(@NotEmpty(message = "主键不能为空") - @PathVariable Integer[] ids) { - return toAjax(sysDemoService.deleteWithValidByIds(List.of(ids), true)); - } -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/SysDemo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/SysDemo.java deleted file mode 100644 index 81187bf2..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/SysDemo.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.ruoyi.system.domain; - -import com.baomidou.mybatisplus.annotation.*; -import lombok.Data; -import lombok.EqualsAndHashCode; - import java.util.Date; - import java.io.Serializable; - -import org.ruoyi.core.domain.BaseEntity; - -/** - * dome管理对象 sys_demo - * - * @author ageerle - * @date Sat Aug 09 21:38:09 CST 2025 - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_demo") -public class SysDemo extends BaseEntity { - - - /** - * ID - */ - @TableId(value = "id") - private Integer id; - - /** - * 系统代码 - */ - private String sysCode; - - /** - * 系统名称 - */ - private String sysName; - - /** - * 系统状态 - */ - private Integer sysStatus; - - /** - * 创建部门 - */ - private Integer createDept; - - /** - * 创建者 - */ - private Integer createBy; - - /** - * 创建时间 - */ - private Date createTime; - - /** - * 更新者 - */ - private Integer updateBy; - - /** - * 更新时间 - */ - private Date updateTime; - - /** - * 备注 - */ - private String remark; - - -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/SysDemoBo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/SysDemoBo.java deleted file mode 100644 index caca34c1..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/SysDemoBo.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.ruoyi.system.domain.bo; - -import org.ruoyi.system.domain.SysDemo; -import org.ruoyi.core.domain.BaseEntity; -import io.github.linpeilie.annotations.AutoMapper; -import lombok.Data; -import lombok.EqualsAndHashCode; -import jakarta.validation.constraints.*; -import java.util.Date; -import java.io.Serializable; -import org.ruoyi.common.core.validate.AddGroup; -import org.ruoyi.common.core.validate.EditGroup; -import java.io.Serializable; -import java.io.Serializable; -import org.ruoyi.common.core.validate.AddGroup; -import org.ruoyi.common.core.validate.EditGroup; - -/** - * dome管理业务对象 sys_demo - * - * @author ageerle - * @date Sat Aug 09 21:38:09 CST 2025 - */ -@Data - -@AutoMapper(target = SysDemo.class, reverseConvertGenerate = false) -public class SysDemoBo implements Serializable { - - /** - * ID - */ - @NotNull(message = "ID不能为空", groups = { EditGroup.class }) - private Integer id; - - /** - * 系统代码 - */ - @NotBlank(message = "系统代码不能为空", groups = { AddGroup.class, EditGroup.class }) - private String sysCode; - - /** - * 系统名称 - */ - @NotBlank(message = "系统名称不能为空", groups = { AddGroup.class, EditGroup.class }) - private String sysName; - - /** - * 系统状态 - */ - @NotNull(message = "系统状态不能为空", groups = { AddGroup.class, EditGroup.class }) - private Integer sysStatus; - - /** - * 创建部门 - */ - private Integer createDept; - - /** - * 创建者 - */ - private Integer createBy; - - /** - * 创建时间 - */ - private Date createTime; - - /** - * 更新者 - */ - private Integer updateBy; - - /** - * 更新时间 - */ - private Date updateTime; - - /** - * 备注 - */ - private String remark; - - -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/SysDemoVo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/SysDemoVo.java deleted file mode 100644 index 0fd9f1a5..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/SysDemoVo.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.ruoyi.system.domain.vo; - - import java.util.Date; - import java.io.Serializable; -import org.ruoyi.system.domain.SysDemo; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; -import io.github.linpeilie.annotations.AutoMapper; -import lombok.Data; - -import java.util.Date; - - -/** - * dome管理视图对象 sys_demo - * - * @author ageerle - * @date Sat Aug 09 21:38:09 CST 2025 - */ -@Data -@ExcelIgnoreUnannotated -@AutoMapper(target = SysDemo.class) -public class SysDemoVo implements Serializable { - - /** - * ID - */ - @ExcelProperty(value = "ID") - private Integer id; - - /** - * 系统代码 - */ - @ExcelProperty(value = "系统代码") - private String sysCode; - - /** - * 系统名称 - */ - @ExcelProperty(value = "系统名称") - private String sysName; - - /** - * 系统状态 - */ - @ExcelProperty(value = "系统状态") - private Integer sysStatus; - - /** - * 创建部门 - */ - @ExcelProperty(value = "创建部门") - private Integer createDept; - - /** - * 创建者 - */ - @ExcelProperty(value = "创建者") - private Integer createBy; - - /** - * 创建时间 - */ - @ExcelProperty(value = "创建时间") - private Date createTime; - - /** - * 更新者 - */ - @ExcelProperty(value = "更新者") - private Integer updateBy; - - /** - * 更新时间 - */ - @ExcelProperty(value = "更新时间") - private Date updateTime; - - /** - * 备注 - */ - @ExcelProperty(value = "备注") - private String remark; - - -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/SysDemoMapper.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/SysDemoMapper.java deleted file mode 100644 index 94351479..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/SysDemoMapper.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.ruoyi.system.mapper; - -import org.ruoyi.system.domain.SysDemo; -import org.ruoyi.system.domain.vo.SysDemoVo; -import org.ruoyi.core.mapper.BaseMapperPlus; -import org.apache.ibatis.annotations.Mapper; - -/** - * dome管理Mapper接口 - * - * @author ageerle - * @date Sat Aug 09 21:38:09 CST 2025 - */ -@Mapper -public interface SysDemoMapper extends BaseMapperPlus { - -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/SysDemoService.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/SysDemoService.java deleted file mode 100644 index ae7f4901..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/SysDemoService.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.ruoyi.system.service; - -import org.ruoyi.system.domain.vo.SysDemoVo; -import org.ruoyi.system.domain.bo.SysDemoBo; - import org.ruoyi.core.page.TableDataInfo; - import org.ruoyi.core.page.PageQuery; - -import java.util.Collection; -import java.util.List; - -/** - * dome管理Service接口 - * - * @author ageerle - * @date Sat Aug 09 21:38:09 CST 2025 - */ -public interface SysDemoService { - - /** - * 查询dome管理 - */ - SysDemoVo queryById(Integer id); - - /** - * 查询dome管理列表 - */ - TableDataInfo queryPageList(SysDemoBo bo, PageQuery pageQuery); - - /** - * 查询dome管理列表 - */ - List queryList(SysDemoBo bo); - - /** - * 新增dome管理 - */ - Boolean insertByBo(SysDemoBo bo); - - /** - * 修改dome管理 - */ - Boolean updateByBo(SysDemoBo bo); - - /** - * 校验并批量删除dome管理信息 - */ - Boolean deleteWithValidByIds(Collection ids, Boolean isValid); -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/SysDemoServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/SysDemoServiceImpl.java deleted file mode 100644 index a0bd9054..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/SysDemoServiceImpl.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.ruoyi.system.service.impl; - -import org.ruoyi.common.core.utils.MapstructUtils; - import org.ruoyi.core.page.TableDataInfo; - import org.ruoyi.core.page.PageQuery; - import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.ruoyi.system.domain.bo.SysDemoBo; -import org.ruoyi.system.domain.vo.SysDemoVo; -import org.ruoyi.system.domain.SysDemo; -import org.ruoyi.system.mapper.SysDemoMapper; -import org.ruoyi.system.service.SysDemoService; -import org.ruoyi.common.core.utils.StringUtils; - -import java.util.List; -import java.util.Map; -import java.util.Collection; - -/** - * dome管理Service业务层处理 - * - * @author ageerle - * @date Sat Aug 09 21:38:09 CST 2025 - */ -@RequiredArgsConstructor -@Service -public class SysDemoServiceImpl implements SysDemoService { - - private final SysDemoMapper baseMapper; - - /** - * 查询dome管理 - */ - @Override - public SysDemoVo queryById(Integer id) { - return baseMapper.selectVoById(id); - } - - /** - * 查询dome管理列表 - */ - @Override - public TableDataInfo queryPageList(SysDemoBo bo, PageQuery pageQuery) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); - return TableDataInfo.build(result); - } - - /** - * 查询dome管理列表 - */ - @Override - public List queryList(SysDemoBo bo) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - return baseMapper.selectVoList(lqw); - } - - private LambdaQueryWrapper buildQueryWrapper(SysDemoBo bo) { - LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.eq(StringUtils.isNotBlank(bo.getSysCode()), SysDemo::getSysCode, bo.getSysCode()); - lqw.eq(StringUtils.isNotBlank(bo.getSysName()), SysDemo::getSysName, bo.getSysName()); - lqw.eq(bo.getSysStatus() != null, SysDemo::getSysStatus, bo.getSysStatus()); - lqw.eq(bo.getCreateDept() != null, SysDemo::getCreateDept, bo.getCreateDept()); - lqw.eq(bo.getCreateBy() != null, SysDemo::getCreateBy, bo.getCreateBy()); - lqw.eq(bo.getCreateTime() != null, SysDemo::getCreateTime, bo.getCreateTime()); - lqw.eq(bo.getUpdateBy() != null, SysDemo::getUpdateBy, bo.getUpdateBy()); - lqw.eq(bo.getUpdateTime() != null, SysDemo::getUpdateTime, bo.getUpdateTime()); - lqw.eq(StringUtils.isNotBlank(bo.getRemark()), SysDemo::getRemark, bo.getRemark()); - return lqw; - } - - /** - * 新增dome管理 - */ - @Override - public Boolean insertByBo(SysDemoBo bo) { - SysDemo add = MapstructUtils.convert(bo, SysDemo. class); - validEntityBeforeSave(add); - boolean flag = baseMapper.insert(add) > 0; - if (flag) { - bo.setId(add.getId()); - } - return flag; - } - - /** - * 修改dome管理 - */ - @Override - public Boolean updateByBo(SysDemoBo bo) { - SysDemo update = MapstructUtils.convert(bo, SysDemo. class); - validEntityBeforeSave(update); - return baseMapper.updateById(update) > 0; - } - - /** - * 保存前的数据校验 - */ - private void validEntityBeforeSave(SysDemo entity) { - //TODO 做一些数据校验,如唯一约束 - } - - /** - * 批量删除dome管理 - */ - @Override - public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { - if (isValid) { - //TODO 做一些业务上的校验,判断是否需要校验 - } - return baseMapper.deleteBatchIds(ids) > 0; - } -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/sys_demo_menu.sql b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/sys_demo_menu.sql deleted file mode 100644 index 5b19392a..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/sys_demo_menu.sql +++ /dev/null @@ -1,19 +0,0 @@ --- 菜单 SQL -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1954175368920645632, 'dome管理', '2000', '1', 'sysDemo', 'system/sysDemo/index', 1, 0, 'C', '0', '0', 'system:sysDemo:list', '#', 103, 1, sysdate(), null, null, 'dome管理菜单'); - --- 按钮 SQL -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1954175368920645633, 'dome管理查询', 1954175368920645632, '1', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:query', '#', 103, 1, sysdate(), null, null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1954175368920645634, 'dome管理新增', 1954175368920645632, '2', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:add', '#', 103, 1, sysdate(), null, null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1954175368920645635, 'dome管理修改', 1954175368920645632, '3', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:edit', '#', 103, 1, sysdate(), null, null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1954175368920645636, 'dome管理删除', 1954175368920645632, '4', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:remove', '#', 103, 1, sysdate(), null, null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1954175368920645637, 'dome管理导出', 1954175368920645632, '5', '#', '', 1, 0, 'F', '0', '0', 'system:sysDemo:export', '#', 103, 1, sysdate(), null, null, ''); From e51425a951568652169527c6792e3daa973b6e5e Mon Sep 17 00:00:00 2001 From: fy53888 Date: Mon, 18 Aug 2025 19:56:26 +0800 Subject: [PATCH 15/36] =?UTF-8?q?=E5=A4=87=E5=88=86=E4=B8=80=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-admin/src/main/resources/application-dev.yml | 4 ++-- ruoyi-admin/src/main/resources/application.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 0b1661e2..15ed79b8 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -16,9 +16,9 @@ spring: master: type: ${spring.datasource.type} driverClassName: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true + url: jdbc:mysql://127.0.0.1:3306/ruoyistore?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true username: root - password: 123456 + password: root hikari: # 最大连接池数量 diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index f6abd846..1992a5ab 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -162,7 +162,7 @@ tenant: - sys_user_role knowledge-role: - enable: true + enable: false # MyBatisPlus配置 # https://baomidou.com/config/ From 62676a54fb0d94168b8d75de99295275e35b7003 Mon Sep 17 00:00:00 2001 From: fy53888 Date: Mon, 18 Aug 2025 22:03:51 +0800 Subject: [PATCH 16/36] =?UTF-8?q?=E5=A4=87=E5=88=86=E4=B8=80=E4=B8=8B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StoreEmployeeController.java | 105 +++++++++++++++++ .../ruoyi/system/domain/StoreEmployee.java | 56 +++++++++ .../system/domain/bo/StoreEmployeeBo.java | 59 ++++++++++ .../system/domain/vo/StoreEmployeeVo.java | 59 ++++++++++ .../system/mapper/StoreEmployeeMapper.java | 17 +++ .../system/service/StoreEmployeeService.java | 48 ++++++++ .../impl/StoreEmployeeServiceImpl.java | 109 ++++++++++++++++++ .../ruoyi/system/sql/store_employee_menu.sql | 19 +++ .../mapper/system/StoreEmployeeMapper.xml | 7 ++ 9 files changed, 479 insertions(+) create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/StoreEmployeeController.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/StoreEmployee.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/StoreEmployeeBo.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/StoreEmployeeMapper.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/StoreEmployeeService.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/StoreEmployeeServiceImpl.java create mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/store_employee_menu.sql create mode 100644 ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/StoreEmployeeMapper.xml diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/StoreEmployeeController.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/StoreEmployeeController.java new file mode 100644 index 00000000..6b59cb93 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/StoreEmployeeController.java @@ -0,0 +1,105 @@ +package org.ruoyi.system.controller; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.ruoyi.common.idempotent.annotation.RepeatSubmit; +import org.ruoyi.common.log.annotation.Log; +import org.ruoyi.common.web.core.BaseController; +import org.ruoyi.core.page.PageQuery; +import org.ruoyi.common.core.domain.R; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; +import org.ruoyi.common.log.enums.BusinessType; +import org.ruoyi.common.excel.utils.ExcelUtil; +import org.ruoyi.system.domain.vo.StoreEmployeeVo; +import org.ruoyi.system.domain.bo.StoreEmployeeBo; +import org.ruoyi.system.service.StoreEmployeeService; +import org.ruoyi.core.page.TableDataInfo; + +/** + * 员工分配 + * + * @author ageerle + * @date Mon Aug 18 21:33:27 CST 2025 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/store/storeEmployee") +public class StoreEmployeeController extends BaseController { + + private final StoreEmployeeService storeEmployeeService; + +/** + * 查询员工分配列表 + */ +@SaCheckPermission("store:storeEmployee:list") +@GetMapping("/list") + public TableDataInfo list(StoreEmployeeBo bo, PageQuery pageQuery) { + return storeEmployeeService.queryPageList(bo, pageQuery); + } + + /** + * 导出员工分配列表 + */ + @SaCheckPermission("store:storeEmployee:export") + @Log(title = "员工分配", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(StoreEmployeeBo bo, HttpServletResponse response) { + List list = storeEmployeeService.queryList(bo); + ExcelUtil.exportExcel(list, "员工分配", StoreEmployeeVo.class, response); + } + + /** + * 获取员工分配详细信息 + * + * @param id 主键 + */ + @SaCheckPermission("store:storeEmployee:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Long id) { + return R.ok(storeEmployeeService.queryById(id)); + } + + /** + * 新增员工分配 + */ + @SaCheckPermission("store:storeEmployee:add") + @Log(title = "员工分配", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody StoreEmployeeBo bo) { + return toAjax(storeEmployeeService.insertByBo(bo)); + } + + /** + * 修改员工分配 + */ + @SaCheckPermission("store:storeEmployee:edit") + @Log(title = "员工分配", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody StoreEmployeeBo bo) { + return toAjax(storeEmployeeService.updateByBo(bo)); + } + + /** + * 删除员工分配 + * + * @param ids 主键串 + */ + @SaCheckPermission("store:storeEmployee:remove") + @Log(title = "员工分配", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] ids) { + return toAjax(storeEmployeeService.deleteWithValidByIds(List.of(ids), true)); + } +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/StoreEmployee.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/StoreEmployee.java new file mode 100644 index 00000000..dff96453 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/StoreEmployee.java @@ -0,0 +1,56 @@ +package org.ruoyi.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; +import java.io.Serializable; + +/** + * 员工分配对象 store_employee + * + * @author ageerle + * @date Mon Aug 18 21:33:27 CST 2025 + */ +@Data +@TableName("store_employee") +public class StoreEmployee implements Serializable { + + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 门店ID + */ + private Long storeId; + + /** + * 员工ID + */ + private Long userId; + + /** + * 职位 + */ + private String roleInStore; + + /** + * 分配时间 + */ + private LocalDateTime assignTime; + + /** + * 门店类型 + */ + private String isPrimary; + + /** + * 分配到期时间 + */ + private LocalDateTime expireTime; + + +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/StoreEmployeeBo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/StoreEmployeeBo.java new file mode 100644 index 00000000..40cdbd96 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/StoreEmployeeBo.java @@ -0,0 +1,59 @@ +package org.ruoyi.system.domain.bo; + +import org.ruoyi.system.domain.StoreEmployee; +import org.ruoyi.core.domain.BaseEntity; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; +import java.time.LocalDateTime; +import java.io.Serializable; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; +import java.io.Serializable; +import java.io.Serializable; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; + +/** + * 员工分配业务对象 store_employee + * + * @author ageerle + * @date Mon Aug 18 21:33:27 CST 2025 + */ +@Data + +@AutoMapper(target = StoreEmployee.class, reverseConvertGenerate = false) +public class StoreEmployeeBo implements Serializable { + + private Long id; + + /** + * 门店ID + */ + @NotNull(message = "门店ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long storeId; + /** + * 员工ID + */ + @NotNull(message = "员工ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long userId; + /** + * 职位 + */ + private String roleInStore; + /** + * 分配时间 + */ + @NotNull(message = "分配时间不能为空", groups = { AddGroup.class, EditGroup.class }) + private LocalDateTime assignTime; + /** + * 门店类型 + */ + private String isPrimary; + /** + * 分配到期时间 + */ + private LocalDateTime expireTime; + +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java new file mode 100644 index 00000000..0b8b0248 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java @@ -0,0 +1,59 @@ +package org.ruoyi.system.domain.vo; + + import java.time.LocalDateTime; + import java.io.Serializable; +import org.ruoyi.system.domain.StoreEmployee; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; + +import java.util.Date; + + +/** + * 员工分配视图对象 store_employee + * + * @author ageerle + * @date Mon Aug 18 21:33:27 CST 2025 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = StoreEmployee.class) +public class StoreEmployeeVo implements Serializable { + + private Long id; + /** + * 门店ID + */ + @ExcelProperty(value = "门店ID") + private Long storeId; + /** + * 员工ID + */ + @ExcelProperty(value = "员工ID") + private Long userId; + /** + * 职位 + */ + @ExcelProperty(value = "职位", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "title_name") + private String roleInStore; + /** + * 分配时间 + */ + @ExcelProperty(value = "分配时间") + private LocalDateTime assignTime; + /** + * 门店类型 + */ + @ExcelProperty(value = "门店类型", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "store_type") + private String isPrimary; + /** + * 分配到期时间 + */ + @ExcelProperty(value = "分配到期时间") + private LocalDateTime expireTime; + +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/StoreEmployeeMapper.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/StoreEmployeeMapper.java new file mode 100644 index 00000000..a73e581f --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/StoreEmployeeMapper.java @@ -0,0 +1,17 @@ +package org.ruoyi.system.mapper; + +import org.ruoyi.system.domain.StoreEmployee; +import org.ruoyi.system.domain.vo.StoreEmployeeVo; +import org.ruoyi.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Mapper; + +/** + * 员工分配Mapper接口 + * + * @author ageerle + * @date Mon Aug 18 21:33:27 CST 2025 + */ +@Mapper +public interface StoreEmployeeMapper extends BaseMapperPlus { + +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/StoreEmployeeService.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/StoreEmployeeService.java new file mode 100644 index 00000000..5235c942 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/StoreEmployeeService.java @@ -0,0 +1,48 @@ +package org.ruoyi.system.service; + +import org.ruoyi.system.domain.vo.StoreEmployeeVo; +import org.ruoyi.system.domain.bo.StoreEmployeeBo; + import org.ruoyi.core.page.TableDataInfo; + import org.ruoyi.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * 员工分配Service接口 + * + * @author ageerle + * @date Mon Aug 18 21:33:27 CST 2025 + */ +public interface StoreEmployeeService { + + /** + * 查询员工分配 + */ + StoreEmployeeVo queryById(Long id); + + /** + * 查询员工分配列表 + */ + TableDataInfo queryPageList(StoreEmployeeBo bo, PageQuery pageQuery); + + /** + * 查询员工分配列表 + */ + List queryList(StoreEmployeeBo bo); + + /** + * 新增员工分配 + */ + Boolean insertByBo(StoreEmployeeBo bo); + + /** + * 修改员工分配 + */ + Boolean updateByBo(StoreEmployeeBo bo); + + /** + * 校验并批量删除员工分配信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/StoreEmployeeServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/StoreEmployeeServiceImpl.java new file mode 100644 index 00000000..44b8ec86 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/StoreEmployeeServiceImpl.java @@ -0,0 +1,109 @@ +package org.ruoyi.system.service.impl; + +import org.ruoyi.common.core.utils.MapstructUtils; + import org.ruoyi.core.page.TableDataInfo; + import org.ruoyi.core.page.PageQuery; + import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.ruoyi.system.domain.bo.StoreEmployeeBo; +import org.ruoyi.system.domain.vo.StoreEmployeeVo; +import org.ruoyi.system.domain.StoreEmployee; +import org.ruoyi.system.mapper.StoreEmployeeMapper; +import org.ruoyi.system.service.StoreEmployeeService; +import org.ruoyi.common.core.utils.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.Collection; + +/** + * 员工分配Service业务层处理 + * + * @author ageerle + * @date Mon Aug 18 21:33:27 CST 2025 + */ +@RequiredArgsConstructor +@Service +public class StoreEmployeeServiceImpl implements StoreEmployeeService { + + private final StoreEmployeeMapper baseMapper; + + /** + * 查询员工分配 + */ + @Override + public StoreEmployeeVo queryById(Long id) { + return baseMapper.selectVoById(id); + } + + /** + * 查询员工分配列表 + */ + @Override + public TableDataInfo queryPageList(StoreEmployeeBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询员工分配列表 + */ + @Override + public List queryList(StoreEmployeeBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(StoreEmployeeBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getStoreId() != null, StoreEmployee::getStoreId, bo.getStoreId()); + lqw.eq(bo.getUserId() != null, StoreEmployee::getUserId, bo.getUserId()); + return lqw; + } + + /** + * 新增员工分配 + */ + @Override + public Boolean insertByBo(StoreEmployeeBo bo) { + StoreEmployee add = MapstructUtils.convert(bo, StoreEmployee. class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改员工分配 + */ + @Override + public Boolean updateByBo(StoreEmployeeBo bo) { + StoreEmployee update = MapstructUtils.convert(bo, StoreEmployee. class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(StoreEmployee entity) { + //TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除员工分配 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } +} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/store_employee_menu.sql b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/store_employee_menu.sql new file mode 100644 index 00000000..6277f1db --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/store_employee_menu.sql @@ -0,0 +1,19 @@ +-- 菜单 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1957435675864506368, '员工分配', '2000', '1', 'storeEmployee', 'store/storeEmployee/index', 1, 0, 'C', '0', '0', 'store:storeEmployee:list', '#', 103, 1, sysdate(), null, null, '员工分配菜单'); + +-- 按钮 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1957435675864506369, '员工分配查询', 1957435675864506368, '1', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:query', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1957435675864506370, '员工分配新增', 1957435675864506368, '2', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:add', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1957435675864506371, '员工分配修改', 1957435675864506368, '3', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:edit', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1957435675864506372, '员工分配删除', 1957435675864506368, '4', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:remove', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1957435675864506373, '员工分配导出', 1957435675864506368, '5', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:export', '#', 103, 1, sysdate(), null, null, ''); diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/StoreEmployeeMapper.xml b/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/StoreEmployeeMapper.xml new file mode 100644 index 00000000..82d21190 --- /dev/null +++ b/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/StoreEmployeeMapper.xml @@ -0,0 +1,7 @@ + + + + + From a1c7b86e72ebf86b5517157b21eafb35e5418ee9 Mon Sep 17 00:00:00 2001 From: fy53888 Date: Mon, 18 Aug 2025 22:04:15 +0800 Subject: [PATCH 17/36] =?UTF-8?q?=E5=A4=87=E5=88=86=E4=B8=80=E4=B8=8B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java index 0b8b0248..a9123c2e 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java @@ -2,7 +2,9 @@ package org.ruoyi.system.domain.vo; import java.time.LocalDateTime; import java.io.Serializable; -import org.ruoyi.system.domain.StoreEmployee; + import org.ruoyi.common.excel.annotation.ExcelDictFormat; + import org.ruoyi.common.excel.convert.ExcelDictConvert; + import org.ruoyi.system.domain.StoreEmployee; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.github.linpeilie.annotations.AutoMapper; From 00f9a1a55b3a09e185c21928bb9883c4842c7aa6 Mon Sep 17 00:00:00 2001 From: fy53888 Date: Tue, 19 Aug 2025 09:48:35 +0800 Subject: [PATCH 18/36] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E4=B8=8B=E6=8B=89=E5=B8=A6=E6=9F=A5=E6=89=BE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/system/service/ISysDictTypeService.java | 9 +++++++++ .../system/service/impl/SysDictTypeServiceImpl.java | 13 +++++++++++++ .../controller/system/SysDictTypeController.java | 8 ++++++++ 3 files changed, 30 insertions(+) diff --git a/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/service/ISysDictTypeService.java b/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/service/ISysDictTypeService.java index fb94c017..7a420805 100644 --- a/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/service/ISysDictTypeService.java +++ b/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/service/ISysDictTypeService.java @@ -16,6 +16,15 @@ import java.util.List; public interface ISysDictTypeService { + +/** + * Select all dictionary types based on the specified conditions + * + * @param dictType The business object containing query conditions for dictionary types + * @return TableDataInfo containing a list of SysDictTypeVo objects that match the query criteria + */ + TableDataInfo selectAll(SysDictTypeBo dictType); + TableDataInfo selectPageDictTypeList(SysDictTypeBo dictType, PageQuery pageQuery); /** diff --git a/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/service/impl/SysDictTypeServiceImpl.java index 6797e200..518048a9 100644 --- a/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/service/impl/SysDictTypeServiceImpl.java +++ b/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/service/impl/SysDictTypeServiceImpl.java @@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; import org.ruoyi.common.core.constant.CacheConstants; import org.ruoyi.common.core.constant.CacheNames; +import org.ruoyi.common.core.constant.HttpStatus; import org.ruoyi.common.core.exception.ServiceException; import org.ruoyi.common.core.service.DictService; import org.ruoyi.common.core.utils.MapstructUtils; @@ -50,6 +51,18 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService private final SysDictTypeMapper baseMapper; private final SysDictDataMapper dictDataMapper; + @Override + public TableDataInfo selectAll(SysDictTypeBo dictType) { + LambdaQueryWrapper lqw = buildQueryWrapper(dictType); + // 2. 查询所有数据(不分页) + List list = baseMapper.selectVoList(lqw); + TableDataInfo rspData = new TableDataInfo<>(); + rspData.setCode(HttpStatus.SUCCESS); // 200 + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal(list.size()); // 总数为列表大小 + return rspData; + } @Override public TableDataInfo selectPageDictTypeList(SysDictTypeBo dictType, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(dictType); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/SysDictTypeController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/SysDictTypeController.java index 9a1a09ff..71dc63e6 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/SysDictTypeController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/SysDictTypeController.java @@ -31,6 +31,14 @@ public class SysDictTypeController extends BaseController { private final ISysDictTypeService dictTypeService; + /** + * 查询所有字典类型列表 + */ + + @GetMapping("/all") + public TableDataInfo all(SysDictTypeBo dictType, PageQuery pageQuery) { + return dictTypeService.selectAll(dictType); + } /** * 查询字典类型列表 */ From b696fde881ac3b939b06b3009b32f5bb256779b6 Mon Sep 17 00:00:00 2001 From: l90215 Date: Tue, 19 Aug 2025 12:43:34 +0800 Subject: [PATCH 19/36] =?UTF-8?q?feat:=20=E5=90=88=E5=B9=B6=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=20=E5=88=A0=E9=99=A4=E4=B8=8D=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E7=9A=84=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yml | 2 +- ruoyi-modules/ruoyi-generator/pom.xml | 4 - .../controller/StoreEmployeeController.java | 105 ----------------- .../ruoyi/system/domain/StoreEmployee.java | 56 --------- .../system/domain/bo/StoreEmployeeBo.java | 59 ---------- .../system/domain/vo/StoreEmployeeVo.java | 61 ---------- .../system/mapper/StoreEmployeeMapper.java | 17 --- .../system/service/StoreEmployeeService.java | 48 -------- .../impl/StoreEmployeeServiceImpl.java | 109 ------------------ .../ruoyi/system/sql/store_employee_menu.sql | 19 --- .../mapper/system/StoreEmployeeMapper.xml | 7 -- .../resources/mapper/system/SysDemoMapper.xml | 7 -- 12 files changed, 1 insertion(+), 493 deletions(-) delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/StoreEmployeeController.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/StoreEmployee.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/StoreEmployeeBo.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/StoreEmployeeMapper.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/StoreEmployeeService.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/StoreEmployeeServiceImpl.java delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/store_employee_menu.sql delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/StoreEmployeeMapper.xml delete mode 100644 ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/SysDemoMapper.xml diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 15ed79b8..8abf1a68 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -16,7 +16,7 @@ spring: master: type: ${spring.datasource.type} driverClassName: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://127.0.0.1:3306/ruoyistore?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true + url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true username: root password: root diff --git a/ruoyi-modules/ruoyi-generator/pom.xml b/ruoyi-modules/ruoyi-generator/pom.xml index e545d91b..c6ab5e3c 100644 --- a/ruoyi-modules/ruoyi-generator/pom.xml +++ b/ruoyi-modules/ruoyi-generator/pom.xml @@ -48,10 +48,6 @@ org.apache.velocity velocity-engine-core - - org.ruoyi - ruoyi-common-excel - diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/StoreEmployeeController.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/StoreEmployeeController.java deleted file mode 100644 index 6b59cb93..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/controller/StoreEmployeeController.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.ruoyi.system.controller; - -import java.util.List; - -import lombok.RequiredArgsConstructor; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.constraints.*; -import cn.dev33.satoken.annotation.SaCheckPermission; -import org.springframework.web.bind.annotation.*; -import org.springframework.validation.annotation.Validated; -import org.ruoyi.common.idempotent.annotation.RepeatSubmit; -import org.ruoyi.common.log.annotation.Log; -import org.ruoyi.common.web.core.BaseController; -import org.ruoyi.core.page.PageQuery; -import org.ruoyi.common.core.domain.R; -import org.ruoyi.common.core.validate.AddGroup; -import org.ruoyi.common.core.validate.EditGroup; -import org.ruoyi.common.log.enums.BusinessType; -import org.ruoyi.common.excel.utils.ExcelUtil; -import org.ruoyi.system.domain.vo.StoreEmployeeVo; -import org.ruoyi.system.domain.bo.StoreEmployeeBo; -import org.ruoyi.system.service.StoreEmployeeService; -import org.ruoyi.core.page.TableDataInfo; - -/** - * 员工分配 - * - * @author ageerle - * @date Mon Aug 18 21:33:27 CST 2025 - */ -@Validated -@RequiredArgsConstructor -@RestController -@RequestMapping("/store/storeEmployee") -public class StoreEmployeeController extends BaseController { - - private final StoreEmployeeService storeEmployeeService; - -/** - * 查询员工分配列表 - */ -@SaCheckPermission("store:storeEmployee:list") -@GetMapping("/list") - public TableDataInfo list(StoreEmployeeBo bo, PageQuery pageQuery) { - return storeEmployeeService.queryPageList(bo, pageQuery); - } - - /** - * 导出员工分配列表 - */ - @SaCheckPermission("store:storeEmployee:export") - @Log(title = "员工分配", businessType = BusinessType.EXPORT) - @PostMapping("/export") - public void export(StoreEmployeeBo bo, HttpServletResponse response) { - List list = storeEmployeeService.queryList(bo); - ExcelUtil.exportExcel(list, "员工分配", StoreEmployeeVo.class, response); - } - - /** - * 获取员工分配详细信息 - * - * @param id 主键 - */ - @SaCheckPermission("store:storeEmployee:query") - @GetMapping("/{id}") - public R getInfo(@NotNull(message = "主键不能为空") - @PathVariable Long id) { - return R.ok(storeEmployeeService.queryById(id)); - } - - /** - * 新增员工分配 - */ - @SaCheckPermission("store:storeEmployee:add") - @Log(title = "员工分配", businessType = BusinessType.INSERT) - @RepeatSubmit() - @PostMapping() - public R add(@Validated(AddGroup.class) @RequestBody StoreEmployeeBo bo) { - return toAjax(storeEmployeeService.insertByBo(bo)); - } - - /** - * 修改员工分配 - */ - @SaCheckPermission("store:storeEmployee:edit") - @Log(title = "员工分配", businessType = BusinessType.UPDATE) - @RepeatSubmit() - @PutMapping() - public R edit(@Validated(EditGroup.class) @RequestBody StoreEmployeeBo bo) { - return toAjax(storeEmployeeService.updateByBo(bo)); - } - - /** - * 删除员工分配 - * - * @param ids 主键串 - */ - @SaCheckPermission("store:storeEmployee:remove") - @Log(title = "员工分配", businessType = BusinessType.DELETE) - @DeleteMapping("/{ids}") - public R remove(@NotEmpty(message = "主键不能为空") - @PathVariable Long[] ids) { - return toAjax(storeEmployeeService.deleteWithValidByIds(List.of(ids), true)); - } -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/StoreEmployee.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/StoreEmployee.java deleted file mode 100644 index dff96453..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/StoreEmployee.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.ruoyi.system.domain; - -import com.baomidou.mybatisplus.annotation.*; -import lombok.Data; -import java.time.LocalDateTime; -import java.io.Serializable; - -/** - * 员工分配对象 store_employee - * - * @author ageerle - * @date Mon Aug 18 21:33:27 CST 2025 - */ -@Data -@TableName("store_employee") -public class StoreEmployee implements Serializable { - - - /** - * 主键ID - */ - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - /** - * 门店ID - */ - private Long storeId; - - /** - * 员工ID - */ - private Long userId; - - /** - * 职位 - */ - private String roleInStore; - - /** - * 分配时间 - */ - private LocalDateTime assignTime; - - /** - * 门店类型 - */ - private String isPrimary; - - /** - * 分配到期时间 - */ - private LocalDateTime expireTime; - - -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/StoreEmployeeBo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/StoreEmployeeBo.java deleted file mode 100644 index 40cdbd96..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/bo/StoreEmployeeBo.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.ruoyi.system.domain.bo; - -import org.ruoyi.system.domain.StoreEmployee; -import org.ruoyi.core.domain.BaseEntity; -import io.github.linpeilie.annotations.AutoMapper; -import lombok.Data; -import lombok.EqualsAndHashCode; -import jakarta.validation.constraints.*; -import java.time.LocalDateTime; -import java.io.Serializable; -import org.ruoyi.common.core.validate.AddGroup; -import org.ruoyi.common.core.validate.EditGroup; -import java.io.Serializable; -import java.io.Serializable; -import org.ruoyi.common.core.validate.AddGroup; -import org.ruoyi.common.core.validate.EditGroup; - -/** - * 员工分配业务对象 store_employee - * - * @author ageerle - * @date Mon Aug 18 21:33:27 CST 2025 - */ -@Data - -@AutoMapper(target = StoreEmployee.class, reverseConvertGenerate = false) -public class StoreEmployeeBo implements Serializable { - - private Long id; - - /** - * 门店ID - */ - @NotNull(message = "门店ID不能为空", groups = { AddGroup.class, EditGroup.class }) - private Long storeId; - /** - * 员工ID - */ - @NotNull(message = "员工ID不能为空", groups = { AddGroup.class, EditGroup.class }) - private Long userId; - /** - * 职位 - */ - private String roleInStore; - /** - * 分配时间 - */ - @NotNull(message = "分配时间不能为空", groups = { AddGroup.class, EditGroup.class }) - private LocalDateTime assignTime; - /** - * 门店类型 - */ - private String isPrimary; - /** - * 分配到期时间 - */ - private LocalDateTime expireTime; - -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java deleted file mode 100644 index a9123c2e..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/domain/vo/StoreEmployeeVo.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.ruoyi.system.domain.vo; - - import java.time.LocalDateTime; - import java.io.Serializable; - import org.ruoyi.common.excel.annotation.ExcelDictFormat; - import org.ruoyi.common.excel.convert.ExcelDictConvert; - import org.ruoyi.system.domain.StoreEmployee; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; -import io.github.linpeilie.annotations.AutoMapper; -import lombok.Data; - -import java.util.Date; - - -/** - * 员工分配视图对象 store_employee - * - * @author ageerle - * @date Mon Aug 18 21:33:27 CST 2025 - */ -@Data -@ExcelIgnoreUnannotated -@AutoMapper(target = StoreEmployee.class) -public class StoreEmployeeVo implements Serializable { - - private Long id; - /** - * 门店ID - */ - @ExcelProperty(value = "门店ID") - private Long storeId; - /** - * 员工ID - */ - @ExcelProperty(value = "员工ID") - private Long userId; - /** - * 职位 - */ - @ExcelProperty(value = "职位", converter = ExcelDictConvert.class) - @ExcelDictFormat(dictType = "title_name") - private String roleInStore; - /** - * 分配时间 - */ - @ExcelProperty(value = "分配时间") - private LocalDateTime assignTime; - /** - * 门店类型 - */ - @ExcelProperty(value = "门店类型", converter = ExcelDictConvert.class) - @ExcelDictFormat(dictType = "store_type") - private String isPrimary; - /** - * 分配到期时间 - */ - @ExcelProperty(value = "分配到期时间") - private LocalDateTime expireTime; - -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/StoreEmployeeMapper.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/StoreEmployeeMapper.java deleted file mode 100644 index a73e581f..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/mapper/StoreEmployeeMapper.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.ruoyi.system.mapper; - -import org.ruoyi.system.domain.StoreEmployee; -import org.ruoyi.system.domain.vo.StoreEmployeeVo; -import org.ruoyi.core.mapper.BaseMapperPlus; -import org.apache.ibatis.annotations.Mapper; - -/** - * 员工分配Mapper接口 - * - * @author ageerle - * @date Mon Aug 18 21:33:27 CST 2025 - */ -@Mapper -public interface StoreEmployeeMapper extends BaseMapperPlus { - -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/StoreEmployeeService.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/StoreEmployeeService.java deleted file mode 100644 index 5235c942..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/StoreEmployeeService.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.ruoyi.system.service; - -import org.ruoyi.system.domain.vo.StoreEmployeeVo; -import org.ruoyi.system.domain.bo.StoreEmployeeBo; - import org.ruoyi.core.page.TableDataInfo; - import org.ruoyi.core.page.PageQuery; - -import java.util.Collection; -import java.util.List; - -/** - * 员工分配Service接口 - * - * @author ageerle - * @date Mon Aug 18 21:33:27 CST 2025 - */ -public interface StoreEmployeeService { - - /** - * 查询员工分配 - */ - StoreEmployeeVo queryById(Long id); - - /** - * 查询员工分配列表 - */ - TableDataInfo queryPageList(StoreEmployeeBo bo, PageQuery pageQuery); - - /** - * 查询员工分配列表 - */ - List queryList(StoreEmployeeBo bo); - - /** - * 新增员工分配 - */ - Boolean insertByBo(StoreEmployeeBo bo); - - /** - * 修改员工分配 - */ - Boolean updateByBo(StoreEmployeeBo bo); - - /** - * 校验并批量删除员工分配信息 - */ - Boolean deleteWithValidByIds(Collection ids, Boolean isValid); -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/StoreEmployeeServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/StoreEmployeeServiceImpl.java deleted file mode 100644 index 44b8ec86..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/service/impl/StoreEmployeeServiceImpl.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.ruoyi.system.service.impl; - -import org.ruoyi.common.core.utils.MapstructUtils; - import org.ruoyi.core.page.TableDataInfo; - import org.ruoyi.core.page.PageQuery; - import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.ruoyi.system.domain.bo.StoreEmployeeBo; -import org.ruoyi.system.domain.vo.StoreEmployeeVo; -import org.ruoyi.system.domain.StoreEmployee; -import org.ruoyi.system.mapper.StoreEmployeeMapper; -import org.ruoyi.system.service.StoreEmployeeService; -import org.ruoyi.common.core.utils.StringUtils; - -import java.util.List; -import java.util.Map; -import java.util.Collection; - -/** - * 员工分配Service业务层处理 - * - * @author ageerle - * @date Mon Aug 18 21:33:27 CST 2025 - */ -@RequiredArgsConstructor -@Service -public class StoreEmployeeServiceImpl implements StoreEmployeeService { - - private final StoreEmployeeMapper baseMapper; - - /** - * 查询员工分配 - */ - @Override - public StoreEmployeeVo queryById(Long id) { - return baseMapper.selectVoById(id); - } - - /** - * 查询员工分配列表 - */ - @Override - public TableDataInfo queryPageList(StoreEmployeeBo bo, PageQuery pageQuery) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); - return TableDataInfo.build(result); - } - - /** - * 查询员工分配列表 - */ - @Override - public List queryList(StoreEmployeeBo bo) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - return baseMapper.selectVoList(lqw); - } - - private LambdaQueryWrapper buildQueryWrapper(StoreEmployeeBo bo) { - LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.eq(bo.getStoreId() != null, StoreEmployee::getStoreId, bo.getStoreId()); - lqw.eq(bo.getUserId() != null, StoreEmployee::getUserId, bo.getUserId()); - return lqw; - } - - /** - * 新增员工分配 - */ - @Override - public Boolean insertByBo(StoreEmployeeBo bo) { - StoreEmployee add = MapstructUtils.convert(bo, StoreEmployee. class); - validEntityBeforeSave(add); - boolean flag = baseMapper.insert(add) > 0; - if (flag) { - bo.setId(add.getId()); - } - return flag; - } - - /** - * 修改员工分配 - */ - @Override - public Boolean updateByBo(StoreEmployeeBo bo) { - StoreEmployee update = MapstructUtils.convert(bo, StoreEmployee. class); - validEntityBeforeSave(update); - return baseMapper.updateById(update) > 0; - } - - /** - * 保存前的数据校验 - */ - private void validEntityBeforeSave(StoreEmployee entity) { - //TODO 做一些数据校验,如唯一约束 - } - - /** - * 批量删除员工分配 - */ - @Override - public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { - if (isValid) { - //TODO 做一些业务上的校验,判断是否需要校验 - } - return baseMapper.deleteBatchIds(ids) > 0; - } -} diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/store_employee_menu.sql b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/store_employee_menu.sql deleted file mode 100644 index 6277f1db..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/system/sql/store_employee_menu.sql +++ /dev/null @@ -1,19 +0,0 @@ --- 菜单 SQL -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1957435675864506368, '员工分配', '2000', '1', 'storeEmployee', 'store/storeEmployee/index', 1, 0, 'C', '0', '0', 'store:storeEmployee:list', '#', 103, 1, sysdate(), null, null, '员工分配菜单'); - --- 按钮 SQL -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1957435675864506369, '员工分配查询', 1957435675864506368, '1', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:query', '#', 103, 1, sysdate(), null, null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1957435675864506370, '员工分配新增', 1957435675864506368, '2', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:add', '#', 103, 1, sysdate(), null, null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1957435675864506371, '员工分配修改', 1957435675864506368, '3', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:edit', '#', 103, 1, sysdate(), null, null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1957435675864506372, '员工分配删除', 1957435675864506368, '4', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:remove', '#', 103, 1, sysdate(), null, null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) -values(1957435675864506373, '员工分配导出', 1957435675864506368, '5', '#', '', 1, 0, 'F', '0', '0', 'store:storeEmployee:export', '#', 103, 1, sysdate(), null, null, ''); diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/StoreEmployeeMapper.xml b/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/StoreEmployeeMapper.xml deleted file mode 100644 index 82d21190..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/StoreEmployeeMapper.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/SysDemoMapper.xml b/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/SysDemoMapper.xml deleted file mode 100644 index c35ba06c..00000000 --- a/ruoyi-modules/ruoyi-generator/src/main/resources/mapper/system/SysDemoMapper.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - From 4f7ad59e4666e511b8a22c740ba7b421c74df72f Mon Sep 17 00:00:00 2001 From: likunlong Date: Mon, 18 Aug 2025 14:30:08 +0800 Subject: [PATCH 20/36] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=8E=B7=E5=8F=96=E9=AB=98=E4=BC=98=E5=85=88=E7=BA=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=92=8C=E6=9C=8D=E5=8A=A1=E7=9A=84=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/ruoyi/domain/ChatModel.java | 5 + .../java/org/ruoyi/domain/bo/ChatModelBo.java | 5 + .../org/ruoyi/service/IChatModelService.java | 6 + .../service/impl/ChatModelServiceImpl.java | 13 ++ .../service/chat/impl/SseServiceImpl.java | 220 +++++++++++++----- 5 files changed, 186 insertions(+), 63 deletions(-) diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java index b708b0a3..e2c75c7e 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java @@ -76,6 +76,11 @@ public class ChatModel extends BaseEntity { */ private String apiKey; + /** + * 优先级 + */ + private Integer priority; + /** * 备注 */ diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatModelBo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatModelBo.java index f333ed0c..b828515b 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatModelBo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatModelBo.java @@ -74,6 +74,11 @@ public class ChatModelBo extends BaseEntity { @NotBlank(message = "请求地址不能为空", groups = { AddGroup.class, EditGroup.class }) private String apiHost; + /** + * 优先级 + */ + private Integer priority; + /** * 密钥 */ diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IChatModelService.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IChatModelService.java index 62dbf970..d93b527e 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IChatModelService.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IChatModelService.java @@ -57,6 +57,12 @@ public interface IChatModelService { * 通过模型分类获取模型信息 */ ChatModelVo selectModelByCategory(String image); + + /** + * 通过模型分类获取优先级最高的模型信息 + */ + ChatModelVo selectModelByCategoryWithHighestPriority(String category); + /** * 获取ppt模型信息 */ diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/ChatModelServiceImpl.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/ChatModelServiceImpl.java index d0b5f5cd..b7acfaa9 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/ChatModelServiceImpl.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/ChatModelServiceImpl.java @@ -136,6 +136,19 @@ public class ChatModelServiceImpl implements IChatModelService { public ChatModelVo selectModelByCategory(String category) { return baseMapper.selectVoOne(Wrappers.lambdaQuery().eq(ChatModel::getCategory, category)); } + + /** + * 通过模型分类获取优先级最高的模型信息 + */ + @Override + public ChatModelVo selectModelByCategoryWithHighestPriority(String category) { + return baseMapper.selectVoOne( + Wrappers.lambdaQuery() + .eq(ChatModel::getCategory, category) + .orderByDesc(ChatModel::getPriority) + .last("LIMIT 1") + ); + } @Override public ChatModel getPPT() { diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index 59c70d41..c61b5673 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -114,8 +114,8 @@ public class SseServiceImpl implements ISseService { chatRequest.setSessionId(chatSessionBo.getId()); } } - // 根据模型分类调用不同的处理逻辑 - IChatService chatService = chatServiceFactory.getChatService(chatModelVo.getCategory()); + // 自动选择模型并获取对应的聊天服务 + IChatService chatService = autoSelectModelAndGetService(chatRequest); chatService.chat(chatRequest, sseEmitter); } catch (Exception e) { log.error(e.getMessage(),e); @@ -124,6 +124,45 @@ public class SseServiceImpl implements ISseService { return sseEmitter; } + /** + * 自动选择模型并获取对应的聊天服务 + */ + private IChatService autoSelectModelAndGetService(ChatRequest chatRequest) { + try { + // 处理特殊模型类型 + if ("gpt-image".equals(chatRequest.getModel())) { + chatModelVo = selectModelByCategory("image"); + return chatServiceFactory.getChatService(chatModelVo.getCategory()); + } + + // 根据模型名称获取模型分类,然后获取该分类下优先级最高的模型 + ChatModelVo tempModel = chatModelService.selectModelByName(chatRequest.getModel()); + if (tempModel == null) { + throw new IllegalStateException("未找到模型名称:" + chatRequest.getModel()); + } + + chatModelVo = selectModelByCategory(tempModel.getCategory()); + + // 直接返回对应的聊天服务 + return chatServiceFactory.getChatService(chatModelVo.getCategory()); + + } catch (Exception e) { + log.error("模型选择和服务获取失败: {}", e.getMessage(), e); + throw new IllegalStateException("模型选择和服务获取失败: " + e.getMessage()); + } + } + + /** + * 根据分类选择优先级最高的模型 + */ + private ChatModelVo selectModelByCategory(String category) { + ChatModelVo model = chatModelService.selectModelByCategoryWithHighestPriority(category); + if (model == null) { + throw new IllegalStateException("未找到" + category + "分类的模型配置"); + } + return model; + } + /** * 获取对话标题 * @@ -145,66 +184,20 @@ public class SseServiceImpl implements ISseService { * 构建消息列表 */ private void buildChatMessageList(ChatRequest chatRequest){ - String sysPrompt; - // 矫正模型名称 如果是gpt-image 则查询image类型模型 获取模型名称 - if(chatRequest.getModel().equals("gpt-image")) { - chatModelVo = chatModelService.selectModelByCategory("image"); - if (chatModelVo == null) { - log.error("未找到image类型的模型配置"); - throw new IllegalStateException("未找到image类型的模型配置"); - } - }else{ - chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); - } - // 获取对话消息列表 List messages = chatRequest.getMessages(); - // 查询向量库相关信息加入到上下文 - if(StringUtils.isNotEmpty(chatRequest.getKid())){ - List knMessages = new ArrayList<>(); - String content = messages.get(messages.size() - 1).getContent().toString(); - // 通过kid查询知识库信息 - KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKid())); - // 查询向量模型配置信息 - ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModelName()); - - QueryVectorBo queryVectorBo = new QueryVectorBo(); - queryVectorBo.setQuery(content); - queryVectorBo.setKid(chatRequest.getKid()); - queryVectorBo.setApiKey(chatModel.getApiKey()); - queryVectorBo.setBaseUrl(chatModel.getApiHost()); - queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModelName()); - queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModelName()); - queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit()); - List nearestList = vectorStoreService.getQueryVector(queryVectorBo); - for (String prompt : nearestList) { - Message userMessage = Message.builder().content(prompt).role(Message.Role.USER).build(); - knMessages.add(userMessage); - } - messages.addAll(knMessages); - // 设置知识库系统提示词 - sysPrompt = knowledgeInfoVo.getSystemPrompt(); - if(StringUtils.isEmpty(sysPrompt)){ - sysPrompt ="###角色设定\n" + - "你是一个智能知识助手,专注于利用上下文中的信息来提供准确和相关的回答。\n" + - "###指令\n" + - "当用户的问题与上下文知识匹配时,利用上下文信息进行回答。如果问题与上下文不匹配,运用自身的推理能力生成合适的回答。\n" + - "###限制\n" + - "确保回答清晰简洁,避免提供不必要的细节。始终保持语气友好" + - "当前时间:"+ DateUtils.getDate(); - } - }else { - sysPrompt = chatModelVo.getSystemPrompt(); - if(StringUtils.isEmpty(sysPrompt)){ - sysPrompt ="你是一个由RuoYI-AI开发的人工智能助手,名字叫熊猫助手。你擅长中英文对话,能够理解并处理各种问题,提供安全、有帮助、准确的回答。" + - "当前时间:"+ DateUtils.getDate()+ - "#注意:回复之前注意结合上下文和工具返回内容进行回复。"; - } - } - // 设置系统默认提示词 - Message sysMessage = Message.builder().content(sysPrompt).role(Message.Role.SYSTEM).build(); - messages.add(0,sysMessage); - + + // 处理知识库相关逻辑 + String sysPrompt = processKnowledgeBase(chatRequest, messages); + + // 设置系统提示词 + Message sysMessage = Message.builder() + .content(sysPrompt) + .role(Message.Role.SYSTEM) + .build(); + messages.add(0, sysMessage); + chatRequest.setSysPrompt(sysPrompt); + // 用户对话内容 String chatString = null; // 获取用户对话信息 @@ -213,12 +206,113 @@ public class SseServiceImpl implements ISseService { if (CollectionUtil.isNotEmpty(listContent)) { chatString = listContent.get(0).toString(); } - } else if (content instanceof String) { - chatString = (String) content; + } else { + chatString = content.toString(); } - // 设置对话信息 chatRequest.setPrompt(chatString); } + + /** + * 处理知识库相关逻辑 + */ + private String processKnowledgeBase(ChatRequest chatRequest, List messages) { + if (StringUtils.isEmpty(chatRequest.getKid())) { + return getDefaultSystemPrompt(); + } + + try { + // 查询知识库信息 + KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKid())); + if (knowledgeInfoVo == null) { + log.warn("知识库信息不存在,kid: {}", chatRequest.getKid()); + return getDefaultSystemPrompt(); + } + + // 查询向量模型配置信息 + ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModelName()); + if (chatModel == null) { + log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModelName()); + return getDefaultSystemPrompt(); + } + + // 构建向量查询参数 + QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel); + + // 获取向量查询结果 + List nearestList = vectorStoreService.getQueryVector(queryVectorBo); + + // 添加知识库消息到上下文 + addKnowledgeMessages(messages, nearestList); + + // 返回知识库系统提示词 + return getKnowledgeSystemPrompt(knowledgeInfoVo); + + } catch (Exception e) { + log.error("处理知识库信息失败: {}", e.getMessage(), e); + return getDefaultSystemPrompt(); + } + } + + /** + * 构建向量查询参数 + */ + private QueryVectorBo buildQueryVectorBo(ChatRequest chatRequest, KnowledgeInfoVo knowledgeInfoVo, ChatModelVo chatModel) { + String content = chatRequest.getMessages().get(chatRequest.getMessages().size() - 1).getContent().toString(); + + QueryVectorBo queryVectorBo = new QueryVectorBo(); + queryVectorBo.setQuery(content); + queryVectorBo.setKid(chatRequest.getKid()); + queryVectorBo.setApiKey(chatModel.getApiKey()); + queryVectorBo.setBaseUrl(chatModel.getApiHost()); + queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModelName()); + queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModelName()); + queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit()); + + return queryVectorBo; + } + + /** + * 添加知识库消息到上下文 + */ + private void addKnowledgeMessages(List messages, List nearestList) { + for (String prompt : nearestList) { + Message userMessage = Message.builder() + .content(prompt) + .role(Message.Role.USER) + .build(); + messages.add(userMessage); + } + } + + /** + * 获取知识库系统提示词 + */ + private String getKnowledgeSystemPrompt(KnowledgeInfoVo knowledgeInfoVo) { + String sysPrompt = knowledgeInfoVo.getSystemPrompt(); + if (StringUtils.isEmpty(sysPrompt)) { + sysPrompt = "###角色设定\n" + + "你是一个智能知识助手,专注于利用上下文中的信息来提供准确和相关的回答。\n" + + "###指令\n" + + "当用户的问题与上下文知识匹配时,利用上下文信息进行回答。如果问题与上下文不匹配,运用自身的推理能力生成合适的回答。\n" + + "###限制\n" + + "确保回答清晰简洁,避免提供不必要的细节。始终保持语气友好\n" + + "当前时间:" + DateUtils.getDate(); + } + return sysPrompt; + } + + /** + * 获取默认系统提示词 + */ + private String getDefaultSystemPrompt() { + String sysPrompt = chatModelVo != null ? chatModelVo.getSystemPrompt() : null; + if (StringUtils.isEmpty(sysPrompt)) { + sysPrompt = "你是一个由RuoYI-AI开发的人工智能助手,名字叫熊猫助手。你擅长中英文对话,能够理解并处理各种问题,提供安全、有帮助、准确的回答。" + + "当前时间:" + DateUtils.getDate() + + "#注意:回复之前注意结合上下文和工具返回内容进行回复。"; + } + return sysPrompt; + } /** From 330bdc3761b29b715275399840e3249813ca9081 Mon Sep 17 00:00:00 2001 From: likunlong Date: Mon, 18 Aug 2025 14:49:56 +0800 Subject: [PATCH 21/36] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E5=BA=93chat?= =?UTF-8?q?=5Fmodel=E6=B7=BB=E5=8A=A0=E4=BC=98=E5=85=88=E7=BA=A7=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/sql/update/chat-model-priority.sql | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 script/sql/update/chat-model-priority.sql diff --git a/script/sql/update/chat-model-priority.sql b/script/sql/update/chat-model-priority.sql new file mode 100644 index 00000000..b7249fa0 --- /dev/null +++ b/script/sql/update/chat-model-priority.sql @@ -0,0 +1,26 @@ +alter table chat_model + add priority int default 1 null comment '模型优先级(值越大优先级越高)'; + +UPDATE `ruoyi-ai`.chat_model t +SET t.priority = 3 +WHERE t.id = 1782792839548735492; + +UPDATE `ruoyi-ai`.chat_model t +SET t.priority = 6 +WHERE t.id = 1859570229117022212; + +UPDATE `ruoyi-ai`.chat_model t +SET t.priority = 5 +WHERE t.id = 1859570229117022211; + +UPDATE `ruoyi-ai`.chat_model t +SET t.priority = 4 +WHERE t.id = 1782792839548735493; + +UPDATE `ruoyi-ai`.chat_model t +SET t.priority = 2 +WHERE t.id = 1828324413241466881; + +UPDATE `ruoyi-ai`.chat_model t +SET t.priority = 2 +WHERE t.id = 1782792839548735491; \ No newline at end of file From 6ce52befe22e4903ab7d02f1ec687468003b97a3 Mon Sep 17 00:00:00 2001 From: likunlong Date: Tue, 19 Aug 2025 10:32:17 +0800 Subject: [PATCH 22/36] =?UTF-8?q?feat:=20=E6=A0=B9=E6=8D=AE=E6=98=AF?= =?UTF-8?q?=E5=90=A6=E6=9C=89=E9=99=84=E4=BB=B6=E5=92=8C=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=EF=BC=8C=E8=87=AA=E5=8A=A8=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=B9=B6=E4=B8=94=E8=8E=B7=E5=8F=96=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/common/chat/request/ChatRequest.java | 10 ++++++++++ .../chat/service/chat/impl/SseServiceImpl.java | 15 ++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java index 20a021de..276f5dc8 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java @@ -67,4 +67,14 @@ public class ChatRequest { */ private Long uuid; + /** + * 是否有附件 + */ + private Boolean hasAttachment; + + /** + * 是否自动切换模型 + */ + private Boolean autoSelectModel; + } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index c61b5673..a9eca9c0 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -129,23 +129,20 @@ public class SseServiceImpl implements ISseService { */ private IChatService autoSelectModelAndGetService(ChatRequest chatRequest) { try { - // 处理特殊模型类型 - if ("gpt-image".equals(chatRequest.getModel())) { + if (Boolean.TRUE.equals(chatRequest.getHasAttachment())) { chatModelVo = selectModelByCategory("image"); - return chatServiceFactory.getChatService(chatModelVo.getCategory()); + } else if (Boolean.TRUE.equals(chatRequest.getAutoSelectModel())) { + chatModelVo = selectModelByCategory("chat"); + } else { + chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); } - // 根据模型名称获取模型分类,然后获取该分类下优先级最高的模型 - ChatModelVo tempModel = chatModelService.selectModelByName(chatRequest.getModel()); - if (tempModel == null) { + if (chatModelVo == null) { throw new IllegalStateException("未找到模型名称:" + chatRequest.getModel()); } - chatModelVo = selectModelByCategory(tempModel.getCategory()); - // 直接返回对应的聊天服务 return chatServiceFactory.getChatService(chatModelVo.getCategory()); - } catch (Exception e) { log.error("模型选择和服务获取失败: {}", e.getMessage(), e); throw new IllegalStateException("模型选择和服务获取失败: " + e.getMessage()); From a0d029c14279048777174f5534d8aa7a387fc187 Mon Sep 17 00:00:00 2001 From: likunlong Date: Tue, 19 Aug 2025 15:12:24 +0800 Subject: [PATCH 23/36] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E8=AF=B7=E6=B1=82=E5=8F=82=E6=95=B0=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=90=8D=E7=A7=B0=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index a9eca9c0..3a948bea 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -140,7 +140,8 @@ public class SseServiceImpl implements ISseService { if (chatModelVo == null) { throw new IllegalStateException("未找到模型名称:" + chatRequest.getModel()); } - + // 自动设置请求参数中的模型名称 + chatRequest.setModel(chatModelVo.getModelName()); // 直接返回对应的聊天服务 return chatServiceFactory.getChatService(chatModelVo.getCategory()); } catch (Exception e) { From aa11c1f233d73e1a4b4e4d0c76b45f5d4ddb5c78 Mon Sep 17 00:00:00 2001 From: likunlong Date: Tue, 19 Aug 2025 16:46:25 +0800 Subject: [PATCH 24/36] =?UTF-8?q?feat:=20=E9=97=AE=E7=AD=94=E6=97=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BB=9F=E4=B8=80=E9=87=8D=E8=AF=95=E5=92=8C?= =?UTF-8?q?=E9=99=8D=E7=BA=A7=E9=80=BB=E8=BE=91=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/ruoyi/domain/vo/ChatModelVo.java | 6 + .../org/ruoyi/service/IChatModelService.java | 5 + .../service/impl/ChatModelServiceImpl.java | 14 +++ .../chat/listener/SSEEventSourceListener.java | 17 ++- .../service/chat/impl/DeepSeekChatImpl.java | 5 + .../service/chat/impl/OpenAIServiceImpl.java | 9 +- .../service/chat/impl/SseServiceImpl.java | 32 ++++- .../ruoyi/chat/support/ChatRetryHelper.java | 115 ++++++++++++++++++ .../org/ruoyi/chat/support/RetryNotifier.java | 39 ++++++ .../java/org/ruoyi/chat/util/SSEUtil.java | 2 +- 10 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/ChatRetryHelper.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java index 8472929d..257c0ec3 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java @@ -90,6 +90,12 @@ public class ChatModelVo implements Serializable { @ExcelProperty(value = "密钥") private String apiKey; + /** + * 优先级(值越大优先级越高) + */ + @ExcelProperty(value = "优先级") + private Integer priority; + /** * 备注 */ diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IChatModelService.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IChatModelService.java index d93b527e..9e90e46b 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IChatModelService.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IChatModelService.java @@ -63,6 +63,11 @@ public interface IChatModelService { */ ChatModelVo selectModelByCategoryWithHighestPriority(String category); + /** + * 在同一分类下,查找优先级小于当前优先级的最高优先级模型(用于降级)。 + */ + ChatModelVo selectFallbackModelByCategoryAndLessPriority(String category, Integer currentPriority); + /** * 获取ppt模型信息 */ diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/ChatModelServiceImpl.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/ChatModelServiceImpl.java index b7acfaa9..069d75a6 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/ChatModelServiceImpl.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/ChatModelServiceImpl.java @@ -150,6 +150,20 @@ public class ChatModelServiceImpl implements IChatModelService { ); } + /** + * 在同一分类下,查找优先级小于当前优先级的最高优先级模型(用于降级)。 + */ + @Override + public ChatModelVo selectFallbackModelByCategoryAndLessPriority(String category, Integer currentPriority) { + return baseMapper.selectVoOne( + Wrappers.lambdaQuery() + .eq(ChatModel::getCategory, category) + .lt(ChatModel::getPriority, currentPriority) + .orderByDesc(ChatModel::getPriority) + .last("LIMIT 1") + ); + } + @Override public ChatModel getPPT() { return baseMapper.selectOne(Wrappers.lambdaQuery().eq(ChatModel::getModelName, "ppt")); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java index b8c8167e..b6087f07 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java @@ -21,6 +21,8 @@ import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.common.core.utils.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.ruoyi.chat.util.SSEUtil; +import org.ruoyi.chat.support.RetryNotifier; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.Objects; @@ -77,6 +79,8 @@ public class SSEEventSourceListener extends EventSourceListener { if ("[DONE]".equals(data)) { //成功响应 emitter.complete(); + // 清理失败回调 + RetryNotifier.clear(sessionId); // 扣除费用 ChatRequest chatRequest = new ChatRequest(); // 设置对话角色 @@ -115,20 +119,31 @@ public class SSEEventSourceListener extends EventSourceListener { @Override public void onClosed(EventSource eventSource) { log.info("OpenAI关闭sse连接..."); + // 清理失败回调 + RetryNotifier.clear(sessionId); } @SneakyThrows @Override public void onFailure(EventSource eventSource, Throwable t, Response response) { if (Objects.isNull(response)) { + // 透传错误到前端 + SSEUtil.sendErrorEvent(emitter, t != null ? t.getMessage() : "SSE连接失败"); + // 通知重试 + RetryNotifier.notifyFailure(sessionId); return; } ResponseBody body = response.body(); if (Objects.nonNull(body)) { - log.error("OpenAI sse连接异常data:{},异常:{}", body.string(), t); + String msg = body.string(); + log.error("OpenAI sse连接异常data:{},异常:{}", msg, t); + SSEUtil.sendErrorEvent(emitter, msg); } else { log.error("OpenAI sse连接异常data:{},异常:{}", response, t); + SSEUtil.sendErrorEvent(emitter, String.valueOf(response)); } + // 通知重试 + RetryNotifier.notifyFailure(sessionId); eventSource.cancel(); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java index 9e59fdd7..f0ddd77b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java @@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import org.ruoyi.chat.support.RetryNotifier; /** * deepseek */ @@ -57,11 +58,15 @@ public class DeepSeekChatImpl implements IChatService { @Override public void onError(Throwable error) { System.err.println("错误: " + error.getMessage()); + // 通知上层失败,进入重试/降级 + RetryNotifier.notifyFailure(chatRequest.getSessionId()); } }); } catch (Exception e) { log.error("deepseek请求失败:{}", e.getMessage()); + // 同步异常直接通知失败 + RetryNotifier.notifyFailure(chatRequest.getSessionId()); } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java index 50693617..b46fa03d 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java @@ -22,6 +22,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.List; +import org.ruoyi.chat.support.RetryNotifier; /** @@ -65,7 +66,13 @@ public class OpenAIServiceImpl implements IChatService { .model(chatRequest.getModel()) .stream(true) .build(); - openAiStreamClient.streamChatCompletion(completion, listener); + try { + openAiStreamClient.streamChatCompletion(completion, listener); + } catch (Exception ex) { + // 同步异常也触发失败回调,按会话维度 + RetryNotifier.notifyFailure(chatRequest.getSessionId()); + throw ex; + } return emitter; } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index 3a948bea..01be0cec 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -9,6 +9,8 @@ import org.ruoyi.chat.factory.ChatServiceFactory; import org.ruoyi.chat.service.chat.IChatCostService; import org.ruoyi.chat.service.chat.IChatService; import org.ruoyi.chat.service.chat.ISseService; +import org.ruoyi.chat.support.ChatRetryHelper; +import org.ruoyi.chat.support.RetryNotifier; import org.ruoyi.chat.util.SSEUtil; import org.ruoyi.common.chat.entity.Tts.TextToSpeech; import org.ruoyi.common.chat.entity.chat.Message; @@ -116,7 +118,27 @@ public class SseServiceImpl implements ISseService { } // 自动选择模型并获取对应的聊天服务 IChatService chatService = autoSelectModelAndGetService(chatRequest); - chatService.chat(chatRequest, sseEmitter); + + // 统一重试与降级:封装启动逻辑,并通过ThreadLocal传递失败回调 + ChatModelVo currentModel = this.chatModelVo; + String currentCategory = currentModel.getCategory(); + ChatRetryHelper.executeWithRetry( + currentModel, + currentCategory, + chatModelService, + sseEmitter, + (modelForTry, onFailure) -> { + // 替换请求中的模型名称 + chatRequest.setModel(modelForTry.getModelName()); + // 将回调注册到ThreadLocal,供底层SSE失败时触发 + RetryNotifier.setFailureCallback(chatRequest.getSessionId(), onFailure); + try { + autoSelectServiceByCategoryAndInvoke(chatRequest, sseEmitter, modelForTry.getCategory()); + } finally { + // 不在此处清理,待下游结束/失败时清理 + } + } + ); } catch (Exception e) { log.error(e.getMessage(),e); SSEUtil.sendErrorEvent(sseEmitter,e.getMessage()); @@ -149,6 +171,14 @@ public class SseServiceImpl implements ISseService { throw new IllegalStateException("模型选择和服务获取失败: " + e.getMessage()); } } + + /** + * 根据给定分类获取服务并发起调用(避免在降级时重复选择模型) + */ + private void autoSelectServiceByCategoryAndInvoke(ChatRequest chatRequest, SseEmitter sseEmitter, String category) { + IChatService service = chatServiceFactory.getChatService(category); + service.chat(chatRequest, sseEmitter); + } /** * 根据分类选择优先级最高的模型 diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/ChatRetryHelper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/ChatRetryHelper.java new file mode 100644 index 00000000..bf4a4a74 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/ChatRetryHelper.java @@ -0,0 +1,115 @@ +package org.ruoyi.chat.support; + +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.chat.util.SSEUtil; +import org.ruoyi.domain.vo.ChatModelVo; +import org.ruoyi.service.IChatModelService; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; + +/** + * 统一的聊天重试与降级调度器。 + * + * 策略: + * - 当前模型最多重试 3 次;仍失败则降级到同分类内、优先级小于当前的最高优先级模型。 + * - 降级模型同样最多重试 3 次;仍失败则向前端返回失败信息并停止。 + * + * 注意:实现依赖调用方在底层异步失败时执行 onFailure.run() 通知本调度器。 + */ +@Slf4j +public class ChatRetryHelper { + + public interface AttemptStarter { + void start(ChatModelVo model, Runnable onFailure) throws Exception; + } + + public static void executeWithRetry( + ChatModelVo primaryModel, + String category, + IChatModelService chatModelService, + SseEmitter emitter, + AttemptStarter attemptStarter + ) { + Objects.requireNonNull(primaryModel, "primaryModel must not be null"); + Objects.requireNonNull(category, "category must not be null"); + Objects.requireNonNull(chatModelService, "chatModelService must not be null"); + Objects.requireNonNull(emitter, "emitter must not be null"); + Objects.requireNonNull(attemptStarter, "attemptStarter must not be null"); + + AtomicInteger mainAttempts = new AtomicInteger(0); + AtomicInteger fallbackAttempts = new AtomicInteger(0); + AtomicBoolean inFallback = new AtomicBoolean(false); + AtomicBoolean scheduling = new AtomicBoolean(false); + + class Scheduler { + volatile ChatModelVo current = primaryModel; + volatile ChatModelVo fallback = null; + + void startAttempt() { + try { + if (!inFallback.get()) { + if (mainAttempts.incrementAndGet() > 3) { + // 进入降级 + inFallback.set(true); + if (fallback == null) { + Integer curPriority = primaryModel.getPriority(); + if (curPriority == null) { + curPriority = Integer.MAX_VALUE; + } + fallback = chatModelService.selectFallbackModelByCategoryAndLessPriority(category, curPriority); + } + if (fallback == null) { + SSEUtil.sendErrorEvent(emitter, "当前模型重试3次均失败,且无可用降级模型"); + emitter.complete(); + return; + } + current = fallback; + mainAttempts.set(3); // 锁定 + fallbackAttempts.set(0); + } + } else { + if (fallbackAttempts.incrementAndGet() > 3) { + SSEUtil.sendErrorEvent(emitter, "降级模型重试3次仍失败"); + emitter.complete(); + return; + } + } + + Runnable onFailure = () -> { + // 去抖:避免同一次失败触发多次重试 + if (scheduling.compareAndSet(false, true)) { + try { + SSEUtil.sendErrorEvent(emitter, (inFallback.get() ? "降级模型" : "当前模型") + "调用失败,准备重试..."); + // 立即发起下一次尝试 + startAttempt(); + } finally { + scheduling.set(false); + } + } + }; + + attemptStarter.start(current, onFailure); + } catch (Exception ex) { + log.error("启动聊天尝试失败: {}", ex.getMessage(), ex); + SSEUtil.sendErrorEvent(emitter, "启动聊天尝试失败: " + ex.getMessage()); + // 直接按失败处理,继续重试/降级 + if (scheduling.compareAndSet(false, true)) { + try { + startAttempt(); + } finally { + scheduling.set(false); + } + } + } + } + } + + new Scheduler().startAttempt(); + } +} + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java new file mode 100644 index 00000000..77044081 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java @@ -0,0 +1,39 @@ +package org.ruoyi.chat.support; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 失败回调通知器:基于 sessionId 绑定回调,底层失败时按 sessionId 通知上层重试调度器。 + */ +public class RetryNotifier { + + private static final Map FAILURE_CALLBACKS = new ConcurrentHashMap<>(); + + public static void setFailureCallback(Long sessionId, Runnable callback) { + if (sessionId == null || callback == null) { + return; + } + FAILURE_CALLBACKS.put(sessionId, callback); + } + + public static void clear(Long sessionId) { + if (sessionId == null) { + return; + } + FAILURE_CALLBACKS.remove(sessionId); + } + + public static void notifyFailure(Long sessionId) { + if (sessionId == null) { + return; + } + Runnable cb = FAILURE_CALLBACKS.get(sessionId); + if (Objects.nonNull(cb)) { + cb.run(); + } + } +} + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/util/SSEUtil.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/util/SSEUtil.java index 9bfb6bf0..293e486e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/util/SSEUtil.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/util/SSEUtil.java @@ -25,6 +25,6 @@ public class SSEUtil { } catch (IOException e) { log.error("SSE发送失败: {}", e.getMessage()); } - sseEmitter.complete(); + // 不立即关闭,由上层策略决定是否继续重试或降级 } } From 359cee28d5071afad3e18a1b36d33f7d9f47d529 Mon Sep 17 00:00:00 2001 From: likunlong Date: Tue, 19 Aug 2025 16:51:51 +0800 Subject: [PATCH 25/36] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E7=9B=AE?= =?UTF-8?q?=E5=89=8D=E5=AE=9E=E7=8E=B0=E7=B1=BB=E4=BD=BF=E7=94=A8=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E9=87=8D=E8=AF=95=E9=99=8D=E7=BA=A7=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FastGPTSSEEventSourceListener.java | 26 +++++++++++++++- .../service/chat/impl/CozeServiceImpl.java | 31 ++++++++++++------- .../service/chat/impl/FastGPTServiceImpl.java | 9 ++++-- .../service/chat/impl/OllamaServiceImpl.java | 4 +++ .../chat/impl/QianWenAiChatServiceImpl.java | 3 ++ .../chat/impl/ZhipuAiChatServiceImpl.java | 5 ++- 6 files changed, 62 insertions(+), 16 deletions(-) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/FastGPTSSEEventSourceListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/FastGPTSSEEventSourceListener.java index 0d58176d..dd8519f2 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/FastGPTSSEEventSourceListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/FastGPTSSEEventSourceListener.java @@ -14,6 +14,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.Objects; +import org.ruoyi.chat.support.RetryNotifier; +import org.ruoyi.chat.util.SSEUtil; @Slf4j @Component @@ -21,12 +23,18 @@ import java.util.Objects; public class FastGPTSSEEventSourceListener extends EventSourceListener { private SseEmitter emitter; + private Long sessionId; @Autowired(required = false) public FastGPTSSEEventSourceListener(SseEmitter emitter) { this.emitter = emitter; } + public FastGPTSSEEventSourceListener(SseEmitter emitter, Long sessionId) { + this.emitter = emitter; + this.sessionId = sessionId; + } + @Override public void onOpen(EventSource eventSource, Response response) { log.info("FastGPT sse连接成功"); @@ -40,6 +48,9 @@ public class FastGPTSSEEventSourceListener extends EventSourceListener { if ("flowResponses".equals(type)){ emitter.send(data); emitter.complete(); + if (sessionId != null) { + RetryNotifier.clear(sessionId); + } } else { emitter.send(data); } @@ -57,13 +68,26 @@ public class FastGPTSSEEventSourceListener extends EventSourceListener { @SneakyThrows public void onFailure(EventSource eventSource, Throwable t, Response response) { if (Objects.isNull(response)) { + if (sessionId != null) { + SSEUtil.sendErrorEvent(emitter, t != null ? t.getMessage() : "SSE连接失败"); + RetryNotifier.notifyFailure(sessionId); + } return; } ResponseBody body = response.body(); if (Objects.nonNull(body)) { - log.error("FastGPT sse连接异常data:{},异常:{}", body.string(), t); + String msg = body.string(); + log.error("FastGPT sse连接异常data:{},异常:{}", msg, t); + if (sessionId != null) { + SSEUtil.sendErrorEvent(emitter, msg); + RetryNotifier.notifyFailure(sessionId); + } } else { log.error("FastGPT sse连接异常data:{},异常:{}", response, t); + if (sessionId != null) { + SSEUtil.sendErrorEvent(emitter, String.valueOf(response)); + RetryNotifier.notifyFailure(sessionId); + } } eventSource.cancel(); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java index 7cbb5927..54269b08 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java @@ -20,6 +20,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.Collections; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.ruoyi.chat.support.RetryNotifier; /** * 扣子聊天管理 @@ -53,19 +54,25 @@ public class CozeServiceImpl implements IChatService { Flowable resp = coze.chat().stream(req); ExecutorService executor = Executors.newFixedThreadPool(10); executor.submit(() -> { - resp.blockingForEach( - event -> { - if (ChatEventType.CONVERSATION_MESSAGE_DELTA.equals(event.getEvent())) { - emitter.send(event.getMessage().getContent()); - log.info("coze: {}", event.getMessage().getContent()); + try { + resp.blockingForEach( + event -> { + if (ChatEventType.CONVERSATION_MESSAGE_DELTA.equals(event.getEvent())) { + emitter.send(event.getMessage().getContent()); + log.info("coze: {}", event.getMessage().getContent()); + } + if (ChatEventType.CONVERSATION_CHAT_COMPLETED.equals(event.getEvent())) { + emitter.complete(); + log.info("Token usage: {}", event.getChat().getUsage().getTokenCount()); + RetryNotifier.clear(chatRequest.getSessionId()); + } } - if (ChatEventType.CONVERSATION_CHAT_COMPLETED.equals(event.getEvent())) { - emitter.complete(); - log.info("Token usage: {}", event.getChat().getUsage().getTokenCount()); - } - } - ); - coze.shutdownExecutor(); + ); + } catch (Exception ex) { + RetryNotifier.notifyFailure(chatRequest.getSessionId()); + } finally { + coze.shutdownExecutor(); + } }); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/FastGPTServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/FastGPTServiceImpl.java index 15acf6f2..b3f41431 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/FastGPTServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/FastGPTServiceImpl.java @@ -33,7 +33,7 @@ public class FastGPTServiceImpl implements IChatService { ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); OpenAiStreamClient openAiStreamClient = ChatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey()); List messages = chatRequest.getMessages(); - FastGPTSSEEventSourceListener listener = new FastGPTSSEEventSourceListener(emitter); + FastGPTSSEEventSourceListener listener = new FastGPTSSEEventSourceListener(emitter, chatRequest.getSessionId()); FastGPTChatCompletion completion = FastGPTChatCompletion .builder() .messages(messages) @@ -41,7 +41,12 @@ public class FastGPTServiceImpl implements IChatService { .detail(true) .stream(true) .build(); - openAiStreamClient.streamChatCompletion(completion, listener); + try { + openAiStreamClient.streamChatCompletion(completion, listener); + } catch (Exception ex) { + org.ruoyi.chat.support.RetryNotifier.notifyFailure(chatRequest.getSessionId()); + throw ex; + } return emitter; } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java index 532b052e..669f1c2f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; +import org.ruoyi.chat.support.RetryNotifier; /** @@ -66,12 +67,15 @@ public class OllamaServiceImpl implements IChatService { emitter.send(substr); } catch (IOException e) { SSEUtil.sendErrorEvent(emitter, e.getMessage()); + RetryNotifier.notifyFailure(chatRequest.getSessionId()); } }; api.chat(requestModel, streamHandler); emitter.complete(); + RetryNotifier.clear(chatRequest.getSessionId()); } catch (Exception e) { SSEUtil.sendErrorEvent(emitter, e.getMessage()); + RetryNotifier.notifyFailure(chatRequest.getSessionId()); } }); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java index 850ebf6c..8462529c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java @@ -51,15 +51,18 @@ public class QianWenAiChatServiceImpl implements IChatService { public void onCompleteResponse(ChatResponse completeResponse) { emitter.complete(); log.info("消息结束,完整消息ID: {}", completeResponse); + org.ruoyi.chat.support.RetryNotifier.clear(chatRequest.getSessionId()); } @Override public void onError(Throwable error) { error.printStackTrace(); + org.ruoyi.chat.support.RetryNotifier.notifyFailure(chatRequest.getSessionId()); } }); } catch (Exception e) { log.error("千问请求失败:{}", e.getMessage()); + org.ruoyi.chat.support.RetryNotifier.notifyFailure(chatRequest.getSessionId()); } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java index da44d6c0..1fd12406 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java @@ -51,14 +51,16 @@ public class ZhipuAiChatServiceImpl implements IChatService { @SneakyThrows @Override public void onError(Throwable error) { - // System.out.println(error.getMessage()); + // 透传错误并触发重试 emitter.send(error.getMessage()); + org.ruoyi.chat.support.RetryNotifier.notifyFailure(chatRequest.getSessionId()); } @Override public void onCompleteResponse(ChatResponse response) { emitter.complete(); log.info("消息结束,完整消息ID: {}", response.aiMessage()); + org.ruoyi.chat.support.RetryNotifier.clear(chatRequest.getSessionId()); } }; @@ -71,6 +73,7 @@ public class ZhipuAiChatServiceImpl implements IChatService { model.chat(chatRequest.getPrompt(), handler); } catch (Exception e) { log.error("智谱清言请求失败:{}", e.getMessage()); + org.ruoyi.chat.support.RetryNotifier.notifyFailure(chatRequest.getSessionId()); } return emitter; From c43d4784de7c6d6fd792a5b05710a3bd7a1cc85b Mon Sep 17 00:00:00 2001 From: likunlong Date: Tue, 19 Aug 2025 17:39:20 +0800 Subject: [PATCH 26/36] =?UTF-8?q?feat:=20=E5=A4=84=E7=90=86=E5=9C=A8?= =?UTF-8?q?=E9=9D=9EWeb=E7=BA=BF=E7=A8=8B=E4=B8=AD=E8=8E=B7=E5=8F=96Reques?= =?UTF-8?q?t=E4=B8=ADtoken=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/ruoyi/common/chat/request/ChatRequest.java | 5 +++++ .../org/ruoyi/chat/service/chat/impl/ImageServiceImpl.java | 4 ++-- .../ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java | 2 +- .../org/ruoyi/chat/service/chat/impl/SseServiceImpl.java | 7 +++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java index 276f5dc8..71ebc4e5 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java @@ -77,4 +77,9 @@ public class ChatRequest { */ private Boolean autoSelectModel; + /** + * 会话令牌(为避免在非Web线程中获取Request,入口处注入) + */ + private String token; + } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ImageServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ImageServiceImpl.java index 709c58ef..1c4af69d 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ImageServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ImageServiceImpl.java @@ -128,8 +128,8 @@ public class ImageServiceImpl implements IChatService { OpenAiStreamClient openAiStreamClient = ChatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey()); List messages = chatRequest.getMessages(); - // 获取会话token - String token = StpUtil.getTokenValue(); + // 获取会话token(从入口透传,避免非Web线程取值报错) + String token = chatRequest.getToken(); // 创建 SSE 事件源监听器 SSEEventSourceListener listener = new SSEEventSourceListener(emitter, chatRequest.getUserId(), chatRequest.getSessionId(), token); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java index b46fa03d..c81cc0ea 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java @@ -58,7 +58,7 @@ public class OpenAIServiceImpl implements IChatService { Message userMessage = Message.builder().content("工具返回信息:"+toolString).role(Message.Role.USER).build(); messages.add(userMessage); } - String token = StpUtil.getTokenValue(); + String token = chatRequest.getToken(); SSEEventSourceListener listener = new SSEEventSourceListener(emitter,chatRequest.getUserId(),chatRequest.getSessionId(), token); ChatCompletion completion = ChatCompletion .builder() diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index 01be0cec..7fe65818 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -47,6 +47,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import cn.dev33.satoken.stp.StpUtil; /** * @author ageer @@ -77,6 +78,12 @@ public class SseServiceImpl implements ISseService { public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) { SseEmitter sseEmitter = new SseEmitter(0L); try { + // 记录当前会话令牌,供异步线程使用 + try { + chatRequest.setToken(StpUtil.getTokenValue()); + } catch (Exception ignore) { + // 保底:无token场景下忽略 + } // 构建消息列表 buildChatMessageList(chatRequest); // 设置对话角色 From 4b37cfe97d0e78a25a3f0cc3228cb0245d956d9d Mon Sep 17 00:00:00 2001 From: likunlong Date: Tue, 19 Aug 2025 17:53:27 +0800 Subject: [PATCH 27/36] =?UTF-8?q?feat:=20=E5=A4=B1=E8=B4=A5=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E5=99=A8=E4=B8=AD=E4=BD=BF=E7=94=A8emitter=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E7=9A=84=E5=94=AF=E4=B8=80hash=E4=BD=9C=E4=B8=BAkey?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8session=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E4=B8=8E=E4=B8=9A=E5=8A=A1=E8=BF=9B=E8=A1=8C=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=EF=BC=8C=E5=90=8C=E6=97=B6=E4=B9=9F=E4=BF=9D=E8=AF=81?= =?UTF-8?q?=E8=B7=A8=E7=BA=BF=E7=A8=8B=E8=B0=83=E7=94=A8=E7=9A=84=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E6=80=A7=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FastGPTSSEEventSourceListener.java | 22 +++++-------- .../chat/listener/SSEEventSourceListener.java | 12 +++---- .../service/chat/impl/CozeServiceImpl.java | 4 +-- .../service/chat/impl/DeepSeekChatImpl.java | 6 ++-- .../service/chat/impl/OllamaServiceImpl.java | 6 ++-- .../service/chat/impl/OpenAIServiceImpl.java | 4 +-- .../chat/impl/QianWenAiChatServiceImpl.java | 6 ++-- .../service/chat/impl/SseServiceImpl.java | 4 +-- .../chat/impl/ZhipuAiChatServiceImpl.java | 8 ++--- .../org/ruoyi/chat/support/RetryNotifier.java | 31 +++++++++++-------- 10 files changed, 50 insertions(+), 53 deletions(-) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/FastGPTSSEEventSourceListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/FastGPTSSEEventSourceListener.java index dd8519f2..3895c1a3 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/FastGPTSSEEventSourceListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/FastGPTSSEEventSourceListener.java @@ -48,9 +48,7 @@ public class FastGPTSSEEventSourceListener extends EventSourceListener { if ("flowResponses".equals(type)){ emitter.send(data); emitter.complete(); - if (sessionId != null) { - RetryNotifier.clear(sessionId); - } + RetryNotifier.clear(emitter); } else { emitter.send(data); } @@ -68,26 +66,20 @@ public class FastGPTSSEEventSourceListener extends EventSourceListener { @SneakyThrows public void onFailure(EventSource eventSource, Throwable t, Response response) { if (Objects.isNull(response)) { - if (sessionId != null) { - SSEUtil.sendErrorEvent(emitter, t != null ? t.getMessage() : "SSE连接失败"); - RetryNotifier.notifyFailure(sessionId); - } + SSEUtil.sendErrorEvent(emitter, t != null ? t.getMessage() : "SSE连接失败"); + RetryNotifier.notifyFailure(emitter); return; } ResponseBody body = response.body(); if (Objects.nonNull(body)) { String msg = body.string(); log.error("FastGPT sse连接异常data:{},异常:{}", msg, t); - if (sessionId != null) { - SSEUtil.sendErrorEvent(emitter, msg); - RetryNotifier.notifyFailure(sessionId); - } + SSEUtil.sendErrorEvent(emitter, msg); + RetryNotifier.notifyFailure(emitter); } else { log.error("FastGPT sse连接异常data:{},异常:{}", response, t); - if (sessionId != null) { - SSEUtil.sendErrorEvent(emitter, String.valueOf(response)); - RetryNotifier.notifyFailure(sessionId); - } + SSEUtil.sendErrorEvent(emitter, String.valueOf(response)); + RetryNotifier.notifyFailure(emitter); } eventSource.cancel(); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java index b6087f07..d61e8bb4 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java @@ -79,8 +79,8 @@ public class SSEEventSourceListener extends EventSourceListener { if ("[DONE]".equals(data)) { //成功响应 emitter.complete(); - // 清理失败回调 - RetryNotifier.clear(sessionId); + // 清理失败回调(以 emitter 为键) + RetryNotifier.clear(emitter); // 扣除费用 ChatRequest chatRequest = new ChatRequest(); // 设置对话角色 @@ -120,7 +120,7 @@ public class SSEEventSourceListener extends EventSourceListener { public void onClosed(EventSource eventSource) { log.info("OpenAI关闭sse连接..."); // 清理失败回调 - RetryNotifier.clear(sessionId); + RetryNotifier.clear(emitter); } @SneakyThrows @@ -129,8 +129,8 @@ public class SSEEventSourceListener extends EventSourceListener { if (Objects.isNull(response)) { // 透传错误到前端 SSEUtil.sendErrorEvent(emitter, t != null ? t.getMessage() : "SSE连接失败"); - // 通知重试 - RetryNotifier.notifyFailure(sessionId); + // 通知重试(以 emitter 为键) + RetryNotifier.notifyFailure(emitter); return; } ResponseBody body = response.body(); @@ -143,7 +143,7 @@ public class SSEEventSourceListener extends EventSourceListener { SSEUtil.sendErrorEvent(emitter, String.valueOf(response)); } // 通知重试 - RetryNotifier.notifyFailure(sessionId); + RetryNotifier.notifyFailure(emitter); eventSource.cancel(); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java index 54269b08..e9863b6c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java @@ -64,12 +64,12 @@ public class CozeServiceImpl implements IChatService { if (ChatEventType.CONVERSATION_CHAT_COMPLETED.equals(event.getEvent())) { emitter.complete(); log.info("Token usage: {}", event.getChat().getUsage().getTokenCount()); - RetryNotifier.clear(chatRequest.getSessionId()); + RetryNotifier.clear(emitter); } } ); } catch (Exception ex) { - RetryNotifier.notifyFailure(chatRequest.getSessionId()); + RetryNotifier.notifyFailure(emitter); } finally { coze.shutdownExecutor(); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java index f0ddd77b..0a6e6693 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java @@ -58,15 +58,15 @@ public class DeepSeekChatImpl implements IChatService { @Override public void onError(Throwable error) { System.err.println("错误: " + error.getMessage()); - // 通知上层失败,进入重试/降级 - RetryNotifier.notifyFailure(chatRequest.getSessionId()); + // 通知上层失败,进入重试/降级(以 emitter 为键) + RetryNotifier.notifyFailure(emitter); } }); } catch (Exception e) { log.error("deepseek请求失败:{}", e.getMessage()); // 同步异常直接通知失败 - RetryNotifier.notifyFailure(chatRequest.getSessionId()); + RetryNotifier.notifyFailure(emitter); } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java index 669f1c2f..2401b83e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java @@ -67,15 +67,15 @@ public class OllamaServiceImpl implements IChatService { emitter.send(substr); } catch (IOException e) { SSEUtil.sendErrorEvent(emitter, e.getMessage()); - RetryNotifier.notifyFailure(chatRequest.getSessionId()); + RetryNotifier.notifyFailure(emitter); } }; api.chat(requestModel, streamHandler); emitter.complete(); - RetryNotifier.clear(chatRequest.getSessionId()); + RetryNotifier.clear(emitter); } catch (Exception e) { SSEUtil.sendErrorEvent(emitter, e.getMessage()); - RetryNotifier.notifyFailure(chatRequest.getSessionId()); + RetryNotifier.notifyFailure(emitter); } }); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java index c81cc0ea..cc264803 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java @@ -69,8 +69,8 @@ public class OpenAIServiceImpl implements IChatService { try { openAiStreamClient.streamChatCompletion(completion, listener); } catch (Exception ex) { - // 同步异常也触发失败回调,按会话维度 - RetryNotifier.notifyFailure(chatRequest.getSessionId()); + // 同步异常也触发失败回调(以 emitter 为键) + RetryNotifier.notifyFailure(emitter); throw ex; } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java index 8462529c..3f5c00b0 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java @@ -51,18 +51,18 @@ public class QianWenAiChatServiceImpl implements IChatService { public void onCompleteResponse(ChatResponse completeResponse) { emitter.complete(); log.info("消息结束,完整消息ID: {}", completeResponse); - org.ruoyi.chat.support.RetryNotifier.clear(chatRequest.getSessionId()); + org.ruoyi.chat.support.RetryNotifier.clear(emitter); } @Override public void onError(Throwable error) { error.printStackTrace(); - org.ruoyi.chat.support.RetryNotifier.notifyFailure(chatRequest.getSessionId()); + org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter); } }); } catch (Exception e) { log.error("千问请求失败:{}", e.getMessage()); - org.ruoyi.chat.support.RetryNotifier.notifyFailure(chatRequest.getSessionId()); + org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter); } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index 7fe65818..c6d3fe06 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -137,8 +137,8 @@ public class SseServiceImpl implements ISseService { (modelForTry, onFailure) -> { // 替换请求中的模型名称 chatRequest.setModel(modelForTry.getModelName()); - // 将回调注册到ThreadLocal,供底层SSE失败时触发 - RetryNotifier.setFailureCallback(chatRequest.getSessionId(), onFailure); + // 以 emitter 实例为唯一键注册失败回调 + RetryNotifier.setFailureCallback(sseEmitter, onFailure); try { autoSelectServiceByCategoryAndInvoke(chatRequest, sseEmitter, modelForTry.getCategory()); } finally { diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java index 1fd12406..7405a77b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java @@ -51,16 +51,16 @@ public class ZhipuAiChatServiceImpl implements IChatService { @SneakyThrows @Override public void onError(Throwable error) { - // 透传错误并触发重试 + // 透传错误并触发重试(以 emitter 为键) emitter.send(error.getMessage()); - org.ruoyi.chat.support.RetryNotifier.notifyFailure(chatRequest.getSessionId()); + org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter); } @Override public void onCompleteResponse(ChatResponse response) { emitter.complete(); log.info("消息结束,完整消息ID: {}", response.aiMessage()); - org.ruoyi.chat.support.RetryNotifier.clear(chatRequest.getSessionId()); + org.ruoyi.chat.support.RetryNotifier.clear(emitter); } }; @@ -73,7 +73,7 @@ public class ZhipuAiChatServiceImpl implements IChatService { model.chat(chatRequest.getPrompt(), handler); } catch (Exception e) { log.error("智谱清言请求失败:{}", e.getMessage()); - org.ruoyi.chat.support.RetryNotifier.notifyFailure(chatRequest.getSessionId()); + org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter); } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java index 77044081..25f65c44 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java @@ -5,31 +5,36 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; /** - * 失败回调通知器:基于 sessionId 绑定回调,底层失败时按 sessionId 通知上层重试调度器。 + * 失败回调通知器:基于发射器实例(SseEmitter 等对象地址)绑定回调, + * 避免与业务标识绑定,且能跨线程正确关联。 */ public class RetryNotifier { - private static final Map FAILURE_CALLBACKS = new ConcurrentHashMap<>(); + private static final Map FAILURE_CALLBACKS = new ConcurrentHashMap<>(); - public static void setFailureCallback(Long sessionId, Runnable callback) { - if (sessionId == null || callback == null) { - return; - } - FAILURE_CALLBACKS.put(sessionId, callback); + private static int keyOf(Object obj) { + return System.identityHashCode(obj); } - public static void clear(Long sessionId) { - if (sessionId == null) { + public static void setFailureCallback(Object emitterLike, Runnable callback) { + if (emitterLike == null || callback == null) { return; } - FAILURE_CALLBACKS.remove(sessionId); + FAILURE_CALLBACKS.put(keyOf(emitterLike), callback); } - public static void notifyFailure(Long sessionId) { - if (sessionId == null) { + public static void clear(Object emitterLike) { + if (emitterLike == null) { return; } - Runnable cb = FAILURE_CALLBACKS.get(sessionId); + FAILURE_CALLBACKS.remove(keyOf(emitterLike)); + } + + public static void notifyFailure(Object emitterLike) { + if (emitterLike == null) { + return; + } + Runnable cb = FAILURE_CALLBACKS.get(keyOf(emitterLike)); if (Objects.nonNull(cb)) { cb.run(); } From ccdbb209351b6b486cd33e71a9a08bfc96fae9b4 Mon Sep 17 00:00:00 2001 From: likunlong Date: Tue, 19 Aug 2025 18:00:20 +0800 Subject: [PATCH 28/36] =?UTF-8?q?feat:=20=E4=B8=8D=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E8=87=AA=E5=8A=A8=E9=80=89=E6=8B=A9=E6=97=B6?= =?UTF-8?q?=E8=B5=B0=E5=8E=9F=E5=A7=8B=E9=BB=98=E8=AE=A4=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/chat/impl/SseServiceImpl.java | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index c6d3fe06..f94d0331 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -126,26 +126,31 @@ public class SseServiceImpl implements ISseService { // 自动选择模型并获取对应的聊天服务 IChatService chatService = autoSelectModelAndGetService(chatRequest); - // 统一重试与降级:封装启动逻辑,并通过ThreadLocal传递失败回调 - ChatModelVo currentModel = this.chatModelVo; - String currentCategory = currentModel.getCategory(); - ChatRetryHelper.executeWithRetry( - currentModel, - currentCategory, - chatModelService, - sseEmitter, - (modelForTry, onFailure) -> { - // 替换请求中的模型名称 - chatRequest.setModel(modelForTry.getModelName()); - // 以 emitter 实例为唯一键注册失败回调 - RetryNotifier.setFailureCallback(sseEmitter, onFailure); - try { - autoSelectServiceByCategoryAndInvoke(chatRequest, sseEmitter, modelForTry.getCategory()); - } finally { - // 不在此处清理,待下游结束/失败时清理 + // 仅当 autoSelectModel = true 时,才启用重试与降级 + if (Boolean.TRUE.equals(chatRequest.getAutoSelectModel())) { + ChatModelVo currentModel = this.chatModelVo; + String currentCategory = currentModel.getCategory(); + ChatRetryHelper.executeWithRetry( + currentModel, + currentCategory, + chatModelService, + sseEmitter, + (modelForTry, onFailure) -> { + // 替换请求中的模型名称 + chatRequest.setModel(modelForTry.getModelName()); + // 以 emitter 实例为唯一键注册失败回调 + RetryNotifier.setFailureCallback(sseEmitter, onFailure); + try { + autoSelectServiceByCategoryAndInvoke(chatRequest, sseEmitter, modelForTry.getCategory()); + } finally { + // 不在此处清理,待下游结束/失败时清理 + } } - } - ); + ); + } else { + // 不重试不降级,直接调用 + chatService.chat(chatRequest, sseEmitter); + } } catch (Exception e) { log.error(e.getMessage(),e); SSEUtil.sendErrorEvent(sseEmitter,e.getMessage()); From 43426054ec6d4bdaf94478480fca684dd3554bd1 Mon Sep 17 00:00:00 2001 From: likunlong Date: Tue, 19 Aug 2025 20:28:53 +0800 Subject: [PATCH 29/36] =?UTF-8?q?feat:=20=E5=85=BC=E5=AE=B9=E4=B8=8D?= =?UTF-8?q?=E9=80=89=E8=87=AA=E5=8A=A8=E6=A8=A1=E5=9E=8B=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E5=8E=9F=E5=85=88=E9=80=BB=E8=BE=91=EF=BC=9B=E5=B0=81=E8=A3=85?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E6=96=B9=E6=B3=95=EF=BC=8C=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=9C=89=E7=9B=91=E6=8E=A7=E7=9A=84SSE?= =?UTF-8?q?=EF=BC=8C=E7=AE=80=E5=8C=96=E6=B5=81=E5=BC=8F=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E5=B9=B6=E9=80=9A=E7=9F=A5=E9=87=8D=E8=AF=95?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/listener/SSEEventSourceListener.java | 21 ++++++--- .../service/chat/impl/CozeServiceImpl.java | 3 +- .../service/chat/impl/DeepSeekChatImpl.java | 8 ++-- .../service/chat/impl/ImageServiceImpl.java | 10 ++++- .../service/chat/impl/OllamaServiceImpl.java | 7 ++- .../service/chat/impl/OpenAIServiceImpl.java | 9 ++-- .../chat/impl/QianWenAiChatServiceImpl.java | 5 ++- .../chat/impl/ZhipuAiChatServiceImpl.java | 7 ++- .../ruoyi/chat/support/ChatServiceHelper.java | 45 +++++++++++++++++++ .../org/ruoyi/chat/support/RetryNotifier.java | 7 +++ 10 files changed, 93 insertions(+), 29 deletions(-) create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/ChatServiceHelper.java diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java index d61e8bb4..ceeb9e7b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java @@ -46,12 +46,15 @@ public class SSEEventSourceListener extends EventSourceListener { private String token; + private boolean retryEnabled; + @Autowired(required = false) - public SSEEventSourceListener(SseEmitter emitter,Long userId,Long sessionId, String token) { + public SSEEventSourceListener(SseEmitter emitter,Long userId,Long sessionId, String token, boolean retryEnabled) { this.emitter = emitter; this.userId = userId; this.sessionId = sessionId; this.token = token; + this.retryEnabled = retryEnabled; } @@ -129,8 +132,12 @@ public class SSEEventSourceListener extends EventSourceListener { if (Objects.isNull(response)) { // 透传错误到前端 SSEUtil.sendErrorEvent(emitter, t != null ? t.getMessage() : "SSE连接失败"); - // 通知重试(以 emitter 为键) - RetryNotifier.notifyFailure(emitter); + if (retryEnabled) { + // 通知重试(以 emitter 为键) + RetryNotifier.notifyFailure(emitter); + } else { + emitter.complete(); + } return; } ResponseBody body = response.body(); @@ -142,8 +149,12 @@ public class SSEEventSourceListener extends EventSourceListener { log.error("OpenAI sse连接异常data:{},异常:{}", response, t); SSEUtil.sendErrorEvent(emitter, String.valueOf(response)); } - // 通知重试 - RetryNotifier.notifyFailure(emitter); + if (retryEnabled) { + // 通知重试 + RetryNotifier.notifyFailure(emitter); + } else { + emitter.complete(); + } eventSource.cancel(); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java index e9863b6c..730c0181 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/CozeServiceImpl.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.ruoyi.chat.support.RetryNotifier; +import org.ruoyi.chat.support.ChatServiceHelper; /** * 扣子聊天管理 @@ -69,7 +70,7 @@ public class CozeServiceImpl implements IChatService { } ); } catch (Exception ex) { - RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, ex.getMessage()); } finally { coze.shutdownExecutor(); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java index 0a6e6693..c2697c11 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java @@ -9,14 +9,13 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.ruoyi.chat.enums.ChatModeType; import org.ruoyi.chat.service.chat.IChatService; +import org.ruoyi.chat.support.ChatServiceHelper; import org.ruoyi.common.chat.request.ChatRequest; import org.ruoyi.domain.vo.ChatModelVo; import org.ruoyi.service.IChatModelService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import org.ruoyi.chat.support.RetryNotifier; /** * deepseek */ @@ -58,15 +57,14 @@ public class DeepSeekChatImpl implements IChatService { @Override public void onError(Throwable error) { System.err.println("错误: " + error.getMessage()); - // 通知上层失败,进入重试/降级(以 emitter 为键) - RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, error.getMessage()); } }); } catch (Exception e) { log.error("deepseek请求失败:{}", e.getMessage()); // 同步异常直接通知失败 - RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, e.getMessage()); } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ImageServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ImageServiceImpl.java index 1c4af69d..36aa9c54 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ImageServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ImageServiceImpl.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.*; +import org.ruoyi.chat.support.ChatServiceHelper; /** * 图片识别模型 @@ -131,7 +132,7 @@ public class ImageServiceImpl implements IChatService { // 获取会话token(从入口透传,避免非Web线程取值报错) String token = chatRequest.getToken(); // 创建 SSE 事件源监听器 - SSEEventSourceListener listener = new SSEEventSourceListener(emitter, chatRequest.getUserId(), chatRequest.getSessionId(), token); + SSEEventSourceListener listener = ChatServiceHelper.createOpenAiListener(emitter, chatRequest); // 构建聊天完成请求 ChatCompletion completion = ChatCompletion @@ -142,7 +143,12 @@ public class ImageServiceImpl implements IChatService { .build(); // 发起流式聊天完成请求 - openAiStreamClient.streamChatCompletion(completion, listener); + try { + openAiStreamClient.streamChatCompletion(completion, listener); + } catch (Exception ex) { + ChatServiceHelper.onStreamError(emitter, ex.getMessage()); + throw ex; + } return emitter; } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java index 2401b83e..7ce42215 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import org.ruoyi.chat.support.RetryNotifier; +import org.ruoyi.chat.support.ChatServiceHelper; /** @@ -66,16 +67,14 @@ public class OllamaServiceImpl implements IChatService { try { emitter.send(substr); } catch (IOException e) { - SSEUtil.sendErrorEvent(emitter, e.getMessage()); - RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, e.getMessage()); } }; api.chat(requestModel, streamHandler); emitter.complete(); RetryNotifier.clear(emitter); } catch (Exception e) { - SSEUtil.sendErrorEvent(emitter, e.getMessage()); - RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, e.getMessage()); } }); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java index cc264803..6b8f72d6 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java @@ -1,12 +1,12 @@ package org.ruoyi.chat.service.chat.impl; -import cn.dev33.satoken.stp.StpUtil; import io.modelcontextprotocol.client.McpSyncClient; import lombok.extern.slf4j.Slf4j; import org.ruoyi.chat.config.ChatConfig; import org.ruoyi.chat.enums.ChatModeType; import org.ruoyi.chat.listener.SSEEventSourceListener; import org.ruoyi.chat.service.chat.IChatService; +import org.ruoyi.chat.support.ChatServiceHelper; import org.ruoyi.common.chat.entity.chat.ChatCompletion; import org.ruoyi.common.chat.entity.chat.Message; import org.ruoyi.common.chat.openai.OpenAiStreamClient; @@ -22,7 +22,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.List; -import org.ruoyi.chat.support.RetryNotifier; /** @@ -58,8 +57,7 @@ public class OpenAIServiceImpl implements IChatService { Message userMessage = Message.builder().content("工具返回信息:"+toolString).role(Message.Role.USER).build(); messages.add(userMessage); } - String token = chatRequest.getToken(); - SSEEventSourceListener listener = new SSEEventSourceListener(emitter,chatRequest.getUserId(),chatRequest.getSessionId(), token); + SSEEventSourceListener listener = ChatServiceHelper.createOpenAiListener(emitter, chatRequest); ChatCompletion completion = ChatCompletion .builder() .messages(messages) @@ -69,8 +67,7 @@ public class OpenAIServiceImpl implements IChatService { try { openAiStreamClient.streamChatCompletion(completion, listener); } catch (Exception ex) { - // 同步异常也触发失败回调(以 emitter 为键) - RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, ex.getMessage()); throw ex; } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java index 3f5c00b0..4128b84a 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java @@ -14,6 +14,7 @@ import org.ruoyi.service.IChatModelService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import org.ruoyi.chat.support.ChatServiceHelper; /** @@ -57,12 +58,12 @@ public class QianWenAiChatServiceImpl implements IChatService { @Override public void onError(Throwable error) { error.printStackTrace(); - org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, error.getMessage()); } }); } catch (Exception e) { log.error("千问请求失败:{}", e.getMessage()); - org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, e.getMessage()); } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java index 7405a77b..50e545e0 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ZhipuAiChatServiceImpl.java @@ -15,6 +15,7 @@ import org.ruoyi.service.IChatModelService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import org.ruoyi.chat.support.ChatServiceHelper; @@ -51,9 +52,7 @@ public class ZhipuAiChatServiceImpl implements IChatService { @SneakyThrows @Override public void onError(Throwable error) { - // 透传错误并触发重试(以 emitter 为键) - emitter.send(error.getMessage()); - org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, error.getMessage()); } @Override @@ -73,7 +72,7 @@ public class ZhipuAiChatServiceImpl implements IChatService { model.chat(chatRequest.getPrompt(), handler); } catch (Exception e) { log.error("智谱清言请求失败:{}", e.getMessage()); - org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter); + ChatServiceHelper.onStreamError(emitter, e.getMessage()); } return emitter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/ChatServiceHelper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/ChatServiceHelper.java new file mode 100644 index 00000000..042dee9c --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/ChatServiceHelper.java @@ -0,0 +1,45 @@ +package org.ruoyi.chat.support; + +import org.ruoyi.chat.listener.SSEEventSourceListener; +import org.ruoyi.common.chat.request.ChatRequest; +import org.ruoyi.chat.util.SSEUtil; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +/** + * 抽取各聊天实现类的通用逻辑: + * - 创建带开关的 SSE 监听器 + * - 统一的流错误处理(根据是否在重试场景决定通知或直接结束) + * - 统一的完成处理(清理回调并 complete) + */ +public class ChatServiceHelper { + + public static SSEEventSourceListener createOpenAiListener(SseEmitter emitter, ChatRequest chatRequest) { + boolean retryEnabled = Boolean.TRUE.equals(chatRequest.getAutoSelectModel()); + return new SSEEventSourceListener( + emitter, + chatRequest.getUserId(), + chatRequest.getSessionId(), + chatRequest.getToken(), + retryEnabled + ); + } + + public static void onStreamError(SseEmitter emitter, String errorMessage) { + SSEUtil.sendErrorEvent(emitter, errorMessage); + if (RetryNotifier.hasCallback(emitter)) { + RetryNotifier.notifyFailure(emitter); + } else { + emitter.complete(); + } + } + + public static void onStreamComplete(SseEmitter emitter) { + try { + emitter.complete(); + } finally { + RetryNotifier.clear(emitter); + } + } +} + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java index 25f65c44..c37f82cb 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/support/RetryNotifier.java @@ -39,6 +39,13 @@ public class RetryNotifier { cb.run(); } } + + public static boolean hasCallback(Object emitterLike) { + if (emitterLike == null) { + return false; + } + return FAILURE_CALLBACKS.containsKey(keyOf(emitterLike)); + } } From 7a374d877b30b65d5f27ee4a8084e62c9e7c377b Mon Sep 17 00:00:00 2001 From: l90215 Date: Wed, 20 Aug 2025 17:52:51 +0800 Subject: [PATCH 30/36] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E9=97=AE=E7=AD=94=E6=8E=A5=E5=85=A5=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/service/IPromptTemplateService.java | 7 + .../impl/PromptTemplateServiceImpl.java | 9 ++ .../ruoyi/chat/enums/promptTemplateEnum.java | 22 +++ .../service/chat/impl/SseServiceImpl.java | 131 ++++++++++-------- script/sql/ruoyi-ai.sql | 35 +++-- 5 files changed, 130 insertions(+), 74 deletions(-) create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/enums/promptTemplateEnum.java diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IPromptTemplateService.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IPromptTemplateService.java index 89cff106..5a123ef0 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IPromptTemplateService.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/IPromptTemplateService.java @@ -46,4 +46,11 @@ public interface IPromptTemplateService { * 校验并批量删除提示词模板信息 */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 根据分类查询提示词模板 + * + * @param category 分类 + */ + PromptTemplateVo queryByCategory(String category); } diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/PromptTemplateServiceImpl.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/PromptTemplateServiceImpl.java index 85fdcd40..2f3a7572 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/PromptTemplateServiceImpl.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/service/impl/PromptTemplateServiceImpl.java @@ -109,4 +109,13 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService { } return baseMapper.deleteBatchIds(ids) > 0; } + + @Override + public PromptTemplateVo queryByCategory(String category) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(PromptTemplate.class); + queryWrapper.eq(PromptTemplate::getCategory, category); + queryWrapper.orderByDesc(PromptTemplate::getUpdateTime); + queryWrapper.last("limit 1"); + return baseMapper.selectVoOne(queryWrapper); + } } \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/enums/promptTemplateEnum.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/enums/promptTemplateEnum.java new file mode 100644 index 00000000..f71128fd --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/enums/promptTemplateEnum.java @@ -0,0 +1,22 @@ +package org.ruoyi.chat.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 提示词模板分类 + * + * @author evo + */ +@Getter +@AllArgsConstructor +public enum promptTemplateEnum { + CHAT(1, "chat"), + VECTOR(2, "vector"), + ; + + private final Integer code; + private final String desc; + +} + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index f94d0331..e3f7d82b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -1,10 +1,12 @@ package org.ruoyi.chat.service.chat.impl; +import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.collection.CollectionUtil; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import okhttp3.ResponseBody; +import org.ruoyi.chat.enums.promptTemplateEnum; import org.ruoyi.chat.factory.ChatServiceFactory; import org.ruoyi.chat.service.chat.IChatCostService; import org.ruoyi.chat.service.chat.IChatService; @@ -27,9 +29,11 @@ import org.ruoyi.domain.bo.ChatSessionBo; import org.ruoyi.domain.bo.QueryVectorBo; import org.ruoyi.domain.vo.ChatModelVo; import org.ruoyi.domain.vo.KnowledgeInfoVo; +import org.ruoyi.domain.vo.PromptTemplateVo; import org.ruoyi.service.IChatModelService; import org.ruoyi.service.IChatSessionService; import org.ruoyi.service.IKnowledgeInfoService; +import org.ruoyi.service.IPromptTemplateService; import org.ruoyi.service.VectorStoreService; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; @@ -45,9 +49,8 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; -import cn.dev33.satoken.stp.StpUtil; +import java.util.Objects; /** * @author ageer @@ -73,6 +76,9 @@ public class SseServiceImpl implements ISseService { private ChatModelVo chatModelVo; + // 提示词模板服务 + private final IPromptTemplateService promptTemplateService; + @Override public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) { @@ -89,9 +95,9 @@ public class SseServiceImpl implements ISseService { // 设置对话角色 chatRequest.setRole(Message.Role.USER.getName()); - if(LoginHelper.isLogin()){ + if (LoginHelper.isLogin()) { - // 设置用户id + // 设置用户id chatRequest.setUserId(LoginHelper.getUserId()); @@ -100,10 +106,10 @@ public class SseServiceImpl implements ISseService { //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) { // 设置会话id - if (chatRequest.getUuid() == null){ + if (chatRequest.getUuid() == null) { //暂时随机生成会话id chatRequest.setSessionId(System.currentTimeMillis()); - }else{ + } else { //这里或许需要修改一下,这里应该用uuid 或者 前端传递 sessionId chatRequest.setSessionId(chatRequest.getUuid()); } @@ -114,7 +120,7 @@ public class SseServiceImpl implements ISseService { chatCostService.saveMessage(chatRequest); chatCostService.publishBillingEvent(chatRequest); chatRequest.setUserId(chatCostService.getUserId()); - if(chatRequest.getSessionId()==null){ + if (chatRequest.getSessionId() == null) { ChatSessionBo chatSessionBo = new ChatSessionBo(); chatSessionBo.setUserId(chatCostService.getUserId()); chatSessionBo.setSessionTitle(getFirst10Characters(chatRequest.getPrompt())); @@ -131,29 +137,30 @@ public class SseServiceImpl implements ISseService { ChatModelVo currentModel = this.chatModelVo; String currentCategory = currentModel.getCategory(); ChatRetryHelper.executeWithRetry( - currentModel, - currentCategory, - chatModelService, - sseEmitter, - (modelForTry, onFailure) -> { - // 替换请求中的模型名称 - chatRequest.setModel(modelForTry.getModelName()); - // 以 emitter 实例为唯一键注册失败回调 - RetryNotifier.setFailureCallback(sseEmitter, onFailure); - try { - autoSelectServiceByCategoryAndInvoke(chatRequest, sseEmitter, modelForTry.getCategory()); - } finally { - // 不在此处清理,待下游结束/失败时清理 + currentModel, + currentCategory, + chatModelService, + sseEmitter, + (modelForTry, onFailure) -> { + // 替换请求中的模型名称 + chatRequest.setModel(modelForTry.getModelName()); + // 以 emitter 实例为唯一键注册失败回调 + RetryNotifier.setFailureCallback(sseEmitter, onFailure); + try { + autoSelectServiceByCategoryAndInvoke(chatRequest, sseEmitter, + modelForTry.getCategory()); + } finally { + // 不在此处清理,待下游结束/失败时清理 + } } - } ); } else { // 不重试不降级,直接调用 chatService.chat(chatRequest, sseEmitter); } } catch (Exception e) { - log.error(e.getMessage(),e); - SSEUtil.sendErrorEvent(sseEmitter,e.getMessage()); + log.error(e.getMessage(), e); + SSEUtil.sendErrorEvent(sseEmitter, e.getMessage()); } return sseEmitter; } @@ -170,7 +177,7 @@ public class SseServiceImpl implements ISseService { } else { chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); } - + if (chatModelVo == null) { throw new IllegalStateException("未找到模型名称:" + chatRequest.getModel()); } @@ -191,7 +198,7 @@ public class SseServiceImpl implements ISseService { IChatService service = chatServiceFactory.getChatService(category); service.chat(chatRequest, sseEmitter); } - + /** * 根据分类选择优先级最高的模型 */ @@ -221,23 +228,23 @@ public class SseServiceImpl implements ISseService { } /** - * 构建消息列表 + * 构建消息列表 */ - private void buildChatMessageList(ChatRequest chatRequest){ + private void buildChatMessageList(ChatRequest chatRequest) { List messages = chatRequest.getMessages(); - + // 处理知识库相关逻辑 String sysPrompt = processKnowledgeBase(chatRequest, messages); - + // 设置系统提示词 Message sysMessage = Message.builder() .content(sysPrompt) .role(Message.Role.SYSTEM) .build(); messages.add(0, sysMessage); - + chatRequest.setSysPrompt(sysPrompt); - + // 用户对话内容 String chatString = null; // 获取用户对话信息 @@ -251,54 +258,55 @@ public class SseServiceImpl implements ISseService { } chatRequest.setPrompt(chatString); } - + /** * 处理知识库相关逻辑 */ private String processKnowledgeBase(ChatRequest chatRequest, List messages) { if (StringUtils.isEmpty(chatRequest.getKid())) { - return getDefaultSystemPrompt(); + return getPromptTemplatePrompt(promptTemplateEnum.VECTOR.getDesc()); } - + try { // 查询知识库信息 KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKid())); if (knowledgeInfoVo == null) { log.warn("知识库信息不存在,kid: {}", chatRequest.getKid()); - return getDefaultSystemPrompt(); + return getPromptTemplatePrompt(promptTemplateEnum.VECTOR.getDesc()); } - + // 查询向量模型配置信息 ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModelName()); if (chatModel == null) { log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModelName()); - return getDefaultSystemPrompt(); + return getPromptTemplatePrompt(promptTemplateEnum.VECTOR.getDesc()); } - + // 构建向量查询参数 QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel); - + // 获取向量查询结果 List nearestList = vectorStoreService.getQueryVector(queryVectorBo); - + // 添加知识库消息到上下文 addKnowledgeMessages(messages, nearestList); - + // 返回知识库系统提示词 return getKnowledgeSystemPrompt(knowledgeInfoVo); - + } catch (Exception e) { log.error("处理知识库信息失败: {}", e.getMessage(), e); - return getDefaultSystemPrompt(); + return getPromptTemplatePrompt(promptTemplateEnum.VECTOR.getDesc()); } } - + /** * 构建向量查询参数 */ - private QueryVectorBo buildQueryVectorBo(ChatRequest chatRequest, KnowledgeInfoVo knowledgeInfoVo, ChatModelVo chatModel) { + private QueryVectorBo buildQueryVectorBo(ChatRequest chatRequest, KnowledgeInfoVo knowledgeInfoVo, + ChatModelVo chatModel) { String content = chatRequest.getMessages().get(chatRequest.getMessages().size() - 1).getContent().toString(); - + QueryVectorBo queryVectorBo = new QueryVectorBo(); queryVectorBo.setQuery(content); queryVectorBo.setKid(chatRequest.getKid()); @@ -307,10 +315,10 @@ public class SseServiceImpl implements ISseService { queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModelName()); queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModelName()); queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit()); - + return queryVectorBo; } - + /** * 添加知识库消息到上下文 */ @@ -323,7 +331,7 @@ public class SseServiceImpl implements ISseService { messages.add(userMessage); } } - + /** * 获取知识库系统提示词 */ @@ -340,16 +348,29 @@ public class SseServiceImpl implements ISseService { } return sysPrompt; } - + + + /** + * 获取提示词模板提示词 + */ + private String getPromptTemplatePrompt(String category) { + PromptTemplateVo promptTemplateVo = promptTemplateService.queryByCategory(category); + if (Objects.isNull(promptTemplateVo) || StringUtils.isEmpty(promptTemplateVo.getTemplateContent())) { + return getDefaultSystemPrompt(); + } + return promptTemplateVo.getTemplateContent(); + } + /** * 获取默认系统提示词 */ private String getDefaultSystemPrompt() { String sysPrompt = chatModelVo != null ? chatModelVo.getSystemPrompt() : null; if (StringUtils.isEmpty(sysPrompt)) { - sysPrompt = "你是一个由RuoYI-AI开发的人工智能助手,名字叫熊猫助手。你擅长中英文对话,能够理解并处理各种问题,提供安全、有帮助、准确的回答。" + - "当前时间:" + DateUtils.getDate() + - "#注意:回复之前注意结合上下文和工具返回内容进行回复。"; + sysPrompt = "你是一个由RuoYI-AI开发的人工智能助手,名字叫RuoYI人工智能助手。" + + "你擅长中英文对话,能够理解并处理各种问题,提供安全、有帮助、准确的回答。" + + "当前时间:" + DateUtils.getDate() + + "#注意:回复之前注意结合上下文和工具返回内容进行回复。"; } return sysPrompt; } @@ -366,8 +387,8 @@ public class SseServiceImpl implements ISseService { InputStreamResource resource = new InputStreamResource(body.byteStream()); // 创建并返回ResponseEntity return ResponseEntity.ok() - .contentType(MediaType.parseMediaType("audio/mpeg")) - .body(resource); + .contentType(MediaType.parseMediaType("audio/mpeg")) + .body(resource); } else { // 如果ResponseBody为空,返回404状态码 return ResponseEntity.notFound().build(); diff --git a/script/sql/ruoyi-ai.sql b/script/sql/ruoyi-ai.sql index 2c824c8f..663fa1b4 100644 --- a/script/sql/ruoyi-ai.sql +++ b/script/sql/ruoyi-ai.sql @@ -647,28 +647,25 @@ INSERT INTO `sys_menu` VALUES (1929170702299045893, '提示词模板修改', 192 INSERT INTO `sys_menu` VALUES (1929170702299045894, '提示词模板删除', 1929170702299045890, '4', '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:remove', '#', 103, 1, sysdate(), null, null, ''); INSERT INTO `sys_menu` VALUES (1929170702299045895, '提示词模板导出', 1929170702299045890, '5', '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:export', '#', 103, 1, sysdate(), null, null, ''); +INSERT INTO sys_menu VALUES (2000, '在线开发', 0, 20, 'dev', '', '', 1, 0, 'M', '0', '0', '', 'carbon:development', 103, 1, '2025-07-11 19:38:05', 1, '2025-07-11 19:43:03', '在线开发目录'); +INSERT INTO sys_menu VALUES (1944213468857495553, '模型分组', 2000, 1, 'schemaGroup', 'dev/schemaGroup/index', null, 1, 0, 'C', '0', '0', null, '#', 103, 1, '2025-07-13 09:53:07', 1, '2025-07-13 09:54:45', '模型分组菜单'); +INSERT INTO sys_menu VALUES (1944229086906281985, '数据模型', 2000, 2, 'schema', 'dev/schema/index', null, 1, 0, 'C', '0', '0', null, '#', 103, 1, '2025-07-13 10:55:11', null, '2025-07-13 10:55:11', '数据模型菜单'); +INSERT INTO sys_menu VALUES (1946466176918249473, '模型字段管理', 2000, 3, 'schemaField', 'dev/schemaField/index', null, 1, 0, 'C', '0', '0', null, '#', 103, 1, '2025-07-19 15:04:35', null, '2025-07-19 15:04:35', '模型字段管理菜单'); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (2000, '在线开发', 0, 20, 'dev', '', '', 1, 0, 'M', '0', '0', '', 'carbon:development', 103, 1, '2025-07-11 19:38:05', 1, '2025-07-11 19:43:03', '在线开发目录'); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944213468857495553, '模型分组', 2000, 1, 'schemaGroup', 'dev/schemaGroup/index', null, 1, 0, 'C', '0', '0', null, '#', 103, 1, '2025-07-13 09:53:07', 1, '2025-07-13 09:54:45', '模型分组菜单'); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944229086906281985, '数据模型', 2000, 2, 'schema', 'dev/schema/index', null, 1, 0, 'C', '0', '0', null, '#', 103, 1, '2025-07-13 10:55:11', null, '2025-07-13 10:55:11', '数据模型菜单'); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1946466176918249473, '模型字段管理', 2000, 3, 'schemaField', 'dev/schemaField/index', null, 1, 0, 'C', '0', '0', null, '#', 103, 1, '2025-07-19 15:04:35', null, '2025-07-19 15:04:35', '模型字段管理菜单'); +INSERT INTO sys_menu VALUES (1944213468857495554, '模型分组查询', 1944213468857495553, 1, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaGroup:list', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1944213468857495555, '模型分组新增', 1944213468857495553, 2, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaGroup:add', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1944213468857495556, '模型分组修改', 1944213468857495553, 3, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaGroup:edit', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1944213468857495557, '模型分组删除', 1944213468857495553, 4, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaGroup:remove', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944213468857495554, '模型分组查询', 1944213468857495553, 1, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaGroup:list', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944213468857495555, '模型分组新增', 1944213468857495553, 2, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaGroup:add', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944213468857495556, '模型分组修改', 1944213468857495553, 3, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaGroup:edit', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944213468857495557, '模型分组删除', 1944213468857495553, 4, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaGroup:remove', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1944229086906281986, '模型数据查询', 1944229086906281985, 1, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schema:list', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1944229086906281987, '模型数据新增', 1944229086906281985, 2, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schema:add', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1944229086906281988, '模型数据修改', 1944229086906281985, 3, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schema:edit', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1944229086906281989, '模型数据删除', 1944229086906281985, 4, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schema:remove', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); - -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944229086906281986, '模型数据查询', 1944229086906281985, 1, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schema:list', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944229086906281987, '模型数据新增', 1944229086906281985, 2, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schema:add', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944229086906281988, '模型数据修改', 1944229086906281985, 3, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schema:edit', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1944229086906281989, '模型数据删除', 1944229086906281985, 4, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schema:remove', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); - - -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1946466176918249474, '模型字段管理查询', 1946466176918249473, 1, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaField:list', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1946466176918249475, '模型字段管理新增', 1946466176918249473, 2, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaField:add', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1946466176918249476, '模型字段管理修改', 1946466176918249473, 3, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaField:edit', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1946466176918249477, '模型字段管理删除', 1946466176918249473, 4, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaField:remove', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1946466176918249474, '模型字段管理查询', 1946466176918249473, 1, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaField:list', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1946466176918249475, '模型字段管理新增', 1946466176918249473, 2, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaField:add', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1946466176918249476, '模型字段管理修改', 1946466176918249473, 3, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaField:edit', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); +INSERT INTO sys_menu VALUES (1946466176918249477, '模型字段管理删除', 1946466176918249473, 4, '#', '', null, 1, 0, 'F', '0', '0', 'dev:schemaField:remove', '#', 103, 1, '2025-06-24 19:06:58', null, null, ''); From ef69778bb759dd241a1d33ade6770579e3678c34 Mon Sep 17 00:00:00 2001 From: likunlong Date: Wed, 20 Aug 2025 16:09:53 +0800 Subject: [PATCH 31/36] =?UTF-8?q?feat:=20=E4=B8=8B=E6=8E=89=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=83=BD=E5=8A=9B=E9=80=BB=E8=BE=91=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/ruoyi/domain/ChatModel.java | 7 +- .../java/org/ruoyi/domain/vo/ChatModelVo.java | 136 +----------------- script/sql/update/20250808.sql | 3 - 3 files changed, 2 insertions(+), 144 deletions(-) delete mode 100644 script/sql/update/20250808.sql diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java index e2c75c7e..e8f0e308 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java @@ -8,7 +8,7 @@ import lombok.EqualsAndHashCode; import org.ruoyi.core.domain.BaseEntity; import java.io.Serial; -import java.util.List; + /** * 聊天模型对象 chat_model @@ -86,10 +86,5 @@ public class ChatModel extends BaseEntity { */ private String remark; - /** - * 模型能力 - */ - private String modelCapability; - } diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java index 257c0ec3..0638c13a 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java @@ -5,14 +5,13 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; -import lombok.Getter; import org.ruoyi.common.sensitive.annotation.Sensitive; import org.ruoyi.common.sensitive.core.SensitiveStrategy; import org.ruoyi.domain.ChatModel; import java.io.Serial; import java.io.Serializable; -import java.util.List; + /** @@ -102,138 +101,5 @@ public class ChatModelVo implements Serializable { @ExcelProperty(value = "备注") private String remark; - /** - * 模型能力 - */ - @ExcelProperty(value = "模型能力") - private String modelCapability; - /** - * 模型能力列表 - */ - private List modelAbilities = getModelAbilities(); - - /** - * 模型能力类,类似枚举的静态内部类 - */ - @Getter - public static final class Ability { - // 获取能力名称 - private final String name; - private final String description; - - // 静态字段存储默认能力(类似枚举常量) - public static final Ability IMAGE = new Ability("IMAGE", "图片理解"); - public static final Ability VIDEO = new Ability("VIDEO", "视频理解"); - public static final Ability SPEECH = new Ability("SPEECH", "语音理解"); - - // 动态扩展能力存储 - private static final java.util.Map EXTENDED_ABILITIES = new java.util.HashMap<>(); - - // 私有构造确保受限 - private Ability(String name, String description) { - this.name = name; - this.description = description; - } - - // 静态工厂方法(类似枚举valueOf) - public static Ability valueOf(String name) { - // 先检查默认能力 - switch (name) { - case "IMAGE": return IMAGE; - case "VIDEO": return VIDEO; - case "SPEECH": return SPEECH; - default: - // 检查扩展能力 - Ability ability = EXTENDED_ABILITIES.get(name); - if (ability != null) return ability; - throw new IllegalArgumentException("Unknown ability: " + name); - } - } - - // 动态注册新能力(后期从数据库调用) - public static synchronized Ability registerAbility(String name, String description) { - if (name == null || name.trim().isEmpty()) { - throw new IllegalArgumentException("Ability name cannot be empty"); - } - - // 避免重复注册 - if (EXTENDED_ABILITIES.containsKey(name)) { - return EXTENDED_ABILITIES.get(name); - } - - // 检查是否与默认能力冲突 - try { - valueOf(name); - throw new IllegalArgumentException("Ability already exists as default: " + name); - } catch (IllegalArgumentException e) { - // 正常情况,继续注册 - } - - Ability newAbility = new Ability(name, description); - EXTENDED_ABILITIES.put(name, newAbility); - return newAbility; - } - - // 获取所有能力(默认+扩展) - public static java.util.Set getAllAbilities() { - java.util.Map all = new java.util.HashMap<>(); - all.put(IMAGE.name, IMAGE); - all.put(VIDEO.name, VIDEO); - all.put(SPEECH.name, SPEECH); - all.putAll(EXTENDED_ABILITIES); - return java.util.Collections.unmodifiableSet((java.util.Set) all.values()); - } - - @Override - public String toString() { - return name; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Ability ability = (Ability) o; - return name.equals(ability.name); - } - - @Override - public int hashCode() { - return name.hashCode(); - } - } - - /** - * 将 modelCapability 字符串转换为 Ability 列表 - * @return Ability 列表 - */ - public java.util.List getModelAbilities() { - if (modelCapability == null || modelCapability.trim().isEmpty()) { - return java.util.Collections.emptyList(); - } - - // 解析 JSON 格式的字符串数组 - String trimmed = modelCapability.trim(); - if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) { - throw new IllegalArgumentException("Invalid modelCapability format: " + modelCapability); - } - - String content = trimmed.substring(1, trimmed.length() - 1).trim(); - if (content.isEmpty()) { - return java.util.Collections.emptyList(); - } - - java.util.List abilities = new java.util.ArrayList<>(); - String[] items = content.split(","); - for (String item : items) { - String cleanedItem = item.trim(); - if (cleanedItem.startsWith("\"") && cleanedItem.endsWith("\"") && cleanedItem.length() >= 2) { - String abilityName = cleanedItem.substring(1, cleanedItem.length() - 1); - abilities.add(Ability.valueOf(abilityName)); - } - } - - return abilities; - } } \ No newline at end of file diff --git a/script/sql/update/20250808.sql b/script/sql/update/20250808.sql deleted file mode 100644 index 3c54c207..00000000 --- a/script/sql/update/20250808.sql +++ /dev/null @@ -1,3 +0,0 @@ --- 聊天模型表添加模型能力字段 -alter table chat_model - add model_capability varchar(255) default '[]' not null comment '模型能力'; From 6e6ba84fd28d27bcd1ee73998629bfe7749e387b Mon Sep 17 00:00:00 2001 From: AmAzing129 Date: Fri, 22 Aug 2025 14:29:03 +0800 Subject: [PATCH 32/36] docs: add contributors bubble --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 75be1549..9b223e4e 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,10 @@ > 💡 **小贴士**:建议将 PR 提交到 GitHub,我们会自动同步到其他代码托管平台 + + Contribution Leaderboard + + ## 📄 开源协议 本项目采用 **MIT 开源协议**,详情请查看 [LICENSE](LICENSE) 文件。 From 9f7f00e50cfa4119684643e3e4b3c1661a3d6b68 Mon Sep 17 00:00:00 2001 From: Administrator <1037463791@qq.com> Date: Wed, 27 Aug 2025 15:30:59 +0800 Subject: [PATCH 33/36] =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=B6=88=E6=81=AF=20=E2=86=92=20=E9=A2=84=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=20=E2=86=92=20=E4=BF=9D=E5=AD=98=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=B6=88=E6=81=AF=20=E2=86=92=20=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E8=AE=A1=E8=B4=B9=E4=BA=8B=E4=BB=B6=20=E2=86=92=20=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=89=A3=E8=B4=B9=20=E2=86=92=20=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E8=B4=A6=E5=8D=95=E8=AE=B0=E5=BD=95=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86billingType=E8=AE=A1=E8=B4=B9=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E6=B6=88=E6=81=AF=E4=BF=9D=E5=AD=98=E7=9A=84?= =?UTF-8?q?=E6=97=B6=E5=80=99=E5=86=99=E5=85=A5=E8=BF=9B=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/ruoyi/domain/ChatMessage.java | 5 ++ .../org/ruoyi/domain/bo/ChatMessageBo.java | 5 ++ .../org/ruoyi/domain/vo/ChatMessageVo.java | 10 ++++ .../chat/impl/ChatCostServiceImpl.java | 60 +++++++++++++++---- .../service/chat/impl/SseServiceImpl.java | 24 ++++---- .../sql/update/chat-message-billing-type.sql | 4 ++ 6 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 script/sql/update/chat-message-billing-type.sql diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatMessage.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatMessage.java index 4adcded6..6a4fac2c 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatMessage.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatMessage.java @@ -69,5 +69,10 @@ public class ChatMessage extends BaseEntity { */ private String remark; + /** + * 计费类型(1-token计费,2-次数计费,null-普通消息) + */ + private String billingType; + } diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatMessageBo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatMessageBo.java index fd228f73..5b22b008 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatMessageBo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatMessageBo.java @@ -75,5 +75,10 @@ public class ChatMessageBo extends BaseEntity { @NotBlank(message = "备注不能为空", groups = { AddGroup.class, EditGroup.class }) private String remark; + /** + * 计费类型(1-token计费,2-次数计费,null-普通消息) + */ + private String billingType; + } diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatMessageVo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatMessageVo.java index 00920ef9..140398fc 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatMessageVo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatMessageVo.java @@ -4,6 +4,8 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; +import org.ruoyi.common.excel.annotation.ExcelDictFormat; +import org.ruoyi.common.excel.convert.ExcelDictConvert; import org.ruoyi.domain.ChatMessage; import java.io.Serial; @@ -73,6 +75,13 @@ public class ChatMessageVo implements Serializable { @ExcelProperty(value = "模型名称") private String modelName; + /** + * 计费类型(1-token计费,2-次数计费) + */ + @ExcelProperty(value = "计费类型", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "sys_model_billing") + private String billingType; + /** * 备注 */ @@ -87,4 +96,5 @@ public class ChatMessageVo implements Serializable { private Date createTime; + } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java index 6947893d..22fffce2 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java @@ -80,7 +80,7 @@ public class ChatCostServiceImpl implements IChatCostService { } // 记录账单消息 - saveBillingRecord(chatRequest, tokens, numberCost.doubleValue(), "TIMES_BILLING"); + saveBillingRecord(chatRequest, tokens, numberCost.doubleValue(), chatModelVo.getModelType()); return; } @@ -121,7 +121,7 @@ public class ChatCostServiceImpl implements IChatCostService { log.debug("deductToken->扣费成功,更新余数: {}", remainder); // 记录账单消息 - saveBillingRecord(chatRequest, billable, numberCost.doubleValue(), "TOKEN_BILLING"); + saveBillingRecord(chatRequest, billable, numberCost.doubleValue(), chatModelVo.getModelType()); } catch (ServiceException e) { // 余额不足时,不更新token累计,保持原有累计数 log.warn("deductToken->余额不足,本次token累计保持不变: {}", totalTokens); @@ -167,6 +167,19 @@ public class ChatCostServiceImpl implements IChatCostService { // 普通消息不涉及扣费,deductCost保持null chatMessageBo.setDeductCost(null); chatMessageBo.setRemark("用户消息"); + + // 设置计费类型(根据模型配置获取计费类型) + try { + ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); + if (chatModelVo != null) { + chatMessageBo.setBillingType(chatModelVo.getModelType()); + } else { + chatMessageBo.setBillingType(null); // 模型不存在时设为null + } + } catch (Exception e) { + log.warn("saveMessage->获取模型计费类型失败,设为null,模型: {}", chatRequest.getModel()); + chatMessageBo.setBillingType(null); + } try { chatMessageService.insertByBo(chatMessageBo); @@ -261,7 +274,7 @@ public class ChatCostServiceImpl implements IChatCostService { /** * 保存账单记录 */ - private void saveBillingRecord(ChatRequest chatRequest, int billedTokens, double cost, String billingType) { + private void saveBillingRecord(ChatRequest chatRequest, int billedTokens, double cost, String billingTypeCode) { try { ChatMessageBo billingMessage = new ChatMessageBo(); billingMessage.setUserId(chatRequest.getUserId()); @@ -270,26 +283,47 @@ public class ChatCostServiceImpl implements IChatCostService { billingMessage.setModelName(chatRequest.getModel()); billingMessage.setTotalTokens(billedTokens); billingMessage.setDeductCost(cost); - billingMessage.setRemark(billingType); + billingMessage.setRemark(getBillingTypeName(billingTypeCode)); - // 构建账单消息内容 - String content; - if ("TIMES_BILLING".equals(billingType)) { - content = String.format("按次计费:消耗 %d tokens,扣费 %.2f 元", billedTokens, cost); - } else { - content = String.format("按量计费:结算 %d tokens,扣费 %.2f 元", billedTokens, cost); - } - billingMessage.setContent(content); + // 设置计费类型和构建消息内容 + setBillingTypeAndContent(billingMessage, billingTypeCode, billedTokens, cost); chatMessageService.insertByBo(billingMessage); log.debug("saveBillingRecord->保存账单记录成功,用户ID: {}, 计费类型: {}, 费用: {}", - chatRequest.getUserId(), billingType, cost); + chatRequest.getUserId(), billingTypeCode, cost); } catch (Exception e) { log.error("saveBillingRecord->保存账单记录失败", e); // 账单记录失败不影响主流程,只记录错误日志 } } + /** + * 设置计费类型和构建消息内容 + */ + private void setBillingTypeAndContent(ChatMessageBo billingMessage, String billingTypeCode, int billedTokens, double cost) { + billingMessage.setBillingType(billingTypeCode); + + // 使用枚举获取计费类型并构建消息内容 + BillingType billingType = BillingType.fromCode(billingTypeCode); + if (billingType != null) { + String content = switch (billingType) { + case TIMES -> String.format("%s:消耗 %d tokens,扣费 %.2f 元", billingType.getDescription(), billedTokens, cost); + case TOKEN -> String.format("%s:结算 %d tokens,扣费 %.2f 元", billingType.getDescription(), billedTokens, cost); + }; + billingMessage.setContent(content); + } else { + billingMessage.setContent(String.format("系统计费:处理 %d tokens,扣费 %.2f 元", billedTokens, cost)); + } + } + + /** + * 获取计费类型名称(用于remark字段) + */ + private String getBillingTypeName(String billingTypeCode) { + BillingType billingType = BillingType.fromCode(billingTypeCode); + return billingType != null ? billingType.getDescription() : "系统计费"; + } + /** * 从用户余额中扣除费用 * diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index e3f7d82b..d07cce19 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -104,21 +104,21 @@ public class SseServiceImpl implements ISseService { //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) - { - // 设置会话id - if (chatRequest.getUuid() == null) { - //暂时随机生成会话id - chatRequest.setSessionId(System.currentTimeMillis()); - } else { - //这里或许需要修改一下,这里应该用uuid 或者 前端传递 sessionId - chatRequest.setSessionId(chatRequest.getUuid()); - } - } +// { +// // 设置会话id +// if (chatRequest.getUuid() == null) { +// //暂时随机生成会话id +// chatRequest.setSessionId(System.currentTimeMillis()); +// } else { +// //这里或许需要修改一下,这里应该用uuid 或者 前端传递 sessionId +// chatRequest.setSessionId(chatRequest.getUuid()); +// } +// } // 先保存消息,再发布异步计费事件 chatCostService.saveMessage(chatRequest); - chatCostService.publishBillingEvent(chatRequest); + chatRequest.setUserId(chatCostService.getUserId()); if (chatRequest.getSessionId() == null) { ChatSessionBo chatSessionBo = new ChatSessionBo(); @@ -131,7 +131,7 @@ public class SseServiceImpl implements ISseService { } // 自动选择模型并获取对应的聊天服务 IChatService chatService = autoSelectModelAndGetService(chatRequest); - + chatCostService.publishBillingEvent(chatRequest); // 仅当 autoSelectModel = true 时,才启用重试与降级 if (Boolean.TRUE.equals(chatRequest.getAutoSelectModel())) { ChatModelVo currentModel = this.chatModelVo; diff --git a/script/sql/update/chat-message-billing-type.sql b/script/sql/update/chat-message-billing-type.sql new file mode 100644 index 00000000..364645a8 --- /dev/null +++ b/script/sql/update/chat-message-billing-type.sql @@ -0,0 +1,4 @@ +-- 为 chat_message 表添加 billing_type 字段 +ALTER TABLE chat_message + ADD COLUMN billing_type char NULL COMMENT '计费类型(1-token计费,2-次数计费,null-普通消息)'; + From 1e3b49c9b8aa572baa961d3d60a7e1a05243fc90 Mon Sep 17 00:00:00 2001 From: Administrator <1037463791@qq.com> Date: Wed, 27 Aug 2025 16:48:48 +0800 Subject: [PATCH 34/36] =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=B6=88=E6=81=AF=20=E2=86=92=20=E9=A2=84=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=20=E2=86=92=20=E4=BF=9D=E5=AD=98=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=B6=88=E6=81=AF=20=E2=86=92=20=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E8=AE=A1=E8=B4=B9=E4=BA=8B=E4=BB=B6=20=E2=86=92=20=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=89=A3=E8=B4=B9=20=E2=86=92=20=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E8=B4=A6=E5=8D=95=E8=AE=B0=E5=BD=95=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86billingType=E8=AE=A1=E8=B4=B9=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E6=B6=88=E6=81=AF=E4=BF=9D=E5=AD=98=E7=9A=84?= =?UTF-8?q?=E6=97=B6=E5=80=99=E5=86=99=E5=85=A5=E8=BF=9B=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/chat/impl/SseServiceImpl.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index d07cce19..eb9dba8b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -104,16 +104,16 @@ public class SseServiceImpl implements ISseService { //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) -// { -// // 设置会话id -// if (chatRequest.getUuid() == null) { -// //暂时随机生成会话id -// chatRequest.setSessionId(System.currentTimeMillis()); -// } else { -// //这里或许需要修改一下,这里应该用uuid 或者 前端传递 sessionId -// chatRequest.setSessionId(chatRequest.getUuid()); -// } -// } + { + // 设置会话id + if (chatRequest.getUuid() == null) { + //暂时随机生成会话id + chatRequest.setSessionId(System.currentTimeMillis()); + } else { + //这里或许需要修改一下,这里应该用uuid 或者 前端传递 sessionId + chatRequest.setSessionId(chatRequest.getUuid()); + } + } // 先保存消息,再发布异步计费事件 From 1e4af3d01b732f3d8fd59d7e2826306392a15759 Mon Sep 17 00:00:00 2001 From: Administrator <1037463791@qq.com> Date: Fri, 29 Aug 2025 15:19:37 +0800 Subject: [PATCH 35/36] =?UTF-8?q?fix(billing):=20=E4=BF=AE=E5=A4=8DToken?= =?UTF-8?q?=E8=AE=A1=E8=B4=B9=E9=80=BB=E8=BE=91=E5=92=8C=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复Token计费算法:按批次计费而非Token数量计费 * 添加ChatRequest.messageId字段支持消息关联更新 * 优化消息保存流程:分离基础信息保存和计费信息更新 * 修复预检查逻辑:统一预检查和实际扣费计算方式 * 调整Token阈值:100 → 1000,减少扣费频次 * 完善事件传递:ChatMessageCreatedEvent增加messageId Fixes: 余额预检查误判、消息计费信息缺失、Token计费不准确 --- .../common/chat/request/ChatRequest.java | 5 + .../chat/event/ChatMessageCreatedEvent.java | 5 +- .../chat/listener/BillingEventListener.java | 1 + .../chat/impl/ChatCostServiceImpl.java | 198 ++++++++++-------- .../service/chat/impl/DifyServiceImpl.java | 6 +- 5 files changed, 126 insertions(+), 89 deletions(-) diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java index 71ebc4e5..f0eacdfc 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java @@ -82,4 +82,9 @@ public class ChatRequest { */ private String token; + /** + * 消息ID(保存消息成功后设置,用于后续扣费更新) + */ + private Long messageId; + } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/event/ChatMessageCreatedEvent.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/event/ChatMessageCreatedEvent.java index 7ecaea26..be067f15 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/event/ChatMessageCreatedEvent.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/event/ChatMessageCreatedEvent.java @@ -12,14 +12,16 @@ public class ChatMessageCreatedEvent extends ApplicationEvent { private final String modelName; private final String role; private final String content; + private final Long messageId; - public ChatMessageCreatedEvent(Long userId, Long sessionId, String modelName, String role, String content) { + public ChatMessageCreatedEvent(Long userId, Long sessionId, String modelName, String role, String content, Long messageId) { super(userId); this.userId = userId; this.sessionId = sessionId; this.modelName = modelName; this.role = role; this.content = content; + this.messageId = messageId; } public Long getUserId() { return userId; } @@ -27,5 +29,6 @@ public class ChatMessageCreatedEvent extends ApplicationEvent { public String getModelName() { return modelName; } public String getRole() { return role; } public String getContent() { return content; } + public Long getMessageId() { return messageId; } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java index 74eda273..91631f87 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/BillingEventListener.java @@ -30,6 +30,7 @@ public class BillingEventListener { chatRequest.setModel(event.getModelName()); chatRequest.setRole(event.getRole()); chatRequest.setPrompt(event.getContent()); + chatRequest.setMessageId(event.getMessageId()); // 设置消息ID // 异步执行计费累计与扣费 log.debug("BillingEventListener->开始执行计费逻辑"); chatCostService.deductToken(chatRequest); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java index 22fffce2..9c1cf04f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java @@ -70,7 +70,7 @@ public class ChatCostServiceImpl implements IChatCostService { BigDecimal numberCost = unitPrice.setScale(2, RoundingMode.HALF_UP); deductUserBalance(chatRequest.getUserId(), numberCost.doubleValue()); log.debug("deductToken->按次数扣费,费用: {},模型: {}", numberCost, modelName); - + // 清理可能存在的历史累计token(模型计费方式可能发生过变更) ChatUsageToken existingToken = chatTokenService.queryByUserId(chatRequest.getUserId(), modelName); if (existingToken != null && existingToken.getToken() > 0) { @@ -78,14 +78,14 @@ public class ChatCostServiceImpl implements IChatCostService { chatTokenService.editToken(existingToken); log.debug("deductToken->按次计费,清理历史累计token: {}", existingToken.getToken()); } - - // 记录账单消息 - saveBillingRecord(chatRequest, tokens, numberCost.doubleValue(), chatModelVo.getModelType()); + + // 更新消息的计费信息到备注 + updateMessageBilling(chatRequest, tokens, numberCost.doubleValue(), chatModelVo.getModelType()); return; } // 按token计费:累加并按阈值批量扣费,保留余数 - final int threshold = 100; + final int threshold = 1000; // 获得记录的累计token数 // TODO: 这里存在并发竞态条件,需要在chatTokenService层面添加乐观锁或分布式锁 @@ -105,11 +105,14 @@ public class ChatCostServiceImpl implements IChatCostService { int remainder = totalTokens - billable; // 结算后保留的余数 if (billable > 0) { + // 计算批次数:每1000个Token为一批,每批扣费单价 + int batches = billable / threshold; BigDecimal numberCost = unitPrice - .multiply(BigDecimal.valueOf(billable)) + .multiply(BigDecimal.valueOf(batches)) .setScale(2, RoundingMode.HALF_UP); - log.debug("deductToken->按token扣费,结算token数量: {},单价: {},费用: {}", billable, unitPrice, numberCost); - + log.debug("deductToken->按token扣费,结算token数量: {},批次数: {},单价: {},费用: {}", + billable, batches, unitPrice, numberCost); + try { // 先尝试扣费 deductUserBalance(chatRequest.getUserId(), numberCost.doubleValue()); @@ -119,9 +122,9 @@ public class ChatCostServiceImpl implements IChatCostService { chatToken.setToken(remainder); chatTokenService.editToken(chatToken); log.debug("deductToken->扣费成功,更新余数: {}", remainder); - - // 记录账单消息 - saveBillingRecord(chatRequest, billable, numberCost.doubleValue(), chatModelVo.getModelType()); + + // 更新消息的计费信息到备注 + updateMessageBilling(chatRequest, billable, numberCost.doubleValue(), chatModelVo.getModelType()); } catch (ServiceException e) { // 余额不足时,不更新token累计,保持原有累计数 log.warn("deductToken->余额不足,本次token累计保持不变: {}", totalTokens); @@ -134,11 +137,15 @@ public class ChatCostServiceImpl implements IChatCostService { chatToken.setUserId(chatRequest.getUserId()); chatToken.setToken(totalTokens); chatTokenService.editToken(chatToken); + + // 虽未扣费,但要更新消息的基本信息(实际token数、计费类型等) + updateMessageWithoutBilling(chatRequest, tokens, chatModelVo.getModelType()); } } /** * 保存聊天消息记录(不进行计费) + * 保存成功后将消息ID设置到ChatRequest中,供后续扣费使用 */ @Override public void saveMessage(ChatRequest chatRequest) { @@ -146,45 +153,32 @@ public class ChatCostServiceImpl implements IChatCostService { log.warn("saveMessage->用户ID或会话ID为空,跳过保存消息"); return; } - + // 验证消息内容 if (chatRequest.getPrompt() == null || chatRequest.getPrompt().trim().isEmpty()) { log.warn("saveMessage->消息内容为空,跳过保存"); return; } - + ChatMessageBo chatMessageBo = new ChatMessageBo(); chatMessageBo.setUserId(chatRequest.getUserId()); chatMessageBo.setSessionId(chatRequest.getSessionId()); chatMessageBo.setRole(chatRequest.getRole()); chatMessageBo.setContent(chatRequest.getPrompt().trim()); chatMessageBo.setModelName(chatRequest.getModel()); - - // 计算并保存本次消息的token数 - int tokens = TikTokensUtil.tokens(chatRequest.getModel(), chatRequest.getPrompt()); - chatMessageBo.setTotalTokens(tokens); - - // 普通消息不涉及扣费,deductCost保持null - chatMessageBo.setDeductCost(null); - chatMessageBo.setRemark("用户消息"); - - // 设置计费类型(根据模型配置获取计费类型) - try { - ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); - if (chatModelVo != null) { - chatMessageBo.setBillingType(chatModelVo.getModelType()); - } else { - chatMessageBo.setBillingType(null); // 模型不存在时设为null - } - } catch (Exception e) { - log.warn("saveMessage->获取模型计费类型失败,设为null,模型: {}", chatRequest.getModel()); - chatMessageBo.setBillingType(null); - } + +// // 基础消息信息,计费相关数据(tokens、费用、计费类型等)在扣费时统一设置 +// chatMessageBo.setTotalTokens(0); // 初始设为0,扣费时更新 +// chatMessageBo.setDeductCost(null); +// chatMessageBo.setBillingType(null); +// chatMessageBo.setRemark("用户消息"); try { chatMessageService.insertByBo(chatMessageBo); - log.debug("saveMessage->成功保存消息,用户ID: {}, 会话ID: {}, tokens: {}", - chatRequest.getUserId(), chatRequest.getSessionId(), tokens); + // 保存成功后,将生成的消息ID设置到ChatRequest中 + chatRequest.setMessageId(chatMessageBo.getId()); + log.debug("saveMessage->成功保存消息,消息ID: {}, 用户ID: {}, 会话ID: {}", + chatMessageBo.getId(), chatRequest.getUserId(), chatRequest.getSessionId()); } catch (Exception e) { log.error("saveMessage->保存消息失败", e); throw new ServiceException("保存消息失败"); @@ -195,28 +189,29 @@ public class ChatCostServiceImpl implements IChatCostService { @Override public void publishBillingEvent(ChatRequest chatRequest) { - log.debug("publishBillingEvent->发布计费事件,用户ID: {},会话ID: {},模型: {}", + log.debug("publishBillingEvent->发布计费事件,用户ID: {},会话ID: {},模型: {}", chatRequest.getUserId(), chatRequest.getSessionId(), chatRequest.getModel()); - + // 预检查:评估可能的扣费金额,如果余额不足则直接抛异常 try { preCheckBalance(chatRequest); } catch (ServiceException e) { - log.warn("publishBillingEvent->预检查余额不足,用户ID: {},模型: {}", + log.warn("publishBillingEvent->预检查余额不足,用户ID: {},模型: {}", chatRequest.getUserId(), chatRequest.getModel()); throw e; // 直接抛出,阻止消息保存和对话继续 } - + eventPublisher.publishEvent(new ChatMessageCreatedEvent( chatRequest.getUserId(), chatRequest.getSessionId(), chatRequest.getModel(), chatRequest.getRole(), - chatRequest.getPrompt() + chatRequest.getPrompt(), + chatRequest.getMessageId() )); log.debug("publishBillingEvent->计费事件发布完成"); } - + /** * 预检查用户余额是否足够支付可能的费用 */ @@ -224,34 +219,36 @@ public class ChatCostServiceImpl implements IChatCostService { if (chatRequest.getUserId() == null) { return; } - + int tokens = TikTokensUtil.tokens(chatRequest.getModel(), chatRequest.getPrompt()); String modelName = chatRequest.getModel(); ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName); BigDecimal unitPrice = BigDecimal.valueOf(chatModelVo.getModelPrice()); - + // 按次计费:直接检查单次费用 if (BillingType.TIMES.getCode().equals(chatModelVo.getModelType())) { BigDecimal numberCost = unitPrice.setScale(2, RoundingMode.HALF_UP); checkUserBalanceWithoutDeduct(chatRequest.getUserId(), numberCost.doubleValue()); return; } - + // 按token计费:检查累计后可能的费用 - final int threshold = 100; + final int threshold = 1000; ChatUsageToken chatToken = chatTokenService.queryByUserId(chatRequest.getUserId(), modelName); int previousUnpaid = (chatToken == null) ? 0 : chatToken.getToken(); int totalTokens = previousUnpaid + tokens; - + int billable = (totalTokens / threshold) * threshold; if (billable > 0) { + // 计算批次数:每1000个Token为一批,每批扣费单价 + int batches = billable / threshold; BigDecimal numberCost = unitPrice - .multiply(BigDecimal.valueOf(billable)) + .multiply(BigDecimal.valueOf(batches)) .setScale(2, RoundingMode.HALF_UP); checkUserBalanceWithoutDeduct(chatRequest.getUserId(), numberCost.doubleValue()); } } - + /** * 检查用户余额是否足够,但不扣除 */ @@ -260,69 +257,97 @@ public class ChatCostServiceImpl implements IChatCostService { if (sysUser == null) { throw new ServiceException("用户不存在"); } - + BigDecimal userBalance = BigDecimal.valueOf(sysUser.getUserBalance() == null ? 0D : sysUser.getUserBalance()) .setScale(2, RoundingMode.HALF_UP); BigDecimal cost = BigDecimal.valueOf(numberCost == null ? 0D : numberCost) .setScale(2, RoundingMode.HALF_UP); - + if (userBalance.compareTo(cost) < 0 || userBalance.compareTo(BigDecimal.ZERO) == 0) { throw new ServiceException("余额不足, 请充值。当前余额: " + userBalance + ",需要: " + cost); } } - + /** - * 保存账单记录 + * 更新消息的基本信息(不涉及扣费) */ - private void saveBillingRecord(ChatRequest chatRequest, int billedTokens, double cost, String billingTypeCode) { + private void updateMessageWithoutBilling(ChatRequest chatRequest, int actualTokens, String billingTypeCode) { + // 检查是否有消息ID可以更新 + if (chatRequest.getMessageId() == null) { + log.warn("updateMessageWithoutBilling->消息ID为空,无法更新基本信息"); + return; + } + try { - ChatMessageBo billingMessage = new ChatMessageBo(); - billingMessage.setUserId(chatRequest.getUserId()); - billingMessage.setSessionId(chatRequest.getSessionId()); - billingMessage.setRole("system"); // 系统账单消息 - billingMessage.setModelName(chatRequest.getModel()); - billingMessage.setTotalTokens(billedTokens); - billingMessage.setDeductCost(cost); - billingMessage.setRemark(getBillingTypeName(billingTypeCode)); - - // 设置计费类型和构建消息内容 - setBillingTypeAndContent(billingMessage, billingTypeCode, billedTokens, cost); - - chatMessageService.insertByBo(billingMessage); - log.debug("saveBillingRecord->保存账单记录成功,用户ID: {}, 计费类型: {}, 费用: {}", - chatRequest.getUserId(), billingTypeCode, cost); + // 创建更新对象,只更新基本信息,不涉及扣费 + ChatMessageBo updateMessage = new ChatMessageBo(); + updateMessage.setId(chatRequest.getMessageId()); + updateMessage.setTotalTokens(actualTokens); // 设置实际token数 + updateMessage.setBillingType(billingTypeCode); // 设置计费类型 + updateMessage.setRemark("用户消息(累计中,未达扣费阈值)"); // 说明状态 + + // 更新消息 + chatMessageService.updateByBo(updateMessage); + log.debug("updateMessageWithoutBilling->更新消息基本信息成功,消息ID: {}, 实际tokens: {}, 计费类型: {}", + chatRequest.getMessageId(), actualTokens, billingTypeCode); } catch (Exception e) { - log.error("saveBillingRecord->保存账单记录失败", e); - // 账单记录失败不影响主流程,只记录错误日志 + log.error("updateMessageWithoutBilling->更新消息基本信息失败,消息ID: {}", chatRequest.getMessageId(), e); + // 更新失败不影响主流程,只记录错误日志 } } /** - * 设置计费类型和构建消息内容 + * 更新消息的计费信息到备注字段 */ - private void setBillingTypeAndContent(ChatMessageBo billingMessage, String billingTypeCode, int billedTokens, double cost) { - billingMessage.setBillingType(billingTypeCode); - - // 使用枚举获取计费类型并构建消息内容 + private void updateMessageBilling(ChatRequest chatRequest, int billedTokens, double cost, String billingTypeCode) { + // 检查是否有消息ID可以更新 + if (chatRequest.getMessageId() == null) { + log.warn("updateMessageBilling->消息ID为空,无法更新计费信息"); + return; + } + + try { + // 计算本次消息的实际token数 + int actualTokens = TikTokensUtil.tokens(chatRequest.getModel(), chatRequest.getPrompt()); + + // 构建计费信息 + String billingInfo = buildBillingInfo(billingTypeCode, billedTokens, cost); + + // 创建更新对象 + ChatMessageBo updateMessage = new ChatMessageBo(); + updateMessage.setId(chatRequest.getMessageId()); + updateMessage.setTotalTokens(actualTokens); // 设置实际token数 + updateMessage.setDeductCost(cost); + updateMessage.setRemark(billingInfo); + updateMessage.setBillingType(billingTypeCode); + + // 更新消息 + chatMessageService.updateByBo(updateMessage); + log.debug("updateMessageBilling->更新消息计费信息成功,消息ID: {}, 实际tokens: {}, 计费tokens: {}, 费用: {}", + chatRequest.getMessageId(), actualTokens, billedTokens, cost); + } catch (Exception e) { + log.error("updateMessageBilling->更新消息计费信息失败,消息ID: {}", chatRequest.getMessageId(), e); + // 更新失败不影响主流程,只记录错误日志 + } + } + + /** + * 构建计费信息字符串 + */ + private String buildBillingInfo(String billingTypeCode, int billedTokens, double cost) { + // 使用枚举获取计费类型并构建计费信息 BillingType billingType = BillingType.fromCode(billingTypeCode); if (billingType != null) { - String content = switch (billingType) { + return switch (billingType) { case TIMES -> String.format("%s:消耗 %d tokens,扣费 %.2f 元", billingType.getDescription(), billedTokens, cost); case TOKEN -> String.format("%s:结算 %d tokens,扣费 %.2f 元", billingType.getDescription(), billedTokens, cost); }; - billingMessage.setContent(content); } else { - billingMessage.setContent(String.format("系统计费:处理 %d tokens,扣费 %.2f 元", billedTokens, cost)); + return String.format("系统计费:处理 %d tokens,扣费 %.2f 元", billedTokens, cost); } } - /** - * 获取计费类型名称(用于remark字段) - */ - private String getBillingTypeName(String billingTypeCode) { - BillingType billingType = BillingType.fromCode(billingTypeCode); - return billingType != null ? billingType.getDescription() : "系统计费"; - } + /** * 从用户余额中扣除费用 @@ -377,6 +402,7 @@ public class ChatCostServiceImpl implements IChatCostService { chatMessageBo.setContent(prompt); chatMessageBo.setDeductCost(cost); chatMessageBo.setTotalTokens(0); + chatMessageBo.setRemark(String.format("任务计费:%s,扣费 %.2f 元", type, cost)); chatMessageService.insertByBo(chatMessageBo); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java index 51f9a960..66b32e6f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java @@ -91,6 +91,8 @@ public class DifyServiceImpl implements IChatService { public void onMessageEnd(MessageEndEvent event) { emitter.complete(); log.info("消息结束,完整消息ID: {}", event.getMessageId()); + // 扣除费用 + ChatRequest chatRequestResponse = new ChatRequest(); // 更新conversationId if (StrUtil.isBlank(sessionInfo.getConversationId())) { String conversationId = event.getConversationId(); @@ -104,9 +106,9 @@ public class DifyServiceImpl implements IChatService { chatSessionBo.setSessionContent(sessionInfo.getSessionContent()); chatSessionBo.setRemark(sessionInfo.getRemark()); chatSessionService.updateByBo(chatSessionBo); + chatRequestResponse.setMessageId(chatSessionBo.getId()); } - // 扣除费用 - ChatRequest chatRequestResponse = new ChatRequest(); + // 设置对话角色 chatRequestResponse.setRole(Message.Role.ASSISTANT.getName()); chatRequestResponse.setModel(chatRequest.getModel()); From c7554d7e35c32f8aa98dd7ba4fdef04e77d80fc5 Mon Sep 17 00:00:00 2001 From: Administrator <1037463791@qq.com> Date: Thu, 4 Sep 2025 15:37:52 +0800 Subject: [PATCH 36/36] =?UTF-8?q?fix(billing):=201.=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=AE=A1=E8=B4=B9=E4=BB=A3=E7=90=86=20Billin?= =?UTF-8?q?gChatServiceProxy=E4=BD=8D=E7=BD=AE=EF=BC=9Aruoyi-modules/ruoyi?= =?UTF-8?q?-chat/src/main/java/org/ruoyi/chat/service/chat/proxy/BillingCh?= =?UTF-8?q?atServiceProxy.java=20=E4=BD=9C=E7=94=A8=EF=BC=9A=E4=B8=BA?= =?UTF-8?q?=E6=89=80=E6=9C=89ChatService=E5=AE=9E=E7=8E=B0=E7=B1=BB?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E9=80=8F=E6=98=8E=E7=9A=84=E8=AE=A1=E8=B4=B9?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E5=8C=85=E8=A3=85=20=20=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=20=20=20AI=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E5=89=8D=E4=BD=99=E9=A2=9D=E9=A2=84=E6=A3=80=E6=9F=A5=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E6=97=A0=E6=95=88=E6=B6=88=E8=80=97=20=20=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=94=B6=E9=9B=86AI=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E5=86=85=E5=AE=B9=20=20=20=E7=BB=9F=E4=B8=80=E5=A4=84=E7=90=86?= =?UTF-8?q?AI=E5=9B=9E=E5=A4=8D=E7=9A=84=E4=BF=9D=E5=AD=98=E5=92=8C?= =?UTF-8?q?=E8=AE=A1=E8=B4=B9=20=20=20=20=E9=80=82=E9=85=8D=E5=A4=9A?= =?UTF-8?q?=E7=A7=8DAI=E6=9C=8D=E5=8A=A1=E7=9A=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=20=20=202.=20=E9=87=8D=E6=9E=84=E5=B7=A5?= =?UTF-8?q?=E5=8E=82=E7=B1=BB=20=20=20ChatServiceFactory=20=20=20=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=EF=BC=9A=E8=87=AA=E5=8A=A8=E4=B8=BA=E6=89=80=E6=9C=89?= =?UTF-8?q?ChatService=E5=8C=85=E8=A3=85=E8=AE=A1=E8=B4=B9=E4=BB=A3?= =?UTF-8?q?=E7=90=86=20=20=E6=96=B0=E5=A2=9E=E6=96=B9=E6=B3=95=EF=BC=9Aget?= =?UTF-8?q?OriginalService()=20=E7=94=A8=E4=BA=8E=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=9C=AA=E5=8C=85=E8=A3=85=E7=9A=84=E5=8E=9F=E5=A7=8B=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=BC=98=E5=8A=BF=EF=BC=9A=E8=B0=83=E7=94=A8=E6=96=B9?= =?UTF-8?q?=E6=97=A0=E9=9C=80=E5=85=B3=E5=BF=83=E8=AE=A1=E8=B4=B9=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=AE=8C=E5=85=A8=E9=80=8F=E6=98=8E=20=203.?= =?UTF-8?q?=20=E5=A2=9E=E5=BC=BA=E8=AE=A1=E8=B4=B9=E6=9C=8D=E5=8A=A1=20ICh?= =?UTF-8?q?atCostService=20=E6=8E=A5=E5=8F=A3=20=20=20=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=96=B9=E6=B3=95=EF=BC=9AcheckBalanceSufficient()=20?= =?UTF-8?q?-=20=E4=BD=99=E9=A2=9D=E9=A2=84=E6=A3=80=E6=9F=A5=20=20=20=20?= =?UTF-8?q?=E5=88=86=E7=A6=BB=E5=85=B3=E6=B3=A8=E7=82=B9=EF=BC=9AsaveMessa?= =?UTF-8?q?ge()=20-=20=E4=BB=85=E4=BF=9D=E5=AD=98=E6=B6=88=E6=81=AF=20=20?= =?UTF-8?q?=20=20=20publishBillingEvent()=20-=20=E4=BB=85=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E8=AE=A1=E8=B4=B9=E4=BA=8B=E4=BB=B6=20=20=20=20=20ded?= =?UTF-8?q?uctToken()=20-=20=E4=BB=85=E6=89=A7=E8=A1=8C=E8=AE=A1=E8=B4=B9?= =?UTF-8?q?=E6=89=A3=E8=B4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/factory/ChatServiceFactory.java | 30 ++++++++++++++++++- .../chat/listener/SSEEventSourceListener.java | 3 ++ .../chat/service/chat/IChatCostService.java | 8 +++++ .../chat/impl/ChatCostServiceImpl.java | 25 ++++++++++++++++ .../service/chat/impl/DifyServiceImpl.java | 12 ++++---- .../service/chat/impl/SseServiceImpl.java | 10 ++++--- 6 files changed, 77 insertions(+), 11 deletions(-) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/ChatServiceFactory.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/ChatServiceFactory.java index f1e88a9a..8fec93e9 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/ChatServiceFactory.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/ChatServiceFactory.java @@ -1,6 +1,8 @@ package org.ruoyi.chat.factory; +import org.ruoyi.chat.service.chat.IChatCostService; import org.ruoyi.chat.service.chat.IChatService; +import org.ruoyi.chat.service.chat.proxy.BillingChatServiceProxy; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -18,13 +20,18 @@ import java.util.concurrent.ConcurrentHashMap; @Component public class ChatServiceFactory implements ApplicationContextAware { private final Map chatServiceMap = new ConcurrentHashMap<>(); + private IChatCostService chatCostService; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + // 获取计费服务 + this.chatCostService = applicationContext.getBean(IChatCostService.class); + // 初始化时收集所有IChatService的实现 Map serviceMap = applicationContext.getBeansOfType(IChatService.class); for (IChatService service : serviceMap.values()) { - if (service != null) { + if (service != null && !isBillingProxy(service)) { + // 只收集非代理的原始服务 chatServiceMap.put(service.getCategory(), service); } } @@ -32,12 +39,33 @@ public class ChatServiceFactory implements ApplicationContextAware { /** * 根据模型类别获取对应的聊天服务实现 + * 自动应用计费代理包装 */ public IChatService getChatService(String category) { + IChatService originalService = chatServiceMap.get(category); + if (originalService == null) { + throw new IllegalArgumentException("不支持的模型类别: " + category); + } + + // 自动包装为计费代理 + return new BillingChatServiceProxy(originalService, chatCostService); + } + + /** + * 获取原始服务(不包装代理) + */ + public IChatService getOriginalService(String category) { IChatService service = chatServiceMap.get(category); if (service == null) { throw new IllegalArgumentException("不支持的模型类别: " + category); } return service; } + + /** + * 判断是否为计费代理实例 + */ + private boolean isBillingProxy(IChatService service) { + return service instanceof BillingChatServiceProxy; + } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java index ceeb9e7b..52e94b9e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/listener/SSEEventSourceListener.java @@ -84,6 +84,8 @@ public class SSEEventSourceListener extends EventSourceListener { emitter.complete(); // 清理失败回调(以 emitter 为键) RetryNotifier.clear(emitter); + // 🔥 注释:AI回复的保存和计费已由BillingChatServiceProxy统一处理,此处代码已废弃 + /* // 扣除费用 ChatRequest chatRequest = new ChatRequest(); // 设置对话角色 @@ -97,6 +99,7 @@ public class SSEEventSourceListener extends EventSourceListener { // 先保存助手消息,再发布异步计费事件 chatCostService.saveMessage(chatRequest); chatCostService.publishBillingEvent(chatRequest); + */ return; } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java index 36104f9f..ccdd0617 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatCostService.java @@ -61,4 +61,12 @@ public interface IChatCostService { * 获取登录用户id */ Long getUserId(); + + /** + * 检查用户余额是否足够支付预估费用 + * + * @param chatRequest 对话信息 + * @return true=余额充足,false=余额不足 + */ + boolean checkBalanceSufficient(ChatRequest chatRequest); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java index 9c1cf04f..16507b94 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java @@ -428,4 +428,29 @@ public class ChatCostServiceImpl implements IChatCostService { } return loginUser.getUserId(); } + + /** + * 检查用户余额是否足够支付预估费用 + */ + @Override + public boolean checkBalanceSufficient(ChatRequest chatRequest) { + if (chatRequest.getUserId() == null) { + log.warn("checkBalanceSufficient->用户ID为空,视为余额不足"); + return false; + } + + try { + // 重用现有的预检查逻辑,但不抛异常,只返回boolean + preCheckBalance(chatRequest); + return true; // 预检查通过,余额充足 + } catch (ServiceException e) { + log.debug("checkBalanceSufficient->余额不足,用户ID: {}, 模型: {}, 错误: {}", + chatRequest.getUserId(), chatRequest.getModel(), e.getMessage()); + return false; // 预检查失败,余额不足 + } catch (Exception e) { + log.error("checkBalanceSufficient->检查余额时发生异常,用户ID: {}, 模型: {}", + chatRequest.getUserId(), chatRequest.getModel(), e); + return false; // 异常情况视为余额不足,保守处理 + } + } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java index 66b32e6f..43608516 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DifyServiceImpl.java @@ -110,12 +110,12 @@ public class DifyServiceImpl implements IChatService { } // 设置对话角色 - chatRequestResponse.setRole(Message.Role.ASSISTANT.getName()); - chatRequestResponse.setModel(chatRequest.getModel()); - chatRequestResponse.setUserId(chatRequest.getUserId()); - chatRequestResponse.setSessionId(chatRequest.getSessionId()); - chatRequestResponse.setPrompt(respMessage.toString()); - chatCostService.deductToken(chatRequestResponse); +// chatRequestResponse.setRole(Message.Role.ASSISTANT.getName()); +// chatRequestResponse.setModel(chatRequest.getModel()); +// chatRequestResponse.setUserId(chatRequest.getUserId()); +// chatRequestResponse.setSessionId(chatRequest.getSessionId()); +// chatRequestResponse.setPrompt(respMessage.toString()); +// chatCostService.deductToken(chatRequestResponse); RetryNotifier.clear(emitter); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index eb9dba8b..0250177a 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -116,8 +116,6 @@ public class SseServiceImpl implements ISseService { } - // 先保存消息,再发布异步计费事件 - chatCostService.saveMessage(chatRequest); chatRequest.setUserId(chatCostService.getUserId()); if (chatRequest.getSessionId() == null) { @@ -128,11 +126,15 @@ public class SseServiceImpl implements ISseService { chatSessionService.insertByBo(chatSessionBo); chatRequest.setSessionId(chatSessionBo.getId()); } + + // 保存用户消息 + chatCostService.saveMessage(chatRequest); } // 自动选择模型并获取对应的聊天服务 IChatService chatService = autoSelectModelAndGetService(chatRequest); - chatCostService.publishBillingEvent(chatRequest); - // 仅当 autoSelectModel = true 时,才启用重试与降级 + + // 用户消息只保存不计费,AI回复由BillingChatServiceProxy自动处理计费 + // chatCostService.publishBillingEvent(chatRequest); // 用户输入不计费 if (Boolean.TRUE.equals(chatRequest.getAutoSelectModel())) { ChatModelVo currentModel = this.chatModelVo; String currentCategory = currentModel.getCategory();