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
chenhao 2026-03-20 11:24:23 +08:00
parent adae1ab595
commit 48bad8a9a6
33 changed files with 1699 additions and 40 deletions

139
docx/prompt.md 100644
View File

@ -0,0 +1,139 @@
你是一个资深 Java 后端工程师,请在当前 Spring BootJava8项目中实现一个完整的 MCP Server并加入“AI 自动选 ToolRAG + 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 streamingwrite + 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 APIOpenAI / 本地模型)
3. 支持多 MCP Server未来扩展
# 十、输出要求
1. 输出完整代码:
* McpController
* McpService
* McpToolRegistry
* ToolRetriever
* ToolRouter
* ToolEmbedding
* ToolInitializer
* McpRequest
2. 代码必须:
* 可运行
* 无伪代码
* 包含必要注释
* 代码生成目录在ruoyi-sip/src/main/java/com/ruoyi/sip/llm
3. 不要解释,只输出代码

View File

@ -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',

View File

@ -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(',')
})
}
}

View File

@ -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<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));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
/**
* thymeleafshiro
*/

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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));
// }
//}

View File

@ -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;

View File

@ -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;
/**
*

View File

@ -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");
}
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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());
}
}

View File

@ -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());
}
}
}

View File

@ -0,0 +1,8 @@
package com.ruoyi.sip.llm.tools;
import com.ruoyi.sip.llm.McpTool;
public interface McpToolProvider {
McpTool buildTool();
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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) ;
}

View File

@ -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);

View File

@ -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) {
}
}

View File

@ -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();

View File

@ -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>
@ -273,4 +278,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
</delete>
</mapper>
</mapper>

View File

@ -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">
@ -839,4 +852,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
</delete>
</mapper>
</mapper>

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}
}
}