feat: 添加MCP Bot凭证生成和管理功能
- 新增McpBotCredentialUtils工具类,提供Bot ID、Secret、Salt及哈希生成方法 - 在SysProfileController中添加Bot凭证相关接口,支持查询和生成Bot凭证 - 引入新的依赖ruoyi-framework,并更新pom.xml - 扩展InventoryOuter实体类,添加orderCodeList属性 - 实现IOrderLLMService接口及其服务实现类IOrderLLMServiceImpl - 添加McpToolProvider接口及相关实现类ProjectOrderInfoToolProvider和ProductInfoToolProvider - 增加AbstractMcpToolProvider抽象类,提供通用的MCP工具构建逻辑 - 更新ProjectOrderInfo实体类,添加createTimeStart和createTimeEnd属性 - 新增SysBotCredentialMapper接口,定义Bot凭证相关数据库操作 - 添加McpBotAuthFilter过滤器,处理Bot身份验证逻辑dev_1.0.2
parent
adae1ab595
commit
48bad8a9a6
|
|
@ -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>
|
||||
* ToolEmbedding:
|
||||
|
||||
* toolName
|
||||
* description
|
||||
* embedding(double[])
|
||||
|
||||
2. 提供方法:
|
||||
List<McpTool> 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. 不要解释,只输出代码
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
</li>
|
||||
<li class="list-group-item">
|
||||
<svg-icon icon-class="tree" />所属部门
|
||||
<div class="pull-right" v-if="user.dept">{{ user.dept.deptName }} </div>
|
||||
<div class="pull-right" v-if="user.dept">{{ user.dept.deptName }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<svg-icon icon-class="peoples" />所属角色
|
||||
|
|
@ -51,6 +51,9 @@
|
|||
<el-tab-pane label="修改密码" name="resetPwd">
|
||||
<resetPwd />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Bot 凭证" name="bot">
|
||||
<botConfig />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
|
@ -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(',')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,13 @@ 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.domain.SysBotCredential;
|
||||
import com.ruoyi.system.service.ISysBotCredentialService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 个人信息 业务处理
|
||||
*
|
||||
|
|
@ -44,6 +49,9 @@ public class SysProfileController extends BaseController
|
|||
@Autowired
|
||||
private SysPasswordService passwordService;
|
||||
|
||||
@Autowired
|
||||
private ISysBotCredentialService botCredentialService;
|
||||
|
||||
/**
|
||||
* 个人信息
|
||||
*/
|
||||
|
|
@ -186,4 +194,33 @@ public class SysProfileController extends BaseController
|
|||
return error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/bot")
|
||||
@ResponseBody
|
||||
public AjaxResult currentBot()
|
||||
{
|
||||
SysUser user = getSysUser();
|
||||
SysBotCredential credential = botCredentialService.selectByUserId(user.getUserId());
|
||||
Map<String, Object> 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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, Object> buildError(int code, String message, String type) {
|
||||
Map<String, Object> error = new LinkedHashMap<>();
|
||||
error.put("code", code);
|
||||
error.put("message", message);
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("type", type);
|
||||
error.put("data", data);
|
||||
return error;
|
||||
}
|
||||
|
||||
public static Map<String, Object> buildError(int code, String message, String type, Map<String, Object> extraData) {
|
||||
Map<String, Object> error = new LinkedHashMap<>();
|
||||
error.put("code", code);
|
||||
error.put("message", message);
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("type", type);
|
||||
if (extraData != null && !extraData.isEmpty()) {
|
||||
data.putAll(extraData);
|
||||
}
|
||||
error.put("data", data);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String> generateCredentialPair(String prefix) {
|
||||
String botId = generateBotId(prefix);
|
||||
String botSecret = generateSecret();
|
||||
String salt = generateSalt();
|
||||
String secretHash = hashSecret(botId, botSecret, salt);
|
||||
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
result.put("botId", botId);
|
||||
result.put("botSecret", botSecret);
|
||||
result.put("secretSalt", salt);
|
||||
result.put("secretHash", secretHash);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -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框架的整合
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<String, Object> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,6 +40,10 @@
|
|||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-framework</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf</groupId>
|
||||
<artifactId>thymeleaf</artifactId>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// * <pre> Copyright: Copyright(c) 2025 </pre>
|
||||
// * <pre> Company : 紫光汇智信息技术有限公司 </pre>
|
||||
// * 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<String, SseEmitter > 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));
|
||||
// }
|
||||
//}
|
||||
|
|
@ -41,6 +41,7 @@ public class InventoryOuter extends BaseEntity
|
|||
/** 订单编码 */
|
||||
@Excel(name = "合同编号")
|
||||
private String orderCode;
|
||||
private List<String> orderCodeList;
|
||||
private String orderId;
|
||||
@Excel(name = "项目名称")
|
||||
private String projectName;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 订单状态
|
||||
|
|
|
|||
|
|
@ -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<String, Object> whoami() {
|
||||
Map<String, Object> 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<String, Object> 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Object> 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<String, Object> getParams() { return params; }
|
||||
public void setParams(Map<String, Object> params) { this.params = params; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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<String, Object> params = request.getParams();
|
||||
|
||||
if ("initialize".equals(method)) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("protocolVersion", "2024-11-05");
|
||||
|
||||
Map<String, Object> capabilities = new HashMap<>();
|
||||
capabilities.put("tools", new HashMap<String, Object>());
|
||||
result.put("capabilities", capabilities);
|
||||
|
||||
Map<String, String> 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<String, Object> result = new HashMap<>();
|
||||
List<McpTool> tools = registry.list();
|
||||
result.put("tools", tools.stream().map(t -> {
|
||||
Map<String, Object> 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<String, Object> args = params != null ? (Map<String, Object>) 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<String, Object> rawMap = (Map<String, Object>) rawResult;
|
||||
if (rawMap.containsKey("content")) {
|
||||
return rawMap;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
List<Map<String, Object>> content = new ArrayList<>();
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Object> inputSchema;
|
||||
private Function<Map<String, Object>, 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<String, Object> getInputSchema() { return inputSchema; }
|
||||
public void setInputSchema(Map<String, Object> inputSchema) { this.inputSchema = inputSchema; }
|
||||
public Function<Map<String, Object>, Object> getHandler() { return handler; }
|
||||
public void setHandler(Function<Map<String, Object>, Object> handler) { this.handler = handler; }
|
||||
}
|
||||
|
|
@ -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<String, McpTool> tools = new ConcurrentHashMap<>();
|
||||
|
||||
public void register(McpTool tool) {
|
||||
tools.put(tool.getName(), tool);
|
||||
}
|
||||
|
||||
public McpTool get(String name) {
|
||||
return tools.get(name);
|
||||
}
|
||||
|
||||
public List<McpTool> list() {
|
||||
return new ArrayList<>(tools.values());
|
||||
}
|
||||
}
|
||||
|
|
@ -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<McpToolProvider> toolProviders;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
for (McpToolProvider toolProvider : toolProviders) {
|
||||
registry.register(toolProvider.buildTool());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.llm.McpTool;
|
||||
|
||||
public interface McpToolProvider {
|
||||
|
||||
McpTool buildTool();
|
||||
}
|
||||
|
|
@ -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<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("product_code", stringProperty("产品编码"));
|
||||
properties.put("model", stringProperty("产品型号"));
|
||||
return objectSchema(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> 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<ProductInfo> productList = productInfoService.selectProductInfoList(queryCondition);
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (ProductInfo item : productList) {
|
||||
items.add(toProductItem(item));
|
||||
}
|
||||
|
||||
Map<String, Object> query = mapOf(
|
||||
"product_code", productCode,
|
||||
"model", model
|
||||
);
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("total", items.size());
|
||||
data.put("items", items);
|
||||
|
||||
Map<String, Object> 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<String, Object> toProductItem(ProductInfo item) {
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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<String> 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<String, Object> buildInputSchema() {
|
||||
Map<String, Object> 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<String, Object> 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<Map<String, Object>> exportRows = projectOrderInfoService.listExportData(queryCondition);
|
||||
Map<String, List<Map<String, Object>>> outboundOrderMap = loadOutboundOrderMap(exportRows);
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Map<String, Object> exportRow : exportRows) {
|
||||
items.add(attachExtraInfo(exportRow, outboundOrderMap));
|
||||
}
|
||||
|
||||
Map<String, Object> query = mapOf(
|
||||
"create_time_start", createTimeStart,
|
||||
"create_time_end", createTimeEnd
|
||||
);
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("total", items.size());
|
||||
data.put("items", items);
|
||||
|
||||
Map<String, Object> 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<String, Object> attachExtraInfo(Map<String, Object> exportRow,
|
||||
Map<String, List<Map<String, Object>>> outboundOrderMap) {
|
||||
Map<String, Object> result = toCamelCaseItem(exportRow);
|
||||
String orderCode = stringValue(exportRow.get("_order_code"));
|
||||
List<Map<String, Object>> outboundOrders = outboundOrderMap.get(orderCode);
|
||||
result.put("outboundOrders", outboundOrders == null ? Collections.emptyList() : outboundOrders);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> toCamelCaseItem(Map<String, Object> exportRow) {
|
||||
List<Object> values = new ArrayList<>();
|
||||
for (Map.Entry<String, Object> entry : exportRow.entrySet()) {
|
||||
if ("_order_code".equals(entry.getKey()) || "_project_id".equals(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
values.add(entry.getValue());
|
||||
}
|
||||
|
||||
Map<String, Object> 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<Map<String, Object>> details = new ArrayList<>();
|
||||
for (int i = detailStart; i + 4 < detailEnd; i += 5) {
|
||||
Map<String, Object> 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<String, List<Map<String, Object>>> loadOutboundOrderMap(List<Map<String, Object>> exportRows) {
|
||||
if (exportRows == null || exportRows.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Set<String> 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<InventoryOuter> inventoryOuters = inventoryOuterService.selectInventoryOuterList(query);
|
||||
if (inventoryOuters == null || inventoryOuters.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Set<String> productCodes = inventoryOuters.stream()
|
||||
.map(InventoryOuter::getProductCode)
|
||||
.filter(code -> !isBlank(code))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<String, String> productNameMap = loadProductNameMap(productCodes);
|
||||
|
||||
Map<String, List<Map<String, Object>>> 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<String, Object> toOutboundOrderItem(InventoryOuter inventoryOuter, Map<String, String> productNameMap) {
|
||||
Map<String, Object> 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<String, String> loadProductNameMap(Set<String> productCodes) {
|
||||
if (productCodes == null || productCodes.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ProductInfo> 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<String, Object> 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<String, Object> buildItemFieldMetadata() {
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Object> buildInputSchema();
|
||||
|
||||
protected abstract Object handle(Map<String, Object> params);
|
||||
|
||||
protected Map<String, Object> objectSchema(Map<String, Object> properties, String... requiredFields) {
|
||||
Map<String, Object> 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<String, Object> stringProperty(String description) {
|
||||
Map<String, Object> property = new HashMap<>();
|
||||
property.put("type", "string");
|
||||
property.put("description", description);
|
||||
return property;
|
||||
}
|
||||
|
||||
protected Map<String, Object> response(Object metadata, Object query, Object data) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("metadata", metadata);
|
||||
result.put("query", query);
|
||||
result.put("data", data);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected Map<String, Object> mapOf(String... keyValues) {
|
||||
if (keyValues == null || keyValues.length % 2 != 0) {
|
||||
throw new IllegalArgumentException("mapOf requires an even number of arguments");
|
||||
}
|
||||
Map<String, Object> 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<String, Object> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
* <pre> Copyright: Copyright(c) 2026 </pre>
|
||||
* <pre> Company : 紫光汇智信息技术有限公司 </pre>
|
||||
* 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) ;
|
||||
}
|
||||
|
|
@ -75,9 +75,10 @@ public interface IProjectOrderInfoService
|
|||
* @date 2025/05/30 16:13
|
||||
*/
|
||||
|
||||
List< ProjectOrderInfo> selectProjectOrderInfoByProjectId(List<Long> projectId);
|
||||
List< ProjectOrderInfo> selectProjectOrderInfoByProjectId(List<Long> projectId);
|
||||
|
||||
String exportList(ProjectOrderInfo projectOrderInfo);
|
||||
List<java.util.Map<String, Object>> listExportData(ProjectOrderInfo projectOrderInfo);
|
||||
|
||||
List<StatisticsDetailDto> listHomePageData(HomepageQueryDto dto);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* <pre> Copyright: Copyright(c) 2026 </pre>
|
||||
* <pre> Company : 紫光汇智信息技术有限公司 </pre>
|
||||
* 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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -561,6 +561,37 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> listExportData(ProjectOrderInfo projectOrderInfo) {
|
||||
List<ProjectOrderInfo> projectInfos = this.selectProjectOrderInfoList(projectOrderInfo);
|
||||
if (CollUtil.isEmpty(projectInfos)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
enrichProjectData(projectInfos);
|
||||
List<Integer> maxCounts = calculateMaxProductCounts(projectInfos);
|
||||
List<List<String>> headers = buildExcelHeader(maxCounts);
|
||||
List<List<Object>> dataRows = buildExcelData(projectInfos, maxCounts);
|
||||
|
||||
List<String> headerNames = headers.stream()
|
||||
.map(item -> CollUtil.isEmpty(item) ? "" : item.get(0))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (int rowIndex = 0; rowIndex < dataRows.size(); rowIndex++) {
|
||||
List<Object> row = dataRows.get(rowIndex);
|
||||
ProjectOrderInfo info = projectInfos.get(rowIndex);
|
||||
Map<String, Object> 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<StatisticsDetailDto> listHomePageData(HomepageQueryDto dto) {
|
||||
SysUser sysUser = ShiroUtils.getSysUser();
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
|
||||
</if>
|
||||
<if test="orderCode != null and orderCode != ''">and t1.order_code = #{orderCode}</if>
|
||||
<if test="orderCodeList != null and orderCodeList.size>0">and t1.order_code in
|
||||
<foreach item="item" index="index" collection="orderCodeList" separator="," open="(" close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="contactPerson != null and contactPerson != ''">and t1.contact_person = #{contactPerson}</if>
|
||||
<if test="contactPhone != null and contactPhone != ''">and t1.contact_phone = #{contactPhone}</if>
|
||||
<if test="deliveryStatus != null and deliveryStatus != ''">and t1.delivery_status = #{deliveryStatus}</if>
|
||||
|
|
|
|||
|
|
@ -126,6 +126,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<if test="keyword != null and keyword != ''">and (t1.order_code like concat('%',#{keyword},'%') or
|
||||
t2.project_name like concat('%',#{keyword},'%') or t2.customer_name like concat('%',#{keyword},'%'))
|
||||
</if>
|
||||
<if test="createTimeStart != null or createTimeEnd != null">
|
||||
<choose>
|
||||
<when test="createTimeStart != null and createTimeEnd != null">
|
||||
and t1.create_time between #{createTimeStart} and #{createTimeEnd}
|
||||
</when>
|
||||
<when test="createTimeStart != null">
|
||||
and t1.create_time <![CDATA[ >= ]]> #{createTimeStart}
|
||||
</when>
|
||||
<when test="createTimeEnd != null">
|
||||
and t1.create_time <![CDATA[ <= ]]> #{createTimeEnd}
|
||||
</when>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="deliveryTimeStart != null or deliveryTimeEnd != null">
|
||||
<choose>
|
||||
<when test="deliveryTimeStart != null and deliveryTimeEnd != null">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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<String, Object> generateCredentialForUser(SysUser user);
|
||||
}
|
||||
|
|
@ -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<String, Object> 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<String, Object> 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<SysRole> roles = user.getRoles();
|
||||
if (roles == null || roles.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (SysRole role : roles) {
|
||||
if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && !role.isAdmin()) {
|
||||
Set<String> rolePerms = menuService.selectPermsByRoleId(role.getRoleId());
|
||||
role.setPermissions(rolePerms);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue