diff --git a/docx/prompt.md b/docx/prompt.md new file mode 100644 index 00000000..59595541 --- /dev/null +++ b/docx/prompt.md @@ -0,0 +1,139 @@ +你是一个资深 Java 后端工程师,请在当前 Spring Boot(Java8)项目中实现一个完整的 MCP Server,并加入“AI 自动选 Tool(RAG + Tool Routing)”能力。 + +# 一、基础 MCP 能力(必须实现) + +1. 提供统一入口: + POST /mcp + +2. 实现 JSON-RPC 2.0: + + * initialize + * tools/list + * tools/call + +3. 返回格式: + { + "jsonrpc": "2.0", + "id": "...", + "result": ... + } + +# 二、流式能力(必须) + +1. 禁止使用 SseEmitter +2. 使用 HttpServletResponse + OutputStream +3. 实现 chunked streaming(write + flush) + +# 三、Tool 注册中心(必须) + +实现: + +* McpToolRegistry +* register / get / list + +Tool结构: + +* name +* description(非常重要,用于向量检索) +* inputSchema +* handler + +# 四、RAG Tool 检索模块(新增重点) + +实现一个 ToolRetriever 组件: + +1. 构建向量索引(简化实现): + + * 使用内存存储 List + * ToolEmbedding: + + * toolName + * description + * embedding(double[]) + +2. 提供方法: + List retrieve(String query, int topK) + +3. embedding 实现(简化): + + * 可使用: + + * 简单 TF-IDF + * 或 mock embedding(字符串相似度) + * 不依赖外部服务(保证可运行) + +# 五、Tool Router(核心) + +实现 ToolRouter: + +流程: + +1. 接收用户问题 query +2. 调用 ToolRetriever → 获取 topK tools +3. 构建 prompt(只包含候选 tools) +4. 返回候选 tools 给上层(或直接选) + +要求: + +* 不允许返回全部 tools +* 默认 topK = 3~5 + +# 六、tools/call 升级(关键) + +当 method = tools/call 时: + +1. 如果请求未指定 tool: + + * 自动触发 ToolRouter + * 选出最优 tool + +2. 如果指定 tool: + + * 直接执行 + +# 七、默认内置工具 + +实现: + +1. hello + 输入:name + 输出:hello xxx + +2. meeting.summary + 输入:text + 输出:模拟总结 + +# 八、架构分层(必须) + +* controller +* service +* registry +* retriever(新增) +* router(新增) + +# 九、扩展能力(必须预留) + +1. 支持替换为真实向量数据库(如 Milvus / ES) +2. 支持 embedding API(OpenAI / 本地模型) +3. 支持多 MCP Server(未来扩展) + +# 十、输出要求 + +1. 输出完整代码: + + * McpController + * McpService + * McpToolRegistry + * ToolRetriever + * ToolRouter + * ToolEmbedding + * ToolInitializer + * McpRequest + +2. 代码必须: + + * 可运行 + * 无伪代码 + * 包含必要注释 + * 代码生成目录在ruoyi-sip/src/main/java/com/ruoyi/sip/llm +3. 不要解释,只输出代码 diff --git a/oms_web/oms_vue/src/api/system/user.js b/oms_web/oms_vue/src/api/system/user.js index 8169ac5e..5f70aa50 100644 --- a/oms_web/oms_vue/src/api/system/user.js +++ b/oms_web/oms_vue/src/api/system/user.js @@ -1,7 +1,7 @@ import request from '@/utils/request' import { parseStrEmpty } from "@/utils/ruoyi"; -// 查询用户列表 +// 鏌ヨ鐢ㄦ埛鍒楄〃 export function listUser(query) { return request({ url: '/system/user/list', @@ -10,7 +10,7 @@ export function listUser(query) { }) } -// 查询用户详细 +// 鏌ヨ鐢ㄦ埛璇︾粏 export function getUser(userId) { return request({ url: '/system/user/' + parseStrEmpty(userId), @@ -18,7 +18,7 @@ export function getUser(userId) { }) } -// 新增用户 +// 鏂板鐢ㄦ埛 export function addUser(data) { return request({ url: '/system/user/add', @@ -30,7 +30,7 @@ export function addUser(data) { }) } -// 修改用户 +// 淇敼鐢ㄦ埛 export function updateUser(data) { return request({ url: '/system/user', @@ -39,7 +39,7 @@ export function updateUser(data) { }) } -// 删除用户 +// 鍒犻櫎鐢ㄦ埛 export function delUser(userId) { return request({ url: '/system/user/' + userId, @@ -47,7 +47,7 @@ export function delUser(userId) { }) } -// 用户密码重置 +// 鐢ㄦ埛瀵嗙爜閲嶇疆 export function resetUserPwd(data) { return request({ @@ -60,7 +60,7 @@ export function resetUserPwd(data) { }) } -// 用户状态修改 +// 鐢ㄦ埛鐘舵€佷慨鏀? export function changeUserStatus(userId, status) { const data = { userId, @@ -76,7 +76,7 @@ export function changeUserStatus(userId, status) { }) } -// 查询用户个人信息 +// 鏌ヨ鐢ㄦ埛涓汉淇℃伅 export function getUserProfile() { return request({ url: '/system/user/profile/vue', @@ -84,7 +84,7 @@ export function getUserProfile() { }) } -// 修改用户个人信息 +// 淇敼鐢ㄦ埛涓汉淇℃伅 export function updateUserProfile(data) { return request({ url: '/system/user/profile', @@ -93,7 +93,7 @@ export function updateUserProfile(data) { }) } -// 用户密码重置 +// 鐢ㄦ埛瀵嗙爜閲嶇疆 export function updateUserPwd(data) { return request({ url: '/system/user/profile/resetPwd', @@ -103,7 +103,7 @@ export function updateUserPwd(data) { }) } -// 用户头像上传 +// 鐢ㄦ埛澶村儚涓婁紶 export function uploadAvatar(data) { return request({ url: '/system/user/profile/updateAvatar', @@ -113,7 +113,20 @@ export function uploadAvatar(data) { }) } -// 查询授权角色 +export function getUserBotProfile() { + return request({ + url: '/system/user/profile/bot', + method: 'get' + }) +} + +export function generateUserBotProfile() { + return request({ + url: '/system/user/profile/bot/generate', + method: 'post' + }) +} +// 鏌ヨ鎺堟潈瑙掕壊 export function getAuthRole(userId) { return request({ url: '/system/user/vue/authRole/' + userId, @@ -121,7 +134,7 @@ export function getAuthRole(userId) { }) } -// 保存授权角色 +// 淇濆瓨鎺堟潈瑙掕壊 export function updateAuthRole(data) { return request({ url: '/system/user/authRole', @@ -130,7 +143,7 @@ export function updateAuthRole(data) { }) } -// 查询部门下拉树结构 +// 鏌ヨ閮ㄩ棬涓嬫媺鏍戠粨鏋? export function deptTreeSelect() { return request({ url: '/system/user/deptTree', diff --git a/oms_web/oms_vue/src/views/system/user/profile/index.vue b/oms_web/oms_vue/src/views/system/user/profile/index.vue index 6a4f8a90..27127cda 100644 --- a/oms_web/oms_vue/src/views/system/user/profile/index.vue +++ b/oms_web/oms_vue/src/views/system/user/profile/index.vue @@ -25,7 +25,7 @@
  • 所属部门 -
    {{ user.dept.deptName }}
    +
    {{ user.dept.deptName }}
  • 所属角色 @@ -51,6 +51,9 @@ + + + @@ -62,11 +65,12 @@ import userAvatar from "./userAvatar" import userInfo from "./userInfo" import resetPwd from "./resetPwd" +import botConfig from "./botConfig" import { getUserProfile } from "@/api/system/user" export default { name: "Profile", - components: { userAvatar, userInfo, resetPwd }, + components: { userAvatar, userInfo, resetPwd, botConfig }, data() { return { user: {}, @@ -86,8 +90,7 @@ export default { getUser() { getUserProfile().then(response => { this.user = response.data - this.roleGroup = this.user.roles.map(item=>item.roleName).join(',') - + this.roleGroup = this.user.roles.map(item => item.roleName).join(',') }) } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java index 883350b9..d414f412 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java @@ -2,11 +2,11 @@ package com.ruoyi.web.controller.system; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -22,8 +22,13 @@ import com.ruoyi.common.utils.ShiroUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.file.MimeTypeUtils; -import com.ruoyi.framework.shiro.service.SysPasswordService; -import com.ruoyi.system.service.ISysUserService; +import com.ruoyi.framework.shiro.service.SysPasswordService; +import com.ruoyi.system.domain.SysBotCredential; +import com.ruoyi.system.service.ISysBotCredentialService; +import com.ruoyi.system.service.ISysUserService; + +import java.util.LinkedHashMap; +import java.util.Map; /** * 个人信息 业务处理 @@ -41,8 +46,11 @@ public class SysProfileController extends BaseController @Autowired private ISysUserService userService; - @Autowired - private SysPasswordService passwordService; + @Autowired + private SysPasswordService passwordService; + + @Autowired + private ISysBotCredentialService botCredentialService; /** * 个人信息 @@ -58,11 +66,11 @@ public class SysProfileController extends BaseController } @GetMapping("/vue") @ResponseBody - public AjaxResult profileVue() - { - SysUser user = getSysUser(); - - return AjaxResult.success(user); + public AjaxResult profileVue() + { + SysUser user = getSysUser(); + + return AjaxResult.success(user); } @GetMapping("/checkPassword") @@ -163,8 +171,8 @@ public class SysProfileController extends BaseController @Log(title = "个人信息", businessType = BusinessType.UPDATE) @PostMapping("/updateAvatar") @ResponseBody - public AjaxResult updateAvatar(@RequestParam("avatarfile") MultipartFile file) - { + public AjaxResult updateAvatar(@RequestParam("avatarfile") MultipartFile file) + { SysUser currentUser = getSysUser(); try { @@ -184,6 +192,35 @@ public class SysProfileController extends BaseController { log.error("修改头像失败!", e); return error(e.getMessage()); - } - } -} + } + } + + @GetMapping("/bot") + @ResponseBody + public AjaxResult currentBot() + { + SysUser user = getSysUser(); + SysBotCredential credential = botCredentialService.selectByUserId(user.getUserId()); + Map data = new LinkedHashMap<>(); + data.put("hasBot", credential != null); + if (credential != null) + { + data.put("botId", credential.getBotId()); + data.put("status", credential.getStatus()); + data.put("expireTime", credential.getExpireTime()); + data.put("lastAccessTime", credential.getLastAccessTime()); + data.put("lastAccessIp", credential.getLastAccessIp()); + data.put("remark", credential.getRemark()); + } + return AjaxResult.success(data); + } + + @Log(title = "MCP Bot凭证", businessType = BusinessType.INSERT) + @PostMapping("/bot/generate") + @ResponseBody + public AjaxResult generateBot() + { + SysUser user = getSysUser(); + return AjaxResult.success("生成成功", botCredentialService.generateCredentialForUser(user)); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/McpErrorUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/McpErrorUtils.java new file mode 100644 index 00000000..fe4d4734 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/McpErrorUtils.java @@ -0,0 +1,42 @@ +package com.ruoyi.common.utils; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class McpErrorUtils { + public static final int PARSE_ERROR = -32700; + public static final int INVALID_REQUEST = -32600; + public static final int METHOD_NOT_FOUND = -32601; + public static final int INVALID_PARAMS = -32602; + public static final int INTERNAL_ERROR = -32603; + public static final int AUTH_ERROR = -32001; + public static final int SERVER_ERROR = -32000; + + private McpErrorUtils() { + } + + public static Map buildError(int code, String message, String type) { + Map error = new LinkedHashMap<>(); + error.put("code", code); + error.put("message", message); + + Map data = new LinkedHashMap<>(); + data.put("type", type); + error.put("data", data); + return error; + } + + public static Map buildError(int code, String message, String type, Map extraData) { + Map error = new LinkedHashMap<>(); + error.put("code", code); + error.put("message", message); + + Map data = new LinkedHashMap<>(); + data.put("type", type); + if (extraData != null && !extraData.isEmpty()) { + data.putAll(extraData); + } + error.put("data", data); + return error; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/security/McpBotCredentialUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/security/McpBotCredentialUtils.java new file mode 100644 index 00000000..3e7e73a8 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/security/McpBotCredentialUtils.java @@ -0,0 +1,43 @@ +package com.ruoyi.common.utils.security; + +import com.ruoyi.common.utils.StringUtils; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +public class McpBotCredentialUtils { + private McpBotCredentialUtils() { + } + + public static String generateBotId(String prefix) { + String normalizedPrefix = StringUtils.isEmpty(prefix) ? "mcp" : prefix.trim(); + return normalizedPrefix + "_" + UUID.randomUUID().toString().replace("-", "").substring(0, 16); + } + + public static String generateSecret() { + return UUID.randomUUID().toString().replace("-", ""); + } + + public static String generateSalt() { + return UUID.randomUUID().toString().replace("-", "").substring(0, 8); + } + + public static String hashSecret(String botId, String botSecret, String salt) { + return Md5Utils.hash(StringUtils.trim(botId) + StringUtils.trim(botSecret) + StringUtils.trim(salt)); + } + + public static Map generateCredentialPair(String prefix) { + String botId = generateBotId(prefix); + String botSecret = generateSecret(); + String salt = generateSalt(); + String secretHash = hashSecret(botId, botSecret, salt); + + Map result = new LinkedHashMap<>(); + result.put("botId", botId); + result.put("botSecret", botSecret); + result.put("secretSalt", salt); + result.put("secretHash", secretHash); + return result; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java index c52aa9e1..248d0e32 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import javax.servlet.Filter; import org.apache.commons.io.IOUtils; +import org.apache.shiro.SecurityUtils; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.codec.Base64; import org.apache.shiro.config.ConfigurationException; @@ -34,11 +35,13 @@ import com.ruoyi.framework.shiro.web.CustomShiroFilterFactoryBean; import com.ruoyi.framework.shiro.web.filter.LogoutFilter; import com.ruoyi.framework.shiro.web.filter.captcha.CaptchaValidateFilter; import com.ruoyi.framework.shiro.web.filter.kickout.KickoutSessionFilter; +import com.ruoyi.framework.shiro.web.filter.mcp.McpBotAuthFilter; import com.ruoyi.framework.shiro.web.filter.online.OnlineSessionFilter; import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter; import com.ruoyi.framework.shiro.web.filter.user.CustomUserFilter; import com.ruoyi.framework.shiro.web.session.OnlineWebSessionManager; import com.ruoyi.framework.shiro.web.session.SpringSessionValidationScheduler; +import com.ruoyi.system.service.ISysBotCredentialService; import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; /** @@ -298,6 +301,8 @@ public class ShiroConfig permitAllUrl.forEach(url -> filterChainDefinitionMap.put(url, "anon")); } // 退出 logout地址,shiro去清除session + filterChainDefinitionMap.put("/mcp", "mcpBot"); + filterChainDefinitionMap.put("/mcp/**", "mcpBot"); filterChainDefinitionMap.put("/logout", "logout"); // 不需要拦截的访问 filterChainDefinitionMap.put("/login", "anon,captchaValidate"); @@ -312,6 +317,7 @@ public class ShiroConfig filters.put("captchaValidate", captchaValidateFilter()); filters.put("kickout", kickoutSessionFilter()); // 注销成功,则跳转到指定页面 + filters.put("mcpBot", mcpBotAuthFilter(securityManager, userRealm(getEhCacheManager()))); filters.put("logout", logoutFilter()); filters.put("user", customUserFilter()); shiroFilterFactoryBean.setFilters(filters); @@ -413,6 +419,15 @@ public class ShiroConfig return customUserFilter; } + public McpBotAuthFilter mcpBotAuthFilter(SecurityManager securityManager, UserRealm userRealm) + { + McpBotAuthFilter filter = new McpBotAuthFilter(); + filter.setSecurityManager(securityManager); + filter.setRealmName(userRealm.getName()); + filter.setBotCredentialService(SpringUtils.getBean(ISysBotCredentialService.class)); + return filter; + } + /** * thymeleaf模板引擎和shiro框架的整合 */ diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/mcp/McpBotAuthFilter.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/mcp/McpBotAuthFilter.java new file mode 100644 index 00000000..bcea6e60 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/mcp/McpBotAuthFilter.java @@ -0,0 +1,98 @@ +package com.ruoyi.framework.shiro.web.filter.mcp; + +import com.alibaba.fastjson.JSON; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.IpUtils; +import com.ruoyi.common.utils.McpErrorUtils; +import com.ruoyi.system.service.ISysBotCredentialService; +import org.apache.shiro.mgt.SecurityManager; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.web.filter.AccessControlFilter; +import org.apache.shiro.web.subject.WebSubject; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.LinkedHashMap; +import java.util.Map; + +public class McpBotAuthFilter extends AccessControlFilter { + private static final String HEADER_BOT_ID = "X-Bot-Id"; + private static final String HEADER_BOT_SECRET = "X-Bot-Secret"; + private static final String ATTR_ERROR_MESSAGE = "mcpBotErrorMessage"; + + private ISysBotCredentialService botCredentialService; + private SecurityManager securityManager; + private String realmName; + + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { + HttpServletRequest httpRequest = (HttpServletRequest) request; + String botId = httpRequest.getHeader(HEADER_BOT_ID); + String botSecret = httpRequest.getHeader(HEADER_BOT_SECRET); + + if (isBlank(botId) && isBlank(botSecret)) { + return true; + } + + if (isBlank(botId) || isBlank(botSecret)) { + request.setAttribute(ATTR_ERROR_MESSAGE, "Both X-Bot-Id and X-Bot-Secret are required"); + return false; + } + + try { + SysUser user = botCredentialService.authenticateMcpBot(botId, botSecret, IpUtils.getIpAddr(httpRequest)); + SimplePrincipalCollection principals = new SimplePrincipalCollection(user, realmName); + WebSubject.Builder builder = new WebSubject.Builder(securityManager, httpRequest, (HttpServletResponse) response); + builder.principals(principals); + builder.authenticated(true); +// builder.sessionCreationEnabled(false); + Subject subject = builder.buildWebSubject(); + ThreadContext.bind(subject); + return true; + } catch (ServiceException e) { + request.setAttribute(ATTR_ERROR_MESSAGE, e.getMessage()); + return false; + } + } + + @Override + protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpResponse.setContentType("application/json"); + httpResponse.setCharacterEncoding("UTF-8"); + + Map body = new LinkedHashMap<>(); + body.put("jsonrpc", "2.0"); + body.put("id", null); + body.put("error", McpErrorUtils.buildError( + McpErrorUtils.AUTH_ERROR, + String.valueOf(request.getAttribute(ATTR_ERROR_MESSAGE)), + "authentication_error" + )); + httpResponse.getWriter().write(JSON.toJSONString(body)); + return false; + } + + public void setBotCredentialService(ISysBotCredentialService botCredentialService) { + this.botCredentialService = botCredentialService; + } + + public void setSecurityManager(SecurityManager securityManager) { + this.securityManager = securityManager; + } + + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } +} + diff --git a/ruoyi-sip/pom.xml b/ruoyi-sip/pom.xml index 4f2aec39..c4ddd953 100644 --- a/ruoyi-sip/pom.xml +++ b/ruoyi-sip/pom.xml @@ -40,6 +40,10 @@ com.ruoyi ruoyi-system + + com.ruoyi + ruoyi-framework + org.thymeleaf thymeleaf diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/SseController.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/SseController.java new file mode 100644 index 00000000..ce5b8c7e --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/controller/SseController.java @@ -0,0 +1,88 @@ +//package com.ruoyi.sip.controller; +// +// +//import com.ruoyi.common.annotation.Anonymous; +//import com.ruoyi.common.utils.ShiroUtils; +//import lombok.extern.slf4j.Slf4j; +//import org.apache.shiro.session.Session; +//import org.springframework.stereotype.Controller; +//import org.springframework.web.bind.annotation.*; +//import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; +//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +// +//import java.io.IOException; +//import java.util.Map; +//import java.util.Set; +//import java.util.UUID; +//import java.util.concurrent.ConcurrentHashMap; +// +///** +// * @author : ch +// * @version : 1.0 +// * @ClassName : SseController +// * @Description : +// * @DATE : Created in 10:19 2025/6/6 +// *
           Copyright: Copyright(c) 2025     
    +// *
           Company :   	紫光汇智信息技术有限公司		           
    +// * Modification History: +// * Date Author Version Discription +// * -------------------------------------------------------------------------- +// * 2025/06/06 ch 1.0 Why & What is modified: <修改原因描述> * +// */ +//@Controller +//@Slf4j +//@RequestMapping("/sse") +//public class SseController { +// +// private static Map emitterMap = new ConcurrentHashMap<>(); +// @GetMapping("") +// @CrossOrigin(value = "*") +// public SseEmitter sseServer(){ +// String sessionId = ShiroUtils.getSession().getId().toString(); +// SseEmitter emitter = null; +// if ((emitter = emitterMap.get(sessionId)) != null){ +// return emitter; +// } +// // 判断其他服务器有没有对应的连接,有的话,就算了。直接返回。或者直接转发。可以通过直接调用或者通过mq推送之类的 +// emitter = new SseEmitter(300000L); +// emitter.onTimeout(()->{ +// emitterMap.remove(sessionId); +// log.info("timeout"); +// }); +// emitter.onCompletion(()->{ +// emitterMap.remove(sessionId); +// log.info("completion"); +// }); +// // 在客户端断开连接的时候会触发error回调 +// emitter.onError(e->{ +// emitterMap.remove(sessionId); +// log.error("error",e); +// }); +// log.info("create for {}",sessionId); +// emitterMap.put(sessionId, emitter); +// return emitter; +// } +// @GetMapping("/msg") +// @Anonymous +// @ResponseBody +// public void sendMsg(@RequestParam String message){ +// send(message); +// } +// private void send(String message){ +// emitterMap.values().forEach(emitter -> { +// try { +// doSend(emitter,message); +// } catch (IOException e) { +// log.warn("客户端断开连接了"); +// } +// }); +// } +// private void doSend(SseEmitter emitter,String message) throws IOException { +// SseEmitter.SseEventBuilder message1 = SseEmitter.event() +// .id(UUID.randomUUID().toString()) +// .name("message") +// .data(message); +// +// emitter.send( SseEmitter.event().name("message").data(message)); +// } +//} diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/InventoryOuter.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/InventoryOuter.java index 9249df91..d3776aef 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/InventoryOuter.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/InventoryOuter.java @@ -41,6 +41,7 @@ public class InventoryOuter extends BaseEntity /** 订单编码 */ @Excel(name = "合同编号") private String orderCode; + private List orderCodeList; private String orderId; @Excel(name = "项目名称") private String projectName; diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectOrderInfo.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectOrderInfo.java index edb9b741..a6ac3a20 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectOrderInfo.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectOrderInfo.java @@ -71,6 +71,8 @@ public class ProjectOrderInfo extends BaseEntity { @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") // @Excel(name="下单时间", width = 30, dateFormat = "yyyy-MM-dd") private Date createTime; + private Date createTimeStart; + private Date createTimeEnd; /** * 订单状态 diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpController.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpController.java new file mode 100644 index 00000000..02cde0fc --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpController.java @@ -0,0 +1,92 @@ +package com.ruoyi.sip.llm; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.utils.McpErrorUtils; +import com.ruoyi.common.utils.ShiroUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +@RestController +@RequestMapping("/mcp") +public class McpController { + + @Autowired + private McpService mcpService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public McpController() { + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + @PostMapping(produces = "application/json") + public void handleMcpRequest(@RequestBody McpRequest request, HttpServletResponse response) { + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.setHeader("Transfer-Encoding", "chunked"); + + try (OutputStream os = response.getOutputStream()) { + McpResponse mcpResponse = new McpResponse(); + mcpResponse.setId(request.getId()); + mcpResponse.setJsonrpc(request.getJsonrpc()); + + try { + Object result = mcpService.handleRequest(request); + mcpResponse.setResult(result); + } catch (Exception e) { + mcpResponse.setError(toMcpError(e)); + } + + String jsonOutput = objectMapper.writeValueAsString(mcpResponse); + + byte[] bytes = jsonOutput.getBytes(StandardCharsets.UTF_8); + int chunkSize = 1024; + for (int i = 0; i < bytes.length; i += chunkSize) { + int length = Math.min(chunkSize, bytes.length - i); + os.write(bytes, i, length); + os.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + @GetMapping(value = "/whoami", produces = "application/json") + @ResponseBody + public Map whoami() { + Map result = new LinkedHashMap<>(); + SysUser user = ShiroUtils.getSysUser(); + result.put("authenticated", user != null); + if (user != null) { + result.put("userId", user.getUserId()); + result.put("loginName", user.getLoginName()); + result.put("userName", user.getUserName()); + result.put("deptId", user.getDeptId()); + } + return result; + } + + private Map toMcpError(Exception e) { + String message = e.getMessage() != null ? e.getMessage() : "Internal server error"; + String lowerMessage = message.toLowerCase(); + + if (lowerMessage.contains("tool name is required") + || lowerMessage.contains("invalid") + || lowerMessage.contains("missing")) { + return McpErrorUtils.buildError(McpErrorUtils.INVALID_PARAMS, message, "invalid_params"); + } + if (lowerMessage.contains("tool not found") || lowerMessage.contains("method not supported")) { + return McpErrorUtils.buildError(McpErrorUtils.METHOD_NOT_FOUND, message, "method_not_found"); + } + return McpErrorUtils.buildError(McpErrorUtils.INTERNAL_ERROR, message, "internal_error"); + } +} diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpRequest.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpRequest.java new file mode 100644 index 00000000..b8bc4677 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpRequest.java @@ -0,0 +1,19 @@ +package com.ruoyi.sip.llm; + +import java.util.Map; + +public class McpRequest { + private String jsonrpc = "2.0"; + private String id; + private String method; + private Map params; + + public String getJsonrpc() { return jsonrpc; } + public void setJsonrpc(String jsonrpc) { this.jsonrpc = jsonrpc; } + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getMethod() { return method; } + public void setMethod(String method) { this.method = method; } + public Map getParams() { return params; } + public void setParams(Map params) { this.params = params; } +} \ No newline at end of file diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpResponse.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpResponse.java new file mode 100644 index 00000000..f586a8fd --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpResponse.java @@ -0,0 +1,20 @@ +package com.ruoyi.sip.llm; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class McpResponse { + private String jsonrpc = "2.0"; + private String id; + private Object result; + private Object error; + + public String getJsonrpc() { return jsonrpc; } + public void setJsonrpc(String jsonrpc) { this.jsonrpc = jsonrpc; } + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public Object getResult() { return result; } + public void setResult(Object result) { this.result = result; } + public Object getError() { return error; } + public void setError(Object error) { this.error = error; } +} \ No newline at end of file diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpService.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpService.java new file mode 100644 index 00000000..bd45c8c7 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpService.java @@ -0,0 +1,110 @@ +package com.ruoyi.sip.llm; + +import com.alibaba.fastjson.JSON; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class McpService { + + @Autowired + private McpToolRegistry registry; + + @SuppressWarnings("unchecked") + public Object handleRequest(McpRequest request) { + String method = request.getMethod(); + Map params = request.getParams(); + + if ("initialize".equals(method)) { + Map result = new HashMap<>(); + result.put("protocolVersion", "2024-11-05"); + + Map capabilities = new HashMap<>(); + capabilities.put("tools", new HashMap()); + result.put("capabilities", capabilities); + + Map serverInfo = new HashMap<>(); + serverInfo.put("name", "UNIS-OMS-MCP"); + serverInfo.put("version", "1.0.0"); + result.put("serverInfo", serverInfo); + return result; + } + + if ("notifications/initialized".equals(method) || "initialized".equals(method)) { + return new HashMap<>(); + } + + if ("ping".equals(method)) { + return new HashMap<>(); + } + + if ("tools/list".equals(method)) { + Map result = new HashMap<>(); + List tools = registry.list(); + result.put("tools", tools.stream().map(t -> { + Map tm = new HashMap<>(); + tm.put("name", t.getName()); + tm.put("description", t.getDescription()); + tm.put("inputSchema", t.getInputSchema()); + return tm; + }).collect(Collectors.toList())); + return result; + } + + if ("tools/call".equals(method)) { + String toolName = params != null ? (String) params.get("name") : null; + if (toolName == null || toolName.isEmpty()) { + throw new RuntimeException("Tool name is required"); + } + + McpTool tool = registry.get(toolName); + if (tool != null) { + Map args = params != null ? (Map) params.get("arguments") : new HashMap<>(); + return wrapToolResult(tool.getHandler().apply(args)); + } else { + throw new RuntimeException("Tool not found: " + toolName); + } + } + + throw new RuntimeException("Method not supported: " + method); + } + + @SuppressWarnings("unchecked") + private Object wrapToolResult(Object rawResult) { + if (rawResult instanceof Map) { + Map rawMap = (Map) rawResult; + if (rawMap.containsKey("content")) { + return rawMap; + } + } + + Map result = new HashMap<>(); + List> content = new ArrayList<>(); + Map textContent = new HashMap<>(); + textContent.put("type", "text"); + textContent.put("text", toTextPayload(rawResult)); + content.add(textContent); + result.put("content", content); + + if (rawResult instanceof Map || rawResult instanceof List) { + result.put("structuredContent", rawResult); + } + return result; + } + + private String toTextPayload(Object rawResult) { + if (rawResult == null) { + return ""; + } + if (rawResult instanceof Map || rawResult instanceof List) { + return JSON.toJSONString(rawResult); + } + return String.valueOf(rawResult); + } +} \ No newline at end of file diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpTool.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpTool.java new file mode 100644 index 00000000..f42efddd --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpTool.java @@ -0,0 +1,20 @@ +package com.ruoyi.sip.llm; + +import java.util.Map; +import java.util.function.Function; + +public class McpTool { + private String name; + private String description; + private Map inputSchema; + private Function, Object> handler; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public Map getInputSchema() { return inputSchema; } + public void setInputSchema(Map inputSchema) { this.inputSchema = inputSchema; } + public Function, Object> getHandler() { return handler; } + public void setHandler(Function, Object> handler) { this.handler = handler; } +} \ No newline at end of file diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpToolRegistry.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpToolRegistry.java new file mode 100644 index 00000000..b510adc8 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/McpToolRegistry.java @@ -0,0 +1,25 @@ +package com.ruoyi.sip.llm; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class McpToolRegistry { + private final Map tools = new ConcurrentHashMap<>(); + + public void register(McpTool tool) { + tools.put(tool.getName(), tool); + } + + public McpTool get(String name) { + return tools.get(name); + } + + public List list() { + return new ArrayList<>(tools.values()); + } +} \ No newline at end of file diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/ToolInitializer.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/ToolInitializer.java new file mode 100644 index 00000000..26abb637 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/ToolInitializer.java @@ -0,0 +1,25 @@ +package com.ruoyi.sip.llm; + +import com.ruoyi.sip.llm.tools.McpToolProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.List; + +@Component +public class ToolInitializer { + + @Autowired + private McpToolRegistry registry; + + @Autowired + private List toolProviders; + + @PostConstruct + public void init() { + for (McpToolProvider toolProvider : toolProviders) { + registry.register(toolProvider.buildTool()); + } + } +} \ No newline at end of file diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/McpToolProvider.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/McpToolProvider.java new file mode 100644 index 00000000..614bb169 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/McpToolProvider.java @@ -0,0 +1,8 @@ +package com.ruoyi.sip.llm.tools; + +import com.ruoyi.sip.llm.McpTool; + +public interface McpToolProvider { + + McpTool buildTool(); +} \ No newline at end of file diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/ProductInfoToolProvider.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/ProductInfoToolProvider.java new file mode 100644 index 00000000..a2876d4a --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/ProductInfoToolProvider.java @@ -0,0 +1,143 @@ +package com.ruoyi.sip.llm.tools; + +import com.ruoyi.sip.domain.ProductInfo; +import com.ruoyi.sip.llm.tools.support.AbstractMcpToolProvider; +import com.ruoyi.sip.service.IProductInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Component +public class ProductInfoToolProvider extends AbstractMcpToolProvider { + + private static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + + @Autowired + private IProductInfoService productInfoService; + + @Override + protected String getToolName() { + return "product_info"; + } + + @Override + protected String getToolDescription() { + return "根据产品编码或型号查询产品信息。"; + } + + @Override + protected Map buildInputSchema() { + Map properties = new LinkedHashMap<>(); + properties.put("product_code", stringProperty("产品编码")); + properties.put("model", stringProperty("产品型号")); + return objectSchema(properties); + } + + @Override + protected Object handle(Map params) { + String productCode = getString(params, "product_code"); + String model = getString(params, "model"); + if (isBlank(productCode) && isBlank(model)) { + throw new RuntimeException("Either product_code or model is required"); + } + + ProductInfo queryCondition = new ProductInfo(); + queryCondition.setProductCode(productCode); + queryCondition.setModel(model); + List productList = productInfoService.selectProductInfoList(queryCondition); + + List> items = new ArrayList<>(); + for (ProductInfo item : productList) { + items.add(toProductItem(item)); + } + + Map query = mapOf( + "product_code", productCode, + "model", model + ); + + Map data = new LinkedHashMap<>(); + data.put("total", items.size()); + data.put("items", items); + + Map metadata = new LinkedHashMap<>(); + metadata.put("tool", getToolName()); + metadata.put("description", "根据产品编码或型号查询产品信息"); + metadata.put("query_fields", mapOf( + "product_code", "产品编码,支持模糊匹配", + "model", "产品型号,支持模糊匹配" + )); + metadata.put("data_fields", mapOf( + "total", "命中的产品数量", + "items", "命中的产品列表" + )); + metadata.put("item_fields", mapOf( + "id", "Primary key id", + "productCode", "产品编码", + "productName", "产品名称", + "model", "Product model", + "description", "Product description", + "type", "Product type code", + "typeName", "产品类型名称", + "hzCode", "HZ 编码", + "vendorCode", "厂商编码", + "vendorName", "厂商名称", + "cataloguePrice", "目录价", + "guidanceDiscount", "指导折扣", + "availableCount", "可用库存数量", + "cumulativeCount", "累计出货数量", + "createdAt", "创建时间", + "updatedAt", "更新时间", + "remark", "备注" + )); + + return response(metadata, query, data); + } + + private Map toProductItem(ProductInfo item) { + Map result = new LinkedHashMap<>(); + result.put("id", item.getId()); + result.put("productCode", item.getProductCode()); + result.put("productName", item.getProductName()); + result.put("model", item.getModel()); + result.put("description", item.getDescription()); + result.put("type", item.getType()); + result.put("typeName", productTypeName(item.getType())); + result.put("hzCode", item.getHzCode()); + result.put("vendorCode", item.getVendorCode()); + result.put("vendorName", item.getVendorName()); + result.put("cataloguePrice", item.getCataloguePrice()); + result.put("guidanceDiscount", item.getGuidanceDiscount()); + result.put("availableCount", item.getAvailableCount()); + result.put("cumulativeCount", item.getCumulativeCount()); + result.put("createdAt", formatDate(item.getCreatedAt())); + result.put("updatedAt", formatDate(item.getUpdatedAt())); + result.put("remark", item.getRemark()); + return result; + } + + private String productTypeName(String type) { + if (isBlank(type)) { + return null; + } + for (ProductInfo.ProductTypeEnum value : ProductInfo.ProductTypeEnum.values()) { + if (type.equals(value.getType())) { + return value.getDesc(); + } + } + return type; + } + + private String formatDate(Date value) { + if (value == null) { + return null; + } + return new SimpleDateFormat(DATETIME_PATTERN).format(value); + } +} diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/ProjectOrderInfoToolProvider.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/ProjectOrderInfoToolProvider.java new file mode 100644 index 00000000..4b3a2b7e --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/ProjectOrderInfoToolProvider.java @@ -0,0 +1,343 @@ +package com.ruoyi.sip.llm.tools; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.sip.domain.InventoryOuter; +import com.ruoyi.sip.domain.ProductInfo; +import com.ruoyi.sip.domain.ProjectOrderInfo; +import com.ruoyi.sip.llm.tools.support.AbstractMcpToolProvider; +import com.ruoyi.sip.service.IInventoryOuterService; +import com.ruoyi.sip.service.IProductInfoService; +import com.ruoyi.sip.service.IProjectOrderInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class ProjectOrderInfoToolProvider extends AbstractMcpToolProvider { + + private static final List FIXED_ITEM_KEYS = Arrays.asList( + "projectCode", + "orderDate", + "orderEffectiveDate", + "requiredDeliveryDate", + "orderCode", + "projectName", + "agentName", + "ownerName", + "orderChannel", + "supplierName", + "customerName", + "bgName", + "industryType", + "projectPartnerName", + "businessContactName", + "businessContactPhone", + "currencyType", + "partnerName", + "partnerLevel", + "partnerContactName", + "partnerContactPhone", + "executionDeadline", + "archiveTime", + "wssQuantity", + "wssAmount", + "wssTaxRate", + "wspQuantity", + "wspAmount", + "wspTaxRate", + "lsQuantity", + "lsAmount", + "lsTaxRate" + ); + + private static final List TAIL_ITEM_KEYS = Arrays.asList( + "shipmentAmount", + "totalAmount", + "maintenanceAmount", + "softwareSubtotal", + "hardwareSubtotal", + "serviceSubtotal" + ); + + @Autowired + private IProjectOrderInfoService projectOrderInfoService; + + @Autowired + private IInventoryOuterService inventoryOuterService; + + @Autowired + private IProductInfoService productInfoService; + + @Override + protected String getToolName() { + return "project_order_info"; + } + + @Override + protected String getToolDescription() { + return "根据创建时间范围查询项目订单信息。"; + } + + @Override + protected Map buildInputSchema() { + Map properties = new LinkedHashMap<>(); + properties.put("create_time_start", stringProperty("创建时间开始,格式 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss")); + properties.put("create_time_end", stringProperty("创建时间结束,格式 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss")); + return objectSchema(properties, "create_time_start", "create_time_end"); + } + + @Override + protected Object handle(Map params) { + String createTimeStart = getString(params, "create_time_start"); + String createTimeEnd = getString(params, "create_time_end"); + if (isBlank(createTimeStart) || isBlank(createTimeEnd)) { + throw new RuntimeException("create_time_start and create_time_end are required"); + } + + ProjectOrderInfo queryCondition = new ProjectOrderInfo(); + queryCondition.setCreateTimeStart(parseDateTime(createTimeStart)); + queryCondition.setCreateTimeEnd(parseDateTime(createTimeEnd)); + + List> exportRows = projectOrderInfoService.listExportData(queryCondition); + Map>> outboundOrderMap = loadOutboundOrderMap(exportRows); + + List> items = new ArrayList<>(); + for (Map exportRow : exportRows) { + items.add(attachExtraInfo(exportRow, outboundOrderMap)); + } + + Map query = mapOf( + "create_time_start", createTimeStart, + "create_time_end", createTimeEnd + ); + + Map data = new LinkedHashMap<>(); + data.put("total", items.size()); + data.put("items", items); + + Map metadata = new LinkedHashMap<>(); + metadata.put("tool", getToolName()); + metadata.put("description", "根据创建时间范围查询项目订单信息,data.items 使用英文驼峰字段返回"); + metadata.put("query_fields", mapOf( + "create_time_start", "创建时间范围开始,包含边界", + "create_time_end", "创建时间范围结束,包含边界" + )); + metadata.put("data_fields", mapOf( + "total", "命中的项目订单数量", + "items", "项目订单列表,字段键已统一为英文驼峰" + )); + metadata.put("item_fields", buildItemFieldMetadata()); + metadata.put("dynamic_field_rules", mapOf( + "details", "产品明细列表", + "outboundOrders", "出库单列表" + )); + metadata.put("detail_fields", mapOf( + "productCode", "产品编码", + "model", "型号", + "quantity", "数量", + "amount", "金额", + "taxRate", "税率" + )); + metadata.put("outbound_order_fields", mapOf( + "outboundOrderCode", "出库单号", + "productCode", "产品编码", + "productName", "产品名称", + "quantity", "数量" + )); + + return response(metadata, query, data); + } + + private Map attachExtraInfo(Map exportRow, + Map>> outboundOrderMap) { + Map result = toCamelCaseItem(exportRow); + String orderCode = stringValue(exportRow.get("_order_code")); + List> outboundOrders = outboundOrderMap.get(orderCode); + result.put("outboundOrders", outboundOrders == null ? Collections.emptyList() : outboundOrders); + return result; + } + + private Map toCamelCaseItem(Map exportRow) { + List values = new ArrayList<>(); + for (Map.Entry entry : exportRow.entrySet()) { + if ("_order_code".equals(entry.getKey()) || "_project_id".equals(entry.getKey())) { + continue; + } + values.add(entry.getValue()); + } + + Map result = new LinkedHashMap<>(); + int fixedCount = Math.min(FIXED_ITEM_KEYS.size(), values.size()); + for (int i = 0; i < fixedCount; i++) { + result.put(FIXED_ITEM_KEYS.get(i), values.get(i)); + } + + int detailStart = fixedCount; + int detailEnd = Math.max(detailStart, values.size() - TAIL_ITEM_KEYS.size()); + List> details = new ArrayList<>(); + for (int i = detailStart; i + 4 < detailEnd; i += 5) { + Map detail = new LinkedHashMap<>(); + detail.put("productCode", values.get(i)); + detail.put("model", values.get(i + 1)); + detail.put("quantity", values.get(i + 2)); + detail.put("amount", values.get(i + 3)); + detail.put("taxRate", values.get(i + 4)); + if (!isEmptyDetail(detail)) { + details.add(detail); + } + } + result.put("details", details); + + int tailStart = Math.max(detailEnd, values.size() - TAIL_ITEM_KEYS.size()); + for (int i = 0; i < TAIL_ITEM_KEYS.size() && tailStart + i < values.size(); i++) { + result.put(TAIL_ITEM_KEYS.get(i), values.get(tailStart + i)); + } + return result; + } + + private Map>> loadOutboundOrderMap(List> exportRows) { + if (exportRows == null || exportRows.isEmpty()) { + return Collections.emptyMap(); + } + + Set orderCodes = exportRows.stream() + .map(row -> stringValue(row.get("_order_code"))) + .filter(code -> !isBlank(code)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (orderCodes.isEmpty()) { + return Collections.emptyMap(); + } + + InventoryOuter query = new InventoryOuter(); + query.setOrderCodeList(new ArrayList<>(orderCodes)); + List inventoryOuters = inventoryOuterService.selectInventoryOuterList(query); + if (inventoryOuters == null || inventoryOuters.isEmpty()) { + return Collections.emptyMap(); + } + + Set productCodes = inventoryOuters.stream() + .map(InventoryOuter::getProductCode) + .filter(code -> !isBlank(code)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map productNameMap = loadProductNameMap(productCodes); + + Map>> result = new LinkedHashMap<>(); + for (InventoryOuter inventoryOuter : inventoryOuters) { + String orderCode = inventoryOuter.getOrderCode(); + if (isBlank(orderCode)) { + continue; + } + result.computeIfAbsent(orderCode, key -> new ArrayList<>()) + .add(toOutboundOrderItem(inventoryOuter, productNameMap)); + } + return result; + } + + private Map toOutboundOrderItem(InventoryOuter inventoryOuter, Map productNameMap) { + Map item = new LinkedHashMap<>(); + item.put("outboundOrderCode", inventoryOuter.getOuterCode()); + item.put("productCode", inventoryOuter.getProductCode()); + item.put("productName", productNameMap.get(inventoryOuter.getProductCode())); + item.put("quantity", inventoryOuter.getQuantity()); + return item; + } + + private Map loadProductNameMap(Set productCodes) { + if (productCodes == null || productCodes.isEmpty()) { + return Collections.emptyMap(); + } + List productInfos = productInfoService.selectProductInfoByCodeList(new ArrayList<>(productCodes)); + return productInfos.stream().collect(Collectors.toMap(ProductInfo::getProductCode, ProductInfo::getProductName, (a, b) -> a)); + } + + private Date parseDateTime(String value) { + Date parsed = tryParse(value, DateUtils.YYYY_MM_DD_HH_MM_SS); + if (parsed != null) { + return parsed; + } + parsed = tryParse(value, DateUtils.YYYY_MM_DD); + if (parsed != null) { + return parsed; + } + throw new RuntimeException("Invalid datetime format: " + value); + } + + private Date tryParse(String value, String pattern) { + try { + return DateUtils.dateTime(pattern, value); + } catch (Exception ex) { + return null; + } + } + + private String stringValue(Object value) { + return value == null ? null : String.valueOf(value); + } + + private boolean isEmptyDetail(Map detail) { + if (detail == null || detail.isEmpty()) { + return true; + } + for (Object value : detail.values()) { + if (value != null && !isBlank(String.valueOf(value))) { + return false; + } + } + return true; + } + + private Map buildItemFieldMetadata() { + Map metadata = new LinkedHashMap<>(); + metadata.put("projectCode", "项目编号"); + metadata.put("orderDate", "下单时间"); + metadata.put("orderEffectiveDate", "订单生效时间"); + metadata.put("requiredDeliveryDate", "要求到货时间"); + metadata.put("orderCode", "合同或订单编号"); + metadata.put("projectName", "项目名称"); + metadata.put("agentName", "代表处"); + metadata.put("ownerName", "汇智责任人"); + metadata.put("orderChannel", "下单通路"); + metadata.put("supplierName", "供货商"); + metadata.put("customerName", "最终客户"); + metadata.put("bgName", "BG"); + metadata.put("industryType", "行业"); + metadata.put("projectPartnerName", "运作方"); + metadata.put("businessContactName", "进货商接口人"); + metadata.put("businessContactPhone", "进货商接口人联系方式"); + metadata.put("currencyType", "币种"); + metadata.put("partnerName", "进货商"); + metadata.put("partnerLevel", "进货商类型"); + metadata.put("partnerContactName", "进货商联系人"); + metadata.put("partnerContactPhone", "进货商联系方式"); + metadata.put("executionDeadline", "执行单截至时间"); + metadata.put("archiveTime", "归档时间"); + metadata.put("wssQuantity", "WS 瘦授权软件数量"); + metadata.put("wssAmount", "WS 瘦授权软件金额"); + metadata.put("wssTaxRate", "WS 瘦授权软件税率"); + metadata.put("wspQuantity", "WS 胖授权软件数量"); + metadata.put("wspAmount", "WS 胖授权软件金额"); + metadata.put("wspTaxRate", "WS 胖授权软件税率"); + metadata.put("lsQuantity", "LS 软件数量"); + metadata.put("lsAmount", "LS 软件金额"); + metadata.put("lsTaxRate", "LS 软件税率"); + metadata.put("details", "产品明细列表"); + metadata.put("shipmentAmount", "总发货金额"); + metadata.put("totalAmount", "总价合计"); + metadata.put("maintenanceAmount", "维保金额"); + metadata.put("softwareSubtotal", "软件折后小计"); + metadata.put("hardwareSubtotal", "硬件折后小计"); + metadata.put("serviceSubtotal", "服务折后小计"); + metadata.put("outboundOrders", "出库单列表"); + return metadata; + } +} diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/support/AbstractMcpToolProvider.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/support/AbstractMcpToolProvider.java new file mode 100644 index 00000000..d63fddb9 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/llm/tools/support/AbstractMcpToolProvider.java @@ -0,0 +1,79 @@ +package com.ruoyi.sip.llm.tools.support; + +import com.ruoyi.sip.llm.McpTool; +import com.ruoyi.sip.llm.tools.McpToolProvider; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public abstract class AbstractMcpToolProvider implements McpToolProvider { + + @Override + public McpTool buildTool() { + McpTool tool = new McpTool(); + tool.setName(getToolName()); + tool.setDescription(getToolDescription()); + tool.setInputSchema(buildInputSchema()); + tool.setHandler(this::handle); + return tool; + } + + protected abstract String getToolName(); + + protected abstract String getToolDescription(); + + protected abstract Map buildInputSchema(); + + protected abstract Object handle(Map params); + + protected Map objectSchema(Map properties, String... requiredFields) { + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", properties); + schema.put("required", requiredFields == null || requiredFields.length == 0 + ? Collections.emptyList() + : java.util.Arrays.asList(requiredFields)); + schema.put("additionalProperties", false); + return schema; + } + + protected Map stringProperty(String description) { + Map property = new HashMap<>(); + property.put("type", "string"); + property.put("description", description); + return property; + } + + protected Map response(Object metadata, Object query, Object data) { + Map result = new LinkedHashMap<>(); + result.put("metadata", metadata); + result.put("query", query); + result.put("data", data); + return result; + } + + protected Map mapOf(String... keyValues) { + if (keyValues == null || keyValues.length % 2 != 0) { + throw new IllegalArgumentException("mapOf requires an even number of arguments"); + } + Map map = new LinkedHashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + map.put(keyValues[i], keyValues[i + 1]); + } + return map; + } + + protected String getString(Map params, String key) { + if (params == null) { + return null; + } + Object value = params.get(key); + return value == null ? null : String.valueOf(value).trim(); + } + + protected boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } +} \ No newline at end of file diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IOrderLLMService.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IOrderLLMService.java new file mode 100644 index 00000000..da290b61 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IOrderLLMService.java @@ -0,0 +1,23 @@ +package com.ruoyi.sip.service; + + +import javax.xml.ws.Service; +import java.util.Date; + +/** + * @author : ch + * @version : 1.0 + * @ClassName : IOrderLLMService + * @Description : + * @DATE : Created in 16:20 2026/3/19 + *
           Copyright: Copyright(c) 2026     
    + *
           Company :   	紫光汇智信息技术有限公司		           
    + * Modification History: + * Date Author Version Discription + * -------------------------------------------------------------------------- + * 2026/03/19 ch 1.0 Why & What is modified: <修改原因描述> * + */ +public interface IOrderLLMService { + + void listLLMData(Date date, Date date1) ; +} diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectOrderInfoService.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectOrderInfoService.java index 4f256695..9939f851 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectOrderInfoService.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectOrderInfoService.java @@ -75,9 +75,10 @@ public interface IProjectOrderInfoService * @date 2025/05/30 16:13 */ - List< ProjectOrderInfo> selectProjectOrderInfoByProjectId(List projectId); + List< ProjectOrderInfo> selectProjectOrderInfoByProjectId(List projectId); String exportList(ProjectOrderInfo projectOrderInfo); + List> listExportData(ProjectOrderInfo projectOrderInfo); List listHomePageData(HomepageQueryDto dto); diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/IOrderLLMServiceImpl.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/IOrderLLMServiceImpl.java new file mode 100644 index 00000000..be13aa14 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/IOrderLLMServiceImpl.java @@ -0,0 +1,31 @@ +package com.ruoyi.sip.service.impl; + + +import com.ruoyi.sip.service.IOrderLLMService; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Date; + +/** + * @author : ch + * @version : 1.0 + * @ClassName : IOrderLLMServiceImpl + * @Description : + * @DATE : Created in 16:21 2026/3/19 + *
           Copyright: Copyright(c) 2026     
    + *
           Company :   	紫光汇智信息技术有限公司		           
    + * Modification History: + * Date Author Version Discription + * -------------------------------------------------------------------------- + * 2026/03/19 ch 1.0 Why & What is modified: <修改原因描述> * + */ +public class IOrderLLMServiceImpl implements IOrderLLMService { + + + + + @Override + public void listLLMData(Date date, Date date1) { + + } +} diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java index 936442d6..d18a49c0 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java @@ -561,6 +561,37 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To } } + @Override + public List> listExportData(ProjectOrderInfo projectOrderInfo) { + List projectInfos = this.selectProjectOrderInfoList(projectOrderInfo); + if (CollUtil.isEmpty(projectInfos)) { + return Collections.emptyList(); + } + + enrichProjectData(projectInfos); + List maxCounts = calculateMaxProductCounts(projectInfos); + List> headers = buildExcelHeader(maxCounts); + List> dataRows = buildExcelData(projectInfos, maxCounts); + + List headerNames = headers.stream() + .map(item -> CollUtil.isEmpty(item) ? "" : item.get(0)) + .collect(Collectors.toList()); + + List> result = new ArrayList<>(); + for (int rowIndex = 0; rowIndex < dataRows.size(); rowIndex++) { + List row = dataRows.get(rowIndex); + ProjectOrderInfo info = projectInfos.get(rowIndex); + Map rowMap = new LinkedHashMap<>(); + for (int colIndex = 0; colIndex < headerNames.size(); colIndex++) { + rowMap.put(headerNames.get(colIndex), colIndex < row.size() ? row.get(colIndex) : null); + } + rowMap.put("_order_code", info.getOrderCode()); + rowMap.put("_project_id", info.getProjectId()); + result.add(rowMap); + } + return result; + } + @Override public List listHomePageData(HomepageQueryDto dto) { SysUser sysUser = ShiroUtils.getSysUser(); diff --git a/ruoyi-sip/src/main/resources/mapper/inventory/InventoryOuterMapper.xml b/ruoyi-sip/src/main/resources/mapper/inventory/InventoryOuterMapper.xml index a829c925..ef6fdda7 100644 --- a/ruoyi-sip/src/main/resources/mapper/inventory/InventoryOuterMapper.xml +++ b/ruoyi-sip/src/main/resources/mapper/inventory/InventoryOuterMapper.xml @@ -94,6 +94,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and t1.order_code = #{orderCode} + and t1.order_code in + + #{item} + + and t1.contact_person = #{contactPerson} and t1.contact_phone = #{contactPhone} and t1.delivery_status = #{deliveryStatus} @@ -273,4 +278,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - \ No newline at end of file + diff --git a/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderInfoMapper.xml b/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderInfoMapper.xml index 83f6f168..3f40c168 100644 --- a/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderInfoMapper.xml +++ b/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderInfoMapper.xml @@ -126,6 +126,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and (t1.order_code like concat('%',#{keyword},'%') or t2.project_name like concat('%',#{keyword},'%') or t2.customer_name like concat('%',#{keyword},'%')) + + + + and t1.create_time between #{createTimeStart} and #{createTimeEnd} + + + and t1.create_time = ]]> #{createTimeStart} + + + and t1.create_time #{createTimeEnd} + + + @@ -839,4 +852,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - \ No newline at end of file + diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysBotCredential.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysBotCredential.java new file mode 100644 index 00000000..01e4a119 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysBotCredential.java @@ -0,0 +1,22 @@ +package com.ruoyi.system.domain; + +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; + +import java.util.Date; + +@Data +public class SysBotCredential extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + private String botId; + private String secretHash; + private String secretSalt; + private Long userId; + private String status; + private Date expireTime; + private Date lastAccessTime; + private String lastAccessIp; + private String remark; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysBotCredentialMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysBotCredentialMapper.java new file mode 100644 index 00000000..47285f67 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysBotCredentialMapper.java @@ -0,0 +1,15 @@ +package com.ruoyi.system.mapper; + +import com.ruoyi.system.domain.SysBotCredential; + +public interface SysBotCredentialMapper { + SysBotCredential selectByBotId(String botId); + + SysBotCredential selectByUserId(Long userId); + + int insertSysBotCredential(SysBotCredential credential); + + int updateSysBotCredential(SysBotCredential credential); + + int updateLastAccessInfo(SysBotCredential credential); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysBotCredentialService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysBotCredentialService.java new file mode 100644 index 00000000..ce4808d7 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysBotCredentialService.java @@ -0,0 +1,14 @@ +package com.ruoyi.system.service; + +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.system.domain.SysBotCredential; + +import java.util.Map; + +public interface ISysBotCredentialService { + SysUser authenticateMcpBot(String botId, String botSecret, String clientIp); + + SysBotCredential selectByUserId(Long userId); + + Map generateCredentialForUser(SysUser user); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysBotCredentialServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysBotCredentialServiceImpl.java new file mode 100644 index 00000000..3147367b --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysBotCredentialServiceImpl.java @@ -0,0 +1,135 @@ +package com.ruoyi.system.service.impl; + +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.security.McpBotCredentialUtils; +import com.ruoyi.common.utils.security.Md5Utils; +import com.ruoyi.system.domain.SysBotCredential; +import com.ruoyi.system.mapper.SysBotCredentialMapper; +import com.ruoyi.system.service.ISysBotCredentialService; +import com.ruoyi.system.service.ISysMenuService; +import com.ruoyi.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.LinkedHashMap; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Service +public class SysBotCredentialServiceImpl implements ISysBotCredentialService { + + @Autowired + private SysBotCredentialMapper botCredentialMapper; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysMenuService menuService; + + @Override + public SysUser authenticateMcpBot(String botId, String botSecret, String clientIp) { + if (StringUtils.isEmpty(botId) || StringUtils.isEmpty(botSecret)) { + throw new ServiceException("Missing MCP bot credentials"); + } + + SysBotCredential credential = botCredentialMapper.selectByBotId(botId); + if (credential == null) { + throw new ServiceException("Invalid MCP bot credentials"); + } + + if (!UserConstants.NORMAL.equals(credential.getStatus())) { + throw new ServiceException("MCP bot is disabled"); + } + + if (credential.getExpireTime() != null && credential.getExpireTime().before(new Date())) { + throw new ServiceException("MCP bot has expired"); + } + + String salt = StringUtils.defaultString(credential.getSecretSalt()); + String expectedHash = Md5Utils.hash(botId + botSecret + salt); + if (!StringUtils.equals(expectedHash, credential.getSecretHash())) { + throw new ServiceException("Invalid MCP bot credentials"); + } + + SysUser user = userService.selectUserById(credential.getUserId()); + if (user == null || !UserConstants.NORMAL.equals(user.getStatus()) || !"0".equals(user.getDelFlag())) { + throw new ServiceException("Bound user is disabled or deleted"); + } + + enrichRolePermissions(user); + + SysBotCredential update = new SysBotCredential(); + update.setId(credential.getId()); + update.setLastAccessTime(new Date()); + update.setLastAccessIp(clientIp); + botCredentialMapper.updateLastAccessInfo(update); + return user; + } + + @Override + public SysBotCredential selectByUserId(Long userId) { + return botCredentialMapper.selectByUserId(userId); + } + + @Override + public Map generateCredentialForUser(SysUser user) { + if (user == null || user.getUserId() == null) { + throw new ServiceException("Current user not found"); + } + if (!UserConstants.NORMAL.equals(user.getStatus()) || !"0".equals(user.getDelFlag())) { + throw new ServiceException("Current user is disabled or deleted"); + } + + SysBotCredential existing = botCredentialMapper.selectByUserId(user.getUserId()); + String botId = existing != null && StringUtils.isNotEmpty(existing.getBotId()) + ? existing.getBotId() + : McpBotCredentialUtils.generateBotId("u" + user.getUserId()); + String botSecret = McpBotCredentialUtils.generateSecret(); + String salt = McpBotCredentialUtils.generateSalt(); + String secretHash = McpBotCredentialUtils.hashSecret(botId, botSecret, salt); + + SysBotCredential credential = existing != null ? existing : new SysBotCredential(); + credential.setBotId(botId); + credential.setSecretSalt(salt); + credential.setSecretHash(secretHash); + credential.setUserId(user.getUserId()); + credential.setStatus(UserConstants.NORMAL); + credential.setRemark("MCP bot credential"); + credential.setUpdateBy(user.getLoginName()); + + if (existing == null) { + credential.setCreateBy(user.getLoginName()); + botCredentialMapper.insertSysBotCredential(credential); + } else { + botCredentialMapper.updateSysBotCredential(credential); + } + + Map result = new LinkedHashMap<>(); + result.put("botId", botId); + result.put("botSecret", botSecret); + result.put("status", credential.getStatus()); + result.put("expireTime", credential.getExpireTime()); + return result; + } + + private void enrichRolePermissions(SysUser user) { + List roles = user.getRoles(); + if (roles == null || roles.isEmpty()) { + return; + } + + for (SysRole role : roles) { + if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && !role.isAdmin()) { + Set rolePerms = menuService.selectPermsByRoleId(role.getRoleId()); + role.setPermissions(rolePerms); + } + } + } +}