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 request from '@/utils/request'
|
||||||
import { parseStrEmpty } from "@/utils/ruoyi";
|
import { parseStrEmpty } from "@/utils/ruoyi";
|
||||||
|
|
||||||
// 查询用户列表
|
// 鏌ヨ鐢ㄦ埛鍒楄〃
|
||||||
export function listUser(query) {
|
export function listUser(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/list',
|
url: '/system/user/list',
|
||||||
|
|
@ -10,7 +10,7 @@ export function listUser(query) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询用户详细
|
// 鏌ヨ鐢ㄦ埛璇︾粏
|
||||||
export function getUser(userId) {
|
export function getUser(userId) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/' + parseStrEmpty(userId),
|
url: '/system/user/' + parseStrEmpty(userId),
|
||||||
|
|
@ -18,7 +18,7 @@ export function getUser(userId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增用户
|
// 鏂板鐢ㄦ埛
|
||||||
export function addUser(data) {
|
export function addUser(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/add',
|
url: '/system/user/add',
|
||||||
|
|
@ -30,7 +30,7 @@ export function addUser(data) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改用户
|
// 淇敼鐢ㄦ埛
|
||||||
export function updateUser(data) {
|
export function updateUser(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user',
|
url: '/system/user',
|
||||||
|
|
@ -39,7 +39,7 @@ export function updateUser(data) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除用户
|
// 鍒犻櫎鐢ㄦ埛
|
||||||
export function delUser(userId) {
|
export function delUser(userId) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/' + userId,
|
url: '/system/user/' + userId,
|
||||||
|
|
@ -47,7 +47,7 @@ export function delUser(userId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户密码重置
|
// 鐢ㄦ埛瀵嗙爜閲嶇疆
|
||||||
export function resetUserPwd(data) {
|
export function resetUserPwd(data) {
|
||||||
|
|
||||||
return request({
|
return request({
|
||||||
|
|
@ -60,7 +60,7 @@ export function resetUserPwd(data) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户状态修改
|
// 鐢ㄦ埛鐘舵€佷慨鏀?
|
||||||
export function changeUserStatus(userId, status) {
|
export function changeUserStatus(userId, status) {
|
||||||
const data = {
|
const data = {
|
||||||
userId,
|
userId,
|
||||||
|
|
@ -76,7 +76,7 @@ export function changeUserStatus(userId, status) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询用户个人信息
|
// 鏌ヨ鐢ㄦ埛涓汉淇℃伅
|
||||||
export function getUserProfile() {
|
export function getUserProfile() {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/profile/vue',
|
url: '/system/user/profile/vue',
|
||||||
|
|
@ -84,7 +84,7 @@ export function getUserProfile() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改用户个人信息
|
// 淇敼鐢ㄦ埛涓汉淇℃伅
|
||||||
export function updateUserProfile(data) {
|
export function updateUserProfile(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/profile',
|
url: '/system/user/profile',
|
||||||
|
|
@ -93,7 +93,7 @@ export function updateUserProfile(data) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户密码重置
|
// 鐢ㄦ埛瀵嗙爜閲嶇疆
|
||||||
export function updateUserPwd(data) {
|
export function updateUserPwd(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/profile/resetPwd',
|
url: '/system/user/profile/resetPwd',
|
||||||
|
|
@ -103,7 +103,7 @@ export function updateUserPwd(data) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户头像上传
|
// 鐢ㄦ埛澶村儚涓婁紶
|
||||||
export function uploadAvatar(data) {
|
export function uploadAvatar(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/profile/updateAvatar',
|
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) {
|
export function getAuthRole(userId) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/vue/authRole/' + userId,
|
url: '/system/user/vue/authRole/' + userId,
|
||||||
|
|
@ -121,7 +134,7 @@ export function getAuthRole(userId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存授权角色
|
// 淇濆瓨鎺堟潈瑙掕壊
|
||||||
export function updateAuthRole(data) {
|
export function updateAuthRole(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/authRole',
|
url: '/system/user/authRole',
|
||||||
|
|
@ -130,7 +143,7 @@ export function updateAuthRole(data) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询部门下拉树结构
|
// 鏌ヨ閮ㄩ棬涓嬫媺鏍戠粨鏋?
|
||||||
export function deptTreeSelect() {
|
export function deptTreeSelect() {
|
||||||
return request({
|
return request({
|
||||||
url: '/system/user/deptTree',
|
url: '/system/user/deptTree',
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<svg-icon icon-class="tree" />所属部门
|
<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>
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<svg-icon icon-class="peoples" />所属角色
|
<svg-icon icon-class="peoples" />所属角色
|
||||||
|
|
@ -51,6 +51,9 @@
|
||||||
<el-tab-pane label="修改密码" name="resetPwd">
|
<el-tab-pane label="修改密码" name="resetPwd">
|
||||||
<resetPwd />
|
<resetPwd />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="Bot 凭证" name="bot">
|
||||||
|
<botConfig />
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
@ -62,11 +65,12 @@
|
||||||
import userAvatar from "./userAvatar"
|
import userAvatar from "./userAvatar"
|
||||||
import userInfo from "./userInfo"
|
import userInfo from "./userInfo"
|
||||||
import resetPwd from "./resetPwd"
|
import resetPwd from "./resetPwd"
|
||||||
|
import botConfig from "./botConfig"
|
||||||
import { getUserProfile } from "@/api/system/user"
|
import { getUserProfile } from "@/api/system/user"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Profile",
|
name: "Profile",
|
||||||
components: { userAvatar, userInfo, resetPwd },
|
components: { userAvatar, userInfo, resetPwd, botConfig },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: {},
|
user: {},
|
||||||
|
|
@ -86,8 +90,7 @@ export default {
|
||||||
getUser() {
|
getUser() {
|
||||||
getUserProfile().then(response => {
|
getUserProfile().then(response => {
|
||||||
this.user = response.data
|
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.FileUploadUtils;
|
||||||
import com.ruoyi.common.utils.file.MimeTypeUtils;
|
import com.ruoyi.common.utils.file.MimeTypeUtils;
|
||||||
import com.ruoyi.framework.shiro.service.SysPasswordService;
|
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 com.ruoyi.system.service.ISysUserService;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 个人信息 业务处理
|
* 个人信息 业务处理
|
||||||
*
|
*
|
||||||
|
|
@ -44,6 +49,9 @@ public class SysProfileController extends BaseController
|
||||||
@Autowired
|
@Autowired
|
||||||
private SysPasswordService passwordService;
|
private SysPasswordService passwordService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISysBotCredentialService botCredentialService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 个人信息
|
* 个人信息
|
||||||
*/
|
*/
|
||||||
|
|
@ -186,4 +194,33 @@ public class SysProfileController extends BaseController
|
||||||
return error(e.getMessage());
|
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 java.util.Map;
|
||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.apache.shiro.cache.ehcache.EhCacheManager;
|
import org.apache.shiro.cache.ehcache.EhCacheManager;
|
||||||
import org.apache.shiro.codec.Base64;
|
import org.apache.shiro.codec.Base64;
|
||||||
import org.apache.shiro.config.ConfigurationException;
|
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.LogoutFilter;
|
||||||
import com.ruoyi.framework.shiro.web.filter.captcha.CaptchaValidateFilter;
|
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.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.online.OnlineSessionFilter;
|
||||||
import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter;
|
import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter;
|
||||||
import com.ruoyi.framework.shiro.web.filter.user.CustomUserFilter;
|
import com.ruoyi.framework.shiro.web.filter.user.CustomUserFilter;
|
||||||
import com.ruoyi.framework.shiro.web.session.OnlineWebSessionManager;
|
import com.ruoyi.framework.shiro.web.session.OnlineWebSessionManager;
|
||||||
import com.ruoyi.framework.shiro.web.session.SpringSessionValidationScheduler;
|
import com.ruoyi.framework.shiro.web.session.SpringSessionValidationScheduler;
|
||||||
|
import com.ruoyi.system.service.ISysBotCredentialService;
|
||||||
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
|
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -298,6 +301,8 @@ public class ShiroConfig
|
||||||
permitAllUrl.forEach(url -> filterChainDefinitionMap.put(url, "anon"));
|
permitAllUrl.forEach(url -> filterChainDefinitionMap.put(url, "anon"));
|
||||||
}
|
}
|
||||||
// 退出 logout地址,shiro去清除session
|
// 退出 logout地址,shiro去清除session
|
||||||
|
filterChainDefinitionMap.put("/mcp", "mcpBot");
|
||||||
|
filterChainDefinitionMap.put("/mcp/**", "mcpBot");
|
||||||
filterChainDefinitionMap.put("/logout", "logout");
|
filterChainDefinitionMap.put("/logout", "logout");
|
||||||
// 不需要拦截的访问
|
// 不需要拦截的访问
|
||||||
filterChainDefinitionMap.put("/login", "anon,captchaValidate");
|
filterChainDefinitionMap.put("/login", "anon,captchaValidate");
|
||||||
|
|
@ -312,6 +317,7 @@ public class ShiroConfig
|
||||||
filters.put("captchaValidate", captchaValidateFilter());
|
filters.put("captchaValidate", captchaValidateFilter());
|
||||||
filters.put("kickout", kickoutSessionFilter());
|
filters.put("kickout", kickoutSessionFilter());
|
||||||
// 注销成功,则跳转到指定页面
|
// 注销成功,则跳转到指定页面
|
||||||
|
filters.put("mcpBot", mcpBotAuthFilter(securityManager, userRealm(getEhCacheManager())));
|
||||||
filters.put("logout", logoutFilter());
|
filters.put("logout", logoutFilter());
|
||||||
filters.put("user", customUserFilter());
|
filters.put("user", customUserFilter());
|
||||||
shiroFilterFactoryBean.setFilters(filters);
|
shiroFilterFactoryBean.setFilters(filters);
|
||||||
|
|
@ -413,6 +419,15 @@ public class ShiroConfig
|
||||||
return customUserFilter;
|
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框架的整合
|
* 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>
|
<groupId>com.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-system</artifactId>
|
<artifactId>ruoyi-system</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.ruoyi</groupId>
|
||||||
|
<artifactId>ruoyi-framework</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.thymeleaf</groupId>
|
<groupId>org.thymeleaf</groupId>
|
||||||
<artifactId>thymeleaf</artifactId>
|
<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 = "合同编号")
|
@Excel(name = "合同编号")
|
||||||
private String orderCode;
|
private String orderCode;
|
||||||
|
private List<String> orderCodeList;
|
||||||
private String orderId;
|
private String orderId;
|
||||||
@Excel(name = "项目名称")
|
@Excel(name = "项目名称")
|
||||||
private String projectName;
|
private String projectName;
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ public class ProjectOrderInfo extends BaseEntity {
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||||
// @Excel(name="下单时间", width = 30, dateFormat = "yyyy-MM-dd")
|
// @Excel(name="下单时间", width = 30, dateFormat = "yyyy-MM-dd")
|
||||||
private Date createTime;
|
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
|
* @date 2025/05/30 16:13
|
||||||
*/
|
*/
|
||||||
|
|
||||||
List< ProjectOrderInfo> selectProjectOrderInfoByProjectId(List<Long> projectId);
|
List< ProjectOrderInfo> selectProjectOrderInfoByProjectId(List<Long> projectId);
|
||||||
|
|
||||||
String exportList(ProjectOrderInfo projectOrderInfo);
|
String exportList(ProjectOrderInfo projectOrderInfo);
|
||||||
|
List<java.util.Map<String, Object>> listExportData(ProjectOrderInfo projectOrderInfo);
|
||||||
|
|
||||||
List<StatisticsDetailDto> listHomePageData(HomepageQueryDto dto);
|
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
|
@Override
|
||||||
public List<StatisticsDetailDto> listHomePageData(HomepageQueryDto dto) {
|
public List<StatisticsDetailDto> listHomePageData(HomepageQueryDto dto) {
|
||||||
SysUser sysUser = ShiroUtils.getSysUser();
|
SysUser sysUser = ShiroUtils.getSysUser();
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
|
||||||
</if>
|
</if>
|
||||||
<if test="orderCode != null and orderCode != ''">and t1.order_code = #{orderCode}</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="contactPerson != null and contactPerson != ''">and t1.contact_person = #{contactPerson}</if>
|
||||||
<if test="contactPhone != null and contactPhone != ''">and t1.contact_phone = #{contactPhone}</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>
|
<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
|
<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},'%'))
|
t2.project_name like concat('%',#{keyword},'%') or t2.customer_name like concat('%',#{keyword},'%'))
|
||||||
</if>
|
</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">
|
<if test="deliveryTimeStart != null or deliveryTimeEnd != null">
|
||||||
<choose>
|
<choose>
|
||||||
<when test="deliveryTimeStart != null and deliveryTimeEnd != null">
|
<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